Compare commits
1 Commits
ratatui-ma
...
jm/modular
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26163effdf |
@@ -1,2 +0,0 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
22
.cz.toml
22
.cz.toml
@@ -32,17 +32,17 @@ schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)
|
||||
type = "list"
|
||||
name = "change_type"
|
||||
choices = [
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
]
|
||||
message = "Select the type of change you are committing"
|
||||
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
github: ratatui
|
||||
open_collective: ratatui
|
||||
25
.github/workflows/bench_base.yml
vendored
Normal file
25
.github/workflows/bench_base.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
benchmark_base_branch:
|
||||
name: Continuous Benchmarking with Bencher
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track base branch benchmarks with Bencher
|
||||
run: |
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch main \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
cargo bench
|
||||
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run and Cache Benchmarks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
benchmark_fork_pr_branch:
|
||||
name: Run Fork PR Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench > benchmark_results.txt
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_results.txt
|
||||
path: ./benchmark_results.txt
|
||||
- name: Upload GitHub Pull Request Event
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: event.json
|
||||
path: ${{ github.event_path }}
|
||||
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
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: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
async function downloadArtifact(artifactName) {
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == artifactName
|
||||
})[0];
|
||||
if (!matchArtifact) {
|
||||
core.setFailed(`Failed to find artifact: ${artifactName}`);
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
await downloadArtifact(process.env.BENCHMARK_RESULTS);
|
||||
await downloadArtifact(process.env.PR_EVENT);
|
||||
- name: Unzip Benchmark Results
|
||||
run: |
|
||||
unzip $BENCHMARK_RESULTS.zip
|
||||
unzip $PR_EVENT.zip
|
||||
- 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.number}/merge`);
|
||||
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 '${{ env.PR_HEAD }}' \
|
||||
--branch-start-point '${{ env.PR_BASE }}' \
|
||||
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||
--ci-number '${{ env.PR_NUMBER }}' \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
184
.github/workflows/ci.yml
vendored
184
.github/workflows/ci.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
@@ -20,149 +21,91 @@ concurrency:
|
||||
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
|
||||
# without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
# Lint the formatting of the codebase.
|
||||
lint-formatting:
|
||||
name: Check Formatting
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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
|
||||
components: rustfmt
|
||||
- run: cargo +nightly fmt --all --check
|
||||
|
||||
# Check for typos in the codebase.
|
||||
# See <https://github.com/crate-ci/typos/>
|
||||
lint-typos:
|
||||
name: Check Typos
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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>
|
||||
dependencies:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
cargo-machete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: bnjbvr/cargo-machete@v0.6.2
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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.
|
||||
# See <https://github.com/bnjbvr/cargo-machete/>
|
||||
cargo-machete:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: bnjbvr/cargo-machete@v0.8.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@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: clippy
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask clippy
|
||||
- run: cargo make clippy
|
||||
|
||||
# Run markdownlint on all markdown files in the repository.
|
||||
lint-markdown:
|
||||
name: Check Markdown
|
||||
markdownlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v19
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v17
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
'!target'
|
||||
|
||||
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
|
||||
# <https://app.codecov.io/gh/ratatui/ratatui>
|
||||
coverage:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@v5
|
||||
- run: cargo make coverage
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Run cargo check. This is a fast way to catch any obvious errors in the code.
|
||||
check:
|
||||
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask check --all-features
|
||||
- run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
build-no-std:
|
||||
name: Build No-Std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-none
|
||||
- uses: Swatinem/rust-cache@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 --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 --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@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
|
||||
# package.metadata.docs.rs configured in Cargo.toml. https://github.com/dtolnay/cargo-docs-rs
|
||||
lint-docs:
|
||||
name: Check Docs
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
@@ -171,48 +114,47 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask docs
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
|
||||
# the package.metadata.docs.rs configured in Cargo.toml.
|
||||
# https://github.com/dtolnay/cargo-docs-rs
|
||||
- run: cargo +nightly docs-rs
|
||||
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
# compile, or any other issues in the documentation.
|
||||
test-docs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@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.
|
||||
test-libs:
|
||||
name: Test Libs ${{ matrix.toolchain }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
steps:
|
||||
- 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.
|
||||
test-backends:
|
||||
name: Test ${{matrix.backend}} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make,nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
- run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
55
.github/workflows/release-plz.yml
vendored
55
.github/workflows/release-plz.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Release-plz
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Release unpublished packages.
|
||||
release-plz-release:
|
||||
name: Release-plz release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@v0.5
|
||||
with:
|
||||
command: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_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
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@v0.5
|
||||
with:
|
||||
command: release-pr
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
|
||||
12
.taplo.toml
12
.taplo.toml
@@ -1,12 +0,0 @@
|
||||
include = ["**/*.toml"]
|
||||
|
||||
[formatting]
|
||||
column_width = 100
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies", "dev-dependencies", "build-dependencies", "workspace.dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = true
|
||||
@@ -10,26 +10,8 @@ GitHub with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.30.0 Unreleased](#v0300-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.81.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`
|
||||
- [v0.29.0](#v0290)
|
||||
- `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`
|
||||
- `Tabs::select` now accepts `Into<Option<usize>>`
|
||||
- `Color::from_hsl` is now behind the `palette` feature
|
||||
- [v0.28.0](#v0280)
|
||||
- `Backend::size` returns `Size` instead of `Rect`
|
||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend` trait migrates to `get/set_cursor_position`
|
||||
- Ratatui now requires Crossterm 0.28.0
|
||||
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
|
||||
@@ -83,317 +65,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
|
||||
|
||||
### `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.
|
||||
|
||||
### The MSRV is now 1.81.0 ([#1786])
|
||||
|
||||
[#1786]: https://github.com/ratatui/ratatui/pull/1786
|
||||
|
||||
The minimum supported Rust version (MSRV) is now 1.81.0. This is due to the use of `#[expect]` in
|
||||
the codebase, which is only available in Rust 1.81.0 and later.
|
||||
|
||||
### `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.
|
||||
|
||||
### `FrameExt` trait for `unstable-widget-ref` feature ([#1530])
|
||||
|
||||
[#1530]: https://github.com/ratatui/ratatui/pull/1530
|
||||
|
||||
To call `Frame::render_widget_ref()` or `Frame::render_stateful_widget_ref()` you now need to:
|
||||
|
||||
1. Import the `FrameExt` trait from `ratatui::widgets`.
|
||||
2. Enable the `unstable-widget-ref` feature.
|
||||
|
||||
For example:
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
widgets::{Block, FrameExt},
|
||||
};
|
||||
|
||||
let block = Block::new();
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
frame.render_widget_ref(&block, area);
|
||||
```
|
||||
|
||||
### `WidgetRef` no longer has a blanket implementation of Widget
|
||||
|
||||
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
|
||||
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
|
||||
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
|
||||
type.
|
||||
|
||||
```diff
|
||||
-impl WidgetRef for Foo {
|
||||
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
|
||||
+impl Widget for &Foo {
|
||||
+ fn render(self, area: Rect, buf: &mut Buffer)
|
||||
}
|
||||
```
|
||||
|
||||
### The `From` impls for backend types are now replaced with more specific traits [#1464]
|
||||
|
||||
[#1464]: https://github.com/ratatui/ratatui/pull/1464
|
||||
|
||||
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
|
||||
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
|
||||
|
||||
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
|
||||
once the crossterm types are moved to a separate crate.
|
||||
|
||||
```diff
|
||||
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
|
||||
|
||||
let crossterm_color = crossterm::style::Color::Black;
|
||||
- let ratatui_color = crossterm_color.into();
|
||||
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
|
||||
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
|
||||
- let crossterm_color = ratatui_color.into();
|
||||
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
|
||||
+ let crossterm_color = ratatui_color.into_crossterm();
|
||||
|
||||
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
|
||||
- let ratatui_modifier = crossterm_attribute.into();
|
||||
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
|
||||
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
|
||||
- let crossterm_attribute = ratatui_modifier.into();
|
||||
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
|
||||
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
|
||||
```
|
||||
|
||||
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
|
||||
Crossterm and the various Termion and Termwiz types as well.
|
||||
|
||||
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
|
||||
|
||||
[#1471]: https://github.com/ratatui/ratatui/pull/1471
|
||||
|
||||
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
|
||||
|
||||
for `Bar::label()`:
|
||||
|
||||
```diff
|
||||
- Bar::default().label("foo".into());
|
||||
+ Bar::default().label("foo");
|
||||
```
|
||||
|
||||
for `BarGroup::label()`:
|
||||
|
||||
```diff
|
||||
- BarGroup::default().label("bar".into());
|
||||
+ BarGroup::default().label("bar");
|
||||
```
|
||||
|
||||
### `Bar::text_value` now accepts `Into<String>` ([#1471])
|
||||
|
||||
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
|
||||
|
||||
for `Bar::text_value()`:
|
||||
|
||||
```diff
|
||||
- Bar::default().text_value("foobar".into());
|
||||
+ 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.
|
||||
|
||||
## [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])
|
||||
|
||||
[#1326]: https://github.com/ratatui/ratatui/pull/1326
|
||||
|
||||
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
|
||||
instead of `&[u64]`.
|
||||
|
||||
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
|
||||
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
|
||||
data points differently, depending on whether they are present or absent.
|
||||
|
||||
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
|
||||
addition to any other styling applied to the `Sparkline`.
|
||||
|
||||
Several `From` implementations have been added to `SparklineBar` to support existing callers who
|
||||
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
|
||||
|
||||
If you encounter any type inference issues, you may need to provide an explicit type for the data
|
||||
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
|
||||
`into()` to convert it to form that can be used as a `SparklineBar`:
|
||||
|
||||
```diff
|
||||
let value = 1u8;
|
||||
- Sparkline::default().data(&[value.into()]);
|
||||
+ Sparkline::default().data(&[u64::from(value)]);
|
||||
```
|
||||
|
||||
As a consequence of this change, the `data` method is no longer a `const fn`.
|
||||
|
||||
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
|
||||
|
||||
[#1418]: https://github.com/ratatui/ratatui/pull/1418
|
||||
|
||||
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
|
||||
single `palette::Hsl` value and is gated behind a `palette` feature flag.
|
||||
|
||||
```diff
|
||||
- Color::from_hsl(360.0, 100.0, 100.0)
|
||||
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
|
||||
```
|
||||
|
||||
### Removed public fields from `Rect` iterators ([#1358], [#1424])
|
||||
|
||||
[#1358]: https://github.com/ratatui/ratatui/pull/1358
|
||||
[#1424]: https://github.com/ratatui/ratatui/pull/1424
|
||||
|
||||
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
|
||||
These fields were not intended to be public and should not have been accessed directly.
|
||||
|
||||
### `Rect::area()` now returns u32 instead of u16 ([#1378])
|
||||
|
||||
[#1378]: https://github.com/ratatui/ratatui/pull/1378
|
||||
|
||||
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
|
||||
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
|
||||
|
||||
### `Line` now implements `From<Cow<str>` ([#1373])
|
||||
|
||||
[#1373]: https://github.com/ratatui/ratatui/pull/1373
|
||||
|
||||
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
|
||||
|
||||
```rust
|
||||
// given:
|
||||
struct Foo { ... }
|
||||
impl From<Foo> for String { ... }
|
||||
impl From<Foo> for Cow<str> { ... }
|
||||
|
||||
let foo = Foo { ... };
|
||||
let line = Line::from(foo); // now fails due to now ambiguous inferred type
|
||||
// replace with e.g.
|
||||
let line = Line::from(String::from(foo));
|
||||
```
|
||||
|
||||
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
|
||||
|
||||
[#1413]: https://github.com/ratatui/ratatui/pull/1413
|
||||
|
||||
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
|
||||
any code already using parameter type inference:
|
||||
|
||||
```diff
|
||||
let selected = 1u8;
|
||||
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
|
||||
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
|
||||
```
|
||||
|
||||
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
||||
|
||||
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
||||
|
||||
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
|
||||
|
||||
Also, the serialized output of the `TableState` will now include the "selected_column" field.
|
||||
Software that manually parse the serialized the output (with anything other than the `Serialize`
|
||||
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
|
||||
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
|
||||
implementation on the state.
|
||||
|
||||
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
|
||||
## v0.28.0
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||
|
||||
@@ -462,7 +134,7 @@ are also named terminal, and confusion about module exports for newer Rust users
|
||||
|
||||
This change simplifies the trait and makes it easier to implement.
|
||||
|
||||
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
|
||||
### `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
|
||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||
|
||||
|
||||
3003
CHANGELOG.md
3003
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -31,15 +31,8 @@ guarantee that the behavior is unchanged.
|
||||
|
||||
### 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
|
||||
```
|
||||
Run `cargo make format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
@@ -63,7 +56,7 @@ documented.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
Running `cargo xtask ci` before pushing will perform the same checks that we do in the CI process.
|
||||
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
|
||||
It's not mandatory to do this before pushing, however it may save you time to do so instead of
|
||||
waiting for GitHub to run the checks.
|
||||
|
||||
@@ -78,17 +71,17 @@ in GitHub docs.
|
||||
|
||||
### Setup
|
||||
|
||||
TL;DR: Clone the repo and build it using `cargo xtask`.
|
||||
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
|
||||
|
||||
Ratatui is an ordinary Rust project where common tasks are managed with
|
||||
[cargo-xtask](https://github.com/matklad/cargo-xtask). It wraps common `cargo` commands with sane
|
||||
[cargo-make](https://github.com/sagiegurari/cargo-make/). It wraps common `cargo` commands with sane
|
||||
defaults depending on your platform of choice. Building the project should be as easy as running
|
||||
`cargo xtask build`.
|
||||
`cargo make build`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui/ratatui.git
|
||||
cd ratatui
|
||||
cargo xtask build
|
||||
cargo make build
|
||||
```
|
||||
|
||||
### Tests
|
||||
@@ -189,7 +182,7 @@ We use GitHub Actions for the CI where we perform the following checks:
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo xtask ci` which will offer you
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you
|
||||
a shorter feedback loop than pushing to github.
|
||||
|
||||
## Relationship with `tui-rs`
|
||||
|
||||
4342
Cargo.lock
generated
4342
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
164
Cargo.toml
164
Cargo.toml
@@ -1,119 +1,57 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
# this is not included as it doesn't compile on windows
|
||||
# "ratatui-termion",
|
||||
"ratatui-macros",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"examples/apps/*",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
repository = "https://github.com/ratatui/ratatui"
|
||||
homepage = "https://ratatui.rs"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
categories = ["command-line-interface"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
edition = "2024"
|
||||
rust-version = "1.85.0"
|
||||
members = ["ratatui*"]
|
||||
# disabled for now because of the orphan rule on conversions
|
||||
# <https://github.com/ratatui/ratatui/issues/1388#issuecomment-2379895747>
|
||||
exclude = ["ratatui-termion", "ratatui-termwiz"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anstyle = "1"
|
||||
bitflags = "2.9"
|
||||
color-eyre = "0.6"
|
||||
compact_str = { version = "0.9", default-features = false }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
crossterm = "0.29"
|
||||
document-features = "0.2"
|
||||
fakeit = "1"
|
||||
futures = "0.3"
|
||||
hashbrown = "0.15"
|
||||
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.14"
|
||||
palette = "0.7"
|
||||
pretty_assertions = "1"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.4" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.5" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.4" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.3" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.4" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.4" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.4" }
|
||||
rstest = "0.25"
|
||||
ratatui = { path = "ratatui" }
|
||||
ratatui-core = { path = "ratatui-core" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm" }
|
||||
ratatui-termion = { path = "ratatui-termion" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz" }
|
||||
ratatui-widgets = { path = "ratatui-widgets" }
|
||||
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
crossterm = { version = "0.28.1" }
|
||||
document-features = { version = "0.2.7" }
|
||||
instability = "0.3.1"
|
||||
itertools = "0.13"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
palette = { version = "0.7.6" }
|
||||
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"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = "0.3"
|
||||
trybuild = "1"
|
||||
unicode-segmentation = "1"
|
||||
unicode-truncate = { version = "2", default-features = false }
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = "=0.2.0"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
termion = { version = "4.0.0" }
|
||||
termwiz = { version = "0.22.0" }
|
||||
time = { version = "0.3.11", features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "=0.1.13"
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
# dev-dependencies
|
||||
argh = "0.1.12"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.40.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
tokio = { version = "1.39.2", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023-2025 The Ratatui Developers
|
||||
Copyright (c) 2023-2024 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
154
Makefile.toml
Normal file
154
Makefile.toml
Normal file
@@ -0,0 +1,154 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
description = "Run continuous integration tasks"
|
||||
dependencies = ["lint", "clippy", "check", "test"]
|
||||
|
||||
[tasks.lint]
|
||||
description = "Lint code style (formatting, typos, docs, markdown)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.lint-format]
|
||||
description = "Lint code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.format]
|
||||
description = "Fix code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all"]
|
||||
|
||||
[tasks.lint-typos]
|
||||
description = "Run typo checks"
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.lint-docs]
|
||||
description = "Check documentation for errors and warnings"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"rustdoc",
|
||||
"--all-features",
|
||||
"--",
|
||||
"-Zunstable-options",
|
||||
"--check",
|
||||
"-Dwarnings",
|
||||
]
|
||||
|
||||
[tasks.lint-markdown]
|
||||
description = "Check markdown files for errors and warnings"
|
||||
command = "markdownlint-cli2"
|
||||
args = ["**/*.md", "!target"]
|
||||
|
||||
[tasks.check]
|
||||
description = "Check code for errors and warnings"
|
||||
command = "cargo"
|
||||
args = ["check", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.build]
|
||||
description = "Compile the project"
|
||||
command = "cargo"
|
||||
args = ["build", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Run Clippy for linting"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.install-nextest]
|
||||
description = "Install cargo-nextest"
|
||||
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
run_task = { name = ["test-lib", "test-doc"] }
|
||||
|
||||
[tasks.test-lib]
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = ["nextest", "run", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc", "--all-features"]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${NON_BACKEND_FEATURES},${@}",
|
||||
]
|
||||
|
||||
[tasks.coverage]
|
||||
description = "Generate code coverage report"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"run",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}",
|
||||
"--features",
|
||||
"all-widgets",
|
||||
]
|
||||
|
||||
[tasks.build-examples]
|
||||
description = "Compile project examples"
|
||||
command = "cargo"
|
||||
args = ["build", "--examples", "--release", "--features", "all-widgets"]
|
||||
|
||||
[tasks.run-examples]
|
||||
description = "Run project examples"
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
477
README.md
477
README.md
@@ -1,170 +1,433 @@
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [Quickstart](#quickstart)
|
||||
- [Documentation](#documentation)
|
||||
- [Templates](#templates)
|
||||
- [Built with Ratatui](#built-with-ratatui)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Contributing](#contributing)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [License](#license)
|
||||
- [Ratatui](#ratatui)
|
||||
- [Installation](#installation)
|
||||
- [Introduction](#introduction)
|
||||
- [Other documentation](#other-documentation)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
|
||||
- [Drawing the UI](#drawing-the-ui)
|
||||
- [Handling events](#handling-events)
|
||||
- [Example](#example)
|
||||
- [Layout](#layout)
|
||||
- [Text and styling](#text-and-styling)
|
||||
- [Status of this fork](#status-of-this-fork)
|
||||
- [Widgets](#widgets)
|
||||
- [Built in](#built-in)
|
||||
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
- [Apps](#apps)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
- [License](#license)
|
||||
|
||||
</details>
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]][Crate] [![Repo Badge]][Repo] [![Docs Badge]][Docs] [![License Badge]][License] \
|
||||
[![CI Badge]][CI] [![Deps Badge]][Deps] [![Codecov Badge]][Codecov] [![Sponsors Badge]][Sponsors] \
|
||||
[Ratatui Website] · [Docs] · [Widget Examples] · [App Examples] · [Changelog] \
|
||||
[Breaking Changes] · [Contributing] · [Report a bug] · [Request a Feature]
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
|
||||
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
|
||||
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
|
||||
[![Forum Badge]][Forum]<br>
|
||||
|
||||
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
|
||||
</div>
|
||||
|
||||
[Ratatui][Ratatui Website] (_ˌræ.təˈtu.i_) is a Rust crate for cooking up terminal user interfaces
|
||||
(TUIs). It provides a simple and flexible way to create text-based user interfaces in the terminal,
|
||||
which can be used for command-line applications, dashboards, and other interactive console programs.
|
||||
# Ratatui
|
||||
|
||||
## Quickstart
|
||||
[Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
|
||||
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
|
||||
Ratatui has [templates] available to help you get started quickly. You can use the
|
||||
[`cargo-generate`] command to create a new project with Ratatui:
|
||||
## Installation
|
||||
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo install --locked cargo-generate
|
||||
cargo generate ratatui/templates
|
||||
cargo add ratatui
|
||||
```
|
||||
|
||||
Selecting the Hello World template produces the following application:
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
|
||||
```rust
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
## Introduction
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if matches!(event::read()?, Event::Key(_)) {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
frame.render_widget("hello world", frame.area());
|
||||
}
|
||||
```
|
||||
## Other documentation
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials.
|
||||
- [Ratatui Forum] - a place to ask questions and discuss the library.
|
||||
- [Widget Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [App Examples] - a collection of more complex examples that demonstrate how to build apps.
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
|
||||
- [API Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
You can also watch the [EuroRust 2024 talk] to learn about common concepts in Ratatui and what's
|
||||
possible to build with it.
|
||||
## Quickstart
|
||||
|
||||
## Templates
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
the [Examples] directory. For more guidance on different ways to structure your application see
|
||||
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
|
||||
If you're looking to get started quickly, you can use one of the available templates from the
|
||||
[templates] repository using [`cargo-generate`]:
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
|
||||
```shell
|
||||
cargo generate ratatui/templates
|
||||
- Initialize the terminal
|
||||
- A main loop to:
|
||||
- Handle input events
|
||||
- Draw the UI
|
||||
- Restore the terminal state
|
||||
|
||||
The library contains a [`prelude`] module that re-exports the most commonly used traits and
|
||||
types for convenience. Most examples in the documentation will use this instead of showing the
|
||||
full path of each type.
|
||||
|
||||
### Initialize and restore the terminal
|
||||
|
||||
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
|
||||
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
|
||||
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
|
||||
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
|
||||
[Termwiz].
|
||||
|
||||
Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
### Drawing the UI
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
|
||||
Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
|
||||
let mut should_quit = false;
|
||||
while !should_quit {
|
||||
terminal.draw(ui)?;
|
||||
should_quit = handle_events()?;
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events() -> io::Result<bool> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.area(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Built with Ratatui
|
||||
Running this example produces the following output:
|
||||
|
||||
[][awesome-ratatui]
|
||||
![docsrs-hello]
|
||||
|
||||
Check out the [showcase] section of the website, or the [awesome-ratatui] repository for a curated
|
||||
list of awesome apps and libraries built with Ratatui!
|
||||
## Layout
|
||||
|
||||
## Alternatives
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
allows you to split the available space into multiple areas and then render widgets in each
|
||||
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
section of the [Ratatui Website] for more info.
|
||||
|
||||
- [Cursive](https://crates.io/crates/cursive) - a ncurses-based TUI library.
|
||||
- [iocraft](https://crates.io/crates/iocraft) - a declarative TUI library.
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
widgets::Block,
|
||||
Frame,
|
||||
};
|
||||
|
||||
## Contributing
|
||||
fn ui(frame: &mut Frame) {
|
||||
let [title_area, main_area, status_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(frame.area());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
|
||||
[![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix] [![Forum Badge]][Ratatui Forum]
|
||||
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
frame.render_widget(Block::bordered().title("Left"), left_area);
|
||||
frame.render_widget(Block::bordered().title("Right"), right_area);
|
||||
}
|
||||
```
|
||||
|
||||
Feel free to join our [Discord server](https://discord.gg/pMCEU9hNEj) for discussions and questions!
|
||||
There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org). We have also recently launched the
|
||||
[Ratatui Forum].
|
||||
Running this example produces the following output:
|
||||
|
||||
We rely on GitHub for [bugs][Report a bug] and [feature requests][Request a Feature].
|
||||
![docsrs-layout]
|
||||
|
||||
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines before [creating a pull
|
||||
request][Create a Pull Request].
|
||||
## Text and styling
|
||||
|
||||
## Acknowledgements
|
||||
The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
|
||||
many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
|
||||
is a string with a specific style.
|
||||
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development. None of
|
||||
this could be possible without [Florian Dehau] who originally created [tui-rs] which inspired many
|
||||
Rust TUIs.
|
||||
The [`style` module] provides types that represent the various styling options. The most
|
||||
important one is [`Style`] which represents the foreground and background colors and the text
|
||||
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
Special thanks to [Pavel Fomchenkov] for his work in designing an awesome logo for the Ratatui
|
||||
project and organization.
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
## License
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
|
||||
This project is licensed under the [MIT License][License].
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Hello "),
|
||||
Span::styled(
|
||||
"World",
|
||||
Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
"!".red().on_light_yellow().italic(),
|
||||
]);
|
||||
frame.render_widget(line, areas[0]);
|
||||
|
||||
// using the short-hand syntax and implicit conversions
|
||||
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
frame.render_widget(paragraph, areas[1]);
|
||||
|
||||
// style the whole widget instead of just the text
|
||||
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
|
||||
frame.render_widget(paragraph, areas[2]);
|
||||
|
||||
// use the simpler short-hand syntax
|
||||
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
frame.render_widget(paragraph, areas[3]);
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-styling]
|
||||
|
||||
[Repo]: https://github.com/ratatui/ratatui
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Ratatui Forum]: https://forum.ratatui.rs
|
||||
[Docs]: https://docs.rs/ratatui
|
||||
[Widget Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui-widgets/examples
|
||||
[App Examples]: https://github.com/ratatui/ratatui/tree/main/examples
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[EuroRust 2024 talk]: https://www.youtube.com/watch?v=hWG51Mc1DlM
|
||||
[Installation]: https://ratatui.rs/installation/
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
[Backends]: https://ratatui.rs/concepts/backends/
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
[Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
[Layout]: https://ratatui.rs/how-to/layout/
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[API Docs]: https://docs.rs/ratatui
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
[`Frame`]: terminal::Frame
|
||||
[`render_widget`]: terminal::Frame::render_widget
|
||||
[`Widget`]: widgets::Widget
|
||||
[`Layout`]: layout::Layout
|
||||
[`Text`]: text::Text
|
||||
[`Line`]: text::Line
|
||||
[`Span`]: text::Span
|
||||
[`Style`]: style::Style
|
||||
[`style` module]: style
|
||||
[`Stylize`]: style::Stylize
|
||||
[`Backend`]: backend::Backend
|
||||
[`backend` module]: backend
|
||||
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
|
||||
[Crate]: https://crates.io/crates/ratatui
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[Sponsors]: https://github.com/sponsors/ratatui
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&color=E05D44
|
||||
[Repo Badge]: https://img.shields.io/badge/repo-ratatui/ratatui-1370D3?style=flat-square&logo=github
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3
|
||||
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
[Deps Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?path=ratatui&style=flat-square
|
||||
[Deps]: https://deps.rs/repo/github/ratatui/ratatui?path=ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/badge/docs-ratatui-1370D3?style=flat-square&logo=rust
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
[Forum]: https://forum.ratatui.rs
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[showcase]: https://ratatui.rs/showcase/
|
||||
[awesome-ratatui]: https://github.com/ratatui/awesome-ratatui
|
||||
[Pavel Fomchenkov]: https://github.com/nawok
|
||||
[Florian Dehau]: https://github.com/fdehau
|
||||
[`cargo-generate`]: https://crates.io/crates/cargo-generate
|
||||
[License]: ./LICENSE
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
## Status of this fork
|
||||
|
||||
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
|
||||
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
|
||||
the community forked the project and created this crate. We look forward to continuing the work
|
||||
started by Florian 🚀
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
|
||||
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
|
||||
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
|
||||
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
|
||||
Pull Request].
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each widget has an associated example which can be found in the [Examples] folder. Run each example
|
||||
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`ratatui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`ratatui::style::Color`
|
||||
- [templates](https://github.com/ratatui/templates) — Starter templates for
|
||||
bootstrapping a Rust TUI application with Ratatui & crossterm
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
|
||||
with a React/Elm inspired approach
|
||||
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
|
||||
Tui-realm
|
||||
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Widget for tree data
|
||||
structures.
|
||||
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
|
||||
windows and their rendering
|
||||
- [tui-textarea](https://github.com/rhysd/tui-textarea) — Simple yet powerful multi-line text editor
|
||||
widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
- [tui-input](https://github.com/sayanarijit/tui-input) — TUI input library supporting multiple
|
||||
backends and tui-rs.
|
||||
- [tui-term](https://github.com/a-kenji/tui-term) — A pseudoterminal widget library
|
||||
that enables the rendering of terminal applications as ratatui widgets.
|
||||
|
||||
## Apps
|
||||
|
||||
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and ratatui organization.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
@@ -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
|
||||
|
||||
80
bacon.toml
80
bacon.toml
@@ -8,64 +8,101 @@
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "xtask", "check"]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "xtask", "check", "--all-features"]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "xtask", "check-backend", "crossterm"]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "xtask", "check-backend", "termion"]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "xtask", "check-backend", "termwiz"]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy-all]
|
||||
command = ["cargo", "xtask", "clippy"]
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = ["cargo", "xtask", "test"]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = ["cargo", "xtask", "test-libs"]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "xtask", "docs"]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = ["cargo", "xtask", "docs", "--open"]
|
||||
on_success = "job:doc"
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
[jobs.coverage]
|
||||
command = ["cargo", "xtask", "coverage"]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = ["cargo", "xtask", "coverage", "--lib"]
|
||||
|
||||
[jobs.hack]
|
||||
command = ["cargo", "xtask", "hack"]
|
||||
|
||||
[jobs.format]
|
||||
command = ["cargo", "xtask", "format"]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
ctrl-h = "job:hack"
|
||||
# alt-m = "job:my-job"
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
@@ -73,4 +110,3 @@ v = "job:coverage"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
f = "job:format"
|
||||
|
||||
33
cliff.toml
33
cliff.toml
@@ -24,14 +24,14 @@ body = """
|
||||
{%- if not version %}
|
||||
## [unreleased]
|
||||
{% else -%}
|
||||
## {{ package }} - [{{ version }}]({{ release_link }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
|
||||
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
|
||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{%- endif %}
|
||||
@@ -57,21 +57,36 @@ body = """
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
**Full Changelog**: {{ release_link }}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.owner }}/{{ remote.repo }}\
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
|
||||
{% 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 = '' },
|
||||
@@ -92,15 +107,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" },
|
||||
|
||||
16
clippy.toml
16
clippy.toml
@@ -1 +1,17 @@
|
||||
avoid-breaking-exported-api = false
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
|
||||
# ratatui -> bitflags v2.3
|
||||
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
29
deny.toml
29
deny.toml
@@ -4,16 +4,13 @@
|
||||
version = 2
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
@@ -26,15 +23,3 @@ multiple-versions = "allow"
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "ring"
|
||||
# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses
|
||||
# https://spdx.org/licenses/OpenSSL.html
|
||||
# ISC - Both BoringSSL and ring use this for their new files
|
||||
# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT
|
||||
# license, for third_party/fiat, which, unlike other third_party directories, is
|
||||
# compiled into non-test libraries, is included below."
|
||||
# OpenSSL - Obviously
|
||||
expression = "ISC AND MIT AND OpenSSL"
|
||||
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
# Examples
|
||||
|
||||
This folder contains examples that are more application focused.
|
||||
There are also [widget examples] in `ratatui-widgets`.
|
||||
|
||||
[widget examples]: ../ratatui-widgets/examples
|
||||
|
||||
You can run these examples using:
|
||||
|
||||
```shell
|
||||
cargo run -p example-name
|
||||
```
|
||||
|
||||
This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||
of the `main` branch for code which is guaranteed to work with the released ratatui version.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> There may be backwards incompatible changes in these examples, as they are designed to compile
|
||||
> against the `main` branch.
|
||||
>
|
||||
> There are a few workaround for this problem:
|
||||
>
|
||||
> - View the examples as they were when the latest version was release by selecting the tag that
|
||||
> matches that version. E.g. <https://github.com/ratatui/ratatui/tree/v0.26.1/examples>.
|
||||
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
|
||||
> allows you to select any previous tagged version.
|
||||
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
|
||||
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
|
||||
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
|
||||
> the dependency, or remotely by adding `git = "https://github.com/ratatui/ratatui"`
|
||||
>
|
||||
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
|
||||
>
|
||||
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
|
||||
> `git-cliff -u` against a cloned version of this repository.
|
||||
|
||||
## Design choices
|
||||
|
||||
The examples contain some opinionated choices in order to make it easier for newer rustaceans to
|
||||
easily be productive in creating applications:
|
||||
|
||||
- Each example has an `App` struct, with methods that implement a main loop, handle events and drawing
|
||||
the UI.
|
||||
- We use `color_eyre` for handling errors and panics. See [How to use color-eyre with Ratatui] on the
|
||||
website for more information about this.
|
||||
- Common code is not extracted into a separate file. This makes each example self-contained and easy
|
||||
to read as a whole.
|
||||
|
||||
[How to use color-eyre with Ratatui]: https://ratatui.rs/recipes/apps/color-eyre/
|
||||
|
||||
## Demo
|
||||
|
||||
This is the original demo example from the main README. It is available for each of the backends.
|
||||
[Source](./apps/demo/).
|
||||
|
||||

|
||||
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. [Source](./apps/demo2/).
|
||||
|
||||

|
||||
|
||||
## Async GitHub
|
||||
|
||||
Shows how to fetch data from GitHub API asynchronously. [Source](./apps/async-github/).
|
||||
|
||||
![Async GitHub demo][async-github.gif]
|
||||
|
||||
## Calendar Explorer
|
||||
|
||||
Shows how to render a calendar with different styles. [Source](./apps/calendar-explorer/).
|
||||
|
||||
![Calendar explorer demo][calendar-explorer.gif]
|
||||
|
||||
## Canvas
|
||||
|
||||
Shows how to render a canvas with different shapes. [Source](./apps/canvas/).
|
||||
|
||||
![Canvas demo][canvas.gif]
|
||||
|
||||
## Chart
|
||||
|
||||
Shows how to render line, bar, and scatter charts. [Source](./apps/chart/).
|
||||
|
||||
![Chart demo][chart.gif]
|
||||
|
||||
## Color Explorer
|
||||
|
||||
Shows how to handle the supported colors. [Source](./apps/color-explorer/).
|
||||
|
||||
![Color explorer demo][color-explorer.gif]
|
||||
|
||||
## Colors-RGB demo
|
||||
|
||||
Shows the full range of RGB colors in an animation. [Source](./apps/colors-rgb/).
|
||||
|
||||
![Colors-RGB demo][colors-rgb.gif]
|
||||
|
||||
## Constraint Explorer
|
||||
|
||||
Shows how different constraints can be used to layout widgets. [Source](./apps/constraint-explorer/).
|
||||
|
||||
![Constraint Explorer demo][constraint-explorer.gif]
|
||||
|
||||
## Constraints
|
||||
|
||||
Shows different types of constraints. [Source](./apps/constraints/).
|
||||
|
||||
![Constraints demo][constraints.gif]
|
||||
|
||||
## Custom Widget
|
||||
|
||||
Shows how to create a custom widget that can be interacted with the mouse. [Source](./apps/custom-widget/).
|
||||
|
||||
![Custom widget demo][custom-widget.gif]
|
||||
|
||||
## Hyperlink
|
||||
|
||||
Shows how to render hyperlinks in a terminal using [OSC
|
||||
8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). [Source](./apps/hyperlink/).
|
||||
|
||||
![Hyperlink demo][hyperlink.gif]
|
||||
|
||||
## Flex
|
||||
|
||||
Shows how to use the flex layouts. [Source](./apps/flex/).
|
||||
|
||||
![Flex demo][flex.gif]
|
||||
|
||||
## Hello World
|
||||
|
||||
Shows how to create a simple TUI with a text. [Source](./apps/hello-world/).
|
||||
|
||||
![Hello World demo][hello-world.gif]
|
||||
|
||||
## Gauge
|
||||
|
||||
Shows different types of gauges. [Source](./apps/gauge/).
|
||||
|
||||
## Inline
|
||||
|
||||
Shows how to use the inlined viewport to render in a specific area of the screen. [Source](./apps/inline/).
|
||||
|
||||
![Inline demo][inline.gif]
|
||||
|
||||
## Input Form
|
||||
|
||||
Shows how to render a form with input fields. [Source](./apps/input-form/).
|
||||
|
||||
## Modifiers
|
||||
|
||||
Shows different types of modifiers. [Source](./apps/modifiers/).
|
||||
|
||||
![Modifiers demo][modifiers.gif]
|
||||
|
||||
## Mouse Drawing
|
||||
|
||||
Shows how to handle mouse events. [Source](./apps/mouse-drawing/).
|
||||
|
||||
## Minimal
|
||||
|
||||
Shows how to create a minimal application. [Source](./apps/minimal/).
|
||||
|
||||
![Minimal demo][minimal.gif]
|
||||
|
||||
## Panic
|
||||
|
||||
Shows how to handle panics. [Source](./apps/panic/).
|
||||
|
||||
![Panic demo][panic.gif]
|
||||
|
||||
## Popup
|
||||
|
||||
Shows how to handle popups. [Source](./apps/popup/).
|
||||
|
||||
![Popup demo][popup.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
Shows how to render different types of scrollbars. [Source](./apps/scrollbar/).
|
||||
|
||||
![Scrollbar demo][scrollbar.gif]
|
||||
|
||||
## Table
|
||||
|
||||
Shows how to create an interactive table. [Source](./apps/table/).
|
||||
|
||||
![Table demo][table.gif]
|
||||
|
||||
## Todo List
|
||||
|
||||
Shows how to create a simple todo list application. [Source](./apps/todo-list/).
|
||||
|
||||
![Todo List demo][todo-list.gif]
|
||||
|
||||
## Tracing
|
||||
|
||||
Shows how to use the [tracing](https://crates.io/crates/tracing) crate to log to a file. [Source](./apps/tracing/).
|
||||
|
||||
![Tracing demo][tracing.gif]
|
||||
|
||||
## User Input
|
||||
|
||||
Shows how to handle user input. [Source](./apps/user-input/). [Source](./apps/user-input/).
|
||||
|
||||
![User input demo][user-input.gif]
|
||||
|
||||
## Weather
|
||||
|
||||
Shows how to render weather data using barchart widget. [Source](./apps/weather/).
|
||||
|
||||
## WidgetRef Container
|
||||
|
||||
Shows how to use [`WidgetRef`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.WidgetRef.html) to store widgets in a container. [Source](./apps/widget-ref-container/).
|
||||
|
||||
## Advanced Widget Implementation
|
||||
|
||||
Shows how to render the `Widget` trait in different ways.
|
||||
|
||||
![Advanced widget impl demo][advanced-widget-impl.gif]
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>How to update these examples?</summary>
|
||||
|
||||
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||
images themselves are stored in a separate `images` git branch to avoid bloating the `main`
|
||||
branch.
|
||||
|
||||
<!--
|
||||
|
||||
Links to images to make them easier to update in bulk. Use the following script to update and upload
|
||||
the examples to the images branch. (Requires push access to the branch).
|
||||
|
||||
```shell
|
||||
vhs/generate.bash
|
||||
```
|
||||
-->
|
||||
|
||||
</details>
|
||||
|
||||
[advanced-widget-impl.gif]: https://github.com/ratatui/ratatui/blob/images/examples/advanced-widget-impl.gif?raw=true
|
||||
[async-github.gif]: https://github.com/ratatui/ratatui/blob/images/examples/async-github.gif?raw=true
|
||||
[calendar-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/calendar-explorer.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[color-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/color-explorer.gif?raw=true
|
||||
[colors-rgb.gif]: https://github.com/ratatui/ratatui/blob/images/examples/colors-rgb.gif?raw=true
|
||||
[constraint-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
|
||||
[constraints.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraints.gif?raw=true
|
||||
[custom-widget.gif]: https://github.com/ratatui/ratatui/blob/images/examples/custom-widget.gif?raw=true
|
||||
[demo2-destroy.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2-destroy.gif?raw=true
|
||||
[demo2-social.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2-social.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[flex.gif]: https://github.com/ratatui/ratatui/blob/images/examples/flex.gif?raw=true
|
||||
[hello-world.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hello-world.gif?raw=true
|
||||
[hyperlink.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hyperlink.gif?raw=true
|
||||
[inline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[minimal.gif]: https://github.com/ratatui/ratatui/blob/images/examples/minimal.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[panic.gif]: https://github.com/ratatui/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[popup.gif]: https://github.com/ratatui/ratatui/blob/images/examples/popup.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||
[table.gif]: https://github.com/ratatui/ratatui/blob/images/examples/table.gif?raw=true
|
||||
[todo-list.gif]: https://github.com/ratatui/ratatui/blob/images/examples/todo-list.gif?raw=true
|
||||
[tracing.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tracing.gif?raw=true
|
||||
[user-input.gif]: https://github.com/ratatui/ratatui/blob/images/examples/user-input.gif?raw=true
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "advanced-widget-impl"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui = { workspace = true, features = ["unstable-widget-ref"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Advanced Widget Implementation demo
|
||||
|
||||
This example shows how to render the `Widget` trait in different ways.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p advanced-widget-impl
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "async-github"
|
||||
publish = false
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
octocrab = "0.44.0"
|
||||
ratatui.workspace = true
|
||||
tokio = { version = "1.44.2", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.17"
|
||||
@@ -1,9 +0,0 @@
|
||||
# Async GitHub demo
|
||||
|
||||
This example demonstrates how to use Ratatui with widgets that fetch data from GitHub API asynchronously.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p async-github
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "calendar-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
time = { version = "0.3.39", features = ["formatting", "parsing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Calendar explorer demo
|
||||
|
||||
This example shows how to render a calendar with different styles.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p calendar-explorer
|
||||
```
|
||||
@@ -1,243 +0,0 @@
|
||||
//! A Ratatui example that demonstrates how to render calendar with different styles.
|
||||
//!
|
||||
//! Marks the holidays and seasons on the calendar.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::layout::{Constraint, Layout, Margin, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::{Line, Text};
|
||||
use ratatui::widgets::calendar::{CalendarEventStore, Monthly};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
use time::ext::NumericalDuration;
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
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') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('s') => calendar_style = calendar_style.next(),
|
||||
KeyCode::Char('n') | KeyCode::Tab => selected_date = next_month(selected_date),
|
||||
KeyCode::Char('p') | KeyCode::BackTab => selected_date = prev_month(selected_date),
|
||||
KeyCode::Char('h') | KeyCode::Left => selected_date -= 1.days(),
|
||||
KeyCode::Char('j') | KeyCode::Down => selected_date += 1.weeks(),
|
||||
KeyCode::Char('k') | KeyCode::Up => selected_date -= 1.weeks(),
|
||||
KeyCode::Char('l') | KeyCode::Right => selected_date += 1.days(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_month(date: Date) -> Date {
|
||||
if date.month() == Month::December {
|
||||
date.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_year(date.year() + 1)
|
||||
.unwrap()
|
||||
} else {
|
||||
date.replace_month(date.month().next()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_month(date: Date) -> Date {
|
||||
if date.month() == Month::January {
|
||||
date.replace_month(Month::December)
|
||||
.unwrap()
|
||||
.replace_year(date.year() - 1)
|
||||
.unwrap()
|
||||
} else {
|
||||
date.replace_month(date.month().previous()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the UI with a calendar.
|
||||
fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date) {
|
||||
let header = Text::from_iter([
|
||||
Line::from("Calendar Example".bold()),
|
||||
Line::from(
|
||||
"<q> Quit | <s> Change Style | <n> Next Month | <p> Previous Month, <hjkl> Move",
|
||||
),
|
||||
Line::from(format!(
|
||||
"Current date: {selected_date} | Current style: {calendar_style}"
|
||||
)),
|
||||
]);
|
||||
|
||||
let 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)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StyledCalendar {
|
||||
Default,
|
||||
Surrounding,
|
||||
WeekdaysHeader,
|
||||
SurroundingAndWeekdaysHeader,
|
||||
MonthHeader,
|
||||
MonthAndWeekdaysHeader,
|
||||
}
|
||||
|
||||
impl StyledCalendar {
|
||||
// Cycle through the different styles.
|
||||
const fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Default => Self::Surrounding,
|
||||
Self::Surrounding => Self::WeekdaysHeader,
|
||||
Self::WeekdaysHeader => Self::SurroundingAndWeekdaysHeader,
|
||||
Self::SurroundingAndWeekdaysHeader => Self::MonthHeader,
|
||||
Self::MonthHeader => Self::MonthAndWeekdaysHeader,
|
||||
Self::MonthAndWeekdaysHeader => Self::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StyledCalendar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Default => write!(f, "Default"),
|
||||
Self::Surrounding => write!(f, "Show Surrounding"),
|
||||
Self::WeekdaysHeader => write!(f, "Show Weekdays Header"),
|
||||
Self::SurroundingAndWeekdaysHeader => write!(f, "Show Surrounding and Weekdays Header"),
|
||||
Self::MonthHeader => write!(f, "Show Month Header"),
|
||||
Self::MonthAndWeekdaysHeader => write!(f, "Show Month Header and Weekdays Header"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledCalendar {
|
||||
fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> {
|
||||
let events = events(date)?;
|
||||
|
||||
let 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)
|
||||
.unwrap()
|
||||
.replace_month(Month::try_from(i as u8 + 1).unwrap())
|
||||
.unwrap();
|
||||
self.render_month(frame, area, month, &events);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_month(self, frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) {
|
||||
let calendar = match self {
|
||||
Self::Default => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default()),
|
||||
Self::Surrounding => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::new().dim()),
|
||||
Self::WeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::new().bold().green()),
|
||||
Self::SurroundingAndWeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::new().dim())
|
||||
.show_weekdays_header(Style::new().bold().green()),
|
||||
Self::MonthHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_month_header(Style::new().bold().green()),
|
||||
Self::MonthAndWeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::new().bold().dim().light_yellow()),
|
||||
};
|
||||
frame.render_widget(calendar, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a list of dates for the current year.
|
||||
fn events(selected_date: Date) -> Result<CalendarEventStore> {
|
||||
const SELECTED: Style = Style::new()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
const HOLIDAY: Style = Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
const SEASON: Style = Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
let y = selected_date.year();
|
||||
|
||||
// new year's
|
||||
list.add(Date::from_calendar_date(y, Month::January, 1)?, HOLIDAY);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(Date::from_calendar_date(y + 1, Month::January, 1)?, HOLIDAY);
|
||||
// groundhog day
|
||||
list.add(Date::from_calendar_date(y, Month::February, 2)?, HOLIDAY);
|
||||
// april fool's
|
||||
list.add(Date::from_calendar_date(y, Month::April, 1)?, HOLIDAY);
|
||||
// earth day
|
||||
list.add(Date::from_calendar_date(y, Month::April, 22)?, HOLIDAY);
|
||||
// star wars day
|
||||
list.add(Date::from_calendar_date(y, Month::May, 4)?, HOLIDAY);
|
||||
// festivus
|
||||
list.add(Date::from_calendar_date(y, Month::December, 23)?, HOLIDAY);
|
||||
// new year's eve
|
||||
list.add(Date::from_calendar_date(y, Month::December, 31)?, HOLIDAY);
|
||||
|
||||
// seasons
|
||||
// spring equinox
|
||||
list.add(Date::from_calendar_date(y, Month::March, 22)?, SEASON);
|
||||
// summer solstice
|
||||
list.add(Date::from_calendar_date(y, Month::June, 21)?, SEASON);
|
||||
// fall equinox
|
||||
list.add(Date::from_calendar_date(y, Month::September, 22)?, SEASON);
|
||||
// winter solstice
|
||||
list.add(Date::from_calendar_date(y, Month::December, 21)?, SEASON);
|
||||
|
||||
// selected date
|
||||
list.add(selected_date, SELECTED);
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "canvas"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Canvas demo
|
||||
|
||||
This example shows how to render various shapes and a map on a canvas.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p canvas
|
||||
```
|
||||
@@ -1,260 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to draw on a canvas.
|
||||
///
|
||||
/// This example demonstrates how to draw various shapes such as rectangles, circles, and lines
|
||||
/// on a canvas. It also demonstrates how to draw a map.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::{
|
||||
io::stdout,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::ExecutableCommand;
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEventKind,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::layout::{Constraint, Layout, Position, Rect};
|
||||
use ratatui::style::{Color, Stylize};
|
||||
use ratatui::symbols::Marker;
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle};
|
||||
use ratatui::widgets::{Block, Widget};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
stdout().execute(EnableMouseCapture)?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
stdout().execute(DisableMouseCapture)?;
|
||||
app_result
|
||||
}
|
||||
|
||||
struct App {
|
||||
exit: bool,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Circle,
|
||||
playground: Rect,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
marker: Marker,
|
||||
points: Vec<Position>,
|
||||
is_drawing: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Circle {
|
||||
x: 20.0,
|
||||
y: 40.0,
|
||||
radius: 10.0,
|
||||
color: Color::Yellow,
|
||||
},
|
||||
playground: Rect::new(10, 10, 200, 100),
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
marker: Marker::Dot,
|
||||
points: vec![],
|
||||
is_drawing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
let mut last_tick = Instant::now();
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if !event::poll(timeout)? {
|
||||
self.on_tick();
|
||||
last_tick = Instant::now();
|
||||
continue;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) => self.handle_key_event(key),
|
||||
Event::Mouse(event) => self.handle_mouse_event(event),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) {
|
||||
if !key.is_press() {
|
||||
return;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.exit = true,
|
||||
KeyCode::Char('j') | KeyCode::Down => self.y += 1.0,
|
||||
KeyCode::Char('k') | KeyCode::Up => self.y -= 1.0,
|
||||
KeyCode::Char('l') | KeyCode::Right => self.x += 1.0,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.x -= 1.0,
|
||||
KeyCode::Enter => self.cycle_marker(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
|
||||
match event.kind {
|
||||
MouseEventKind::Down(_) => self.is_drawing = true,
|
||||
MouseEventKind::Up(_) => self.is_drawing = false,
|
||||
MouseEventKind::Drag(_) => {
|
||||
self.points.push(Position::new(event.column, event.row));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
const fn cycle_marker(&mut self) {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
};
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
let playground = self.playground;
|
||||
if ball.x - ball.radius < f64::from(playground.left())
|
||||
|| ball.x + ball.radius > f64::from(playground.right())
|
||||
{
|
||||
self.vx = -self.vx;
|
||||
}
|
||||
if ball.y - ball.radius < f64::from(playground.top())
|
||||
|| ball.y + ball.radius > f64::from(playground.bottom())
|
||||
{
|
||||
self.vy = -self.vy;
|
||||
}
|
||||
self.ball.x += self.vx;
|
||||
self.ball.y += self.vy;
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let header = Text::from_iter([
|
||||
"Canvas Example".bold(),
|
||||
"<q> Quit | <enter> Change Marker | <hjkl> Move".into(),
|
||||
]);
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]);
|
||||
let [text_area, up, down] = vertical.areas(frame.area());
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
|
||||
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);
|
||||
frame.render_widget(self.pong_canvas(), pong);
|
||||
frame.render_widget(self.boxes_canvas(boxes), boxes);
|
||||
}
|
||||
|
||||
fn map_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("World"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::Green,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(self.x, -self.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
}
|
||||
|
||||
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Draw here"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([0.0, f64::from(area.width)])
|
||||
.y_bounds([0.0, f64::from(area.height)])
|
||||
.paint(move |ctx| {
|
||||
let points = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
f64::from(p.x) - f64::from(area.left()),
|
||||
f64::from(area.bottom()) - f64::from(p.y),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
ctx.draw(&Points {
|
||||
coords: &points,
|
||||
color: Color::White,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Pong"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&self.ball);
|
||||
})
|
||||
.x_bounds([10.0, 210.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
}
|
||||
|
||||
fn boxes_canvas(&self, area: Rect) -> impl Widget {
|
||||
let left = 0.0;
|
||||
let right = f64::from(area.width);
|
||||
let bottom = 0.0;
|
||||
let top = f64::from(area.height).mul_add(2.0, -4.0);
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Rects"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([left, right])
|
||||
.y_bounds([bottom, top])
|
||||
.paint(|ctx| {
|
||||
for i in 0..=11 {
|
||||
ctx.draw(&Rectangle {
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 2.0,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Red,
|
||||
});
|
||||
ctx.draw(&Rectangle {
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 21.0,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Blue,
|
||||
});
|
||||
}
|
||||
for i in 0..100 {
|
||||
if i % 10 != 0 {
|
||||
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
}
|
||||
if i % 2 == 0 && i % 10 != 0 {
|
||||
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "chart"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Chart demo
|
||||
|
||||
This example shows how to render line, bar, and scatter charts.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p chart
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "color-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Color explorer demo
|
||||
|
||||
This example shows how to handle the supported colors.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p color-explorer
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "colors-rgb"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
palette = "0.7.6"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Colors-RGB demo
|
||||
|
||||
This example shows the full range of RGB colors in an animation.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p colors-rgb
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "constraint-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Constraint explorer demo
|
||||
|
||||
This interactive example shows how different constraints can be used to layout widgets.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p constraint-explorer
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "constraints"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Constraints demo
|
||||
|
||||
This example shows different types of constraints.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p constraints
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "custom-widget"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Custom widget demo
|
||||
|
||||
This example shows how to create a custom widget that can be interacted with the mouse.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p custom-widget
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "demo"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
crossterm = ["ratatui/crossterm", "dep:crossterm"]
|
||||
termion = ["ratatui/termion", "dep:termion"]
|
||||
termwiz = ["ratatui/termwiz", "dep:termwiz"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
rand = "0.9.1"
|
||||
ratatui.workspace = true
|
||||
termwiz = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
termion = { workspace = true, optional = true }
|
||||
@@ -1,25 +0,0 @@
|
||||
# Demo example
|
||||
|
||||
This is the original demo that was developed for Tui-rs (the library that Ratatui was forked from).
|
||||
|
||||

|
||||
|
||||
This example is available for each backend. To run it:
|
||||
|
||||
## crossterm
|
||||
|
||||
```shell
|
||||
cargo run -p demo
|
||||
```
|
||||
|
||||
## termion
|
||||
|
||||
```shell
|
||||
cargo run -p demo --no-default-features --features termion
|
||||
```
|
||||
|
||||
## termwiz
|
||||
|
||||
```shell
|
||||
cargo run -p demo --no-default-features --features termwiz
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "demo2"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.3"
|
||||
crossterm.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
palette = "0.7.6"
|
||||
rand = "0.9.1"
|
||||
rand_chacha = "0.9.0"
|
||||
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||
strum.workspace = true
|
||||
time = "0.3.39"
|
||||
unicode-width = "0.2.0"
|
||||
@@ -1,9 +0,0 @@
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
|
||||
```shell
|
||||
cargo run -p demo2
|
||||
```
|
||||
|
||||

|
||||
@@ -1,73 +0,0 @@
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin, Rect};
|
||||
use ratatui::widgets::{
|
||||
Block, Borders, Clear, MascotEyeColor, Padding, Paragraph, RatatuiMascot, Widget, Wrap,
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct AboutTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl AboutTab {
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AboutTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let 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
|
||||
} else {
|
||||
MascotEyeColor::Red
|
||||
};
|
||||
RatatuiMascot::default().set_eye(eye_state).render(
|
||||
logo_area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
}),
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf); // clear out the color swatches
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
let text = "- cooking up terminal user interfaces -
|
||||
|
||||
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
|
||||
screen efficiently every frame.";
|
||||
Paragraph::new(text)
|
||||
.style(THEME.description)
|
||||
.block(
|
||||
Block::new()
|
||||
.title(" Ratatui ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::TOP)
|
||||
.border_style(THEME.description_title)
|
||||
.padding(Padding::new(0, 0, 0, 0)),
|
||||
)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((0, 0))
|
||||
.render(area, buf);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "flex"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Flex demo
|
||||
|
||||
This interactive example shows how to use the flex layouts.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p flex
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "gauge"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Gauge demo
|
||||
|
||||
This example shows different types of gauges.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p gauge
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "hello-world"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Hello World demo
|
||||
|
||||
This example shows how to create a simple TUI with a text.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p hello-world
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "hyperlink"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Hyperlink demo
|
||||
|
||||
This example shows how to render hyperlinks in a terminal using [OSC 8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p hyperlink
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "inline"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand = "0.9.1"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Inline demo
|
||||
|
||||
This example shows how to use the inlined viewport to render in a specific area of the screen.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p inline
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "input-form"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,27 +0,0 @@
|
||||
# Input Form example
|
||||
|
||||
This example demonstrates how to handle input across several form fields (2 strings and an number).
|
||||
It uses an enum to track the focused field, and sends keyboard events to one which is current.
|
||||
|
||||
Run this example with:
|
||||
|
||||
```shell
|
||||
cargo run -p input-form
|
||||
```
|
||||
|
||||
This example does not handle things like cursor movement within the line (just keys and backspace).
|
||||
Most apps would benefit from using the following crates for text input rather than directly using
|
||||
strings:
|
||||
|
||||
- [`tui-input`](https://crates.io/crates/tui-input)
|
||||
- [`tui-prompts`](https://crates.io/crates/tui-prompts)
|
||||
- [`tui-textarea`](https://crates.io/crates/tui-textarea)
|
||||
- [`rat-salsa`](https://crates.io/crates/rat-salsa)
|
||||
|
||||
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` discssion](https://github.com/bevyengine/bevy/discussions/15374) about focus
|
||||
more generally.
|
||||
@@ -1,267 +0,0 @@
|
||||
//! A Ratatui example that demonstrates how to handle input form focus
|
||||
//!
|
||||
//! This example demonstrates how to handle cursor and input focus between multiple fields in a
|
||||
//! form. You can navigate between fields using the Tab key.
|
||||
//!
|
||||
//! This does not handle cursor movement etc. This is just a simple example. In a real application,
|
||||
//! consider using [`tui-input`], or [`tui-prompts`], or [`tui-textarea`].
|
||||
//!
|
||||
//! 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
|
||||
//! [`tui-input`]: https://crates.io/crates/tui-input
|
||||
//! [`tui-prompts`]: https://crates.io/crates/tui-prompts
|
||||
//! [`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 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 result {
|
||||
Ok(Some(form)) => println!("{}", serde_json::to_string_pretty(&form)?),
|
||||
Ok(None) => println!("Canceled"),
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
form: InputForm,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Cancelled,
|
||||
Submitted,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<Option<InputForm>> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
match self.state {
|
||||
AppState::Cancelled => Ok(None),
|
||||
AppState::Submitted => Ok(Some(self.form)),
|
||||
AppState::Running => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
self.form.render(frame);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
match key.code {
|
||||
KeyCode::Esc => self.state = AppState::Cancelled,
|
||||
KeyCode::Enter => self.state = AppState::Submitted,
|
||||
_ => self.form.on_key_press(key),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct InputForm {
|
||||
#[serde(skip)]
|
||||
focus: Focus,
|
||||
first_name: StringField,
|
||||
last_name: StringField,
|
||||
age: AgeField,
|
||||
}
|
||||
|
||||
impl Default for InputForm {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
focus: Focus::FirstName,
|
||||
first_name: StringField::new("First Name"),
|
||||
last_name: StringField::new("Last Name"),
|
||||
age: AgeField::new("Age"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputForm {
|
||||
// Handle focus navigation or pass the event to the focused field.
|
||||
fn on_key_press(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Tab => self.focus = self.focus.next(),
|
||||
_ => match self.focus {
|
||||
Focus::FirstName => self.first_name.on_key_press(event),
|
||||
Focus::LastName => self.last_name.on_key_press(event),
|
||||
Focus::Age => self.age.on_key_press(event),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the form with the current focus.
|
||||
///
|
||||
/// The cursor is placed at the end of the focused field.
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
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);
|
||||
frame.render_widget(&self.age, age_area);
|
||||
|
||||
let cursor_position = match self.focus {
|
||||
Focus::FirstName => first_name_area.offset(self.first_name.cursor_offset()),
|
||||
Focus::LastName => last_name_area.offset(self.last_name.cursor_offset()),
|
||||
Focus::Age => age_area.offset(self.age.cursor_offset()),
|
||||
};
|
||||
frame.set_cursor_position(cursor_position);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
enum Focus {
|
||||
#[default]
|
||||
FirstName,
|
||||
LastName,
|
||||
Age,
|
||||
}
|
||||
|
||||
impl Focus {
|
||||
// Round-robin focus order.
|
||||
const fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::FirstName => Self::LastName,
|
||||
Self::LastName => Self::Age,
|
||||
Self::Age => Self::FirstName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A new-type representing a string field with a label.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct StringField {
|
||||
#[serde(skip)]
|
||||
label: &'static str,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl StringField {
|
||||
const fn new(label: &'static str) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle input events for the string input.
|
||||
fn on_key_press(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Char(c) => self.value.push(c),
|
||||
KeyCode::Backspace => {
|
||||
self.value.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> Offset {
|
||||
let x = (self.label.len() + self.value.len() + 2) as i32;
|
||||
Offset::new(x, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &StringField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = [
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// A new-type representing a person's age in years (0-130).
|
||||
#[derive(Default, Clone, Copy, Serialize)]
|
||||
struct AgeField {
|
||||
#[serde(skip)]
|
||||
label: &'static str,
|
||||
value: u8,
|
||||
}
|
||||
|
||||
impl AgeField {
|
||||
const MAX: u8 = 130;
|
||||
|
||||
const fn new(label: &'static str) -> Self {
|
||||
Self { label, value: 0 }
|
||||
}
|
||||
|
||||
/// Handle input events for the age input.
|
||||
///
|
||||
/// Digits are accepted as input, with any input which would exceed the maximum age being
|
||||
/// ignored. The up/down arrow keys and 'j'/'k' keys can be used to increment/decrement the
|
||||
/// age.
|
||||
fn on_key_press(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Char(digit @ '0'..='9') => {
|
||||
let value = self
|
||||
.value
|
||||
.saturating_mul(10)
|
||||
.saturating_add(digit as u8 - b'0');
|
||||
if value <= Self::MAX {
|
||||
self.value = value;
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => self.value /= 10,
|
||||
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) {
|
||||
self.value = self.value.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> Offset {
|
||||
let x = (self.label.len() + self.value.to_string().len() + 2) as i32;
|
||||
Offset::new(x, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &AgeField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = [
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
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);
|
||||
value.render(value_area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "minimal"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Minimal demo
|
||||
|
||||
This example shows how to create a minimal application.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p minimal
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
/// 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;
|
||||
use ratatui::text::Text;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = ratatui::init();
|
||||
loop {
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
|
||||
.expect("failed to draw frame");
|
||||
if event::read().expect("failed to read event").is_key_press() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ratatui::restore();
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "modifiers"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Modifiers demo
|
||||
|
||||
This example shows different types of modifiers.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p modifiers
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "mouse-drawing"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
## a collection of line drawing algorithms (e.g. Bresenham's line algorithm)
|
||||
line_drawing = "1.0.1"
|
||||
rand = "0.9.1"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Mouse drawing demo
|
||||
|
||||
This example shows how to receive mouse and handle mouse events.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p mouse-drawing
|
||||
```
|
||||
@@ -1,121 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to handle mouse events.
|
||||
///
|
||||
/// This example demonstrates how to handle mouse events in Ratatui. You can draw lines by
|
||||
/// clicking and dragging the mouse.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEvent,
|
||||
MouseEventKind,
|
||||
};
|
||||
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()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = MouseDrawingApp::default().run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseDrawingApp {
|
||||
// Whether the app should exit
|
||||
pub should_exit: bool,
|
||||
// The last known mouse position
|
||||
pub mouse_position: Option<Position>,
|
||||
// The points that have been clicked / drawn by dragging the mouse
|
||||
pub points: Vec<(Position, Color)>,
|
||||
// The color to draw with
|
||||
pub current_color: Color,
|
||||
}
|
||||
|
||||
impl MouseDrawingApp {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
execute!(std::io::stdout(), EnableMouseCapture)?;
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
execute!(std::io::stdout(), DisableMouseCapture)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
match event::read()? {
|
||||
Event::Key(event) => self.on_key_event(event),
|
||||
Event::Mouse(event) => self.on_mouse_event(event),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
KeyCode::Char(' ') => {
|
||||
self.current_color = Color::Rgb(rand::random(), rand::random(), rand::random());
|
||||
}
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds any points which were clicked or dragged to the `points` vector.
|
||||
fn on_mouse_event(&mut self, event: MouseEvent) {
|
||||
let position = Position::new(event.column, event.row);
|
||||
match event.kind {
|
||||
MouseEventKind::Down(_) => self.points.push((position, self.current_color)),
|
||||
MouseEventKind::Drag(_) => self.draw_line(position),
|
||||
_ => {}
|
||||
}
|
||||
self.mouse_position = Some(position);
|
||||
}
|
||||
|
||||
/// Draw a line between the last point and the given position
|
||||
fn draw_line(&mut self, position: Position) {
|
||||
if let Some(start) = self.points.last() {
|
||||
let (x0, y0) = (i32::from(start.0.x), i32::from(start.0.y));
|
||||
let (x1, y1) = (i32::from(position.x), i32::from(position.y));
|
||||
for (x, y) in line_drawing::Bresenham::new((x0, y0), (x1, y1)) {
|
||||
let point = (Position::new(x as u16, y as u16), self.current_color);
|
||||
self.points.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
// call order is important here as later elements are drawn on top of earlier elements
|
||||
self.render_points(frame);
|
||||
self.render_mouse_cursor(frame);
|
||||
let value = "Mouse Example ('Esc' to quit. Click / drag to draw. 'Space' to change color)";
|
||||
let title = Line::from(value).centered();
|
||||
frame.render_widget(title, frame.area());
|
||||
}
|
||||
|
||||
fn render_points(&self, frame: &mut Frame<'_>) {
|
||||
for (position, color) in &self.points {
|
||||
let area = Rect::from((*position, Size::new(1, 1))).clamp(frame.area());
|
||||
frame.render_widget(symbols::block::FULL.fg(*color), area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_mouse_cursor(&self, frame: &mut Frame<'_>) {
|
||||
if let Some(position) = self.mouse_position {
|
||||
let area = Rect::from((position, Size::new(1, 1))).clamp(frame.area());
|
||||
frame.render_widget("╳".bg(self.current_color), area);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "panic"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Panic demo
|
||||
|
||||
This example shows how to handle panics in your application.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p panic
|
||||
```
|
||||
@@ -1,99 +0,0 @@
|
||||
/// A Ratatui example that demonstrates how to handle panics in your application.
|
||||
///
|
||||
/// Prior to Ratatui 0.28.1, a panic hook had to be manually set up to ensure that the terminal
|
||||
/// was reset when a panic occurred. This was necessary because a panic would interrupt the
|
||||
/// normal control flow and leave the terminal in a distorted state.
|
||||
///
|
||||
/// Starting with Ratatui 0.28.1, the panic hook is automatically set up by the new
|
||||
/// `ratatui::init` function, so you no longer need to manually set up the panic hook. This
|
||||
/// example now demonstrates how the panic hook acts when it is enabled by default.
|
||||
///
|
||||
/// When exiting normally or when handling `Result::Err`, we can reset the terminal manually at
|
||||
/// the end of `main` just before we print the error.
|
||||
///
|
||||
/// Because a panic interrupts the normal control flow, manually resetting the terminal at the
|
||||
/// end of `main` won't do us any good. Instead, we need to make sure to set up a panic hook
|
||||
/// that first resets the terminal before handling the panic. This both reuses the standard
|
||||
/// panic hook to ensure a consistent panic handling UX and properly resets the terminal to not
|
||||
/// distort the output.
|
||||
///
|
||||
/// That's why this example is set up to show both situations, with and without the panic hook,
|
||||
/// to see the difference.
|
||||
///
|
||||
/// For more information on how to set this up manually, see the [Color Eyre recipe] in the
|
||||
/// Ratatui website.
|
||||
///
|
||||
/// 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
|
||||
/// [Color Eyre recipe]: https://ratatui.rs/recipes/apps/color-eyre
|
||||
use color_eyre::{Result, eyre::bail};
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Paragraph};
|
||||
use ratatui::{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,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self { hook_enabled: true }
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
|
||||
if let Some(key) = event::read()?.as_key_press_event() {
|
||||
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();
|
||||
self.hook_enabled = false;
|
||||
}
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let text = vec![
|
||||
if self.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("Press `p` to cause a panic"),
|
||||
Line::from("Press `e` to cause an error"),
|
||||
Line::from("Press `h` to disable the panic hook"),
|
||||
Line::from("Press `q` to quit"),
|
||||
Line::from(""),
|
||||
Line::from("When your app panics without a panic hook, you will likely have to"),
|
||||
Line::from("reset your terminal afterwards with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("Try first with the panic handler enabled, and then with it disabled"),
|
||||
Line::from("to see the difference"),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
frame.render_widget(paragraph, frame.area());
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "popup"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Popup demo
|
||||
|
||||
This example shows how to handle popups.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p popup
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "scrollbar"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Scrollbar demo
|
||||
|
||||
This example shows how to render different types of scrollbars.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p scrollbar
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "table"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
fakeit = "1.1"
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Table demo
|
||||
|
||||
This example shows how to create an interactive table.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p table
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "todo-list"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Todo list demo
|
||||
|
||||
This example demonstrates a simple todo list application.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p todo-list
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "tracing"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Tracing demo
|
||||
|
||||
This example demonstrates how to use the [tracing](https://crates.io/crates/tracing) crate to log to a file.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p tracing
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "user-input"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# User input demo
|
||||
|
||||
This example shows how to handle user input.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p user-input
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "weather"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand = "0.9.1"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# Weather demo
|
||||
|
||||
This example shows how to render weather data using barchart widget.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p weather
|
||||
```
|
||||
@@ -1,97 +0,0 @@
|
||||
//! A Ratatui example that demonstrates how to render weather data using [`BarChart`] widget.
|
||||
//!
|
||||
//! Generates random temperature data for each hour of the day and renders it as a vertical bar.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use rand::{Rng, rng};
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Bar, BarChart, BarGroup};
|
||||
use ratatui::{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 {
|
||||
should_exit: bool,
|
||||
temperatures: Vec<u8>,
|
||||
}
|
||||
|
||||
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.render(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if event::read()?
|
||||
.as_key_press_event()
|
||||
.is_some_and(|key| key.code == KeyCode::Char('q'))
|
||||
{
|
||||
self.should_exit = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&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 {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(hour, value)| vertical_bar(hour, value))
|
||||
.collect();
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.bar_width(5)
|
||||
}
|
||||
|
||||
fn vertical_bar(hour: usize, temperature: &u8) -> Bar {
|
||||
Bar::default()
|
||||
.value(u64::from(*temperature))
|
||||
.label(Line::from(format!("{hour:>02}:00")))
|
||||
.text_value(format!("{temperature:>3}°"))
|
||||
.style(temperature_style(*temperature))
|
||||
.value_style(temperature_style(*temperature).reversed())
|
||||
}
|
||||
|
||||
/// create a yellow to red value based on the value (50-90)
|
||||
fn temperature_style(value: u8) -> Style {
|
||||
let green = (255.0 * (1.0 - f64::from(value - 50) / 40.0)) as u8;
|
||||
let color = Color::Rgb(255, green, 0);
|
||||
Style::new().fg(color)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "widget-ref-container"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui = { workspace = true, features = ["unstable-widget-ref"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,9 +0,0 @@
|
||||
# WidgetRef Container demo
|
||||
|
||||
This example shows how to use [`WidgetRef`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.WidgetRef.html) to store widgets in a container.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p widget-ref-container
|
||||
```
|
||||
@@ -1,86 +0,0 @@
|
||||
/// 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::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::widgets::{Block, Paragraph, Widget, WidgetRef};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if event::read()?.is_key_press() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(frame: &mut Frame) {
|
||||
let container = StackContainer {
|
||||
direction: Direction::Vertical,
|
||||
widgets: vec![
|
||||
(Box::new(&Greeting), Constraint::Percentage(50)),
|
||||
(Box::new(&Farewell), Constraint::Percentage(50)),
|
||||
],
|
||||
};
|
||||
frame.render_widget(&container, frame.area());
|
||||
}
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl Widget for &Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Hello")
|
||||
.block(Block::bordered())
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct Farewell;
|
||||
|
||||
impl Widget for &Farewell {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Goodbye")
|
||||
.block(Block::bordered())
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct StackContainer {
|
||||
direction: Direction,
|
||||
widgets: Vec<(Box<dyn WidgetRef>, Constraint)>,
|
||||
}
|
||||
|
||||
impl Widget for &StackContainer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::default()
|
||||
.direction(self.direction)
|
||||
.constraints(self.widgets.iter().map(|(_, constraint)| *constraint))
|
||||
.split(area);
|
||||
let widgets = self.widgets.iter().map(|(widget, _)| widget);
|
||||
for (widget, area) in zip(widgets, layout.iter()) {
|
||||
widget.render_ref(*area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +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/advanced-widget-impl.tape`
|
||||
Output "target/advanced-widget-impl.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 760
|
||||
Set Height 260
|
||||
Hide
|
||||
Type "cargo run -p advanced-widget-impl"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,16 +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/async-github.tape`
|
||||
Output "target/async-github.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run -p async-github"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 1s
|
||||
Set TypingSpeed 0.5s
|
||||
Down 3
|
||||
Up
|
||||
Sleep 1s
|
||||
@@ -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/calendar-explorer.tape`
|
||||
Output "target/calendar-explorer.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run -p calendar-explorer"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Show
|
||||
Set TypingSpeed 1s
|
||||
Type "s"
|
||||
Type "s"
|
||||
Type "s"
|
||||
Type 'n'
|
||||
Type 'p'
|
||||
Down 5
|
||||
Right 5
|
||||
@@ -1,82 +1,50 @@
|
||||
[package]
|
||||
name = "ratatui-core"
|
||||
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-alpha.5"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[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 = ["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.
|
||||
underline-color = []
|
||||
|
||||
## Use terminal scrolling regions to make some operations less prone to
|
||||
## flickering. (i.e. Terminal::insert_before).
|
||||
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 = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
unstable-widget-ref = []
|
||||
|
||||
[dependencies]
|
||||
anstyle = { workspace = true, optional = true }
|
||||
bitflags.workspace = true
|
||||
cassowary.workspace = true
|
||||
compact_str.workspace = true
|
||||
document-features = { workspace = true, optional = true }
|
||||
hashbrown.workspace = true
|
||||
indoc.workspace = true
|
||||
document-features.workspace = true
|
||||
instability.workspace = true
|
||||
itertools.workspace = true
|
||||
kasuari = { workspace = true, default-features = false }
|
||||
lru.workspace = true
|
||||
paste.workspace = true
|
||||
palette = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
thiserror = { workspace = true, default-features = false }
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
termwiz = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate = { workspace = true, default-features = false }
|
||||
unicode-truncate.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
rstest.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
argh = "0.1.12"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.40.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
tokio = { version = "1.39.2", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# Ratatui Core
|
||||
|
||||
[](https://crates.io/crates/ratatui-core)
|
||||
[](https://docs.rs/ratatui-core)
|
||||
[](../LICENSE)
|
||||
|
||||
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
**ratatui-core** is the core library of the [ratatui] project,
|
||||
providing the essential building blocks for creating rich terminal user interfaces in Rust.
|
||||
|
||||
[ratatui]: https://github.com/ratatui/ratatui
|
||||
|
||||
### Why `ratatui-core`?
|
||||
|
||||
The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
|
||||
to offer better stability for widget library authors. Widget libraries should generally depend
|
||||
on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
|
||||
|
||||
Applications, on the other hand, should depend on the main `ratatui` crate, which includes
|
||||
built-in widgets and additional features.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `ratatui-core` to your `Cargo.toml`:
|
||||
|
||||
```shell
|
||||
cargo add ratatui-core
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
|
||||
guide for more details on how to get involved.
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
@@ -24,10 +24,10 @@
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
@@ -56,7 +56,7 @@
|
||||
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
|
||||
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
|
||||
//! implements raw mode.
|
||||
//!
|
||||
|
||||
//! # Alternate Screen
|
||||
//!
|
||||
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
|
||||
@@ -90,21 +90,24 @@
|
||||
//! backend being used, and developers should consult the specific backend's documentation to
|
||||
//! understand how it implements mouse capture.
|
||||
//!
|
||||
//! [`CrosstermBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.CrosstermBackend.html
|
||||
//! [`TermionBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermionBackend.html
|
||||
//! [`TermwizBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermwizBackend.html
|
||||
//! [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [`Terminal`]: crate::terminal::Terminal
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
//! [Backend Comparison]: https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui.rs
|
||||
use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::{Position, Size};
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
};
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
@@ -144,24 +147,21 @@ pub struct WindowSize {
|
||||
/// Most applications should not need to interact with the `Backend` trait directly as the
|
||||
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
|
||||
///
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
pub trait Backend {
|
||||
/// Error type associated with this Backend.
|
||||
type Error: core::error::Error;
|
||||
|
||||
/// Draw the given content to the terminal screen.
|
||||
///
|
||||
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples, where the first two
|
||||
/// elements represent the x and y coordinates, and the third element is a reference to the
|
||||
/// [`Cell`] to be drawn.
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
|
||||
/// Insert `n` line breaks to the terminal screen.
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends.
|
||||
fn append_lines(&mut self, _n: u16) -> Result<(), Self::Error> {
|
||||
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -171,11 +171,9 @@ pub trait Backend {
|
||||
/// See also [`show_cursor`].
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.hide_cursor()?;
|
||||
/// // do something with hidden cursor
|
||||
/// backend.show_cursor()?;
|
||||
@@ -183,14 +181,14 @@ pub trait Backend {
|
||||
/// ```
|
||||
///
|
||||
/// [`show_cursor`]: Self::show_cursor
|
||||
fn hide_cursor(&mut self) -> Result<(), Self::Error>;
|
||||
fn hide_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
///
|
||||
/// See [`hide_cursor`] for an example.
|
||||
///
|
||||
/// [`hide_cursor`]: Self::hide_cursor
|
||||
fn show_cursor(&mut self) -> Result<(), Self::Error>;
|
||||
fn show_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
@@ -200,7 +198,7 @@ pub trait Backend {
|
||||
/// See [`set_cursor_position`] for an example.
|
||||
///
|
||||
/// [`set_cursor_position`]: Self::set_cursor_position
|
||||
fn get_cursor_position(&mut self) -> Result<Position, Self::Error>;
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
@@ -208,23 +206,22 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::{backend::Backend, layout::Position};
|
||||
///
|
||||
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
|
||||
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), Self::Error>;
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// The returned tuple contains the x and y coordinates of the cursor. The origin
|
||||
/// (0, 0) is at the top left corner of the screen.
|
||||
#[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), Self::Error> {
|
||||
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
@@ -232,8 +229,8 @@ pub trait Backend {
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), Self::Error> {
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
@@ -241,15 +238,13 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.clear()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn clear(&mut self) -> Result<(), Self::Error>;
|
||||
fn clear(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
///
|
||||
@@ -259,11 +254,9 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::{backend::{TestBackend}};
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::{Backend, ClearType};
|
||||
///
|
||||
/// backend.clear_region(ClearType::All)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -274,7 +267,18 @@ pub trait Backend {
|
||||
/// return an error if the `clear_type` is not supported by the backend.
|
||||
///
|
||||
/// [`clear`]: Self::clear
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error>;
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
ClearType::AfterCursor
|
||||
| ClearType::BeforeCursor
|
||||
| ClearType::CurrentLine
|
||||
| ClearType::UntilNewLine => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("clear_type [{clear_type:?}] not supported with this backend"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Size`].
|
||||
///
|
||||
@@ -282,92 +286,27 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::{backend::{TestBackend}};
|
||||
/// # let backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::{backend::Backend, layout::Size};
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// assert_eq!(backend.size()?, Size::new(80, 25));
|
||||
/// # Result::Ok(())
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn size(&self) -> Result<Size, Self::Error>;
|
||||
fn size(&self) -> io::Result<Size>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
|
||||
///
|
||||
/// The reason for this not returning only the pixel size, given the redundancy with the
|
||||
/// `size()` method, is that the underlying backends most likely get both values with one
|
||||
/// syscall, and the user is also most likely to need columns and rows along with pixel size.
|
||||
fn window_size(&mut self) -> Result<WindowSize, Self::Error>;
|
||||
fn window_size(&mut self) -> io::Result<WindowSize>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
|
||||
/// of rows.
|
||||
///
|
||||
/// Each row in the region is replaced by the row `line_count` rows below it, except the bottom
|
||||
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
|
||||
/// than the number of rows in the region, then all rows are replaced with empty rows.
|
||||
///
|
||||
/// If the region includes row 0, then `line_count` rows are copied into the bottom of the
|
||||
/// scrollback buffer. These rows are first taken from the old contents of the region, starting
|
||||
/// from the top. If there aren't sufficient rows in the region, then the remainder are empty
|
||||
/// rows.
|
||||
///
|
||||
/// The position of the cursor afterwards is undefined.
|
||||
///
|
||||
/// The behavior is designed to match what ANSI terminals do when scrolling regions are
|
||||
/// established. With ANSI terminals, a scrolling region can be established with the "^[[X;Yr"
|
||||
/// sequence, where X and Y define the lines of the region. The scrolling region can be reset
|
||||
/// to be the whole screen with the "^[[r" sequence.
|
||||
///
|
||||
/// When a scrolling region is established in an ANSI terminal, various operations' behaviors
|
||||
/// are changed in such a way that the scrolling region acts like a "virtual screen". In
|
||||
/// particular, the scrolling sequence "^[[NS", which scrolls lines up by a count of N.
|
||||
///
|
||||
/// On an ANSI terminal, this method will probably translate to something like:
|
||||
/// "^[[X;Yr^[[NS^[[r". That is, set the scrolling region, scroll up, then reset the scrolling
|
||||
/// region.
|
||||
///
|
||||
/// For examples of how this function is expected to work, refer to the tests for
|
||||
/// [`TestBackend::scroll_region_up`].
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(
|
||||
&mut self,
|
||||
region: core::ops::Range<u16>,
|
||||
line_count: u16,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
|
||||
/// of rows.
|
||||
///
|
||||
/// Each row in the region is replaced by the row `line_count` rows above it, except the top
|
||||
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
|
||||
/// than the number of rows in the region, then all rows are replaced with empty rows.
|
||||
///
|
||||
/// The position of the cursor afterwards is undefined.
|
||||
///
|
||||
/// See the documentation for [`Self::scroll_region_down`] for more information about how this
|
||||
/// is expected to be implemented for ANSI terminals. All of that applies, except the ANSI
|
||||
/// sequence to scroll down is "^[[NT".
|
||||
///
|
||||
/// This function is asymmetrical with regards to the scrollback buffer. The reason is that
|
||||
/// this how terminals seem to implement things.
|
||||
///
|
||||
/// For examples of how this function is expected to work, refer to the tests for
|
||||
/// [`TestBackend::scroll_region_down`].
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(
|
||||
&mut self,
|
||||
region: core::ops::Range<u16>,
|
||||
line_count: u16,
|
||||
) -> Result<(), Self::Error>;
|
||||
fn flush(&mut self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::string::ToString;
|
||||
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
//! This module provides the `TestBackend` implementation for the [`Backend`] trait.
|
||||
//! It is used in the integration tests to verify the correctness of the library.
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use core::fmt::{self, Write};
|
||||
use core::iter;
|
||||
use std::{
|
||||
fmt::{self, Write},
|
||||
io, iter,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::backend::{Backend, ClearType, WindowSize};
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::{Position, Rect, Size};
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Position, Rect, Size},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
|
||||
///
|
||||
@@ -21,13 +23,13 @@ use crate::layout::{Position, Rect, Size};
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::backend::{Backend, TestBackend};
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::TestBackend, prelude::*};
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer_lines([" "; 2]);
|
||||
/// # Result::Ok(())
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
@@ -56,7 +58,7 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
} else {
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = core::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
@@ -78,28 +80,6 @@ impl TestBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `TestBackend` with the specified lines as the initial screen state.
|
||||
///
|
||||
/// The backend's screen size is determined from the initial lines.
|
||||
#[must_use]
|
||||
pub fn with_lines<'line, Lines>(lines: Lines) -> Self
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
let buffer = Buffer::with_lines(lines);
|
||||
let scrollback = Buffer::empty(Rect {
|
||||
width: buffer.area.width,
|
||||
..Rect::ZERO
|
||||
});
|
||||
Self {
|
||||
buffer,
|
||||
scrollback,
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the `TestBackend`.
|
||||
pub const fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
@@ -138,7 +118,7 @@ impl TestBackend {
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[expect(deprecated)]
|
||||
#[allow(deprecated)]
|
||||
#[track_caller]
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
// TODO: use assert_eq!()
|
||||
@@ -232,12 +212,8 @@ impl fmt::Display for TestBackend {
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T, E = core::convert::Infallible> = core::result::Result<T, E>;
|
||||
|
||||
impl Backend for TestBackend {
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<()>
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
@@ -247,31 +223,31 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<()> {
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<()> {
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> Result<Position> {
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
Ok(self.pos.into())
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<()> {
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
self.pos = position.into().into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<()> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<()> {
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
let region = match clear_type {
|
||||
ClearType::All => return self.clear(),
|
||||
ClearType::AfterCursor => {
|
||||
@@ -311,7 +287,7 @@ impl Backend for TestBackend {
|
||||
/// the cursor y position then that number of empty lines (at most the buffer's height in this
|
||||
/// case but this limit is instead replaced with scrolling in most backend implementations) will
|
||||
/// be added after the current position and the cursor will be moved to the last row.
|
||||
fn append_lines(&mut self, line_count: u16) -> Result<()> {
|
||||
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
|
||||
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
|
||||
let Rect { width, height, .. } = self.buffer.area;
|
||||
|
||||
@@ -348,11 +324,11 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Size> {
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
Ok(self.buffer.area.as_size())
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
// Some arbitrary window pixel size, probably doesn't need much testing.
|
||||
const WINDOW_PIXEL_SIZE: Size = Size {
|
||||
width: 640,
|
||||
@@ -364,74 +340,7 @@ impl Backend for TestBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: core::ops::Range<u16>, scroll_by: u16) -> Result<()> {
|
||||
let width: usize = self.buffer.area.width.into();
|
||||
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
|
||||
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
|
||||
let cell_region_len = cell_region_end - cell_region_start;
|
||||
let cells_to_scroll_by = width * scroll_by as usize;
|
||||
|
||||
// Deal with the simple case where nothing needs to be copied into scrollback.
|
||||
if cell_region_start > 0 {
|
||||
if cells_to_scroll_by >= cell_region_len {
|
||||
// The scroll amount is large enough to clear the whole region.
|
||||
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
|
||||
} else {
|
||||
// Scroll up by rotating, then filling in the bottom with empty cells.
|
||||
self.buffer.content[cell_region_start..cell_region_end]
|
||||
.rotate_left(cells_to_scroll_by);
|
||||
self.buffer.content[cell_region_end - cells_to_scroll_by..cell_region_end]
|
||||
.fill_with(Default::default);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The rows inserted into the scrollback will first come from the buffer, and if that is
|
||||
// insufficient, will then be blank rows.
|
||||
let cells_from_region = cell_region_len.min(cells_to_scroll_by);
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
self.buffer.content.splice(
|
||||
0..cells_from_region,
|
||||
iter::repeat_with(Default::default).take(cells_from_region),
|
||||
),
|
||||
);
|
||||
if cells_to_scroll_by < cell_region_len {
|
||||
// Rotate the remaining cells to the front of the region.
|
||||
self.buffer.content[cell_region_start..cell_region_end].rotate_left(cells_from_region);
|
||||
} else {
|
||||
// Splice cleared out the region. Insert empty rows in scrollback.
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
iter::repeat_with(Default::default).take(cells_to_scroll_by - cell_region_len),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: core::ops::Range<u16>, scroll_by: u16) -> Result<()> {
|
||||
let width: usize = self.buffer.area.width.into();
|
||||
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
|
||||
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
|
||||
let cell_region_len = cell_region_end - cell_region_start;
|
||||
let cells_to_scroll_by = width * scroll_by as usize;
|
||||
|
||||
if cells_to_scroll_by >= cell_region_len {
|
||||
// The scroll amount is large enough to clear the whole region.
|
||||
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
|
||||
} else {
|
||||
// Scroll up by rotating, then filling in the top with empty cells.
|
||||
self.buffer.content[cell_region_start..cell_region_end]
|
||||
.rotate_right(cells_to_scroll_by);
|
||||
self.buffer.content[cell_region_start..cell_region_start + cells_to_scroll_by]
|
||||
.fill_with(Default::default);
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -453,8 +362,6 @@ fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item =
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::format;
|
||||
|
||||
use itertools::Itertools as _;
|
||||
|
||||
use super::*;
|
||||
@@ -479,12 +386,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn buffer_view_with_overwrites() {
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 2 wide
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 8 wide
|
||||
let buffer = Buffer::with_lines([multi_byte_char]);
|
||||
assert_eq!(
|
||||
buffer_view(&buffer),
|
||||
format!(
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " ")]
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
|
||||
"#,
|
||||
)
|
||||
);
|
||||
@@ -585,7 +492,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -605,7 +513,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -628,7 +537,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -651,7 +561,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -674,7 +585,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -697,7 +609,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -735,7 +648,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_lines_at_last_line() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -767,7 +681,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -796,7 +711,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_past_last_line() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -823,7 +739,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -856,7 +773,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -881,7 +799,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -916,7 +835,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_truncates_beyond_u16_max() -> Result<()> {
|
||||
fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
|
||||
// Fill the scrollback with 65535 + 10 lines.
|
||||
@@ -997,81 +916,4 @@ mod tests {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.flush().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
mod scrolling_regions {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const A: &str = "aaaa";
|
||||
const B: &str = "bbbb";
|
||||
const C: &str = "cccc";
|
||||
const D: &str = "dddd";
|
||||
const E: &str = "eeee";
|
||||
const S: &str = " ";
|
||||
|
||||
#[rstest]
|
||||
#[case([A, B, C, D, E], 0..5, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..5, 2, [A, B], [C, D, E, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 5, [A, B, C, D, E], [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 7, [A, B, C, D, E, S, S], [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..3, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 2, [A, B], [C, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 3, [A, B, C], [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 4, [A, B, C, S], [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 2, [], [A, D, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 3, [], [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 4, [], [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 2, [S, S], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 2, [], [A, B, C, D, E])]
|
||||
fn scroll_region_up<const L: usize, const M: usize, const N: usize>(
|
||||
#[case] initial_screen: [&'static str; L],
|
||||
#[case] range: core::ops::Range<u16>,
|
||||
#[case] scroll_by: u16,
|
||||
#[case] expected_scrollback: [&'static str; M],
|
||||
#[case] expected_buffer: [&'static str; N],
|
||||
) {
|
||||
let mut backend = TestBackend::with_lines(initial_screen);
|
||||
backend.scroll_region_up(range, scroll_by).unwrap();
|
||||
if expected_scrollback.is_empty() {
|
||||
backend.assert_scrollback_empty();
|
||||
} else {
|
||||
backend.assert_scrollback_lines(expected_scrollback);
|
||||
}
|
||||
backend.assert_buffer_lines(expected_buffer);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case([A, B, C, D, E], 0..5, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..5, 2, [S, S, A, B, C])]
|
||||
#[case([A, B, C, D, E], 0..5, 5, [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 7, [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..3, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 2, [S, S, A, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 3, [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 4, [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 2, [A, S, S, B, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 3, [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 4, [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 2, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 2, [A, B, C, D, E])]
|
||||
fn scroll_region_down<const M: usize, const N: usize>(
|
||||
#[case] initial_screen: [&'static str; M],
|
||||
#[case] range: core::ops::Range<u16>,
|
||||
#[case] scroll_by: u16,
|
||||
#[case] expected_buffer: [&'static str; N],
|
||||
) {
|
||||
let mut backend = TestBackend::with_lines(initial_screen);
|
||||
backend.scroll_region_down(range, scroll_by).unwrap();
|
||||
backend.assert_scrollback_empty();
|
||||
backend.assert_buffer_lines(expected_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user