Compare commits
3 Commits
main
...
v0.20.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b700afdb9 | ||
|
|
c70a84b3fe | ||
|
|
8ed244eeb9 |
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
|
||||
@@ -1,2 +0,0 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
115
.cliffignore
115
.cliffignore
@@ -1,115 +0,0 @@
|
||||
# skip entries from <https://github.com/ratatui/ratatui/pull/1652>
|
||||
50af9a5d80ed5446f3e6cc554911f606580edde9
|
||||
272e9709c6eed45cd7e0c183624b7898f4e0ae69
|
||||
adc8fdc35aa57d6dad2ae8dd30ec2e9256576c09
|
||||
31711dbf82a4c7bb3b78692da34d9f469725dd6e
|
||||
b6356aa7a529a491d63dd6628b8985adae337f16
|
||||
885558b6f89df642317d39c5b44c94c742d1e0c8
|
||||
6440eb9d76379340953420629ab0a3d9039d6c48
|
||||
3870583ea868452191857f9bda97a3d5c35d0a4f
|
||||
487edc8399683fb8a9a66359729c015780d248f2
|
||||
250c222cc4aaab09184a28efc68f75ca03133794
|
||||
590a392ab11c1a215767614931036781f4cf6a29
|
||||
6443f7408af4a8834bc68cd35d2ba9be47e45f38
|
||||
8339cce10a51c9c951b3c9750d527d80168626eb
|
||||
ac342231c344e893f2f630ee38167aab28c736a6
|
||||
0fb103680028a6e26a1923f87b60bde51acaec4f
|
||||
4335a90a00aeeedb69a92442c7f2711727944017
|
||||
71480242a926f98e9081ed6e2dc8c381757b3a42
|
||||
e1a31db55913bd690bcaef380e9dbc3b6a5dc175
|
||||
38490ff8da6f11e309ba1bcb3e88a562f7c953c5
|
||||
a4bb143e4767137d6a9e9927d3da66562611f86f
|
||||
bc73f2dcbe5ea48fc4d1555a8e931f40d7b0e03f
|
||||
77dea441e5637b1c428c2aa71ea67fb3aac20c12
|
||||
06af14107e70e49a4cba3babb8ab0a0c57b4bdf8
|
||||
9acdab32df69517b93dd2b861b85586d47c71540
|
||||
2e684c6500be61fbe69744d183c8086564ac0051
|
||||
b7f8ec0ff9659473d936eae53a57cd9de38cec1f
|
||||
31a2c4548c304270a8c852f19baa7a4eaac5e75c
|
||||
57b681b053c019b66e0ed92959638997fea731b1
|
||||
131b9ec41751163d43d94564363247b60f031486
|
||||
8b32f82b4dd526580d00fa13f053bf507e8ea933
|
||||
76d1e5b1733d47a7f05acf563db26cb1a66b540d
|
||||
57a0a34f924e0d488c9e9f917900e677c3488dc4
|
||||
12aa58601ddd0704256c56019bd2c7139d41f7a2
|
||||
2dc81833c60951d16f9bd60f3da003edfc9a11f5
|
||||
20e41f1d1da6db8abbc2504814531d4d97bdf94b
|
||||
68b55a12a29e70ebfcc063c2d5d5845de3b5a27e
|
||||
693003314a25e792ffc5d53146b28bfb6a4582e3
|
||||
63441e259bc38b56e0369197bed14788b2cb3d54
|
||||
fa88152c808eeb6c9d9b3662361aab1e57e1b1bf
|
||||
30d9daa59b1843786cde00e25c3e69cfe818b80b
|
||||
92540b2f6ab25f3a5400aebb28af3c498ac793a4
|
||||
29edc3a7a38c512611a80cf5d8d42027558419b2
|
||||
819499d6ffc0e8453ed3220067645933a4882a74
|
||||
4756526829a4e849d9e256b6cf821eb66afe3ade
|
||||
8f35437d5ad78d31cd45c4af888f20f0b5ab4196
|
||||
39bd72b1f702dadb1ebbaf4e77ad2fada166ac49
|
||||
c6d2cee3e967c9234176c5229858512b0c79d6a9
|
||||
bd90e3d928e0f9f0b915933ebbc32c2256fe8cfa
|
||||
b820c0c7e4c576c1e39b5e482a8aac08076a039c
|
||||
74194759756bb111b4da3e9a5cdb968275a2fab0
|
||||
38b2f27efe0e1829bc503df7fd64b94b7bb80d97
|
||||
21aa3232d762d6e3f81f15fc5b66ba462385ac05
|
||||
903bb0ae32d22393783edfda96db900739864f0a
|
||||
5cee13ab6d9c49751cf9283d9099e37f0cc3632c
|
||||
0a0997702dd4cf2217160f5652f5c39cbd4a1010
|
||||
778f2f5ec511bef431b54157242b91d083ea9840
|
||||
7d23bd2ceaf96e81972b5f746fdcba0d17f6391f
|
||||
2c02a56bce31519386303571e0b66b7d4beb378e
|
||||
f33d51e7d9ccf9fe52ec3289d04d97c722d9ee17
|
||||
91cd81aaa032887bb2327bc3fe3cad6b3c9fbacf
|
||||
5d5a1ccb0b4e2f293f215ce026fba33f1c069689
|
||||
6b9417db5f2adbdc60e9dd8dc5acbbe2a1f77ba9
|
||||
dce1e4b138eb1333c9e773bacd579a8cdddd73fb
|
||||
b4aacb045e2200896b0d0136a2b8688b47828d73
|
||||
dcba0bcd5d5d6e33ddc1fa94ebb94819fdda600e
|
||||
6f52350ecfb62e3a5bac16f0824e74b757cc6cd2
|
||||
2c6f324b9aa5034771e00758b143fd8df94d859d
|
||||
bf0210602948f8d26ae323996fe7b22fb218a446
|
||||
07aff91b015b5e7e0504680c12edbce70d7dba1e
|
||||
f6d49dde14af73ed467d75d8f6ec0f502db2908f
|
||||
9a7467b30576d5cb7491ea6e09efcae97eadf9bb
|
||||
a0c35f1d7bcce10e092582b95f5b0a3f20ad7bf3
|
||||
d24747d46982192b575a40b8cc18d1c948fac3d7
|
||||
8060f7bc578b29dde6ca0c4c64569f9c73218f46
|
||||
0dc5b2d2e0aa6438ffc1b3965b1ab31c721adbcc
|
||||
8ecdd892f53d7db95bbb53a61700d36e3fcefdd4
|
||||
570c35868147a2400a13331e85d562d1ef96a011
|
||||
3855c3a84a77037aeee40dbe9e52454fb1f9afee
|
||||
93372f35c1669da0138ca776890f3ff3d38a6539
|
||||
6cf08d4a2f0398856fd593f50bf077fd59b08230
|
||||
f78d3bfec32d07c1124eee8d0249477ce3fb0884
|
||||
204307fa50aaaa373946342084f7fd3af39f3cd3
|
||||
c50b01d098e5ab405a50c3e14e858da27d606e8f
|
||||
f71d1ac73e8290f37d55a67d6a6507a3653ec174
|
||||
ae2868c0e0b1ac8b5126fd43269383fa533d87b5
|
||||
be8def963956c605bca28bcd8df673bd7ec3740b
|
||||
4ac4d9d3ab97176d71e287bcdd6d41e66f2f7ccb
|
||||
fafabb8dab84e9460a076199ea646262e51c855b
|
||||
2f97d35bd8618e8c0cc006cb1d4a9b151c1b9b4a
|
||||
39d5a745acbbd3510de707d4e7c471c17e02ae59
|
||||
a1acdcdc4c002390a76f01699cfa006a36cf3f56
|
||||
ad54cf29ad1a4335ba208fb94a8fc5dfbba260e3
|
||||
c7649575e7b199794be4252f79da80aaecdcee28
|
||||
8913e2ce1f40d451ddb4527f08ec75f198d5063c
|
||||
1b9e310300f22bfc72364f027a9caeddadf61a99
|
||||
89d7dd46031511f0556b2d29ab34035f42e3a24c
|
||||
ca4fa0b9bf5ba707aa0447ba7c38fbacdadb7eec
|
||||
8cecfdf2f6dd5b0de507f79b469517cc0fb42add
|
||||
7eeb6afb3dbc56e52f9387a74f826b186cd19137
|
||||
d0f75eb371a96f8d5f174e23de074efd840e9e44
|
||||
f8a70ea9da8e6df2bf7a5f74cce45615fc292afe
|
||||
f28b9730061bffadb9d87ad63edf7d10b245d2c1
|
||||
afc5cf2140f22fea6bd6933dd0f9c302229a1980
|
||||
b75df78cdca58d5dca0c51fb8e106067aa6cb752
|
||||
28f5a6dbd4091aa3efa86eed6767eeb44a655f0f
|
||||
345e6a1ebd853858463a33953585ce407a60378c
|
||||
c45a4de47c601554f6b981d211181468b4798e41
|
||||
bbaa9a5432ff6ad5518344123c3b56f349347e99
|
||||
f804c90f96221f334371ccd01b0e6df7b1cfc1e8
|
||||
16ba867c5877d8c97968987ecb5f8bff966d0a82
|
||||
38a1474ca12aa6a796afc1e277882d997a999e14
|
||||
92c4078413fc79fcc83f5d3d8708abb58696ff1a
|
||||
d4415204e1eb3aed2a74a722aeaaa274975dd2d7
|
||||
e48bcf5f21f14acb27996fdc02231c140f5b817c
|
||||
22
.cz.toml
22
.cz.toml
@@ -32,17 +32,17 @@ schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)
|
||||
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" },
|
||||
{ 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"
|
||||
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@@ -1,11 +1,8 @@
|
||||
# See <https://help.github.com/articles/about-codeowners/>
|
||||
|
||||
# See https://help.github.com/articles/about-codeowners/
|
||||
# for more info about CODEOWNERS file
|
||||
|
||||
# It uses the same pattern rule for gitignore file
|
||||
|
||||
# <https://git-scm.com/docs/gitignore#_pattern_format>
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
|
||||
* @ratatui/maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
github: ratatui
|
||||
open_collective: ratatui
|
||||
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
@@ -13,43 +13,31 @@ A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
## Description
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
## To Reproduce
|
||||
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
## Expected behavior
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
## Screenshots
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
## Are you willing to contribute a fix?
|
||||
|
||||
<!--
|
||||
If you would like to work on a fix, check one of the boxes below. Maintainers can help point
|
||||
you to the right place in the codebase.
|
||||
-->
|
||||
|
||||
- [ ] I am willing to open a PR for this bug.
|
||||
- [ ] I can try to investigate, but I will need guidance.
|
||||
- [ ] I am not able to work on a fix right now.
|
||||
|
||||
## Environment
|
||||
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
- OS: Linux
|
||||
@@ -66,7 +54,6 @@ Add a description of the systems where you are observing the issue. For example:
|
||||
- Backend:
|
||||
|
||||
## Additional context
|
||||
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
If you already looked into the issue, include all the leads you have explored.
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,14 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frequently Asked Questions
|
||||
url: https://ratatui.rs/faq/
|
||||
about: Check the website FAQ section to see if your question has already been answered
|
||||
- name: Ratatui Forum
|
||||
url: https://forum.ratatui.rs
|
||||
about: Ask questions about ratatui on our Forum
|
||||
- name: Discord Chat
|
||||
url: https://discord.gg/pMCEU9hNEj
|
||||
about: Ask questions about ratatui on Discord
|
||||
- name: Matrix Chat
|
||||
url: https://matrix.to/#/#ratatui:matrix.org
|
||||
about: Ask questions about ratatui on Matrix
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,19 +2,17 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Type: Enhancement'
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Solution
|
||||
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
Things to consider:
|
||||
@@ -24,24 +22,11 @@ Things to consider:
|
||||
-->
|
||||
|
||||
## Alternatives
|
||||
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Are you willing to contribute an implementation?
|
||||
|
||||
<!--
|
||||
If you would like to work on this, check one of the boxes below. Maintainers can help refine
|
||||
the scope and discuss approach.
|
||||
-->
|
||||
|
||||
- [ ] I am willing to open a PR implementing this.
|
||||
- [ ] I can try to implement it, but I will need guidance.
|
||||
- [ ] I am not able to implement this right now.
|
||||
|
||||
## Additional context
|
||||
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
|
||||
81
.github/copilot-instructions.md
vendored
81
.github/copilot-instructions.md
vendored
@@ -1,81 +0,0 @@
|
||||
# GitHub Copilot Code Review Instructions
|
||||
|
||||
## General Review Principles
|
||||
|
||||
### Pull Request Size and Scope
|
||||
- **Flag large PRs**: Comment if a PR changes more than 500 lines or touches many unrelated areas
|
||||
- **Suggest splitting**: Recommend breaking large changes into smaller, focused PRs
|
||||
- **Question scope creep**: Ask about unrelated changes that seem outside the PR's stated purpose
|
||||
|
||||
### Code Quality and Style
|
||||
- **Check adherence to style guidelines**: Verify changes follow the project's Rust conventions in [CONTRIBUTING.md](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#code-formatting)
|
||||
- **Verify xtask compliance**: Ensure `cargo xtask format` and `cargo xtask lint` would pass
|
||||
- **Look for AI-generated patterns**: Be suspicious of verbose, overly-commented, or non-idiomatic code
|
||||
|
||||
### Architectural Considerations
|
||||
- **Reference ARCHITECTURE.md**: Point to [ARCHITECTURE.md](https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md) for changes affecting crate boundaries
|
||||
- **Question fundamental changes**: Flag modifications to core configuration, linting rules, or build setup without clear justification
|
||||
- **Verify appropriate crate placement**: Ensure changes are in the correct crate per the modular structure
|
||||
|
||||
### Breaking Changes and Deprecation
|
||||
- **Require deprecation**: Insist on deprecation warnings rather than immediate removal of public APIs
|
||||
- **Ask for migration path**: Request clear upgrade instructions for breaking changes
|
||||
- **Suggest feature flags**: Recommend feature flags for experimental or potentially disruptive changes
|
||||
- **Reference versioning policy**: Point to the requirement of at least one version notice before removal
|
||||
|
||||
### Testing and Documentation
|
||||
- **Verify test coverage**: Ensure new functionality includes appropriate tests
|
||||
- **Check for test removal**: Question any removal of existing tests without clear justification
|
||||
- **Require documentation**: Ensure public APIs are documented with examples
|
||||
- **Validate examples**: Check that code examples are minimal and follow project style
|
||||
|
||||
### Specific Areas of Concern
|
||||
|
||||
#### Configuration Changes
|
||||
- **Lint configuration**: Question changes to `.clippy.toml`, `rustfmt.toml`, or CI configuration
|
||||
- **Cargo.toml modifications**: Scrutinize dependency changes or workspace modifications
|
||||
- **Build system changes**: Require justification for xtask or build process modifications
|
||||
|
||||
#### Large Code Additions
|
||||
- **Question necessity**: Ask if large code additions could be implemented more simply
|
||||
- **Check for duplication**: Look for code that duplicates existing functionality
|
||||
- **Verify integration**: Ensure new code integrates well with existing patterns
|
||||
|
||||
#### File Organization
|
||||
- **Validate module structure**: Ensure new modules follow the project's organization
|
||||
- **Check import organization**: Verify imports follow the std/external/local grouping pattern
|
||||
- **Review file placement**: Confirm files are in appropriate locations per ARCHITECTURE.md
|
||||
|
||||
## Comment Templates
|
||||
|
||||
### For Large PRs
|
||||
```
|
||||
This PR seems quite large with changes across multiple areas. Consider splitting it into smaller, focused PRs:
|
||||
- Core functionality changes
|
||||
- Documentation updates
|
||||
- Test additions
|
||||
- Configuration changes
|
||||
|
||||
See our [contribution guidelines](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#keep-prs-small-intentional-and-focused) for more details.
|
||||
```
|
||||
|
||||
### For Breaking Changes
|
||||
```
|
||||
This appears to introduce breaking changes. Please consider:
|
||||
- Adding deprecation warnings instead of immediate removal
|
||||
- Providing a clear migration path in the PR description
|
||||
- Following our [deprecation policy](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#deprecation-notice)
|
||||
```
|
||||
|
||||
### For Configuration Changes
|
||||
```
|
||||
Changes to project configuration (linting, formatting, build) should be discussed first. Please explain:
|
||||
- Why this change is necessary
|
||||
- What problem it solves
|
||||
- Whether it affects contributor workflow
|
||||
```
|
||||
|
||||
### For Style Issues
|
||||
```
|
||||
Please run `cargo xtask format` and `cargo xtask lint` to ensure code follows our style guidelines. See [CONTRIBUTING.md](https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#code-formatting) for details.
|
||||
```
|
||||
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@@ -1,18 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for Cargo
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
80
.github/workflows/cd.yml
vendored
Normal file
80
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
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-nightly:
|
||||
name: Create a nightly release
|
||||
runs-on: ubuntu-latest
|
||||
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 describe --abbrev=0 --tags `git rev-list --tags --max-count=1`)"
|
||||
if [[ "${last_tag}" = *"${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
alpha=$(echo "${last_tag}" | grep -oE '([0-9]+)$')
|
||||
next_alpha=$((alpha + 1))
|
||||
next_tag=$(echo "${last_tag}" | sed "s/\.[0-9]\+$/\.${next_alpha}/")
|
||||
else
|
||||
# start the alpha version from 0
|
||||
next_tag="${last_tag}${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 nightly release: ${next_tag} 🐭"
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --dry-run --allow-dirty
|
||||
|
||||
- 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: --dry-run
|
||||
93
.github/workflows/check-pr.yml
vendored
93
.github/workflows/check-pr.yml
vendored
@@ -1,93 +0,0 @@
|
||||
name: Check Pull Requests
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
# this workflow is required to be run on pull_request_target as it modifies the PR comments
|
||||
# care should be taken that the jobs do not run any untrusted input
|
||||
# zizmor: ignore[dangerous-triggers]
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- labeled
|
||||
- unlabeled
|
||||
|
||||
jobs:
|
||||
check-title:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # 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@773744901bac0e8cbb5a0dc842800d45e9b2b405 # 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@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
check-breaking-change-label:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# use an environment variable to pass untrusted input to the script
|
||||
# see https://securitylab.github.com/research/github-actions-untrusted-input/
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
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 "${PR_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@v8
|
||||
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: ['Type: 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
|
||||
22
.github/workflows/check-semver.yml
vendored
22
.github/workflows/check-semver.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: Check Semver
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-semver:
|
||||
name: Check semver
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check semver
|
||||
uses: obi1kenobi/cargo-semver-checks-action@5b298c9520f7096a4683c0bd981a7ac5a7e249ae # v2
|
||||
329
.github/workflows/ci.yml
vendored
329
.github/workflows/ci.yml
vendored
@@ -1,8 +1,4 @@
|
||||
name: Continuous Integration
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
name: CI
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
@@ -13,6 +9,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
@@ -20,276 +17,136 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting,
|
||||
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
|
||||
# without having to wait for the comprehensive matrix of tests to complete.
|
||||
env:
|
||||
# don't install husky hooks during CI as they are only needed for for pre-push
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
|
||||
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
|
||||
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
# Lint the formatting of the codebase.
|
||||
lint-formatting:
|
||||
name: Check Formatting
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- 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:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Check conventional commits
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: HEAD
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: taplo-cli
|
||||
- run: cargo xtask format --check
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Check formatting
|
||||
run: cargo make fmt
|
||||
|
||||
# Check for typos in the codebase.
|
||||
# See <https://github.com/crate-ci/typos/>
|
||||
lint-typos:
|
||||
name: Check Typos
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: crate-ci/typos@bb4666ad77b539a6b4ce4eda7ebb6de553704021 # master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
checks:
|
||||
- advisories
|
||||
- bans licenses sources
|
||||
# Prevent sudden announcement of a new advisory from failing ci:
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
|
||||
with:
|
||||
rust-toolchain: stable
|
||||
log-level: info
|
||||
arguments: --all-features --exclude-unpublished
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
# Check for any unused dependencies in the codebase.
|
||||
# See <https://github.com/bnjbvr/cargo-machete/>
|
||||
cargo-machete:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: bnjbvr/cargo-machete@7959c845782fed02ee69303126d4a12d64f1db18 # v0.9.1
|
||||
|
||||
# Run cargo clippy.
|
||||
#
|
||||
# We check for clippy warnings on beta, but these are not hard failures. They should often be
|
||||
# fixed to prevent clippy failing on the next stable release, but don't block PRs on them unless
|
||||
# they are introduced by the PR.
|
||||
lint-clippy:
|
||||
name: Check Clippy
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["stable", "beta"]
|
||||
continue-on-error: ${{ matrix.toolchain == 'beta' }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask clippy
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Run cargo make clippy-all
|
||||
run: cargo make clippy
|
||||
|
||||
# Run markdownlint on all markdown files in the repository.
|
||||
lint-markdown:
|
||||
name: Check Markdown
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v21
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
'!target'
|
||||
|
||||
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
|
||||
# <https://app.codecov.io/gh/ratatui/ratatui>
|
||||
coverage:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
- name: Install cargo-llvm-cov and cargo-make
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
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
|
||||
|
||||
# Run cargo check. This is a fast way to catch any obvious errors in the code.
|
||||
check:
|
||||
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.86.0", "stable"]
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust {{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask check --all-features
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Run cargo make check
|
||||
run: cargo make check
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
build-no-std:
|
||||
name: Build No-Std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-none
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
# This makes it easier to debug the exact versions of the dependencies
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-core
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-widgets
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-macros
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui --no-default-features
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui-core
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui-widgets
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui-macros
|
||||
- run: cargo build --target x86_64-unknown-none -p ratatui --no-default-features
|
||||
|
||||
# Check if README.md is up-to-date with the crate's documentation.
|
||||
check-readme:
|
||||
name: Check README
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-rdme
|
||||
- run: cargo xtask readme --check
|
||||
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account the
|
||||
# package.metadata.docs.rs configured in Cargo.toml. https://github.com/dtolnay/cargo-docs-rs
|
||||
lint-docs:
|
||||
name: Check Docs
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/install@74f735cdf643820234e37ae1c4089a08fd266d8a # master
|
||||
with:
|
||||
crate: cargo-docs-rs
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask docs
|
||||
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
# compile, or any other issues in the documentation.
|
||||
test-docs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
# Run cargo test on the libraries of the crate.
|
||||
test-libs:
|
||||
name: Test Libs ${{ matrix.toolchain }}
|
||||
runs-on: ubuntu-latest
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["1.86.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask test-libs
|
||||
|
||||
# Run cargo test on all the backends.
|
||||
test-backends:
|
||||
name: Test ${{matrix.backend}} on ${{ matrix.os }}
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
backend: [ crossterm, termion, termwiz ]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust ${{ matrix.toolchain }}}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
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
|
||||
|
||||
71
.github/workflows/release-plz.yml
vendored
71
.github/workflows/release-plz.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: Release-plz
|
||||
|
||||
# Set the permissions of the github token to the minimum and only enable what is needed
|
||||
# See https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Release unpublished packages.
|
||||
release-plz-release:
|
||||
name: Release-plz release
|
||||
environment: release
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec # v1
|
||||
id: auth
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # v0.5
|
||||
with:
|
||||
command: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
|
||||
# Create a PR with the new versions and changelog, preparing the next release.
|
||||
release-plz-pr:
|
||||
name: Release-plz PR
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
concurrency:
|
||||
group: release-plz-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # v0.5
|
||||
with:
|
||||
command: release-pr
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||
26
.github/workflows/zizmor.yml
vendored
26
.github/workflows/zizmor.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
# docs https://docs.zizmor.sh/integrations/#github-actions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor 🌈
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
.idea/
|
||||
.env
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
# configuration for https://github.com/DavidAnson/markdownlint
|
||||
|
||||
first-line-heading: false
|
||||
no-inline-html:
|
||||
allowed_elements:
|
||||
- img
|
||||
- details
|
||||
- summary
|
||||
- div
|
||||
- br
|
||||
line-length:
|
||||
line_length: 100
|
||||
|
||||
# to support repeated headers in the changelog
|
||||
no-duplicate-heading: false
|
||||
|
||||
12
.taplo.toml
12
.taplo.toml
@@ -1,12 +0,0 @@
|
||||
include = ["**/*.toml"]
|
||||
|
||||
[formatting]
|
||||
column_width = 100
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies", "dev-dependencies", "build-dependencies", "workspace.dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = true
|
||||
203
ARCHITECTURE.md
203
ARCHITECTURE.md
@@ -1,203 +0,0 @@
|
||||
# Ratatui Architecture
|
||||
|
||||
This document provides a comprehensive overview of Ratatui's architecture and crate
|
||||
organization, introduced in version 0.30.0.
|
||||
|
||||
## Overview
|
||||
|
||||
Starting with Ratatui 0.30.0, the project was reorganized from a single monolithic crate into
|
||||
a modular workspace consisting of multiple specialized crates. This architectural decision was
|
||||
made to improve modularity, reduce compilation times, enable more flexible dependency
|
||||
management, and provide better API stability for third-party widget libraries.
|
||||
|
||||
## Crate Organization
|
||||
|
||||
The Ratatui project is now organized as a Cargo workspace containing the following crates:
|
||||
|
||||
### Core Crates
|
||||
|
||||
#### `ratatui` (Main Crate)
|
||||
|
||||
- **Purpose**: The main entry point that most applications should use
|
||||
- **Contents**: Re-exports everything from other crates for convenience, plus experimental features
|
||||
- **Target Users**: Application developers building terminal UIs
|
||||
- **Key Features**:
|
||||
- Complete widget ecosystem
|
||||
- Backend implementations
|
||||
- Layout system
|
||||
- Terminal management
|
||||
- Experimental `WidgetRef` and `StatefulWidgetRef` traits
|
||||
|
||||
#### `ratatui-core`
|
||||
|
||||
- **Purpose**: Foundational types and traits for the Ratatui ecosystem
|
||||
- **Contents**: Core widget traits, text types, buffer, layout, style, and symbols
|
||||
- **Target Users**: Widget library authors, minimalist projects
|
||||
- **Key Features**:
|
||||
- `Widget` and `StatefulWidget` traits
|
||||
- Text rendering (`Text`, `Line`, `Span`)
|
||||
- Buffer management
|
||||
- Layout system
|
||||
- Style and color definitions
|
||||
- Symbol collections
|
||||
|
||||
#### `ratatui-widgets`
|
||||
|
||||
- **Purpose**: Built-in widget implementations
|
||||
- **Contents**: All standard widgets like `Block`, `Paragraph`, `List`, `Chart`, etc.
|
||||
- **Target Users**: Applications needing standard widgets, widget library authors
|
||||
- **Key Features**:
|
||||
- Complete set of built-in widgets
|
||||
- Optimized implementations
|
||||
- Comprehensive documentation and examples
|
||||
|
||||
### Backend Crates
|
||||
|
||||
#### `ratatui-crossterm`
|
||||
|
||||
- **Purpose**: Crossterm backend implementation
|
||||
- **Contents**: Cross-platform terminal backend using the `crossterm` crate
|
||||
- **Target Users**: Applications targeting multiple platforms
|
||||
|
||||
#### `ratatui-termion`
|
||||
|
||||
- **Purpose**: Termion backend implementation
|
||||
- **Contents**: Unix-specific terminal backend using the `termion` crate
|
||||
- **Target Users**: Unix-specific applications requiring low-level control
|
||||
|
||||
#### `ratatui-termwiz`
|
||||
|
||||
- **Purpose**: Termwiz backend implementation
|
||||
- **Contents**: Terminal backend using the `termwiz` crate
|
||||
- **Target Users**: Applications needing advanced terminal features
|
||||
|
||||
### Utility Crates
|
||||
|
||||
#### `ratatui-macros`
|
||||
|
||||
- **Purpose**: Declarative macros for common patterns and boilerplate reduction
|
||||
- **Contents**: Macros for common patterns and boilerplate reduction
|
||||
- **Target Users**: Applications and libraries wanting macro support
|
||||
|
||||
## Dependency Relationships
|
||||
|
||||
```text
|
||||
ratatui
|
||||
├── ratatui-core
|
||||
├── ratatui-widgets → ratatui-core
|
||||
├── ratatui-crossterm → ratatui-core
|
||||
├── ratatui-termion → ratatui-core
|
||||
├── ratatui-termwiz → ratatui-core
|
||||
└── ratatui-macros
|
||||
```
|
||||
|
||||
### Key Dependencies
|
||||
|
||||
- **ratatui-core**: Foundation for all other crates
|
||||
- **ratatui-widgets**: Depends on `ratatui-core` for widget traits and types
|
||||
- **Backend crates**: Each depends on `ratatui-core` for backend traits and types
|
||||
- **ratatui**: Depends on all other crates and re-exports their public APIs
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Stability and Compatibility
|
||||
|
||||
The modular architecture provides different levels of API stability:
|
||||
|
||||
- **ratatui-core**: Designed for maximum stability to minimize breaking changes for widget
|
||||
libraries
|
||||
- **ratatui-widgets**: Focused on widget implementations with moderate stability requirements
|
||||
- **Backend crates**: Isolated from core changes, allowing backend-specific updates
|
||||
- **ratatui**: Main crate that can evolve more freely while maintaining backward compatibility
|
||||
through re-exports
|
||||
|
||||
### Compilation Performance
|
||||
|
||||
The split architecture enables:
|
||||
|
||||
- **Reduced compilation times**: Widget libraries only need to compile core types
|
||||
- **Parallel compilation**: Different crates can be compiled in parallel
|
||||
- **Selective compilation**: Applications can exclude unused backends or widgets
|
||||
|
||||
### Ecosystem Benefits
|
||||
|
||||
- **Widget Library Authors**: Can depend on stable `ratatui-core` without frequent updates
|
||||
- **Application Developers**: Can use the convenient `ratatui` crate with everything included
|
||||
- **Minimalist Projects**: Can use only `ratatui-core` for lightweight applications
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Application Developers
|
||||
|
||||
Most applications should continue using the main `ratatui` crate with minimal changes:
|
||||
|
||||
```rust
|
||||
// No changes needed - everything is re-exported
|
||||
use ratatui::{
|
||||
widgets::{Block, Paragraph},
|
||||
layout::{Layout, Constraint},
|
||||
Terminal,
|
||||
};
|
||||
```
|
||||
|
||||
### For Widget Library Authors
|
||||
|
||||
Consider migrating to `ratatui-core` for better stability:
|
||||
|
||||
```rust
|
||||
// Before (0.29.x and earlier)
|
||||
use ratatui::{
|
||||
widgets::{Widget, StatefulWidget},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
};
|
||||
|
||||
// After (0.30.0+)
|
||||
use ratatui_core::{
|
||||
widgets::{Widget, StatefulWidget},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
};
|
||||
```
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
All existing code using the `ratatui` crate will continue to work unchanged, as the main crate
|
||||
re-exports all public APIs from the specialized crates.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Potential Enhancements
|
||||
|
||||
- **Widget-specific crates**: Further split widgets into individual crates for even more
|
||||
granular dependencies
|
||||
- **Plugin system**: Enable dynamic widget loading and third-party widget ecosystems
|
||||
- **Feature flags**: More granular feature flags for compile-time customization
|
||||
|
||||
### Version Synchronization
|
||||
|
||||
Currently, all crates are versioned together for simplicity. Future versions may adopt
|
||||
independent versioning once the API stabilizes further.
|
||||
|
||||
## Related Issues and PRs
|
||||
|
||||
This architecture was developed through extensive discussion and implementation across multiple
|
||||
PRs:
|
||||
|
||||
- [Issue #1388](https://github.com/ratatui/ratatui/issues/1388): Original RFC for modularization
|
||||
- [PR #1459](https://github.com/ratatui/ratatui/pull/1459): Move ratatui crate into workspace
|
||||
folder
|
||||
- [PR #1460](https://github.com/ratatui/ratatui/pull/1460): Move core types to ratatui-core
|
||||
- [PR #1474](https://github.com/ratatui/ratatui/pull/1474): Move widgets into ratatui-widgets
|
||||
crate
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to the Ratatui project, please consider:
|
||||
|
||||
- **Core changes**: Submit PRs against `ratatui-core` for fundamental improvements
|
||||
- **Widget changes**: Submit PRs against `ratatui-widgets` for widget-specific improvements
|
||||
- **Backend changes**: Submit PRs against the appropriate backend crate
|
||||
- **Integration changes**: Submit PRs against the main `ratatui` crate
|
||||
|
||||
See the [CONTRIBUTING.md](CONTRIBUTING.md) guide for more details on the contribution process.
|
||||
1158
BREAKING-CHANGES.md
1158
BREAKING-CHANGES.md
File diff suppressed because it is too large
Load Diff
8480
CHANGELOG.md
8480
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
220
CONTRIBUTING.md
220
CONTRIBUTING.md
@@ -6,26 +6,9 @@ If your contribution is not straightforward, please first discuss the change you
|
||||
creating a new issue before making the change, or starting a discussion on
|
||||
[discord](https://discord.gg/pMCEU9hNEj).
|
||||
|
||||
## AI Generated Content
|
||||
|
||||
We welcome high quality PRs, whether they are human generated or made with the assistance of AI
|
||||
tools, but we ask that you follow these guidelines:
|
||||
|
||||
- **Attribution**: Tell us about your use of AI tools, don't make us guess whether you're using it.
|
||||
- **Review**: Make sure you review every line of AI generated content for correctness and relevance.
|
||||
- **Quality**: AI-generated content should meet the same quality standards as human-written content.
|
||||
- **Quantity**: Avoid submitting large amounts of AI generated content in a single PR. Remember that
|
||||
quality is usually more important than quantity.
|
||||
- **License**: Ensure that the AI-generated content is compatible with Ratatui's
|
||||
[License](./LICENSE).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Remember that AI tools can assist in generating content, but you are responsible for the final
|
||||
> quality and accuracy of the contributions and communicating about it.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
|
||||
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
|
||||
please check that it has not already been reported by searching for some related keywords. Please
|
||||
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
|
||||
found.
|
||||
@@ -34,54 +17,18 @@ found.
|
||||
|
||||
All contributions are obviously welcome. Please include as many details as possible in your PR
|
||||
description to help the reviewer (follow the provided template). Make sure to highlight changes
|
||||
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
|
||||
which may need additional attention or you are uncertain about. Any idea with a large scale impact
|
||||
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
### Keep PRs small, intentional, and focused
|
||||
### Keep PRs small, intentional and focused
|
||||
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponentially with the size
|
||||
of the change. Small focused PRs will generally be much faster to review. PRs that include both
|
||||
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.
|
||||
|
||||
Guidelines for PR size:
|
||||
|
||||
- Aim for PRs under 500 lines of changes when possible.
|
||||
- Split large features into incremental PRs that build on each other.
|
||||
- Separate refactoring, formatting, and functional changes into different PRs.
|
||||
- If a large PR is unavoidable, clearly explain why in the PR description.
|
||||
|
||||
### Breaking changes and backwards compatibility
|
||||
|
||||
We prioritize maintaining backwards compatibility and minimizing disruption to users:
|
||||
|
||||
- **Prefer deprecation over removal**: Add deprecation warnings rather than immediately removing
|
||||
public APIs
|
||||
- **Provide migration paths**: Include clear upgrade instructions for any breaking changes
|
||||
- **Follow our deprecation policy**: Wait at least two versions before removing deprecated items
|
||||
- **Consider feature flags**: Use feature flags for experimental or potentially disruptive changes
|
||||
- **Document breaking changes**: Clearly mark breaking changes in commit messages and PR descriptions
|
||||
|
||||
See our [deprecation notice policy](#deprecation-notice) for more details.
|
||||
|
||||
### Code formatting
|
||||
|
||||
Run `cargo xtask format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml). We use some unstable formatting
|
||||
options as they lead to subjectively better formatting. These require a nightly version of Rust
|
||||
to be installed when running rustfmt. You can install the nightly version of Rust using
|
||||
[`rustup`](https://rustup.rs/):
|
||||
|
||||
```shell
|
||||
rustup install nightly
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Do not modify formatting configuration (`rustfmt.toml`, `.clippy.toml`) without
|
||||
> prior discussion. These changes affect all contributors and should be carefully considered.
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
|
||||
@@ -102,20 +49,21 @@ describes the nature of the problem that the commit is solving and any unintuiti
|
||||
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
|
||||
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
|
||||
|
||||
Running `cargo xtask ci` before pushing will perform the same checks that we do in the CI process.
|
||||
It's not mandatory to do this before pushing, however it may save you time to do so instead of
|
||||
waiting for GitHub to run the checks.
|
||||
|
||||
Available xtask commands:
|
||||
|
||||
- `cargo xtask ci` - Run all CI checks
|
||||
- `cargo xtask format` - Format code
|
||||
- `cargo xtask lint` - Run linting checks
|
||||
- `cargo xtask test` - Run all tests
|
||||
|
||||
Run `cargo xtask --help` to see all available commands.
|
||||
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
|
||||
|
||||
@@ -124,64 +72,31 @@ they are signed. To set up your machine to sign commits, see [managing commit si
|
||||
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
|
||||
in GitHub docs.
|
||||
|
||||
### Configuration and build system changes
|
||||
|
||||
Changes to project configuration files require special consideration:
|
||||
|
||||
- Linting configuration (`.clippy.toml`, `rustfmt.toml`): Affects all contributors.
|
||||
- CI configuration (`.github/workflows/`): Affects build and deployment.
|
||||
- Build system (`xtask/`, `Cargo.toml` workspace config): Affects development workflow.
|
||||
- Dependencies: Consider MSRV compatibility and licensing.
|
||||
|
||||
Please discuss these changes in an issue before implementing them.
|
||||
|
||||
### Collaborative development
|
||||
|
||||
We may occasionally make changes directly to your branch—such as force-pushes—to help move a PR
|
||||
forward, speed up review, or ensure it meets our quality standards. If you would prefer we do not do
|
||||
this, or if your workflow depends on us avoiding force-pushes (for example, if your app points to
|
||||
your branch in `Cargo.toml`), please mention this in your PR description and we will respect your
|
||||
preference.
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Setup
|
||||
|
||||
TL;DR: Clone the repo and build it using `cargo xtask`.
|
||||
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-xtask](https://github.com/matklad/cargo-xtask). It wraps common `cargo` commands with sane
|
||||
[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 xtask build`.
|
||||
`cargo make build`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui/ratatui.git
|
||||
git clone https://github.com/ratatui-org/ratatui.git
|
||||
cd ratatui
|
||||
cargo xtask build
|
||||
cargo make build
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
For an understanding of the crate organization and design decisions, see [ARCHITECTURE.md]. This
|
||||
document explains the modular workspace structure introduced in version 0.30.0 and provides
|
||||
guidance on which crate to use for different use cases.
|
||||
|
||||
When making changes, consider:
|
||||
|
||||
- Which crate should contain your changes per the modular structure,
|
||||
- Whether your changes affect the public API of `ratatui-core` (requires extra care),
|
||||
- And how your changes fit into the overall architecture.
|
||||
|
||||
[ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
|
||||
|
||||
### Tests
|
||||
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
|
||||
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
|
||||
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 [ratatui/tests/widgets_block.rs](./ratatui/tests/widgets_block.rs) for an example.
|
||||
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.
|
||||
@@ -190,10 +105,6 @@ If an area that you're making a change in is not tested, write tests to characte
|
||||
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`).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Do not remove existing tests without clear justification. If tests need to be
|
||||
> modified due to API changes, explain why in your PR description.
|
||||
|
||||
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
|
||||
@@ -202,89 +113,22 @@ 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 [VS Code 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]``
|
||||
|
||||
### Deprecation notice
|
||||
|
||||
We generally want to wait at least two versions before removing deprecated items, so users have
|
||||
time to update. However, if a deprecation is blocking for us to implement a new feature we may
|
||||
*consider* removing it in a one version notice.
|
||||
|
||||
Deprecation process:
|
||||
|
||||
1. Add `#[deprecated]` attribute with a clear message.
|
||||
2. Update documentation to point to the replacement.
|
||||
3. Add an entry to `BREAKING-CHANGES.md` if applicable.
|
||||
4. Wait at least two versions before removal.
|
||||
5. Consider the impact on the ecosystem before removing.
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However, there
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
|
||||
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
|
||||
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision.
|
||||
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:
|
||||
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 xtask ci` which will offer you
|
||||
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.
|
||||
|
||||
## Relationship with `tui-rs`
|
||||
@@ -293,12 +137,12 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
|
||||
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
|
||||
([@fdehau](https://github.com/fdehau)).
|
||||
|
||||
The original repository contains all the issues, PRs, and discussion that were raised originally, and
|
||||
The original repository contains all the issues, PRs and discussion that were raised originally, and
|
||||
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
|
||||
|
||||
We imported all the PRs from the original repository, implemented many of the smaller ones, and
|
||||
We imported all the PRs from the original repository and implemented many of the smaller ones and
|
||||
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
|
||||
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
We have documented the current state of those PRs, and anyone is welcome to pick them up and
|
||||
continue the work on them.
|
||||
|
||||
|
||||
4681
Cargo.lock
generated
4681
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
263
Cargo.toml
263
Cargo.toml
@@ -1,123 +1,162 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*", "examples/concepts/*"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
# this is not included as it doesn't compile on windows
|
||||
# "ratatui-termion",
|
||||
"ratatui-macros",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"examples/apps/*",
|
||||
"examples/concepts/*",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.22.0" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library to build rich terminal user interfaces or dashboards"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
repository = "https://github.com/ratatui/ratatui"
|
||||
homepage = "https://ratatui.rs"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
categories = ["command-line-interface"]
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
edition = "2024"
|
||||
rust-version = "1.86.0"
|
||||
exclude = [
|
||||
"assets/*",
|
||||
".github",
|
||||
"Makefile.toml",
|
||||
"CONTRIBUTING.md",
|
||||
"*.log",
|
||||
"tags",
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anstyle = "1"
|
||||
bitflags = "2.10"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
color-eyre = "0.6"
|
||||
compact_str = { version = "0.9", default-features = false }
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
crossterm = "0.29"
|
||||
document-features = "0.2"
|
||||
fakeit = "1"
|
||||
futures = "0.3"
|
||||
hashbrown = "0.16"
|
||||
indoc = "2"
|
||||
instability = "0.3"
|
||||
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
|
||||
kasuari = { version = "0.4", default-features = false }
|
||||
line-clipping = "0.3"
|
||||
lru = "0.16"
|
||||
octocrab = "0.49"
|
||||
palette = "0.7"
|
||||
pretty_assertions = "1"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
ratatui = { path = "ratatui", version = "0.30.0" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0" }
|
||||
rstest = "0.26"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
strum = { version = "0.27", default-features = false, features = ["derive"] }
|
||||
termion = "4"
|
||||
termwiz = "0.23"
|
||||
thiserror = { version = "2", default-features = false }
|
||||
time = { version = "0.3.37", default-features = false }
|
||||
tokio = "1"
|
||||
tokio-stream = "0.1"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = "0.3"
|
||||
trybuild = "1"
|
||||
unicode-segmentation = "1"
|
||||
unicode-truncate = { version = "2", default-features = false }
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = ">=0.2.0, <=0.2.2"
|
||||
[badges]
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
all-widgets = ["widget-calendar"]
|
||||
widget-calendar = ["time"]
|
||||
macros = []
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
[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"]
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
crossterm = { version = "0.26", optional = true }
|
||||
indoc = "2.0"
|
||||
paste = "1.0.2"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
itertools = "0.10"
|
||||
rand = "0.8"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
implicit_clone = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
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 = "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 = "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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023-2025 The Ratatui Developers
|
||||
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
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Maintainers
|
||||
|
||||
This file documents current and past maintainers.
|
||||
|
||||
- [orhun](https://github.com/orhun)
|
||||
- [joshka](https://github.com/joshka)
|
||||
- [kdheepak](https://github.com/kdheepak)
|
||||
- [j-g00da](https://github.com/j-g00da)
|
||||
|
||||
## Past Maintainers
|
||||
|
||||
- [fdehau](https://github.com/fdehau)
|
||||
- [mindoodoo](https://github.com/mindoodoo)
|
||||
- [sayanarijit](https://github.com/sayanarijit)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
173
Makefile.toml
Normal file
173
Makefile.toml
Normal file
@@ -0,0 +1,173 @@
|
||||
# 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"
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = [
|
||||
"style-check",
|
||||
"clippy",
|
||||
"check",
|
||||
"test",
|
||||
]
|
||||
|
||||
[tasks.style-check]
|
||||
dependencies = ["fmt", "typos"]
|
||||
|
||||
[tasks.fmt]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.typos]
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--all-features"
|
||||
]
|
||||
|
||||
[tasks.check.windows]
|
||||
args = [
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.build.windows]
|
||||
args = [
|
||||
"build",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.clippy]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--all-features",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.clippy.windows]
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
dependencies = [
|
||||
"test-doc",
|
||||
]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
|
||||
[tasks.test-windows]
|
||||
dependencies = [
|
||||
"test-doc",
|
||||
]
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test", "--doc",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.test-doc.windows]
|
||||
args = [
|
||||
"test", "--doc",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},${@}"
|
||||
]
|
||||
|
||||
|
||||
[tasks.coverage]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.coverage.windows]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path", "target/lcov.info",
|
||||
"--no-default-features",
|
||||
"--features", "${ALL_FEATURES},crossterm,termwiz",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
|
||||
|
||||
[tasks.build-examples]
|
||||
command = "cargo"
|
||||
args = ["build", "--examples", "--release", "--features", "all-widgets"]
|
||||
|
||||
[tasks.run-examples]
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
365
README.md
365
README.md
@@ -1,182 +1,265 @@
|
||||
# Ratatui
|
||||
|
||||
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
|
||||
|
||||
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal user interfaces and
|
||||
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
project.
|
||||
|
||||
[](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)
|
||||
|
||||
<!-- See RELEASE.md for instructions on creating the demo gif --->
|
||||

|
||||
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [Quickstart](#quickstart)
|
||||
- [Documentation](#documentation)
|
||||
- [Templates](#templates)
|
||||
- [Built with Ratatui](#built-with-ratatui)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Contributing](#contributing)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [License](#license)
|
||||
* [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)
|
||||
|
||||
</details>
|
||||
|
||||

|
||||
## Installation
|
||||
|
||||
<div align="center">
|
||||
```shell
|
||||
cargo add ratatui --features all-widgets
|
||||
```
|
||||
|
||||
[![Crate Badge]][Crate] [![Repo Badge]][Repo] [![Docs Badge]][Docs] [![License Badge]][License] \
|
||||
[![CI Badge]][CI] [![Deps Badge]][Deps] [![Codecov Badge]][Codecov] [![Sponsors Badge]][Sponsors] \
|
||||
[Ratatui Website] · [Docs] · [Widget Examples] · [App Examples] · [Changelog] \
|
||||
[Breaking Changes] · [Contributing] · [Report a bug] · [Request a Feature]
|
||||
Or modify your `Cargo.toml`
|
||||
|
||||
</div>
|
||||
```toml
|
||||
[dependencies]
|
||||
ratatui = { version = "0.22.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
[Ratatui][Ratatui Website] (_ˌræ.təˈtu.i_) is a Rust crate for cooking up terminal user interfaces
|
||||
(TUIs). It provides a simple and flexible way to create text-based user interfaces in the terminal,
|
||||
which can be used for command-line applications, dashboards, and other interactive console programs.
|
||||
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
|
||||
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
|
||||
E.g.:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tui = { package = "ratatui", version = "0.22.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
|
||||
|
||||
Ratatui has [templates] available to help you get started quickly. You can use the
|
||||
[`cargo-generate`] command to create a new project with Ratatui:
|
||||
|
||||
```shell
|
||||
cargo install --locked cargo-generate
|
||||
cargo generate ratatui/templates
|
||||
```
|
||||
|
||||
Selecting the Hello World template produces the following application:
|
||||
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
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
run(&mut terminal)?;
|
||||
restore_terminal(&mut terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if matches!(event::read()?, Event::Key(_)) {
|
||||
break 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
frame.render_widget("hello world", frame.area());
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 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 are also plans to implement a [Matrix](https://matrix.org/)
|
||||
bridge in the near future. **Discord is not a MUST to contribute**. We follow a pretty standard
|
||||
github centered open source workflow keeping the most important conversations on GitHub, open an
|
||||
issue or PR and it will be addressed. 😄
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials.
|
||||
- [Ratatui Forum] - a place to ask questions and discuss the library.
|
||||
- [Widget Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [App Examples] - a collection of more complex examples that demonstrate how to build apps.
|
||||
- [ARCHITECTURE.md] - explains the crate organization and modular workspace structure.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
|
||||
|
||||
You can also watch the [EuroRust 2024 talk] to learn about common concepts in Ratatui and what's
|
||||
possible to build with it.
|
||||
## Examples
|
||||
|
||||
## Templates
|
||||
|
||||
If you're looking to get started quickly, you can use one of the available templates from the
|
||||
[templates] repository using [`cargo-generate`]:
|
||||
The demo shown in the gif above is available on all available backends.
|
||||
|
||||
```shell
|
||||
cargo generate ratatui/templates
|
||||
# crossterm
|
||||
cargo run --example demo
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion
|
||||
# termwiz
|
||||
cargo run --example demo --no-default-features --features=termwiz
|
||||
```
|
||||
|
||||
## Built with Ratatui
|
||||
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).
|
||||
|
||||
[][awesome-ratatui]
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
|
||||
want to run the demo without those symbols:
|
||||
|
||||
Check out the [showcase] section of the website, or the [awesome-ratatui] repository for a curated
|
||||
list of awesome apps and libraries built with Ratatui!
|
||||
```shell
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
More examples are available in the [examples](./examples/) folder.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
|
||||
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
|
||||
pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`tui::text::Text`
|
||||
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`tui::style::Color`
|
||||
* [rust-tui-template](https://github.com/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.
|
||||
|
||||
## Apps
|
||||
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
|
||||
|
||||
## Alternatives
|
||||
|
||||
- [Cursive](https://crates.io/crates/cursive) - a ncurses-based TUI library.
|
||||
- [iocraft](https://crates.io/crates/iocraft) - a declarative TUI library.
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Contributing
|
||||
## Contributors
|
||||
|
||||
[![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix] [![Forum Badge]][Ratatui Forum]
|
||||
[](https://github.com/ratatui-org/ratatui/graphs/contributors)
|
||||
|
||||
Feel free to join our [Discord server](https://discord.gg/pMCEU9hNEj) for discussions and questions!
|
||||
There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org). We have also recently launched the
|
||||
[Ratatui Forum].
|
||||
## Acknowledgments
|
||||
|
||||
We rely on GitHub for [bugs][Report a bug] and [feature requests][Request a Feature].
|
||||
|
||||
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines before [creating a pull
|
||||
request][Create a Pull Request]. We accept AI generated code, but please read the [AI Contributions]
|
||||
guidelines to ensure compliance.
|
||||
|
||||
If you'd like to show your support, you can add the Ratatui badge to your project's README:
|
||||
|
||||
```md
|
||||
[](https://ratatui.rs/)
|
||||
```
|
||||
|
||||
[](https://ratatui.rs/)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development. None of
|
||||
this could be possible without [Florian Dehau] who originally created [tui-rs] which inspired many
|
||||
Rust TUIs.
|
||||
|
||||
Special thanks to [Pavel Fomchenkov] for his work in designing an awesome logo for the Ratatui
|
||||
project and organization.
|
||||
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
|
||||
|
||||
This project is licensed under the [MIT License][License].
|
||||
|
||||
[Repo]: https://github.com/ratatui/ratatui
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Ratatui Forum]: https://forum.ratatui.rs
|
||||
[Docs]: https://docs.rs/ratatui
|
||||
[Widget Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui-widgets/examples
|
||||
[App Examples]: https://github.com/ratatui/ratatui/tree/main/examples
|
||||
[ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[EuroRust 2024 talk]: https://www.youtube.com/watch?v=hWG51Mc1DlM
|
||||
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||
[AI Contributions]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md#ai-generated-content
|
||||
[Crate]: https://crates.io/crates/ratatui
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[Sponsors]: https://github.com/sponsors/ratatui
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&color=E05D44
|
||||
[Repo Badge]: https://img.shields.io/badge/repo-ratatui/ratatui-1370D3?style=flat-square&logo=github
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
[Deps Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?path=ratatui&style=flat-square
|
||||
[Deps]: https://deps.rs/repo/github/ratatui/ratatui?path=ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/badge/docs-ratatui-1370D3?style=flat-square&logo=rust
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[showcase]: https://ratatui.rs/showcase/
|
||||
[awesome-ratatui]: https://github.com/ratatui/awesome-ratatui
|
||||
[Pavel Fomchenkov]: https://github.com/nawok
|
||||
[Florian Dehau]: https://github.com/fdehau
|
||||
[`cargo-generate`]: https://crates.io/crates/cargo-generate
|
||||
[License]: ./LICENSE
|
||||
[MIT](./LICENSE)
|
||||
|
||||
49
RELEASE.md
49
RELEASE.md
@@ -1,51 +1,30 @@
|
||||
# Creating a Release
|
||||
|
||||
Our release strategy is:
|
||||
|
||||
> Release major versions with detailed summaries when necessary, while releasing minor versions
|
||||
> weekly or as needed without extensive announcements.
|
||||
>
|
||||
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
|
||||
|
||||
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
|
||||
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
|
||||
1. Record a new demo gif if necessary. The preferred tool for this is
|
||||
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
|
||||
1. Record a new demo gif. The preferred tool for this is [ttyrec](http://0xcc.net/ttyrec/) and
|
||||
[ttygif](https://github.com/icholy/ttygif). [Asciinema](https://asciinema.org/) handles block
|
||||
character height poorly, [termanilizer](https://www.terminalizer.com/) takes forever to render,
|
||||
[vhs](https://github.com/charmbracelet/vhs) handles braille
|
||||
characters poorly (though if <https://github.com/charmbracelet/vhs/issues/322> is fixed, then
|
||||
it's probably the best option).
|
||||
|
||||
```shell
|
||||
cargo build --example demo2
|
||||
vhs examples/demo2.tape
|
||||
cargo build --example demo
|
||||
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
|
||||
ttygif demo.rec
|
||||
```
|
||||
|
||||
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
|
||||
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
|
||||
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
|
||||
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
|
||||
Then upload it somewhere (e.g. use `vhs publish tty.gif` to publish it or upload it to a GitHub
|
||||
wiki page as an attachment). Avoid adding the gif to the git repo as binary files tend to bloat
|
||||
repositories.
|
||||
|
||||
1. Bump the version in [Cargo.toml](Cargo.toml).
|
||||
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
|
||||
can be used for generating the entries.
|
||||
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
|
||||
1. Commit and push the changes.
|
||||
1. Create a new tag: `git tag -a v[0.x.y]`
|
||||
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/ratatui/actions) workflow to
|
||||
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
|
||||
finish.
|
||||
|
||||
## Alpha Releases
|
||||
|
||||
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
|
||||
and can be manually created when necessary by triggering the [Continuous
|
||||
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
|
||||
|
||||
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
|
||||
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
|
||||
0.22.1-alpha.1.
|
||||
|
||||
These releases will have whatever happened to be in main at the time of release, so they're useful
|
||||
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
|
||||
tested than normal releases.
|
||||
|
||||
See [#147](https://github.com/ratatui/ratatui/issues/147) and
|
||||
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only support the latest version of this crate.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1 +0,0 @@
|
||||
<svg role="img" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><title>Ratatui</title><path d="M17 29h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1h1v1H5v-1H4v-1H3v-1H2v-1H1v-1H0v-2h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h2zm-2 14h1v1h1v1h1v-2h-1v-1h-1v-1h-1zm0-6h-3v1h1v1h4v-4h-1v-1h-1zm35-21h-1v4h-1v1h-1v1h-1v1h-2v1h-1v1h-1v1h-1v1h-2v9h5v1h1v9h-1v1h-1v2h-1v1h-1v-1h-1v-1h-1v-1h1v-1h1v-1h1v-5h-1v1h-3v1h-1v3h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-2h1v-2h1v-1h-2v1h-4v-1h-1v-1h-1v-1h-1v-2h1v-1h1v-1h2v-1h7v-1h1v-1h1v-1h1v-1h1v-1h1v-1h4v-1h3v-1h10zm-17 2h-1v2h1v1h2v-1h1v-2h-1v-1h-2zM29 1h1v9h1v1h1v2h1v2h-1v1h-1v1h-1v1h-1v1h-1v1h-3v-1h-1v-1h-2v-1h-1v-1h-1v-1h-6v-1h-1V9h1V8h1V7h1V6h1V5h1V4h1V3h1V2h1V1h2V0h6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 831 B |
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg role="img" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><title>Ratatui</title><path d="M50 16h-1v4h-1v1h-1v1h-1v1h-2v1h-1v1h-1v1h-1v1h-2v9h5v1h1v9h-1v1h-1v2h-1v1h-1v-1h-1v1h-2v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-1v-1h-2v1h-1v1h-1v1h-1v1h-1v1h-1v1h-1v1H9v1H8v1H7v1H6v1H5v1H4v1H3v1H2v2h1v1h1v1h1v1h1v1h1v1H5v-1H4v-1H3v-1H2v-1H1v-1H0v-2h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h1v-1h2v1h1v-1h-1v-1h-1v-2h1v-1h1v-1h2v-1h7v-1h1v-1h1v-1h1v-1h1v-1h1v-1h4v-1h3v-1h10zM39 49h1v-1h-1zm2-8h-3v1h-1v3h-1v-1h-1v1h1v1h1v1h1v1h1v-1h1v-1h1v-1h1v-5h-1zm-7 3h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-1-1h1v-1h-1zm-5-5h1v1h1v1h1v1h1v1h1v-2h1v-2h1v-1h-2v1h-4v-1h-1zm14-11h-1v2h1v1h2v-1h1v-2h-1v-1h-2zM18 40h1v1h1v2h-1v-1h-1v-1h-1v-2h1zm0-7h1v4h-4v-1h-1v-1h3v-3h1zM29 1h1v9h1v1h1v2h1v2h-1v1h-1v1h-1v1h-1v1h-1v1h-3v-1h-1v-1h-2v-1h-1v-1h-1v-1h-6v-1h-1V9h1V8h1V7h1V6h1V5h1V4h1V3h1V2h1V1h2V0h6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
98
bacon.toml
98
bacon.toml
@@ -8,87 +8,93 @@
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "xtask", "check"]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "xtask", "check", "--all-features"]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "xtask", "check-backend", "crossterm"]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "xtask", "check-backend", "termion"]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "xtask", "check-backend", "termwiz"]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy-all]
|
||||
command = ["cargo", "xtask", "clippy"]
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = ["cargo", "xtask", "test"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = ["cargo", "xtask", "test-libs"]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "xtask", "docs"]
|
||||
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", "xtask", "docs", "--open"]
|
||||
on_success = "job:doc"
|
||||
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", "xtask", "coverage"]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = ["cargo", "xtask", "coverage", "--lib"]
|
||||
|
||||
[jobs.hack]
|
||||
command = ["cargo", "xtask", "hack"]
|
||||
|
||||
[jobs.format]
|
||||
command = ["cargo", "xtask", "format"]
|
||||
|
||||
[jobs.zizmor-offline]
|
||||
# zizmor checks the workflow files for security issues. The offline version is generally faster, but
|
||||
# checks for fewer issues.
|
||||
command = ["zizmor", "--color", "always", ".github/workflows", "--offline"]
|
||||
need_stdout = true
|
||||
default_watch = false
|
||||
watch = [".github/workflows/"]
|
||||
|
||||
[jobs.zizmor-online]
|
||||
# zizmor checks the workflow files for security issues. The online version is a bit slower, but it
|
||||
# checks for more issues
|
||||
command = ["zizmor", "--color", "always", ".github/workflows"]
|
||||
need_stdout = true
|
||||
default_watch = false
|
||||
watch = [".github/workflows/"]
|
||||
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]
|
||||
ctrl-h = "job:hack"
|
||||
# alt-m = "job:my-job"
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
v = "job:coverage"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
f = "job:format"
|
||||
z = "job:zizmor-offline"
|
||||
shift-z = "job:zizmor-online"
|
||||
u = "job:coverage-unit-tests-only"
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{BatchSize, Bencher, BenchmarkId, Criterion, criterion_group};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::{Paragraph, Widget, Wrap};
|
||||
use criterion::{black_box, criterion_group, criterion_main, 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;
|
||||
const PARAGRAPH_DEFAULT_HEIGHT: u16 = 50;
|
||||
|
||||
/// 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.
|
||||
fn paragraph(c: &mut Criterion) {
|
||||
pub fn paragraph(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("paragraph");
|
||||
for line_count in [64, 2048, u16::MAX] {
|
||||
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
|
||||
let lines = random_lines(line_count);
|
||||
let lines = lines.as_str();
|
||||
let y_scroll = line_count - PARAGRAPH_DEFAULT_HEIGHT;
|
||||
|
||||
// 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)));
|
||||
b.iter(|| Paragraph::new(black_box(lines)))
|
||||
});
|
||||
|
||||
// render the paragraph with no scroll
|
||||
@@ -37,14 +36,14 @@ fn paragraph(c: &mut Criterion) {
|
||||
// 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((y_scroll / 2, 0)),
|
||||
&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((y_scroll, 0)),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
@@ -60,25 +59,19 @@ fn paragraph(c: &mut Criterion) {
|
||||
BenchmarkId::new("render_wrap_scroll_full", line_count),
|
||||
&Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((y_scroll, 0)),
|
||||
.scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Render the paragraph into a buffer with the given width.
|
||||
/// 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, PARAGRAPH_DEFAULT_HEIGHT));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
|| paragraph.to_owned(),
|
||||
|bench_paragraph| {
|
||||
bench_paragraph.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
|
||||
bencher.iter(|| {
|
||||
paragraph.clone().render(buffer.area, &mut buffer);
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a string with the given number of lines filled with nonsense words
|
||||
@@ -86,10 +79,11 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
/// English language has about 5.1 average characters per word so including the space between words
|
||||
/// this should emit around 200 characters per paragraph on average.
|
||||
fn random_lines(count: u16) -> String {
|
||||
let count = i64::from(count);
|
||||
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);
|
||||
186
cliff.toml
186
cliff.toml
@@ -1,106 +1,41 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "ratatui"
|
||||
repo = "ratatui"
|
||||
# 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.
|
||||
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# 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.
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{%- if not version %}
|
||||
## [unreleased]
|
||||
{% else -%}
|
||||
{%- if package -%} {# release-plz specific variable #}
|
||||
## {{ package }} - [{{ version }}]({{ release_link }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- else -%}
|
||||
## [{{ version }}]({{ self::remote_url() }}/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- endif %}
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.remote.username %} by `@{{ commit.remote.username }}`{%- endif -%}\
|
||||
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}\n
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
>
|
||||
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{{- footer.separator }}
|
||||
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% if version %}\
|
||||
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% 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 %}
|
||||
|
||||
{%- if not release_link -%}
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
{%- if release_link -%}
|
||||
**Full Changelog**: {{ release_link }} {# release-plz specific variable #}
|
||||
{%- else -%}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
{%- if remote.owner -%} {# release-plz specific variable #}
|
||||
https://github.com/{{ remote.owner }}/{{ remote.repo }}\
|
||||
{%- else -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = false
|
||||
# postprocessors for the changelog body
|
||||
postprocessors = [
|
||||
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
|
||||
{ pattern = '>---+\n', replace = '' },
|
||||
{ pattern = ' +\n', replace = "\n" },
|
||||
]
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
@@ -111,51 +46,30 @@ filter_unconventional = true
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
# Typos that squeaked through and which would otherwise trigger the typos linter.
|
||||
# Regex obsfucation is to avoid triggering the linter in this file until there's a per file config
|
||||
# See https://github.com/crate-ci/typos/issues/724
|
||||
{ pattern = '\<[d]eatil\>', replace = "detail" },
|
||||
{ pattern = '\<[f]eatuers\>', replace = "features" },
|
||||
{ pattern = '\<[s]pecically\>', replace = "specially" },
|
||||
{ pattern = '\<[g]ague\>', replace = "gauge" },
|
||||
{ pattern = '\<[a]rithmentic\>', replace = "arithmetic" },
|
||||
{ pattern = '\<[i]ntructions\>', replace = "instructions" },
|
||||
{ pattern = '\<[i]mplementated\>', replace = "implemented" },
|
||||
{ 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 = [
|
||||
# release-plz adds 000000 as a placeholder for release commits
|
||||
{ field = "id", pattern = "0000000", skip = true },
|
||||
{ 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: release", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^build\\(deps\\)", skip = true },
|
||||
{ message = "^build\\(release\\)", skip = true },
|
||||
{ message = "^build", group = "<!-- 08 -->Build" },
|
||||
{ body = ".*security", group = "<!-- 09 -->Security" },
|
||||
{ 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" },
|
||||
{ message = "^feat", group = "<!-- 00 -->Features" },
|
||||
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
]
|
||||
|
||||
# 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
|
||||
@@ -163,9 +77,9 @@ filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "beta|alpha|v0.1.0-rc.1"
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = "rc"
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
avoid-breaking-exported-api = false
|
||||
14
codecov.yml
14
codecov.yml
@@ -1,14 +0,0 @@
|
||||
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
|
||||
precision: 1 # e.g. 89.1%
|
||||
round: down
|
||||
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
|
||||
status: # https://docs.codecov.com/docs/commit-status
|
||||
project:
|
||||
default:
|
||||
threshold: 1% # Avoid false negatives
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
comment: # https://docs.codecov.com/docs/pull-request-comments
|
||||
# make the comments less noisy
|
||||
require_changes: true
|
||||
35
deny.toml
35
deny.toml
@@ -1,22 +1,23 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
default = "deny"
|
||||
unlicensed = "deny"
|
||||
copyleft = "deny"
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
version = 2
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
@@ -25,15 +26,3 @@ multiple-versions = "allow"
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "ring"
|
||||
# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses
|
||||
# https://spdx.org/licenses/OpenSSL.html
|
||||
# ISC - Both BoringSSL and ring use this for their new files
|
||||
# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT
|
||||
# license, for third_party/fiat, which, unlike other third_party directories, is
|
||||
# compiled into non-test libraries, is included below."
|
||||
# OpenSSL - Obviously
|
||||
expression = "ISC AND MIT AND OpenSSL"
|
||||
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
|
||||
|
||||
@@ -1,275 +1,209 @@
|
||||
# Examples
|
||||
|
||||
This folder contains examples that are more application focused.
|
||||
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
|
||||
|
||||
> [!TIP]
|
||||
> There are also [widget examples] in `ratatui-widgets`.
|
||||
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.
|
||||
|
||||
[widget examples]: ../ratatui-widgets/examples
|
||||
|
||||
You can run these examples using:
|
||||
## Barchart ([barchart.rs](./barchart.rs)
|
||||
|
||||
```shell
|
||||
cargo run -p example-name
|
||||
cargo run --example=barchart --features=crossterm
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||
> of the `main` branch for code which is guaranteed to work with the released Ratatui version.
|
||||
![Barchart][barchart.gif]
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> There may be backwards incompatible changes in these examples, as they are designed to compile
|
||||
> against the `main` branch.
|
||||
>
|
||||
> There are a few workaround for this problem:
|
||||
>
|
||||
> - View the examples as they were when the latest version was release by selecting the tag that
|
||||
> matches that version. E.g. <https://github.com/ratatui/ratatui/tree/v0.26.1/examples>.
|
||||
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
|
||||
> allows you to select any previous tagged version.
|
||||
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
|
||||
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
|
||||
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
|
||||
> the dependency, or remotely by adding `git = "https://github.com/ratatui/ratatui"`
|
||||
>
|
||||
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
|
||||
>
|
||||
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
|
||||
> `git-cliff -u` against a cloned version of this repository.
|
||||
## Block ([block.rs](./block.rs))
|
||||
|
||||
## Design choices
|
||||
```shell
|
||||
cargo run --example=block --features=crossterm
|
||||
```
|
||||
|
||||
The examples contain some opinionated choices in order to make it easier for newer rustaceans to
|
||||
easily be productive in creating applications:
|
||||
![Block][block.gif]
|
||||
|
||||
- Each example has an `App` struct, with methods that implement a main loop, handle events and drawing
|
||||
the UI.
|
||||
- We use `color_eyre` for handling errors and panics. See [How to use color-eyre with Ratatui] on the
|
||||
website for more information about this.
|
||||
- Common code is not extracted into a separate file. This makes each example self-contained and easy
|
||||
to read as a whole.
|
||||
## Calendar ([calendar.rs](./calendar.rs))
|
||||
|
||||
[How to use color-eyre with Ratatui]: https://ratatui.rs/recipes/apps/color-eyre/
|
||||
```shell
|
||||
cargo run --example=calendar --features=crossterm widget-calendar
|
||||
```
|
||||
|
||||
## Demo
|
||||
![Calendar][calendar.gif]
|
||||
|
||||
This is the original demo example from the main README. It is available for each of the backends.
|
||||
[Source](./apps/demo/).
|
||||
## Canvas ([canvas.rs](./canvas.rs))
|
||||
|
||||

|
||||
```shell
|
||||
cargo run --example=canvas --features=crossterm
|
||||
```
|
||||
|
||||
## Demo2
|
||||
![Canvas][canvas.gif]
|
||||
|
||||
This is the demo example from the main README and crate page. [Source](./apps/demo2/).
|
||||
## Chart ([chart.rs](./chart.rs))
|
||||
|
||||

|
||||
```shell
|
||||
cargo run --example=chart --features=crossterm
|
||||
```
|
||||
|
||||
## Async GitHub
|
||||
![Chart][chart.gif]
|
||||
|
||||
Shows how to fetch data from GitHub API asynchronously. [Source](./apps/async-github/).
|
||||
## Custom Widget ([custom_widget.rs](./custom_widget.rs))
|
||||
|
||||
![Async GitHub demo][async-github.gif]
|
||||
```shell
|
||||
cargo run --example=custom_widget --features=crossterm
|
||||
```
|
||||
|
||||
## Calendar Explorer
|
||||
This is not a particularly exciting example visually, but it demonstrates how to implement your own widget.
|
||||
|
||||
Shows how to render a calendar with different styles. [Source](./apps/calendar-explorer/).
|
||||
![Custom Widget][custom_widget.gif]
|
||||
|
||||
![Calendar explorer demo][calendar-explorer.gif]
|
||||
## Gauge ([gauge.rs](./gauge.rs))
|
||||
|
||||
## Canvas
|
||||
Please note: the background renders poorly when we generate this example using VHS.
|
||||
This problem doesn't generally happen during normal rendering in a terminal.
|
||||
See <https://github.com/charmbracelet/vhs/issues/344> for more details
|
||||
|
||||
Shows how to render a canvas with different shapes. [Source](./apps/canvas/).
|
||||
```shell
|
||||
cargo run --example=gauge --features=crossterm
|
||||
```
|
||||
|
||||
![Canvas demo][canvas.gif]
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Chart
|
||||
## Hello World ([hello_world.rs](./hello_world.rs))
|
||||
|
||||
Shows how to render line, bar, and scatter charts. [Source](./apps/chart/).
|
||||
```shell
|
||||
cargo run --example=hello_world --features=crossterm
|
||||
```
|
||||
|
||||
![Chart demo][chart.gif]
|
||||
This is a pretty boring example, but it contains some good comments of documentation on some of the
|
||||
standard approaches to writing tui apps.
|
||||
|
||||
## Color Explorer
|
||||
![Hello World][hello_world.gif]
|
||||
|
||||
Shows how to handle the supported colors. [Source](./apps/color-explorer/).
|
||||
## Inline ([inline.rs](./inline.rs))
|
||||
|
||||
![Color explorer demo][color-explorer.gif]
|
||||
```shell
|
||||
cargo run --example=inline --features=crossterm
|
||||
```
|
||||
|
||||
## Colors-RGB demo
|
||||
![Inline][inline.gif]
|
||||
|
||||
Shows the full range of RGB colors in an animation. [Source](./apps/colors-rgb/).
|
||||
## Layout ([layout.rs](./layout.rs))
|
||||
|
||||
![Colors-RGB demo][colors-rgb.gif]
|
||||
```shell
|
||||
cargo run --example=layout --features=crossterm
|
||||
```
|
||||
|
||||
## Constraint Explorer
|
||||
![Layout][layout.gif]
|
||||
|
||||
Shows how different constraints can be used to layout widgets. [Source](./apps/constraint-explorer/).
|
||||
## List ([list.rs](./list.rs))
|
||||
|
||||
![Constraint Explorer demo][constraint-explorer.gif]
|
||||
```shell
|
||||
cargo run --example=list --features=crossterm
|
||||
```
|
||||
|
||||
## Constraints
|
||||
![List][list.gif]
|
||||
|
||||
Shows different types of constraints. [Source](./apps/constraints/).
|
||||
## Panic ([panic.rs](./panic.rs))
|
||||
|
||||
![Constraints demo][constraints.gif]
|
||||
```shell
|
||||
cargo run --example=panic --features=crossterm
|
||||
```
|
||||
|
||||
## Custom Widget
|
||||
![Panic][panic.gif]
|
||||
|
||||
Shows how to create a custom widget that can be interacted with the mouse. [Source](./apps/custom-widget/).
|
||||
## Paragraph ([paragraph.rs](./paragraph.rs))
|
||||
|
||||
![Custom widget demo][custom-widget.gif]
|
||||
```shell
|
||||
cargo run --example=paragraph --features=crossterm
|
||||
```
|
||||
|
||||
## Hyperlink
|
||||
![Paragraph][paragraph.gif]
|
||||
|
||||
Shows how to render hyperlinks in a terminal using [OSC
|
||||
8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). [Source](./apps/hyperlink/).
|
||||
## Popup ([popup.rs](./popup.rs))
|
||||
|
||||
![Hyperlink demo][hyperlink.gif]
|
||||
```shell
|
||||
cargo run --example=popup --features=crossterm
|
||||
```
|
||||
|
||||
## Flex
|
||||
Please note: the background renders poorly when we generate this example using VHS.
|
||||
This problem doesn't generally happen during normal rendering in a terminal.
|
||||
See <https://github.com/charmbracelet/vhs/issues/344> for more details
|
||||
|
||||
Shows how to use the flex layouts. [Source](./apps/flex/).
|
||||
![Popup][popup.gif]
|
||||
|
||||
![Flex demo][flex.gif]
|
||||
## Scrollbar ([scrollbar.rs](./scrollbar.rs))
|
||||
|
||||
## Hello World
|
||||
```shell
|
||||
cargo run --example=scrollbar --features=crossterm
|
||||
```
|
||||
|
||||
Shows how to create a simple TUI with a text. [Source](./apps/hello-world/).
|
||||
![Scrollbar][scrollbar.gif]
|
||||
|
||||
![Hello World demo][hello-world.gif]
|
||||
## Sparkline ([sparkline.rs](./sparkline.rs))
|
||||
|
||||
## Gauge
|
||||
```shell
|
||||
cargo run --example=sparkline --features=crossterm
|
||||
```
|
||||
|
||||
Shows different types of gauges. [Source](./apps/gauge/).
|
||||
![Sparkline][sparkline.gif]
|
||||
|
||||
## Inline
|
||||
## Table ([table.rs](./table.rs))
|
||||
|
||||
Shows how to use the inlined viewport to render in a specific area of the screen. [Source](./apps/inline/).
|
||||
```shell
|
||||
cargo run --example=table --features=crossterm
|
||||
```
|
||||
|
||||
![Inline demo][inline.gif]
|
||||
![Table][table.gif]
|
||||
|
||||
## Input Form
|
||||
## Tabs ([tabs.rs](./tabs.rs))
|
||||
|
||||
Shows how to render a form with input fields. [Source](./apps/input-form/).
|
||||
```shell
|
||||
cargo run --example=tabs --features=crossterm
|
||||
```
|
||||
|
||||
## Modifiers
|
||||
![Tabs][tabs.gif]
|
||||
|
||||
Shows different types of modifiers. [Source](./apps/modifiers/).
|
||||
## User Input ([user_input.rs](./user_input.rs))
|
||||
|
||||
![Modifiers demo][modifiers.gif]
|
||||
```shell
|
||||
cargo run --example=user_input --features=crossterm
|
||||
```
|
||||
|
||||
## Mouse Drawing
|
||||
|
||||
Shows how to handle mouse events. [Source](./apps/mouse-drawing/).
|
||||
|
||||
## Minimal
|
||||
|
||||
Shows how to create a minimal application. [Source](./apps/minimal/).
|
||||
|
||||
![Minimal demo][minimal.gif]
|
||||
|
||||
## Panic
|
||||
|
||||
Shows how to handle panics. [Source](./apps/panic/).
|
||||
|
||||
![Panic demo][panic.gif]
|
||||
|
||||
## Popup
|
||||
|
||||
Shows how to handle popups. [Source](./apps/popup/).
|
||||
|
||||
![Popup demo][popup.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
Shows how to render different types of scrollbars. [Source](./apps/scrollbar/).
|
||||
|
||||
![Scrollbar demo][scrollbar.gif]
|
||||
|
||||
## Table
|
||||
|
||||
Shows how to create an interactive table. [Source](./apps/table/).
|
||||
|
||||
![Table demo][table.gif]
|
||||
|
||||
## Todo List
|
||||
|
||||
Shows how to create a simple todo list application. [Source](./apps/todo-list/).
|
||||
|
||||
![Todo List demo][todo-list.gif]
|
||||
|
||||
## Tracing
|
||||
|
||||
Shows how to use the [tracing](https://crates.io/crates/tracing) crate to log to a file. [Source](./apps/tracing/).
|
||||
|
||||
![Tracing demo][tracing.gif]
|
||||
|
||||
## User Input
|
||||
|
||||
Shows how to handle user input. [Source](./apps/user-input/).
|
||||
|
||||
![User input demo][user-input.gif]
|
||||
|
||||
## Weather
|
||||
|
||||
Shows how to render weather data using barchart widget. [Source](./apps/weather/).
|
||||
|
||||
## WidgetRef Container
|
||||
|
||||
Shows how to use [`WidgetRef`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.WidgetRef.html) to store widgets in a container. [Source](./apps/widget-ref-container/).
|
||||
|
||||
## Advanced Widget Implementation
|
||||
|
||||
Shows how to render the `Widget` trait in different ways.
|
||||
|
||||
![Advanced widget impl demo][advanced-widget-impl.gif]
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>How to update these examples?</summary>
|
||||
|
||||
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||
images themselves are stored in a separate `images` git branch to avoid bloating the `main`
|
||||
branch.
|
||||
![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`
|
||||
|
||||
Links to images to make them easier to update in bulk. Use the following script to update and upload
|
||||
the examples to the images branch. (Requires push access to the branch).
|
||||
|
||||
To update these examples in bulk:
|
||||
```shell
|
||||
vhs/generate.bash
|
||||
# build to ensure that running the examples doesn't have to wait so long
|
||||
cargo build --examples --features=crossterm,all-widgets
|
||||
for i in examples/*.tape
|
||||
do
|
||||
echo -n "[${i:s:examples/:::s:.tape:.gif:}]: "
|
||||
vhs $i --publish --quiet
|
||||
# may need to adjust this depending on if you see rate limiting from VHS
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
-->
|
||||
|
||||
</details>
|
||||
|
||||
[advanced-widget-impl.gif]: https://github.com/ratatui/ratatui/blob/images/examples/advanced-widget-impl.gif?raw=true
|
||||
[async-github.gif]: https://github.com/ratatui/ratatui/blob/images/examples/async-github.gif?raw=true
|
||||
[calendar-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/calendar-explorer.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[color-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/color-explorer.gif?raw=true
|
||||
[colors-rgb.gif]: https://github.com/ratatui/ratatui/blob/images/examples/colors-rgb.gif?raw=true
|
||||
[constraint-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
|
||||
[constraints.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraints.gif?raw=true
|
||||
[custom-widget.gif]: https://github.com/ratatui/ratatui/blob/images/examples/custom-widget.gif?raw=true
|
||||
[demo2-destroy.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2-destroy.gif?raw=true
|
||||
[demo2-social.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2-social.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[flex.gif]: https://github.com/ratatui/ratatui/blob/images/examples/flex.gif?raw=true
|
||||
[hello-world.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hello-world.gif?raw=true
|
||||
[hyperlink.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hyperlink.gif?raw=true
|
||||
[inline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[minimal.gif]: https://github.com/ratatui/ratatui/blob/images/examples/minimal.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[panic.gif]: https://github.com/ratatui/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[popup.gif]: https://github.com/ratatui/ratatui/blob/images/examples/popup.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||
[table.gif]: https://github.com/ratatui/ratatui/blob/images/examples/table.gif?raw=true
|
||||
[todo-list.gif]: https://github.com/ratatui/ratatui/blob/images/examples/todo-list.gif?raw=true
|
||||
[tracing.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tracing.gif?raw=true
|
||||
[user-input.gif]: https://github.com/ratatui/ratatui/blob/images/examples/user-input.gif?raw=true
|
||||
[barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif
|
||||
[block.gif]: https://vhs.charm.sh/vhs-1sEo9vVkHRwFtu95MOXrTj.gif
|
||||
[calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif
|
||||
[canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif
|
||||
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
|
||||
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
|
||||
[gauge.gif]: https://vhs.charm.sh/vhs-2rvSeP5r4lRkGTzNCKpm9a.gif
|
||||
[hello_world.gif]: https://vhs.charm.sh/vhs-3CKUwxFuQi8oKQMS5zkPfQ.gif
|
||||
[inline.gif]: https://vhs.charm.sh/vhs-miRl1mosKFoJV7LjjvF4T.gif
|
||||
[layout.gif]: https://vhs.charm.sh/vhs-5R8O3LQGQ5pQVWwlPVrdbQ.gif
|
||||
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
|
||||
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
|
||||
[paragraph.gif]: https://vhs.charm.sh/vhs-2qIPDi79DUmtmeNDEeHVEF.gif
|
||||
[popup.gif]: https://vhs.charm.sh/vhs-2QnC682AUeNYNXcjNlKTyp.gif
|
||||
[scrollbar.gif]: https://vhs.charm.sh/vhs-2p13MMFreW7Gwt1xIonIWu.gif
|
||||
[sparkline.gif]: https://vhs.charm.sh/vhs-4t59Vxw5Za33Rtvt9QrftA.gif
|
||||
[table.gif]: https://vhs.charm.sh/vhs-6IrGHgT385DqA6xnwGF9oD.gif
|
||||
[tabs.gif]: https://vhs.charm.sh/vhs-61WkbfhyDk0kbkjncErdHT.gif
|
||||
[user_input.gif]: https://vhs.charm.sh/vhs-4fxUgkpEWcVyBRXuyYKODY.gif
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "advanced-widget-impl"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui = { workspace = true, features = ["unstable-widget-ref"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Advanced Widget Implementation demo
|
||||
|
||||
This example shows how to render the `Widget` trait in different ways.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p advanced-widget-impl
|
||||
```
|
||||
@@ -1,241 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to implement the `Widget` trait.
|
||||
///
|
||||
/// This example demonstrates various ways to implement `Widget` traits in Ratatui on a type, a
|
||||
/// reference, and a mutable reference. It also shows how to use the `WidgetRef` trait to
|
||||
/// render boxed widgets.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Position, Rect, Size};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Widget, WidgetRef};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
timer: Timer,
|
||||
boxed_squares: BoxedSquares,
|
||||
green_square: RightAlignedSquare,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while !self.should_quit {
|
||||
self.render(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
|
||||
tui.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
// Handle events at least 50 frames per second (gifs are usually 50fps)
|
||||
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
if event::read()?.is_key_press() {
|
||||
self.should_quit = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `Widget` trait on a mutable reference to the `App` type.
|
||||
///
|
||||
/// This allows the `App` type to be rendered as a widget. The `App` type owns several other widgets
|
||||
/// that are rendered as part of the app. The `Widget` trait is implemented on a mutable reference
|
||||
/// to the `App` type, which allows this to be rendered without consuming the `App` type, and allows
|
||||
/// the sub-widgets to be mutable.
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = Constraint::from_lengths([1, 1, 2, 1]);
|
||||
let [greeting, timer, squares, position] = area.layout(&Layout::vertical(constraints));
|
||||
|
||||
// render an ephemeral greeting widget
|
||||
Greeting::new("Ratatui!").render(greeting, buf);
|
||||
|
||||
// render a reference to the timer widget
|
||||
self.timer.render(timer, buf);
|
||||
|
||||
// render a boxed widget containing red and blue squares
|
||||
self.boxed_squares.render(squares, buf);
|
||||
|
||||
// render a mutable reference to the green square widget
|
||||
self.green_square.render(squares, buf);
|
||||
// Display the dynamically updated position of the green square
|
||||
let square_position = format!("Green square is at {}", self.green_square.last_position);
|
||||
square_position.render(position, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// An ephemeral greeting widget.
|
||||
///
|
||||
/// This widget is implemented on the type itself, which means that it is consumed when it is
|
||||
/// rendered. This is useful for widgets that are cheap to create, don't need to be reused, and
|
||||
/// don't need to store any state between renders. This is the simplest way to implement a widget in
|
||||
/// Ratatui, but in most cases, it is better to implement the `Widget` trait on a reference to the
|
||||
/// type, as shown in the other examples below.
|
||||
///
|
||||
/// This was the way most widgets were implemented in Ratatui before `Widget` was implemented on
|
||||
/// references in [PR #903] (merged in Ratatui 0.26.0).
|
||||
///
|
||||
/// [PR #903]: https://github.com/ratatui/ratatui/pull/903
|
||||
struct Greeting {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Greeting {
|
||||
fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let greeting = format!("Hello, {}!", self.name);
|
||||
greeting.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A timer widget that displays the elapsed time since the timer was started.
|
||||
#[derive(Debug)]
|
||||
struct Timer {
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl Default for Timer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This implements `Widget` on a reference to the type, which means that it can be reused and
|
||||
/// doesn't need to be consumed when it is rendered. This is useful for widgets that need to store
|
||||
/// state and be updated over time.
|
||||
///
|
||||
/// This approach was probably always available in Ratatui, but it wasn't widely used until `Widget`
|
||||
/// was implemented on references in [PR #903] (merged in Ratatui 0.26.0). This is because all the
|
||||
/// built-in widgets previously would consume themselves when rendered.
|
||||
impl Widget for &Timer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let elapsed = self.start.elapsed().as_secs_f32();
|
||||
let message = format!("Elapsed: {elapsed:.1?}s");
|
||||
message.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that contains a list of several different widgets.
|
||||
struct BoxedSquares {
|
||||
squares: Vec<Box<dyn WidgetRef>>,
|
||||
}
|
||||
|
||||
impl Default for BoxedSquares {
|
||||
fn default() -> Self {
|
||||
let red_square: Box<dyn WidgetRef> = Box::new(RedSquare);
|
||||
let blue_square: Box<dyn WidgetRef> = Box::new(BlueSquare);
|
||||
Self {
|
||||
squares: vec![red_square, blue_square],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that renders a red square.
|
||||
struct RedSquare;
|
||||
|
||||
/// A widget that renders a blue square.
|
||||
struct BlueSquare;
|
||||
|
||||
/// This implements the `Widget` trait on a reference to the type. It contains a list of boxed
|
||||
/// widgets that implement the `WidgetRef` trait. This is useful for widgets that contain a list of
|
||||
/// other widgets that can be different types.
|
||||
impl Widget for &BoxedSquares {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = vec![Constraint::Length(4); self.squares.len()];
|
||||
let areas = area.layout_vec(&Layout::horizontal(constraints));
|
||||
for (widget, area) in self.squares.iter().zip(areas) {
|
||||
widget.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `RedSquare` and `BlueSquare` are widgets that render a red and blue square, respectively. They
|
||||
/// implement the `WidgetRef` trait instead of the `Widget` trait, which which allows them to be
|
||||
/// rendered as boxed widgets. It's not possible to use Widget for this as a dynamic reference to a
|
||||
/// widget cannot generally be moved out of the box.
|
||||
impl WidgetRef for RedSquare {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
fill(area, buf, "█", Color::Red);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for BlueSquare {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
fill(area, buf, "█", Color::Blue);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that renders a green square aligned to the right of the area.
|
||||
#[derive(Default)]
|
||||
struct RightAlignedSquare {
|
||||
last_position: Position,
|
||||
}
|
||||
|
||||
/// This widget is implemented on a mutable reference to the type, which means that it can store
|
||||
/// state and update it when it is rendered. This is useful for widgets that need to store the
|
||||
/// result of some calculation that can only be done when the widget is rendered.
|
||||
///
|
||||
/// The x and y coordinates of the square are stored in the widget and updated when the widget is
|
||||
/// rendered. This allows the square to be aligned to the right of the area. These coordinates could
|
||||
/// be used to perform hit testing (e.g. checking if a mouse click is inside the square). This app
|
||||
/// just displays the coordinates as a string.
|
||||
///
|
||||
/// This approach was probably always available in Ratatui, but it wasn't widely used either. This
|
||||
/// is an alternative to implementing the `StatefulWidget` trait, for situations where you want to
|
||||
/// store the state in the widget itself instead of a separate struct.
|
||||
impl Widget for &mut RightAlignedSquare {
|
||||
/// Render a green square aligned to the right of the area and store the position.
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
const WIDTH: u16 = 4;
|
||||
let x = area.right() - WIDTH; // Align to the right
|
||||
self.last_position = Position { x, y: area.y };
|
||||
let size = Size::new(WIDTH, area.height);
|
||||
let area = Rect::from((self.last_position, size));
|
||||
fill(area, buf, "█", Color::Green);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill the area with the specified symbol and style.
|
||||
///
|
||||
/// This probably should be a method on the `Buffer` type, but it is defined here for simplicity.
|
||||
/// <https://github.com/ratatui/ratatui/issues/1146>
|
||||
fn fill<S: Into<Style>>(area: Rect, buf: &mut Buffer, symbol: &str, style: S) {
|
||||
let style = style.into();
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
buf[(x, y)].set_symbol(symbol).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "async-github"
|
||||
publish = false
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
octocrab.workspace = true
|
||||
ratatui.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream.workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Async GitHub demo
|
||||
|
||||
This example demonstrates how to use Ratatui with widgets that fetch data from GitHub API asynchronously.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p async-github
|
||||
```
|
||||
@@ -1,236 +0,0 @@
|
||||
//! # [Ratatui] Async example
|
||||
//!
|
||||
//! This example demonstrates how to use Ratatui with widgets that fetch data asynchronously. It
|
||||
//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API.
|
||||
//!
|
||||
//! <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token>
|
||||
//! <https://github.com/settings/tokens/new> to create a new token (select classic, and no scopes)
|
||||
//!
|
||||
//! This example does not cover message passing between threads, it only demonstrates how to manage
|
||||
//! shared state between the main thread and a background task, which acts mostly as a one-shot
|
||||
//! fetcher. For more complex scenarios, you may need to use channels or other synchronization
|
||||
//! primitives.
|
||||
//!
|
||||
//! A simple app might have multiple widgets that fetch data from different sources, and each widget
|
||||
//! would have its own background task to fetch the data. The main thread would then render the
|
||||
//! widgets with the latest data.
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{Event, EventStream, KeyCode};
|
||||
use octocrab::Page;
|
||||
use octocrab::params::Direction;
|
||||
use octocrab::params::pulls::Sort;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal).await;
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
pull_requests: PullRequestListWidget,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const FRAMES_PER_SECOND: f32 = 60.0;
|
||||
|
||||
pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
self.pull_requests.run();
|
||||
|
||||
let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND);
|
||||
let mut interval = tokio::time::interval(period);
|
||||
let mut events = EventStream::new();
|
||||
|
||||
while !self.should_quit {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => { terminal.draw(|frame| self.render(frame))?; },
|
||||
Some(Ok(event)) = events.next() => self.handle_event(&event),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
let [title_area, body_area] = frame.area().layout(&layout);
|
||||
let title = Line::from("Ratatui async example").centered().bold();
|
||||
frame.render_widget(title, title_area);
|
||||
frame.render_widget(&self.pull_requests, body_area);
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &Event) {
|
||||
if let Some(key) = event.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||
KeyCode::Char('j') | KeyCode::Down => self.pull_requests.scroll_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.pull_requests.scroll_up(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that displays a list of pull requests.
|
||||
///
|
||||
/// This is an async widget that fetches the list of pull requests from the GitHub API. It contains
|
||||
/// an inner `Arc<RwLock<PullRequestListState>>` that holds the state of the widget. Cloning the
|
||||
/// widget will clone the Arc, so you can pass it around to other threads, and this is used to spawn
|
||||
/// a background task to fetch the pull requests.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct PullRequestListWidget {
|
||||
state: Arc<RwLock<PullRequestListState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PullRequestListState {
|
||||
pull_requests: Vec<PullRequest>,
|
||||
loading_state: LoadingState,
|
||||
table_state: TableState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PullRequest {
|
||||
id: String,
|
||||
title: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
enum LoadingState {
|
||||
#[default]
|
||||
Idle,
|
||||
Loading,
|
||||
Loaded,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl PullRequestListWidget {
|
||||
/// Start fetching the pull requests in the background.
|
||||
///
|
||||
/// This method spawns a background task that fetches the pull requests from the GitHub API.
|
||||
/// The result of the fetch is then passed to the `on_load` or `on_err` methods.
|
||||
fn run(&self) {
|
||||
let this = self.clone(); // clone the widget to pass to the background task
|
||||
tokio::spawn(this.fetch_pulls());
|
||||
}
|
||||
|
||||
async fn fetch_pulls(self) {
|
||||
// this runs once, but you could also run this in a loop, using a channel that accepts
|
||||
// messages to refresh on demand, or with an interval timer to refresh every N seconds
|
||||
self.set_loading_state(LoadingState::Loading);
|
||||
match octocrab::instance()
|
||||
.pulls("ratatui", "ratatui")
|
||||
.list()
|
||||
.sort(Sort::Updated)
|
||||
.direction(Direction::Descending)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(page) => self.on_load(&page),
|
||||
Err(err) => self.on_err(&err),
|
||||
}
|
||||
}
|
||||
fn on_load(&self, page: &Page<OctoPullRequest>) {
|
||||
let prs = page.items.iter().map(Into::into);
|
||||
let mut state = self.state.write().unwrap();
|
||||
state.loading_state = LoadingState::Loaded;
|
||||
state.pull_requests.extend(prs);
|
||||
if !state.pull_requests.is_empty() {
|
||||
state.table_state.select(Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_err(&self, err: &octocrab::Error) {
|
||||
self.set_loading_state(LoadingState::Error(err.to_string()));
|
||||
}
|
||||
|
||||
fn set_loading_state(&self, state: LoadingState) {
|
||||
self.state.write().unwrap().loading_state = state;
|
||||
}
|
||||
|
||||
fn scroll_down(&self) {
|
||||
self.state.write().unwrap().table_state.scroll_down_by(1);
|
||||
}
|
||||
|
||||
fn scroll_up(&self) {
|
||||
self.state.write().unwrap().table_state.scroll_up_by(1);
|
||||
}
|
||||
}
|
||||
|
||||
type OctoPullRequest = octocrab::models::pulls::PullRequest;
|
||||
|
||||
impl From<&OctoPullRequest> for PullRequest {
|
||||
fn from(pr: &OctoPullRequest) -> Self {
|
||||
Self {
|
||||
id: pr.number.to_string(),
|
||||
title: pr.title.as_ref().unwrap().to_string(),
|
||||
url: pr
|
||||
.html_url
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &PullRequestListWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = self.state.write().unwrap();
|
||||
|
||||
// a block with a right aligned title with the loading state on the right
|
||||
let loading_state = Line::from(format!("{:?}", state.loading_state)).right_aligned();
|
||||
let block = Block::bordered()
|
||||
.title("Pull Requests")
|
||||
.title(loading_state)
|
||||
.title_bottom("j/k to scroll, q to quit");
|
||||
|
||||
// a table with the list of pull requests
|
||||
let rows = state.pull_requests.iter();
|
||||
let widths = [
|
||||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Max(49),
|
||||
];
|
||||
let table = Table::new(rows, widths)
|
||||
.block(block)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_symbol(">>")
|
||||
.row_highlight_style(Style::new().on_blue());
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut state.table_state);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PullRequest> for Row<'_> {
|
||||
fn from(pr: &PullRequest) -> Self {
|
||||
let pr = pr.clone();
|
||||
Row::new(vec![pr.id, pr.title, pr.url])
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "calendar-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
time = { workspace = true, features = ["formatting", "parsing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Calendar explorer demo
|
||||
|
||||
This example shows how to render a calendar with different styles.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p calendar-explorer
|
||||
```
|
||||
@@ -1,236 +0,0 @@
|
||||
//! A Ratatui example that demonstrates how to render calendar with different styles.
|
||||
//!
|
||||
//! Marks the holidays and seasons on the calendar.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::layout::{Constraint, Layout, Margin, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::{Line, Text};
|
||||
use ratatui::widgets::calendar::{CalendarEventStore, Monthly};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use time::ext::NumericalDuration;
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(run)
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
let mut selected_date = OffsetDateTime::now_local()?.date();
|
||||
let mut calendar_style = StyledCalendar::Default;
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, calendar_style, selected_date))?;
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break Ok(()),
|
||||
KeyCode::Char('s') => calendar_style = calendar_style.next(),
|
||||
KeyCode::Char('n') | KeyCode::Tab => selected_date = next_month(selected_date),
|
||||
KeyCode::Char('p') | KeyCode::BackTab => selected_date = prev_month(selected_date),
|
||||
KeyCode::Char('h') | KeyCode::Left => selected_date -= 1.days(),
|
||||
KeyCode::Char('j') | KeyCode::Down => selected_date += 1.weeks(),
|
||||
KeyCode::Char('k') | KeyCode::Up => selected_date -= 1.weeks(),
|
||||
KeyCode::Char('l') | KeyCode::Right => selected_date += 1.days(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_month(date: Date) -> Date {
|
||||
if date.month() == Month::December {
|
||||
date.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_year(date.year() + 1)
|
||||
.unwrap()
|
||||
} else {
|
||||
date.replace_month(date.month().next()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_month(date: Date) -> Date {
|
||||
if date.month() == Month::January {
|
||||
date.replace_month(Month::December)
|
||||
.unwrap()
|
||||
.replace_year(date.year() - 1)
|
||||
.unwrap()
|
||||
} else {
|
||||
date.replace_month(date.month().previous()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the UI with a calendar.
|
||||
fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date) {
|
||||
let header = Text::from_iter([
|
||||
Line::from("Calendar Example".bold()),
|
||||
Line::from(
|
||||
"<q> Quit | <s> Change Style | <n> Next Month | <p> Previous Month, <hjkl> Move",
|
||||
),
|
||||
Line::from(format!(
|
||||
"Current date: {selected_date} | Current style: {calendar_style}"
|
||||
)),
|
||||
]);
|
||||
|
||||
let [text_area, area] = frame.area().layout(&Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Fill(1),
|
||||
]));
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
calendar_style
|
||||
.render_year(frame, area, selected_date)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StyledCalendar {
|
||||
Default,
|
||||
Surrounding,
|
||||
WeekdaysHeader,
|
||||
SurroundingAndWeekdaysHeader,
|
||||
MonthHeader,
|
||||
MonthAndWeekdaysHeader,
|
||||
}
|
||||
|
||||
impl StyledCalendar {
|
||||
// Cycle through the different styles.
|
||||
const fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Default => Self::Surrounding,
|
||||
Self::Surrounding => Self::WeekdaysHeader,
|
||||
Self::WeekdaysHeader => Self::SurroundingAndWeekdaysHeader,
|
||||
Self::SurroundingAndWeekdaysHeader => Self::MonthHeader,
|
||||
Self::MonthHeader => Self::MonthAndWeekdaysHeader,
|
||||
Self::MonthAndWeekdaysHeader => Self::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StyledCalendar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Default => write!(f, "Default"),
|
||||
Self::Surrounding => write!(f, "Show Surrounding"),
|
||||
Self::WeekdaysHeader => write!(f, "Show Weekdays Header"),
|
||||
Self::SurroundingAndWeekdaysHeader => write!(f, "Show Surrounding and Weekdays Header"),
|
||||
Self::MonthHeader => write!(f, "Show Month Header"),
|
||||
Self::MonthAndWeekdaysHeader => write!(f, "Show Month Header and Weekdays Header"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledCalendar {
|
||||
fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> {
|
||||
let events = events(date)?;
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Ratio(1, 3); 3]);
|
||||
let horizontal = &Layout::horizontal([Constraint::Ratio(1, 4); 4]);
|
||||
let areas = area
|
||||
.inner(Margin::new(1, 1))
|
||||
.layout_vec(&vertical)
|
||||
.into_iter()
|
||||
.flat_map(|row| row.layout_vec(horizontal));
|
||||
for (i, area) in areas.enumerate() {
|
||||
let month = date
|
||||
.replace_day(1)
|
||||
.unwrap()
|
||||
.replace_month(Month::try_from(i as u8 + 1).unwrap())
|
||||
.unwrap();
|
||||
self.render_month(frame, area, month, &events);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_month(self, frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) {
|
||||
let calendar = match self {
|
||||
Self::Default => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default()),
|
||||
Self::Surrounding => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::new().dim()),
|
||||
Self::WeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::new().bold().green()),
|
||||
Self::SurroundingAndWeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::new().dim())
|
||||
.show_weekdays_header(Style::new().bold().green()),
|
||||
Self::MonthHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_month_header(Style::new().bold().green()),
|
||||
Self::MonthAndWeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::new().bold().dim().light_yellow()),
|
||||
};
|
||||
frame.render_widget(calendar, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a list of dates for the current year.
|
||||
fn events(selected_date: Date) -> Result<CalendarEventStore> {
|
||||
const SELECTED: Style = Style::new()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
const HOLIDAY: Style = Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
const SEASON: Style = Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
let y = selected_date.year();
|
||||
|
||||
// new year's
|
||||
list.add(Date::from_calendar_date(y, Month::January, 1)?, HOLIDAY);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(Date::from_calendar_date(y + 1, Month::January, 1)?, HOLIDAY);
|
||||
// groundhog day
|
||||
list.add(Date::from_calendar_date(y, Month::February, 2)?, HOLIDAY);
|
||||
// april fool's
|
||||
list.add(Date::from_calendar_date(y, Month::April, 1)?, HOLIDAY);
|
||||
// earth day
|
||||
list.add(Date::from_calendar_date(y, Month::April, 22)?, HOLIDAY);
|
||||
// star wars day
|
||||
list.add(Date::from_calendar_date(y, Month::May, 4)?, HOLIDAY);
|
||||
// festivus
|
||||
list.add(Date::from_calendar_date(y, Month::December, 23)?, HOLIDAY);
|
||||
// new year's eve
|
||||
list.add(Date::from_calendar_date(y, Month::December, 31)?, HOLIDAY);
|
||||
|
||||
// seasons
|
||||
// spring equinox
|
||||
list.add(Date::from_calendar_date(y, Month::March, 22)?, SEASON);
|
||||
// summer solstice
|
||||
list.add(Date::from_calendar_date(y, Month::June, 21)?, SEASON);
|
||||
// fall equinox
|
||||
list.add(Date::from_calendar_date(y, Month::September, 22)?, SEASON);
|
||||
// winter solstice
|
||||
list.add(Date::from_calendar_date(y, Month::December, 21)?, SEASON);
|
||||
|
||||
// selected date
|
||||
list.add(selected_date, SELECTED);
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "canvas"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Canvas demo
|
||||
|
||||
This example shows how to render various shapes and a map on a canvas.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p canvas
|
||||
```
|
||||
@@ -1,263 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to draw on a canvas.
|
||||
///
|
||||
/// This example demonstrates how to draw various shapes such as rectangles, circles, and lines
|
||||
/// on a canvas. It also demonstrates how to draw a map.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::{
|
||||
io::stdout,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::ExecutableCommand;
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEventKind,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::layout::{Constraint, Layout, Position, Rect};
|
||||
use ratatui::style::{Color, Stylize};
|
||||
use ratatui::symbols::Marker;
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle};
|
||||
use ratatui::widgets::{Block, Widget};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
stdout().execute(EnableMouseCapture)?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
stdout().execute(DisableMouseCapture)?;
|
||||
app_result
|
||||
}
|
||||
|
||||
struct App {
|
||||
exit: bool,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Circle,
|
||||
playground: Rect,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
marker: Marker,
|
||||
points: Vec<Position>,
|
||||
is_drawing: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Circle {
|
||||
x: 20.0,
|
||||
y: 40.0,
|
||||
radius: 10.0,
|
||||
color: Color::Yellow,
|
||||
},
|
||||
playground: Rect::new(10, 10, 200, 100),
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
marker: Marker::Dot,
|
||||
points: vec![],
|
||||
is_drawing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
let mut last_tick = Instant::now();
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
self.on_tick();
|
||||
last_tick = Instant::now();
|
||||
continue;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) => self.handle_key_event(key),
|
||||
Event::Mouse(event) => self.handle_mouse_event(event),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) {
|
||||
if !key.is_press() {
|
||||
return;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.exit = true,
|
||||
KeyCode::Char('j') | KeyCode::Down => self.y += 1.0,
|
||||
KeyCode::Char('k') | KeyCode::Up => self.y -= 1.0,
|
||||
KeyCode::Char('l') | KeyCode::Right => self.x += 1.0,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.x -= 1.0,
|
||||
KeyCode::Enter => self.cycle_marker(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
|
||||
match event.kind {
|
||||
MouseEventKind::Down(_) => self.is_drawing = true,
|
||||
MouseEventKind::Up(_) => self.is_drawing = false,
|
||||
MouseEventKind::Drag(_) => {
|
||||
self.points.push(Position::new(event.column, event.row));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
const fn cycle_marker(&mut self) {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Quadrant,
|
||||
Marker::Quadrant => Marker::Sextant,
|
||||
Marker::Sextant => Marker::Octant,
|
||||
Marker::Octant => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
let playground = self.playground;
|
||||
if ball.x - ball.radius < f64::from(playground.left())
|
||||
|| ball.x + ball.radius > f64::from(playground.right())
|
||||
{
|
||||
self.vx = -self.vx;
|
||||
}
|
||||
if ball.y - ball.radius < f64::from(playground.top())
|
||||
|| ball.y + ball.radius > f64::from(playground.bottom())
|
||||
{
|
||||
self.vy = -self.vy;
|
||||
}
|
||||
self.ball.x += self.vx;
|
||||
self.ball.y += self.vy;
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let header = Text::from_iter([
|
||||
"Canvas Example".bold(),
|
||||
"<q> Quit | <enter> Change Marker | <hjkl> Move".into(),
|
||||
]);
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
]);
|
||||
let [text_area, up, down] = frame.area().layout(&vertical);
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
|
||||
let horizontal = Layout::horizontal([Constraint::Fill(1); 2]);
|
||||
let [draw, pong] = up.layout(&horizontal);
|
||||
let [map, boxes] = down.layout(&horizontal);
|
||||
|
||||
frame.render_widget(self.map_canvas(), map);
|
||||
frame.render_widget(self.draw_canvas(draw), draw);
|
||||
frame.render_widget(self.pong_canvas(), pong);
|
||||
frame.render_widget(self.boxes_canvas(boxes), boxes);
|
||||
}
|
||||
|
||||
fn map_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("World"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::Green,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(self.x, -self.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
}
|
||||
|
||||
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Draw here"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([0.0, f64::from(area.width)])
|
||||
.y_bounds([0.0, f64::from(area.height)])
|
||||
.paint(move |ctx| {
|
||||
let points = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
f64::from(p.x) - f64::from(area.left()),
|
||||
f64::from(area.bottom()) - f64::from(p.y),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
ctx.draw(&Points {
|
||||
coords: &points,
|
||||
color: Color::White,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Pong"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&self.ball);
|
||||
})
|
||||
.x_bounds([10.0, 210.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
}
|
||||
|
||||
fn boxes_canvas(&self, area: Rect) -> impl Widget {
|
||||
let left = 0.0;
|
||||
let right = f64::from(area.width);
|
||||
let bottom = 0.0;
|
||||
let top = f64::from(area.height).mul_add(2.0, -4.0);
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Rects"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([left, right])
|
||||
.y_bounds([bottom, top])
|
||||
.paint(|ctx| {
|
||||
for i in 0..=11 {
|
||||
ctx.draw(&Rectangle {
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 2.0,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Red,
|
||||
});
|
||||
ctx.draw(&Rectangle {
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 21.0,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Blue,
|
||||
});
|
||||
}
|
||||
for i in 0..100 {
|
||||
if i % 10 != 0 {
|
||||
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
}
|
||||
if i % 2 == 0 && i % 10 != 0 {
|
||||
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "chart"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Chart demo
|
||||
|
||||
This example shows how to render line, bar, and scatter charts.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p chart
|
||||
```
|
||||
@@ -1,352 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to handle charts.
|
||||
///
|
||||
/// This example demonstrates how to draw various types of charts such as line, bar, and
|
||||
/// scatter charts.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::symbols::{self, Marker};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
signal2: SinSignal,
|
||||
data2: Vec<(f64, f64)>,
|
||||
window: [f64; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
Self {
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
data2,
|
||||
window: [0.0, 20.0],
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
self.on_tick();
|
||||
last_tick = Instant::now();
|
||||
continue;
|
||||
}
|
||||
if event::read()?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| key.code == KeyCode::Char('q'))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.data1.drain(0..5);
|
||||
self.data1.extend(self.signal1.by_ref().take(5));
|
||||
|
||||
self.data2.drain(0..10);
|
||||
self.data2.extend(self.signal2.by_ref().take(10));
|
||||
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Fill(1); 2]);
|
||||
let [top, bottom] = frame.area().layout(&vertical);
|
||||
let horizontal = Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]);
|
||||
let [animated_chart, bar_chart] = top.layout(&horizontal);
|
||||
let [line_chart, scatter] = bottom.layout(&Layout::horizontal([Constraint::Fill(1); 2]));
|
||||
|
||||
self.render_animated_chart(frame, animated_chart);
|
||||
render_barchart(frame, bar_chart);
|
||||
render_line_chart(frame, line_chart);
|
||||
render_scatter(frame, scatter);
|
||||
}
|
||||
|
||||
fn render_animated_chart(&self, frame: &mut Frame, area: Rect) {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", self.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", f64::midpoint(self.window[0], self.window[1]))),
|
||||
Span::styled(
|
||||
format!("{}", self.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&self.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&self.data2),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(Block::bordered())
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(x_labels)
|
||||
.bounds(self.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||
let dataset = Dataset::default()
|
||||
.marker(symbols::Marker::HalfBlock)
|
||||
.style(Style::new().fg(Color::Blue))
|
||||
.graph_type(GraphType::Bar)
|
||||
// a bell curve
|
||||
.data(&[
|
||||
(0., 0.4),
|
||||
(10., 2.9),
|
||||
(20., 13.5),
|
||||
(30., 41.1),
|
||||
(40., 80.1),
|
||||
(50., 100.0),
|
||||
(60., 80.1),
|
||||
(70., 41.1),
|
||||
(80., 13.5),
|
||||
(90., 2.9),
|
||||
(100., 0.4),
|
||||
]);
|
||||
|
||||
let chart = Chart::new(vec![dataset])
|
||||
.block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 100.0])
|
||||
.labels(["0".bold(), "50".into(), "100.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 100.0])
|
||||
.labels(["0".bold(), "50".into(), "100.0".bold()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
frame.render_widget(chart, bar_chart);
|
||||
}
|
||||
|
||||
fn render_line_chart(frame: &mut Frame, area: Rect) {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Line from only 2 points".italic())
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&[(1., 1.), (4., 4.)]),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(Block::bordered().title(Line::from("Line chart").cyan().bold().centered()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_scatter(frame: &mut Frame, area: Rect) {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Heavy")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().yellow())
|
||||
.data(&HEAVY_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Medium".underlined())
|
||||
.marker(Marker::Braille)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().magenta())
|
||||
.data(&MEDIUM_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Small")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().cyan())
|
||||
.data(&SMALL_PAYLOAD_DATA),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(Block::bordered().title(Line::from("Scatter chart").cyan().bold().centered()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Year")
|
||||
.bounds([1960., 2020.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(["1960", "1990", "2020"]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Cost")
|
||||
.bounds([0., 75000.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(["0", "37 500", "75 000"]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
|
||||
// Data from https://ourworldindata.org/space-exploration-satellites
|
||||
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
|
||||
(1965., 8200.),
|
||||
(1967., 5400.),
|
||||
(1981., 65400.),
|
||||
(1989., 30800.),
|
||||
(1997., 10200.),
|
||||
(2004., 11600.),
|
||||
(2014., 4500.),
|
||||
(2016., 7900.),
|
||||
(2018., 1500.),
|
||||
];
|
||||
|
||||
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||
(1963., 29500.),
|
||||
(1964., 30600.),
|
||||
(1965., 177_900.),
|
||||
(1965., 21000.),
|
||||
(1966., 17900.),
|
||||
(1966., 8400.),
|
||||
(1975., 17500.),
|
||||
(1982., 8300.),
|
||||
(1985., 5100.),
|
||||
(1988., 18300.),
|
||||
(1990., 38800.),
|
||||
(1990., 9900.),
|
||||
(1991., 18700.),
|
||||
(1992., 9100.),
|
||||
(1994., 10500.),
|
||||
(1994., 8500.),
|
||||
(1994., 8700.),
|
||||
(1997., 6200.),
|
||||
(1999., 18000.),
|
||||
(1999., 7600.),
|
||||
(1999., 8900.),
|
||||
(1999., 9600.),
|
||||
(2000., 16000.),
|
||||
(2001., 10000.),
|
||||
(2002., 10400.),
|
||||
(2002., 8100.),
|
||||
(2010., 2600.),
|
||||
(2013., 13600.),
|
||||
(2017., 8000.),
|
||||
];
|
||||
|
||||
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
|
||||
(1961., 118_500.),
|
||||
(1962., 14900.),
|
||||
(1975., 21400.),
|
||||
(1980., 32800.),
|
||||
(1988., 31100.),
|
||||
(1990., 41100.),
|
||||
(1993., 23600.),
|
||||
(1994., 20600.),
|
||||
(1994., 34600.),
|
||||
(1996., 50600.),
|
||||
(1997., 19200.),
|
||||
(1997., 45800.),
|
||||
(1998., 19100.),
|
||||
(2000., 73100.),
|
||||
(2003., 11200.),
|
||||
(2008., 12600.),
|
||||
(2010., 30500.),
|
||||
(2012., 20000.),
|
||||
(2013., 10600.),
|
||||
(2013., 34500.),
|
||||
(2015., 10600.),
|
||||
(2018., 23100.),
|
||||
(2019., 17300.),
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "color-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Color explorer demo
|
||||
|
||||
This example shows how to handle the supported colors.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p color-explorer
|
||||
```
|
||||
@@ -1,232 +0,0 @@
|
||||
//! A Ratatui example that demonstrates how to handle colors.
|
||||
//!
|
||||
//! This example shows all the colors supported by Ratatui. It will render a grid of foreground
|
||||
//! and background colors with their names and indexes.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use itertools::Itertools;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let [named, indexed_colors, indexed_greys] = Layout::vertical([
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.areas(frame.area());
|
||||
|
||||
render_named_colors(frame, named);
|
||||
render_indexed_colors(frame, indexed_colors);
|
||||
render_indexed_grayscale(frame, indexed_greys);
|
||||
}
|
||||
|
||||
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(frame: &mut Frame, area: Rect) {
|
||||
let layout = Layout::vertical([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(frame: &mut Frame, 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 vertical = Layout::vertical([Constraint::Length(1); 2]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 8); 8]);
|
||||
let areas = inner
|
||||
.layout_vec(&vertical)
|
||||
.into_iter()
|
||||
.flat_map(|area| area.layout_vec(&horizontal));
|
||||
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) {
|
||||
let color_name = fg.to_string();
|
||||
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bg_named_colors(frame: &mut Frame, 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 vertical = Layout::vertical([Constraint::Length(1); 2]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 8); 8]);
|
||||
let areas = inner
|
||||
.layout_vec(&vertical)
|
||||
.into_iter()
|
||||
.flat_map(|area| area.layout_vec(&horizontal));
|
||||
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) {
|
||||
let color_name = bg.to_string();
|
||||
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_indexed_colors(frame: &mut Frame, area: Rect) {
|
||||
let block = title_block("Indexed colors".into());
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::vertical([
|
||||
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::horizontal([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::horizontal([Constraint::Length(27); 3])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
// each with 6 rows
|
||||
.flat_map(|area| {
|
||||
Layout::vertical([Constraint::Length(1); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
// each with 6 columns
|
||||
.flat_map(|area| {
|
||||
Layout::horizontal([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::new()
|
||||
.borders(Borders::TOP)
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title_style(Style::reset())
|
||||
.title(title)
|
||||
}
|
||||
|
||||
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1), // 232 - 243
|
||||
Constraint::Length(1), // 244 - 255
|
||||
])
|
||||
.split(area)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::horizontal([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]);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "colors-rgb"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
palette.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Colors-RGB demo
|
||||
|
||||
This example shows the full range of RGB colors in an animation.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p colors-rgb
|
||||
```
|
||||
@@ -1,239 +0,0 @@
|
||||
//! A Ratatui example that shows the full range of RGB colors that can be displayed in the terminal.
|
||||
//!
|
||||
//! Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
//!
|
||||
//! This example also demonstrates how implementing the Widget trait on a mutable reference
|
||||
//! allows the widget to update its state while it is being rendered. This allows the fps
|
||||
//! widget to update the fps calculation and the colors widget to update a cached version of
|
||||
//! the colors to render instead of recalculating them every frame.
|
||||
//!
|
||||
//! This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
|
||||
//! is useful when the state is only used by the widget and doesn't need to be shared with
|
||||
//! other widgets.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use palette::convert::FromColorUnclamped;
|
||||
use palette::{Okhsv, Srgb};
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Position, Rect};
|
||||
use ratatui::style::Color;
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
/// The current state of the app (running or quit)
|
||||
state: AppState,
|
||||
|
||||
/// A widget that displays the current frames per second
|
||||
fps_widget: FpsWidget,
|
||||
|
||||
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
|
||||
colors_widget: ColorsWidget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
/// The app is running
|
||||
#[default]
|
||||
Running,
|
||||
|
||||
/// The user has requested the app to quit
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// A widget that displays the current frames per second
|
||||
#[derive(Debug)]
|
||||
struct FpsWidget {
|
||||
/// The number of elapsed frames that have passed - used to calculate the fps
|
||||
frame_count: usize,
|
||||
|
||||
/// The last instant that the fps was calculated
|
||||
last_instant: Instant,
|
||||
|
||||
/// The current frames per second
|
||||
fps: Option<f32>,
|
||||
}
|
||||
|
||||
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
|
||||
///
|
||||
/// This widget is animated and will change colors over time.
|
||||
#[derive(Debug, Default)]
|
||||
struct ColorsWidget {
|
||||
/// The colors to render - should be double the height of the area as we render two rows of
|
||||
/// pixels for each row of the widget using the half block character. This is computed any time
|
||||
/// the size of the widget changes.
|
||||
colors: Vec<Vec<Color>>,
|
||||
|
||||
/// the number of elapsed frames that have passed - used to animate the colors by shifting the
|
||||
/// x index by the frame number
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Run the app
|
||||
///
|
||||
/// This is the main event loop for the app.
|
||||
pub fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn is_running(&self) -> bool {
|
||||
matches!(self.state, AppState::Running)
|
||||
}
|
||||
|
||||
/// Handle any events that have occurred since the last time the app was rendered.
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
// Ensure that the app only blocks for a period that allows the app to render at
|
||||
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
|
||||
// also update the app immediately any time an event occurs)
|
||||
let timeout = Duration::from_secs_f32(1.0 / 60.0);
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
if event::read()?.is_key_press() {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the Widget trait for &mut App so that it can be rendered
|
||||
///
|
||||
/// This is implemented on a mutable reference so that the app can update its state while it is
|
||||
/// being rendered. This allows the fps widget to update the fps calculation and the colors widget
|
||||
/// to update the colors to render.
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
let [top, colors] = area.layout(&Layout::vertical([Length(1), Min(0)]));
|
||||
let [title, fps] = top.layout(&Layout::horizontal([Min(0), Length(8)]));
|
||||
Text::from("colors_rgb example. Press q to quit")
|
||||
.centered()
|
||||
.render(title, buf);
|
||||
self.fps_widget.render(fps, buf);
|
||||
self.colors_widget.render(colors, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Default impl for `FpsWidget`
|
||||
///
|
||||
/// Manual impl is required because we need to initialize the `last_instant` field to the current
|
||||
/// instant.
|
||||
impl Default for FpsWidget {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_count: 0,
|
||||
last_instant: Instant::now(),
|
||||
fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget impl for `FpsWidget`
|
||||
///
|
||||
/// This is implemented on a mutable reference so that we can update the frame count and fps
|
||||
/// calculation while rendering.
|
||||
impl Widget for &mut FpsWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.calculate_fps();
|
||||
if let Some(fps) = self.fps {
|
||||
let text = format!("{fps:.1} fps");
|
||||
Text::from(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FpsWidget {
|
||||
/// Update the fps calculation.
|
||||
///
|
||||
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
|
||||
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
|
||||
/// machines that can't render at least 2 frames per second.
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
fn calculate_fps(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
|
||||
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
|
||||
self.frame_count = 0;
|
||||
self.last_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget impl for `ColorsWidget`
|
||||
///
|
||||
/// This is implemented on a mutable reference so that we can update the frame count and store a
|
||||
/// cached version of the colors to render instead of recalculating them every frame.
|
||||
impl Widget for &mut ColorsWidget {
|
||||
/// Render the widget
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.setup_colors(area);
|
||||
let colors = &self.colors;
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
// animate the colors by shifting the x index by the frame number
|
||||
let xi = (xi + self.frame_count) % (area.width as usize);
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
// render a half block character for each row of pixels with the foreground color
|
||||
// set to the color of the pixel and the background color set to the color of the
|
||||
// pixel below it
|
||||
let fg = colors[yi * 2][xi];
|
||||
let bg = colors[yi * 2 + 1][xi];
|
||||
buf[Position::new(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
self.frame_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorsWidget {
|
||||
/// Setup the colors to render.
|
||||
///
|
||||
/// This is called once per frame to setup the colors to render. It caches the colors so that
|
||||
/// they don't need to be recalculated every frame.
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
fn setup_colors(&mut self, size: Rect) {
|
||||
let Rect { width, height, .. } = size;
|
||||
// double the height because each screen row has two rows of half block pixels
|
||||
let height = height as usize * 2;
|
||||
let width = width as usize;
|
||||
// only update the colors if the size has changed since the last time we rendered
|
||||
if self.colors.len() == height && self.colors[0].len() == width {
|
||||
return;
|
||||
}
|
||||
self.colors = Vec::with_capacity(height);
|
||||
for y in 0..height {
|
||||
let mut row = Vec::with_capacity(width);
|
||||
for x in 0..width {
|
||||
let hue = x as f32 * 360.0 / width as f32;
|
||||
let value = (height - y) as f32 / height as f32;
|
||||
let saturation = Okhsv::max_saturation();
|
||||
let color = Okhsv::new(hue, saturation, value);
|
||||
let color = Srgb::<f32>::from_color_unclamped(color);
|
||||
let color: Srgb<u8> = color.into_format();
|
||||
let color = Color::Rgb(color.red, color.green, color.blue);
|
||||
row.push(color);
|
||||
}
|
||||
self.colors.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "constraint-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Constraint explorer demo
|
||||
|
||||
This interactive example shows how different constraints can be used to layout widgets.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p constraint-explorer
|
||||
```
|
||||
@@ -1,605 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// A Ratatui example that demonstrates how different layout constraints work.
|
||||
///
|
||||
/// It also supports swapping constraints, adding and removing blocks, and changing the spacing
|
||||
/// between blocks.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use itertools::Itertools;
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio};
|
||||
use ratatui::layout::{Flex, Layout, Rect};
|
||||
use ratatui::style::palette::tailwind::{BLUE, SKY, SLATE, STONE};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::symbols::{self, line};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget, Wrap};
|
||||
use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
mode: AppMode,
|
||||
spacing: i16,
|
||||
constraints: Vec<Constraint>,
|
||||
selected_index: usize,
|
||||
value: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
enum AppMode {
|
||||
#[default]
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// A variant of [`Constraint`] that can be rendered as a tab.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
|
||||
enum ConstraintName {
|
||||
#[default]
|
||||
Length,
|
||||
Percentage,
|
||||
Ratio,
|
||||
Min,
|
||||
Max,
|
||||
Fill,
|
||||
}
|
||||
|
||||
/// A widget that renders a [`Constraint`] as a block. E.g.:
|
||||
/// ```plain
|
||||
/// ┌──────────────┐
|
||||
/// │ Length(16) │
|
||||
/// │ 16px │
|
||||
/// └──────────────┘
|
||||
/// ```
|
||||
struct ConstraintBlock {
|
||||
constraint: Constraint,
|
||||
legend: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌ ┐
|
||||
/// 8 px
|
||||
/// └ ┘
|
||||
/// ```
|
||||
struct SpacerBlock;
|
||||
|
||||
// App behaviour
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
self.insert_test_defaults();
|
||||
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO remove these - these are just for testing
|
||||
fn insert_test_defaults(&mut self) {
|
||||
self.constraints = vec![
|
||||
Constraint::Length(20),
|
||||
Constraint::Length(20),
|
||||
Constraint::Length(20),
|
||||
];
|
||||
self.value = 20;
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.mode == AppMode::Running
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
|
||||
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
KeyCode::Char('x') => self.delete_block(),
|
||||
KeyCode::Char('a') => self.insert_block(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn increment_value(&mut self) {
|
||||
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
match constraint {
|
||||
Constraint::Length(v)
|
||||
| Constraint::Min(v)
|
||||
| Constraint::Max(v)
|
||||
| Constraint::Fill(v)
|
||||
| Constraint::Percentage(v) => *v = v.saturating_add(1),
|
||||
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn decrement_value(&mut self) {
|
||||
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
match constraint {
|
||||
Constraint::Length(v)
|
||||
| Constraint::Min(v)
|
||||
| Constraint::Max(v)
|
||||
| Constraint::Fill(v)
|
||||
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
|
||||
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// select the next block with wrap around
|
||||
fn next_block(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.constraints.len();
|
||||
self.selected_index = (self.selected_index + 1) % len;
|
||||
}
|
||||
|
||||
/// select the previous block with wrap around
|
||||
fn prev_block(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.constraints.len();
|
||||
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
|
||||
}
|
||||
|
||||
/// delete the selected block
|
||||
fn delete_block(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.constraints.remove(self.selected_index);
|
||||
self.selected_index = self.selected_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// insert a block after the selected block
|
||||
fn insert_block(&mut self) {
|
||||
let index = self
|
||||
.selected_index
|
||||
.saturating_add(1)
|
||||
.min(self.constraints.len());
|
||||
let constraint = Constraint::Length(self.value);
|
||||
self.constraints.insert(index, constraint);
|
||||
self.selected_index = index;
|
||||
}
|
||||
|
||||
const fn increment_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_add(1);
|
||||
}
|
||||
|
||||
const fn decrement_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_sub(1);
|
||||
}
|
||||
|
||||
const fn exit(&mut self) {
|
||||
self.mode = AppMode::Quit;
|
||||
}
|
||||
|
||||
fn swap_constraint(&mut self, name: ConstraintName) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let constraint = match name {
|
||||
ConstraintName::Length => Length(self.value),
|
||||
ConstraintName::Percentage => Percentage(self.value),
|
||||
ConstraintName::Min => Min(self.value),
|
||||
ConstraintName::Max => Max(self.value),
|
||||
ConstraintName::Fill => Fill(self.value),
|
||||
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
|
||||
};
|
||||
self.constraints[self.selected_index] = constraint;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Constraint> for ConstraintName {
|
||||
fn from(constraint: Constraint) -> Self {
|
||||
match constraint {
|
||||
Length(_) => Self::Length,
|
||||
Percentage(_) => Self::Percentage,
|
||||
Ratio(_, _) => Self::Ratio,
|
||||
Min(_) => Self::Min,
|
||||
Max(_) => Self::Max,
|
||||
Fill(_) => Self::Fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [
|
||||
header_area,
|
||||
instructions_area,
|
||||
swap_legend_area,
|
||||
_,
|
||||
blocks_area,
|
||||
] = area.layout(&Layout::vertical([
|
||||
Length(2), // header
|
||||
Length(2), // instructions
|
||||
Length(1), // swap key legend
|
||||
Length(1), // gap
|
||||
Fill(1), // blocks
|
||||
]));
|
||||
|
||||
App::header().render(header_area, buf);
|
||||
App::instructions().render(instructions_area, buf);
|
||||
App::swap_legend().render(swap_legend_area, buf);
|
||||
self.render_layout_blocks(blocks_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// App rendering
|
||||
impl App {
|
||||
const HEADER_COLOR: Color = SLATE.c200;
|
||||
const TEXT_COLOR: Color = SLATE.c400;
|
||||
const AXIS_COLOR: Color = SLATE.c500;
|
||||
|
||||
fn header() -> impl Widget {
|
||||
let text = "Constraint Explorer";
|
||||
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
fn instructions() -> impl Widget {
|
||||
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, +/-: spacing";
|
||||
Paragraph::new(text)
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.centered()
|
||||
.wrap(Wrap { trim: false })
|
||||
}
|
||||
|
||||
fn swap_legend() -> impl Widget {
|
||||
#[expect(unstable_name_collisions)]
|
||||
Paragraph::new(
|
||||
Line::from(
|
||||
[
|
||||
ConstraintName::Min,
|
||||
ConstraintName::Max,
|
||||
ConstraintName::Length,
|
||||
ConstraintName::Percentage,
|
||||
ConstraintName::Ratio,
|
||||
ConstraintName::Fill,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
format!(" {i}: {name} ", i = i + 1)
|
||||
.fg(SLATE.c200)
|
||||
.bg(name.color())
|
||||
})
|
||||
.intersperse(Span::from(" "))
|
||||
.collect_vec(),
|
||||
)
|
||||
.centered(),
|
||||
)
|
||||
.wrap(Wrap { trim: false })
|
||||
}
|
||||
|
||||
/// A bar like `<----- 80 px (gap: 2 px) ----->`
|
||||
///
|
||||
/// Only shows the gap when spacing is not zero
|
||||
fn axis(&self, width: u16) -> impl Widget {
|
||||
let label = match self.spacing.cmp(&0) {
|
||||
Ordering::Greater => format!("{width} px (gap: {} px)", self.spacing),
|
||||
Ordering::Less => {
|
||||
format!("{width} px (overlap: {} px)", self.spacing.unsigned_abs())
|
||||
}
|
||||
Ordering::Equal => format!("{width} px"),
|
||||
};
|
||||
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
|
||||
}
|
||||
|
||||
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
|
||||
let main_layout = Layout::vertical([Length(3), Fill(1)]).spacing(1);
|
||||
let [user_constraints, area] = area.layout(&main_layout);
|
||||
|
||||
self.render_user_constraints_legend(user_constraints, buf);
|
||||
|
||||
let [
|
||||
start,
|
||||
center,
|
||||
end,
|
||||
space_between,
|
||||
space_around,
|
||||
space_evenly,
|
||||
] = area.layout(&Layout::vertical([Length(7); 6]));
|
||||
|
||||
self.render_layout_block(Flex::Start, start, buf);
|
||||
self.render_layout_block(Flex::Center, center, buf);
|
||||
self.render_layout_block(Flex::End, end, buf);
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||
self.render_layout_block(Flex::SpaceEvenly, space_evenly, buf);
|
||||
}
|
||||
|
||||
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = self.constraints.iter().map(|_| Constraint::Fill(1));
|
||||
let blocks = Layout::horizontal(constraints).split(area);
|
||||
|
||||
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
||||
let selected = self.selected_index == i;
|
||||
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(1), Max(1), Length(4)]);
|
||||
let [label_area, axis_area, blocks_area] = area.layout(&layout);
|
||||
|
||||
if label_area.height > 0 {
|
||||
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
||||
}
|
||||
|
||||
self.axis(area.width).render(axis_area, buf);
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(flex)
|
||||
.spacing(self.spacing)
|
||||
.split_with_spacers(blocks_area);
|
||||
|
||||
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
||||
let selected = self.selected_index == i;
|
||||
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
|
||||
}
|
||||
|
||||
for area in spacers.iter() {
|
||||
SpacerBlock.render(*area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ConstraintBlock {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match area.height {
|
||||
1 => self.render_1px(area, buf),
|
||||
2 => self.render_2px(area, buf),
|
||||
_ => self.render_4px(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstraintBlock {
|
||||
const TEXT_COLOR: Color = SLATE.c200;
|
||||
|
||||
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
||||
Self {
|
||||
constraint,
|
||||
legend,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, width: u16) -> String {
|
||||
let long_width = format!("{width} px");
|
||||
let short_width = format!("{width}");
|
||||
// border takes up 2 columns
|
||||
let available_space = width.saturating_sub(2) as usize;
|
||||
let width_label = if long_width.len() < available_space {
|
||||
long_width
|
||||
} else if short_width.len() < available_space {
|
||||
short_width
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!("{}\n{}", self.constraint, width_label)
|
||||
}
|
||||
|
||||
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||
let main_color = ConstraintName::from(self.constraint).color();
|
||||
let selected_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::new()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(selected_color)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||
let main_color = ConstraintName::from(self.constraint).color();
|
||||
let selected_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(selected_color).reversed())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||
let main_color = ConstraintName::from(self.constraint).color();
|
||||
let selected_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
let color = if self.legend {
|
||||
selected_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
let label = self.label(area.width);
|
||||
let block = Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(color).reversed())
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(color);
|
||||
Paragraph::new(label)
|
||||
.centered()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(color)
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
|
||||
if !self.legend {
|
||||
let border_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
if let Some(last_row) = area.rows().next_back() {
|
||||
buf.set_style(last_row, border_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SpacerBlock {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match area.height {
|
||||
1 => (),
|
||||
2 => Self::render_2px(area, buf),
|
||||
3 => Self::render_3px(area, buf),
|
||||
_ => Self::render_4px(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpacerBlock {
|
||||
const TEXT_COLOR: Color = SLATE.c500;
|
||||
const BORDER_COLOR: Color = SLATE.c600;
|
||||
|
||||
/// A block with a corner borders
|
||||
fn block() -> impl Widget {
|
||||
let corners_only = symbols::border::Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(corners_only)
|
||||
.border_style(Self::BORDER_COLOR)
|
||||
}
|
||||
|
||||
/// A vertical line used if there is not enough space to render the block
|
||||
fn line() -> impl Widget {
|
||||
Paragraph::new(Text::from(vec![
|
||||
Line::from(""),
|
||||
Line::from("│"),
|
||||
Line::from("│"),
|
||||
Line::from(""),
|
||||
]))
|
||||
.style(Self::BORDER_COLOR)
|
||||
}
|
||||
|
||||
/// A label that says "Spacer" if there is enough space
|
||||
fn spacer_label(width: u16) -> impl Widget {
|
||||
let label = if width >= 6 { "Spacer" } else { "" };
|
||||
label.fg(Self::TEXT_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
/// A label that says "8 px" if there is enough space
|
||||
fn label(width: u16) -> impl Widget {
|
||||
let long_label = format!("{width} px");
|
||||
let short_label = format!("{width}");
|
||||
let label = if long_label.len() < width as usize {
|
||||
long_label
|
||||
} else if short_label.len() < width as usize {
|
||||
short_label
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
Line::styled(label, Self::TEXT_COLOR).centered()
|
||||
}
|
||||
|
||||
fn render_2px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_3px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
|
||||
let row = area.rows().nth(1).unwrap_or_default();
|
||||
Self::spacer_label(area.width).render(row, buf);
|
||||
}
|
||||
|
||||
fn render_4px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
|
||||
let row = area.rows().nth(1).unwrap_or_default();
|
||||
Self::spacer_label(area.width).render(row, buf);
|
||||
|
||||
let row = area.rows().nth(2).unwrap_or_default();
|
||||
Self::label(area.width).render(row, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstraintName {
|
||||
const fn color(self) -> Color {
|
||||
match self {
|
||||
Self::Length => SLATE.c700,
|
||||
Self::Percentage => SLATE.c800,
|
||||
Self::Ratio => SLATE.c900,
|
||||
Self::Fill => SLATE.c950,
|
||||
Self::Min => BLUE.c800,
|
||||
Self::Max => BLUE.c900,
|
||||
}
|
||||
}
|
||||
|
||||
const fn lighter_color(self) -> Color {
|
||||
match self {
|
||||
Self::Length => STONE.c500,
|
||||
Self::Percentage => STONE.c600,
|
||||
Self::Ratio => STONE.c700,
|
||||
Self::Fill => STONE.c800,
|
||||
Self::Min => SKY.c600,
|
||||
Self::Max => SKY.c700,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "constraints"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Constraints demo
|
||||
|
||||
This example shows different types of constraints.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p constraints
|
||||
```
|
||||
@@ -1,388 +0,0 @@
|
||||
/// A Ratatui example that demonstrates different types of constraints.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio};
|
||||
use ratatui::layout::{Layout, Rect};
|
||||
use ratatui::style::palette::tailwind;
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{
|
||||
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||
Tabs, Widget,
|
||||
};
|
||||
use ratatui::{DefaultTerminal, symbols};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const SPACER_HEIGHT: u16 = 0;
|
||||
const ILLUSTRATION_HEIGHT: u16 = 4;
|
||||
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
|
||||
|
||||
// priority 2
|
||||
const MIN_COLOR: Color = tailwind::BLUE.c900;
|
||||
const MAX_COLOR: Color = tailwind::BLUE.c800;
|
||||
// priority 3
|
||||
const LENGTH_COLOR: Color = tailwind::SLATE.c700;
|
||||
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
|
||||
const RATIO_COLOR: Color = tailwind::SLATE.c900;
|
||||
// priority 4
|
||||
const FILL_COLOR: Color = tailwind::SLATE.c950;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct App {
|
||||
selected_tab: SelectedTab,
|
||||
scroll_offset: u16,
|
||||
max_scroll_offset: u16,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
/// Tabs for the different examples
|
||||
///
|
||||
/// The order of the variants is the order in which they are displayed.
|
||||
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
Min,
|
||||
Max,
|
||||
Length,
|
||||
Percentage,
|
||||
Ratio,
|
||||
Fill,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
self.update_max_scroll_offset();
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn update_max_scroll_offset(&mut self) {
|
||||
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
|
||||
}
|
||||
|
||||
fn is_running(self) -> bool {
|
||||
self.state == AppState::Running
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
self.update_max_scroll_offset();
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
self.update_max_scroll_offset();
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
const fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn down(&mut self) {
|
||||
self.scroll_offset = self
|
||||
.scroll_offset
|
||||
.saturating_add(1)
|
||||
.min(self.max_scroll_offset);
|
||||
}
|
||||
|
||||
const fn top(&mut self) {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
const fn bottom(&mut self) {
|
||||
self.scroll_offset = self.max_scroll_offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [tabs, axis, demo] = area.layout(&Layout::vertical([Length(3), Length(3), Fill(0)]));
|
||||
|
||||
self.render_tabs(tabs, buf);
|
||||
Self::render_axis(axis, buf);
|
||||
self.render_demo(demo, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
|
||||
let block = Block::new()
|
||||
.title("Constraints ".bold())
|
||||
.title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll");
|
||||
Tabs::new(titles)
|
||||
.block(block)
|
||||
.highlight_style(Modifier::REVERSED)
|
||||
.select(self.selected_tab as usize)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_axis(area: Rect, buf: &mut Buffer) {
|
||||
let width = area.width as usize;
|
||||
// a bar like `<----- 80 px ----->`
|
||||
let width_label = format!("{width} px");
|
||||
let width_bar = format!(
|
||||
"<{width_label:-^width$}>",
|
||||
width = width - width_label.len() / 2
|
||||
);
|
||||
Paragraph::new(width_bar.dark_gray())
|
||||
.centered()
|
||||
.block(Block::new().padding(Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 1,
|
||||
bottom: 0,
|
||||
}))
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Render the demo content
|
||||
///
|
||||
/// This function renders the demo content into a separate buffer and then splices the buffer
|
||||
/// into the main buffer. This is done to make it possible to handle scrolling easily.
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn render_demo(self, area: Rect, buf: &mut Buffer) {
|
||||
// render demo content into a separate buffer so all examples fit we add an extra
|
||||
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||
// at the max
|
||||
let height = self.selected_tab.get_example_count() * EXAMPLE_HEIGHT;
|
||||
let demo_area = Rect::new(0, 0, area.width, height + area.height);
|
||||
let mut demo_buf = Buffer::empty(demo_area);
|
||||
|
||||
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
|
||||
let content_area = if scrollbar_needed {
|
||||
Rect {
|
||||
width: demo_area.width - 1,
|
||||
..demo_area
|
||||
}
|
||||
} else {
|
||||
demo_area
|
||||
};
|
||||
self.selected_tab.render(content_area, &mut demo_buf);
|
||||
|
||||
let visible_content = demo_buf
|
||||
.content
|
||||
.into_iter()
|
||||
.skip((demo_area.width * self.scroll_offset) as usize)
|
||||
.take(area.area() as usize);
|
||||
for (i, cell) in visible_content.enumerate() {
|
||||
let x = i as u16 % area.width;
|
||||
let y = i as u16 / area.width;
|
||||
buf[(area.x + x, area.y + y)] = cell;
|
||||
}
|
||||
|
||||
if scrollbar_needed {
|
||||
let mut state = ScrollbarState::new(self.max_scroll_offset as usize)
|
||||
.position(self.scroll_offset as usize);
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
const fn get_example_count(self) -> u16 {
|
||||
#[expect(clippy::match_same_arms)]
|
||||
match self {
|
||||
Self::Length => 4,
|
||||
Self::Percentage => 5,
|
||||
Self::Ratio => 4,
|
||||
Self::Fill => 2,
|
||||
Self::Min => 5,
|
||||
Self::Max => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
let text = format!(" {value} ");
|
||||
let color = match value {
|
||||
Self::Length => LENGTH_COLOR,
|
||||
Self::Percentage => PERCENTAGE_COLOR,
|
||||
Self::Ratio => RATIO_COLOR,
|
||||
Self::Fill => FILL_COLOR,
|
||||
Self::Min => MIN_COLOR,
|
||||
Self::Max => MAX_COLOR,
|
||||
};
|
||||
text.fg(tailwind::SLATE.c200).bg(color).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match self {
|
||||
Self::Length => Self::render_length_example(area, buf),
|
||||
Self::Percentage => Self::render_percentage_example(area, buf),
|
||||
Self::Ratio => Self::render_ratio_example(area, buf),
|
||||
Self::Fill => Self::render_fill_example(area, buf),
|
||||
Self::Min => Self::render_min_example(area, buf),
|
||||
Self::Max => Self::render_max_example(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_length_example(area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 4]);
|
||||
let [example1, example2, example3, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Length(20), Length(20)]).render(example1, buf);
|
||||
Example::new(&[Length(20), Min(20)]).render(example2, buf);
|
||||
Example::new(&[Length(20), Max(20)]).render(example3, buf);
|
||||
}
|
||||
|
||||
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
|
||||
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
|
||||
Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
|
||||
Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
|
||||
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 5]);
|
||||
let [example1, example2, example3, example4, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
|
||||
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
|
||||
Example::new(&[Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf);
|
||||
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
|
||||
}
|
||||
|
||||
fn render_fill_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, _] = area.layout(&Layout::vertical([Length(EXAMPLE_HEIGHT); 3]));
|
||||
|
||||
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
|
||||
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
|
||||
}
|
||||
|
||||
fn render_min_example(area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
|
||||
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
|
||||
Example::new(&[Percentage(100), Min(40)]).render(example3, buf);
|
||||
Example::new(&[Percentage(100), Min(60)]).render(example4, buf);
|
||||
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_max_example(area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(EXAMPLE_HEIGHT); 6]);
|
||||
let [example1, example2, example3, example4, example5, _] = area.layout(&layout);
|
||||
|
||||
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
|
||||
Example::new(&[Percentage(0), Max(40)]).render(example3, buf);
|
||||
Example::new(&[Percentage(0), Max(60)]).render(example4, buf);
|
||||
Example::new(&[Percentage(0), Max(80)]).render(example5, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct Example {
|
||||
constraints: Vec<Constraint>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(constraints: &[Constraint]) -> Self {
|
||||
Self {
|
||||
constraints: constraints.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Example {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let vertical = Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]);
|
||||
let horizontal = Layout::horizontal(&self.constraints);
|
||||
let [area, _] = area.layout(&vertical);
|
||||
let blocks = area.layout_vec(&horizontal);
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||
let color = match constraint {
|
||||
Constraint::Length(_) => LENGTH_COLOR,
|
||||
Constraint::Percentage(_) => PERCENTAGE_COLOR,
|
||||
Constraint::Ratio(_, _) => RATIO_COLOR,
|
||||
Constraint::Fill(_) => FILL_COLOR,
|
||||
Constraint::Min(_) => MIN_COLOR,
|
||||
Constraint::Max(_) => MAX_COLOR,
|
||||
};
|
||||
let fg = Color::White;
|
||||
let title = format!("{constraint}");
|
||||
let content = format!("{width} px");
|
||||
let text = format!("{title}\n{content}");
|
||||
let block = Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(color).reversed())
|
||||
.style(Style::default().fg(fg).bg(color));
|
||||
Paragraph::new(text).centered().block(block)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "custom-widget"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Custom widget demo
|
||||
|
||||
This example shows how to create a custom widget that can be interacted with the mouse.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p custom-widget
|
||||
```
|
||||
@@ -1,258 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to create a custom widget that can be interacted
|
||||
/// with using the mouse.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::{io::stdout, ops::ControlFlow, time::Duration};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseButton,
|
||||
MouseEvent, MouseEventKind,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Paragraph, Widget};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
execute!(stdout(), EnableMouseCapture)?;
|
||||
let app_result = run(terminal);
|
||||
ratatui::restore();
|
||||
if let Err(err) = execute!(stdout(), DisableMouseCapture) {
|
||||
eprintln!("Error disabling mouse capture: {err}");
|
||||
}
|
||||
app_result
|
||||
}
|
||||
|
||||
/// A custom widget that renders a button with a label, theme and state.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Button<'a> {
|
||||
label: Line<'a>,
|
||||
theme: Theme,
|
||||
state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum State {
|
||||
Normal,
|
||||
Selected,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Theme {
|
||||
text: Color,
|
||||
background: Color,
|
||||
highlight: Color,
|
||||
shadow: Color,
|
||||
}
|
||||
|
||||
const BLUE: Theme = Theme {
|
||||
text: Color::Rgb(16, 24, 48),
|
||||
background: Color::Rgb(48, 72, 144),
|
||||
highlight: Color::Rgb(64, 96, 192),
|
||||
shadow: Color::Rgb(32, 48, 96),
|
||||
};
|
||||
|
||||
const RED: Theme = Theme {
|
||||
text: Color::Rgb(48, 16, 16),
|
||||
background: Color::Rgb(144, 48, 48),
|
||||
highlight: Color::Rgb(192, 64, 64),
|
||||
shadow: Color::Rgb(96, 32, 32),
|
||||
};
|
||||
|
||||
const GREEN: Theme = Theme {
|
||||
text: Color::Rgb(16, 48, 16),
|
||||
background: Color::Rgb(48, 144, 48),
|
||||
highlight: Color::Rgb(64, 192, 64),
|
||||
shadow: Color::Rgb(32, 96, 32),
|
||||
};
|
||||
|
||||
/// A button with a label that can be themed.
|
||||
impl<'a> Button<'a> {
|
||||
pub fn new<T: Into<Line<'a>>>(label: T) -> Self {
|
||||
Button {
|
||||
label: label.into(),
|
||||
theme: BLUE,
|
||||
state: State::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn theme(mut self, theme: Theme) -> Self {
|
||||
self.theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn state(mut self, state: State) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Button<'_> {
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let (background, text, shadow, highlight) = self.colors();
|
||||
buf.set_style(area, Style::new().bg(background).fg(text));
|
||||
|
||||
// render top line if there's enough space
|
||||
if area.height > 2 {
|
||||
buf.set_string(
|
||||
area.x,
|
||||
area.y,
|
||||
"▔".repeat(area.width as usize),
|
||||
Style::new().fg(highlight).bg(background),
|
||||
);
|
||||
}
|
||||
// render bottom line if there's enough space
|
||||
if area.height > 1 {
|
||||
buf.set_string(
|
||||
area.x,
|
||||
area.y + area.height - 1,
|
||||
"▁".repeat(area.width as usize),
|
||||
Style::new().fg(shadow).bg(background),
|
||||
);
|
||||
}
|
||||
// render label centered
|
||||
buf.set_line(
|
||||
area.x + (area.width.saturating_sub(self.label.width() as u16)) / 2,
|
||||
area.y + (area.height.saturating_sub(1)) / 2,
|
||||
&self.label,
|
||||
area.width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Button<'_> {
|
||||
const fn colors(&self) -> (Color, Color, Color, Color) {
|
||||
let theme = self.theme;
|
||||
match self.state {
|
||||
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
|
||||
State::Selected => (theme.highlight, theme.text, theme.shadow, theme.highlight),
|
||||
State::Active => (theme.background, theme.text, theme.highlight, theme.shadow),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let mut selected_button: usize = 0;
|
||||
let mut button_states = [State::Selected, State::Normal, State::Normal];
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, button_states))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
continue;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, states: [State; 3]) {
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [title, buttons, help, _] = frame.area().layout(&layout);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||
title,
|
||||
);
|
||||
render_buttons(frame, buttons, states);
|
||||
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
|
||||
}
|
||||
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
|
||||
let layout = Layout::horizontal([Constraint::Length(15); 3]).flex(Flex::Start);
|
||||
let [red, green, blue] = area.layout(&layout);
|
||||
|
||||
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
|
||||
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
|
||||
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
|
||||
}
|
||||
|
||||
fn handle_key_event(
|
||||
key: KeyEvent,
|
||||
button_states: &mut [State; 3],
|
||||
selected_button: &mut usize,
|
||||
) -> ControlFlow<()> {
|
||||
if !key.is_press() {
|
||||
return ControlFlow::Continue(());
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return ControlFlow::Break(()),
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_sub(1);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_add(1).min(2);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
if button_states[*selected_button] == State::Active {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
} else {
|
||||
button_states[*selected_button] = State::Active;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
|
||||
fn handle_mouse_event(
|
||||
mouse: MouseEvent,
|
||||
button_states: &mut [State; 3],
|
||||
selected_button: &mut usize,
|
||||
) {
|
||||
match mouse.kind {
|
||||
MouseEventKind::Moved => {
|
||||
let old_selected_button = *selected_button;
|
||||
*selected_button = match mouse.column {
|
||||
x if x < 15 => 0,
|
||||
x if x < 30 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
if old_selected_button != *selected_button {
|
||||
if button_states[old_selected_button] != State::Active {
|
||||
button_states[old_selected_button] = State::Normal;
|
||||
}
|
||||
if button_states[*selected_button] != State::Active {
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
if button_states[*selected_button] == State::Active {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
} else {
|
||||
button_states[*selected_button] = State::Active;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "demo"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
crossterm = ["ratatui/crossterm", "dep:crossterm"]
|
||||
termion = ["ratatui/termion", "dep:termion"]
|
||||
termwiz = ["ratatui/termwiz", "dep:termwiz"]
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
crossterm = { workspace = true, optional = true }
|
||||
rand.workspace = true
|
||||
ratatui.workspace = true
|
||||
termwiz = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
termion = { workspace = true, optional = true }
|
||||
@@ -1,25 +0,0 @@
|
||||
# Demo example
|
||||
|
||||
This is the original demo that was developed for Tui-rs (the library that Ratatui was forked from).
|
||||
|
||||

|
||||
|
||||
This example is available for each backend. To run it:
|
||||
|
||||
## crossterm
|
||||
|
||||
```shell
|
||||
cargo run -p demo
|
||||
```
|
||||
|
||||
## termion
|
||||
|
||||
```shell
|
||||
cargo run -p demo --no-default-features --features termion
|
||||
```
|
||||
|
||||
## termwiz
|
||||
|
||||
```shell
|
||||
cargo run -p demo --no-default-features --features termwiz
|
||||
```
|
||||
@@ -1,76 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, KeyCode};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::{Backend, CrosstermBackend};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::ui;
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Crossterm Demo", enhanced_graphics);
|
||||
let app_result = 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) = app_result {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
B::Error: 'static,
|
||||
{
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
continue;
|
||||
}
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('h') | KeyCode::Left => app.on_left(),
|
||||
KeyCode::Char('j') | KeyCode::Down => app.on_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.on_up(),
|
||||
KeyCode::Char('l') | KeyCode::Right => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! # [Ratatui] Original Demo example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::error::Error;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(all(not(windows), feature = "termion"))]
|
||||
mod termion;
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
|
||||
mod ui;
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[arg(short, long, default_value_t = 250)]
|
||||
tick_rate: u64,
|
||||
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[arg(short, long, default_value_t = true)]
|
||||
unicode: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli = Cli::parse();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
#[cfg(feature = "crossterm")]
|
||||
crate::crossterm::run(tick_rate, cli.unicode)?;
|
||||
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))]
|
||||
crate::termion::run(tick_rate, cli.unicode)?;
|
||||
#[cfg(all(
|
||||
feature = "termwiz",
|
||||
not(feature = "crossterm"),
|
||||
not(feature = "termion")
|
||||
))]
|
||||
crate::termwiz::run(tick_rate, cli.unicode)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "demo2"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
palette.workspace = true
|
||||
rand.workspace = true
|
||||
rand_chacha.workspace = true
|
||||
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||
strum.workspace = true
|
||||
time.workspace = true
|
||||
unicode-width.workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
|
||||
```shell
|
||||
cargo run -p demo2
|
||||
```
|
||||
|
||||

|
||||
@@ -1,215 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::Context;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use itertools::Itertools;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::Color;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Tabs, Widget};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
use crate::tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab};
|
||||
use crate::{THEME, destroy};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct App {
|
||||
mode: Mode,
|
||||
tab: Tab,
|
||||
about_tab: AboutTab,
|
||||
recipe_tab: RecipeTab,
|
||||
email_tab: EmailTab,
|
||||
traceroute_tab: TracerouteTab,
|
||||
weather_tab: WeatherTab,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Running,
|
||||
Destroy,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
|
||||
enum Tab {
|
||||
#[default]
|
||||
About,
|
||||
Recipe,
|
||||
Email,
|
||||
Traceroute,
|
||||
Weather,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Run the app until the user quits.
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal
|
||||
.draw(|frame| self.render(frame))
|
||||
.wrap_err("terminal.draw")?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.mode != Mode::Quit
|
||||
}
|
||||
|
||||
/// Render a single frame of the app.
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
frame.render_widget(self, frame.area());
|
||||
if self.mode == Mode::Destroy {
|
||||
destroy::destroy(frame);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle events from the terminal.
|
||||
///
|
||||
/// This function is called once per frame, The events are polled from the stdin with timeout of
|
||||
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
|
||||
KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => self.next_tab(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.prev(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&mut self) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.prev_row(),
|
||||
Tab::Recipe => self.recipe_tab.prev(),
|
||||
Tab::Email => self.email_tab.prev(),
|
||||
Tab::Traceroute => self.traceroute_tab.prev_row(),
|
||||
Tab::Weather => self.weather_tab.prev(),
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.next_row(),
|
||||
Tab::Recipe => self.recipe_tab.next(),
|
||||
Tab::Email => self.email_tab.next(),
|
||||
Tab::Traceroute => self.traceroute_tab.next_row(),
|
||||
Tab::Weather => self.weather_tab.next(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_tab(&mut self) {
|
||||
self.tab = self.tab.prev();
|
||||
}
|
||||
|
||||
fn next_tab(&mut self) {
|
||||
self.tab = self.tab.next();
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.mode = Mode::Destroy;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the
|
||||
/// entire app state on every frame. For this example, the app state is small enough that it doesn't
|
||||
/// matter, but for larger apps this can be a significant performance improvement.
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [title_bar, tab, bottom_bar] = area.layout(&layout);
|
||||
|
||||
Block::new().style(THEME.root).render(area, buf);
|
||||
self.render_title_bar(title_bar, buf);
|
||||
self.render_selected_tab(tab, buf);
|
||||
App::render_bottom_bar(bottom_bar, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
|
||||
let [title, tabs] = area.layout(&layout);
|
||||
|
||||
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
||||
let titles = Tab::iter().map(Tab::title);
|
||||
Tabs::new(titles)
|
||||
.style(THEME.tabs)
|
||||
.highlight_style(THEME.tabs_selected)
|
||||
.select(self.tab as usize)
|
||||
.divider("")
|
||||
.padding("", "")
|
||||
.render(tabs, buf);
|
||||
}
|
||||
|
||||
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.render(area, buf),
|
||||
Tab::Recipe => self.recipe_tab.render(area, buf),
|
||||
Tab::Email => self.email_tab.render(area, buf),
|
||||
Tab::Traceroute => self.traceroute_tab.render(area, buf),
|
||||
Tab::Weather => self.weather_tab.render(area, buf),
|
||||
};
|
||||
}
|
||||
|
||||
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
|
||||
let keys = [
|
||||
("H/←", "Left"),
|
||||
("L/→", "Right"),
|
||||
("K/↑", "Up"),
|
||||
("J/↓", "Down"),
|
||||
("D/Del", "Destroy"),
|
||||
("Q/Esc", "Quit"),
|
||||
];
|
||||
let spans = keys
|
||||
.iter()
|
||||
.flat_map(|(key, desc)| {
|
||||
let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
|
||||
let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
|
||||
[key, desc]
|
||||
})
|
||||
.collect_vec();
|
||||
Line::from(spans)
|
||||
.centered()
|
||||
.style((Color::Indexed(236), Color::Indexed(232)))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn prev(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let prev_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(prev_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn title(self) -> String {
|
||||
match self {
|
||||
Self::About => String::new(),
|
||||
tab => format!(" {tab} "),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use palette::{IntoColor, Okhsv, Srgb};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
/// A widget that renders a color swatch of RGB colors.
|
||||
///
|
||||
/// The widget is rendered as a rectangle with the hue changing along the x-axis from 0.0 to 360.0
|
||||
/// and the value changing along the y-axis (from 1.0 to 0.0). Each pixel is rendered as a block
|
||||
/// character with the top half slightly lighter than the bottom half.
|
||||
pub struct RgbSwatch;
|
||||
|
||||
impl Widget for RgbSwatch {
|
||||
#[expect(clippy::cast_precision_loss, clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let value = f32::from(area.height) - yi as f32;
|
||||
let value_fg = value / f32::from(area.height);
|
||||
let value_bg = (value - 0.5) / f32::from(area.height);
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
let hue = xi as f32 * 360.0 / f32::from(area.width);
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
|
||||
buf[(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a hue and value into an RGB color via the Oklab color space.
|
||||
///
|
||||
/// See <https://bottosson.github.io/posts/oklab/> for more details.
|
||||
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {
|
||||
let color: Srgb = Okhsv::new(hue, saturation, value).into_color();
|
||||
let color = color.into_format();
|
||||
Color::Rgb(color.red, color.green, color.blue)
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
use rand::Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use ratatui::Frame;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Flex, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
/// delay the start of the animation so it doesn't start immediately
|
||||
const DELAY: usize = 120;
|
||||
/// higher means more pixels per frame are modified in the animation
|
||||
const DRIP_SPEED: usize = 500;
|
||||
/// delay the start of the text animation so it doesn't start immediately after the initial delay
|
||||
const TEXT_DELAY: usize = 180;
|
||||
|
||||
/// Destroy mode activated by pressing `d`
|
||||
pub fn destroy(frame: &mut Frame<'_>) {
|
||||
let frame_count = frame.count().saturating_sub(DELAY);
|
||||
if frame_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let area = frame.area();
|
||||
let buf = frame.buffer_mut();
|
||||
|
||||
drip(frame_count, area, buf);
|
||||
text(frame_count, area, buf);
|
||||
}
|
||||
|
||||
/// Move a bunch of random pixels down one row.
|
||||
///
|
||||
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
|
||||
/// do this, but it works well enough for this demo.
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
// a seeded rng as we have to move the same random pixels each frame
|
||||
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
|
||||
let ramp_frames = 450;
|
||||
let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
|
||||
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
|
||||
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
|
||||
for _ in 0..pixel_count {
|
||||
let src_x = rng.random_range(0..area.width);
|
||||
let src_y = rng.random_range(1..area.height - 2);
|
||||
let src = buf[(src_x, src_y)].clone();
|
||||
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
|
||||
if rng.random_ratio(1, 100) {
|
||||
let dest_x = rng
|
||||
.random_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
|
||||
.clamp(area.left(), area.right() - 1);
|
||||
let dest_y = area.top() + 1;
|
||||
|
||||
let dest = &mut buf[(dest_x, dest_y)];
|
||||
// copy the cell to the new location about 1/10 of the time blank out the cell the rest
|
||||
// of the time. This has the effect of gradually removing the pixels from the screen.
|
||||
if rng.random_ratio(1, 10) {
|
||||
*dest = src;
|
||||
} else {
|
||||
dest.reset();
|
||||
}
|
||||
} else {
|
||||
// move the pixel down one row
|
||||
let dest_x = src_x;
|
||||
let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
|
||||
// copy the cell to the new location
|
||||
buf[(dest_x, dest_y)] = src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// draw some text fading in and out from black to red and back
|
||||
#[expect(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
|
||||
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
|
||||
if sub_frame == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let logo = indoc::indoc! {"
|
||||
██████ ████ ██████ ████ ██████ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██████ ████████ ██ ████████ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ████ ██
|
||||
"};
|
||||
let logo_text = Text::styled(logo, Color::Rgb(255, 255, 255));
|
||||
let area = centered_rect(area, logo_text.width() as u16, logo_text.height() as u16);
|
||||
|
||||
let mask_buf = &mut Buffer::empty(area);
|
||||
logo_text.render(area, mask_buf);
|
||||
|
||||
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
|
||||
|
||||
for row in area.rows() {
|
||||
for col in row.columns() {
|
||||
let cell = &mut buf[(col.x, col.y)];
|
||||
let mask_cell = &mut mask_buf[(col.x, col.y)];
|
||||
cell.set_symbol(mask_cell.symbol());
|
||||
|
||||
// blend the mask cell color with the cell color
|
||||
let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
|
||||
let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
|
||||
|
||||
let color = blend(mask_color, cell_color, percentage);
|
||||
cell.set_style(Style::new().fg(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||
let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
|
||||
return mask_color;
|
||||
};
|
||||
let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
|
||||
return mask_color;
|
||||
};
|
||||
|
||||
let remain = 1.0 - percentage;
|
||||
|
||||
let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
|
||||
let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
|
||||
let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
|
||||
|
||||
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
Color::Rgb(red as u8, green as u8, blue as u8)
|
||||
}
|
||||
|
||||
/// a centered rect of the given size
|
||||
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
|
||||
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
|
||||
let vertical = Layout::vertical([height]).flex(Flex::Center);
|
||||
let [area] = area.layout(&vertical);
|
||||
let [area] = area.layout(&horizontal);
|
||||
area
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//! # [Ratatui] Demo2 example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod colors;
|
||||
mod destroy;
|
||||
mod tabs;
|
||||
mod theme;
|
||||
|
||||
use std::io::stdout;
|
||||
|
||||
use app::App;
|
||||
use color_eyre::Result;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::{TerminalOptions, Viewport};
|
||||
|
||||
pub use self::colors::{RgbSwatch, color_from_oklab};
|
||||
pub use self::theme::THEME;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
// this size is to match the size of the terminal when running the demo
|
||||
// using vhs in a 1280x640 sized window (github social preview size)
|
||||
let viewport = Viewport::Fixed(Rect::new(0, 0, 81, 18));
|
||||
let terminal = ratatui::init_with_options(TerminalOptions { viewport });
|
||||
execute!(stdout(), EnterAlternateScreen).expect("failed to enter alternate screen");
|
||||
let app_result = App::default().run(terminal);
|
||||
execute!(stdout(), LeaveAlternateScreen).expect("failed to leave alternate screen");
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
mod about;
|
||||
mod email;
|
||||
mod recipe;
|
||||
mod traceroute;
|
||||
mod weather;
|
||||
|
||||
pub use about::AboutTab;
|
||||
pub use email::EmailTab;
|
||||
pub use recipe::RecipeTab;
|
||||
pub use traceroute::TracerouteTab;
|
||||
pub use weather::WeatherTab;
|
||||
@@ -1,73 +0,0 @@
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin, Rect};
|
||||
use ratatui::widgets::{
|
||||
Block, Borders, Clear, MascotEyeColor, Padding, Paragraph, RatatuiMascot, Widget, Wrap,
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct AboutTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl AboutTab {
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AboutTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let layout = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [logo_area, description] = area.layout(&layout);
|
||||
render_crate_description(description, buf);
|
||||
let eye_state = if self.row_index % 2 == 0 {
|
||||
MascotEyeColor::Default
|
||||
} else {
|
||||
MascotEyeColor::Red
|
||||
};
|
||||
RatatuiMascot::default().set_eye(eye_state).render(
|
||||
logo_area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
}),
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf); // clear out the color swatches
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
let text = "- cooking up terminal user interfaces -
|
||||
|
||||
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
|
||||
screen efficiently every frame.";
|
||||
Paragraph::new(text)
|
||||
.style(THEME.description)
|
||||
.block(
|
||||
Block::new()
|
||||
.title(" Ratatui ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::TOP)
|
||||
.border_style(THEME.description_title)
|
||||
.padding(Padding::new(0, 0, 0, 0)),
|
||||
)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((0, 0))
|
||||
.render(area, buf);
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Margin, Rect};
|
||||
use ratatui::style::{Styled, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{
|
||||
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph, Scrollbar,
|
||||
ScrollbarState, StatefulWidget, Tabs, Widget,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Email {
|
||||
from: &'static str,
|
||||
subject: &'static str,
|
||||
body: &'static str,
|
||||
}
|
||||
|
||||
const EMAILS: &[Email] = &[
|
||||
Email {
|
||||
from: "Alice <alice@example.com>",
|
||||
subject: "Hello",
|
||||
body: "Hi Bob,\nHow are you?\n\nAlice",
|
||||
},
|
||||
Email {
|
||||
from: "Bob <bob@example.com>",
|
||||
subject: "Re: Hello",
|
||||
body: "Hi Alice,\nI'm fine, thanks!\n\nBob",
|
||||
},
|
||||
Email {
|
||||
from: "Charlie <charlie@example.com>",
|
||||
subject: "Re: Hello",
|
||||
body: "Hi Alice,\nI'm fine, thanks!\n\nCharlie",
|
||||
},
|
||||
Email {
|
||||
from: "Dave <dave@example.com>",
|
||||
subject: "Re: Hello (STOP REPLYING TO ALL)",
|
||||
body: "Hi Everyone,\nPlease stop replying to all.\n\nDave",
|
||||
},
|
||||
Email {
|
||||
from: "Eve <eve@example.com>",
|
||||
subject: "Re: Hello (STOP REPLYING TO ALL)",
|
||||
body: "Hi Everyone,\nI'm reading all your emails.\n\nEve",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct EmailTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl EmailTab {
|
||||
/// Select the previous email (with wrap around).
|
||||
pub fn prev(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(EMAILS.len() - 1) % EMAILS.len();
|
||||
}
|
||||
|
||||
/// Select the next email (with wrap around).
|
||||
pub fn next(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1) % EMAILS.len();
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for EmailTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
let [inbox, email] = area.layout(&layout);
|
||||
render_inbox(self.row_index, inbox, buf);
|
||||
render_email(self.row_index, email, buf);
|
||||
}
|
||||
}
|
||||
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [tabs, inbox] = area.layout(&layout);
|
||||
let theme = THEME.email;
|
||||
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
|
||||
.style(theme.tabs)
|
||||
.highlight_style(theme.tabs_selected)
|
||||
.select(0)
|
||||
.divider("")
|
||||
.render(tabs, buf);
|
||||
|
||||
let highlight_symbol = ">>";
|
||||
let from_width = EMAILS
|
||||
.iter()
|
||||
.map(|e| e.from.width())
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let items = EMAILS.iter().map(|e| {
|
||||
let from = format!("{:width$}", e.from, width = from_width).into();
|
||||
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
|
||||
});
|
||||
let mut state = ListState::default().with_selected(Some(selected_index));
|
||||
StatefulWidget::render(
|
||||
List::new(items)
|
||||
.style(theme.inbox)
|
||||
.highlight_style(theme.selected_item)
|
||||
.highlight_symbol(highlight_symbol),
|
||||
inbox,
|
||||
buf,
|
||||
&mut state,
|
||||
);
|
||||
let mut scrollbar_state = ScrollbarState::default()
|
||||
.content_length(EMAILS.len())
|
||||
.position(selected_index);
|
||||
Scrollbar::default()
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.track_symbol(None)
|
||||
.thumb_symbol("▐")
|
||||
.render(inbox, buf, &mut scrollbar_state);
|
||||
}
|
||||
|
||||
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
let theme = THEME.email;
|
||||
let email = EMAILS.get(selected_index);
|
||||
let block = Block::new()
|
||||
.style(theme.body)
|
||||
.padding(Padding::new(2, 2, 0, 0))
|
||||
.borders(Borders::TOP)
|
||||
.border_type(BorderType::Thick);
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
if let Some(email) = email {
|
||||
let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||
let [headers_area, body_area] = inner.layout(&layout);
|
||||
let headers = vec![
|
||||
Line::from(vec![
|
||||
"From: ".set_style(theme.header),
|
||||
email.from.set_style(theme.header_value),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Subject: ".set_style(theme.header),
|
||||
email.subject.set_style(theme.header_value),
|
||||
]),
|
||||
"-".repeat(inner.width as usize).dim().into(),
|
||||
];
|
||||
Paragraph::new(headers)
|
||||
.style(theme.body)
|
||||
.render(headers_area, buf);
|
||||
let body = email.body.lines().map(Line::from).collect_vec();
|
||||
Paragraph::new(body)
|
||||
.style(theme.body)
|
||||
.render(body_area, buf);
|
||||
} else {
|
||||
Paragraph::new("No email selected").render(inner, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin, Rect};
|
||||
use ratatui::style::{Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{
|
||||
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
StatefulWidget, Table, TableState, Widget, Wrap,
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Ingredient {
|
||||
quantity: &'static str,
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
impl Ingredient {
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn height(&self) -> u16 {
|
||||
self.name.lines().count() as u16
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ingredient> for Row<'_> {
|
||||
fn from(i: Ingredient) -> Self {
|
||||
Row::new(vec![i.quantity, i.name]).height(i.height())
|
||||
}
|
||||
}
|
||||
|
||||
// https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille
|
||||
const RECIPE: &[(&str, &str)] = &[
|
||||
(
|
||||
"Step 1: ",
|
||||
"Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \
|
||||
leaf, stirring occasionally, until the onion has softened.",
|
||||
),
|
||||
(
|
||||
"Step 2: ",
|
||||
"Add the eggplant and cook, stirring occasionally, for 8 minutes or until the eggplant \
|
||||
has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and cook over \
|
||||
medium heat, stirring occasionally, for 5 to 7 minutes or until the vegetables are \
|
||||
tender. Stir in the basil and few grinds of pepper to taste.",
|
||||
),
|
||||
];
|
||||
|
||||
const INGREDIENTS: &[Ingredient] = &[
|
||||
Ingredient {
|
||||
quantity: "4 tbsp",
|
||||
name: "olive oil",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1",
|
||||
name: "onion thinly sliced",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "4",
|
||||
name: "cloves garlic\npeeled and sliced",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1",
|
||||
name: "small bay leaf",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1",
|
||||
name: "small eggplant cut\ninto 1/2 inch cubes",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1",
|
||||
name: "small zucchini halved\nlengthwise and cut\ninto thin slices",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1",
|
||||
name: "red bell pepper cut\ninto slivers",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "4",
|
||||
name: "plum tomatoes\ncoarsely chopped",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1 tsp",
|
||||
name: "kosher salt",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "1/4 cup",
|
||||
name: "shredded fresh basil\nleaves",
|
||||
},
|
||||
Ingredient {
|
||||
quantity: "",
|
||||
name: "freshly ground black\npepper",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RecipeTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl RecipeTab {
|
||||
/// Select the previous item in the ingredients list (with wrap around)
|
||||
pub fn prev(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(INGREDIENTS.len() - 1) % INGREDIENTS.len();
|
||||
}
|
||||
|
||||
/// Select the next item in the ingredients list (with wrap around)
|
||||
pub fn next(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1) % INGREDIENTS.len();
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RecipeTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
Block::new()
|
||||
.title("Ratatouille Recipe".bold().white())
|
||||
.title_alignment(Alignment::Center)
|
||||
.style(THEME.content)
|
||||
.padding(Padding::new(1, 1, 2, 1))
|
||||
.render(area, buf);
|
||||
|
||||
let scrollbar_area = Rect {
|
||||
y: area.y + 2,
|
||||
height: area.height - 3,
|
||||
..area
|
||||
};
|
||||
render_scrollbar(self.row_index, scrollbar_area, buf);
|
||||
|
||||
let area = area.inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let layout = Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]);
|
||||
let [recipe, ingredients] = area.layout(&layout);
|
||||
|
||||
render_recipe(recipe, buf);
|
||||
render_ingredients(self.row_index, ingredients, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_recipe(area: Rect, buf: &mut Buffer) {
|
||||
let lines = RECIPE
|
||||
.iter()
|
||||
.map(|(step, text)| Line::from(vec![step.white().bold(), text.gray()]))
|
||||
.collect_vec();
|
||||
Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: true })
|
||||
.block(Block::new().padding(Padding::new(0, 1, 0, 0)))
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default().with_selected(Some(selected_row));
|
||||
let rows = INGREDIENTS.iter().copied();
|
||||
let theme = THEME.recipe;
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
.block(Block::new().style(theme.ingredients))
|
||||
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
|
||||
.row_highlight_style(Style::new().light_yellow()),
|
||||
area,
|
||||
buf,
|
||||
&mut state,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = ScrollbarState::default()
|
||||
.content_length(INGREDIENTS.len())
|
||||
.viewport_content_length(6)
|
||||
.position(position);
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.track_symbol(None)
|
||||
.thumb_symbol("▐")
|
||||
.render(area, buf, &mut state);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin, Rect};
|
||||
use ratatui::style::{Styled, Stylize};
|
||||
use ratatui::symbols::Marker;
|
||||
use ratatui::widgets::canvas::{self, Canvas, Map, MapResolution, Points};
|
||||
use ratatui::widgets::{
|
||||
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
Sparkline, StatefulWidget, Table, TableState, Widget,
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct TracerouteTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl TracerouteTab {
|
||||
/// Select the previous row (with wrap around).
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(HOPS.len() - 1) % HOPS.len();
|
||||
}
|
||||
|
||||
/// Select the next row (with wrap around).
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1) % HOPS.len();
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TracerouteTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
|
||||
let [left, map] = area.layout(&horizontal);
|
||||
let [hops, pings] = left.layout(&vertical);
|
||||
|
||||
render_hops(self.row_index, hops, buf);
|
||||
render_ping(self.row_index, pings, buf);
|
||||
render_map(self.row_index, map, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default().with_selected(Some(selected_row));
|
||||
let rows = HOPS.iter().map(|hop| Row::new(vec![hop.host, hop.address]));
|
||||
let block = Block::new()
|
||||
.padding(Padding::new(1, 1, 1, 1))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("Traceroute bad.horse".bold().white());
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
.row_highlight_style(THEME.traceroute.selected)
|
||||
.block(block),
|
||||
area,
|
||||
buf,
|
||||
&mut state,
|
||||
);
|
||||
let mut scrollbar_state = ScrollbarState::default()
|
||||
.content_length(HOPS.len())
|
||||
.position(selected_row);
|
||||
let area = Rect {
|
||||
width: area.width + 1,
|
||||
y: area.y + 3,
|
||||
height: area.height - 4,
|
||||
..area
|
||||
};
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None)
|
||||
.track_symbol(None)
|
||||
.thumb_symbol("▌")
|
||||
.render(area, buf, &mut scrollbar_state);
|
||||
}
|
||||
|
||||
pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
let mut data = [
|
||||
8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
|
||||
7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
|
||||
3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
|
||||
];
|
||||
let mid = progress % data.len();
|
||||
data.rotate_left(mid);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.title("Ping")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Thick),
|
||||
)
|
||||
.data(data)
|
||||
.style(THEME.traceroute.ping)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let theme = THEME.traceroute.map;
|
||||
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
|
||||
let map = Map {
|
||||
resolution: MapResolution::High,
|
||||
color: theme.color,
|
||||
};
|
||||
Canvas::default()
|
||||
.background_color(theme.background_color)
|
||||
.block(
|
||||
Block::new()
|
||||
.padding(Padding::new(1, 0, 1, 0))
|
||||
.style(theme.style),
|
||||
)
|
||||
.marker(Marker::HalfBlock)
|
||||
// picked to show Australia for the demo as it's the most interesting part of the map
|
||||
// (and the only part with hops ;))
|
||||
.x_bounds([112.0, 155.0])
|
||||
.y_bounds([-46.0, -11.0])
|
||||
.paint(|context| {
|
||||
context.draw(&map);
|
||||
if let Some(path) = path {
|
||||
context.draw(&canvas::Line::new(
|
||||
path.0.location.0,
|
||||
path.0.location.1,
|
||||
path.1.location.0,
|
||||
path.1.location.1,
|
||||
theme.path,
|
||||
));
|
||||
context.draw(&Points {
|
||||
color: theme.source,
|
||||
coords: &[path.0.location], // sydney
|
||||
});
|
||||
context.draw(&Points {
|
||||
color: theme.destination,
|
||||
coords: &[path.1.location], // perth
|
||||
});
|
||||
}
|
||||
})
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Hop {
|
||||
host: &'static str,
|
||||
address: &'static str,
|
||||
location: (f64, f64),
|
||||
}
|
||||
|
||||
impl Hop {
|
||||
const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self {
|
||||
Self {
|
||||
host: name,
|
||||
address,
|
||||
location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CANBERRA: (f64, f64) = (149.1, -35.3);
|
||||
const SYDNEY: (f64, f64) = (151.1, -33.9);
|
||||
const MELBOURNE: (f64, f64) = (144.9, -37.8);
|
||||
const PERTH: (f64, f64) = (115.9, -31.9);
|
||||
const DARWIN: (f64, f64) = (130.8, -12.4);
|
||||
const BRISBANE: (f64, f64) = (153.0, -27.5);
|
||||
const ADELAIDE: (f64, f64) = (138.6, -34.9);
|
||||
|
||||
// Go traceroute bad.horse some time, it's fun. these locations are made up and don't correspond
|
||||
// to the actual IP addresses (which are in Toronto, Canada).
|
||||
const HOPS: &[Hop] = &[
|
||||
Hop::new("home", "127.0.0.1", CANBERRA),
|
||||
Hop::new("bad.horse", "162.252.205.130", SYDNEY),
|
||||
Hop::new("bad.horse", "162.252.205.131", MELBOURNE),
|
||||
Hop::new("bad.horse", "162.252.205.132", BRISBANE),
|
||||
Hop::new("bad.horse", "162.252.205.133", SYDNEY),
|
||||
Hop::new("he.rides.across.the.nation", "162.252.205.134", PERTH),
|
||||
Hop::new("the.thoroughbred.of.sin", "162.252.205.135", DARWIN),
|
||||
Hop::new("he.got.the.application", "162.252.205.136", BRISBANE),
|
||||
Hop::new("that.you.just.sent.in", "162.252.205.137", ADELAIDE),
|
||||
Hop::new("it.needs.evaluation", "162.252.205.138", DARWIN),
|
||||
Hop::new("so.let.the.games.begin", "162.252.205.139", PERTH),
|
||||
Hop::new("a.heinous.crime", "162.252.205.140", BRISBANE),
|
||||
Hop::new("a.show.of.force", "162.252.205.141", CANBERRA),
|
||||
Hop::new("a.murder.would.be.nice.of.course", "162.252.205.142", PERTH),
|
||||
Hop::new("bad.horse", "162.252.205.143", MELBOURNE),
|
||||
Hop::new("bad.horse", "162.252.205.144", DARWIN),
|
||||
Hop::new("bad.horse", "162.252.205.145", MELBOURNE),
|
||||
Hop::new("he-s.bad", "162.252.205.146", PERTH),
|
||||
Hop::new("the.evil.league.of.evil", "162.252.205.147", BRISBANE),
|
||||
Hop::new("is.watching.so.beware", "162.252.205.148", DARWIN),
|
||||
Hop::new("the.grade.that.you.receive", "162.252.205.149", PERTH),
|
||||
Hop::new("will.be.your.last.we.swear", "162.252.205.150", ADELAIDE),
|
||||
Hop::new("so.make.the.bad.horse.gleeful", "162.252.205.151", SYDNEY),
|
||||
Hop::new("or.he-ll.make.you.his.mare", "162.252.205.152", MELBOURNE),
|
||||
Hop::new("o_o", "162.252.205.153", BRISBANE),
|
||||
Hop::new("you-re.saddled.up", "162.252.205.154", DARWIN),
|
||||
Hop::new("there-s.no.recourse", "162.252.205.155", PERTH),
|
||||
Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY),
|
||||
Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA),
|
||||
];
|
||||
@@ -1,161 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use palette::Okhsv;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::symbols;
|
||||
use ratatui::widgets::calendar::{CalendarEventStore, Monthly};
|
||||
use ratatui::widgets::{Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{RgbSwatch, THEME, color_from_oklab};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct WeatherTab {
|
||||
pub download_progress: usize,
|
||||
}
|
||||
|
||||
impl WeatherTab {
|
||||
/// Simulate a download indicator by decrementing the row index.
|
||||
pub fn prev(&mut self) {
|
||||
self.download_progress = self.download_progress.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Simulate a download indicator by incrementing the row index.
|
||||
pub fn next(&mut self) {
|
||||
self.download_progress = self.download_progress.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for WeatherTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
|
||||
let area = area.inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let tab_layout = Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [main, _, gauges] = area.layout(&tab_layout);
|
||||
let main_layout = Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]);
|
||||
let [calendar, charts] = main.layout(&main_layout);
|
||||
let charts_layout = Layout::vertical([Constraint::Length(29), Constraint::Min(0)]);
|
||||
let [simple, horizontal] = charts.layout(&charts_layout);
|
||||
|
||||
render_calendar(calendar, buf);
|
||||
render_simple_barchart(simple, buf);
|
||||
render_horizontal_barchart(horizontal, buf);
|
||||
render_gauge(self.download_progress, gauges, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_calendar(area: Rect, buf: &mut Buffer) {
|
||||
let date = OffsetDateTime::now_utc().date();
|
||||
Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
|
||||
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||
.show_month_header(Style::new().bold())
|
||||
.show_weekdays_header(Style::new().italic())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
|
||||
let data = [
|
||||
("Sat", 76),
|
||||
("Sun", 69),
|
||||
("Mon", 65),
|
||||
("Tue", 67),
|
||||
("Wed", 65),
|
||||
("Thu", 69),
|
||||
("Fri", 73),
|
||||
];
|
||||
let data = data
|
||||
.into_iter()
|
||||
.map(|(label, value)| {
|
||||
Bar::default()
|
||||
.value(value)
|
||||
// This doesn't actually render correctly as the text is too wide for the bar
|
||||
// See https://github.com/ratatui/ratatui/issues/513 for more info
|
||||
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
|
||||
.text_value(format!("{value}°"))
|
||||
.style(if value > 70 {
|
||||
Style::new().fg(Color::Red)
|
||||
} else {
|
||||
Style::new().fg(Color::Yellow)
|
||||
})
|
||||
.value_style(if value > 70 {
|
||||
Style::new().fg(Color::Gray).bg(Color::Red).bold()
|
||||
} else {
|
||||
Style::new().fg(Color::DarkGray).bg(Color::Yellow).bold()
|
||||
})
|
||||
.label(label)
|
||||
})
|
||||
.collect_vec();
|
||||
let group = BarGroup::default().bars(&data);
|
||||
BarChart::default()
|
||||
.data(group)
|
||||
.bar_width(3)
|
||||
.bar_gap(1)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||
let bg = Color::Rgb(32, 48, 96);
|
||||
let data = [
|
||||
Bar::default().text_value("Winter 37-51").value(51),
|
||||
Bar::default().text_value("Spring 40-65").value(65),
|
||||
Bar::default().text_value("Summer 54-77").value(77),
|
||||
Bar::default()
|
||||
.text_value("Fall 41-71")
|
||||
.value(71)
|
||||
.value_style(Style::new().bold()), // current season
|
||||
];
|
||||
let group = BarGroup::default().label("GPU").bars(&data);
|
||||
BarChart::default()
|
||||
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||
.direction(Direction::Horizontal)
|
||||
.data(group)
|
||||
.bar_gap(1)
|
||||
.bar_style(Style::new().fg(bg))
|
||||
.value_style(Style::new().bg(bg).fg(Color::Gray))
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
let percent = (progress * 3).min(100) as f64;
|
||||
|
||||
render_line_gauge(percent, area, buf);
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||
let hue = 90.0 - (percent as f32 * 0.6);
|
||||
let value = Okhsv::max_value();
|
||||
let filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let label = if percent < 100.0 {
|
||||
format!("Downloading: {percent}%")
|
||||
} else {
|
||||
"Download Complete!".into()
|
||||
};
|
||||
LineGauge::default()
|
||||
.ratio(percent / 100.0)
|
||||
.label(label)
|
||||
.style(Style::new().light_blue())
|
||||
.filled_style(Style::new().fg(filled_color))
|
||||
.unfilled_style(Style::new().fg(unfilled_color))
|
||||
.filled_symbol(symbols::line::THICK_HORIZONTAL)
|
||||
.unfilled_symbol(symbols::line::THICK_HORIZONTAL)
|
||||
.render(area, buf);
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
pub struct Theme {
|
||||
pub root: Style,
|
||||
pub content: Style,
|
||||
pub app_title: Style,
|
||||
pub tabs: Style,
|
||||
pub tabs_selected: Style,
|
||||
pub borders: Style,
|
||||
pub description: Style,
|
||||
pub description_title: Style,
|
||||
pub key_binding: KeyBinding,
|
||||
pub logo: Logo,
|
||||
pub email: Email,
|
||||
pub traceroute: Traceroute,
|
||||
pub recipe: Recipe,
|
||||
}
|
||||
|
||||
pub struct KeyBinding {
|
||||
pub key: Style,
|
||||
pub description: Style,
|
||||
}
|
||||
|
||||
pub struct Logo {
|
||||
pub rat_eye: Color,
|
||||
pub rat_eye_alt: Color,
|
||||
}
|
||||
|
||||
pub struct Email {
|
||||
pub tabs: Style,
|
||||
pub tabs_selected: Style,
|
||||
pub inbox: Style,
|
||||
pub item: Style,
|
||||
pub selected_item: Style,
|
||||
pub header: Style,
|
||||
pub header_value: Style,
|
||||
pub body: Style,
|
||||
}
|
||||
|
||||
pub struct Traceroute {
|
||||
pub header: Style,
|
||||
pub selected: Style,
|
||||
pub ping: Style,
|
||||
pub map: Map,
|
||||
}
|
||||
|
||||
pub struct Map {
|
||||
pub style: Style,
|
||||
pub color: Color,
|
||||
pub path: Color,
|
||||
pub source: Color,
|
||||
pub destination: Color,
|
||||
pub background_color: Color,
|
||||
}
|
||||
|
||||
pub struct Recipe {
|
||||
pub ingredients: Style,
|
||||
pub ingredients_header: Style,
|
||||
}
|
||||
|
||||
pub const THEME: Theme = Theme {
|
||||
root: Style::new().bg(DARK_BLUE),
|
||||
content: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||
app_title: Style::new()
|
||||
.fg(WHITE)
|
||||
.bg(DARK_BLUE)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
|
||||
tabs_selected: Style::new()
|
||||
.fg(WHITE)
|
||||
.bg(DARK_BLUE)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED),
|
||||
borders: Style::new().fg(LIGHT_GRAY),
|
||||
description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE),
|
||||
description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD),
|
||||
logo: Logo {
|
||||
rat_eye: BLACK,
|
||||
rat_eye_alt: RED,
|
||||
},
|
||||
key_binding: KeyBinding {
|
||||
key: Style::new().fg(BLACK).bg(DARK_GRAY),
|
||||
description: Style::new().fg(DARK_GRAY).bg(BLACK),
|
||||
},
|
||||
email: Email {
|
||||
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
|
||||
tabs_selected: Style::new()
|
||||
.fg(WHITE)
|
||||
.bg(DARK_BLUE)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
inbox: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||
item: Style::new().fg(LIGHT_GRAY),
|
||||
selected_item: Style::new().fg(LIGHT_YELLOW),
|
||||
header: Style::new().add_modifier(Modifier::BOLD),
|
||||
header_value: Style::new().fg(LIGHT_GRAY),
|
||||
body: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||
},
|
||||
traceroute: Traceroute {
|
||||
header: Style::new()
|
||||
.bg(DARK_BLUE)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
selected: Style::new().fg(LIGHT_YELLOW),
|
||||
ping: Style::new().fg(WHITE),
|
||||
map: Map {
|
||||
style: Style::new().bg(DARK_BLUE),
|
||||
background_color: DARK_BLUE,
|
||||
color: LIGHT_GRAY,
|
||||
path: LIGHT_BLUE,
|
||||
source: LIGHT_GREEN,
|
||||
destination: LIGHT_RED,
|
||||
},
|
||||
},
|
||||
recipe: Recipe {
|
||||
ingredients: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||
ingredients_header: Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
},
|
||||
};
|
||||
|
||||
const DARK_BLUE: Color = Color::Rgb(16, 24, 48);
|
||||
const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
|
||||
const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96);
|
||||
const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96);
|
||||
const LIGHT_RED: Color = Color::Rgb(192, 96, 96);
|
||||
const RED: Color = Color::Rgb(215, 0, 0);
|
||||
const BLACK: Color = Color::Rgb(8, 8, 8); // not really black, often #080808
|
||||
const DARK_GRAY: Color = Color::Rgb(68, 68, 68);
|
||||
const MID_GRAY: Color = Color::Rgb(128, 128, 128);
|
||||
const LIGHT_GRAY: Color = Color::Rgb(188, 188, 188);
|
||||
const WHITE: Color = Color::Rgb(238, 238, 238); // not really white, often #eeeeee
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "flex"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Flex demo
|
||||
|
||||
This interactive example shows how to use the flex layouts.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p flex
|
||||
```
|
||||
@@ -1,531 +0,0 @@
|
||||
/// A Ratatui example that demonstrates different types of flex layouts.
|
||||
///
|
||||
/// You can also change the spacing between the constraints, and toggle between different types
|
||||
/// of flex layouts.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio};
|
||||
use ratatui::layout::{Alignment, Flex, Layout, Rect};
|
||||
use ratatui::style::palette::tailwind;
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::symbols::{self, line};
|
||||
use ratatui::text::{Line, Text};
|
||||
use ratatui::widgets::{
|
||||
Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs, Widget,
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
(
|
||||
"Min(u16) takes any excess space always",
|
||||
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1, 10)],
|
||||
),
|
||||
(
|
||||
"Fill(u16) takes any excess space always",
|
||||
&[Length(20), Percentage(20), Ratio(1, 5), Fill(1)],
|
||||
),
|
||||
(
|
||||
"Here's all constraints in one line",
|
||||
&[
|
||||
Length(10),
|
||||
Min(10),
|
||||
Max(10),
|
||||
Percentage(10),
|
||||
Ratio(1, 10),
|
||||
Fill(1),
|
||||
],
|
||||
),
|
||||
("", &[Max(50), Min(50)]),
|
||||
("", &[Max(20), Length(10)]),
|
||||
("", &[Max(20), Length(10)]),
|
||||
(
|
||||
"Min grows always but also allows Fill to grow",
|
||||
&[Percentage(50), Fill(1), Fill(2), Min(50)],
|
||||
),
|
||||
(
|
||||
"In `Legacy`, the last constraint of lowest priority takes excess space",
|
||||
&[Length(20), Length(20), Percentage(20)],
|
||||
),
|
||||
("", &[Length(20), Percentage(20), Length(20)]),
|
||||
(
|
||||
"A lowest priority constraint will be broken before a high priority constraint",
|
||||
&[Ratio(1, 4), Percentage(20)],
|
||||
),
|
||||
(
|
||||
"`Length` is higher priority than `Percentage`",
|
||||
&[Percentage(20), Length(10)],
|
||||
),
|
||||
(
|
||||
"`Min/Max` is higher priority than `Length`",
|
||||
&[Length(10), Max(20)],
|
||||
),
|
||||
("", &[Length(100), Min(20)]),
|
||||
(
|
||||
"`Length` is higher priority than `Min/Max`",
|
||||
&[Max(20), Length(10)],
|
||||
),
|
||||
("", &[Min(20), Length(90)]),
|
||||
(
|
||||
"Fill is the lowest priority and will fill any excess space",
|
||||
&[Fill(1), Ratio(1, 4)],
|
||||
),
|
||||
(
|
||||
"Fill can be used to scale proportionally with other Fill blocks",
|
||||
&[Fill(1), Percentage(20), Fill(2)],
|
||||
),
|
||||
("", &[Ratio(1, 3), Percentage(20), Ratio(2, 3)]),
|
||||
(
|
||||
"Legacy will stretch the last lowest priority constraint\nStretch will only stretch equal weighted constraints",
|
||||
&[Length(20), Length(15)],
|
||||
),
|
||||
("", &[Percentage(20), Length(15)]),
|
||||
(
|
||||
"`Fill(u16)` fills up excess space, but is lower priority to spacers.\ni.e. Fill will only have widths in Flex::Stretch and Flex::Legacy",
|
||||
&[Fill(1), Fill(1)],
|
||||
),
|
||||
("", &[Length(20), Length(20)]),
|
||||
(
|
||||
"When not using `Flex::Stretch` or `Flex::Legacy`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
|
||||
&[Min(20), Max(20)],
|
||||
),
|
||||
("", &[Max(20)]),
|
||||
("", &[Min(20), Max(20), Length(20), Length(20)]),
|
||||
("", &[Fill(0), Fill(0)]),
|
||||
(
|
||||
"`Fill(1)` can be to scale with respect to other `Fill(2)`",
|
||||
&[Fill(1), Fill(2)],
|
||||
),
|
||||
("", &[Fill(1), Min(10), Max(10), Fill(2)]),
|
||||
(
|
||||
"`Fill(0)` collapses if there are other non-zero `Fill(_)`\nconstraints. e.g. `[Fill(0), Fill(0), Fill(1)]`:",
|
||||
&[Fill(0), Fill(0), Fill(1)],
|
||||
),
|
||||
];
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct App {
|
||||
selected_tab: SelectedTab,
|
||||
scroll_offset: u16,
|
||||
spacing: u16,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Example {
|
||||
constraints: Vec<Constraint>,
|
||||
description: String,
|
||||
flex: Flex,
|
||||
spacing: u16,
|
||||
}
|
||||
|
||||
/// Tabs for the different layouts
|
||||
///
|
||||
/// Note: the order of the variants will determine the order of the tabs this uses several derive
|
||||
/// macros from the `strum` crate to make it easier to iterate over the variants.
|
||||
/// (`FromRepr`,`Display`,`EnumIter`).
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
Legacy,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
SpaceAround,
|
||||
SpaceEvenly,
|
||||
SpaceBetween,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
// increase the layout cache to account for the number of layout events. This ensures that
|
||||
// layout is not generally reprocessed on every frame (which would lead to possible janky
|
||||
// results when there are more than one possible solution to the requested layout). This
|
||||
// assumes the user changes spacing about a 100 times or so.
|
||||
let cache_size = EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100;
|
||||
Layout::init_cache(NonZeroUsize::new(cache_size).unwrap());
|
||||
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_running(self) -> bool {
|
||||
self.state == AppState::Running
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
const fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn down(&mut self) {
|
||||
self.scroll_offset = self
|
||||
.scroll_offset
|
||||
.saturating_add(1)
|
||||
.min(max_scroll_offset());
|
||||
}
|
||||
|
||||
const fn top(&mut self) {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn bottom(&mut self) {
|
||||
self.scroll_offset = max_scroll_offset();
|
||||
}
|
||||
|
||||
const fn increment_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_add(1);
|
||||
}
|
||||
|
||||
const fn decrement_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_sub(1);
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
}
|
||||
|
||||
// when scrolling, make sure we don't scroll past the last example
|
||||
fn max_scroll_offset() -> u16 {
|
||||
example_height()
|
||||
- EXAMPLE_DATA
|
||||
.last()
|
||||
.map_or(0, |(desc, _)| get_description_height(desc) + 4)
|
||||
}
|
||||
|
||||
/// The height of all examples combined
|
||||
///
|
||||
/// Each may or may not have a title so we need to account for that.
|
||||
fn example_height() -> u16 {
|
||||
EXAMPLE_DATA
|
||||
.iter()
|
||||
.map(|(desc, _)| get_description_height(desc) + 4)
|
||||
.sum()
|
||||
}
|
||||
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]);
|
||||
let [tabs, axis, demo] = area.layout(&layout);
|
||||
self.tabs().render(tabs, buf);
|
||||
let scroll_needed = self.render_demo(demo, buf);
|
||||
let axis_width = if scroll_needed {
|
||||
axis.width.saturating_sub(1)
|
||||
} else {
|
||||
axis.width
|
||||
};
|
||||
Self::axis(axis_width, self.spacing).render(axis, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn tabs(self) -> impl Widget {
|
||||
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
|
||||
let block = Block::new()
|
||||
.title("Flex Layouts ".bold())
|
||||
.title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing ");
|
||||
Tabs::new(tab_titles)
|
||||
.block(block)
|
||||
.highlight_style(Modifier::REVERSED)
|
||||
.select(self.selected_tab as usize)
|
||||
.divider(" ")
|
||||
.padding("", "")
|
||||
}
|
||||
|
||||
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
|
||||
fn axis(width: u16, spacing: u16) -> impl Widget {
|
||||
let width = width as usize;
|
||||
// only show gap when spacing is not zero
|
||||
let label = if spacing != 0 {
|
||||
format!("{width} px (gap: {spacing} px)")
|
||||
} else {
|
||||
format!("{width} px")
|
||||
};
|
||||
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
Paragraph::new(width_bar.dark_gray()).centered()
|
||||
}
|
||||
|
||||
/// Render the demo content
|
||||
///
|
||||
/// This function renders the demo content into a separate buffer and then splices the buffer
|
||||
/// into the main buffer. This is done to make it possible to handle scrolling easily.
|
||||
///
|
||||
/// Returns bool indicating whether scroll was needed
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
|
||||
// render demo content into a separate buffer so all examples fit we add an extra
|
||||
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||
// at the max
|
||||
let height = example_height();
|
||||
let demo_area = Rect::new(0, 0, area.width, height);
|
||||
let mut demo_buf = Buffer::empty(demo_area);
|
||||
|
||||
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
|
||||
let content_area = if scrollbar_needed {
|
||||
Rect {
|
||||
width: demo_area.width - 1,
|
||||
..demo_area
|
||||
}
|
||||
} else {
|
||||
demo_area
|
||||
};
|
||||
|
||||
let mut spacing = self.spacing;
|
||||
self.selected_tab
|
||||
.render(content_area, &mut demo_buf, &mut spacing);
|
||||
|
||||
let visible_content = demo_buf
|
||||
.content
|
||||
.into_iter()
|
||||
.skip((area.width * self.scroll_offset) as usize)
|
||||
.take(area.area() as usize);
|
||||
for (i, cell) in visible_content.enumerate() {
|
||||
let x = i as u16 % area.width;
|
||||
let y = i as u16 / area.width;
|
||||
buf[(area.x + x, area.y + y)] = cell;
|
||||
}
|
||||
|
||||
if scrollbar_needed {
|
||||
let area = area.intersection(buf.area);
|
||||
let mut state = ScrollbarState::new(max_scroll_offset() as usize)
|
||||
.position(self.scroll_offset as usize);
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
|
||||
}
|
||||
scrollbar_needed
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
use tailwind::{INDIGO, ORANGE, SKY};
|
||||
let text = value.to_string();
|
||||
let color = match value {
|
||||
Self::Legacy => ORANGE.c400,
|
||||
Self::Start => SKY.c400,
|
||||
Self::Center => SKY.c300,
|
||||
Self::End => SKY.c200,
|
||||
Self::SpaceEvenly => INDIGO.c400,
|
||||
Self::SpaceBetween => INDIGO.c300,
|
||||
Self::SpaceAround => INDIGO.c500,
|
||||
};
|
||||
format!(" {text} ").fg(color).bg(Color::Black).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for SelectedTab {
|
||||
type State = u16;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
|
||||
let spacing = *spacing;
|
||||
match self {
|
||||
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing),
|
||||
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),
|
||||
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing),
|
||||
Self::End => Self::render_examples(area, buf, Flex::End, spacing),
|
||||
Self::SpaceEvenly => Self::render_examples(area, buf, Flex::SpaceEvenly, spacing),
|
||||
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
|
||||
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_examples(area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
|
||||
let heights = EXAMPLE_DATA
|
||||
.iter()
|
||||
.map(|(desc, _)| get_description_height(desc) + 4);
|
||||
let areas = Layout::vertical(heights).flex(Flex::Start).split(area);
|
||||
for (area, (description, constraints)) in areas.iter().zip(EXAMPLE_DATA.iter()) {
|
||||
Example::new(constraints, description, flex, spacing).render(*area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(constraints: &[Constraint], description: &str, flex: Flex, spacing: u16) -> Self {
|
||||
Self {
|
||||
constraints: constraints.into(),
|
||||
description: description.into(),
|
||||
flex,
|
||||
spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Example {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let title_height = get_description_height(&self.description);
|
||||
let layout = Layout::vertical([Length(title_height), Fill(0)]);
|
||||
let [title, illustrations] = area.layout(&layout);
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(self.flex)
|
||||
.spacing(self.spacing)
|
||||
.split_with_spacers(illustrations);
|
||||
|
||||
if !self.description.is_empty() {
|
||||
Paragraph::new(
|
||||
self.description
|
||||
.split('\n')
|
||||
.map(|s| format!("// {s}").italic().fg(tailwind::SLATE.c400))
|
||||
.map(Line::from)
|
||||
.collect::<Vec<Line>>(),
|
||||
)
|
||||
.render(title, buf);
|
||||
}
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
}
|
||||
|
||||
for spacer in spacers.iter() {
|
||||
Self::render_spacer(*spacer, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn render_spacer(spacer: Rect, buf: &mut Buffer) {
|
||||
if spacer.width > 1 {
|
||||
let corners_only = symbols::border::Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(corners_only)
|
||||
.border_style(Style::reset().dark_gray())
|
||||
.render(spacer, buf);
|
||||
} else {
|
||||
Paragraph::new(Text::from(vec![
|
||||
Line::from(""),
|
||||
Line::from("│"),
|
||||
Line::from("│"),
|
||||
Line::from(""),
|
||||
]))
|
||||
.style(Style::reset().dark_gray())
|
||||
.render(spacer, buf);
|
||||
}
|
||||
let width = spacer.width;
|
||||
let label = if width > 4 {
|
||||
format!("{width} px")
|
||||
} else if width > 2 {
|
||||
format!("{width}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let text = Text::from(vec![
|
||||
Line::raw(""),
|
||||
Line::raw(""),
|
||||
Line::styled(label, Style::reset().dark_gray()),
|
||||
]);
|
||||
Paragraph::new(text)
|
||||
.style(Style::reset().dark_gray())
|
||||
.alignment(Alignment::Center)
|
||||
.render(spacer, buf);
|
||||
}
|
||||
|
||||
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||
let main_color = color_for_constraint(constraint);
|
||||
let fg_color = Color::White;
|
||||
let title = format!("{constraint}");
|
||||
let content = format!("{width} px");
|
||||
let text = format!("{title}\n{content}");
|
||||
let block = Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(main_color).reversed())
|
||||
.style(Style::default().fg(fg_color).bg(main_color));
|
||||
Paragraph::new(text).centered().block(block)
|
||||
}
|
||||
}
|
||||
|
||||
const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
use tailwind::{BLUE, SLATE};
|
||||
match constraint {
|
||||
Constraint::Min(_) => BLUE.c900,
|
||||
Constraint::Max(_) => BLUE.c800,
|
||||
Constraint::Length(_) => SLATE.c700,
|
||||
Constraint::Percentage(_) => SLATE.c800,
|
||||
Constraint::Ratio(_, _) => SLATE.c900,
|
||||
Constraint::Fill(_) => SLATE.c950,
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
fn get_description_height(s: &str) -> u16 {
|
||||
if s.is_empty() {
|
||||
0
|
||||
} else {
|
||||
s.split('\n').count() as u16
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "gauge"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Gauge demo
|
||||
|
||||
This example shows different types of gauges.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p gauge
|
||||
```
|
||||
@@ -1,192 +0,0 @@
|
||||
/// A Ratatui example that demonstrates different types of gauges.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::palette::tailwind;
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Gauge, Padding, Paragraph, Widget};
|
||||
|
||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||
const GAUGE2_COLOR: Color = tailwind::GREEN.c800;
|
||||
const GAUGE3_COLOR: Color = tailwind::BLUE.c800;
|
||||
const GAUGE4_COLOR: Color = tailwind::ORANGE.c800;
|
||||
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
progress_columns: u16,
|
||||
progress1: u16,
|
||||
progress2: f64,
|
||||
progress3: f64,
|
||||
progress4: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Started,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
self.update(terminal.size()?.width);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, terminal_width: u16) {
|
||||
if self.state != AppState::Started {
|
||||
return;
|
||||
}
|
||||
|
||||
// progress1 and progress2 help show the difference between ratio and percentage measuring
|
||||
// the same thing, but converting to either a u16 or f64. Effectively, we're showing the
|
||||
// difference between how a continuous gauge acts for floor and rounded values.
|
||||
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
|
||||
self.progress1 = self.progress_columns * 100 / terminal_width;
|
||||
self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width);
|
||||
|
||||
// progress3 and progress4 similarly show the difference between unicode and non-unicode
|
||||
// gauges measuring the same thing.
|
||||
self.progress3 = (self.progress3 + 0.1).clamp(40.0, 100.0);
|
||||
self.progress4 = (self.progress4 + 0.1).clamp(40.0, 100.0);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
let timeout = Duration::from_secs_f32(1.0 / 20.0);
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn start(&mut self) {
|
||||
self.state = AppState::Started;
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
#[expect(clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, gauge_area, footer_area] = area.layout(&layout);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 4); 4]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = gauge_area.layout(&layout);
|
||||
|
||||
render_header(header_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
|
||||
self.render_gauge1(gauge1_area, buf);
|
||||
self.render_gauge2(gauge2_area, buf);
|
||||
self.render_gauge3(gauge3_area, buf);
|
||||
self.render_gauge4(gauge4_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Press ENTER to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with percentage");
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(GAUGE1_COLOR)
|
||||
.percent(self.progress1)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with ratio and custom label");
|
||||
let label = Span::styled(
|
||||
format!("{:.1}/100", self.progress2),
|
||||
Style::new().italic().bold().fg(CUSTOM_LABEL_COLOR),
|
||||
);
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(GAUGE2_COLOR)
|
||||
.ratio(self.progress2 / 100.0)
|
||||
.label(label)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with ratio (no unicode)");
|
||||
let label = format!("{:.1}%", self.progress3);
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(GAUGE3_COLOR)
|
||||
.ratio(self.progress3 / 100.0)
|
||||
.label(label)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge4(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with ratio (unicode)");
|
||||
let label = format!("{:.1}%", self.progress3);
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(GAUGE4_COLOR)
|
||||
.ratio(self.progress4 / 100.0)
|
||||
.label(label)
|
||||
.use_unicode(true)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block<'_> {
|
||||
let title = Line::from(title).centered();
|
||||
Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.padding(Padding::vertical(1))
|
||||
.title(title)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "hello-world"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user