Compare commits

..

3 Commits

Author SHA1 Message Date
Orhun Parmaksız
a6d580bc96 fix(examples): run the correct example for chart 2025-02-18 09:40:04 +03:00
Orhun Parmaksız
392b28c194 chore(examples): remove examples dir from ratatui 2025-02-12 00:08:56 +03:00
Orhun Parmaksız
5814c7c395 docs(examples): update app examples with tapes 2025-02-12 00:06:48 +03:00
262 changed files with 5840 additions and 17570 deletions

View File

@@ -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
View 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
View 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 }}

View 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"

View 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}"

View File

@@ -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') }}

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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
View 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 }}

View File

@@ -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

View File

@@ -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
View File

@@ -3,4 +3,3 @@ target
*.rs.rustfmt
.gdb_history
.idea/
.env

View File

@@ -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.

View File

@@ -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])

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -12,7 +12,7 @@
</details>
![Release header](https://github.com/ratatui/ratatui/blob/b23480adfa9430697071c906c7ba4d4f9bd37a73/assets/release-header.png?raw=true)
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
<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
[![Built With Ratatui](https://img.shields.io/badge/Built_With_Ratatui-000?logo=ratatui&logoColor=fff)](https://ratatui.rs/)
```
[![Built With Ratatui](https://img.shields.io/badge/Built_With_Ratatui-000?logo=ratatui&logoColor=fff)](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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -9,6 +9,7 @@ allow = [
"BSD-3-Clause",
"ISC",
"MIT",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"WTFPL",

View File

@@ -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);
}
}
}

View File

@@ -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"

View File

@@ -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(),
_ => {}
}
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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);

View File

@@ -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()))

View File

@@ -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)
}

View File

@@ -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]

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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') => {

View File

@@ -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 }

View File

@@ -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] = [

View File

@@ -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(());

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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()?;

View File

@@ -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

View File

@@ -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),

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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].

View File

@@ -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]

View File

@@ -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))

View File

@@ -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.

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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)),

View File

@@ -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]

View File

@@ -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());
}

View File

@@ -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());
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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<_>>();

View File

@@ -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

View File

@@ -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]

View File

@@ -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")))

View File

@@ -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) {

View File

@@ -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"] }

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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'))))
}

View File

@@ -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

View File

@@ -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