Compare commits
302 Commits
feat-inlin
...
v0.23.1-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c7d547db1 | ||
|
|
638d596a3b | ||
|
|
d4976d4b63 | ||
|
|
a7bf4b3f36 | ||
|
|
94af2a29e1 | ||
|
|
42f816999e | ||
|
|
1414fbcc05 | ||
|
|
1947c58c60 | ||
|
|
1e20475061 | ||
|
|
af36282df5 | ||
|
|
322e46f15d | ||
|
|
983ea7f7a5 | ||
|
|
384e616231 | ||
|
|
6b8725f091 | ||
|
|
17797d83da | ||
|
|
ebd3680a47 | ||
|
|
74c5244be1 | ||
|
|
3cf0b83bda | ||
|
|
127813120e | ||
|
|
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 | ||
|
|
ad288f5168 | ||
|
|
c5d387cb53 | ||
|
|
2f4413be6e | ||
|
|
83d3ec73e7 | ||
|
|
cf8eda04a1 | ||
|
|
6bdb97c55c | ||
|
|
7a6c3d9db1 | ||
|
|
bfcc5504bb | ||
|
|
669a4d5652 | ||
|
|
b808305507 | ||
|
|
a04b190251 | ||
|
|
e869869462 | ||
|
|
20c0051026 | ||
|
|
284b0b8de0 | ||
|
|
130bdf8337 | ||
|
|
8b7b7881f5 | ||
|
|
28a8435a52 | ||
|
|
dca9871744 | ||
|
|
6c2fbbf275 | ||
|
|
43bac80e4d | ||
|
|
0bf6af17e7 | ||
|
|
f7af8a3863 | ||
|
|
492af7a92d | ||
|
|
de4f2b9990 | ||
|
|
4a2ff204ec | ||
|
|
26dbb29b3d | ||
|
|
231aae2920 | ||
|
|
4cc7380a88 | ||
|
|
593fd29d00 | ||
|
|
f84d97b17b | ||
|
|
ef4d743af3 | ||
|
|
9ecc4a15df | ||
|
|
e165025c94 | ||
|
|
d711f2aef3 | ||
|
|
e724bec987 | ||
|
|
40b3543c3f | ||
|
|
e95b5127ca | ||
|
|
5243aa0628 | ||
|
|
509d18501c | ||
|
|
77067bdc58 | ||
|
|
358b50ba21 | ||
|
|
b40ca44e1a | ||
|
|
a68d621f2d | ||
|
|
21ca38d9e9 | ||
|
|
769efc20d1 | ||
|
|
32e416ea95 | ||
|
|
49e0f4e983 | ||
|
|
e08b466166 | ||
|
|
3f9935bbcc | ||
|
|
ef8bc7c5a8 | ||
|
|
fa02cf0f2b | ||
|
|
bc66a27baf | ||
|
|
3e54ac3aca | ||
|
|
a425102cf3 | ||
|
|
60cd28005f | ||
|
|
728f82c084 | ||
|
|
4437835057 | ||
|
|
1cc405d2dc | ||
|
|
2f0d549a50 | ||
|
|
c7aca64ba1 | ||
|
|
548961f610 | ||
|
|
b3072ce354 | ||
|
|
98b6b1911c | ||
|
|
ef96109ea5 | ||
|
|
cf1a759fa5 | ||
|
|
86c3fc9fac | ||
|
|
5f12f06297 | ||
|
|
33b3f4e312 | ||
|
|
603ec3f10a | ||
|
|
37bb82e87d | ||
|
|
047e0b7e8d | ||
|
|
782820c34a | ||
|
|
26cf1f7a89 | ||
|
|
f7ab4f04ac | ||
|
|
2751f08bdc | ||
|
|
0904d448e0 | ||
|
|
fab7952af6 | ||
|
|
6af75d6d40 | ||
|
|
5f1a37f0db | ||
|
|
b7bd3051b1 | ||
|
|
7adc3fe19b | ||
|
|
4842885aa6 | ||
|
|
00e8c0d1b0 | ||
|
|
239efa5fbd | ||
|
|
a9ba23bae8 | ||
|
|
62930f2821 | ||
|
|
4334c71bc7 | ||
|
|
21a029f17e | ||
|
|
9d37c3bd45 | ||
|
|
343ec220b4 | ||
|
|
2da4c10384 | ||
|
|
829dfee9e5 | ||
|
|
26c7bcfdd5 | ||
|
|
cf1b05d16e | ||
|
|
cfa8b8042a | ||
|
|
f0c0985708 | ||
|
|
73c937bc30 | ||
|
|
33e67abbe9 | ||
|
|
c20fb723ef | ||
|
|
ccd142df97 | ||
|
|
f6dbd1c0b5 | ||
|
|
d38d185d1c | ||
|
|
ef79b72471 | ||
|
|
ed12ab16e0 | ||
|
|
24820cfcff | ||
|
|
e10f62663e | ||
|
|
02573b0ad2 | ||
|
|
0dc39434c2 | ||
|
|
33acfce083 | ||
|
|
79d0eadbd6 | ||
|
|
73f7f16298 | ||
|
|
66eb0e42fe | ||
|
|
052ae53b6e | ||
|
|
1c0ed3268b | ||
|
|
0456abb327 | ||
|
|
ec50458491 | ||
|
|
b834ccaa2f | ||
|
|
ffb3de6c36 | ||
|
|
454c8459f2 | ||
|
|
94a0d09591 | ||
|
|
9b7a6ed85d | ||
|
|
7e31035114 | ||
|
|
e15a6146f8 | ||
|
|
e49cb1126b | ||
|
|
142bc5720e | ||
|
|
feaeb7870f | ||
|
|
9df0eefe49 | ||
|
|
8e89a9377a | ||
|
|
d3df8fe7ef | ||
|
|
9feda988a5 | ||
|
|
9534d533e3 | ||
|
|
85eefe1d8b | ||
|
|
3343270680 | ||
|
|
bf9d502742 | ||
|
|
85c0779ac0 | ||
|
|
070de44069 | ||
|
|
33087e3a99 | ||
|
|
fe4eb5e771 | ||
|
|
2fead23556 | ||
|
|
bc5a9e4c06 | ||
|
|
fafad6c961 | ||
|
|
a4de409235 | ||
|
|
a05fd45959 | ||
|
|
24de2f8a96 | ||
|
|
eee37011a5 | ||
|
|
a67706bea0 | ||
|
|
faa69b6cfe | ||
|
|
ba5ea2deff | ||
|
|
a6b25a4877 | ||
|
|
90d8cb6526 | ||
|
|
e71faa988e | ||
|
|
ed0ae81aae | ||
|
|
a61b078dea | ||
|
|
85939306e3 | ||
|
|
cf2d9c2c1d |
16
.cargo-husky/hooks/pre-push
Executable file
16
.cargo-husky/hooks/pre-push
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
|
||||
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
|
||||
echo Please install it by running the following command:
|
||||
echo
|
||||
echo " cargo install --force cargo-make"
|
||||
echo
|
||||
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
|
||||
echo the following command instead of git push:
|
||||
echo
|
||||
echo " git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo make ci
|
||||
78
.cz.toml
Normal file
78
.cz.toml
Normal file
@@ -0,0 +1,78 @@
|
||||
# configuration for https://github.com/commitizen/cz-cli
|
||||
|
||||
[tool.commitizen]
|
||||
name = "cz_customize"
|
||||
tag_format = "$version"
|
||||
version_type = "semver"
|
||||
version_provider = "cargo"
|
||||
update_changelog_on_bump = true
|
||||
major_version_zero = true
|
||||
use_shortcuts = true
|
||||
|
||||
[tool.commitizen.customize]
|
||||
message_template = """{{change_type}}({{scope}}): {{subject}}
|
||||
|
||||
{% if body %}\
|
||||
{{body}}\
|
||||
{% endif %}
|
||||
|
||||
{%if is_breaking_change %}\
|
||||
BREAKING_CHANGE: \
|
||||
{% endif %}\
|
||||
{{footer}}\
|
||||
"""
|
||||
example = "feature: this feature enable customize through config file"
|
||||
schema = "<type>(<scope>): <subject>\n\n<body>\n\n<footer>"
|
||||
schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\\(\\w+\\):\\s(?P<subject>.*)(\\n\\n(?P<body>.*))?(\\n\\n(?P<footer>.*))?"
|
||||
|
||||
# The order needs to be preserved, as it influences the order when executing cz commit/cz c
|
||||
|
||||
# Change types
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "list"
|
||||
name = "change_type"
|
||||
choices = [
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
]
|
||||
message = "Select the type of change you are committing"
|
||||
|
||||
# The scope of the change, can be a file, class name or other context
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "scope"
|
||||
message = "What is the scope of this change? (class or file name): (press [enter] to skip)\n"
|
||||
|
||||
# Summary of the changes
|
||||
[[tool.commitizen.customize.questions]]
|
||||
"type" = "input"
|
||||
"name" = "subject"
|
||||
"message" = "Write a short and imperative summary of the code changes: (lower case and no period)\n"
|
||||
|
||||
# The commit body, elaborate the changes if need be.
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "body"
|
||||
message = "Provide additional contextual information about the code changes: (press [enter] to skip)\n"
|
||||
|
||||
# Specify if the changes are breaking
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "confirm"
|
||||
name = "is_breaking_change"
|
||||
message = "Is this a BREAKING CHANGE?"
|
||||
default = false
|
||||
|
||||
# Reference closing issues and share other
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "footer"
|
||||
message = "Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)"
|
||||
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
|
||||
8
.github/CODEOWNERS
vendored
Normal file
8
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# See https://help.github.com/articles/about-codeowners/
|
||||
# for more info about CODEOWNERS file
|
||||
|
||||
# It uses the same pattern rule for gitignore file
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,7 +7,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there, sorry `tui` is not working as expected.
|
||||
Hi there, sorry `ratatui` is not working as expected.
|
||||
Please fill this bug report conscientiously.
|
||||
A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
18
.github/pull_request_template.md
vendored
18
.github/pull_request_template.md
vendored
@@ -1,17 +1 @@
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what this PR changes.
|
||||
-->
|
||||
|
||||
## Testing guidelines
|
||||
<!--
|
||||
A clear and concise description of how the changes can be tested.
|
||||
For example, you can include a command to run the relevant tests or examples.
|
||||
You can also include screenshots of the expected behavior.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
|
||||
* [ ] I have added relevant tests.
|
||||
* [ ] I have documented all new additions.
|
||||
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
|
||||
|
||||
86
.github/workflows/cd.yml
vendored
Normal file
86
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
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-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
|
||||
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
|
||||
args: --token ${{ secrets.CARGO_TOKEN }}
|
||||
83
.github/workflows/check-pr.yml
vendored
Normal file
83
.github/workflows/check-pr.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Check Pull Requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- labeled
|
||||
- unlabeled
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
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
|
||||
id: check_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Add comment indicating we require pull request titles to follow conventional commits specification
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: always() && (steps.check_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for opening this pull request!
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
> ${{ steps.check_pr_title.outputs.error_message }}
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.check_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
check-breaking-change-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check breaking change label
|
||||
id: check_breaking_change
|
||||
run: |
|
||||
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
|
||||
# Check if pattern matches
|
||||
if echo "${{ github.event.pull_request.title }}" | grep -qE "$pattern"; then
|
||||
echo "breaking_change=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "breaking_change=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Add label
|
||||
if: steps.check_breaking_change.outputs.breaking_change == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['breaking change']
|
||||
})
|
||||
|
||||
do-not-merge:
|
||||
if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }}
|
||||
name: Prevent Merging
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "Pull request is labeled as 'do not merge'"
|
||||
echo "This workflow fails so that the pull request cannot be merged"
|
||||
exit 1
|
||||
|
||||
188
.github/workflows/ci.yml
vendored
188
.github/workflows/ci.yml
vendored
@@ -1,73 +1,157 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
name: CI
|
||||
# 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
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.35.6
|
||||
# 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:
|
||||
linux:
|
||||
name: Linux
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.52.1", "stable"]
|
||||
steps:
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME/.cargo/bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}/cargo-make
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
components: rustfmt
|
||||
- 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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- name: Install cargo-llvm-cov and cargo-make
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- 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
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: ["1.52.1", "stable"]
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust }}
|
||||
components: rustfmt,clippy
|
||||
- uses: actions/checkout@v1
|
||||
- name: "Get cargo bin directory"
|
||||
id: cargo-bin-dir
|
||||
run: echo "::set-output name=dir::$HOME\.cargo\bin"
|
||||
- name: "Cache cargo make"
|
||||
id: cache-cargo-make
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.cargo-bin-dir.outputs.dir }}\cargo-make.exe
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-make-${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
- 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
|
||||
|
||||
12
.markdownlint.yaml
Normal file
12
.markdownlint.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# configuration for https://github.com/DavidAnson/markdownlint
|
||||
|
||||
no-inline-html:
|
||||
allowed_elements:
|
||||
- img
|
||||
- details
|
||||
- summary
|
||||
line-length:
|
||||
line_length: 100
|
||||
|
||||
# to support repeated headers in the changelog
|
||||
no-duplicate-heading: false
|
||||
1237
CHANGELOG.md
1237
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
216
CONTRIBUTING.md
216
CONTRIBUTING.md
@@ -1,33 +1,215 @@
|
||||
# Contributing
|
||||
# Contribution guidelines
|
||||
|
||||
## Building
|
||||
First off, thank you for considering contributing to Ratatui.
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
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).
|
||||
|
||||
`tui` 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`.
|
||||
## Reporting issues
|
||||
|
||||
## :hammer_and_wrench: Pull requests
|
||||
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.
|
||||
|
||||
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.
|
||||
## 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.
|
||||
|
||||
### Keep PRs small, intentional and focused
|
||||
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
|
||||
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
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 `tui` is a test again 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.
|
||||
|
||||
168
Cargo.toml
168
Cargo.toml
@@ -1,95 +1,221 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.16.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
documentation = "https://docs.rs/tui/0.16.0/tui/"
|
||||
name = "ratatui"
|
||||
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/fdehau/tui-rs"
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
exclude = [
|
||||
"assets/*",
|
||||
".github",
|
||||
"Makefile.toml",
|
||||
"CONTRIBUTING.md",
|
||||
"*.log",
|
||||
"tags",
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
rust-version = "1.67.0"
|
||||
|
||||
[badges]
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1"
|
||||
bitflags = "1.3"
|
||||
#! 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"
|
||||
unicode-segmentation = "1.2"
|
||||
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"
|
||||
termion = { version = "1.5", optional = true }
|
||||
crossterm = { version = "0.22", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"]}
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
lru = "0.11.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.2"
|
||||
rand = "0.8"
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1"
|
||||
better-panic = "0.3.0"
|
||||
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"]
|
||||
#! 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"]
|
||||
|
||||
[[bench]]
|
||||
name = "barchart"
|
||||
harness = false
|
||||
|
||||
[[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"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "colors"
|
||||
required-features = ["crossterm"]
|
||||
# this example is a bit verbose, so we don't want to include it in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "colors_rgb"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "modifiers"
|
||||
required-features = ["crossterm"]
|
||||
# this example is a bit verbose, so we don't want to include it in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
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
|
||||
|
||||
177
Makefile.toml
177
Makefile.toml
@@ -1,130 +1,133 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
# Windows does not support building termion, so this avoids the build failure by providing two
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz
|
||||
# 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 = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"test-doc",
|
||||
]
|
||||
[tasks.lint-style]
|
||||
description = "Lint code style (formatting, typos, docs)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.ci-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"test-crossterm",
|
||||
"clippy-crossterm",
|
||||
"test-doc",
|
||||
]
|
||||
[tasks.lint-format]
|
||||
description = "Lint code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.fmt]
|
||||
[tasks.format]
|
||||
description = "Fix code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all"]
|
||||
|
||||
[tasks.lint-typos]
|
||||
description = "Run typo checks"
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.lint-docs]
|
||||
description = "Check documentation for errors and warnings"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"fmt",
|
||||
"--all",
|
||||
"rustdoc",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-Zunstable-options",
|
||||
"--check",
|
||||
"-Dwarnings",
|
||||
]
|
||||
|
||||
[tasks.check-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check"
|
||||
|
||||
[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-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build]
|
||||
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-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Run Clippy for linting"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
dependencies = ["test-doc"]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--lib",
|
||||
"--tests",
|
||||
"--examples",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--doc",
|
||||
"--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]
|
||||
@@ -132,21 +135,21 @@ private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"run",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}"
|
||||
"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
|
||||
|
||||
314
README.md
314
README.md
@@ -1,119 +1,259 @@
|
||||
# tui-rs
|
||||
# Ratatui
|
||||
|
||||
[](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
|
||||
[](https://crates.io/crates/tui)
|
||||
[](https://docs.rs/crate/tui/)
|
||||
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
|
||||
|
||||
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
`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.
|
||||
|
||||
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
[](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/ratatui-org/ratatui)
|
||||
[](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
[](https://matrix.to/#/#ratatui:matrix.org)
|
||||
|
||||
The library supports multiple backends:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
<!-- See RELEASE.md for instructions on creating the demo gif --->
|
||||

|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate
|
||||
buffers. This means that at each new frame you should build all widgets that are
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and
|
||||
interactive UI, this may introduce overhead for highly dynamic content. So, the
|
||||
implementation try to minimize the number of ansi escapes sequences generated to
|
||||
draw the updated UI. In practice, given the speed of `Rust` the overhead rather
|
||||
comes from the terminal emulator than the library itself.
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
Moreover, the library does not provide any input handling nor any event system and
|
||||
you may rely on the previously cited libraries to achieve such features.
|
||||
* [Ratatui](#ratatui)
|
||||
* [Installation](#installation)
|
||||
* [Introduction](#introduction)
|
||||
* [Quickstart](#quickstart)
|
||||
* [Status of this fork](#status-of-this-fork)
|
||||
* [Rust version requirements](#rust-version-requirements)
|
||||
* [Documentation](#documentation)
|
||||
* [Examples](#examples)
|
||||
* [Widgets](#widgets)
|
||||
* [Built in](#built-in)
|
||||
* [Third\-party libraries, bootstrapping templates and
|
||||
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
* [Apps](#apps)
|
||||
* [Alternatives](#alternatives)
|
||||
* [Contributors](#contributors)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
* [License](#license)
|
||||
|
||||
### Rust version requirements
|
||||
</details>
|
||||
|
||||
Since version 0.17.0, `tui` requires **rustc version 1.52.1 or greater**.
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
|
||||
### Demo
|
||||
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
cargo add ratatui --features all-widgets
|
||||
```
|
||||
|
||||
Or modify your `Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ratatui = { version = "0.23.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
`ratatui` is a terminal UI library that supports multiple backends:
|
||||
|
||||
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
* [termion](https://github.com/ticki/termion)
|
||||
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that at each new frame you should build all widgets that are supposed to be part of the UI. While
|
||||
providing a great flexibility for rich and interactive UI, this may introduce overhead for highly
|
||||
dynamic content. So, the implementation try to minimize the number of ansi escapes sequences
|
||||
generated to draw the updated UI. In practice, given the speed of `Rust` the overhead rather comes
|
||||
from the terminal emulator than the library itself.
|
||||
|
||||
Moreover, the library does not provide any input handling nor any event system and you may rely on
|
||||
the previously cited libraries to achieve such features.
|
||||
|
||||
We keep a [CHANGELOG](./CHANGELOG.md) generated by [git-cliff](https://github.com/orhun/git-cliff)
|
||||
utilizing [Conventional Commits](https://www.conventionalcommits.org/).
|
||||
|
||||
## Quickstart
|
||||
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
[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/ratatui-org/rust-tui-template).
|
||||
|
||||
```rust
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
run(&mut terminal)?;
|
||||
restore_terminal(&mut terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
||||
}
|
||||
|
||||
fn restore_terminal(
|
||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
|
||||
Ok(terminal.show_cursor()?)
|
||||
}
|
||||
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
|
||||
Ok(loop {
|
||||
terminal.draw(|frame| {
|
||||
let greeting = Paragraph::new("Hello World!");
|
||||
frame.render_widget(greeting, frame.size());
|
||||
})?;
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if KeyCode::Char('q') == key.code {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Status of this fork
|
||||
|
||||
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
|
||||
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
|
||||
the community forked the project and created this crate. We look forward to continuing the work
|
||||
started by Florian 🚀
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
|
||||
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing.
|
||||
Our primary open-source workflow is centered around GitHub.
|
||||
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
|
||||
|
||||
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.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
|
||||
|
||||
## Examples
|
||||
|
||||
The demo shown in the gif above is available on all available backends.
|
||||
|
||||
```shell
|
||||
# crossterm
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
cargo run --example demo
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
cargo run --example demo --no-default-features --features=termion
|
||||
# termwiz
|
||||
cargo run --example demo --no-default-features --features=termwiz
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
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).
|
||||
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/app.rs).
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
|
||||
want to run the demo without those symbols:
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
|
||||
the demo without those symbols:
|
||||
|
||||
```
|
||||
```shell
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
### Widgets
|
||||
More examples are available in the [examples](./examples/) folder.
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
## Widgets
|
||||
|
||||
* [Block](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/block.rs)
|
||||
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/gauge.rs)
|
||||
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/sparkline.rs)
|
||||
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/chart.rs)
|
||||
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/barchart.rs)
|
||||
* [List](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/list.rs)
|
||||
* [Table](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/table.rs)
|
||||
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/canvas.rs)
|
||||
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/tabs.rs)
|
||||
### Built in
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
### Third-party widgets
|
||||
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`.
|
||||
|
||||
* [tui-logger](https://github.com/gin66/tui-logger)
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Apps using tui
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
|
||||
* [bandwhich](https://github.com/imsnif/bandwhich)
|
||||
* [kmon](https://github.com/orhun/kmon)
|
||||
* [gpg-tui](https://github.com/orhun/gpg-tui)
|
||||
* [ytop](https://github.com/cjbassi/ytop)
|
||||
* [zenith](https://github.com/bvaisvil/zenith)
|
||||
* [bottom](https://github.com/ClementTsang/bottom)
|
||||
* [oha](https://github.com/hatoo/oha)
|
||||
* [gitui](https://github.com/extrawurst/gitui)
|
||||
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
|
||||
* [desed](https://github.com/SoptikHa2/desed)
|
||||
* [diskonaut](https://github.com/imsnif/diskonaut)
|
||||
* [tickrs](https://github.com/tarkah/tickrs)
|
||||
* [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager)
|
||||
* [termchat](https://github.com/lemunozm/termchat)
|
||||
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
|
||||
* [gping](https://github.com/orf/gping/)
|
||||
* [Vector](https://vector.dev)
|
||||
* [KDash](https://github.com/kdash-rs/kdash)
|
||||
* [xplr](https://github.com/sayanarijit/xplr)
|
||||
* [minesweep](https://github.com/cpcloud/minesweep-rs)
|
||||
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
|
||||
* [termscp](https://github.com/veeso/termscp)
|
||||
* [joshuto](https://github.com/kamiyaa/joshuto)
|
||||
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
|
||||
* [hoard](https://github.com/Hyde46/hoard)
|
||||
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`ratatui::text::Text`
|
||||
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`ratatui::style::Color`
|
||||
* [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
|
||||
Tui-rs + Crossterm apps
|
||||
* [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
* [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
* [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
* [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
|
||||
with a React/Elm inspired approach
|
||||
* [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
|
||||
Tui-realm
|
||||
* [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
|
||||
structures.
|
||||
* [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
|
||||
windows and their rendering
|
||||
* [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
|
||||
widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
* [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
|
||||
backends and tui-rs.
|
||||
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
|
||||
that enables the rendering of terminal applications as ratatui widgets.
|
||||
|
||||
### Alternatives
|
||||
## Apps
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
|
||||
alternative solution to build text user interfaces in Rust.
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Contributors
|
||||
|
||||
[](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 ratatui-org organization.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
[MIT](./LICENSE)
|
||||
|
||||
25
RELEASE.md
Normal file
25
RELEASE.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Creating a Release
|
||||
|
||||
[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 if necessary. The preferred tool for this is
|
||||
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
|
||||
|
||||
```shell
|
||||
cargo build --example demo
|
||||
vhs examples/demo.tape --publish --quiet
|
||||
```
|
||||
|
||||
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/ratatui-org/ratatui/actions) workflow to
|
||||
finish.
|
||||
BIN
assets/demo.gif
BIN
assets/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
100
bacon.toml
Normal file
100
bacon.toml
Normal file
@@ -0,0 +1,100 @@
|
||||
# This is a configuration file for the bacon tool
|
||||
#
|
||||
# Bacon repository: https://github.com/Canop/bacon
|
||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||
# You can also check bacon's own bacon.toml file
|
||||
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
|
||||
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
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",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[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.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
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);
|
||||
97
benches/paragraph.rs
Normal file
97
benches/paragraph.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use criterion::{
|
||||
black_box, criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{Paragraph, Widget, Wrap},
|
||||
};
|
||||
|
||||
/// because the scroll offset is a u16, the maximum number of lines that can be scrolled is 65535.
|
||||
/// This is a limitation of the current implementation and may be fixed by changing the type of the
|
||||
/// scroll offset to a u32.
|
||||
const MAX_SCROLL_OFFSET: u16 = u16::MAX;
|
||||
const NO_WRAP_WIDTH: u16 = 200;
|
||||
const WRAP_WIDTH: u16 = 100;
|
||||
|
||||
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
|
||||
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
|
||||
/// as well as comparing with the various settings on the scroll and wrap features.
|
||||
pub fn paragraph(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("paragraph");
|
||||
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
|
||||
let lines = random_lines(line_count);
|
||||
let lines = lines.as_str();
|
||||
|
||||
// benchmark that measures the overhead of creating a paragraph separately from rendering
|
||||
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
|
||||
b.iter(|| Paragraph::new(black_box(lines)))
|
||||
});
|
||||
|
||||
// render the paragraph with no scroll
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render", line_count),
|
||||
&Paragraph::new(lines),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by half the number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_half", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_full", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// render the paragraph wrapped to 100 characters
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_wrap", line_count),
|
||||
&Paragraph::new(lines).wrap(Wrap { trim: false }),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render wrapped to 100 characters
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_wrap_scroll_full", line_count),
|
||||
&Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// 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));
|
||||
// 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
|
||||
///
|
||||
/// English language has about 5.1 average characters per word so including the space between words
|
||||
/// this should emit around 200 characters per paragraph on average.
|
||||
fn random_lines(count: u16) -> String {
|
||||
let count = count as i64;
|
||||
let sentence_count = 3;
|
||||
let word_count = 11;
|
||||
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
|
||||
}
|
||||
|
||||
criterion_group!(benches, paragraph);
|
||||
criterion_main!(benches);
|
||||
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);
|
||||
105
cliff.toml
Normal file
105
cliff.toml
Normal file
@@ -0,0 +1,105 @@
|
||||
# configuration for https://github.com/orhun/git-cliff
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
"""
|
||||
# template for the changelog body
|
||||
# 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 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") | lower }})* {{ 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") %}
|
||||
{{ 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 = false
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
|
||||
{ pattern = '(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 = "^[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
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
3
codecov.yml
Normal file
3
codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
30
committed.toml
Normal file
30
committed.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# configuration for https://github.com/crate-ci/committed
|
||||
|
||||
# https://www.conventionalcommits.org
|
||||
style = "conventional"
|
||||
# disallow merge commits
|
||||
merge_commit = false
|
||||
# subject is not required to be capitalized
|
||||
subject_capitalized = false
|
||||
# subject should start with an imperative verb
|
||||
imperative_subject = true
|
||||
# subject should not end with a punctuation
|
||||
subject_not_punctuated = true
|
||||
# disable line length
|
||||
line_length = 0
|
||||
# disable subject length
|
||||
subject_length = 0
|
||||
# default allowed_types [ "chore", "docs", "feat", "fix", "perf", "refactor", "style", "test" ]
|
||||
allowed_types = [
|
||||
"build",
|
||||
"chore",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"revert",
|
||||
"style",
|
||||
"test",
|
||||
]
|
||||
28
deny.toml
Normal file
28
deny.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
default = "deny"
|
||||
unlicensed = "deny"
|
||||
copyleft = "deny"
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
317
examples/README.md
Normal file
317
examples/README.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# 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]
|
||||
|
||||
## Colors (RGB)
|
||||
|
||||
Demonstrates the available RGB
|
||||
[`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) options. These can be used
|
||||
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=colors_rgb --features=crossterm
|
||||
```
|
||||
|
||||
![Colors RGB][colors_rgb.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
|
||||
[colors_rgb.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.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
|
||||
@@ -1,25 +1,30 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Frame, Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
label: &'a str,
|
||||
bar_style: Style,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
data: Vec<(&'a str, u64)>,
|
||||
months: [&'a str; 4],
|
||||
companies: [Company<'a>; 3],
|
||||
}
|
||||
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
@@ -49,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"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +104,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -116,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)
|
||||
@@ -132,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,119 +1,253 @@
|
||||
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 std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
Frame, Terminal,
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
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("Main block with round corners")
|
||||
.title_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![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title_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);
|
||||
f.render_widget(block, 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);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
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
|
||||
277
examples/calendar.rs
Normal file
277
examples/calendar.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use std::{error::Error, io, rc::Rc};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::calendar::*};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
let _ = terminal.draw(|f| draw(f));
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
#[allow(clippy::single_match)]
|
||||
match key.code {
|
||||
KeyCode::Char(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(f: &mut Frame<B>) {
|
||||
let app_area = f.size();
|
||||
|
||||
let calarea = Rect {
|
||||
x: app_area.x + 1,
|
||||
y: app_area.y + 1,
|
||||
height: app_area.height - 1,
|
||||
width: app_area.width - 1,
|
||||
};
|
||||
|
||||
let mut start = OffsetDateTime::now_local()
|
||||
.unwrap()
|
||||
.date()
|
||||
.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_day(1)
|
||||
.unwrap();
|
||||
|
||||
let list = make_dates(start.year());
|
||||
|
||||
for chunk in split_rows(&calarea)
|
||||
.iter()
|
||||
.flat_map(|row| split_cols(row).to_vec())
|
||||
{
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
f.render_widget(cal, chunk);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn split_rows(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn split_cols(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
// Holidays
|
||||
let holiday_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
// new year's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// groundhog day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// april fool's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// earth day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// star wars day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// festivus
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// new year's eve
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
|
||||
// seasons
|
||||
let season_style = Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
// spring equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// summer solstice
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// fall equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list
|
||||
}
|
||||
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
June => example2(m, y, es),
|
||||
July => example3(m, y, es),
|
||||
December => example3(m, y, es),
|
||||
February => example4(m, y, es),
|
||||
November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(Style::default())
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(default_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
.fg(Color::LightYellow);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -1,23 +1,17 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Frame, Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -29,6 +23,8 @@ struct App {
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
tick_count: u64,
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -48,10 +44,22 @@ impl App {
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
tick_count: 0,
|
||||
marker: Marker::Dot,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 4) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Block,
|
||||
Marker::Block => Marker::Bar,
|
||||
Marker::Bar => Marker::Braille,
|
||||
Marker::Braille => Marker::Dot,
|
||||
};
|
||||
}
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
@@ -100,7 +108,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -155,22 +163,20 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(
|
||||
app.x,
|
||||
-app.y,
|
||||
Span::styled("You are here", Style::default().fg(Color::Yellow)),
|
||||
);
|
||||
ctx.print(app.x, -app.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
|
||||
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
|
||||
@@ -1,22 +1,15 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Frame, Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
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] = [
|
||||
@@ -117,7 +110,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -189,12 +182,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 1",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title("Chart 1".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
@@ -208,11 +196,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
])
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
@@ -226,12 +210,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 2",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title("Chart 2".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
@@ -239,22 +218,14 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
@@ -267,12 +238,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 3",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title("Chart 3".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
@@ -280,22 +246,14 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("25"),
|
||||
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
}
|
||||
|
||||
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
|
||||
160
examples/colors_rgb.rs
Normal file
160
examples/colors_rgb.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
/// This example shows the full range of RGB colors that can be displayed in the terminal.
|
||||
///
|
||||
/// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
install_panic_hook();
|
||||
App::new()?.run()
|
||||
}
|
||||
|
||||
struct App {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
should_quit: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
|
||||
should_quit: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(mut self) -> Result<()> {
|
||||
init_terminal()?;
|
||||
self.terminal.clear()?;
|
||||
while !self.should_quit {
|
||||
self.draw()?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.terminal.draw(|frame| {
|
||||
frame.render_widget(RgbColors, frame.size());
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_quit = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
let _ = restore_terminal();
|
||||
}
|
||||
}
|
||||
|
||||
struct RgbColors;
|
||||
|
||||
impl Widget for RgbColors {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Self::layout(area);
|
||||
let rgb_colors = Self::create_rgb_color_grid(area.width, area.height * 2);
|
||||
Self::render_title(layout[0], buf);
|
||||
Self::render_colors(layout[1], buf, rgb_colors);
|
||||
}
|
||||
}
|
||||
|
||||
impl RgbColors {
|
||||
fn layout(area: Rect) -> Rc<[Rect]> {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area)
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("colors_rgb example. Press q to quit")
|
||||
.dark_gray()
|
||||
.alignment(Alignment::Center)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
|
||||
fn render_colors(area: Rect, buf: &mut Buffer, rgb_colors: Vec<Vec<Color>>) {
|
||||
for (x, column) in (area.left()..area.right()).zip(rgb_colors.iter()) {
|
||||
for (y, (fg, bg)) in (area.top()..area.bottom()).zip(column.iter().tuples()) {
|
||||
let cell = buf.get_mut(x, y);
|
||||
cell.fg = *fg;
|
||||
cell.bg = *bg;
|
||||
cell.symbol = "▀".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a smooth grid of colors
|
||||
///
|
||||
/// Red ranges from 0 to 255 across the x axis. Green ranges from 0 to 255 across the y axis.
|
||||
/// Blue repeats every 32 pixels in both directions, but flipped every 16 pixels so that it
|
||||
/// doesn't transition sharply from light to dark.
|
||||
///
|
||||
/// The result stored in a 2d vector of colors with the x axis as the first dimension, and the
|
||||
/// y axis the second dimension.
|
||||
fn create_rgb_color_grid(width: u16, height: u16) -> Vec<Vec<Color>> {
|
||||
let mut result = vec![];
|
||||
for x in 0..width {
|
||||
let mut column = vec![];
|
||||
for y in 0..height {
|
||||
// flip both axes every 16 pixels. E.g. [0, 1, ... 15, 15, ... 1, 0]
|
||||
let yy = if (y % 32) < 16 { y % 32 } else { 31 - y % 32 };
|
||||
let xx = if (x % 32) < 16 { x % 32 } else { 31 - x % 32 };
|
||||
let r = (256 * x / width) as u8;
|
||||
let g = (256 * y / height) as u8;
|
||||
let b = (yy * 16 + xx) as u8;
|
||||
column.push(Color::Rgb(r, g, b))
|
||||
}
|
||||
result.push(column);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before panicking.
|
||||
fn install_panic_hook() {
|
||||
better_panic::install();
|
||||
let prev_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
prev_hook(info);
|
||||
}));
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
18
examples/colors_rgb.tape
Normal file
18
examples/colors_rgb.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_rgb.tape`
|
||||
Output "target/colors_rgb.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_rgb --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
@@ -1,17 +1,11 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
@@ -52,7 +46,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
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 tui::widgets::ListState;
|
||||
use ratatui::widgets::*;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use crate::{app::App, ui};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -36,7 +35,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -56,13 +55,15 @@ fn run_app<B: Backend>(
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use argh::FromArgs;
|
||||
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
mod ui;
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
use crate::crossterm::run;
|
||||
#[cfg(feature = "termion")]
|
||||
use crate::termion::run;
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, time::Duration};
|
||||
mod ui;
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
@@ -26,6 +26,11 @@ struct Cli {
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "crossterm")]
|
||||
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "termion")]
|
||||
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "termwiz")]
|
||||
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
use crate::{app::App, ui};
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::AlternateScreen,
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
screen::IntoAlternateScreen,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let stdout = io::stdout()
|
||||
.into_raw_mode()
|
||||
.unwrap()
|
||||
.into_alternate_screen()
|
||||
.unwrap();
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let stdout = AlternateScreen::from(stdout);
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
@@ -64,14 +66,14 @@ fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
let stdin = io::stdin();
|
||||
for key in stdin.keys().flatten() {
|
||||
if let Err(err) = keys_tx.send(Event::Input(key)) {
|
||||
eprintln!("{}", err);
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{}", err);
|
||||
eprintln!("{err}");
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
|
||||
76
examples/demo/termwiz.rs
Normal file
76
examples/demo/termwiz.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
let backend = TermwizBackend::new()?;
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termwiz Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
terminal.show_cursor()?;
|
||||
terminal.flush()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app(
|
||||
terminal: &mut Terminal<TermwizBackend>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if let Ok(Some(input)) = terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.terminal()
|
||||
.poll_input(Some(timeout))
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
KeyCode::UpArrow => app.on_up(),
|
||||
KeyCode::DownArrow => app.on_down(),
|
||||
KeyCode::LeftArrow => app.on_left(),
|
||||
KeyCode::RightArrow => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
},
|
||||
InputEvent::Resized { cols, rows } => {
|
||||
terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.resize(cols, rows);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
use crate::app::App;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
widgets::{
|
||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
@@ -21,7 +13,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| Spans::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))
|
||||
@@ -82,6 +74,7 @@ where
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
)
|
||||
.use_unicode(app.enhanced_graphics)
|
||||
.label(label)
|
||||
.ratio(app.progress);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
@@ -137,7 +130,7 @@ where
|
||||
.tasks
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| ListItem::new(vec![Spans::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"))
|
||||
@@ -161,8 +154,8 @@ where
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
};
|
||||
let content = vec![Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
let content = vec![text::Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
Span::raw(evt),
|
||||
])];
|
||||
ListItem::new(content)
|
||||
@@ -261,9 +254,9 @@ where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
Spans::from(""),
|
||||
Spans::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(" "),
|
||||
@@ -272,7 +265,7 @@ where
|
||||
Span::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Span::raw("."),
|
||||
]),
|
||||
Spans::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 "),
|
||||
@@ -283,7 +276,7 @@ where
|
||||
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw(".")
|
||||
]),
|
||||
Spans::from(
|
||||
text::Line::from(
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
@@ -346,9 +339,15 @@ where
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Circle {
|
||||
x: app.servers[2].coords.1,
|
||||
y: app.servers[2].coords.0,
|
||||
radius: 10.0,
|
||||
color: Color::Green,
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&Line {
|
||||
ctx.draw(&canvas::Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
@@ -411,7 +410,7 @@ where
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let cells = vec![
|
||||
Cell::from(Span::raw(format!("{:?}: ", c))),
|
||||
Cell::from(Span::raw(format!("{c:?}: "))),
|
||||
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
|
||||
Cell::from(Span::styled("Background", Style::default().bg(*c))),
|
||||
];
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Frame, Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
progress1: u16,
|
||||
@@ -77,7 +71,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -112,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),
|
||||
@@ -152,9 +145,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
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
|
||||
80
examples/hello_world.rs
Normal file
80
examples/hello_world.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::{
|
||||
io::{self, Stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
/// teardown of a terminal application.
|
||||
///
|
||||
/// A more robust application would probably want to handle errors and ensure that the terminal is
|
||||
/// restored to a sane state before exiting. This example does not do that. It also does not handle
|
||||
/// events or update the application state. It just draws a greeting and exits when the user
|
||||
/// presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal().context("setup failed")?;
|
||||
run(&mut terminal).context("app loop failed")?;
|
||||
restore_terminal(&mut terminal).context("restore terminal failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
|
||||
/// hide the cursor. This example does not handle errors. A more robust application would probably
|
||||
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode().context("failed to enable raw mode")?;
|
||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
||||
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
||||
}
|
||||
|
||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
||||
/// the cursor.
|
||||
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode().context("failed to disable raw mode")?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
||||
.context("unable to switch to main screen")?;
|
||||
terminal.show_cursor().context("unable to show cursor")
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||
/// possible, for example, you could have multiple application states and switch between them based
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(crate::render_app)?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the application. This is where you would draw the application UI. This example just
|
||||
/// draws a greeting.
|
||||
fn render_app(frame: &mut ratatui::Frame<CrosstermBackend<Stdout>>) {
|
||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.size());
|
||||
}
|
||||
|
||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
||||
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
|
||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
return Ok(KeyCode::Char('q') == key.code);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
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
|
||||
@@ -1,4 +1,3 @@
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
@@ -7,16 +6,9 @@ use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{event, span, Level};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, ViewportVariant,
|
||||
};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
@@ -72,18 +64,13 @@ struct Worker {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_writer(io::stderr)
|
||||
.init();
|
||||
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: ViewportVariant::Inline(8),
|
||||
viewport: Viewport::Inline(8),
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -181,8 +168,6 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
redraw = true;
|
||||
|
||||
let span = span!(Level::INFO, "recv");
|
||||
let _guard = span.enter();
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => {
|
||||
if event.code == crossterm::event::KeyCode::Char('q') {
|
||||
@@ -190,32 +175,21 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
Event::Resize => {
|
||||
event!(Level::INFO, "resize");
|
||||
terminal.resize()?;
|
||||
terminal.autoresize()?;
|
||||
}
|
||||
Event::Tick => {
|
||||
event!(Level::INFO, "tick");
|
||||
}
|
||||
Event::DownloadUpdate(worker_id, download_id, progress) => {
|
||||
event!(
|
||||
Level::INFO,
|
||||
worker_id,
|
||||
download_id,
|
||||
progress,
|
||||
"download update"
|
||||
);
|
||||
Event::Tick => {}
|
||||
Event::DownloadUpdate(worker_id, _download_id, progress) => {
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
event!(Level::INFO, worker_id, download_id, "download done");
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(Spans::from(vec![
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::from("Finished "),
|
||||
Span::styled(
|
||||
format!("download {}", download_id),
|
||||
format!("download {download_id}"),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::from(format!(
|
||||
@@ -245,9 +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("Progress")
|
||||
.title_alignment(Alignment::Center);
|
||||
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
@@ -259,7 +231,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{}/{}", done, NUM_DOWNLOADS))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, chunks[0]);
|
||||
|
||||
@@ -271,9 +243,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
// in progress downloads
|
||||
let items: Vec<ListItem> = downloads
|
||||
.in_progress
|
||||
.iter()
|
||||
.map(|(_worker_id, download)| {
|
||||
ListItem::new(Spans::from(vec![
|
||||
.values()
|
||||
.map(|download| {
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(symbols::DOT),
|
||||
Span::styled(
|
||||
format!(" download {:>2}", download.id),
|
||||
|
||||
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
|
||||
@@ -1,15 +1,12 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
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
|
||||
@@ -32,7 +29,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -50,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
|
||||
@@ -1,21 +1,15 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
@@ -63,9 +57,9 @@ impl<T> StatefulList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
|
||||
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
|
||||
/// and have access to features such as natural scrolling.
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
@@ -166,7 +160,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -186,12 +180,14 @@ fn run_app<B: Backend>(
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,12 +211,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Spans::from(i.0)];
|
||||
let mut lines = vec![Line::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(Spans::from(Span::styled(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
lines.push(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
.italic()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
@@ -255,16 +252,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"2020-01-01 10:00:00",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
let header = Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
" ".into(),
|
||||
"2020-01-01 10:00:00".italic(),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
let log = Line::from(vec![event.into()]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
@@ -272,9 +266,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
Line::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Spans::from(""),
|
||||
Line::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
|
||||
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
|
||||
134
examples/panic.rs
Normal file
134
examples/panic.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
||||
//! the terminal.
|
||||
//!
|
||||
//! When exiting normally or when handling `Result::Err`, we can reset the
|
||||
//! terminal manually at the end of `main` just before we print the error.
|
||||
//!
|
||||
//! Because a panic interrupts the normal control flow, manually resetting the
|
||||
//! terminal at the end of `main` won't do us any good. Instead, we need to
|
||||
//! make sure to set up a panic hook that first resets the terminal before
|
||||
//! handling the panic. This both reuses the standard panic hook to ensure a
|
||||
//! consistent panic handling UX and properly resets the terminal to not
|
||||
//! distort the output.
|
||||
//!
|
||||
//! That's why this example is set up to show both situations, with and without
|
||||
//! the chained panic hook, to see the difference.
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn chain_hook(&mut self) {
|
||||
let original_hook = std::panic::take_hook();
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic| {
|
||||
reset_terminal().unwrap();
|
||||
original_hook(panic);
|
||||
}));
|
||||
|
||||
self.hook_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
|
||||
let mut app = App::default();
|
||||
let res = run_tui(&mut terminal, &mut app);
|
||||
|
||||
reset_terminal()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the terminal.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Resets the terminal.
|
||||
fn reset_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the TUI loop.
|
||||
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => {
|
||||
panic!("intentional demo panic");
|
||||
}
|
||||
|
||||
KeyCode::Char('e') => {
|
||||
app.chain_hook();
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the TUI.
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let text = vec![
|
||||
if app.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("press `p` to panic"),
|
||||
Line::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Line::from("press any other key to quit without panic"),
|
||||
Line::from(""),
|
||||
Line::from("when you panic without the chained hook,"),
|
||||
Line::from("you will likely have to reset your terminal afterwards"),
|
||||
Line::from("with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("with the chained panic hook enabled,"),
|
||||
Line::from("you should see the panic report as you would without ratatui"),
|
||||
Line::from(""),
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
}
|
||||
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
|
||||
@@ -1,21 +1,15 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
@@ -55,7 +49,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -95,12 +89,11 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
@@ -113,59 +106,54 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line ",
|
||||
Style::default().fg(Color::Red),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default().bg(Color::Blue),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
"This is a longer line",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_blue()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.on_green()),
|
||||
Line::from("This is a line".green().italic()),
|
||||
Line::from(vec![
|
||||
"Masked text: ".into(),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, no wrap"))
|
||||
.alignment(Alignment::Left);
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Center, wrap"))
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,18 +1,11 @@
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
@@ -46,7 +39,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -57,10 +50,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,18 +73,15 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(Span::styled(
|
||||
text,
|
||||
Style::default().add_modifier(Modifier::SLOW_BLINK),
|
||||
))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::Blue));
|
||||
.on_blue();
|
||||
f.render_widget(block, chunks[1]);
|
||||
|
||||
if app.show_popup {
|
||||
|
||||
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
|
||||
240
examples/scrollbar.rs
Normal file
240
examples/scrollbar.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
pub vertical_scroll_state: ScrollbarState,
|
||||
pub horizontal_scroll_state: ScrollbarState,
|
||||
pub vertical_scroll: usize,
|
||||
pub horizontal_scroll: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
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);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
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);
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.clone()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.clone()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
||||
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.gray()
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let title = Block::default()
|
||||
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block("Vertical scrollbar with arrows"))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(Some("↑"))
|
||||
.end_symbol(Some("↓")),
|
||||
chunks[1],
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Vertical scrollbar without arrows, without track symbol and mirrored",
|
||||
))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 0,
|
||||
}),
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar without arrows & custom thumb and track symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[4]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
}
|
||||
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
|
||||
@@ -1,3 +1,9 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
@@ -7,18 +13,7 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
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 {
|
||||
@@ -99,7 +94,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -134,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
|
||||
@@ -1,16 +1,11 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
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,
|
||||
@@ -95,7 +90,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -106,11 +101,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,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);
|
||||
@@ -148,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
|
||||
@@ -1,17 +1,11 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
@@ -61,7 +55,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -72,11 +66,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,21 +82,17 @@ 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);
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
let block = Block::default().on_white().black();
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Spans::from(vec![
|
||||
Span::styled(first, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(rest, Style::default().fg(Color::Green)),
|
||||
])
|
||||
Line::from(vec![first.yellow(), rest.green()])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
|
||||
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
|
||||
@@ -1,29 +1,25 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
/// A simple example demonstrating how to handle user input. This is
|
||||
/// a bit out of the scope of the library as it does not provide any
|
||||
/// input handling out of the box. However, it may helps some to get
|
||||
/// started.
|
||||
///
|
||||
/// This is a very simple example:
|
||||
/// * 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},
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
enum InputMode {
|
||||
Normal,
|
||||
@@ -34,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
|
||||
@@ -46,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()?;
|
||||
@@ -72,7 +125,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -93,21 +146,26 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Enter => app.submit_message(),
|
||||
KeyCode::Char(to_insert) => {
|
||||
app.enter_char(to_insert);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
app.delete_char();
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.move_cursor_left();
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.move_cursor_right();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,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),
|
||||
@@ -130,31 +187,31 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit, "),
|
||||
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to start editing."),
|
||||
"Press ".into(),
|
||||
"q".bold(),
|
||||
" to exit, ".into(),
|
||||
"e".bold(),
|
||||
" to start editing.".bold(),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to stop editing, "),
|
||||
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to record the message"),
|
||||
"Press ".into(),
|
||||
"Esc".bold(),
|
||||
" to stop editing, ".into(),
|
||||
"Enter".bold(),
|
||||
" to record the message".into(),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Spans::from(msg));
|
||||
let mut text = Text::from(Line::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
let input = Paragraph::new(app.input.as_ref())
|
||||
let input = Paragraph::new(app.input.as_str())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
@@ -167,10 +224,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
||||
// 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,
|
||||
)
|
||||
@@ -182,7 +241,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
|
||||
let content = Line::from(Span::raw(format!("{i}: {m}")));
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
|
||||
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"
|
||||
5
rustfmt.toml
Normal file
5
rustfmt.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# configuration for https://rust-lang.github.io/rustfmt/
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
336
src/backend.rs
Normal file
336
src/backend.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the backend implementations for different terminal libraries.
|
||||
//!
|
||||
//! It defines the [`Backend`] trait which is used to abstract over the specific terminal library
|
||||
//! being used.
|
||||
//!
|
||||
//! Supported terminal backends:
|
||||
//! - [Crossterm]: enable the `crossterm` feature (enabled by default) and use [`CrosstermBackend`]
|
||||
//! - [Termion]: enable the `termion` feature and use [`TermionBackend`]
|
||||
//! - [Termwiz]: enable the `termwiz` feature and use [`TermwizBackend`]
|
||||
//!
|
||||
//! Additionally, a [`TestBackend`] is provided for testing purposes.
|
||||
//!
|
||||
//! See the [Backend Comparison] section of the [Ratatui Book] for more details on the different
|
||||
//! backends.
|
||||
//!
|
||||
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
|
||||
//! screen](#alternate-screen), and [mouse capture](#mouse-capture). These features are generally
|
||||
//! not enabled by default, and must be enabled by the application before they can be used. See the
|
||||
//! documentation for each backend for more details.
|
||||
//!
|
||||
//! Note: most applications should use the [`Terminal`] struct instead of directly calling methods
|
||||
//! on the backend.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.clear()?;
|
||||
//! terminal.draw(|frame| {
|
||||
//! // -- snip --
|
||||
//! })?;
|
||||
//! # std::io::Result::Ok(())
|
||||
//! ```
|
||||
//!
|
||||
//! See the the [examples] directory for more examples.
|
||||
//!
|
||||
//! # Raw Mode
|
||||
//!
|
||||
//! Raw mode is a mode where the terminal does not perform any processing or handling of the input
|
||||
//! and output. This means that features such as echoing input characters, line buffering, and
|
||||
//! special character processing (e.g., CTRL-C for SIGINT) are disabled. This is useful for
|
||||
//! applications that want to have complete control over the terminal input and output, processing
|
||||
//! each keystroke themselves.
|
||||
//!
|
||||
//! For example, in raw mode, the terminal will not perform line buffering on the input, so the
|
||||
//! application will receive each key press as it is typed, instead of waiting for the user to
|
||||
//! press enter. This makes it suitable for real-time applications like text editors,
|
||||
//! terminal-based games, and more.
|
||||
//!
|
||||
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
|
||||
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
|
||||
//! implements raw mode.
|
||||
|
||||
//! # Alternate Screen
|
||||
//!
|
||||
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
|
||||
//! screen. When activated, the terminal will display the alternate screen, hiding the current
|
||||
//! content of the main screen. Applications can write to this screen as if it were the regular
|
||||
//! terminal display, but when the application exits, the terminal will switch back to the main
|
||||
//! screen, and the contents of the alternate screen will be cleared. This is useful for
|
||||
//! applications like text editors or terminal games that want to use the full terminal window
|
||||
//! without disrupting the command line or other terminal content.
|
||||
//!
|
||||
//! This creates a seamless transition between the application and the regular terminal session, as
|
||||
//! the content displayed before launching the application will reappear after the application
|
||||
//! exits.
|
||||
//!
|
||||
//! Note that not all terminal emulators support the alternate screen, and even those that do may
|
||||
//! handle it differently. As a result, the behavior may vary depending on the backend being used.
|
||||
//! Always consult the specific backend's documentation to understand how it implements the
|
||||
//! alternate screen.
|
||||
//!
|
||||
//! # Mouse Capture
|
||||
//!
|
||||
//! Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and
|
||||
//! movement, and sends them to the application as special sequences or events. This enables the
|
||||
//! application to handle and respond to mouse actions, providing a more interactive and graphical
|
||||
//! user experience within the terminal. It's particularly useful for applications like
|
||||
//! terminal-based games, text editors, or other programs that require more direct interaction from
|
||||
//! the user.
|
||||
//!
|
||||
//! Each backend handles mouse capture differently, with variations in the types of events that can
|
||||
//! be captured and how they are represented. As such, the behavior may vary depending on the
|
||||
//! backend being used, and developers should consult the specific backend's documentation to
|
||||
//! understand how it implements mouse capture.
|
||||
//!
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [`Terminal`]: crate::terminal::Terminal
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui-org.github.io/ratatui-book/concepts/backends/comparison.html
|
||||
//! [Ratatui Book]: https://ratatui-org.github.io/ratatui-book
|
||||
use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{buffer::Cell, layout::Size, prelude::Rect};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use self::termwiz::TermwizBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ClearType {
|
||||
/// Clear the entire screen.
|
||||
All,
|
||||
/// Clear everything after the cursor.
|
||||
AfterCursor,
|
||||
/// Clear everything before the cursor.
|
||||
BeforeCursor,
|
||||
/// Clear the current line.
|
||||
CurrentLine,
|
||||
/// Clear everything from the cursor until the next newline.
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
/// The window size in characters (columns / rows) as well as pixels.
|
||||
pub struct WindowSize {
|
||||
/// Size of the window in characters (columns / rows).
|
||||
pub columns_rows: Size,
|
||||
/// Size of the window in pixels.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Most applications should not need to interact with the `Backend` trait directly as the
|
||||
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
|
||||
///
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
pub trait Backend {
|
||||
/// Draw the given content to the terminal screen.
|
||||
///
|
||||
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples, where the first two
|
||||
/// elements represent the x and y coordinates, and the third element is a reference to the
|
||||
/// [`Cell`] to be drawn.
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
|
||||
/// Insert `n` line breaks to the terminal screen.
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends.
|
||||
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hide the cursor on the terminal screen.
|
||||
///
|
||||
///
|
||||
/// See also [`show_cursor`].
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.hide_cursor()?;
|
||||
/// // do something with hidden cursor
|
||||
/// backend.show_cursor()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`show_cursor`]: Backend::show_cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
///
|
||||
/// See [`hide_cursor`] for an example.
|
||||
///
|
||||
/// [`hide_cursor`]: Backend::hide_cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// The returned tuple contains the x and y coordinates of the cursor. The origin
|
||||
/// (0, 0) is at the top left corner of the screen.
|
||||
///
|
||||
/// See [`set_cursor`] for an example.
|
||||
///
|
||||
/// [`set_cursor`]: Backend::set_cursor
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.set_cursor(10, 20)?;
|
||||
/// assert_eq!(backend.get_cursor()?, (10, 20));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`get_cursor`]: Backend::get_cursor
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
|
||||
|
||||
/// Clears the whole terminal scree
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.clear()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn clear(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends. The default
|
||||
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an
|
||||
/// error otherwise.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.clear_region(ClearType::All)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method will return an error if the terminal screen could not be cleared. It will also
|
||||
/// return an error if the `clear_type` is not supported by the backend.
|
||||
///
|
||||
/// [`clear`]: Backend::clear
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
ClearType::AfterCursor
|
||||
| ClearType::BeforeCursor
|
||||
| ClearType::CurrentLine
|
||||
| ClearType::UntilNewLine => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("clear_type [{clear_type:?}] not supported with this backend"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
|
||||
///
|
||||
/// The returned [`Rect`] contains the width and height of the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn size(&self) -> io::Result<Rect>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as a [`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 and rows along with pixel size.
|
||||
fn window_size(&mut self) -> io::Result<WindowSize>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,101 @@
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Crossterm] crate to interact with the terminal.
|
||||
//!
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
SetForegroundColor, SetUnderlineColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
|
||||
///
|
||||
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
|
||||
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
|
||||
/// the cursor, and clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
|
||||
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
|
||||
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
|
||||
/// when the application exits). This is not done automatically by the backend because it is
|
||||
/// possible that the application may want to use the terminal for other purposes (like showing
|
||||
/// help text) before entering alternate screen mode.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stdout, stderr};
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
/// };
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let mut backend = CrosstermBackend::new(stdout());
|
||||
/// // or
|
||||
/// let backend = CrosstermBackend::new(stderr());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// enable_raw_mode()?;
|
||||
/// stdout().execute(EnterAlternateScreen)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
///
|
||||
/// stdout().execute(LeaveAlternateScreen)?;
|
||||
/// disable_raw_mode()?;
|
||||
///
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`Write`]: std::io::Write
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`backend`]: crate::backend
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#examples
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
/// The writer used to send commands to the terminal.
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(buffer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { buffer }
|
||||
/// Creates a new `CrosstermBackend` with the given writer.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// ```
|
||||
pub fn new(writer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { writer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +103,14 @@ impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Writes a buffer of bytes to the underlying buffer.
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
self.writer.write(buf)
|
||||
}
|
||||
|
||||
/// Flushes the underlying buffer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +124,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) {
|
||||
queue!(self.buffer, MoveTo(x, y))?;
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
@@ -64,50 +138,61 @@ where
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.buffer)?;
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
queue!(self.buffer, SetForegroundColor(color))?;
|
||||
queue!(self.writer, SetForegroundColor(color))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
queue!(self.buffer, SetBackgroundColor(color))?;
|
||||
queue!(self.writer, SetBackgroundColor(color))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.buffer, Print(&cell.symbol))?;
|
||||
queue!(self.writer, Print(&cell.symbol))?;
|
||||
}
|
||||
|
||||
queue!(
|
||||
self.buffer,
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
)
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.buffer, Hide)
|
||||
execute!(self.writer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.buffer, Show)
|
||||
execute!(self.writer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
execute!(self.buffer, MoveTo(x, y))
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
execute!(
|
||||
self.buffer,
|
||||
self.writer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
@@ -120,20 +205,34 @@ where
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
queue!(self.buffer, Print("\n"))?;
|
||||
queue!(self.writer, Print("\n"))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
self.writer.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()
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +262,10 @@ impl From<Color> for CColor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 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 {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
BeforeCursor,
|
||||
CurrentLine,
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
pub trait Backend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()>;
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
fn clear(&mut self, clear_type: ClearType) -> Result<(), io::Error>;
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
@@ -1,27 +1,86 @@
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
//! This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Termion] crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`TermionBackend`]: crate::backend::TermionBackend
|
||||
//! [Termion]: https://docs.rs/termion
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
///
|
||||
/// The `TermionBackend` struct is a wrapper around a writer implementing [`Write`], which is used
|
||||
/// to send commands to the terminal. It provides methods for drawing content, manipulating the
|
||||
/// cursor, and clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `TermionBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// Usually applications will enable raw mode and switch to alternate screen mode when starting.
|
||||
/// This is done by calling [`IntoRawMode::into_raw_mode()`] and
|
||||
/// [`IntoAlternateScreen::into_alternate_screen()`] on the writer before creating the backend.
|
||||
/// This is not done automatically by the backend because it is possible that the application may
|
||||
/// want to use the terminal for other purposes (like showing help text) before entering alternate
|
||||
/// screen mode. This backend automatically disable raw mode and switches back to the primary
|
||||
/// screen when the writer is dropped.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stdout, stderr};
|
||||
/// use ratatui::prelude::*;
|
||||
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let mut backend = TermionBackend::new(writer);
|
||||
/// // or
|
||||
/// let writer = stderr().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let backend = TermionBackend::new(stderr());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
|
||||
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [Termion]: https://docs.rs/termion
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
stdout: W,
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W> TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(stdout: W) -> TermionBackend<W> {
|
||||
TermionBackend { stdout }
|
||||
/// Creates a new Termion backend with the given writer.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = TermionBackend::new(stdout());
|
||||
/// ```
|
||||
pub fn new(writer: W) -> TermionBackend<W> {
|
||||
TermionBackend { writer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +89,11 @@ where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.stdout.write(buf)
|
||||
self.writer.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,45 +101,45 @@ impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||
ClearType::All => write!(self.writer, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.writer, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.writer, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.writer, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.writer, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.stdout.flush()
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.stdout)?;
|
||||
writeln!(self.writer)?;
|
||||
}
|
||||
self.stdout.flush()
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
/// Hides cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Hide)?;
|
||||
self.stdout.flush()
|
||||
write!(self.writer, "{}", termion::cursor::Hide)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
/// Shows cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Show)?;
|
||||
self.stdout.flush()
|
||||
write!(self.writer, "{}", termion::cursor::Show)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
/// Gets cursor position (0-based index)
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
|
||||
}
|
||||
|
||||
/// Sets cursor position (0-based index)
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.stdout.flush()
|
||||
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
@@ -123,30 +182,40 @@ where
|
||||
string.push_str(&cell.symbol);
|
||||
}
|
||||
write!(
|
||||
self.stdout,
|
||||
"{}{}{}{}",
|
||||
string,
|
||||
self.writer,
|
||||
"{string}{}{}{}",
|
||||
Fg(Color::Reset),
|
||||
Bg(Color::Reset),
|
||||
termion::style::Reset,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the size of the terminal
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = termion::terminal_size()?;
|
||||
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()
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Fg(Color);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
|
||||
282
src/backend/termwiz.rs
Normal file
282
src/backend/termwiz.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait. It uses the
|
||||
//! [Termwiz] crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`TermwizBackend`]: crate::backend::TermionBackend
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
///
|
||||
/// The `TermwizBackend` struct is a wrapper around a [`BufferedTerminal`], which is used to send
|
||||
/// commands to the terminal. It provides methods for drawing content, manipulating the cursor, and
|
||||
/// clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `TermwizBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// This backend automatically enables raw mode and switches to the alternate screen when it is
|
||||
/// created using the [`TermwizBackend::new`] method (and disables raw mode and returns to the main
|
||||
/// screen when dropped). Use the [`TermwizBackend::with_buffered_terminal`] to create a new
|
||||
/// instance with a custom [`BufferedTerminal`] if this is not desired.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
|
||||
impl TermwizBackend {
|
||||
/// Creates a new Termwiz backend instance.
|
||||
///
|
||||
/// The backend will automatically enable raw mode and enter the alternate screen.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if unable to do any of the following:
|
||||
/// - query the terminal capabilities.
|
||||
/// - enter raw mode.
|
||||
/// - enter the alternate screen.
|
||||
/// - create the system or buffered terminal.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(TermwizBackend { buffered_terminal })
|
||||
}
|
||||
|
||||
/// Creates a new Termwiz backend instance with the given buffered terminal.
|
||||
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
|
||||
TermwizBackend {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
|
||||
&mut self.buffered_terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
},
|
||||
Change::Attribute(AttributeChange::Foreground(cell.fg.into())),
|
||||
Change::Attribute(AttributeChange::Background(cell.bg.into())),
|
||||
]);
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Intensity(
|
||||
if cell.modifier.contains(Modifier::BOLD) {
|
||||
Intensity::Bold
|
||||
} else if cell.modifier.contains(Modifier::DIM) {
|
||||
Intensity::Half
|
||||
} else {
|
||||
Intensity::Normal
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Italic(
|
||||
cell.modifier.contains(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Underline(
|
||||
if cell.modifier.contains(Modifier::UNDERLINED) {
|
||||
Underline::Single
|
||||
} else {
|
||||
Underline::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Reverse(
|
||||
cell.modifier.contains(Modifier::REVERSED),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Invisible(
|
||||
cell.modifier.contains(Modifier::HIDDEN),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
|
||||
cell.modifier.contains(Modifier::CROSSED_OUT),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Blink(
|
||||
if cell.modifier.contains(Modifier::SLOW_BLINK) {
|
||||
Blink::Slow
|
||||
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
|
||||
Blink::Rapid
|
||||
} else {
|
||||
Blink::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(&cell.symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
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),
|
||||
},
|
||||
pixels: Size {
|
||||
width: u16_max(xpixel),
|
||||
height: u16_max(ypixel),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
Color::LightGreen => AnsiColor::Lime.into(),
|
||||
Color::Yellow => AnsiColor::Olive.into(),
|
||||
Color::LightYellow => AnsiColor::Yellow.into(),
|
||||
Color::Magenta => AnsiColor::Purple.into(),
|
||||
Color::LightMagenta => AnsiColor::Fuchsia.into(),
|
||||
Color::Cyan => AnsiColor::Teal.into(),
|
||||
Color::LightCyan => AnsiColor::Aqua.into(),
|
||||
Color::White => AnsiColor::White.into(),
|
||||
Color::Blue => AnsiColor::Navy.into(),
|
||||
Color::LightBlue => AnsiColor::Blue.into(),
|
||||
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
@@ -1,13 +1,39 @@
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
//! This module provides the `TestBackend` implementation for the [`Backend`] trait.
|
||||
//! It is used in the integration tests to verify the correctness of the library.
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Write},
|
||||
io,
|
||||
};
|
||||
use std::{fmt::Write, io};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
#[derive(Debug)]
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation used for integration testing that that renders to an in memory
|
||||
/// buffer.
|
||||
///
|
||||
/// Note: that although many of the integration and unit tests in ratatui are written using this
|
||||
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
|
||||
/// than using this backend. This backend is intended for integration tests that test the entire
|
||||
/// terminal UI.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::TestBackend, prelude::*};
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
@@ -17,6 +43,11 @@ pub struct TestBackend {
|
||||
}
|
||||
|
||||
/// Returns a string representation of the given buffer for debugging purpose.
|
||||
///
|
||||
/// This function is used to visualize the buffer content in a human-readable format.
|
||||
/// It iterates through the buffer content and appends each cell's symbol to the view string.
|
||||
/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
|
||||
/// displayed at the end of the line.
|
||||
fn buffer_view(buffer: &Buffer) -> String {
|
||||
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
|
||||
for cells in buffer.content.chunks(buffer.area.width as usize) {
|
||||
@@ -27,18 +58,13 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol))
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
write!(
|
||||
&mut view,
|
||||
" Hidden by multi-width symbols: {:?}",
|
||||
overwritten
|
||||
)
|
||||
.unwrap();
|
||||
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
|
||||
}
|
||||
view.push('\n');
|
||||
}
|
||||
@@ -46,6 +72,7 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
/// Creates a new TestBackend with the specified width and height.
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
width,
|
||||
@@ -56,16 +83,22 @@ impl TestBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the TestBackend.
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Resizes the TestBackend to the specified width and height.
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@@ -93,15 +126,20 @@ impl TestBackend {
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!(
|
||||
"{}: at ({}, {}) expected {:?} got {:?}",
|
||||
i, x, y, expected_cell, cell
|
||||
)
|
||||
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{}", debug_info);
|
||||
panic!("{debug_info}");
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestBackend {
|
||||
/// Formats the TestBackend for display by calling the buffer_view function
|
||||
/// on its internal buffer.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +155,6 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, _n: u16) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
@@ -140,7 +174,7 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self, _clear_type: ClearType) -> Result<(), io::Error> {
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
@@ -149,7 +183,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();
|
||||
}
|
||||
}
|
||||
|
||||
462
src/buffer.rs
462
src/buffer.rs
@@ -1,19 +1,29 @@
|
||||
use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
use std::cmp::min;
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[allow(deprecated)]
|
||||
use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Spans},
|
||||
};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[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 {
|
||||
@@ -46,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)
|
||||
@@ -58,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +129,7 @@ impl Default for Cell {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use tui::buffer::{Buffer, Cell};
|
||||
/// use tui::layout::Rect;
|
||||
/// use tui::style::{Color, Style, Modifier};
|
||||
/// use ratatui::{prelude::*, buffer::Cell};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
@@ -100,12 +139,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(Debug, Clone, PartialEq, 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,
|
||||
@@ -117,7 +160,7 @@ pub struct Buffer {
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
@@ -132,24 +175,16 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
|
||||
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: AsRef<str>,
|
||||
S: Into<Line<'a>>,
|
||||
{
|
||||
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
let height = lines.len() as u16;
|
||||
let width = lines
|
||||
.iter()
|
||||
.map(|i| i.as_ref().width() as u16)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_string(0, y as u16, line, Style::default());
|
||||
buffer.set_line(0, y as u16, line, width);
|
||||
}
|
||||
buffer
|
||||
}
|
||||
@@ -176,15 +211,14 @@ impl Buffer {
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
@@ -196,8 +230,7 @@ impl Buffer {
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
@@ -210,9 +243,7 @@ impl Buffer {
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
|
||||
x,
|
||||
y,
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
@@ -225,8 +256,7 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
@@ -238,8 +268,7 @@ impl Buffer {
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
@@ -248,13 +277,12 @@ impl Buffer {
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={} len={}",
|
||||
i,
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + i as u16 % self.area.width,
|
||||
self.area.y + i as u16 / self.area.width,
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -289,7 +317,7 @@ impl Buffer {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
@@ -306,7 +334,9 @@ impl Buffer {
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
|
||||
#[allow(deprecated)]
|
||||
#[deprecated(note = "Use `Buffer::set_line` instead")]
|
||||
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &spans.0 {
|
||||
@@ -327,7 +357,28 @@ impl Buffer {
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
@@ -358,7 +409,7 @@ impl Buffer {
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Default::default());
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
@@ -373,7 +424,7 @@ impl Buffer {
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
@@ -433,13 +484,13 @@ impl Buffer {
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceeding multi-width characters:
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
|
||||
// place (the skipped cells should be blank anyway):
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// 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]));
|
||||
}
|
||||
@@ -453,6 +504,128 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(&c.symbol)?;
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
#[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() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
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
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -463,6 +636,81 @@ mod tests {
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_implements_debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.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!(
|
||||
"
|
||||
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, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
@@ -504,21 +752,38 @@ mod tests {
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -529,12 +794,12 @@ mod tests {
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -542,11 +807,11 @@ mod tests {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -650,6 +915,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(
|
||||
@@ -671,7 +948,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -695,7 +972,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
@@ -729,6 +1006,65 @@ mod tests {
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(one, merged);
|
||||
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]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines_accepts_into_lines() {
|
||||
use crate::style::Stylize;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
|
||||
buf.set_string(0, 0, "foo", Style::new().red());
|
||||
buf.set_string(0, 1, "bar", Style::new().blue());
|
||||
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
|
||||
}
|
||||
}
|
||||
|
||||
1680
src/layout.rs
1680
src/layout.rs
File diff suppressed because it is too large
Load Diff
85
src/lib.rs
85
src/lib.rs
@@ -1,16 +1,19 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) 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
|
||||
//!
|
||||
//! ## Adding `tui` as a dependency
|
||||
//! ## Adding `ratatui` as a dependency
|
||||
//!
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.16"
|
||||
//! crossterm = "0.22"
|
||||
//! crossterm = "0.27"
|
||||
//! ratatui = "0.23"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
@@ -19,21 +22,21 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! tui = { version = "0.16", 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.
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
|
||||
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
|
||||
//! abstraction over available backends that provides basic functionalities such as clearing the
|
||||
//! screen, hiding the cursor, etc.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use tui::{backend::CrosstermBackend, Terminal};
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
@@ -48,7 +51,7 @@
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use tui::{backend::TermionBackend, Terminal};
|
||||
//! use ratatui::prelude::*;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
@@ -69,21 +72,16 @@
|
||||
//! 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:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use tui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Widget, Block, Borders},
|
||||
//! layout::{Layout, Constraint, Direction},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
@@ -104,6 +102,12 @@
|
||||
//! f.render_widget(block, size);
|
||||
//! })?;
|
||||
//!
|
||||
//! // Start a thread to discard any input events. Without handling events, the
|
||||
//! // stdin buffer will fill up, and be read into the shell when the program exits.
|
||||
//! thread::spawn(|| loop {
|
||||
//! event::read();
|
||||
//! });
|
||||
//!
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! // restore terminal
|
||||
@@ -126,12 +130,8 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use tui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Frame,
|
||||
//! };
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
@@ -155,10 +155,32 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! 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))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
@@ -169,4 +191,7 @@ pub mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, ViewportVariant};
|
||||
#[doc(inline)]
|
||||
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
34
src/prelude.rs
Normal file
34
src/prelude.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
//! 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::*};
|
||||
//!
|
||||
//! #[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::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
|
||||
text::{self, Line, Masked, Span, Text},
|
||||
};
|
||||
448
src/style.rs
448
src/style.rs
@@ -1,30 +1,73 @@
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
//!
|
||||
//! There are two ways to set styles:
|
||||
//! - Creating and using the [`Style`] struct. (e.g. `Style::new().fg(Color::Red)`).
|
||||
//! - Using style shorthands. (e.g. `"hello".red()`).
|
||||
//!
|
||||
//! # Using the `Style` struct
|
||||
//!
|
||||
//! This is the original approach to styling and likely the most common. This is useful when
|
||||
//! creating style variables to reuse, however the shorthands are often more convenient and
|
||||
//! readable for most use cases.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let heading_style = Style::new()
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! let span = Span::styled("hello", heading_style);
|
||||
//! ```
|
||||
//!
|
||||
//! # Using style shorthands
|
||||
//!
|
||||
//! Originally Ratatui only had the ability to set styles using the `Style` struct. This is still
|
||||
//! supported, but there are now shorthands for all the styles that can be set. These save you from
|
||||
//! having to create a `Style` struct every time you want to set a style.
|
||||
//!
|
||||
//! The shorthands are implemented in the [`Stylize`] trait which is automatically implemented for
|
||||
//! many types via the [`Styled`] trait. This means that you can use the shorthands on any type
|
||||
//! that implements [`Styled`]. E.g.:
|
||||
//! - Strings and string slices when styled return a [`Span`]
|
||||
//! - [`Span`]s can be styled again, which will merge the styles.
|
||||
//! - Many widget types can be styled directly rather than calling their style() method.
|
||||
//!
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
|
||||
//! the [`prelude`] module for convenience.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled(
|
||||
//! "hello",
|
||||
//! Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Paragraph::new("hello").red().on_blue().bold(),
|
||||
//! Paragraph::new("hello")
|
||||
//! .style(Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! [`prelude`]: crate::prelude
|
||||
//! [`Span`]: crate::text::Span
|
||||
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Color {
|
||||
Reset,
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
Gray,
|
||||
DarkGray,
|
||||
LightRed,
|
||||
LightGreen,
|
||||
LightYellow,
|
||||
LightBlue,
|
||||
LightMagenta,
|
||||
LightCyan,
|
||||
White,
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
mod stylize;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
mod color;
|
||||
pub use color::Color;
|
||||
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
@@ -34,11 +77,12 @@ bitflags! {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::Modifier;
|
||||
/// use ratatui::{prelude::*};
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Modifier: u16 {
|
||||
const BOLD = 0b0000_0000_0001;
|
||||
const DIM = 0b0000_0000_0010;
|
||||
@@ -52,27 +96,52 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
/// Implement the `Debug` trait for `Modifier` manually.
|
||||
///
|
||||
/// This will avoid printing the empty modifier as 'Borders(0x0)' and instead print it as 'NONE'.
|
||||
impl fmt::Debug for Modifier {
|
||||
/// Format the modifier as `NONE` if the modifier is empty or as a list of flags separated by
|
||||
/// `|` otherwise.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Style lets you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// use ratatui::{prelude::*};
|
||||
///
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Style::new().black().on_green().italic().bold();
|
||||
/// ```
|
||||
///
|
||||
/// For more information about the style shorthands, see the [`Stylize`] trait.
|
||||
///
|
||||
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// use ratatui::{prelude::*};
|
||||
///
|
||||
/// 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));
|
||||
@@ -83,7 +152,9 @@ bitflags! {
|
||||
/// 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(),
|
||||
@@ -94,9 +165,8 @@ bitflags! {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// use ratatui::{prelude::*};
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
@@ -109,38 +179,61 @@ bitflags! {
|
||||
/// 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)]
|
||||
#[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,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Style {
|
||||
Style::new()
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub fn reset() -> Style {
|
||||
pub const fn reset() -> 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(),
|
||||
}
|
||||
@@ -151,12 +244,12 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
pub fn fg(mut self, color: Color) -> Style {
|
||||
pub const fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -166,16 +259,37 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
pub fn bg(mut self, color: Color) -> Style {
|
||||
pub const fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
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::prelude::*;
|
||||
/// 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.
|
||||
@@ -183,16 +297,16 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// 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
|
||||
}
|
||||
|
||||
@@ -203,16 +317,16 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// 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
|
||||
}
|
||||
|
||||
@@ -221,7 +335,7 @@ impl Style {
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use tui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
@@ -233,6 +347,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);
|
||||
@@ -278,4 +397,229 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
Modifier::UNDERLINED,
|
||||
Modifier::SLOW_BLINK,
|
||||
Modifier::RAPID_BLINK,
|
||||
Modifier::REVERSED,
|
||||
Modifier::HIDDEN,
|
||||
Modifier::CROSSED_OUT,
|
||||
];
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_debug() {
|
||||
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
|
||||
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
|
||||
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
|
||||
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
|
||||
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
|
||||
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
|
||||
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::BOLD | Modifier::DIM),
|
||||
"BOLD | DIM"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::all()),
|
||||
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_const() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
362
src/style/color.rs
Normal file
362
src/style/color.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
use std::{
|
||||
fmt::{self, Debug, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
/// All colors from the [ANSI color table] 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 std::str::FromStr;
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// 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));
|
||||
/// ```
|
||||
///
|
||||
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, 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.
|
||||
///
|
||||
/// Note that only terminals that support 24-bit true color will display this correctly.
|
||||
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
|
||||
/// support this.
|
||||
///
|
||||
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
|
||||
/// fallback to the default text color. Crossterm and Termion do not have this capability and
|
||||
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
|
||||
/// See <https://github.com/ratatui-org/ratatui/issues/475> for an example of this problem.
|
||||
///
|
||||
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
|
||||
///
|
||||
/// [`TermwizBackend`]: crate::backend::TermwizBackend
|
||||
Rgb(u8, u8, u8),
|
||||
/// An 8-bit 256 color.
|
||||
///
|
||||
/// See also <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to parse Colors")
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// See the [`Color`] documentation for more information on the supported color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
/// assert_eq!(color, Color::Blue);
|
||||
///
|
||||
/// let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// let color: Color = Color::from_str("10").unwrap();
|
||||
/// assert_eq!(color, Color::Indexed(10));
|
||||
///
|
||||
/// let color: Result<Color, _> = Color::from_str("invalid_color");
|
||||
/// assert!(color.is_err());
|
||||
/// ```
|
||||
impl FromStr for Color {
|
||||
type Err = ParseColorError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
#[test]
|
||||
fn from_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_indexed_color() {
|
||||
let color: Color = Color::from_str("10").unwrap();
|
||||
assert_eq!(color, Color::Indexed(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 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 '#'
|
||||
"#abcdef00", // too many chars
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
|
||||
for bad_color in bad_colors {
|
||||
assert!(
|
||||
Color::from_str(bad_color).is_err(),
|
||||
"bad color: '{bad_color}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
411
src/style/stylize.rs
Normal file
411
src/style/stylize.rs
Normal file
@@ -0,0 +1,411 @@
|
||||
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>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// An extension trait for styling objects.
|
||||
///
|
||||
/// For any type that implements `Stylize`, the provided methods in this trait can be used to style
|
||||
/// the type further. This trait is automatically implemented for any type that implements the
|
||||
/// [`Styled`] trait which e.g.: [`String`], [`&str`], [`Span`], [`Style`] and many Widget types.
|
||||
///
|
||||
/// This results in much more ergonomic styling of text and widgets. For example, instead of
|
||||
/// writing:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let text = Span::styled("Hello", Style::default().fg(Color::Red).bg(Color::Blue));
|
||||
/// ```
|
||||
///
|
||||
/// You can write:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let text = "Hello".red().on_blue();
|
||||
/// ```
|
||||
///
|
||||
/// This trait implements a provided method for every color as both foreground and background
|
||||
/// (prefixed by `on_`), and all modifiers as both an additive and subtractive modifier (prefixed
|
||||
/// by `not_`). The `reset()` method is also provided to reset the style.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// let span = "hello".red().on_blue().bold();
|
||||
/// let line = Line::from(vec![
|
||||
/// "hello".red().on_blue().bold(),
|
||||
/// "world".green().on_yellow().not_bold(),
|
||||
/// ]);
|
||||
/// let paragraph = Paragraph::new(line).italic().underlined();
|
||||
/// let block = Block::default().title("Title").borders(Borders::ALL).on_white().bold();
|
||||
/// ```
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for String {
|
||||
type Item = Span<'static>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn str_styled() {
|
||||
assert_eq!("hello".style(), Style::default());
|
||||
assert_eq!(
|
||||
"hello".set_style(Style::new().cyan()),
|
||||
Span::styled("hello", Style::new().cyan())
|
||||
);
|
||||
assert_eq!("hello".black(), Span::from("hello").black());
|
||||
assert_eq!("hello".red(), Span::from("hello").red());
|
||||
assert_eq!("hello".green(), Span::from("hello").green());
|
||||
assert_eq!("hello".yellow(), Span::from("hello").yellow());
|
||||
assert_eq!("hello".blue(), Span::from("hello").blue());
|
||||
assert_eq!("hello".magenta(), Span::from("hello").magenta());
|
||||
assert_eq!("hello".cyan(), Span::from("hello").cyan());
|
||||
assert_eq!("hello".gray(), Span::from("hello").gray());
|
||||
assert_eq!("hello".dark_gray(), Span::from("hello").dark_gray());
|
||||
assert_eq!("hello".light_red(), Span::from("hello").light_red());
|
||||
assert_eq!("hello".light_green(), Span::from("hello").light_green());
|
||||
assert_eq!("hello".light_yellow(), Span::from("hello").light_yellow());
|
||||
assert_eq!("hello".light_blue(), Span::from("hello").light_blue());
|
||||
assert_eq!("hello".light_magenta(), Span::from("hello").light_magenta());
|
||||
assert_eq!("hello".light_cyan(), Span::from("hello").light_cyan());
|
||||
assert_eq!("hello".white(), Span::from("hello").white());
|
||||
|
||||
assert_eq!("hello".on_black(), Span::from("hello").on_black());
|
||||
assert_eq!("hello".on_red(), Span::from("hello").on_red());
|
||||
assert_eq!("hello".on_green(), Span::from("hello").on_green());
|
||||
assert_eq!("hello".on_yellow(), Span::from("hello").on_yellow());
|
||||
assert_eq!("hello".on_blue(), Span::from("hello").on_blue());
|
||||
assert_eq!("hello".on_magenta(), Span::from("hello").on_magenta());
|
||||
assert_eq!("hello".on_cyan(), Span::from("hello").on_cyan());
|
||||
assert_eq!("hello".on_gray(), Span::from("hello").on_gray());
|
||||
assert_eq!("hello".on_dark_gray(), Span::from("hello").on_dark_gray());
|
||||
assert_eq!("hello".on_light_red(), Span::from("hello").on_light_red());
|
||||
assert_eq!(
|
||||
"hello".on_light_green(),
|
||||
Span::from("hello").on_light_green()
|
||||
);
|
||||
assert_eq!(
|
||||
"hello".on_light_yellow(),
|
||||
Span::from("hello").on_light_yellow()
|
||||
);
|
||||
assert_eq!("hello".on_light_blue(), Span::from("hello").on_light_blue());
|
||||
assert_eq!(
|
||||
"hello".on_light_magenta(),
|
||||
Span::from("hello").on_light_magenta()
|
||||
);
|
||||
assert_eq!("hello".on_light_cyan(), Span::from("hello").on_light_cyan());
|
||||
assert_eq!("hello".on_white(), Span::from("hello").on_white());
|
||||
|
||||
assert_eq!("hello".bold(), Span::from("hello").bold());
|
||||
assert_eq!("hello".dim(), Span::from("hello").dim());
|
||||
assert_eq!("hello".italic(), Span::from("hello").italic());
|
||||
assert_eq!("hello".underlined(), Span::from("hello").underlined());
|
||||
assert_eq!("hello".slow_blink(), Span::from("hello").slow_blink());
|
||||
assert_eq!("hello".rapid_blink(), Span::from("hello").rapid_blink());
|
||||
assert_eq!("hello".reversed(), Span::from("hello").reversed());
|
||||
assert_eq!("hello".hidden(), Span::from("hello").hidden());
|
||||
assert_eq!("hello".crossed_out(), Span::from("hello").crossed_out());
|
||||
|
||||
assert_eq!("hello".not_bold(), Span::from("hello").not_bold());
|
||||
assert_eq!("hello".not_dim(), Span::from("hello").not_dim());
|
||||
assert_eq!("hello".not_italic(), Span::from("hello").not_italic());
|
||||
assert_eq!(
|
||||
"hello".not_underlined(),
|
||||
Span::from("hello").not_underlined()
|
||||
);
|
||||
assert_eq!(
|
||||
"hello".not_slow_blink(),
|
||||
Span::from("hello").not_slow_blink()
|
||||
);
|
||||
assert_eq!(
|
||||
"hello".not_rapid_blink(),
|
||||
Span::from("hello").not_rapid_blink()
|
||||
);
|
||||
assert_eq!("hello".not_reversed(), Span::from("hello").not_reversed());
|
||||
assert_eq!("hello".not_hidden(), Span::from("hello").not_hidden());
|
||||
assert_eq!(
|
||||
"hello".not_crossed_out(),
|
||||
Span::from("hello").not_crossed_out()
|
||||
);
|
||||
|
||||
assert_eq!("hello".reset(), Span::from("hello").reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_styled() {
|
||||
let s = String::from("hello");
|
||||
assert_eq!(s.style(), Style::default());
|
||||
assert_eq!(
|
||||
s.clone().set_style(Style::new().cyan()),
|
||||
Span::styled("hello", Style::new().cyan())
|
||||
);
|
||||
assert_eq!(s.clone().black(), Span::from("hello").black());
|
||||
assert_eq!(s.clone().on_black(), Span::from("hello").on_black());
|
||||
assert_eq!(s.clone().bold(), Span::from("hello").bold());
|
||||
assert_eq!(s.clone().not_bold(), Span::from("hello").not_bold());
|
||||
assert_eq!(s.clone().reset(), Span::from("hello").reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temporary_string_styled() {
|
||||
// to_string() is used to create a temporary String, which is then styled. Without the
|
||||
// `Styled` trait impl for `String`, this would fail to compile with the error: "temporary
|
||||
// value dropped while borrowed"
|
||||
let s = "hello".to_string().red();
|
||||
assert_eq!(s, Span::from("hello").red());
|
||||
|
||||
// format!() is used to create a temporary String inside a closure, which suffers the same
|
||||
// issue as above without the `Styled` trait impl for `String`
|
||||
let items = vec![String::from("a"), String::from("b")];
|
||||
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
|
||||
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
}
|
||||
117
src/symbols.rs
117
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,12 +242,95 @@ 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
|
||||
/// One point per cell in shape of dot ("•")
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
/// One point per cell in shape of a block ("█")
|
||||
Block,
|
||||
/// Up to 8 points per cell
|
||||
/// One point per cell in the shape of a bar ("▄")
|
||||
Bar,
|
||||
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
|
||||
/// represent data points.
|
||||
///
|
||||
/// This is a 2x4 grid of dots, where each dot can be either on or off.
|
||||
///
|
||||
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
|
||||
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
|
||||
/// characters (<28>) instead of Braille dots.
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
614
src/terminal.rs
614
src/terminal.rs
@@ -1,38 +1,135 @@
|
||||
#![deny(missing_docs)]
|
||||
//! Provides the [`Terminal`], [`Frame`] and related types.
|
||||
//!
|
||||
//! The [`Terminal`] is the main interface of this library. It is responsible for drawing and
|
||||
//! maintaining the state of the different widgets that compose your application.
|
||||
//!
|
||||
//! The [`Frame`] is a consistent view into the terminal state for rendering. It is obtained via
|
||||
//! the closure argument of [`Terminal::draw`]. It is used to render widgets to the terminal and
|
||||
//! control the cursor position.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//! use ratatui::{prelude::*, widgets::Paragraph};
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.draw(|frame| {
|
||||
//! let area = frame.size();
|
||||
//! frame.render_widget(Paragraph::new("Hello world!"), area);
|
||||
//! })?;
|
||||
//! # std::io::Result::Ok(())
|
||||
//! ```
|
||||
//!
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [`backend`]: crate::backend
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
use std::{fmt, io};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::io;
|
||||
use tracing::{event, span, Level};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ViewportVariant {
|
||||
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
|
||||
/// currently visible to the user. It can be either fullscreen, inline or fixed.
|
||||
///
|
||||
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
|
||||
///
|
||||
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
|
||||
/// the viewport is fixed, but the width is the same as the terminal width.
|
||||
///
|
||||
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
|
||||
/// by a [`Rect`].
|
||||
///
|
||||
/// See [`Terminal::with_options`] for more information.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Viewport {
|
||||
/// The viewport is fullscreen
|
||||
#[default]
|
||||
Fullscreen,
|
||||
/// The viewport is inline with the rest of the terminal.
|
||||
///
|
||||
/// The viewport's height is fixed and specified in number of lines. The width is the same as
|
||||
/// the terminal's width. The viewport is drawn below the cursor position.
|
||||
Inline(u16),
|
||||
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
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: ViewportVariant,
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Viewport {
|
||||
variant: ViewportVariant,
|
||||
area: Rect,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
/// An interface that Ratatui to interact and draw [`Frame`]s on the user's terminal.
|
||||
///
|
||||
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
|
||||
/// state of the buffers, cursor and viewport.
|
||||
///
|
||||
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
|
||||
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// The terminal has two buffers that are used to draw the application. The first buffer is the
|
||||
/// current buffer and the second buffer is the previous buffer. The two buffers are compared at
|
||||
/// the end of each draw pass to output only the changes to the terminal.
|
||||
///
|
||||
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
|
||||
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
|
||||
///
|
||||
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
|
||||
/// application with the new size. This will automatically resize the internal buffers to match the
|
||||
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
/// use ratatui::{prelude::*, widgets::Paragraph};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor(0, 0);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Backend`]: crate::backend::Backend
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The backend used to interface with the terminal
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
@@ -43,6 +140,8 @@ where
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
/// Area of the viewport
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
@@ -50,103 +149,6 @@ where
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
terminal: &'a mut Terminal<B>,
|
||||
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Frame size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport.area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::Block;
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut());
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
/// let items = vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ];
|
||||
/// let list = List::new(items);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut(), state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -155,7 +157,7 @@ where
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {}", err);
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,51 +167,58 @@ impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: ViewportVariant::Fullscreen,
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let terminal = Terminal::with_options(
|
||||
/// backend,
|
||||
/// TerminalOptions { viewport },
|
||||
/// )?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
ViewportVariant::Fullscreen => (size, (0, 0)),
|
||||
ViewportVariant::Inline(height) => {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
let max_height = size.height.min(height);
|
||||
backend.append_lines(max_height.saturating_sub(1))?;
|
||||
let missing_lines = row.saturating_add(max_height).saturating_sub(size.height);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
)
|
||||
}
|
||||
ViewportVariant::Fixed(area) => (area, (area.left(), area.top())),
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: Viewport {
|
||||
variant: options.viewport,
|
||||
area: viewport_area,
|
||||
},
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
@@ -223,14 +232,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current buffer as a mutable reference.
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
/// Gets the backend
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Gets the backend as a mutable reference
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
@@ -247,42 +259,21 @@ where
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn resize(&mut self) -> io::Result<()> {
|
||||
let size = self.size()?;
|
||||
if self.last_known_size == size {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
event!(Level::DEBUG, last_known_size = ?self.last_known_size, ?size, "terminal size changed");
|
||||
|
||||
let next_area = match self.viewport.variant {
|
||||
ViewportVariant::Fullscreen => size,
|
||||
ViewportVariant::Inline(height) => {
|
||||
let (_, mut row) = self.get_cursor()?;
|
||||
/// Updates the Terminal so that internal buffers match the requested size.
|
||||
///
|
||||
/// Requested size will be saved so the size can remain consistent when rendering. This leads
|
||||
/// to a full clear of the screen.
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport.area.top());
|
||||
let max_height = height.min(size.height);
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
self.backend.append_lines(lines_after_cursor)?;
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
}
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
ViewportVariant::Fixed(area) => area,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
@@ -292,24 +283,49 @@ where
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.viewport.area = area;
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
event!(Level::DEBUG, area = ?area, "viewport changed");
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
///
|
||||
/// This is the main entry point for drawing to the terminal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, widgets::Paragraph};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor(0, 0);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
let span = span!(Level::DEBUG, "draw");
|
||||
let _guard = span.enter();
|
||||
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.resize()?;
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
@@ -329,37 +345,40 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Swap buffers
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
event!(Level::DEBUG, "completed frame");
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Hides the cursor.
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call and is returned as a tuple of
|
||||
/// `(x, y)` coordinates.
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
@@ -368,18 +387,17 @@ where
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
event!(Level::DEBUG, "clear");
|
||||
match self.viewport.variant {
|
||||
ViewportVariant::Fullscreen => self.backend.clear(ClearType::All)?,
|
||||
ViewportVariant::Inline(_) => {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport.area.left(), self.viewport.area.top())?;
|
||||
self.backend.clear(ClearType::AfterCursor)?;
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
ViewportVariant::Fixed(area) => {
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear(ClearType::AfterCursor)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,6 +406,12 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
@@ -396,8 +420,8 @@ where
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space is
|
||||
/// then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space
|
||||
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
@@ -425,15 +449,11 @@ where
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Paragraph, Widget};
|
||||
/// # use tui::text::{Spans, Span};
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// # use tui::{Terminal};
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Spans::from(vec![
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
@@ -444,9 +464,7 @@ where
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
let span = span!(Level::DEBUG, "insert_before");
|
||||
let _guard = span.enter();
|
||||
if !matches!(self.viewport.variant, ViewportVariant::Inline(_)) {
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -454,11 +472,11 @@ where
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport.area.top());
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport.area.left(),
|
||||
y: self.viewport.area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport.area.width,
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
@@ -473,16 +491,192 @@ where
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport.area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport.area.height)?;
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport.area.height,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
/// A consistent view into the terminal state for rendering a single frame.
|
||||
///
|
||||
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
|
||||
/// to the terminal and control the cursor position.
|
||||
///
|
||||
/// The changes drawn to the frame are not immediately applied to the terminal. They are only
|
||||
/// applied after the closure returns. This allows for widgets to be drawn in any order. The
|
||||
/// changes are then compared to the previous frame and only the necessary updates are applied to
|
||||
/// the terminal.
|
||||
///
|
||||
/// The [`Frame`] is generic over a [`Backend`] implementation which is used to interface with the
|
||||
/// underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Backend`]: crate::backend::Backend
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The terminal that this frame is associated with.
|
||||
terminal: &'a mut Terminal<B>,
|
||||
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The size of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut());
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let mut state = ListState::default().with_selected(Some(1));
|
||||
/// let list = List::new(vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ]);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut(), state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// `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> {
|
||||
/// The buffer that was used to draw the last frame.
|
||||
pub buffer: &'a Buffer,
|
||||
/// The size of the last frame.
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
#[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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user