Compare commits
3 Commits
vhs-test
...
docs/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d580bc96 | ||
|
|
392b28c194 | ||
|
|
5814c7c395 |
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.
|
||||
```
|
||||
25
.github/workflows/bench_base.yml
vendored
Normal file
25
.github/workflows/bench_base.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
benchmark_base_branch:
|
||||
name: Continuous Benchmarking with Bencher
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track base branch benchmarks with Bencher
|
||||
run: |
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch main \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
cargo bench
|
||||
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run and Cache Benchmarks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
benchmark_fork_pr_branch:
|
||||
name: Run Fork PR Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench > benchmark_results.txt
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_results.txt
|
||||
path: ./benchmark_results.txt
|
||||
- name: Upload GitHub Pull Request Event
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: event.json
|
||||
path: ${{ github.event_path }}
|
||||
56
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
56
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Track Benchmarks with Bencher
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Run and Cache Benchmarks]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
track_fork_pr_branch:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BENCHMARK_RESULTS: benchmark_results.txt
|
||||
PR_EVENT: event.json
|
||||
steps:
|
||||
- name: Download Benchmark Results
|
||||
uses: dawidd6/action-download-artifact@v8
|
||||
with:
|
||||
name: ${{ env.BENCHMARK_RESULTS }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
- name: Download PR Event
|
||||
uses: dawidd6/action-download-artifact@v8
|
||||
with:
|
||||
name: ${{ env.PR_EVENT }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
- name: Export PR Event Data
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
|
||||
core.exportVariable("PR_HEAD", prEvent.pull_request.head.ref);
|
||||
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
|
||||
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
|
||||
core.exportVariable("PR_NUMBER", prEvent.number);
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track Benchmarks with Bencher
|
||||
run: |
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch "$PR_HEAD" \
|
||||
--start-point "$PR_BASE" \
|
||||
--start-point-hash "$PR_BASE_SHA" \
|
||||
--start-point-clone-thresholds \
|
||||
--start-point-reset \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||
--ci-number "$PR_NUMBER" \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
# Exit on error inside any functions or subshells.
|
||||
set -o errtrace
|
||||
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||
set -o nounset
|
||||
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||
set -o pipefail
|
||||
# Turn on traces, useful while debugging but commented out by default
|
||||
# set -o xtrace
|
||||
|
||||
last_release="$(git tag --sort=committerdate | grep -P "v0+\.\d+\.\d+$" | tail -1)"
|
||||
echo "🐭 Last release: ${last_release}"
|
||||
|
||||
# detect breaking changes
|
||||
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; then
|
||||
echo "🐭 Breaking changes detected since ${last_release}"
|
||||
git log --oneline ${last_release}..HEAD | grep '!:'
|
||||
# increment the minor version
|
||||
minor="${last_release##v0.}"
|
||||
minor="${minor%.*}"
|
||||
next_minor="$((minor + 1))"
|
||||
next_release="v0.${next_minor}.0"
|
||||
else
|
||||
# increment the patch version
|
||||
patch="${last_release##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_release="${last_release/%${patch}/${next_patch}}"
|
||||
fi
|
||||
echo "🐭 Next release: ${next_release}"
|
||||
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = "${next_release}-${suffix}"* ]]; then
|
||||
echo "🐭 Last alpha release: ${last_tag}"
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
next_tag="${next_release}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "🐭 Next alpha release: ${next_tag}"
|
||||
65
.github/workflows/check-pr.yml
vendored
65
.github/workflows/check-pr.yml
vendored
@@ -1,13 +1,6 @@
|
||||
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
|
||||
@@ -15,21 +8,23 @@ on:
|
||||
- synchronize
|
||||
- labeled
|
||||
- unlabeled
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
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
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
id: check_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Add comment indicating we require pull request titles to follow conventional commits specification
|
||||
- uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: always() && (steps.check_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
@@ -44,42 +39,40 @@ jobs:
|
||||
|
||||
# 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
|
||||
uses: marocchino/sticky-pull-request-comment@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']
|
||||
})
|
||||
- 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@v7
|
||||
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') }}
|
||||
|
||||
10
.github/workflows/check-semver.yml
vendored
10
.github/workflows/check-semver.yml
vendored
@@ -1,9 +1,5 @@
|
||||
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:
|
||||
@@ -15,8 +11,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v4
|
||||
- name: Check semver
|
||||
uses: obi1kenobi/cargo-semver-checks-action@5b298c9520f7096a4683c0bd981a7ac5a7e249ae # v2
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
|
||||
184
.github/workflows/ci.yml
vendored
184
.github/workflows/ci.yml
vendored
@@ -1,9 +1,5 @@
|
||||
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: {}
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
@@ -29,15 +25,11 @@ jobs:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with: { components: rustfmt }
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: taplo-cli
|
||||
- run: cargo xtask format --check
|
||||
@@ -48,10 +40,8 @@ jobs:
|
||||
name: Check Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: crate-ci/typos@85f62a8a84f939ae994ab3763f01a0296d61a7ee # master
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
@@ -59,15 +49,9 @@ jobs:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-deny
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-deny
|
||||
- run: cargo deny --log-level info --all-features check
|
||||
|
||||
# Check for any unused dependencies in the codebase.
|
||||
@@ -76,33 +60,18 @@ jobs:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: bnjbvr/cargo-machete@7959c845782fed02ee69303126d4a12d64f1db18 # v0.9.1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: bnjbvr/cargo-machete@v0.7.0
|
||||
|
||||
# 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with: { components: clippy }
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask clippy
|
||||
|
||||
# Run markdownlint on all markdown files in the repository.
|
||||
@@ -110,10 +79,8 @@ jobs:
|
||||
name: Check Markdown
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v19
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
@@ -125,19 +92,14 @@ jobs:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -149,55 +111,24 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask check --all-features
|
||||
|
||||
build-no-std:
|
||||
name: Build No-Std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-none
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-rdme
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@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
|
||||
@@ -208,19 +139,10 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/install@74f735cdf643820234e37ae1c4089a08fd266d8a # master
|
||||
with:
|
||||
crate: cargo-docs-rs
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask docs
|
||||
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
@@ -229,16 +151,9 @@ jobs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
# Run cargo test on the libraries of the crate.
|
||||
@@ -248,18 +163,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@4575ae687efd0e2c78240087f26013fb2484987f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask test-libs
|
||||
|
||||
# Run cargo test on all the backends.
|
||||
@@ -276,11 +184,7 @@ jobs:
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
|
||||
48
.github/workflows/release-alpha.yml
vendored
Normal file
48
.github/workflows/release-alpha.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Release alpha version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 00:00 on Saturday
|
||||
# https://crontab.guru/#0_0_*_*_6
|
||||
- cron: "0 0 * * 6"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
publish-alpha:
|
||||
name: Create an alpha release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
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
|
||||
38
.github/workflows/release-plz.yml
vendored
38
.github/workflows/release-plz.yml
vendored
@@ -1,68 +1,50 @@
|
||||
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: {}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879 # v1
|
||||
id: auth
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@acb9246af4d59a270d1d4058a8b9af8c3f3a2559 # v0.5
|
||||
uses: release-plz/action@v0.5
|
||||
with:
|
||||
command: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_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
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@acb9246af4d59a270d1d4058a8b9af8c3f3a2559 # v0.5
|
||||
uses: release-plz/action@v0.5
|
||||
with:
|
||||
command: release-pr
|
||||
env:
|
||||
|
||||
45
.github/workflows/release-stable.yml
vendored
Normal file
45
.github/workflows/release-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Release stable version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish-stable:
|
||||
name: Create an stable release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --latest --strip header
|
||||
env:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
prerelease: false
|
||||
bodyFile: BODY.md
|
||||
|
||||
publish-crate:
|
||||
name: Publish crate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||||
35
.github/workflows/vhs-test.yml
vendored
35
.github/workflows/vhs-test.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: vhs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- vhs-test
|
||||
|
||||
jobs:
|
||||
vhs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: FedericoCarboni/setup-ffmpeg@v3
|
||||
id: setup-ffmpeg
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.13.1" # The Go version to download (if necessary) and use.
|
||||
|
||||
- run: |
|
||||
cargo build -p demo2
|
||||
sudo apt update
|
||||
sudo apt install -y ffmpeg ttyd
|
||||
go install github.com/charmbracelet/vhs@latest
|
||||
vhs ./examples/vhs/demo2.tape
|
||||
|
||||
- name: Upload GIF artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: demo2-gif
|
||||
path: ./target/demo2.gif
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,3 @@ target
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
.idea/
|
||||
.env
|
||||
|
||||
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.
|
||||
@@ -10,26 +10,11 @@ GitHub with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.30.0 Unreleased](#v0300-unreleased)
|
||||
- `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps
|
||||
are twice the size of first and last elements
|
||||
- `block::Title` no longer exists
|
||||
- [Unreleased](#unreleased)
|
||||
- The `From` impls for backend types are now replaced with more specific traits
|
||||
- `FrameExt` trait for `unstable-widget-ref` feature
|
||||
- `List::highlight_symbol` now accepts `Into<Line>` instead of `&str`
|
||||
- 'layout::Alignment' is renamed to 'layout::HorizontalAlignment'
|
||||
- The MSRV is now 1.85.0
|
||||
- `Backend` now requires an associated `Error` type and `clear_region` method
|
||||
- `TestBackend` now uses `core::convert::Infallible` for error handling instead of `std::io::Error`
|
||||
- Disabling `default-features` will now disable layout cache, which can have a negative impact on performance
|
||||
- `Layout::init_cache` and `Layout::DEFAULT_CACHE_SIZE` are now only available if `layout-cache`
|
||||
feature is enabled
|
||||
- Disabling `default-features` suppresses the error message if `show_cursor()` fails when dropping
|
||||
`Terminal`
|
||||
- Support a broader range for `unicode-width` version
|
||||
- [v0.29.0](#v0290)
|
||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer
|
||||
const
|
||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||
- Removed public fields from `Rect` iterators
|
||||
- `Line` now implements `From<Cow<str>`
|
||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||
@@ -90,183 +75,7 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.30.0 Unreleased
|
||||
|
||||
### `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps ([#1952])
|
||||
|
||||
[#1952]: https://github.com/ratatui/ratatui/pull/1952
|
||||
|
||||
The old `Flex::SpaceAround` behavior has been changed to distribute space evenly around each
|
||||
element, with the middle spacers being twice the size of the first and last one. The old
|
||||
behavior can be achieved by using `Flex::SpaceEvenly` instead.
|
||||
|
||||
```diff
|
||||
- let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::SpaceAround).split(area);
|
||||
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::SpaceEvenly).split(area);
|
||||
```
|
||||
|
||||
### `block::Title` no longer exists ([#1926])
|
||||
|
||||
[#1926]: https://github.com/ratatui/ratatui/pull/1926
|
||||
|
||||
The title alignment is better expressed in the `Line` as this fits more coherently with the rest of
|
||||
the library.
|
||||
|
||||
- `widgets::block` is no longer exported
|
||||
- `widgets::block::Title` no longer exists
|
||||
- `widgets::block::Position` is now `widgets::TitlePosition`
|
||||
- `Block::title()` now accepts `Into::<Line>` instead of `Into<Title>`
|
||||
- `BlockExt` is now exported at widgets::`BlockExt` instead of `widgets::block::BlockExt`
|
||||
|
||||
```diff
|
||||
- use ratatui::widgets::{Block, block::{Title, Position}};
|
||||
+ use ratatui::widgets::{Block, TitlePosition};
|
||||
|
||||
let block = Block::default()
|
||||
- .title(Title::from("Hello"))
|
||||
- .title(Title::from("Hello").position(Position::Bottom).alignment(Alignment::Center))
|
||||
- .title_position(Position::Bottom);
|
||||
+ .title(Line::from("Hello"))
|
||||
+ .title_bottom(Line::from("Hello").centered());
|
||||
+ .title_position(TitlePosition::Bottom);
|
||||
|
||||
- use ratatui::widgets::block::BlockExt;
|
||||
+ use ratatui::widgets::BlockExt;
|
||||
|
||||
struct MyWidget {
|
||||
block: Option<Block>,
|
||||
}
|
||||
|
||||
impl Widget for &MyWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let area = self.block.inner_if_some();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Style` no longer implements `Styled` ([#1572])
|
||||
|
||||
[#1572]: https://github.com/ratatui/ratatui/pull/1572
|
||||
|
||||
Any calls to methods implemented by the blanket implementation of `Stylize` are now defined directly
|
||||
on `Style`. Remove the `Stylize` import if it is no longer used by your code.
|
||||
|
||||
```diff
|
||||
- use ratatui::style::Stylize;
|
||||
|
||||
let style = Style::new().red();
|
||||
```
|
||||
|
||||
The `reset()` method does not have a direct replacement, as it clashes with the existing `reset()`
|
||||
method. Use the `Style::reset()` method instead.
|
||||
|
||||
```diff
|
||||
- some_style.reset();
|
||||
+ Style::reset();
|
||||
```
|
||||
|
||||
### Disabling `default-features` suppresses the error message if `show_cursor()` fails when dropping `Terminal` ([#1794])
|
||||
|
||||
[#1794]: https://github.com/ratatui/ratatui/pull/1794
|
||||
|
||||
Since disabling `default-features` disables `std`, printing to stderr is not possible. It is
|
||||
recommended to re-enable `std` when not using Ratatui in `no_std` environment.
|
||||
|
||||
### `Layout::init_cache` and `Layout::DEFAULT_CACHE_SIZE` are now only available if `layout-cache` feature is enabled ([#1795])
|
||||
|
||||
[#1795]: https://github.com/ratatui/ratatui/pull/1795
|
||||
|
||||
Previously, `Layout::init_cache` and `Layout::DEFAULT_CACHE_SIZE` were available independently of
|
||||
enabled feature flags.
|
||||
|
||||
### Disabling `default-features` will now disable layout cache, which can have a negative impact on performance ([#1795])
|
||||
|
||||
Layout cache is now opt-in in `ratatui-core` and enabled by default in `ratatui`. If app doesn't
|
||||
make use of `no_std`-compatibility, and disables `default-feature`, it is recommended to explicitly
|
||||
re-enable layout cache. Not doing so may impact performance.
|
||||
|
||||
```diff
|
||||
- ratatui = { version = "0.29.0", default-features = false }
|
||||
+ ratatui = { version = "0.30.0", default-features = false, features = ["layout-cache"] }
|
||||
```
|
||||
|
||||
### `TestBackend` now uses `core::convert::Infallible` for error handling instead of `std::io::Error` ([#1823])
|
||||
|
||||
[#1823]: https://github.com/ratatui/ratatui/pull/1823
|
||||
|
||||
Since `TestBackend` never fails, it now uses `Infallible` as associated `Error`. This may require
|
||||
changes in test cases that use `TestBackend`.
|
||||
|
||||
### `Backend` now requires an associated `Error` type and `clear_region` method ([#1778])
|
||||
|
||||
[#1778]: https://github.com/ratatui/ratatui/pull/1778
|
||||
|
||||
Custom `Backend` implementations must now define an associated `Error` type for method `Result`s
|
||||
and implement the `clear_region` method, which no longer has a default implementation.
|
||||
|
||||
This change was made to provide greater flexibility for custom backends, particularly to remove the
|
||||
explicit dependency on `std::io` for backends that want to support `no_std` targets.
|
||||
|
||||
If your app or library uses the `Backend` trait directly - for example, by providing a generic
|
||||
implementation for many backends - you may need to update the referenced error type.
|
||||
|
||||
```diff
|
||||
- fn run<B: Backend>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
+ fn run<B: Backend>(mut terminal: Terminal<B>) -> Result<(), B::Error> {
|
||||
```
|
||||
|
||||
Alternatively, you can explicitly require the associated error to be `std::io::Error`. This approach
|
||||
may require fewer changes in user code but is generally not recommended, as it limits compatibility
|
||||
with third-party backends. Additionally, the error type used by built-in backends may or may not
|
||||
change in the future, making this approach less future-proof compared to the previous one.
|
||||
|
||||
```diff
|
||||
- fn run<B: Backend>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
+ fn run<B: Backend<Error = io::Error>>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
```
|
||||
|
||||
If your application uses a concrete backend implementation, prefer specifying it explicitly
|
||||
instead.
|
||||
|
||||
```diff
|
||||
- fn run<B: Backend>(mut terminal: Terminal<B>) -> io::Result<()> {
|
||||
+ fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
|
||||
```
|
||||
|
||||
### The MSRV is now 1.85.0 ([#1860])
|
||||
|
||||
[#1860]: https://github.com/ratatui/ratatui/pull/1860
|
||||
|
||||
The minimum supported Rust version (MSRV) is now 1.85.0.
|
||||
|
||||
### `layout::Alignment` is renamed to `layout::HorizontalAlignment` ([#1735])
|
||||
|
||||
[#1735]: https://github.com/ratatui/ratatui/pull/1691
|
||||
|
||||
The `Alignment` enum has been renamed to `HorizontalAlignment` to better reflect its purpose. A type
|
||||
alias has been added to maintain backwards compatibility, however there are some cases where type
|
||||
aliases are not enough to maintain backwards compatibility. E.g. when using glob imports to import
|
||||
all the enum variants.
|
||||
|
||||
We don't expect to remove or deprecate the type alias in the near future, but it is recommended to
|
||||
update your imports to use the new name.
|
||||
|
||||
```diff
|
||||
- use ratatui::layout::Alignment;
|
||||
+ use ratatui::layout::HorizontalAlignment;
|
||||
|
||||
- use Alignment::*;
|
||||
+ use HorizontalAlignment::*;
|
||||
```
|
||||
|
||||
### `List::highlight_symbol` accepts `Into<Line>` ([#1595])
|
||||
|
||||
[#1595]: https://github.com/ratatui/ratatui/pull/1595
|
||||
|
||||
Previously `List::highlight_symbol` accepted `&str`. Any code that uses conversion methods will need
|
||||
to be rewritten. Since `Into::into` is not const, this function cannot be called in const context.
|
||||
## Unreleased (0.30.0)
|
||||
|
||||
### `FrameExt` trait for `unstable-widget-ref` feature ([#1530])
|
||||
|
||||
@@ -369,26 +178,6 @@ for `Bar::text_value()`:
|
||||
+ Bar::default().text_value("foobar");
|
||||
```
|
||||
|
||||
### `termwiz` is upgraded to 0.23.0 ([#1682])
|
||||
|
||||
[#1682]: https://github.com/ratatui/ratatui/pull/1682
|
||||
|
||||
The `termwiz` backend is upgraded from 0.22.0 to 0.23.0.
|
||||
|
||||
This release has a few fixes for hyperlinks and input handling, plus some dependency updates.
|
||||
See the [commits](https://github.com/wezterm/wezterm/commits/main/termwiz) for more details.
|
||||
|
||||
### Support a broader range for `unicode-width` version ([#1999])
|
||||
|
||||
[#1999]: https://github.com/ratatui/ratatui/pull/1999
|
||||
|
||||
Ratatui's dependency on `unicode-width`, previously pinned to 0.2.0, has
|
||||
expanded to allow version 0.2.1. This comes with 2 behavior changes described in
|
||||
[unicode-width#61] and [unicode-width#74].
|
||||
|
||||
[unicode-width#61]: https://github.com/unicode-rs/unicode-width/pull/61
|
||||
[unicode-width#74]: https://github.com/unicode-rs/unicode-width/pull/74
|
||||
|
||||
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
|
||||
|
||||
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
|
||||
|
||||
1922
CHANGELOG.md
1922
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -6,23 +6,6 @@ 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),
|
||||
@@ -39,48 +22,17 @@ on the crate or its users should ideally be discussed in a "Feature Request" iss
|
||||
|
||||
### 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.
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
@@ -108,15 +60,6 @@ Running `cargo xtask ci` before pushing will perform the same checks that we do
|
||||
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.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
We use commit signature verification, which will block commits from being merged via the UI unless
|
||||
@@ -124,25 +67,6 @@ 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
|
||||
@@ -160,20 +84,6 @@ cd ratatui
|
||||
cargo xtask 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
|
||||
@@ -181,7 +91,7 @@ good, but this can always be improved. Focus on keeping the tests simple and obv
|
||||
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 +100,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
|
||||
@@ -261,14 +167,6 @@ We generally want to wait at least two versions before removing deprecated items
|
||||
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
|
||||
|
||||
1867
Cargo.lock
generated
1867
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
76
Cargo.toml
76
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*", "examples/concepts/*"]
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
@@ -11,7 +11,6 @@ default-members = [
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"examples/apps/*",
|
||||
"examples/concepts/*",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -24,57 +23,34 @@ categories = ["command-line-interface"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
edition = "2024"
|
||||
rust-version = "1.85.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.74.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anstyle = "1"
|
||||
bitflags = "2.9"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
color-eyre = "0.6"
|
||||
compact_str = { version = "0.9", default-features = false }
|
||||
criterion = { version = "0.7", 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.44"
|
||||
palette = "0.7"
|
||||
pretty_assertions = "1"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-beta.0" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-beta.0" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-beta.0" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-beta.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-beta.0" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-beta.0" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-beta.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", 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 }
|
||||
bitflags = "2.7.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
document-features = "0.2.7"
|
||||
indoc = "2.0.5"
|
||||
instability = "0.3.7"
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.1" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.2" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.1" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.1" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.1" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.1" }
|
||||
rstest = "0.24.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.138"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
termion = "4.0.0"
|
||||
termwiz = { version = "0.22.0" }
|
||||
unicode-segmentation = "1.12.0"
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = ">=0.2.0, <=0.2.1"
|
||||
unicode-width = "=0.2.0"
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
|
||||
16
README.md
16
README.md
@@ -12,7 +12,7 @@
|
||||
|
||||
</details>
|
||||
|
||||

|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -73,7 +73,6 @@ fn render(frame: &mut Frame) {
|
||||
- [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.
|
||||
|
||||
@@ -113,16 +112,7 @@ There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
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/)
|
||||
request][Create a Pull Request].
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -143,7 +133,6 @@ This project is licensed under the [MIT License][License].
|
||||
[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
|
||||
@@ -153,7 +142,6 @@ This project is licensed under the [MIT License][License].
|
||||
[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
|
||||
|
||||
@@ -36,7 +36,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
## 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
|
||||
and can be manually be 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
|
||||
|
||||
@@ -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 |
@@ -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 |
18
bacon.toml
18
bacon.toml
@@ -60,22 +60,6 @@ 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/"]
|
||||
|
||||
# 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.)
|
||||
@@ -90,5 +74,3 @@ 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"
|
||||
|
||||
46
cliff.toml
46
cliff.toml
@@ -24,11 +24,7 @@ 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) -%}
|
||||
@@ -37,16 +33,6 @@ body = """
|
||||
{% 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 -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
@@ -63,27 +49,24 @@ body = """
|
||||
|
||||
{% 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 %}
|
||||
**Full Changelog**: {{ release_link }}
|
||||
{% 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 %}
|
||||
"""
|
||||
|
||||
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = false
|
||||
# postprocessors for the changelog body
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
postprocessors = [
|
||||
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
|
||||
{ pattern = '>---+\n', replace = '' },
|
||||
@@ -104,15 +87,9 @@ commit_preprocessors = [
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
# a small typo 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" },
|
||||
]
|
||||
# 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" },
|
||||
@@ -126,9 +103,8 @@ commit_parsers = [
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^build\\(deps\\)", skip = true },
|
||||
{ message = "^build", group = "<!-- 08 -->Build" },
|
||||
{ body = ".*security", group = "<!-- 09 -->Security" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
# handle some old commits styles from pre 0.4
|
||||
@@ -143,9 +119,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 = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
|
||||
@@ -9,6 +9,7 @@ allow = [
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
|
||||
@@ -12,16 +12,21 @@
|
||||
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};
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Position, Rect, Size},
|
||||
style::{Color, Style},
|
||||
widgets::{Widget, WidgetRef},
|
||||
DefaultTerminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -33,15 +38,15 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while !self.should_quit {
|
||||
self.render(terminal)?;
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
|
||||
fn draw(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
|
||||
tui.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -52,8 +57,11 @@ impl App {
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
if event::read()?.is_key_press() {
|
||||
self.should_quit = true;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -68,7 +76,7 @@ impl App {
|
||||
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));
|
||||
let [greeting, timer, squares, position] = Layout::vertical(constraints).areas(area);
|
||||
|
||||
// render an ephemeral greeting widget
|
||||
Greeting::new("Ratatui!").render(greeting, buf);
|
||||
@@ -174,9 +182,9 @@ struct BlueSquare;
|
||||
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);
|
||||
let areas = Layout::horizontal(constraints).split(area);
|
||||
for (widget, area) in self.squares.iter().zip(areas.iter()) {
|
||||
widget.render_ref(*area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "async-github"
|
||||
publish = false
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -14,9 +14,9 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
octocrab.workspace = true
|
||||
octocrab = "0.43.0"
|
||||
ratatui.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream.workspace = true
|
||||
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
@@ -27,20 +27,25 @@
|
||||
//! [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 std::{
|
||||
sync::{Arc, RwLock},
|
||||
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 octocrab::{
|
||||
params::{pulls::Sort, Direction},
|
||||
Page,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{Event, EventStream, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -70,28 +75,30 @@ impl App {
|
||||
|
||||
while !self.should_quit {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => { terminal.draw(|frame| self.render(frame))?; },
|
||||
_ = interval.tick() => { terminal.draw(|frame| self.draw(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);
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
let [title_area, body_area] = vertical.areas(frame.area());
|
||||
let title = Line::from("Ratatui async example").centered().bold();
|
||||
frame.render_widget(title, title_area);
|
||||
frame.render_widget(&self.pull_requests, body_area);
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &Event) {
|
||||
if let 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(),
|
||||
_ => {}
|
||||
if let Event::Key(key) = event {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||
KeyCode::Char('j') | KeyCode::Down => self.pull_requests.scroll_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.pull_requests.scroll_up(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
time = { workspace = true, features = ["formatting", "parsing"] }
|
||||
time = { version = "0.3.37", features = ["formatting", "parsing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -11,37 +11,45 @@
|
||||
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};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Text},
|
||||
widgets::calendar::{CalendarEventStore, Monthly},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use time::{ext::NumericalDuration, Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(run)
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut terminal: 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(),
|
||||
_ => {}
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
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 = previous_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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +66,7 @@ fn next_month(date: Date) -> Date {
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_month(date: Date) -> Date {
|
||||
fn previous_month(date: Date) -> Date {
|
||||
if date.month() == Month::January {
|
||||
date.replace_month(Month::December)
|
||||
.unwrap()
|
||||
@@ -69,7 +77,7 @@ fn prev_month(date: Date) -> Date {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the UI with a calendar.
|
||||
/// Draw 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()),
|
||||
@@ -81,10 +89,11 @@ fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date
|
||||
)),
|
||||
]);
|
||||
|
||||
let [text_area, area] = frame.area().layout(&Layout::vertical([
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Fill(1),
|
||||
]));
|
||||
]);
|
||||
let [text_area, area] = vertical.areas(frame.area());
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
calendar_style
|
||||
.render_year(frame, area, selected_date)
|
||||
@@ -132,13 +141,16 @@ 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));
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
});
|
||||
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
|
||||
let areas = rows.iter().flat_map(|row| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||
.split(*row)
|
||||
.to_vec()
|
||||
});
|
||||
for (i, area) in areas.enumerate() {
|
||||
let month = date
|
||||
.replace_day(1)
|
||||
|
||||
@@ -14,18 +14,23 @@ use std::{
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::ExecutableCommand;
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEventKind,
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, EnableMouseCapture, KeyEventKind},
|
||||
ExecutableCommand,
|
||||
};
|
||||
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};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, MouseEventKind},
|
||||
layout::{Constraint, Layout, Position, Rect},
|
||||
style::{Color, Stylize},
|
||||
symbols::Marker,
|
||||
text::Text,
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle},
|
||||
Block, Widget,
|
||||
},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
@@ -75,33 +80,43 @@ impl App {
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
let mut last_tick = Instant::now();
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
if event::poll(timeout)? {
|
||||
match event::read()? {
|
||||
Event::Key(key) => self.handle_key_press(key),
|
||||
Event::Mouse(event) => self.handle_mouse_event(event),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
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() {
|
||||
fn handle_key_press(&mut self, key: event::KeyEvent) {
|
||||
if key.kind != KeyEventKind::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(),
|
||||
KeyCode::Char('q') => self.exit = true,
|
||||
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
|
||||
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
|
||||
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
|
||||
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
|
||||
KeyCode::Enter => {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -117,16 +132,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
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::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
};
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
@@ -145,7 +150,7 @@ impl App {
|
||||
self.ball.y += self.vy;
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let header = Text::from_iter([
|
||||
"Canvas Example".bold(),
|
||||
"<q> Quit | <enter> Change Marker | <hjkl> Move".into(),
|
||||
@@ -153,15 +158,16 @@ impl App {
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]);
|
||||
let [text_area, up, down] = frame.area().layout(&vertical);
|
||||
let [text_area, up, down] = vertical.areas(frame.area());
|
||||
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);
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [draw, pong] = horizontal.areas(up);
|
||||
let [map, boxes] = horizontal.areas(down);
|
||||
|
||||
frame.render_widget(self.map_canvas(), map);
|
||||
frame.render_widget(self.draw_canvas(draw), draw);
|
||||
|
||||
@@ -11,17 +11,22 @@
|
||||
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};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols::{self, Marker},
|
||||
text::{Line, Span},
|
||||
widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
struct App {
|
||||
@@ -75,23 +80,23 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,12 +112,11 @@ impl App {
|
||||
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]));
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
|
||||
let [animated_chart, bar_chart] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
|
||||
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
|
||||
|
||||
self.render_animated_chart(frame, animated_chart);
|
||||
render_barchart(frame, bar_chart);
|
||||
@@ -126,7 +130,7 @@ impl App {
|
||||
format!("{}", self.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", f64::midpoint(self.window[0], self.window[1]))),
|
||||
Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
|
||||
Span::styled(
|
||||
format!("{}", self.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
@@ -206,14 +210,12 @@ fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||
}
|
||||
|
||||
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 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()))
|
||||
|
||||
@@ -9,37 +9,46 @@
|
||||
//! [`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};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
let terminal = ratatui::init();
|
||||
let app_result = run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let [named, indexed_colors, indexed_greys] = Layout::vertical([
|
||||
fn draw(frame: &mut Frame) {
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.areas(frame.area());
|
||||
.split(frame.area());
|
||||
|
||||
render_named_colors(frame, named);
|
||||
render_indexed_colors(frame, indexed_colors);
|
||||
render_indexed_grayscale(frame, indexed_greys);
|
||||
render_named_colors(frame, layout[0]);
|
||||
render_indexed_colors(frame, layout[1]);
|
||||
render_indexed_grayscale(frame, layout[2]);
|
||||
}
|
||||
|
||||
const NAMED_COLORS: [Color; 16] = [
|
||||
@@ -82,12 +91,12 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
|
||||
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));
|
||||
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
|
||||
let areas = vertical.iter().flat_map(|area| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
});
|
||||
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);
|
||||
@@ -100,12 +109,12 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
|
||||
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));
|
||||
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
|
||||
let areas = vertical.iter().flat_map(|area| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
});
|
||||
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);
|
||||
@@ -197,7 +206,7 @@ fn title_block(title: String) -> Block<'static> {
|
||||
.borders(Borders::TOP)
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title_style(Style::reset())
|
||||
.title_style(Style::new().reset())
|
||||
.title(title)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "colors-rgb"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -7,8 +8,7 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
palette.workspace = true
|
||||
palette = "0.7.6"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -19,19 +19,23 @@
|
||||
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;
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Position, Rect},
|
||||
style::Color,
|
||||
text::Text,
|
||||
widgets::Widget,
|
||||
DefaultTerminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -88,7 +92,7 @@ impl App {
|
||||
/// Run the app
|
||||
///
|
||||
/// This is the main event loop for the app.
|
||||
pub fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
@@ -101,16 +105,19 @@ impl App {
|
||||
}
|
||||
|
||||
/// Handle any events that have occurred since the last time the app was rendered.
|
||||
///
|
||||
/// Currently, this only handles the q key to quit the app.
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
// Ensure that the app only blocks for a period that allows the app to render at
|
||||
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
|
||||
// also update the app immediately any time an event occurs)
|
||||
let timeout = Duration::from_secs_f32(1.0 / 60.0);
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
if event::read()?.is_key_press() {
|
||||
self.state = AppState::Quit;
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.state = AppState::Quit;
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -124,8 +131,8 @@ impl App {
|
||||
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)]));
|
||||
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||
Text::from("colors_rgb example. Press q to quit")
|
||||
.centered()
|
||||
.render(title, buf);
|
||||
@@ -168,7 +175,7 @@ impl FpsWidget {
|
||||
/// 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)]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn calculate_fps(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
@@ -210,7 +217,7 @@ impl ColorsWidget {
|
||||
///
|
||||
/// 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)]
|
||||
#[allow(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
|
||||
|
||||
@@ -9,22 +9,31 @@
|
||||
///
|
||||
/// [`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 ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Flex, Layout, Rect,
|
||||
},
|
||||
style::{
|
||||
palette::tailwind::{BLUE, SKY, SLATE, STONE},
|
||||
Color, Style, Stylize,
|
||||
},
|
||||
symbols::{self, line},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Paragraph, Widget, Wrap},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -79,7 +88,7 @@ struct SpacerBlock;
|
||||
|
||||
// App behaviour
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
self.insert_test_defaults();
|
||||
|
||||
while self.is_running() {
|
||||
@@ -104,8 +113,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
|
||||
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
@@ -122,7 +131,8 @@ impl App {
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -138,7 +148,7 @@ impl App {
|
||||
| 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) {
|
||||
@@ -152,7 +162,7 @@ impl App {
|
||||
| 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
|
||||
@@ -193,15 +203,15 @@ impl App {
|
||||
self.selected_index = index;
|
||||
}
|
||||
|
||||
const fn increment_spacing(&mut self) {
|
||||
fn increment_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_add(1);
|
||||
}
|
||||
|
||||
const fn decrement_spacing(&mut self) {
|
||||
fn decrement_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_sub(1);
|
||||
}
|
||||
|
||||
const fn exit(&mut self) {
|
||||
fn exit(&mut self) {
|
||||
self.mode = AppMode::Quit;
|
||||
}
|
||||
|
||||
@@ -236,19 +246,15 @@ impl From<Constraint> for ConstraintName {
|
||||
|
||||
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
|
||||
]));
|
||||
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
|
||||
Layout::vertical([
|
||||
Length(2), // header
|
||||
Length(2), // instructions
|
||||
Length(1), // swap key legend
|
||||
Length(1), // gap
|
||||
Fill(1), // blocks
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
App::header().render(header_area, buf);
|
||||
App::instructions().render(instructions_area, buf);
|
||||
@@ -277,7 +283,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn swap_legend() -> impl Widget {
|
||||
#[expect(unstable_name_collisions)]
|
||||
#[allow(unstable_name_collisions)]
|
||||
Paragraph::new(
|
||||
Line::from(
|
||||
[
|
||||
@@ -318,26 +324,20 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
|
||||
.spacing(1)
|
||||
.areas(area);
|
||||
|
||||
self.render_user_constraints_legend(user_constraints, buf);
|
||||
|
||||
let [
|
||||
start,
|
||||
center,
|
||||
end,
|
||||
space_between,
|
||||
space_around,
|
||||
space_evenly,
|
||||
] = area.layout(&Layout::vertical([Length(7); 6]));
|
||||
let [start, center, end, space_around, space_between] =
|
||||
Layout::vertical([Length(7); 5]).areas(area);
|
||||
|
||||
self.render_layout_block(Flex::Start, start, buf);
|
||||
self.render_layout_block(Flex::Center, center, buf);
|
||||
self.render_layout_block(Flex::End, end, buf);
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||
self.render_layout_block(Flex::SpaceEvenly, space_evenly, buf);
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||
}
|
||||
|
||||
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -351,8 +351,8 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
let [label_area, axis_area, blocks_area] =
|
||||
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
|
||||
|
||||
if label_area.height > 0 {
|
||||
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
||||
@@ -472,7 +472,7 @@ impl ConstraintBlock {
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
if let Some(last_row) = area.rows().next_back() {
|
||||
if let Some(last_row) = area.rows().last() {
|
||||
buf.set_style(last_row, border_color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,22 @@
|
||||
///
|
||||
/// [`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::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Layout, Rect,
|
||||
},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||
Tabs, Widget,
|
||||
},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use ratatui::{DefaultTerminal, symbols};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const SPACER_HEIGHT: u16 = 0;
|
||||
@@ -36,7 +40,10 @@ const FILL_COLOR: Color = tailwind::SLATE.c950;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
@@ -69,7 +76,7 @@ enum AppState {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
self.update_max_scroll_offset();
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
@@ -78,7 +85,7 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn update_max_scroll_offset(&mut self) {
|
||||
fn update_max_scroll_offset(&mut self) {
|
||||
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
|
||||
}
|
||||
|
||||
@@ -87,7 +94,10 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
@@ -102,7 +112,7 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
|
||||
@@ -118,7 +128,7 @@ impl App {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
const fn up(&mut self) {
|
||||
fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
@@ -129,18 +139,18 @@ impl App {
|
||||
.min(self.max_scroll_offset);
|
||||
}
|
||||
|
||||
const fn top(&mut self) {
|
||||
fn top(&mut self) {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
const fn bottom(&mut self) {
|
||||
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)]));
|
||||
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
|
||||
|
||||
self.render_tabs(tabs, buf);
|
||||
Self::render_axis(axis, buf);
|
||||
@@ -186,7 +196,7 @@ impl App {
|
||||
///
|
||||
/// 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)]
|
||||
#[allow(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
|
||||
@@ -241,7 +251,7 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
const fn get_example_count(self) -> u16 {
|
||||
#[expect(clippy::match_same_arms)]
|
||||
#[allow(clippy::match_same_arms)]
|
||||
match self {
|
||||
Self::Length => 4,
|
||||
Self::Percentage => 5,
|
||||
@@ -281,8 +291,8 @@ impl Widget for SelectedTab {
|
||||
|
||||
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);
|
||||
let [example1, example2, example3, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
|
||||
|
||||
Example::new(&[Length(20), Length(20)]).render(example1, buf);
|
||||
Example::new(&[Length(20), Min(20)]).render(example2, buf);
|
||||
@@ -290,8 +300,8 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
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);
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
|
||||
@@ -301,8 +311,8 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
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);
|
||||
let [example1, example2, example3, example4, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
|
||||
|
||||
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
|
||||
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
|
||||
@@ -311,15 +321,15 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
fn render_fill_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, _] = area.layout(&Layout::vertical([Length(EXAMPLE_HEIGHT); 3]));
|
||||
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
|
||||
|
||||
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);
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
|
||||
@@ -329,8 +339,8 @@ impl SelectedTab {
|
||||
}
|
||||
|
||||
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);
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
|
||||
@@ -354,10 +364,9 @@ impl Example {
|
||||
|
||||
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);
|
||||
let [area, _] =
|
||||
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
|
||||
let blocks = Layout::horizontal(&self.constraints).split(area);
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
|
||||
@@ -9,17 +9,21 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Line,
|
||||
widgets::{Paragraph, Widget},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
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()?;
|
||||
@@ -99,7 +103,7 @@ impl<'a> Button<'a> {
|
||||
}
|
||||
|
||||
impl Widget for Button<'_> {
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[allow(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));
|
||||
@@ -147,12 +151,15 @@ 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))?;
|
||||
terminal.draw(|frame| draw(frame, button_states))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
continue;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
if key.kind != event::KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
|
||||
break;
|
||||
}
|
||||
@@ -166,14 +173,14 @@ fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, states: [State; 3]) {
|
||||
let layout = Layout::vertical([
|
||||
fn draw(frame: &mut Frame, states: [State; 3]) {
|
||||
let vertical = 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);
|
||||
let [title, buttons, help, _] = vertical.areas(frame.area());
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||
@@ -184,8 +191,13 @@ fn render(frame: &mut Frame, states: [State; 3]) {
|
||||
}
|
||||
|
||||
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);
|
||||
let horizontal = Layout::horizontal([
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [red, green, blue, _] = horizontal.areas(area);
|
||||
|
||||
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
|
||||
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
|
||||
@@ -193,13 +205,10 @@ fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
|
||||
}
|
||||
|
||||
fn handle_key_event(
|
||||
key: KeyEvent,
|
||||
key: event::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') => {
|
||||
|
||||
@@ -7,16 +7,11 @@ rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
crossterm = ["ratatui/crossterm", "dep:crossterm"]
|
||||
termion = ["ratatui/termion", "dep:termion"]
|
||||
termwiz = ["ratatui/termwiz", "dep:termwiz"]
|
||||
crossterm = ["ratatui/crossterm"]
|
||||
termion = ["ratatui/termion"]
|
||||
termwiz = ["ratatui/termwiz"]
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
crossterm = { workspace = true, optional = true }
|
||||
rand.workspace = true
|
||||
clap = { version = "4.5.27", features = ["derive"] }
|
||||
rand = "0.9.0"
|
||||
ratatui.workspace = true
|
||||
termwiz = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
termion = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use rand::distr::{Distribution, Uniform};
|
||||
use rand::rngs::ThreadRng;
|
||||
use rand::{
|
||||
distr::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
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 std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::{Backend, CrosstermBackend};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::ui;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -45,29 +48,29 @@ fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
B::Error: 'static,
|
||||
{
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
|
||||
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
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(());
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
//! [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 std::{error::Error, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#![allow(dead_code)]
|
||||
use std::error::Error;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use std::{io, thread};
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::{Backend, TermionBackend};
|
||||
use termion::event::Key;
|
||||
use termion::input::{MouseTerminal, TermRead};
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::IntoAlternateScreen;
|
||||
use ratatui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::ui;
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -36,13 +36,10 @@ fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
B::Error: 'static,
|
||||
{
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let events = events(tick_rate);
|
||||
loop {
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
@@ -78,14 +75,12 @@ fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{err}");
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{err}");
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
#![allow(dead_code)]
|
||||
use std::error::Error;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
error::Error,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TermwizBackend;
|
||||
use termwiz::input::{InputEvent, KeyCode};
|
||||
use termwiz::terminal::Terminal as TermwizTerminal;
|
||||
use ratatui::{
|
||||
backend::TermwizBackend,
|
||||
termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::ui;
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
let backend = TermwizBackend::new()?;
|
||||
@@ -36,7 +41,7 @@ fn run_app(
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if let Some(input) = terminal
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{self, Span};
|
||||
use ratatui::widgets::canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle};
|
||||
use ratatui::widgets::{
|
||||
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph, Row,
|
||||
Sparkline, Table, Tabs, Wrap,
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{self, Span},
|
||||
widgets::{
|
||||
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
|
||||
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
|
||||
Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
use ratatui::{Frame, symbols};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &mut App) {
|
||||
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
|
||||
let tabs = app
|
||||
.tabs
|
||||
@@ -95,7 +98,7 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||
frame.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
@@ -235,9 +238,7 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
fn draw_text(frame: &mut Frame, area: Rect) {
|
||||
let text = vec![
|
||||
text::Line::from(
|
||||
"This is a paragraph with several lines. You can change style your text the way you want",
|
||||
),
|
||||
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
text::Line::from(""),
|
||||
text::Line::from(vec![
|
||||
Span::from("For example: "),
|
||||
@@ -252,17 +253,16 @@ fn draw_text(frame: &mut Frame, area: Rect) {
|
||||
Span::raw("Oh and if you didn't "),
|
||||
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
|
||||
Span::raw(" you can "),
|
||||
Span::styled(
|
||||
"automatically",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" "),
|
||||
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
|
||||
Span::raw(" your "),
|
||||
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw("."),
|
||||
Span::raw(".")
|
||||
]),
|
||||
text::Line::from("One more thing is that it should display unicode characters: 10€"),
|
||||
text::Line::from(
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
let block = Block::bordered().title(Span::styled(
|
||||
"Footer",
|
||||
|
||||
@@ -6,14 +6,14 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
color-eyre = "0.6.3"
|
||||
crossterm.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
palette.workspace = true
|
||||
rand.workspace = true
|
||||
rand_chacha.workspace = true
|
||||
palette = "0.7.6"
|
||||
rand = "0.9.0"
|
||||
rand_chacha = "0.9.0"
|
||||
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||
strum.workspace = true
|
||||
time.workspace = true
|
||||
unicode-width.workspace = true
|
||||
time = "0.3.37"
|
||||
unicode-width = "0.2.0"
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::Context;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use crossterm::event;
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Color,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Tabs, Widget},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
use crate::tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab};
|
||||
use crate::{THEME, destroy};
|
||||
use crate::{
|
||||
destroy,
|
||||
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
|
||||
THEME,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct App {
|
||||
@@ -49,7 +54,7 @@ impl App {
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal
|
||||
.draw(|frame| self.render(frame))
|
||||
.draw(|frame| self.draw(frame))
|
||||
.wrap_err("terminal.draw")?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
@@ -60,8 +65,8 @@ impl App {
|
||||
self.mode != Mode::Quit
|
||||
}
|
||||
|
||||
/// Render a single frame of the app.
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
/// Draw a single frame of the app.
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
frame.render_widget(self, frame.area());
|
||||
if self.mode == Mode::Destroy {
|
||||
destroy::destroy(frame);
|
||||
@@ -77,20 +82,25 @@ impl App {
|
||||
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(),
|
||||
_ => {}
|
||||
};
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_press(&mut self, key: KeyEvent) {
|
||||
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 => self.next_tab(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.prev(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn prev(&mut self) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.prev_row(),
|
||||
@@ -129,12 +139,12 @@ impl App {
|
||||
/// 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([
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [title_bar, tab, bottom_bar] = area.layout(&layout);
|
||||
let [title_bar, tab, bottom_bar] = vertical.areas(area);
|
||||
|
||||
Block::new().style(THEME.root).render(area, buf);
|
||||
self.render_title_bar(title_bar, buf);
|
||||
@@ -146,7 +156,7 @@ impl Widget for &App {
|
||||
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);
|
||||
let [title, tabs] = layout.areas(area);
|
||||
|
||||
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
||||
let titles = Tab::iter().map(Tab::title);
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use palette::{IntoColor, Okhsv, Srgb};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
|
||||
|
||||
/// A widget that renders a color swatch of RGB colors.
|
||||
///
|
||||
@@ -12,7 +9,7 @@ use ratatui::widgets::Widget;
|
||||
pub struct RgbSwatch;
|
||||
|
||||
impl Widget for RgbSwatch {
|
||||
#[expect(clippy::cast_precision_loss, clippy::similar_names)]
|
||||
#[allow(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;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Flex, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Text,
|
||||
widgets::Widget,
|
||||
Frame,
|
||||
};
|
||||
|
||||
/// delay the start of the animation so it doesn't start immediately
|
||||
const DELAY: usize = 120;
|
||||
@@ -32,7 +34,7 @@ pub fn destroy(frame: &mut Frame<'_>) {
|
||||
///
|
||||
/// 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(
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
@@ -74,7 +76,7 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
}
|
||||
|
||||
/// draw some text fading in and out from black to red and back
|
||||
#[expect(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
|
||||
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
|
||||
if sub_frame == 0 {
|
||||
@@ -126,7 +128,7 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||
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)]
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
Color::Rgb(red as u8, green as u8, blue as u8)
|
||||
}
|
||||
|
||||
@@ -134,7 +136,7 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||
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);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
area
|
||||
}
|
||||
|
||||
@@ -29,13 +29,16 @@ 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};
|
||||
use crossterm::{
|
||||
execute,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{layout::Rect, TerminalOptions, Viewport};
|
||||
|
||||
pub use self::colors::{RgbSwatch, color_from_oklab};
|
||||
pub use self::theme::THEME;
|
||||
pub use self::{
|
||||
colors::{color_from_oklab, RgbSwatch},
|
||||
theme::THEME,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
widgets::{
|
||||
Block, Borders, Clear, MascotEyeColor, Padding, Paragraph, RatatuiMascot, Widget, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -24,8 +26,8 @@ impl AboutTab {
|
||||
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);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [logo_area, description] = horizontal.areas(area);
|
||||
render_crate_description(description, buf);
|
||||
let eye_state = if self.row_index % 2 == 0 {
|
||||
MascotEyeColor::Default
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{Styled, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
|
||||
Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
|
||||
},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -71,15 +73,15 @@ impl Widget for EmailTab {
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
let [inbox, email] = area.layout(&layout);
|
||||
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
let [inbox, email] = vertical.areas(area);
|
||||
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 vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [tabs, inbox] = vertical.areas(area);
|
||||
let theme = THEME.email;
|
||||
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
|
||||
.style(theme.tabs)
|
||||
@@ -130,8 +132,8 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
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 vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||
let [headers_area, body_area] = vertical.areas(inner);
|
||||
let headers = vec![
|
||||
Line::from(vec![
|
||||
"From: ".set_style(theme.header),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
StatefulWidget, Table, TableState, Widget, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -17,7 +19,7 @@ struct Ingredient {
|
||||
}
|
||||
|
||||
impl Ingredient {
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn height(&self) -> u16 {
|
||||
self.name.lines().count() as u16
|
||||
}
|
||||
@@ -135,8 +137,8 @@ impl Widget for RecipeTab {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let layout = Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]);
|
||||
let [recipe, ingredients] = area.layout(&layout);
|
||||
let [recipe, ingredients] =
|
||||
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
|
||||
|
||||
render_recipe(recipe, buf);
|
||||
render_ingredients(self.row_index, ingredients, buf);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Styled, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{self, Canvas, Map, MapResolution, Points},
|
||||
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
Sparkline, StatefulWidget, Table, TableState, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -39,8 +41,8 @@ impl Widget for TracerouteTab {
|
||||
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);
|
||||
let [left, map] = horizontal.areas(area);
|
||||
let [hops, pings] = vertical.areas(left);
|
||||
|
||||
render_hops(self.row_index, hops, buf);
|
||||
render_ping(self.row_index, pings, buf);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols,
|
||||
widgets::{
|
||||
calendar::{CalendarEventStore, Monthly},
|
||||
Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget,
|
||||
},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{RgbSwatch, THEME, color_from_oklab};
|
||||
use crate::{color_from_oklab, RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct WeatherTab {
|
||||
@@ -41,16 +45,16 @@ impl Widget for WeatherTab {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let tab_layout = Layout::vertical([
|
||||
let [main, _, gauges] = 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);
|
||||
])
|
||||
.areas(area);
|
||||
let [calendar, charts] =
|
||||
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
|
||||
let [simple, horizontal] =
|
||||
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
|
||||
|
||||
render_calendar(calendar, buf);
|
||||
render_simple_barchart(simple, buf);
|
||||
@@ -130,14 +134,14 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
#[allow(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)]
|
||||
#[allow(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);
|
||||
|
||||
@@ -11,29 +11,37 @@
|
||||
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 ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{
|
||||
Alignment,
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Flex, Layout, Rect,
|
||||
},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
symbols::{self, line},
|
||||
text::{Line, Text},
|
||||
widgets::{
|
||||
Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs,
|
||||
Widget,
|
||||
},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
(
|
||||
"Min(u16) takes any excess space always",
|
||||
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1, 10)],
|
||||
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10)],
|
||||
),
|
||||
(
|
||||
"Fill(u16) takes any excess space always",
|
||||
@@ -41,18 +49,20 @@ const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
),
|
||||
(
|
||||
"Here's all constraints in one line",
|
||||
&[
|
||||
Length(10),
|
||||
Min(10),
|
||||
Max(10),
|
||||
Percentage(10),
|
||||
Ratio(1, 10),
|
||||
Fill(1),
|
||||
],
|
||||
&[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)],
|
||||
),
|
||||
("", &[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)],
|
||||
@@ -62,58 +72,44 @@ const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
&[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)],
|
||||
),
|
||||
("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)],
|
||||
),
|
||||
("`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)],
|
||||
),
|
||||
("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)],
|
||||
),
|
||||
("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)],
|
||||
),
|
||||
("`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)]),
|
||||
(
|
||||
"",
|
||||
&[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(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)],
|
||||
&[
|
||||
Fill(0),
|
||||
Fill(0),
|
||||
Fill(1),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
@@ -153,12 +149,11 @@ enum SelectedTab {
|
||||
Center,
|
||||
End,
|
||||
SpaceAround,
|
||||
SpaceEvenly,
|
||||
SpaceBetween,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: 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
|
||||
@@ -178,8 +173,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
@@ -190,7 +185,8 @@ impl App {
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -203,7 +199,7 @@ impl App {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
const fn up(&mut self) {
|
||||
fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
@@ -214,7 +210,7 @@ impl App {
|
||||
.min(max_scroll_offset());
|
||||
}
|
||||
|
||||
const fn top(&mut self) {
|
||||
fn top(&mut self) {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
@@ -222,15 +218,15 @@ impl App {
|
||||
self.scroll_offset = max_scroll_offset();
|
||||
}
|
||||
|
||||
const fn increment_spacing(&mut self) {
|
||||
fn increment_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_add(1);
|
||||
}
|
||||
|
||||
const fn decrement_spacing(&mut self) {
|
||||
fn decrement_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_sub(1);
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
}
|
||||
@@ -256,7 +252,7 @@ fn example_height() -> u16 {
|
||||
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);
|
||||
let [tabs, axis, demo] = layout.areas(area);
|
||||
self.tabs().render(tabs, buf);
|
||||
let scroll_needed = self.render_demo(demo, buf);
|
||||
let axis_width = if scroll_needed {
|
||||
@@ -302,7 +298,7 @@ impl App {
|
||||
/// 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)]
|
||||
#[allow(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
|
||||
@@ -370,9 +366,8 @@ impl SelectedTab {
|
||||
Self::Start => SKY.c400,
|
||||
Self::Center => SKY.c300,
|
||||
Self::End => SKY.c200,
|
||||
Self::SpaceEvenly => INDIGO.c400,
|
||||
Self::SpaceAround => INDIGO.c400,
|
||||
Self::SpaceBetween => INDIGO.c300,
|
||||
Self::SpaceAround => INDIGO.c500,
|
||||
};
|
||||
format!(" {text} ").fg(color).bg(Color::Black).into()
|
||||
}
|
||||
@@ -387,9 +382,8 @@ impl StatefulWidget for SelectedTab {
|
||||
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),
|
||||
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +415,7 @@ 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 [title, illustrations] = layout.areas(area);
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(self.flex)
|
||||
@@ -521,7 +515,7 @@ const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn get_description_height(s: &str) -> u16 {
|
||||
if s.is_empty() {
|
||||
0
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
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};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Gauge, Padding, Paragraph, Widget},
|
||||
DefaultTerminal,
|
||||
};
|
||||
|
||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||
const GAUGE2_COLOR: Color = tailwind::GREEN.c800;
|
||||
@@ -43,11 +44,14 @@ enum AppState {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
@@ -76,37 +80,38 @@ impl App {
|
||||
|
||||
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(),
|
||||
_ => {}
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn start(&mut self) {
|
||||
fn start(&mut self) {
|
||||
self.state = AppState::Started;
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
#[expect(clippy::similar_names)]
|
||||
#[allow(clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, gauge_area, footer_area] = area.layout(&layout);
|
||||
let [header_area, gauge_area, footer_area] = layout.areas(area);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 4); 4]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = gauge_area.layout(&layout);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
|
||||
|
||||
render_header(header_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
@@ -182,7 +187,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block<'_> {
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Line::from(title).centered();
|
||||
Block::new()
|
||||
.borders(Borders::NONE)
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::Context;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
widgets::Paragraph,
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
@@ -21,16 +22,19 @@ use ratatui::{DefaultTerminal, Frame};
|
||||
/// and exits when the user presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?; // augment errors / panics with easy to read messages
|
||||
ratatui::run(run).context("failed to run app")
|
||||
let terminal = ratatui::init();
|
||||
let app_result = run(terminal).context("app loop failed");
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||
/// possible, for example, you could have multiple application states and switch between them based
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
terminal.draw(draw)?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
@@ -40,7 +44,7 @@ fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
|
||||
/// Render the application. This is where you would draw the application UI. This example draws a
|
||||
/// greeting.
|
||||
fn render(frame: &mut Frame) {
|
||||
fn draw(frame: &mut Frame) {
|
||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.area());
|
||||
}
|
||||
@@ -52,11 +56,9 @@ fn render(frame: &mut Frame) {
|
||||
/// updating the application state, without blocking the event loop for too long.
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
let q_pressed = event::read()
|
||||
.context("event read failed")?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| key.code == KeyCode::Char('q'));
|
||||
return Ok(q_pressed);
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
return Ok(KeyCode::Char('q') == key.code);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -7,28 +7,47 @@
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use itertools::Itertools;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::{Line, Text};
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::Rect,
|
||||
style::Stylize,
|
||||
text::{Line, Text},
|
||||
widgets::Widget,
|
||||
DefaultTerminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
|
||||
let hyperlink = Hyperlink::new(text, "https://example.com");
|
||||
struct App {
|
||||
hyperlink: Hyperlink<'static>,
|
||||
}
|
||||
|
||||
ratatui::run(|terminal| {
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
|
||||
let hyperlink = Hyperlink::new(text, "https://example.com");
|
||||
Self { hyperlink }
|
||||
}
|
||||
|
||||
fn run(self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&hyperlink, frame.area()))?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand.workspace = true
|
||||
rand = "0.9.0"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -15,14 +15,17 @@ use std::{
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use rand::distr::{Distribution, Uniform};
|
||||
use ratatui::backend::Backend;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget};
|
||||
use ratatui::{Frame, Terminal, TerminalOptions, Viewport, symbols};
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
crossterm::event,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
@@ -107,7 +110,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||
event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
@@ -117,7 +120,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
});
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
|
||||
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
@@ -157,20 +160,17 @@ fn downloads() -> Downloads {
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_value)]
|
||||
fn run<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn run(
|
||||
terminal: &mut Terminal<impl Backend>,
|
||||
workers: Vec<Worker>,
|
||||
mut downloads: Downloads,
|
||||
rx: mpsc::Receiver<Event>,
|
||||
) -> Result<()>
|
||||
where
|
||||
B::Error: Send + Sync + 'static,
|
||||
{
|
||||
) -> Result<()> {
|
||||
let mut redraw = true;
|
||||
loop {
|
||||
if redraw {
|
||||
terminal.draw(|frame| render(frame, &downloads))?;
|
||||
terminal.draw(|frame| draw(frame, &downloads))?;
|
||||
}
|
||||
redraw = true;
|
||||
|
||||
@@ -215,14 +215,14 @@ where
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, downloads: &Downloads) {
|
||||
fn draw(frame: &mut Frame, downloads: &Downloads) {
|
||||
let area = frame.area();
|
||||
|
||||
let block = Block::new().title(Line::from("Progress").centered());
|
||||
@@ -230,12 +230,12 @@ fn render(frame: &mut Frame, downloads: &Downloads) {
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [progress_area, main] = area.layout(&vertical);
|
||||
let [list_area, gauge_area] = main.layout(&horizontal);
|
||||
let [progress_area, main] = vertical.areas(area);
|
||||
let [list_area, gauge_area] = horizontal.areas(main);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let progress = LineGauge::default()
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
@@ -265,7 +265,7 @@ fn render(frame: &mut Frame, downloads: &Downloads) {
|
||||
let list = List::new(items);
|
||||
frame.render_widget(list, list_area);
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
|
||||
@@ -23,5 +23,5 @@ Some more ideas for handling focus can be found in:
|
||||
- [`focusable`](https://crates.io/crates/focusable) (see also [Ratatui forum
|
||||
post](https://forum.ratatui.rs/t/focusable-crate-manage-focus-state-for-your-widgets/73))
|
||||
- [`rat-focus`](https://crates.io/crates/rat-focus)
|
||||
- A useful [`Bevy` discussion](https://github.com/bevyengine/bevy/discussions/15374) about focus
|
||||
- A useful [`Bevy` discssion](https://github.com/bevyengine/bevy/discussions/15374) about focus
|
||||
more generally.
|
||||
|
||||
@@ -15,19 +15,25 @@
|
||||
//! [`tui-textarea`]: https://crates.io/crates/tui-textarea
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode, KeyEvent};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Offset, Rect};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Offset, Rect},
|
||||
style::Stylize,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
|
||||
// serialize the form to JSON if the user submitted it, otherwise print "Canceled"
|
||||
match ratatui::run(|terminal| App::default().run(terminal)) {
|
||||
match result {
|
||||
Ok(Some(form)) => println!("{}", serde_json::to_string_pretty(&form)?),
|
||||
Ok(None) => println!("Canceled"),
|
||||
Err(err) => eprintln!("{err}"),
|
||||
@@ -50,7 +56,7 @@ enum AppState {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<Option<InputForm>> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<Option<InputForm>> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
@@ -67,12 +73,13 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
match event::read()? {
|
||||
Event::Key(event) if event.kind == KeyEventKind::Press => match event.code {
|
||||
KeyCode::Esc => self.state = AppState::Cancelled,
|
||||
KeyCode::Enter => self.state = AppState::Submitted,
|
||||
_ => self.form.on_key_press(key),
|
||||
}
|
||||
_ => self.form.on_key_press(event),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -115,8 +122,8 @@ impl InputForm {
|
||||
///
|
||||
/// The cursor is placed at the end of the focused field.
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let layout = Layout::vertical(Constraint::from_lengths([1, 1, 1]));
|
||||
let [first_name_area, last_name_area, age_area] = frame.area().layout(&layout);
|
||||
let [first_name_area, last_name_area, age_area] =
|
||||
Layout::vertical(Constraint::from_lengths([1, 1, 1])).areas(frame.area());
|
||||
|
||||
frame.render_widget(&self.first_name, first_name_area);
|
||||
frame.render_widget(&self.last_name, last_name_area);
|
||||
@@ -185,11 +192,11 @@ impl StringField {
|
||||
|
||||
impl Widget for &StringField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::horizontal([
|
||||
let constraints = [
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
]);
|
||||
let [label_area, value_area] = area.layout(&layout);
|
||||
];
|
||||
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
|
||||
let label = Line::from_iter([self.label, ": "]).bold();
|
||||
label.render(label_area, buf);
|
||||
self.value.clone().render(value_area, buf);
|
||||
@@ -231,14 +238,14 @@ impl AgeField {
|
||||
KeyCode::Up | KeyCode::Char('k') => self.increment(),
|
||||
KeyCode::Down | KeyCode::Char('j') => self.decrement(),
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn increment(&mut self) {
|
||||
self.value = self.value.saturating_add(1).min(Self::MAX);
|
||||
}
|
||||
|
||||
const fn decrement(&mut self) {
|
||||
fn decrement(&mut self) {
|
||||
self.value = self.value.saturating_sub(1);
|
||||
}
|
||||
|
||||
@@ -250,11 +257,11 @@ impl AgeField {
|
||||
|
||||
impl Widget for &AgeField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::horizontal([
|
||||
let constraints = [
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
]);
|
||||
let [label_area, value_area] = area.layout(&layout);
|
||||
];
|
||||
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
|
||||
let label = Line::from_iter([self.label, ": "]).bold();
|
||||
let value = self.value.to_string();
|
||||
label.render(label_area, buf);
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
//! A minimal example of a Ratatui application.
|
||||
//!
|
||||
//! This is a bare minimum example. There are many approaches to running an application loop,
|
||||
//! so this is not meant to be prescriptive. See the [examples] folder for more complete
|
||||
//! examples. In particular, the [hello-world] example is a good starting point.
|
||||
//!
|
||||
//! 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
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [hello-world]: https://github.com/ratatui/ratatui/blob/main/examples/apps/hello-world
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget("Hello World!", frame.area()))?;
|
||||
if crossterm::event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
/// A minimal example of a Ratatui application.
|
||||
///
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop,
|
||||
/// so this is not meant to be prescriptive. See the [examples] folder for more complete
|
||||
/// examples. In particular, the [hello-world] example is a good starting point.
|
||||
///
|
||||
/// 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
|
||||
/// [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
/// [hello-world]: https://github.com/ratatui/ratatui/blob/main/examples/apps/hello-world
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{text::Text, Frame};
|
||||
|
||||
fn main() {
|
||||
let mut terminal = ratatui::init();
|
||||
loop {
|
||||
terminal.draw(draw).expect("failed to draw frame");
|
||||
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
|
||||
break;
|
||||
}
|
||||
})
|
||||
}
|
||||
ratatui::restore();
|
||||
}
|
||||
|
||||
fn draw(frame: &mut Frame) {
|
||||
let text = Text::raw("Hello World!");
|
||||
frame.render_widget(text, frame.area());
|
||||
}
|
||||
|
||||
@@ -10,31 +10,40 @@
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::{error::Error, iter::once, result};
|
||||
|
||||
use crossterm::event;
|
||||
use itertools::Itertools;
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::Paragraph,
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
let terminal = ratatui::init();
|
||||
let app_result = run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [text_area, main_area] = frame.area().layout(&layout);
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [text_area, main_area] = vertical.areas(frame.area());
|
||||
frame.render_widget(
|
||||
Paragraph::new("Note: not all terminals support all modifiers")
|
||||
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
|
||||
@@ -9,8 +9,8 @@ rust-version.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
## a collection of line drawing algorithms (e.g. Bresenham's line algorithm)
|
||||
line_drawing = "1"
|
||||
rand.workspace = true
|
||||
line_drawing = "1.0.0"
|
||||
rand = "0.9.0"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -9,19 +9,27 @@
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEvent,
|
||||
MouseEventKind,
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
};
|
||||
use ratatui::{
|
||||
layout::{Position, Rect, Size},
|
||||
style::{Color, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use crossterm::execute;
|
||||
use ratatui::layout::{Position, Rect, Size};
|
||||
use ratatui::style::{Color, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::{DefaultTerminal, Frame, symbols};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| MouseDrawingApp::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let result = MouseDrawingApp::default().run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -37,7 +45,7 @@ struct MouseDrawingApp {
|
||||
}
|
||||
|
||||
impl MouseDrawingApp {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
execute!(std::io::stdout(), EnableMouseCapture)?;
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
@@ -57,11 +65,8 @@ impl MouseDrawingApp {
|
||||
}
|
||||
|
||||
/// Quit the app if the user presses 'q' or 'Esc'
|
||||
fn on_key_event(&mut self, key: KeyEvent) {
|
||||
if !key.is_press() {
|
||||
return;
|
||||
}
|
||||
match key.code {
|
||||
fn on_key_event(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Char(' ') => {
|
||||
self.current_color = Color::Rgb(rand::random(), rand::random(), rand::random());
|
||||
}
|
||||
|
||||
@@ -29,60 +29,73 @@
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
/// [Color Eyre recipe]: https://ratatui.rs/recipes/apps/color-eyre
|
||||
use color_eyre::{Result, eyre::bail};
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::Frame;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Paragraph};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PanicHandlerState {
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
use color_eyre::{eyre::bail, Result};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
text::Line,
|
||||
widgets::{Block, Paragraph},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
let mut panic_hook_state = PanicHandlerState::Enabled;
|
||||
ratatui::run(|terminal| {
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self { hook_enabled: true }
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, &panic_hook_state))?;
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => panic!("intentional demo panic"),
|
||||
KeyCode::Char('e') => bail!("intentional demo error"),
|
||||
KeyCode::Char('h') => {
|
||||
let _ = std::panic::take_hook();
|
||||
panic_hook_state = PanicHandlerState::Disabled;
|
||||
self.hook_enabled = false;
|
||||
}
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, state: &PanicHandlerState) {
|
||||
let text = vec![
|
||||
Line::from(format!("Panic hook is currently: {state:?}")),
|
||||
Line::from(""),
|
||||
Line::from("Press `p` to cause a panic"),
|
||||
Line::from("Press `e` to cause an error"),
|
||||
Line::from("Press `h` to disable the panic hook"),
|
||||
Line::from("Press `q` to quit"),
|
||||
Line::from(""),
|
||||
Line::from("When your app panics without a panic hook, you will likely have to"),
|
||||
Line::from("reset your terminal afterwards with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("Try first with the panic handler enabled, and then with it disabled"),
|
||||
Line::from("to see the difference"),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
frame.render_widget(paragraph, frame.area());
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let text = vec![
|
||||
if self.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("Press `p` to cause a panic"),
|
||||
Line::from("Press `e` to cause an error"),
|
||||
Line::from("Press `h` to disable the panic hook"),
|
||||
Line::from("Press `q` to quit"),
|
||||
Line::from(""),
|
||||
Line::from("When your app panics without a panic hook, you will likely have to"),
|
||||
Line::from("reset your terminal afterwards with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("Try first with the panic handler enabled, and then with it disabled"),
|
||||
Line::from("to see the difference"),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
frame.render_widget(paragraph, frame.area());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,84 @@
|
||||
//! A Ratatui example that demonstrates how to handle popups.
|
||||
//! See also:
|
||||
//! - <https://github.com/joshka/tui-popup> and
|
||||
//! - <https://github.com/sephiroth74/tui-confirm-dialog>
|
||||
//!
|
||||
//! 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
|
||||
/// A Ratatui example that demonstrates how to handle popups.
|
||||
// See also https://github.com/joshka/tui-popup and
|
||||
// https://github.com/sephiroth74/tui-confirm-dialog
|
||||
///
|
||||
/// 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::Frame;
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Clear};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Flex, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
// This flag will be toggled when the user presses 'p'. This could be stored in an app struct
|
||||
// if you have more state to manage than just this flag.
|
||||
let mut show_popup = false;
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
ratatui::run(|terminal| {
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, show_popup))?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => show_popup = !show_popup,
|
||||
_ => {}
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => self.show_popup = !self.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, show_popup: bool) {
|
||||
let area = frame.area();
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let area = frame.area();
|
||||
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||
let [instructions, content] = area.layout(&layout);
|
||||
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [instructions, content] = vertical.areas(area);
|
||||
|
||||
frame.render_widget(
|
||||
Line::from("Press 'p' to toggle popup, 'q' to quit").centered(),
|
||||
instructions,
|
||||
);
|
||||
let text = if self.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true });
|
||||
frame.render_widget(paragraph, instructions);
|
||||
|
||||
frame.render_widget(Block::bordered().title("Content").on_blue(), content);
|
||||
let block = Block::bordered().title("Content").on_blue();
|
||||
frame.render_widget(block, content);
|
||||
|
||||
if show_popup {
|
||||
let popup = Block::bordered().title("Popup");
|
||||
let popup_area = centered_area(area, 60, 20);
|
||||
// clears out any background in the area before rendering the popup
|
||||
frame.render_widget(Clear, popup_area);
|
||||
frame.render_widget(popup, popup_area);
|
||||
if self.show_popup {
|
||||
let block = Block::bordered().title("Popup");
|
||||
let area = popup_area(area, 60, 20);
|
||||
frame.render_widget(Clear, area); //this clears out the background
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a centered rect using up certain percentage of the available rect
|
||||
fn centered_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
|
||||
let [area] = area.layout(&vertical);
|
||||
let [area] = area.layout(&horizontal);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
area
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "release-header"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,13 +0,0 @@
|
||||
# Release Header
|
||||
|
||||
Generates a banner for Ratatui releases featuring a Ratatui logo, version info, and a list of crates.
|
||||
|
||||
Used for README.md, documentation, and release materials. Updated for every release starting with v0.30.0 "Bryndza".
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cargo run --p release-header
|
||||
```
|
||||
|
||||
Press any key to exit. Creates a fixed 68x16 terminal viewport.
|
||||
@@ -1,171 +0,0 @@
|
||||
//! Generates a terminal banner for Ratatui releases featuring a Ratatui logo, version info, and
|
||||
//! a list of crates.
|
||||
//!
|
||||
//! Used for README.md, documentation, and release materials. Updated for every release starting
|
||||
//! with v0.30.0 "Bryndza".
|
||||
//!
|
||||
//! 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;
|
||||
use std::iter::zip;
|
||||
|
||||
use ratatui::crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use ratatui::crossterm::{event, execute};
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect, Spacing};
|
||||
use ratatui::style::{Color, Stylize};
|
||||
use ratatui::symbols::merge::MergeStrategy;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, BorderType, Padding, Paragraph, RatatuiLogo};
|
||||
use ratatui::{DefaultTerminal, Frame, TerminalOptions, Viewport};
|
||||
|
||||
const SEMVER: &str = "0.30.0";
|
||||
const RELEASE_NAME: &str = "Bryndza";
|
||||
|
||||
const MAIN_DISHES: [&str; 4] = [
|
||||
"> ratatui",
|
||||
"> ratatui-core",
|
||||
"> ratatui-widgets",
|
||||
"> ratatui-macros",
|
||||
];
|
||||
const BACKENDS: [&str; 3] = [
|
||||
"> ratatui-crossterm",
|
||||
"> ratatui-termion",
|
||||
"> ratatui-termwiz",
|
||||
];
|
||||
|
||||
const FG_COLOR: Color = Color::Rgb(246, 214, 187); // #F6D6BB
|
||||
const BG_COLOR: Color = Color::Rgb(20, 20, 50); // #141432
|
||||
const MENU_BORDER_COLOR: Color = Color::Rgb(255, 255, 160); // #FFFFA0
|
||||
|
||||
enum Rainbow {
|
||||
Red,
|
||||
Orange,
|
||||
Yellow,
|
||||
Green,
|
||||
Blue,
|
||||
Indigo,
|
||||
Violet,
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let viewport = Viewport::Fixed(Rect::new(0, 0, 68, 16));
|
||||
let terminal = ratatui::init_with_options(TerminalOptions { viewport });
|
||||
execute!(stdout(), EnterAlternateScreen).expect("failed to enter alternate screen");
|
||||
let result = run(terminal);
|
||||
execute!(stdout(), LeaveAlternateScreen).expect("failed to leave alternate screen");
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let area = frame.area();
|
||||
frame.buffer_mut().set_style(area, (FG_COLOR, BG_COLOR));
|
||||
|
||||
let logo_width = 29;
|
||||
let menu_width = 23;
|
||||
let padding = 2; // Padding between logo and menu
|
||||
let menu_borders = 3;
|
||||
let height = MAIN_DISHES.len() as u16 + BACKENDS.len() as u16 + menu_borders;
|
||||
let width = logo_width + menu_width + padding;
|
||||
let center_area = area.centered(Constraint::Length(width), Constraint::Length(height));
|
||||
let layout = Layout::horizontal(Constraint::from_lengths([logo_width, padding, menu_width]));
|
||||
let [logo_area, _, menu_area] = center_area.layout(&layout);
|
||||
|
||||
render_logo(frame, logo_area);
|
||||
render_menu(frame, menu_area);
|
||||
}
|
||||
|
||||
fn render_logo(frame: &mut Frame, area: Rect) {
|
||||
let area = area.inner(Margin::new(1, 0));
|
||||
let layout = Layout::vertical(Constraint::from_lengths([6, 2, 1])).flex(Flex::End);
|
||||
let [shadow_area, logo_area, version_area] = area.layout(&layout);
|
||||
|
||||
// Divide the logo into letter sections for individual coloring, then render a block for each
|
||||
// letter with a color based on the row index.
|
||||
let letter_layout = Layout::horizontal(Constraint::from_lengths([5, 4, 4, 4, 4, 5, 1]));
|
||||
for (row_index, row) in shadow_area.rows().enumerate() {
|
||||
for (rainbow, letter_area) in zip(Rainbow::ROYGBIV, row.layout_vec(&letter_layout)) {
|
||||
let color = rainbow.gradient_color(row_index);
|
||||
frame.render_widget(Block::new().style(color), letter_area);
|
||||
}
|
||||
// Render the Ratatui logo truncated.
|
||||
frame.render_widget(RatatuiLogo::small(), row);
|
||||
}
|
||||
|
||||
frame.render_widget(Block::new().style(FG_COLOR), logo_area);
|
||||
frame.render_widget(RatatuiLogo::small(), logo_area);
|
||||
frame.render_widget(format!("v{SEMVER} \"{RELEASE_NAME}\"").dim(), version_area);
|
||||
}
|
||||
|
||||
impl Rainbow {
|
||||
const RED_GRADIENT: [u8; 6] = [41, 43, 50, 68, 104, 156];
|
||||
const GREEN_GRADIENT: [u8; 6] = [24, 30, 41, 65, 105, 168];
|
||||
const BLUE_GRADIENT: [u8; 6] = [55, 57, 62, 78, 113, 166];
|
||||
const AMBIENT_GRADIENT: [u8; 6] = [17, 18, 20, 25, 40, 60];
|
||||
|
||||
const ROYGBIV: [Self; 7] = [
|
||||
Self::Red,
|
||||
Self::Orange,
|
||||
Self::Yellow,
|
||||
Self::Green,
|
||||
Self::Blue,
|
||||
Self::Indigo,
|
||||
Self::Violet,
|
||||
];
|
||||
|
||||
fn gradient_color(&self, row: usize) -> Color {
|
||||
let ambient = Self::AMBIENT_GRADIENT[row];
|
||||
let red = Self::RED_GRADIENT[row];
|
||||
let green = Self::GREEN_GRADIENT[row];
|
||||
let blue = Self::BLUE_GRADIENT[row];
|
||||
let blue_sat = Self::AMBIENT_GRADIENT[row].saturating_mul(6 - row as u8);
|
||||
let (r, g, b) = match self {
|
||||
Self::Red => (red, ambient, blue_sat),
|
||||
Self::Orange => (red, green / 2, blue_sat),
|
||||
Self::Yellow => (red, green, blue_sat),
|
||||
Self::Green => (ambient, green, blue_sat),
|
||||
Self::Blue => (ambient, ambient, blue.max(blue_sat)),
|
||||
Self::Indigo => (blue, ambient, blue.max(blue_sat)),
|
||||
Self::Violet => (red, ambient, blue.max(blue_sat)),
|
||||
};
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_menu(frame: &mut Frame, area: Rect) {
|
||||
let layout = Layout::vertical(Constraint::from_lengths([
|
||||
MAIN_DISHES.len() as u16 + 2,
|
||||
BACKENDS.len() as u16 + 2,
|
||||
]))
|
||||
.spacing(Spacing::Overlap(1)); // Overlap to merge borders
|
||||
let [main_dishes_area, backends_area] = area.layout(&layout);
|
||||
|
||||
render_menu_block(frame, main_dishes_area, "Main Courses", &MAIN_DISHES);
|
||||
render_menu_block(frame, backends_area, "Pairings", &BACKENDS);
|
||||
}
|
||||
|
||||
fn render_menu_block(frame: &mut Frame, area: Rect, title: &str, menu_items: &[&str]) {
|
||||
let menu_block = Block::bordered()
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(MENU_BORDER_COLOR)
|
||||
.padding(Padding::horizontal(1))
|
||||
.merge_borders(MergeStrategy::Fuzzy)
|
||||
.title(title);
|
||||
|
||||
let menu_lines: Vec<Line> = menu_items.iter().map(|&item| Line::from(item)).collect();
|
||||
frame.render_widget(Paragraph::new(menu_lines).block(menu_block), area);
|
||||
}
|
||||
@@ -11,13 +11,15 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::symbols::scrollbar;
|
||||
use ratatui::text::{Line, Masked, Span};
|
||||
use ratatui::widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Alignment, Constraint, Layout, Margin},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols::scrollbar,
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
@@ -29,60 +31,58 @@ struct App {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
last_tick = Instant::now();
|
||||
continue;
|
||||
}
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.scroll_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.scroll_up(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.scroll_left(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.scroll_right(),
|
||||
_ => {}
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
self.vertical_scroll = self.vertical_scroll.saturating_add(1);
|
||||
self.vertical_scroll_state =
|
||||
self.vertical_scroll_state.position(self.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
self.vertical_scroll = self.vertical_scroll.saturating_sub(1);
|
||||
self.vertical_scroll_state =
|
||||
self.vertical_scroll_state.position(self.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('h') | KeyCode::Left => {
|
||||
self.horizontal_scroll = self.horizontal_scroll.saturating_sub(1);
|
||||
self.horizontal_scroll_state = self
|
||||
.horizontal_scroll_state
|
||||
.position(self.horizontal_scroll);
|
||||
}
|
||||
KeyCode::Char('l') | KeyCode::Right => {
|
||||
self.horizontal_scroll = self.horizontal_scroll.saturating_add(1);
|
||||
self.horizontal_scroll_state = self
|
||||
.horizontal_scroll_state
|
||||
.position(self.horizontal_scroll);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn scroll_down(&mut self) {
|
||||
self.vertical_scroll = self.vertical_scroll.saturating_add(1);
|
||||
self.vertical_scroll_state = self.vertical_scroll_state.position(self.vertical_scroll);
|
||||
}
|
||||
|
||||
const fn scroll_up(&mut self) {
|
||||
self.vertical_scroll = self.vertical_scroll.saturating_sub(1);
|
||||
self.vertical_scroll_state = self.vertical_scroll_state.position(self.vertical_scroll);
|
||||
}
|
||||
|
||||
const fn scroll_left(&mut self) {
|
||||
self.horizontal_scroll = self.horizontal_scroll.saturating_sub(1);
|
||||
self.horizontal_scroll_state = self
|
||||
.horizontal_scroll_state
|
||||
.position(self.horizontal_scroll);
|
||||
}
|
||||
|
||||
const fn scroll_right(&mut self) {
|
||||
self.horizontal_scroll = self.horizontal_scroll.saturating_add(1);
|
||||
self.horizontal_scroll_state = self
|
||||
.horizontal_scroll_state
|
||||
.position(self.horizontal_scroll);
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||
fn render(&mut self, frame: &mut Frame) {
|
||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
let area = frame.area();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
fakeit.workspace = true
|
||||
fakeit = "1.1"
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode, KeyModifiers};
|
||||
use crossterm::event::KeyModifiers;
|
||||
use itertools::Itertools;
|
||||
use ratatui::layout::{Constraint, Layout, Margin, Rect};
|
||||
use ratatui::style::{self, Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::{
|
||||
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
||||
ScrollbarState, Table, TableState,
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Stylize},
|
||||
text::Text,
|
||||
widgets::{
|
||||
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
||||
ScrollbarState, Table, TableState,
|
||||
},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use style::palette::tailwind;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -34,7 +37,10 @@ const ITEM_HEIGHT: usize = 4;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
struct TableColors {
|
||||
buffer_bg: Color,
|
||||
@@ -111,7 +117,6 @@ impl App {
|
||||
items: data_vec,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
@@ -150,44 +155,46 @@ impl App {
|
||||
self.state.select_previous_column();
|
||||
}
|
||||
|
||||
pub const fn next_color(&mut self) {
|
||||
pub fn next_color(&mut self) {
|
||||
self.color_index = (self.color_index + 1) % PALETTES.len();
|
||||
}
|
||||
|
||||
pub const fn previous_color(&mut self) {
|
||||
pub fn previous_color(&mut self) {
|
||||
let count = PALETTES.len();
|
||||
self.color_index = (self.color_index + count - 1) % count;
|
||||
}
|
||||
|
||||
pub const fn set_colors(&mut self) {
|
||||
pub fn set_colors(&mut self) {
|
||||
self.colors = TableColors::new(&PALETTES[self.color_index]);
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
let shift_pressed = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next_row(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.previous_row(),
|
||||
KeyCode::Char('l') | KeyCode::Right if shift_pressed => self.next_color(),
|
||||
KeyCode::Char('h') | KeyCode::Left if shift_pressed => {
|
||||
self.previous_color();
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
let shift_pressed = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next_row(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.previous_row(),
|
||||
KeyCode::Char('l') | KeyCode::Right if shift_pressed => self.next_color(),
|
||||
KeyCode::Char('h') | KeyCode::Left if shift_pressed => {
|
||||
self.previous_color();
|
||||
}
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_column(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_column(),
|
||||
_ => {}
|
||||
}
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_column(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_column(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, frame: &mut Frame) {
|
||||
let layout = Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
|
||||
let rects = frame.area().layout_vec(&layout);
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
|
||||
let rects = vertical.split(frame.area());
|
||||
|
||||
self.set_colors();
|
||||
|
||||
@@ -328,7 +335,7 @@ fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
(name_len as u16, address_len as u16, email_len as u16)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,22 @@
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode, KeyEvent};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::palette::tailwind::{BLUE, GREEN, SLATE};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{
|
||||
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
|
||||
StatefulWidget, Widget, Wrap,
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{
|
||||
palette::tailwind::{BLUE, GREEN, SLATE},
|
||||
Color, Modifier, Style, Stylize,
|
||||
},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
|
||||
StatefulWidget, Widget, Wrap,
|
||||
},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use ratatui::{DefaultTerminal, symbols};
|
||||
|
||||
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
|
||||
const NORMAL_ROW_BG: Color = SLATE.c950;
|
||||
@@ -27,7 +32,10 @@ const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::default().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
|
||||
@@ -64,36 +72,12 @@ impl Default for App {
|
||||
Self {
|
||||
should_exit: false,
|
||||
todo_list: TodoList::from_iter([
|
||||
(
|
||||
Status::Todo,
|
||||
"Rewrite everything with Rust!",
|
||||
"I can't hold my inner voice. He tells me to rewrite the complete universe with Rust",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Rewrite all of your tui apps with Ratatui",
|
||||
"Yes, you heard that right. Go and replace your tui with Ratatui.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Pet your cat",
|
||||
"Minnak loves to be pet by you! Don't forget to pet and give some treats!",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Walk with your dog",
|
||||
"Max is bored, go walk with him!",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Pay the bills",
|
||||
"Pay the train subscription!!!",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Refactor list example",
|
||||
"If you see this info that means I completed this task!",
|
||||
),
|
||||
(Status::Todo, "Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust"),
|
||||
(Status::Completed, "Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui."),
|
||||
(Status::Todo, "Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!"),
|
||||
(Status::Todo, "Walk with your dog", "Max is bored, go walk with him!"),
|
||||
(Status::Completed, "Pay the bills", "Pay the train subscription!!!"),
|
||||
(Status::Completed, "Refactor list example", "If you see this info that means I completed this task!"),
|
||||
]),
|
||||
}
|
||||
}
|
||||
@@ -121,17 +105,20 @@ impl TodoItem {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
self.handle_key(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.select_none(),
|
||||
@@ -146,7 +133,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
const fn select_none(&mut self) {
|
||||
fn select_none(&mut self) {
|
||||
self.todo_list.state.select(None);
|
||||
}
|
||||
|
||||
@@ -157,11 +144,11 @@ impl App {
|
||||
self.todo_list.state.select_previous();
|
||||
}
|
||||
|
||||
const fn select_first(&mut self) {
|
||||
fn select_first(&mut self) {
|
||||
self.todo_list.state.select_first();
|
||||
}
|
||||
|
||||
const fn select_last(&mut self) {
|
||||
fn select_last(&mut self) {
|
||||
self.todo_list.state.select_last();
|
||||
}
|
||||
|
||||
@@ -178,15 +165,15 @@ impl App {
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let main_layout = Layout::vertical([
|
||||
let [header_area, main_area, footer_area] = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [header_area, content_area, footer_area] = area.layout(&main_layout);
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let content_layout = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]);
|
||||
let [list_area, item_area] = content_area.layout(&content_layout);
|
||||
let [list_area, item_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
|
||||
|
||||
App::render_header(header_area, buf);
|
||||
App::render_footer(footer_area, buf);
|
||||
|
||||
@@ -9,9 +9,9 @@ rust-version.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::{fs::File, time::Duration};
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::Context;
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use ratatui::Frame;
|
||||
use ratatui::widgets::{Block, Paragraph};
|
||||
use tracing::{Level, debug, info, instrument, trace};
|
||||
use tracing_appender::non_blocking;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use tracing::{debug, info, instrument, trace, Level};
|
||||
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -40,7 +40,7 @@ fn main() -> Result<()> {
|
||||
let mut events = vec![]; // a buffer to store the recent events to display in the UI
|
||||
while !should_exit(&events) {
|
||||
handle_events(&mut events)?;
|
||||
terminal.draw(|frame| render(frame, &events))?;
|
||||
terminal.draw(|frame| draw(frame, &events))?;
|
||||
}
|
||||
ratatui::restore();
|
||||
|
||||
@@ -69,7 +69,7 @@ fn handle_events(events: &mut Vec<Event>) -> Result<()> {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn render(frame: &mut Frame, events: &[Event]) {
|
||||
fn draw(frame: &mut Frame, events: &[Event]) {
|
||||
// To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing`
|
||||
trace!(frame_count = frame.count(), event_count = events.len());
|
||||
let events = events.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
|
||||
|
||||
@@ -21,16 +21,21 @@
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode, KeyEventKind};
|
||||
use ratatui::layout::{Constraint, Layout, Position};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, List, ListItem, Paragraph};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Position},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, List, ListItem, Paragraph},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| App::new().run(terminal))
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
/// App holds the state of the application
|
||||
@@ -114,7 +119,7 @@ impl App {
|
||||
new_cursor_pos.clamp(0, self.input.chars().count())
|
||||
}
|
||||
|
||||
const fn reset_cursor(&mut self) {
|
||||
fn reset_cursor(&mut self) {
|
||||
self.character_index = 0;
|
||||
}
|
||||
|
||||
@@ -124,11 +129,11 @@ impl App {
|
||||
self.reset_cursor();
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match self.input_mode {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
@@ -154,13 +159,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let layout = Layout::vertical([
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]);
|
||||
let [help_area, input_area, messages_area] = frame.area().layout(&layout);
|
||||
let [help_area, input_area, messages_area] = vertical.areas(frame.area());
|
||||
|
||||
let (msg, style) = match self.input_mode {
|
||||
InputMode::Normal => (
|
||||
@@ -201,7 +206,7 @@ impl App {
|
||||
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
InputMode::Editing => frame.set_cursor_position(Position::new(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position can be controlled via the left and right arrow key
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand.workspace = true
|
||||
rand = "0.9.0"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -9,38 +9,68 @@
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use rand::{Rng, rng};
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Bar, BarChart, BarGroup};
|
||||
use rand::{rng, Rng};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Bar, BarChart, BarGroup},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let temperatures: Vec<u8> = (0..24).map(|_| rng().random_range(50..90)).collect();
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, &temperatures))?;
|
||||
if event::read()?.is_key_press() {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame, temperatures: &[u8]) {
|
||||
let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
|
||||
let [title, main] = frame.area().layout(&layout);
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
temperatures: Vec<u8>,
|
||||
}
|
||||
|
||||
frame.render_widget("Weather demo".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(temperatures), main);
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut rng = rng();
|
||||
let temperatures = (0..24).map(|_| rng.random_range(50..90)).collect();
|
||||
Self {
|
||||
should_exit: false,
|
||||
temperatures,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let [title, main] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)])
|
||||
.spacing(1)
|
||||
.areas(frame.area());
|
||||
|
||||
frame.render_widget("Weather demo".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(&self.temperatures), main);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vertical bar chart from the temperatures data.
|
||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart<'_> {
|
||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -51,7 +81,7 @@ fn vertical_barchart(temperatures: &[u8]) -> BarChart<'_> {
|
||||
.bar_width(5)
|
||||
}
|
||||
|
||||
fn vertical_bar(hour: usize, temperature: &u8) -> Bar<'_> {
|
||||
fn vertical_bar(hour: usize, temperature: &u8) -> Bar {
|
||||
Bar::default()
|
||||
.value(u64::from(*temperature))
|
||||
.label(Line::from(format!("{hour:>02}:00")))
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
//! An example of how to use [`WidgetRef`] to store heterogeneous widgets in a container.
|
||||
//!
|
||||
//! This example creates a `StackContainer` widget that can hold any number of widgets of
|
||||
//! different types. It creates two widgets, `Greeting` and `Farewell`, and stores them in a
|
||||
//! `StackContainer` with a vertical layout. The `StackContainer` widget renders each of its
|
||||
//! child widgets in the order they were added.
|
||||
//!
|
||||
//! 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
|
||||
/// An example of how to use [`WidgetRef`] to store heterogeneous widgets in a container.
|
||||
///
|
||||
/// This example creates a `StackContainer` widget that can hold any number of widgets of
|
||||
/// different types. It creates two widgets, `Greeting` and `Farewell`, and stores them in a
|
||||
/// `StackContainer` with a vertical layout. The `StackContainer` widget renders each of its
|
||||
/// child widgets in the order they were added.
|
||||
///
|
||||
/// 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::iter::zip;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event;
|
||||
use ratatui::Frame;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget, WidgetRef};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
widgets::{Block, Paragraph, Widget, WidgetRef},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
}
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if matches!(event::read()?, event::Event::Key(_)) {
|
||||
break Ok(());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "ratatui-state-examples"
|
||||
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"] }
|
||||
@@ -1,179 +0,0 @@
|
||||
# Ratatui State Management Examples
|
||||
|
||||
This collection demonstrates various patterns for handling both mutable and immutable state in
|
||||
Ratatui applications. Each example solves a counter problem - incrementing a counter value - but
|
||||
uses different architectural approaches. These patterns represent common solutions to state
|
||||
management challenges you'll encounter when building TUI applications.
|
||||
|
||||
For more information about widgets in Ratatui, see the [widgets module documentation](https://docs.rs/ratatui/latest/ratatui/widgets/index.html).
|
||||
|
||||
## When to Use Each Pattern
|
||||
|
||||
Choose the pattern that best fits your application's architecture and complexity:
|
||||
|
||||
- **Simple applications**: Use `render-function` or `mutable-widget` patterns
|
||||
- **Clean separation**: Consider `stateful-widget` or `component-trait` patterns
|
||||
- **Complex hierarchies**: Use `nested-*` patterns for parent-child relationships
|
||||
- **Shared state**: Use `refcell` when multiple widgets need access to the same state
|
||||
- **Advanced scenarios**: Use `widget-with-mutable-ref` when you understand Rust lifetimes well
|
||||
|
||||
## Running the Examples
|
||||
|
||||
To run any example, use:
|
||||
|
||||
```bash
|
||||
cargo run --bin example-name
|
||||
```
|
||||
|
||||
Press any key (or resize the terminal) to increment the counter. Press `<Esc>` or `q` to exit.
|
||||
|
||||
## Examples
|
||||
|
||||
### Immutable State Patterns
|
||||
|
||||
These patterns keep widget state immutable during rendering, with state updates happening outside
|
||||
the render cycle. They're generally easier to reason about and less prone to borrowing issues.
|
||||
|
||||
#### [`immutable-function.rs`] - Function-Based Immutable State
|
||||
|
||||
**Best for**: Simple applications with pure rendering functions
|
||||
**Pros**: Pure functions, easy to test, clear separation of concerns
|
||||
**Cons**: Verbose parameter passing, limited integration with Ratatui ecosystem
|
||||
|
||||
Uses standalone functions that take immutable references to state. State updates happen in the
|
||||
application loop outside of rendering.
|
||||
|
||||
#### [`immutable-shared-ref.rs`] - Shared Reference Pattern (Recommended)
|
||||
|
||||
**Best for**: Most modern Ratatui applications
|
||||
**Pros**: Reusable widgets, efficient, integrates with Ratatui ecosystem, modern best practice
|
||||
**Cons**: Requires external state management for dynamic behavior
|
||||
|
||||
Implements [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) for
|
||||
`&T`, allowing widgets to be rendered multiple times by reference without being consumed.
|
||||
|
||||
#### [`immutable-consuming.rs`] - Consuming Widget Pattern
|
||||
|
||||
**Best for**: Compatibility with older code, simple widgets created fresh each frame
|
||||
**Pros**: Simple implementation, widely compatible, familiar pattern
|
||||
**Cons**: Widget consumed on each render, requires reconstruction for reuse
|
||||
|
||||
Implements [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) directly
|
||||
on the owned type, consuming the widget when rendered.
|
||||
|
||||
### Mutable State Patterns
|
||||
|
||||
These patterns allow widgets to modify their state during rendering, useful for widgets that need
|
||||
to update state as part of their rendering behavior.
|
||||
|
||||
#### [`mutable-function.rs`] - Function-Based Mutable State
|
||||
|
||||
**Best for**: Simple applications with minimal mutable state
|
||||
**Pros**: Easy to understand, no traits to implement, direct control
|
||||
**Cons**: State gets passed around as function parameters, harder to organize as complexity grows
|
||||
|
||||
Uses simple functions that accept mutable state references. State is managed at the application
|
||||
level and passed down to render functions.
|
||||
|
||||
#### [`mutable-widget.rs`] - Mutable Widget Pattern
|
||||
|
||||
**Best for**: Self-contained widgets with their own mutable state
|
||||
**Pros**: Encapsulates state within the widget, familiar OOP-style approach
|
||||
**Cons**: Requires `&mut` references, can be challenging with complex borrowing scenarios
|
||||
|
||||
Implements [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) for
|
||||
`&mut T`, allowing the widget to mutate its own state during rendering.
|
||||
|
||||
### Intermediate Patterns
|
||||
|
||||
#### [`stateful-widget.rs`] - Stateful Widget Pattern
|
||||
|
||||
**Best for**: Clean separation of widget logic from state
|
||||
**Pros**: Separates widget logic from state, reusable, idiomatic Ratatui pattern
|
||||
**Cons**: State must be managed externally
|
||||
|
||||
Uses [`StatefulWidget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidget.html)
|
||||
to keep rendering logic separate from state management.
|
||||
|
||||
#### [`component-trait.rs`] - Custom Component Trait
|
||||
|
||||
**Best for**: Implementing consistent behavior across multiple widget types
|
||||
**Pros**: Flexible, allows custom render signatures, good for widget frameworks
|
||||
**Cons**: Non-standard, requires users to learn your custom API
|
||||
|
||||
Creates a custom trait similar to [`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html)
|
||||
but with a `&mut self` render method for direct mutation.
|
||||
|
||||
### Advanced Patterns
|
||||
|
||||
#### [`nested-mutable-widget.rs`] - Nested Mutable Widgets
|
||||
|
||||
**Best for**: Parent-child widget relationships with mutable state
|
||||
**Pros**: Hierarchical organization, each widget manages its own state
|
||||
**Cons**: Complex borrowing, requires careful lifetime management
|
||||
|
||||
Demonstrates how to nest widgets that both need mutable access to their state.
|
||||
|
||||
#### [`nested-stateful-widget.rs`] - Nested Stateful Widgets
|
||||
|
||||
**Best for**: Complex applications with hierarchical state management
|
||||
**Pros**: Clean separation, composable, scales well with application complexity
|
||||
**Cons**: More boilerplate, requires understanding of nested state patterns
|
||||
|
||||
Shows how to compose multiple [`StatefulWidget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidget.html)s
|
||||
in a parent-child hierarchy.
|
||||
|
||||
#### [`refcell.rs`] - Interior Mutability Pattern
|
||||
|
||||
**Best for**: Shared state across multiple widgets, complex state sharing scenarios
|
||||
**Pros**: Allows shared mutable access, works with immutable widget references
|
||||
**Cons**: Runtime borrow checking, potential panics, harder to debug
|
||||
|
||||
Uses [`Rc<RefCell<T>>`](https://doc.rust-lang.org/std/rc/struct.Rc.html) for interior mutability
|
||||
when multiple widgets need access to the same state.
|
||||
|
||||
#### [`widget-with-mutable-ref.rs`] - Lifetime-Based Mutable References
|
||||
|
||||
**Best for**: Advanced users who need precise control over state lifetime
|
||||
**Pros**: Zero-cost abstraction, explicit lifetime management
|
||||
**Cons**: Complex lifetimes, requires deep Rust knowledge, easy to get wrong
|
||||
|
||||
Stores mutable references directly in widget structs using explicit lifetimes.
|
||||
|
||||
## Choosing the Right Pattern
|
||||
|
||||
**For most applications, start with immutable patterns:**
|
||||
|
||||
1. **Simple apps**: Use `immutable-function` for basic rendering with external state management
|
||||
2. **Modern Ratatui**: Use `immutable-shared-ref` for reusable, efficient widgets (recommended)
|
||||
3. **Legacy compatibility**: Use `immutable-consuming` when working with older code patterns
|
||||
|
||||
**Use mutable patterns when widgets need to update state during rendering:**
|
||||
|
||||
1. **Simple mutable state**: Begin with `mutable-function` or `mutable-widget` for prototypes
|
||||
2. **Clean separation**: Use `stateful-widget` when you want to separate widget logic from state
|
||||
3. **Hierarchical widgets**: Use `nested-*` patterns for complex widget relationships
|
||||
4. **Shared state**: Use `refcell` when multiple widgets need the same state
|
||||
5. **Performance critical**: Consider `widget-with-mutable-ref` for advanced lifetime management
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Borrowing issues**: The borrow checker can be challenging with mutable state.
|
||||
[`StatefulWidget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.StatefulWidget.html)
|
||||
often provides the cleanest solution.
|
||||
- **Overengineering**: Don't use complex patterns like `refcell` or `widget-with-mutable-ref`
|
||||
unless you actually need them.
|
||||
- **State organization**: Keep state close to where it's used. Don't pass state through many
|
||||
layers unnecessarily.
|
||||
|
||||
[`component-trait.rs`]: ./src/bin/component-trait.rs
|
||||
[`immutable-consuming.rs`]: ./src/bin/immutable-consuming.rs
|
||||
[`immutable-function.rs`]: ./src/bin/immutable-function.rs
|
||||
[`immutable-shared-ref.rs`]: ./src/bin/immutable-shared-ref.rs
|
||||
[`mutable-widget.rs`]: ./src/bin/mutable-widget.rs
|
||||
[`nested-mutable-widget.rs`]: ./src/bin/nested-mutable-widget.rs
|
||||
[`nested-stateful-widget.rs`]: ./src/bin/nested-stateful-widget.rs
|
||||
[`refcell.rs`]: ./src/bin/refcell.rs
|
||||
[`mutable-function.rs`]: ./src/bin/mutable-function.rs
|
||||
[`stateful-widget.rs`]: ./src/bin/stateful-widget.rs
|
||||
[`widget-with-mutable-ref.rs`]: ./src/bin/widget-with-mutable-ref.rs
|
||||
@@ -1,83 +0,0 @@
|
||||
//! # Custom Component Trait Pattern
|
||||
//!
|
||||
//! This example demonstrates using a custom trait instead of the standard `Widget` trait for
|
||||
//! handling mutable state during rendering. This pattern is useful when you want to implement
|
||||
//! consistent behavior across multiple widget types without implementing the `Widget` trait for
|
||||
//! each one.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You're building a widget framework or library with custom behavior
|
||||
//! - You want a consistent API across multiple widget types
|
||||
//! - You need more control over the render method signature
|
||||
//! - You're prototyping widget behavior before standardizing on `Widget` or `StatefulWidget`
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Flexible - you can define custom method signatures
|
||||
//! - Consistent - enforces the same behavior across widget types
|
||||
//! - Simple - no need to understand `StatefulWidget` complexity
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Non-standard - users must learn your custom API instead of Ratatui's standard traits
|
||||
//! - Less discoverable - doesn't integrate with Ratatui's widget ecosystem
|
||||
//! - Limited reuse - can't be used with existing Ratatui functions expecting `Widget`
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The custom `Component` trait allows widgets to mutate their state directly during rendering
|
||||
//! by taking `&mut self` instead of `self`. This is similar to the mutable widget pattern but
|
||||
//! with a custom trait interface.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the custom component trait pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using a custom `Component` trait and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| counter.render(frame, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A custom trait for components that can render themselves while mutating their state.
|
||||
///
|
||||
/// This trait provides an alternative to the standard `Widget` trait by allowing components to
|
||||
/// take `&mut self`, enabling direct state mutation during rendering.
|
||||
trait Component {
|
||||
/// Render the component to the given area of the frame.
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect);
|
||||
}
|
||||
|
||||
/// A simple counter component that increments its value each time it's rendered.
|
||||
///
|
||||
/// Demonstrates how the custom `Component` trait allows widgets to maintain and mutate
|
||||
/// their own state during the rendering process.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Component for Counter {
|
||||
fn render(&mut self, frame: &mut Frame, area: Rect) {
|
||||
self.count += 1;
|
||||
frame.render_widget(format!("Counter: {count}", count = self.count), area);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//! # Consuming Widget Pattern with Immutable State
|
||||
//!
|
||||
//! This example demonstrates implementing the `Widget` trait directly on the widget type,
|
||||
//! causing it to be consumed when rendered. This was the original pattern in Ratatui and
|
||||
//! is still commonly used, especially for simple widgets that are created fresh each frame.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You're working with existing code that uses this pattern
|
||||
//! - Your widgets are simple and created fresh each frame
|
||||
//! - You want maximum compatibility with older Ratatui code
|
||||
//! - You don't need to reuse widget instances
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Simple - straightforward implementation
|
||||
//! - Compatible - works with all Ratatui versions
|
||||
//! - Familiar - widely used pattern in existing code
|
||||
//! - No borrowing - no need to manage references
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Consuming - widget is destroyed after each render
|
||||
//! - Inefficient - requires reconstruction for repeated use
|
||||
//! - Limited reuse - cannot store and reuse widget instances
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget implements `Widget` directly on the owned type, meaning it's consumed
|
||||
//! when rendered and must be recreated for subsequent renders.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the consuming widget pattern for immutable state rendering.
|
||||
///
|
||||
/// Creates a new counter widget instance each frame, showing how the consuming
|
||||
/// pattern works with immutable state that's managed externally.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
// Widget is created fresh each time and consumed when rendered
|
||||
let counter = Counter::new(count);
|
||||
frame.render_widget(counter, frame.area());
|
||||
})?;
|
||||
// State updates happen outside of widget lifecycle
|
||||
count += 1;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A simple counter widget that displays a count value.
|
||||
///
|
||||
/// Implements `Widget` directly on the owned type, meaning the widget is consumed
|
||||
/// when rendered. The count state is managed externally and passed in during construction.
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Create a new counter widget with the given count.
|
||||
fn new(count: usize) -> Self {
|
||||
Self { count }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Widget is consumed here - self is moved, not borrowed
|
||||
format!("Counter: {}", self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//! # Function-Based Pattern with Immutable State
|
||||
//!
|
||||
//! This example demonstrates using standalone functions for rendering widgets with immutable
|
||||
//! state. This pattern keeps state management completely separate from widget rendering logic,
|
||||
//! making it easy to test and reason about.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You prefer functional programming approaches
|
||||
//! - Your rendering logic is simple and doesn't need complex widget hierarchies
|
||||
//! - You want clear separation between state and rendering
|
||||
//! - You're building simple UIs or prototyping quickly
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Simple - easy to understand and test
|
||||
//! - Pure functions - no side effects in rendering
|
||||
//! - Flexible - can easily compose multiple render functions
|
||||
//! - Clear separation - state management is completely separate from rendering
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Limited - doesn't integrate with Ratatui's widget ecosystem
|
||||
//! - Verbose - requires passing state explicitly to every function
|
||||
//! - No reuse - can't be used with existing Ratatui widget infrastructure
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The function takes immutable references to both the frame and state, ensuring that
|
||||
//! rendering is a pure operation that doesn't modify state.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the function-based pattern for immutable state rendering.
|
||||
///
|
||||
/// Creates a counter state and renders it using a pure function, incrementing the counter
|
||||
/// in the application loop rather than during rendering.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter_state = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| render_counter(frame, frame.area(), &counter_state))?;
|
||||
// State updates happen outside of rendering
|
||||
counter_state.increment();
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// State for the counter.
|
||||
///
|
||||
/// This state is managed externally and passed to render functions as an immutable reference.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Increment the counter value.
|
||||
fn increment(&mut self) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure render function that displays the counter state.
|
||||
///
|
||||
/// Takes immutable references to ensure rendering has no side effects on state.
|
||||
/// This function can be easily tested and composed with other render functions.
|
||||
fn render_counter(frame: &mut Frame, area: Rect, state: &Counter) {
|
||||
frame.render_widget(format!("Counter: {}", state.count), area);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
//! # Shared Reference Pattern with Immutable State
|
||||
//!
|
||||
//! This example demonstrates implementing the `Widget` trait on a shared reference (`&Widget`)
|
||||
//! with immutable state. This is the recommended pattern for most widgets in modern Ratatui
|
||||
//! applications, as it allows widgets to be reused without being consumed.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You want to reuse widgets across multiple renders
|
||||
//! - Your widget doesn't need to modify its state during rendering
|
||||
//! - You want the benefits of Ratatui's widget ecosystem
|
||||
//! - You're building modern, efficient Ratatui applications
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Reusable - widget can be rendered multiple times without reconstruction
|
||||
//! - Efficient - no cloning or reconstruction needed
|
||||
//! - Standard - integrates with Ratatui's widget ecosystem
|
||||
//! - Modern - follows current Ratatui best practices
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Immutable - cannot modify widget state during rendering
|
||||
//! - External state - requires external state management for dynamic behavior
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget implements `Widget for &Counter`, allowing it to be rendered by reference
|
||||
//! while keeping its internal data immutable during rendering.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the shared reference pattern for immutable widget rendering.
|
||||
///
|
||||
/// Creates a counter widget that can be rendered multiple times by reference,
|
||||
/// with state updates happening outside the widget's render method.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = Counter::new();
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
// Widget is rendered by reference, can be reused
|
||||
frame.render_widget(&counter, frame.area());
|
||||
})?;
|
||||
// State updates happen outside of rendering
|
||||
counter.increment();
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget with immutable rendering behavior.
|
||||
///
|
||||
/// Implements `Widget` on a shared reference, allowing the widget to be rendered
|
||||
/// multiple times without being consumed while keeping its data immutable during rendering.
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Create a new counter.
|
||||
fn new() -> Self {
|
||||
Self { count: 0 }
|
||||
}
|
||||
|
||||
/// Increment the counter value.
|
||||
///
|
||||
/// This method modifies the counter's state outside of the rendering process,
|
||||
/// maintaining the separation between state updates and rendering.
|
||||
fn increment(&mut self) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Rendering is immutable - no state changes occur here
|
||||
format!("Counter: {}", self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//! # Render Function Pattern
|
||||
//!
|
||||
//! This example demonstrates the simplest approach to handling mutable state - using regular
|
||||
//! functions that accept mutable state references. This pattern works well for simple applications
|
||||
//! and prototypes where you don't need the complexity of widget traits.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Simple applications with minimal state management needs
|
||||
//! - Prototypes and quick experiments
|
||||
//! - When you prefer functional programming over object-oriented approaches
|
||||
//! - Applications where state is naturally managed at the top level
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Extremely simple - no traits to implement or understand
|
||||
//! - Direct and explicit - state flow is obvious
|
||||
//! - Flexible - easy to modify without interface constraints
|
||||
//! - Beginner-friendly - uses basic Rust concepts
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - State must be passed through function parameters
|
||||
//! - Harder to organize as application complexity grows
|
||||
//! - No encapsulation - state management is scattered
|
||||
//! - Less reusable than widget-based approaches
|
||||
//! - Can lead to parameter passing through many layers
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! State is managed at the application level and passed to render functions as needed.
|
||||
//! This approach works well when state is simple and doesn't need complex organization.
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the render function pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter using simple functions and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, &mut counter))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Renders a counter using a simple function-based approach.
|
||||
///
|
||||
/// Demonstrates the functional approach to state management where state is managed externally
|
||||
/// and passed in as a parameter.
|
||||
fn render(frame: &mut Frame, counter: &mut usize) {
|
||||
*counter += 1;
|
||||
frame.render_widget(format!("Counter: {counter}"), frame.area());
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
//! # Mutable Widget Pattern
|
||||
//!
|
||||
//! This example demonstrates implementing the `Widget` trait on a mutable reference (`&mut T`)
|
||||
//! to allow direct state mutation during rendering. This is one of the simplest approaches for
|
||||
//! widgets that need to maintain their own state.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You have self-contained widgets with their own state
|
||||
//! - You prefer an object-oriented approach to widget design
|
||||
//! - Your widget's state is simple and doesn't need complex sharing
|
||||
//! - You want to encapsulate state within the widget itself
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Simple and intuitive - state is encapsulated within the widget
|
||||
//! - Familiar pattern for developers coming from OOP backgrounds
|
||||
//! - Direct state access without external state management
|
||||
//! - Works well with Rust's ownership system for simple cases
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Can lead to borrowing challenges in complex scenarios
|
||||
//! - Requires mutable access to the widget, which may not always be available
|
||||
//! - Less flexible than `StatefulWidget` for shared or complex state patterns
|
||||
//! - May require careful lifetime management in nested scenarios
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget implements `Widget` for `&mut Self`, allowing it to mutate its internal state
|
||||
//! during the render call. Each render increments a counter, demonstrating state mutation.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the mutable widget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using `Widget` for `&mut Self` and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&mut counter, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that maintains its own state and increments on each render.
|
||||
///
|
||||
/// Demonstrates the mutable widget pattern by implementing `Widget` for `&mut Self`.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
counter: usize,
|
||||
}
|
||||
|
||||
impl Widget for &mut Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.counter += 1;
|
||||
format!("Counter: {counter}", counter = self.counter).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
//! # Nested Mutable Widget Pattern
|
||||
//!
|
||||
//! This example demonstrates nesting widgets that both need mutable access to their state.
|
||||
//! This pattern is useful when you have a parent-child widget relationship where both widgets
|
||||
//! need to maintain and mutate their own state during rendering.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You have hierarchical widget relationships (parent-child)
|
||||
//! - Each widget needs to maintain its own distinct state
|
||||
//! - You prefer the mutable widget pattern over StatefulWidget
|
||||
//! - Widgets have clear ownership of their state
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Clear hierarchical organization
|
||||
//! - Each widget encapsulates its own state
|
||||
//! - Intuitive parent-child relationships
|
||||
//! - State ownership is explicit
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Complex borrowing scenarios can arise
|
||||
//! - Requires careful lifetime management
|
||||
//! - May lead to borrow checker issues in complex hierarchies
|
||||
//! - Less flexible than StatefulWidget for state sharing
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The parent `App` widget contains a child `Counter` widget. Both implement `Widget` for
|
||||
//! `&mut Self`, allowing them to mutate their respective states during rendering. The parent
|
||||
//! delegates rendering to the child while maintaining its own state structure.
|
||||
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the nested mutable widget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a parent-child widget hierarchy using mutable widgets and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let app = App::default();
|
||||
ratatui::run(|terminal| app.run(terminal))
|
||||
}
|
||||
|
||||
/// The main application widget that contains and manages child widgets.
|
||||
///
|
||||
/// Demonstrates the parent widget in a nested mutable widget hierarchy.
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
counter: Counter,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Run the application with the given terminal.
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.counter.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A counter widget that maintains its own state within a nested hierarchy.
|
||||
///
|
||||
/// Can be used standalone or as a child within other widgets, demonstrating
|
||||
/// how mutable widgets can be composed together.
|
||||
#[derive(Default)]
|
||||
struct Counter {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Widget for &mut Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.count += 1;
|
||||
format!("Counter: {count}", count = self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
//! # Nested StatefulWidget Pattern
|
||||
//!
|
||||
//! This example demonstrates composing multiple `StatefulWidget`s in a parent-child hierarchy.
|
||||
//! This pattern is ideal for complex applications where you need clean separation of concerns
|
||||
//! and want to leverage the benefits of the StatefulWidget pattern at multiple levels.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Complex applications with hierarchical state management needs
|
||||
//! - When you want clean separation between widgets and their state
|
||||
//! - Building composable widget systems
|
||||
//! - Applications that need testable, reusable widget components
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Excellent separation of concerns
|
||||
//! - Highly composable and reusable widgets
|
||||
//! - Easy to test individual widgets and their state
|
||||
//! - Scales well with application complexity
|
||||
//! - Follows idiomatic Ratatui patterns
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - More boilerplate code than simpler patterns
|
||||
//! - Requires understanding of nested state management
|
||||
//! - State structures can become complex
|
||||
//! - May be overkill for simple applications
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The parent `App` widget manages application-level state while delegating specific
|
||||
//! functionality to child widgets like `Counter`. Each widget is responsible for its own
|
||||
//! state type and rendering logic, making the system highly modular.
|
||||
|
||||
use ratatui::DefaultTerminal;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::{StatefulWidget, Widget};
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the nested StatefulWidget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a parent-child widget hierarchy using StatefulWidgets and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(App::run)
|
||||
}
|
||||
|
||||
/// The main application widget using the StatefulWidget pattern.
|
||||
///
|
||||
/// Demonstrates how to compose multiple StatefulWidgets together while coordinating
|
||||
/// between different child widgets.
|
||||
struct App;
|
||||
|
||||
impl App {
|
||||
/// Run the application with the given terminal.
|
||||
fn run(terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
|
||||
let mut state = AppState { counter: 0 };
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_stateful_widget(App, frame.area(), &mut state))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Application state that contains all the state needed by the app and its child widgets.
|
||||
///
|
||||
/// Demonstrates how to organize hierarchical state in the StatefulWidget pattern.
|
||||
struct AppState {
|
||||
counter: usize,
|
||||
}
|
||||
|
||||
impl StatefulWidget for App {
|
||||
type State = AppState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Counter.render(area, buf, &mut state.counter);
|
||||
}
|
||||
}
|
||||
|
||||
/// A counter widget that uses StatefulWidget for clean state separation.
|
||||
///
|
||||
/// Focuses purely on rendering logic and can be reused with different state instances.
|
||||
struct Counter;
|
||||
|
||||
impl StatefulWidget for Counter {
|
||||
type State = usize;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
*state += 1;
|
||||
format!("Counter: {state}").render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//! # Interior Mutability Pattern (RefCell)
|
||||
//!
|
||||
//! This example demonstrates using `Rc<RefCell<T>>` for interior mutability, allowing multiple
|
||||
//! widgets to share and mutate the same state. This pattern is useful when you need shared
|
||||
//! mutable state but can't use mutable references due to borrowing 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Multiple widgets need to access and modify the same state
|
||||
//! - You can't use mutable references due to borrowing constraints
|
||||
//! - You need shared ownership of mutable data
|
||||
//! - Complex widget hierarchies where state needs to be accessed from multiple locations
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Allows shared mutable access to state
|
||||
//! - Works with immutable widget references
|
||||
//! - Enables complex state sharing patterns
|
||||
//! - Can be cloned cheaply (reference counting)
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Runtime borrow checking - potential for panics if you violate borrowing rules
|
||||
//! - Less efficient than compile-time borrow checking
|
||||
//! - Harder to debug when borrow violations occur
|
||||
//! - More complex than simpler state management patterns
|
||||
//! - Can lead to subtle bugs if not used carefully
|
||||
//!
|
||||
//! ## Important Safety Notes
|
||||
//!
|
||||
//! - Only one mutable borrow can exist at a time
|
||||
//! - Violating this rule will cause a panic at runtime
|
||||
//! - Always minimize the scope of borrows to avoid conflicts
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget wraps its state in `Rc<RefCell<T>>`, allowing the state to be shared and mutated
|
||||
//! even when the widget itself is used by value (as required by the `Widget` trait).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::ops::AddAssign;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the interior mutability pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using `Rc<RefCell<T>>` and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let counter = Counter::default();
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(counter.clone(), frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that uses interior mutability for shared state management.
|
||||
///
|
||||
/// Demonstrates how `Rc<RefCell<T>>` enables mutable state access even when the
|
||||
/// widget itself is used by value.
|
||||
#[derive(Default, Clone)]
|
||||
struct Counter {
|
||||
count: Rc<RefCell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for Counter {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.count.borrow_mut().add_assign(1);
|
||||
format!("Counter: {count}", count = self.count.borrow()).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
//! # StatefulWidget Pattern (Recommended)
|
||||
//!
|
||||
//! This example demonstrates the `StatefulWidget` trait, which is the recommended approach for
|
||||
//! handling mutable state in Ratatui applications. This pattern separates the widget's rendering
|
||||
//! logic from its state, making it more flexible and reusable.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - Most Ratatui applications (this is the recommended default)
|
||||
//! - When building reusable widget libraries
|
||||
//! - When you need clean separation between rendering logic and state
|
||||
//! - When multiple widgets might share similar state structures
|
||||
//! - When you want to follow idiomatic Ratatui patterns
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Clean separation of concerns between widget and state
|
||||
//! - Reusable - the same widget can work with different state instances
|
||||
//! - Testable - state and rendering logic can be tested independently
|
||||
//! - Composable - works well with complex application architectures
|
||||
//! - Idiomatic - follows Ratatui's recommended patterns
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Slightly more verbose than direct mutation patterns
|
||||
//! - Requires understanding of the `StatefulWidget` trait
|
||||
//! - State must be managed externally
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget defines its rendering behavior through `StatefulWidget`, while the state is
|
||||
//! managed separately. This allows the same widget to be used with different state instances
|
||||
//! and makes testing easier.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::{StatefulWidget, Widget};
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the StatefulWidget pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using `StatefulWidget` and runs the application loop,
|
||||
/// updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
terminal.draw(|frame| {
|
||||
frame.render_stateful_widget(CounterWidget, frame.area(), &mut counter)
|
||||
})?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that uses the StatefulWidget pattern for state management.
|
||||
///
|
||||
/// Demonstrates the separation of rendering logic from state, making the widget reusable
|
||||
/// with different state instances and easier to test.
|
||||
struct CounterWidget;
|
||||
|
||||
impl StatefulWidget for CounterWidget {
|
||||
type State = usize;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
*state += 1;
|
||||
format!("Counter: {state}").render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
//! # Lifetime-Based Mutable References Pattern
|
||||
//!
|
||||
//! This example demonstrates storing mutable references directly in widget structs using explicit
|
||||
//! lifetimes. This is an advanced pattern that provides zero-cost state access but requires
|
||||
//! careful lifetime management.
|
||||
//!
|
||||
//! 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
|
||||
//!
|
||||
//! ## When to Use This Pattern
|
||||
//!
|
||||
//! - You need maximum performance with zero runtime overhead
|
||||
//! - You have a good understanding of Rust lifetimes and borrowing
|
||||
//! - State lifetime is clearly defined and relatively simple
|
||||
//! - You're building performance-critical applications
|
||||
//!
|
||||
//! ## Trade-offs
|
||||
//!
|
||||
//! **Pros:**
|
||||
//! - Zero runtime cost - no reference counting or runtime borrow checking
|
||||
//! - Compile-time safety - borrow checker ensures memory safety
|
||||
//! - Direct access to state without indirection
|
||||
//! - Maximum performance for state access
|
||||
//!
|
||||
//! **Cons:**
|
||||
//! - Complex lifetime management - requires deep Rust knowledge
|
||||
//! - Easy to create compilation errors that are hard to understand
|
||||
//! - Inflexible - lifetime constraints can make code harder to refactor
|
||||
//! - Not suitable for beginners - requires advanced Rust skills
|
||||
//! - Widget structs become less reusable due to lifetime constraints
|
||||
//!
|
||||
//! ## Important Considerations
|
||||
//!
|
||||
//! - The widget's lifetime is tied to the state's lifetime
|
||||
//! - You must ensure the state outlives the widget
|
||||
//! - Lifetime annotations can become complex in larger applications
|
||||
//! - Consider simpler patterns unless performance is critical
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! The widget stores a mutable reference to external state, allowing direct access without
|
||||
//! runtime overhead. The widget must be recreated for each render call due to the lifetime
|
||||
//! constraints.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui_state_examples::is_exit_key_pressed;
|
||||
|
||||
/// Demonstrates the lifetime-based mutable references pattern for mutable state management.
|
||||
///
|
||||
/// Creates a counter widget using mutable references with explicit lifetimes and runs the
|
||||
/// application loop, updating the counter on each render cycle until the user exits.
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
ratatui::run(|terminal| {
|
||||
let mut count = 0;
|
||||
loop {
|
||||
let counter = CounterWidget { count: &mut count };
|
||||
terminal.draw(|frame| frame.render_widget(counter, frame.area()))?;
|
||||
if is_exit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A counter widget that holds a mutable reference to external state.
|
||||
///
|
||||
/// Demonstrates the lifetime-based pattern where the widget directly stores a
|
||||
/// mutable reference to external state.
|
||||
struct CounterWidget<'a> {
|
||||
count: &'a mut usize,
|
||||
}
|
||||
|
||||
impl Widget for CounterWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
*self.count += 1;
|
||||
format!("Counter: {count}", count = self.count).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//! Helper functions for checking if exit keys are pressed
|
||||
use crossterm::event::{self, KeyCode};
|
||||
|
||||
pub fn is_exit_key_pressed() -> std::io::Result<bool> {
|
||||
Ok(event::read()?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| matches!(key.code, KeyCode::Esc | KeyCode::Char('q'))))
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/vhs/release-header.tape`
|
||||
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
|
||||
# The reason for this strange size is that the social preview image for this
|
||||
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
|
||||
# without the padding for README.md, etc.
|
||||
Set Theme {"background": "#141432"}
|
||||
Set FontSize 25
|
||||
Set Width 1120
|
||||
Set Height 480
|
||||
Set Padding 0
|
||||
Hide
|
||||
Type "cargo run -p release-header"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 1s
|
||||
Screenshot assets/release-header.png
|
||||
Sleep 1s
|
||||
@@ -4,7 +4,7 @@ description = """
|
||||
Core types and traits for the Ratatui Terminal UI library.
|
||||
Widget libraries should use this crate. Applications should use the main Ratatui crate.
|
||||
"""
|
||||
version = "0.1.0-beta.0"
|
||||
version = "0.1.0-alpha.2"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
@@ -24,27 +24,11 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## enables std
|
||||
std = [
|
||||
"itertools/use_std",
|
||||
"thiserror/std",
|
||||
"kasuari/std",
|
||||
"compact_str/std",
|
||||
"unicode-truncate/std",
|
||||
"strum/std",
|
||||
]
|
||||
|
||||
## enables layout cache
|
||||
layout-cache = ["std"]
|
||||
|
||||
## enables conversions to / from colors, modifiers, and styles in the ['anstyle'] crate
|
||||
anstyle = ["dep:anstyle"]
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["std", "dep:palette"]
|
||||
|
||||
## enables portable-atomic integration for targets that don't support atomic types.
|
||||
portable-atomic = ["kasuari/portable-atomic"]
|
||||
palette = ["dep:palette"]
|
||||
|
||||
## enables the backend code that sets the underline color. Underline color is only supported by
|
||||
## the Crossterm backend, and is not supported on Windows 7.
|
||||
@@ -56,24 +40,24 @@ scrolling-regions = []
|
||||
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["std", "dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
[dependencies]
|
||||
anstyle = { workspace = true, optional = true }
|
||||
bitflags.workspace = true
|
||||
compact_str.workspace = true
|
||||
anstyle = { version = "1", optional = true }
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
document-features = { workspace = true, optional = true }
|
||||
hashbrown.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
kasuari = { workspace = true, default-features = false }
|
||||
lru.workspace = true
|
||||
palette = { workspace = true, optional = true }
|
||||
lru = "0.12.0"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
paste = "1.0.2"
|
||||
serde = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
thiserror = { workspace = true, default-features = false }
|
||||
thiserror = "2"
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate = { workspace = true, default-features = false }
|
||||
unicode-truncate = "2"
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user