Compare commits
13 Commits
v0.28.0
...
kd/multi-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf9d92f10 | ||
|
|
b712034644 | ||
|
|
38fca62fa9 | ||
|
|
3a51a027a6 | ||
|
|
5b30f2275c | ||
|
|
b4c27c744c | ||
|
|
977a4899c8 | ||
|
|
cd27b4829a | ||
|
|
feee871519 | ||
|
|
0051bb2037 | ||
|
|
477217c77a | ||
|
|
31de3586f7 | ||
|
|
f702025b75 |
16
.cargo-husky/hooks/pre-push
Executable file
16
.cargo-husky/hooks/pre-push
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
|
||||
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
|
||||
echo Please install it by running the following command:
|
||||
echo
|
||||
echo " cargo install --force cargo-make"
|
||||
echo
|
||||
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
|
||||
echo the following command instead of git push:
|
||||
echo
|
||||
echo " git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo make ci
|
||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -5,4 +5,7 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @ratatui-org/maintainers
|
||||
* @orhun @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
|
||||
# Past maintainers
|
||||
# @mindoodoo @sayanarijit
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
@@ -17,22 +17,26 @@ A detailed and complete issue is more likely to be processed quickly.
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frequently Asked Questions
|
||||
url: https://ratatui.rs/faq/
|
||||
about: Check the website FAQ section to see if your question has already been answered
|
||||
- name: Ratatui Forum
|
||||
url: https://forum.ratatui.rs
|
||||
about: Ask questions about ratatui on our Forum
|
||||
- name: Discord Chat
|
||||
url: https://discord.gg/pMCEU9hNEj
|
||||
about: Ask questions about ratatui on Discord
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Type: Enhancement'
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
25
.github/workflows/bench_base.yml
vendored
25
.github/workflows/bench_base.yml
vendored
@@ -1,25 +0,0 @@
|
||||
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
25
.github/workflows/bench_run_fork_pr.yml
vendored
@@ -1,25 +0,0 @@
|
||||
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
75
.github/workflows/bench_track_fork_pr.yml
vendored
@@ -1,75 +0,0 @@
|
||||
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"
|
||||
2
.github/workflows/check-pr.yml
vendored
2
.github/workflows/check-pr.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Type: Breaking Change']
|
||||
labels: ['breaking change']
|
||||
})
|
||||
|
||||
do-not-merge:
|
||||
|
||||
16
.github/workflows/check-semver.yml
vendored
16
.github/workflows/check-semver.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Check Semver
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-semver:
|
||||
name: Check semver
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check semver
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
91
.github/workflows/ci.yml
vendored
91
.github/workflows/ci.yml
vendored
@@ -25,56 +25,49 @@ env:
|
||||
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
|
||||
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
rustfmt:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo +nightly fmt --all --check
|
||||
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Check formatting
|
||||
run: cargo make lint-format
|
||||
- name: Check documentation
|
||||
run: cargo make lint-docs
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make clippy
|
||||
|
||||
markdownlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint markdown
|
||||
uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
'!target'
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run cargo make clippy-all
|
||||
run: cargo make clippy
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -83,7 +76,8 @@ jobs:
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
@@ -100,29 +94,21 @@ jobs:
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust {{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make check
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run cargo make check
|
||||
run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make lint-docs
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -130,12 +116,14 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
env:
|
||||
@@ -154,7 +142,8 @@ jobs:
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust ${{ matrix.toolchain }}}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
@@ -163,7 +152,9 @@ jobs:
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-backend ${{ matrix.backend }}
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
@@ -10,24 +10,6 @@ GitHub with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `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>>`
|
||||
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
|
||||
- `ratatui::terminal` module is now private
|
||||
- `ToText` no longer has a lifetime
|
||||
- `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
- [v0.27.0](#v0270)
|
||||
- List no clamps the selected index to list
|
||||
- Prelude items added / removed
|
||||
- 'termion' updated to 4.0
|
||||
- `Rect::inner` takes `Margin` directly instead of reference
|
||||
- `Buffer::filled` takes `Cell` directly instead of reference
|
||||
- `Stylize::bg()` now accepts `Into<Color>`
|
||||
- Removed deprecated `List::start_corner`
|
||||
- `LineGauge::gauge_style` is deprecated
|
||||
- [v0.26.0](#v0260)
|
||||
- `Flex::Start` is the new default flex mode for `Layout`
|
||||
- `patch_style` & `reset_style` now consume and return `Self`
|
||||
@@ -65,216 +47,6 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.28.0
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||
|
||||
[#1254]: https://github.com/ratatui-org/ratatui/pull/1254
|
||||
|
||||
The `Backend::size` method returns a `Size` instead of a `Rect`.
|
||||
There is no need for the position here as it was always 0,0.
|
||||
|
||||
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
|
||||
|
||||
[#1284]: https://github.com/ratatui-org/ratatui/pull/1284
|
||||
|
||||
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
|
||||
nothing is a breaking change for you.
|
||||
|
||||
If you implement the Backend trait yourself, you need to update the implementation and add the
|
||||
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
|
||||
and a default implementation for them exists.
|
||||
|
||||
### Ratatui now requires Crossterm 0.28.0 ([#1278])
|
||||
|
||||
[#1278]: https://github.com/ratatui-org/ratatui/pull/1278
|
||||
|
||||
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
|
||||
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
|
||||
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
|
||||
|
||||
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
|
||||
|
||||
[#1273]: https://github.com/ratatui-org/ratatui/pull/1173
|
||||
[#1283]: https://github.com/ratatui-org/ratatui/pull/1283
|
||||
|
||||
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
|
||||
type will need to be rewritten as the compiler cannot infer the correct type.
|
||||
|
||||
```diff
|
||||
- Axis::default().labels(vec!["a".into(), "b".into()])
|
||||
+ Axis::default().labels(["a", "b"])
|
||||
```
|
||||
|
||||
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
|
||||
|
||||
[#1245]: https://github.com/ratatui-org/ratatui/pull/1245
|
||||
|
||||
```diff
|
||||
- let is_initialized = Layout::init_cache(100);
|
||||
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
|
||||
```
|
||||
|
||||
### `ratatui::terminal` module is now private ([#1160])
|
||||
|
||||
[#1160]: https://github.com/ratatui-org/ratatui/pull/1160
|
||||
|
||||
The `terminal` module is now private and can not be used directly. The types under this module are
|
||||
exported from the root of the crate. This reduces clashes with other modules in the backends that
|
||||
are also named terminal, and confusion about module exports for newer Rust users.
|
||||
|
||||
```diff
|
||||
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||
```
|
||||
|
||||
### `ToText` no longer has a lifetime ([#1234])
|
||||
|
||||
[#1234]: https://github.com/ratatui-org/ratatui/pull/1234
|
||||
|
||||
This change simplifies the trait and makes it easier to implement.
|
||||
|
||||
### `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
|
||||
[#1293]: https://github.com/ratatui-org/ratatui/pull/1293
|
||||
|
||||
`Frame::size` is renamed to `Frame::area` as its the more correct name.
|
||||
|
||||
## [v0.27.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.27.0)
|
||||
|
||||
### List no clamps the selected index to list ([#1159])
|
||||
|
||||
[#1149]: https://github.com/ratatui-org/ratatui/pull/1149
|
||||
|
||||
The `List` widget now clamps the selected index to the bounds of the list when navigating with
|
||||
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
|
||||
|
||||
Previously selecting an index past the end of the list would show treat the list as having a
|
||||
selection which was not visible. Now the last item in the list will be selected instead.
|
||||
|
||||
### Prelude items added / removed ([#1149])
|
||||
|
||||
The following items have been removed from the prelude:
|
||||
|
||||
- `style::Styled` - this trait is useful for widgets that want to
|
||||
support the Stylize trait, but it adds complexity as widgets have two
|
||||
`style` methods and a `set_style` method.
|
||||
- `symbols::Marker` - this item is used by code that needs to draw to
|
||||
the `Canvas` widget, but it's not a common item that would be used by
|
||||
most users of the library.
|
||||
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
|
||||
are rarely used by code that needs to interact with the terminal, and
|
||||
they're generally only ever used once in any app.
|
||||
|
||||
The following items have been added to the prelude:
|
||||
|
||||
- `layout::{Position, Size}` - these items are used by code that needs
|
||||
to interact with the layout system. These are newer items that were
|
||||
added in the last few releases, which should be used more liberally.
|
||||
This may cause conflicts for types defined elsewhere with a similar
|
||||
name.
|
||||
|
||||
To update your app:
|
||||
|
||||
```diff
|
||||
// if your app uses Styled::style() or Styled::set_style():
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, style::Styled};
|
||||
|
||||
// if your app uses symbols::Marker:
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, symbols::Marker}
|
||||
|
||||
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
|
||||
|
||||
// to disambiguate existing types named Position or Size:
|
||||
- use some_crate::{Position, Size};
|
||||
- let size: Size = ...;
|
||||
- let position: Position = ...;
|
||||
+ let size: some_crate::Size = ...;
|
||||
+ let position: some_crate::Position = ...;
|
||||
```
|
||||
|
||||
### Termion is updated to 4.0 [#1106]
|
||||
|
||||
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
|
||||
|
||||
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
|
||||
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
|
||||
`MouseRight`, or add a wildcard.
|
||||
|
||||
[#1106]: https://github.com/ratatui-org/ratatui/pull/1106
|
||||
|
||||
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
|
||||
|
||||
[#1008]: https://github.com/ratatui-org/ratatui/pull/1008
|
||||
|
||||
`Margin` needs to be passed without reference now.
|
||||
|
||||
```diff
|
||||
-let area = area.inner(&Margin {
|
||||
+let area = area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
```
|
||||
|
||||
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
|
||||
|
||||
[#1148]: https://github.com/ratatui-org/ratatui/pull/1148
|
||||
|
||||
`Buffer::filled` moves the `Cell` instead of taking a reference.
|
||||
|
||||
```diff
|
||||
-Buffer::filled(area, &Cell::new("X"));
|
||||
+Buffer::filled(area, Cell::new("X"));
|
||||
```
|
||||
|
||||
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
|
||||
|
||||
[#1103]: https://github.com/ratatui-org/ratatui/pull/1103
|
||||
|
||||
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
|
||||
flexible types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
|
||||
|
||||
[#759]: https://github.com/ratatui-org/ratatui/pull/759
|
||||
|
||||
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::TopLeft);
|
||||
- list.start_corner(Corner::TopRight);
|
||||
// This is not an error, BottomRight rendered top to bottom previously
|
||||
- list.start_corner(Corner::BottomRight);
|
||||
// all becomes
|
||||
+ list.direction(ListDirection::TopToBottom);
|
||||
```
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::BottomLeft);
|
||||
// becomes
|
||||
+ list.direction(ListDirection::BottomToTop);
|
||||
```
|
||||
|
||||
`layout::Corner` was removed entirely.
|
||||
|
||||
### `LineGauge::gauge_style` is deprecated ([#565])
|
||||
|
||||
[#565]: https://github.com/ratatui-org/ratatui/pull/1148
|
||||
|
||||
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
|
||||
|
||||
```diff
|
||||
let gauge = LineGauge::default()
|
||||
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
|
||||
+ .filled_style(Style::default().fg(Color::Green))
|
||||
+ .unfilled_style(Style::default().fg(Color::White));
|
||||
```
|
||||
|
||||
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
|
||||
|
||||
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
|
||||
@@ -302,7 +74,7 @@ existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Le
|
||||
|
||||
[#774]: https://github.com/ratatui-org/ratatui/pull/774
|
||||
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
|
||||
can some break type inference in the calling scope for empty containers.
|
||||
|
||||
@@ -319,7 +91,7 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
|
||||
|
||||
[#776]: https://github.com/ratatui-org/ratatui/pull/776
|
||||
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
|
||||
@@ -360,6 +132,8 @@ The following example shows how to migrate for `Line`, but the same applies for
|
||||
|
||||
### Remove deprecated `Block::title_on_bottom` ([#757])
|
||||
|
||||
[#757]: https://github.com/ratatui-org/ratatui/pull/757
|
||||
|
||||
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
|
||||
|
||||
```diff
|
||||
@@ -615,8 +389,8 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
|
||||
|
||||
[#205]: https://github.com/ratatui-org/ratatui/issues/205
|
||||
|
||||
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
|
||||
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||
The `serde` representation of `bitflags` has changed. Any existing serialized types that have Borders or
|
||||
Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
|
||||
|
||||
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
|
||||
|
||||
1121
CHANGELOG.md
1121
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -17,10 +17,10 @@ found.
|
||||
|
||||
All contributions are obviously welcome. Please include as many details as possible in your PR
|
||||
description to help the reviewer (follow the provided template). Make sure to highlight changes
|
||||
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
|
||||
which may need additional attention or you are uncertain about. Any idea with a large scale impact
|
||||
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
### Keep PRs small, intentional, and focused
|
||||
### Keep PRs small, intentional and focused
|
||||
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
|
||||
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
|
||||
@@ -32,7 +32,7 @@ guarantee that the behavior is unchanged.
|
||||
### Code formatting
|
||||
|
||||
Run `cargo make format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
@@ -56,9 +56,11 @@ documented.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
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.
|
||||
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
|
||||
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
|
||||
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
|
||||
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
|
||||
check, you can use `git push --no-verify`.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
@@ -147,7 +149,7 @@ fn foo() {}
|
||||
```
|
||||
|
||||
- Max line length is 100 characters
|
||||
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
|
||||
- Doc comments are above macros
|
||||
i.e.
|
||||
@@ -163,19 +165,19 @@ i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||
|
||||
### Deprecation notice
|
||||
|
||||
We generally want to wait at least two versions before removing deprecated items, so users have
|
||||
We generally want to wait at least two versions before removing deprecated items so users have
|
||||
time to update. However, if a deprecation is blocking for us to implement a new feature we may
|
||||
*consider* removing it in a one version notice.
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However, there
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
|
||||
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
|
||||
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use GitHub Actions for the CI where we perform the following checks:
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
@@ -191,10 +193,10 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
|
||||
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
|
||||
([@fdehau](https://github.com/fdehau)).
|
||||
|
||||
The original repository contains all the issues, PRs, and discussion that were raised originally, and
|
||||
The original repository contains all the issues, PRs and discussion that were raised originally, and
|
||||
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
|
||||
|
||||
We imported all the PRs from the original repository, implemented many of the smaller ones, and
|
||||
We imported all the PRs from the original repository and implemented many of the smaller ones and
|
||||
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
|
||||
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
We have documented the current state of those PRs, and anyone is welcome to pick them up and
|
||||
|
||||
156
Cargo.toml
156
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.28.0" # crate version
|
||||
version = "0.26.2" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
@@ -18,6 +18,7 @@ exclude = [
|
||||
"*.log",
|
||||
"tags",
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.74.0"
|
||||
|
||||
@@ -26,49 +27,41 @@ rust-version = "1.74.0"
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
crossterm = { version = "0.28.1", optional = true }
|
||||
compact_str = "0.7.1"
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
instability = "0.3.1"
|
||||
itertools = "0.13"
|
||||
itertools = "0.12"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
stability = "0.2.0"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
strum_macros = { version = "0.26.3" }
|
||||
termion = { version = "4.0.0", optional = true }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "0.1.13"
|
||||
unicode-width = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1.12"
|
||||
better-panic = "0.3.0"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
"user-hooks",
|
||||
] }
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||
derive_builder = "0.20.0"
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.39.0"
|
||||
palette = "0.7.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
rstest = "0.19.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"] }
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
@@ -84,10 +77,7 @@ 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"
|
||||
wildcard_imports = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
@@ -101,15 +91,11 @@ 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"
|
||||
|
||||
[features]
|
||||
@@ -120,37 +106,32 @@ use_self = "warn"
|
||||
## which allows you to set the underline color of text.
|
||||
default = ["crossterm", "underline-color"]
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`].
|
||||
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
|
||||
crossterm = ["dep:crossterm"]
|
||||
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
|
||||
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
|
||||
termion = ["dep:termion"]
|
||||
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
|
||||
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
|
||||
termwiz = ["dep:termwiz"]
|
||||
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## 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"]
|
||||
|
||||
## enables the [`border!`] macro.
|
||||
macros = []
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["dep:palette"]
|
||||
|
||||
## enables all widgets.
|
||||
all-widgets = ["widget-calendar"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`].
|
||||
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
#! The following optional features are only available for some backends:
|
||||
|
||||
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
|
||||
#! on Windows 7.
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
@@ -158,13 +139,13 @@ underline-color = ["dep:crossterm"]
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods
|
||||
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
|
||||
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
|
||||
## the future.
|
||||
unstable-widget-ref = []
|
||||
|
||||
@@ -174,46 +155,52 @@ all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
[[bench]]
|
||||
name = "barchart"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "block"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "list"
|
||||
harness = false
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "main"
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "sparkline"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "async"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "barchart-grouped"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
@@ -229,19 +216,9 @@ doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "colors_rgb"
|
||||
required-features = ["crossterm", "palette"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
@@ -254,7 +231,7 @@ doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "demo2"
|
||||
required-features = ["crossterm", "palette", "widget-calendar"]
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
@@ -262,11 +239,6 @@ name = "docsrs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
@@ -277,24 +249,24 @@ name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "line_gauge"
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hyperlink"
|
||||
required-features = ["crossterm", "unstable-widget-ref"]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
@@ -302,12 +274,6 @@ name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "minimal"
|
||||
required-features = ["crossterm"]
|
||||
# prefer to show the more featureful examples in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "modifiers"
|
||||
required-features = ["crossterm"]
|
||||
@@ -355,12 +321,12 @@ required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tracing"
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Maintainers
|
||||
|
||||
This file documents current and past maintainers.
|
||||
|
||||
- [orhun](https://github.com/orhun)
|
||||
- [joshka](https://github.com/joshka)
|
||||
- [kdheepak](https://github.com/kdheepak)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
|
||||
## Past Maintainers
|
||||
|
||||
- [fdehau](https://github.com/fdehau)
|
||||
- [mindoodoo](https://github.com/mindoodoo)
|
||||
- [sayanarijit](https://github.com/sayanarijit)
|
||||
@@ -19,11 +19,11 @@ alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
description = "Run continuous integration tasks"
|
||||
dependencies = ["lint", "clippy", "check", "test"]
|
||||
dependencies = ["lint-style", "clippy", "check", "test"]
|
||||
|
||||
[tasks.lint]
|
||||
description = "Lint code style (formatting, typos, docs, markdown)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs", "lint-markdown"]
|
||||
[tasks.lint-style]
|
||||
description = "Lint code style (formatting, typos, docs)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.lint-format]
|
||||
description = "Lint code formatting"
|
||||
@@ -56,11 +56,6 @@ args = [
|
||||
"-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"
|
||||
|
||||
173
README.md
173
README.md
@@ -21,14 +21,14 @@
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
[![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>
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
[![Matrix Badge]][Matrix]<br>
|
||||
|
||||
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
@@ -43,10 +43,10 @@ Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its deve
|
||||
|
||||
## Installation
|
||||
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui
|
||||
cargo add ratatui crossterm
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
@@ -67,7 +67,6 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
|
||||
## Other documentation
|
||||
|
||||
- [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.
|
||||
@@ -111,8 +110,7 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
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]
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
@@ -127,18 +125,12 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
|
||||
```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,
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
@@ -169,7 +161,8 @@ fn handle_events() -> io::Result<bool> {
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
Paragraph::new("Hello World!")
|
||||
.block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
@@ -187,27 +180,40 @@ area. This lets you describe a responsive terminal UI by nesting layouts. See th
|
||||
section of the [Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
widgets::Block,
|
||||
Frame,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
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.size());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
let main_layout = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
main_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Status Bar"),
|
||||
main_layout[2],
|
||||
);
|
||||
|
||||
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);
|
||||
let inner_layout = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::bordered().title("Left"),
|
||||
inner_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::bordered().title("Right"),
|
||||
inner_layout[1],
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -228,41 +234,48 @@ short-hand syntax to apply a style to widgets and text. See the [Styling Text] s
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
|
||||
let areas = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
|
||||
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]);
|
||||
let span1 = Span::raw("Hello ");
|
||||
let span2 = Span::styled(
|
||||
"World",
|
||||
Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let span3 = "!".red().on_light_yellow().italic();
|
||||
|
||||
// using the short-hand syntax and implicit conversions
|
||||
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
frame.render_widget(paragraph, areas[1]);
|
||||
let line = Line::from(vec![span1, span2, span3]);
|
||||
let text: Text = Text::from(vec![line]);
|
||||
|
||||
// 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]);
|
||||
frame.render_widget(Paragraph::new(text), areas[0]);
|
||||
// or using the short-hand syntax and implicit conversions
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!".red().on_white().bold()),
|
||||
areas[1],
|
||||
);
|
||||
|
||||
// use the simpler short-hand syntax
|
||||
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
frame.render_widget(paragraph, areas[3]);
|
||||
// to style the whole widget instead of just the text
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
|
||||
areas[2],
|
||||
);
|
||||
// or using the short-hand syntax
|
||||
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -318,17 +331,18 @@ Running this example produces the following output:
|
||||
[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-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov Badge]:
|
||||
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[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/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 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-org?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
@@ -344,14 +358,17 @@ In order to organize ourselves, we currently use a [Discord server](https://disc
|
||||
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].
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing.
|
||||
Our primary open-source workflow is centered around GitHub.
|
||||
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
|
||||
@@ -6,4 +6,4 @@ We only support the latest version of this crate.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report secuirity vulnerability, please use the form at <https://github.com/ratatui-org/ratatui/security/advisories/new>
|
||||
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use rand::Rng;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
@@ -70,3 +70,4 @@ fn render(bencher: &mut Bencher, barchart: &BarChart) {
|
||||
}
|
||||
|
||||
criterion_group!(benches, barchart);
|
||||
criterion_main!(benches);
|
||||
@@ -1,4 +1,4 @@
|
||||
use criterion::{criterion_group, BatchSize, Bencher, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
@@ -59,3 +59,4 @@ fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
|
||||
}
|
||||
|
||||
criterion_group!(benches, block);
|
||||
criterion_main!(benches);
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{criterion_group, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
@@ -36,3 +36,4 @@ fn line_render(criterion: &mut Criterion) {
|
||||
}
|
||||
|
||||
criterion_group!(benches, line_render);
|
||||
criterion_main!(benches);
|
||||
@@ -1,4 +1,4 @@
|
||||
use criterion::{criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
@@ -70,3 +70,4 @@ fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
|
||||
}
|
||||
|
||||
criterion_group!(benches, list);
|
||||
criterion_main!(benches);
|
||||
@@ -1,22 +0,0 @@
|
||||
pub mod main {
|
||||
pub mod barchart;
|
||||
pub mod block;
|
||||
pub mod buffer;
|
||||
pub mod line;
|
||||
pub mod list;
|
||||
pub mod paragraph;
|
||||
pub mod rect;
|
||||
pub mod sparkline;
|
||||
}
|
||||
pub use main::*;
|
||||
|
||||
criterion::criterion_main!(
|
||||
barchart::benches,
|
||||
block::benches,
|
||||
buffer::benches,
|
||||
line::benches,
|
||||
list::benches,
|
||||
paragraph::benches,
|
||||
rect::benches,
|
||||
sparkline::benches
|
||||
);
|
||||
@@ -1,65 +0,0 @@
|
||||
use criterion::{black_box, BenchmarkId, Criterion};
|
||||
use ratatui::{
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
text::Line,
|
||||
};
|
||||
|
||||
criterion::criterion_group!(benches, empty, filled, with_lines);
|
||||
|
||||
const fn rect(size: u16) -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: size,
|
||||
height: size,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("buffer/empty");
|
||||
for size in [16, 64, 255] {
|
||||
let area = rect(size);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &area, |b, &area| {
|
||||
b.iter(|| {
|
||||
let _buffer = Buffer::empty(black_box(area));
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// This likely should have the same performance as `empty`, but it's here for completeness
|
||||
/// and to catch any potential performance regressions.
|
||||
fn filled(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("buffer/filled");
|
||||
for size in [16, 64, 255] {
|
||||
let area = rect(size);
|
||||
let cell = Cell::new("AAAA"); // simulate a multi-byte character
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(size),
|
||||
&(area, cell),
|
||||
|b, (area, cell)| {
|
||||
b.iter(|| {
|
||||
let _buffer = Buffer::filled(black_box(*area), cell.clone());
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn with_lines(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("buffer/with_lines");
|
||||
for size in [16, 64, 255] {
|
||||
let word_count = 50;
|
||||
let lines = fakeit::words::sentence(word_count);
|
||||
let lines = lines.lines().map(Line::from);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), &lines, |b, lines| {
|
||||
b.iter(|| {
|
||||
let _buffer = Buffer::with_lines(black_box(lines.clone()));
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
use criterion::{black_box, criterion_group, BenchmarkId, Criterion};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
fn rect_rows_benchmark(c: &mut Criterion) {
|
||||
let rect_sizes = vec![
|
||||
Rect::new(0, 0, 1, 16),
|
||||
Rect::new(0, 0, 1, 1024),
|
||||
Rect::new(0, 0, 1, 65535),
|
||||
];
|
||||
let mut group = c.benchmark_group("rect_rows");
|
||||
for rect in rect_sizes {
|
||||
group.bench_with_input(BenchmarkId::new("rows", rect.height), &rect, |b, rect| {
|
||||
b.iter(|| {
|
||||
for row in rect.rows() {
|
||||
// Perform any necessary operations on each row
|
||||
black_box(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, rect_rows_benchmark);
|
||||
@@ -1,4 +1,6 @@
|
||||
use criterion::{black_box, criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{
|
||||
black_box, criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
@@ -92,3 +94,4 @@ fn random_lines(count: u16) -> String {
|
||||
}
|
||||
|
||||
criterion_group!(benches, paragraph);
|
||||
criterion_main!(benches);
|
||||
@@ -1,4 +1,4 @@
|
||||
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use rand::Rng;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
@@ -42,3 +42,4 @@ fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
|
||||
}
|
||||
|
||||
criterion_group!(benches, sparkline);
|
||||
criterion_main!(benches);
|
||||
22
cliff.toml
22
cliff.toml
@@ -11,8 +11,6 @@ header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
@@ -33,14 +31,16 @@ body = """
|
||||
{% 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) }}
|
||||
{%- if commit.body %}
|
||||
|
||||
````text {#- 4 backticks escape any backticks in body #}
|
||||
{{commit.body | indent(prefix=" ") }}
|
||||
````
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}\n
|
||||
{%- for footer in commit.footers %}
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
>
|
||||
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{{- footer.separator }}
|
||||
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
|
||||
|
||||
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
@@ -87,11 +87,6 @@ trim = false
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
postprocessors = [
|
||||
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
|
||||
{ pattern = '>---+\n', replace = '' },
|
||||
{ pattern = ' +\n', replace = "\n" },
|
||||
]
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
@@ -131,7 +126,6 @@ commit_parsers = [
|
||||
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
]
|
||||
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
version = 2
|
||||
default = "deny"
|
||||
unlicensed = "deny"
|
||||
copyleft = "deny"
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
@@ -14,7 +16,8 @@ allow = [
|
||||
]
|
||||
|
||||
[advisories]
|
||||
version = 2
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
@@ -23,26 +23,6 @@ This folder might use unreleased code. View the examples for the latest release
|
||||
> 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.
|
||||
|
||||
Not every example has been updated with all these points in mind yet, however over time they will
|
||||
be. None of the above choices are strictly necessary for Ratatui apps, but these choices make
|
||||
examples easier to run, maintain and explain. These choices are designed to help newer users fall
|
||||
into the pit of success when incorporating example code into their own apps. We may also eventually
|
||||
move some of these design choices into the core of Ratatui to simplify apps.
|
||||
|
||||
[How to use color-eyre with Ratatui]: https://ratatui.rs/how-to/develop-apps/color_eyre/
|
||||
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
@@ -88,17 +68,6 @@ cargo run --example=barchart --features=crossterm
|
||||
|
||||
![Barchart][barchart.gif]
|
||||
|
||||
## Barchart (Grouped)
|
||||
|
||||
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
widget with groups. Source: [barchart-grouped.rs](./barchart-grouped.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=barchart-grouped --features=crossterm
|
||||
```
|
||||
|
||||
![Barchart Grouped][barchart-grouped.gif]
|
||||
|
||||
## Block
|
||||
|
||||
Demonstrates the [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
@@ -172,30 +141,6 @@ of the VHS tape.
|
||||
|
||||
<https://github.com/ratatui-org/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
|
||||
|
||||
## Constraint Explorer
|
||||
|
||||
Demonstrates the behaviour of each
|
||||
[`Constraint`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html) option with
|
||||
respect to each other across different `Flex` modes.
|
||||
|
||||
```shell
|
||||
cargo run --example=constraint-explorer --features=crossterm
|
||||
```
|
||||
|
||||
![Constraint Explorer][constraint-explorer.gif]
|
||||
|
||||
## Constraints
|
||||
|
||||
Demonstrates how to use
|
||||
[`Constraint`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html) options for
|
||||
defining layout element sizes.
|
||||
|
||||
![Constraints][constraints.gif]
|
||||
|
||||
```shell
|
||||
cargo run --example=constraints --features=crossterm
|
||||
```
|
||||
|
||||
## Custom Widget
|
||||
|
||||
Demonstrates how to implement the
|
||||
@@ -219,39 +164,6 @@ cargo run --example=gauge --features=crossterm
|
||||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Flex
|
||||
|
||||
Demonstrates the different [`Flex`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Flex.html)
|
||||
modes for controlling layout space distribution.
|
||||
|
||||
```shell
|
||||
cargo run --example=flex --features=crossterm
|
||||
```
|
||||
|
||||
![Flex][flex.gif]
|
||||
|
||||
## Line Gauge
|
||||
|
||||
Demonstrates the [`Line
|
||||
Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.LineGauge.html) widget. Source:
|
||||
[line_gauge.rs](./line_gauge.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=line_gauge --features=crossterm
|
||||
```
|
||||
|
||||
![LineGauge][line_gauge.gif]
|
||||
|
||||
## Hyperlink
|
||||
|
||||
Demonstrates how to use OSC 8 to create hyperlinks in the terminal.
|
||||
|
||||
```shell
|
||||
cargo run --example=hyperlink --features="crossterm unstable-widget-ref"
|
||||
```
|
||||
|
||||
![Hyperlink][hyperlink.gif]
|
||||
|
||||
## Inline
|
||||
|
||||
Demonstrates how to use the
|
||||
@@ -298,16 +210,6 @@ cargo run --example=modifiers --features=crossterm
|
||||
|
||||
![Modifiers][modifiers.gif]
|
||||
|
||||
## Minimal
|
||||
|
||||
Demonstrates how to create a minimal `Hello World!` program.
|
||||
|
||||
```shell
|
||||
cargo run --example=minimal --features=crossterm
|
||||
```
|
||||
|
||||
![Minimal][minimal.gif]
|
||||
|
||||
## Panic
|
||||
|
||||
Demonstrates how to handle panics by ensuring that panic messages are written correctly to the
|
||||
@@ -399,17 +301,6 @@ cargo run --example=tabs --features=crossterm
|
||||
|
||||
![Tabs][tabs.gif]
|
||||
|
||||
## Tracing
|
||||
|
||||
Demonstrates how to use the [tracing crate](https://crates.io/crates/tracing) for logging. Creates
|
||||
a file named `tracing.log` in the current directory.
|
||||
|
||||
```shell
|
||||
cargo run --example=tracing --features=crossterm
|
||||
```
|
||||
|
||||
![Tracing][tracing.gif]
|
||||
|
||||
## User Input
|
||||
|
||||
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
|
||||
@@ -442,26 +333,19 @@ examples/vhs/generate.bash
|
||||
-->
|
||||
|
||||
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.gif?raw=true
|
||||
[barchart-grouped.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart-grouped.gif?raw=true
|
||||
[block.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/block.gif?raw=true
|
||||
[calendar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/calendar.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
|
||||
[constraint-explorer.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
|
||||
[constraints.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/constraints.gif?raw=true
|
||||
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
[flex.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/flex.gif?raw=true
|
||||
[gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/gauge.gif?raw=true
|
||||
[hello_world.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hello_world.gif?raw=true
|
||||
[hyperlink.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hyperlink.gif?raw=true
|
||||
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[layout.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/layout.gif?raw=true
|
||||
[list.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/list.gif?raw=true
|
||||
[line_gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/line_gauge.gif?raw=true
|
||||
[minimal.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/minimal.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
|
||||
@@ -471,7 +355,6 @@ examples/vhs/generate.bash
|
||||
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
|
||||
[table.gif]: https://vhs.charm.sh/vhs-6njXBytDf0rwPufUtmSSpI.gif
|
||||
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
|
||||
[tracing.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tracing.gif?raw=true
|
||||
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true
|
||||
|
||||
[alpha version of Ratatui]: https://crates.io/crates/ratatui/versions
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
//! # [Ratatui] Async example
|
||||
//!
|
||||
//! This example demonstrates how to use Ratatui with widgets that fetch data asynchronously. It
|
||||
//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API. You will need an
|
||||
//! environment variable named `GITHUB_TOKEN` with a valid GitHub personal access token. The token
|
||||
//! does not need any special permissions.
|
||||
//!
|
||||
//! <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token>
|
||||
//! <https://github.com/settings/tokens/new> to create a new token (select classic, and no scopes)
|
||||
//!
|
||||
//! This example does not cover message passing between threads, it only demonstrates how to manage
|
||||
//! shared state between the main thread and a background task, which acts mostly as a one-shot
|
||||
//! fetcher. For more complex scenarios, you may need to use channels or other synchronization
|
||||
//! primitives.
|
||||
//!
|
||||
//! A simple app might have multiple widgets that fetch data from different sources, and each widget
|
||||
//! would have its own background task to fetch the data. The main thread would then render the
|
||||
//! widgets with the latest data.
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
use std::{
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::Context, Result, Section};
|
||||
use futures::StreamExt;
|
||||
use octocrab::{
|
||||
params::{pulls::Sort, Direction},
|
||||
OctocrabBuilder, Page,
|
||||
};
|
||||
use ratatui::{
|
||||
crossterm::event::{Event, EventStream, KeyCode},
|
||||
layout::Offset,
|
||||
prelude::{Buffer, Constraint, Line, Modifier, Rect, Stylize},
|
||||
widgets::{
|
||||
Block, BorderType, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
use self::terminal::Terminal;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
init_octocrab()?;
|
||||
let terminal = terminal::init()?;
|
||||
let app_result = App::default().run(terminal).await;
|
||||
terminal::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn init_octocrab() -> Result<()> {
|
||||
let token = std::env::var("GITHUB_TOKEN")
|
||||
.wrap_err("The GITHUB_TOKEN environment variable was not found")
|
||||
.suggestion(
|
||||
"Go to https://github.com/settings/tokens/new to create a token, and re-run:
|
||||
GITHUB_TOKEN=ghp_... cargo run --example async --features crossterm",
|
||||
)?;
|
||||
let crab = OctocrabBuilder::new().personal_token(token).build()?;
|
||||
octocrab::initialise(crab);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
pulls: PullRequestsWidget,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const FRAMES_PER_SECOND: f32 = 60.0;
|
||||
|
||||
pub async fn run(mut self, mut terminal: Terminal) -> Result<()> {
|
||||
self.pulls.run();
|
||||
|
||||
let mut interval =
|
||||
tokio::time::interval(Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND));
|
||||
let mut events = EventStream::new();
|
||||
|
||||
while !self.should_quit {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => self.draw(&mut terminal)?,
|
||||
Some(Ok(event)) = events.next() => self.handle_event(&event),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
||||
terminal.draw(|frame| {
|
||||
let area = frame.area();
|
||||
frame.render_widget(
|
||||
Line::from("ratatui async example").centered().cyan().bold(),
|
||||
area,
|
||||
);
|
||||
let area = area.offset(Offset { x: 0, y: 1 }).intersection(area);
|
||||
frame.render_widget(&self.pulls, area);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &Event) {
|
||||
if let Event::Key(event) = event {
|
||||
match event.code {
|
||||
KeyCode::Char('q') => self.should_quit = true,
|
||||
KeyCode::Char('j') => self.pulls.scroll_down(),
|
||||
KeyCode::Char('k') => self.pulls.scroll_up(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that displays a list of pull requests.
|
||||
///
|
||||
/// This is an async widget that fetches the list of pull requests from the GitHub API. It contains
|
||||
/// an inner `Arc<RwLock<PullRequests>>` that holds the state of the widget. Cloning the widget
|
||||
/// will clone the Arc, so you can pass it around to other threads, and this is used to spawn a
|
||||
/// background task to fetch the pull requests.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct PullRequestsWidget {
|
||||
inner: Arc<RwLock<PullRequests>>,
|
||||
selected_index: usize, // no need to lock this since it's only accessed by the main thread
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PullRequests {
|
||||
pulls: Vec<PullRequest>,
|
||||
loading_state: LoadingState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PullRequest {
|
||||
id: String,
|
||||
title: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
enum LoadingState {
|
||||
#[default]
|
||||
Idle,
|
||||
Loading,
|
||||
Loaded,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl PullRequestsWidget {
|
||||
/// Start fetching the pull requests in the background.
|
||||
///
|
||||
/// This method spawns a background task that fetches the pull requests from the GitHub API.
|
||||
/// The result of the fetch is then passed to the `on_load` or `on_err` methods.
|
||||
fn run(&self) {
|
||||
let this = self.clone(); // clone the widget to pass to the background task
|
||||
tokio::spawn(this.fetch_pulls());
|
||||
}
|
||||
|
||||
async fn fetch_pulls(self) {
|
||||
// this runs once, but you could also run this in a loop, using a channel that accepts
|
||||
// messages to refresh on demand, or with an interval timer to refresh every N seconds
|
||||
self.set_loading_state(LoadingState::Loading);
|
||||
match octocrab::instance()
|
||||
.pulls("ratatui-org", "ratatui")
|
||||
.list()
|
||||
.sort(Sort::Updated)
|
||||
.direction(Direction::Descending)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(page) => self.on_load(&page),
|
||||
Err(err) => self.on_err(&err),
|
||||
}
|
||||
}
|
||||
fn on_load(&self, page: &Page<OctoPullRequest>) {
|
||||
let prs = page.items.iter().map(Into::into);
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
inner.loading_state = LoadingState::Loaded;
|
||||
inner.pulls.extend(prs);
|
||||
}
|
||||
|
||||
fn on_err(&self, err: &octocrab::Error) {
|
||||
self.set_loading_state(LoadingState::Error(err.to_string()));
|
||||
}
|
||||
|
||||
fn set_loading_state(&self, state: LoadingState) {
|
||||
self.inner.write().unwrap().loading_state = state;
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self) {
|
||||
self.selected_index = self.selected_index.saturating_add(1);
|
||||
}
|
||||
|
||||
fn scroll_up(&mut self) {
|
||||
self.selected_index = self.selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
type OctoPullRequest = octocrab::models::pulls::PullRequest;
|
||||
|
||||
impl From<&OctoPullRequest> for PullRequest {
|
||||
fn from(pr: &OctoPullRequest) -> Self {
|
||||
Self {
|
||||
id: pr.number.to_string(),
|
||||
title: pr.title.as_ref().unwrap().to_string(),
|
||||
url: pr
|
||||
.html_url
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &PullRequestsWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
// a block with a right aligned title with the loading state
|
||||
let loading_state = Line::from(format!("{:?}", inner.loading_state)).right_aligned();
|
||||
let block = Block::bordered()
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Pull Requests")
|
||||
.title(loading_state);
|
||||
|
||||
// a table with the list of pull requests
|
||||
let rows = inner.pulls.iter();
|
||||
let widths = [
|
||||
Constraint::Length(5),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Max(49),
|
||||
];
|
||||
let table = Table::new(rows, widths)
|
||||
.block(block)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_symbol(">>")
|
||||
.highlight_style(Modifier::REVERSED);
|
||||
let mut table_state = TableState::new().with_selected(self.selected_index);
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut table_state);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PullRequest> for Row<'_> {
|
||||
fn from(pr: &PullRequest) -> Self {
|
||||
let pr = pr.clone();
|
||||
Row::new(vec![pr.id, pr.title, pr.url])
|
||||
}
|
||||
}
|
||||
|
||||
mod terminal {
|
||||
use std::io;
|
||||
|
||||
use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::prelude::{CrosstermBackend, Terminal as RatatuiTerminal};
|
||||
|
||||
/// A type alias for the terminal type used in this example.
|
||||
pub type Terminal = RatatuiTerminal<CrosstermBackend<io::Stdout>>;
|
||||
|
||||
pub fn init() -> io::Result<Terminal> {
|
||||
set_panic_hook();
|
||||
enable_raw_mode()?;
|
||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
Terminal::new(backend)
|
||||
}
|
||||
|
||||
fn set_panic_hook() {
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
restore();
|
||||
hook(info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Restores the terminal to its original state.
|
||||
pub fn restore() {
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
eprintln!("error disabling raw mode: {err}");
|
||||
}
|
||||
if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) {
|
||||
eprintln!("error leaving alternate screen: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
//! # [Ratatui] `BarChart` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::iter::zip;
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
prelude::{Color, Constraint, Direction, Frame, Layout, Line, Style},
|
||||
style::Stylize,
|
||||
widgets::{Bar, BarChart, BarGroup, Block},
|
||||
};
|
||||
|
||||
use self::terminal::Terminal;
|
||||
|
||||
const COMPANY_COUNT: usize = 3;
|
||||
const PERIOD_COUNT: usize = 4;
|
||||
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
companies: [Company; COMPANY_COUNT],
|
||||
revenues: [Revenues; PERIOD_COUNT],
|
||||
}
|
||||
|
||||
struct Revenues {
|
||||
period: &'static str,
|
||||
revenues: [u32; COMPANY_COUNT],
|
||||
}
|
||||
|
||||
struct Company {
|
||||
short_name: &'static str,
|
||||
name: &'static str,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let mut terminal = terminal::init()?;
|
||||
let app = App::new();
|
||||
app.run(&mut terminal)?;
|
||||
terminal::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
should_exit: false,
|
||||
companies: fake_companies(),
|
||||
revenues: fake_revenues(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut Terminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
self.draw(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
use Constraint::{Fill, Length, Min};
|
||||
let [title, top, bottom] = Layout::vertical([Length(1), Fill(1), Min(20)])
|
||||
.spacing(1)
|
||||
.areas(frame.area());
|
||||
|
||||
frame.render_widget("Grouped Barchart".bold().into_centered_line(), title);
|
||||
frame.render_widget(self.vertical_revenue_barchart(), top);
|
||||
frame.render_widget(self.horizontal_revenue_barchart(), bottom);
|
||||
}
|
||||
|
||||
/// Create a vertical revenue bar chart with the data from the `revenues` field.
|
||||
fn vertical_revenue_barchart(&self) -> BarChart<'_> {
|
||||
let title = Line::from("Company revenues (Vertical)").centered();
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::new().title(title))
|
||||
.bar_gap(0)
|
||||
.bar_width(6)
|
||||
.group_gap(2);
|
||||
for group in self
|
||||
.revenues
|
||||
.iter()
|
||||
.map(|revenue| revenue.to_vertical_bar_group(&self.companies))
|
||||
{
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
barchart
|
||||
}
|
||||
|
||||
/// Create a horizontal revenue bar chart with the data from the `revenues` field.
|
||||
fn horizontal_revenue_barchart(&self) -> BarChart<'_> {
|
||||
let title = Line::from("Company Revenues (Horizontal)").centered();
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::new().title(title))
|
||||
.bar_width(1)
|
||||
.group_gap(2)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal);
|
||||
for group in self
|
||||
.revenues
|
||||
.iter()
|
||||
.map(|revenue| revenue.to_horizontal_bar_group(&self.companies))
|
||||
{
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
barchart
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate fake company data
|
||||
const fn fake_companies() -> [Company; COMPANY_COUNT] {
|
||||
[
|
||||
Company::new("BAKE", "Bake my day", Color::LightRed),
|
||||
Company::new("BITE", "Bits and Bites", Color::Blue),
|
||||
Company::new("TART", "Tart of the Table", Color::White),
|
||||
]
|
||||
}
|
||||
|
||||
/// Some fake revenue data
|
||||
const fn fake_revenues() -> [Revenues; PERIOD_COUNT] {
|
||||
[
|
||||
Revenues::new("Jan", [8500, 6500, 7000]),
|
||||
Revenues::new("Feb", [9000, 7500, 8500]),
|
||||
Revenues::new("Mar", [9500, 4500, 8200]),
|
||||
Revenues::new("Apr", [6300, 4000, 5000]),
|
||||
]
|
||||
}
|
||||
|
||||
impl Revenues {
|
||||
/// Create a new instance of `Revenues`
|
||||
const fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self {
|
||||
Self { period, revenues }
|
||||
}
|
||||
|
||||
/// Create a `BarGroup` with vertical bars for each company
|
||||
fn to_vertical_bar_group<'a>(&self, companies: &'a [Company]) -> BarGroup<'a> {
|
||||
let bars: Vec<Bar> = zip(companies, self.revenues)
|
||||
.map(|(company, revenue)| company.vertical_revenue_bar(revenue))
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(self.period).centered())
|
||||
.bars(&bars)
|
||||
}
|
||||
|
||||
/// Create a `BarGroup` with horizontal bars for each company
|
||||
fn to_horizontal_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> {
|
||||
let bars: Vec<Bar> = zip(companies, self.revenues)
|
||||
.map(|(company, revenue)| company.horizontal_revenue_bar(revenue))
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(self.period).centered())
|
||||
.bars(&bars)
|
||||
}
|
||||
}
|
||||
|
||||
impl Company {
|
||||
/// Create a new instance of `Company`
|
||||
const fn new(short_name: &'static str, name: &'static str, color: Color) -> Self {
|
||||
Self {
|
||||
short_name,
|
||||
name,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vertical revenue bar for the company
|
||||
///
|
||||
/// The label is the short name of the company, and will be displayed under the bar
|
||||
fn vertical_revenue_bar(&self, revenue: u32) -> Bar {
|
||||
let text_value = format!("{:.1}M", f64::from(revenue) / 1000.);
|
||||
Bar::default()
|
||||
.label(self.short_name.into())
|
||||
.value(u64::from(revenue))
|
||||
.text_value(text_value)
|
||||
.style(self.color)
|
||||
.value_style(Style::new().fg(Color::Black).bg(self.color))
|
||||
}
|
||||
|
||||
/// Create a horizontal revenue bar for the company
|
||||
///
|
||||
/// The label is the long name of the company combined with the revenue and will be displayed
|
||||
/// on the bar
|
||||
fn horizontal_revenue_bar(&self, revenue: u32) -> Bar {
|
||||
let text_value = format!("{} ({:.1} M)", self.name, f64::from(revenue) / 1000.);
|
||||
Bar::default()
|
||||
.value(u64::from(revenue))
|
||||
.text_value(text_value)
|
||||
.style(self.color)
|
||||
.value_style(Style::new().fg(Color::Black).bg(self.color))
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains functions common to all examples
|
||||
mod terminal {
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
panic,
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
execute,
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// A type alias to simplify the usage of the terminal and make it easier to change the backend
|
||||
// or choice of writer.
|
||||
pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Initialize the terminal by enabling raw mode and entering the alternate screen.
|
||||
///
|
||||
/// This function should be called before the program starts to ensure that the terminal is in
|
||||
/// the correct state for the application.
|
||||
pub fn init() -> io::Result<Terminal> {
|
||||
install_panic_hook();
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout(), EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Restore the terminal by leaving the alternate screen and disabling raw mode.
|
||||
///
|
||||
/// This function should be called before the program exits to ensure that the terminal is
|
||||
/// restored to its original state.
|
||||
pub fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
stdout(),
|
||||
LeaveAlternateScreen,
|
||||
Clear(ClearType::FromCursorDown),
|
||||
)
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before printing the panic.
|
||||
///
|
||||
/// This prevents error messages from being messed up by the terminal state.
|
||||
fn install_panic_hook() {
|
||||
let panic_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = restore();
|
||||
panic_hook(panic_info);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -13,187 +13,286 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use rand::{thread_rng, Rng};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
prelude::{Color, Line, Style, Stylize},
|
||||
widgets::{Bar, BarChart, BarGroup, Block},
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use self::terminal::Terminal;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
|
||||
};
|
||||
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
temperatures: Vec<u8>,
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
label: &'a str,
|
||||
bar_style: Style,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let mut terminal = terminal::init()?;
|
||||
struct App<'a> {
|
||||
data: Vec<(&'a str, u64)>,
|
||||
months: [&'a str; 4],
|
||||
companies: [Company<'a>; 3],
|
||||
}
|
||||
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> Self {
|
||||
App {
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
("B3", 5),
|
||||
("B4", 8),
|
||||
("B5", 2),
|
||||
("B6", 4),
|
||||
("B7", 5),
|
||||
("B8", 9),
|
||||
("B9", 14),
|
||||
("B10", 15),
|
||||
("B11", 1),
|
||||
("B12", 0),
|
||||
("B13", 4),
|
||||
("B14", 6),
|
||||
("B15", 4),
|
||||
("B16", 6),
|
||||
("B17", 4),
|
||||
("B18", 7),
|
||||
("B19", 13),
|
||||
("B20", 8),
|
||||
("B21", 11),
|
||||
("B22", 9),
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
],
|
||||
companies: [
|
||||
Company {
|
||||
label: "Comp.A",
|
||||
revenue: [9500, 12500, 5300, 8500],
|
||||
bar_style: Style::default().fg(Color::Green),
|
||||
},
|
||||
Company {
|
||||
label: "Comp.B",
|
||||
revenue: [1500, 2500, 3000, 500],
|
||||
bar_style: Style::default().fg(Color::Yellow),
|
||||
},
|
||||
Company {
|
||||
label: "Comp.C",
|
||||
revenue: [10500, 10600, 9000, 4200],
|
||||
bar_style: Style::default().fg(Color::White),
|
||||
},
|
||||
],
|
||||
months: ["Mars", "Apr", "May", "Jun"],
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
app.run(&mut terminal)?;
|
||||
terminal::restore()?;
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let temperatures = (0..24).map(|_| rng.gen_range(50..90)).collect();
|
||||
Self {
|
||||
should_exit: false,
|
||||
temperatures,
|
||||
}
|
||||
}
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
fn run(mut self, terminal: &mut Terminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
self.draw(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut ratatui::Frame) {
|
||||
let [title, vertical, horizontal] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.spacing(1)
|
||||
.areas(frame.area());
|
||||
frame.render_widget("Barchart".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
|
||||
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vertical bar chart from the temperatures data.
|
||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.map(|v| u64::from(*v))
|
||||
.enumerate()
|
||||
.map(|(i, value)| {
|
||||
Bar::default()
|
||||
.value(value)
|
||||
.label(Line::from(format!("{i:>02}:00")))
|
||||
.text_value(format!("{value:>3}°"))
|
||||
.style(temperature_style(value))
|
||||
.value_style(temperature_style(value).reversed())
|
||||
})
|
||||
.collect();
|
||||
let title = Line::from("Weather (Vertical)").centered();
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.block(Block::new().title(title))
|
||||
.bar_width(5)
|
||||
fn ui(frame: &mut Frame, app: &App) {
|
||||
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [top, bottom] = vertical.areas(frame.size());
|
||||
let [left, right] = horizontal.areas(bottom);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
|
||||
frame.render_widget(barchart, top);
|
||||
draw_bar_with_group_labels(frame, app, left);
|
||||
draw_horizontal_bars(frame, app, right);
|
||||
}
|
||||
|
||||
/// Create a horizontal bar chart from the temperatures data.
|
||||
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
app.months
|
||||
.iter()
|
||||
.map(|v| u64::from(*v))
|
||||
.enumerate()
|
||||
.map(|(i, value)| {
|
||||
let style = temperature_style(value);
|
||||
Bar::default()
|
||||
.value(value)
|
||||
.label(Line::from(format!("{i:>02}:00")))
|
||||
.text_value(format!("{value:>3}°"))
|
||||
.style(style)
|
||||
.value_style(style.reversed())
|
||||
.map(|(i, &month)| {
|
||||
let bars: Vec<Bar> = app
|
||||
.companies
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut bar = Bar::default()
|
||||
.value(c.revenue[i])
|
||||
.style(c.bar_style)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(c.bar_style.fg.unwrap())
|
||||
.fg(Color::Black),
|
||||
);
|
||||
|
||||
if combine_values_and_labels {
|
||||
bar = bar.text_value(format!(
|
||||
"{} ({:.1} M)",
|
||||
c.label,
|
||||
(c.revenue[i] as f64) / 1000.
|
||||
));
|
||||
} else {
|
||||
bar = bar
|
||||
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
|
||||
.label(c.label.into());
|
||||
}
|
||||
bar
|
||||
})
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(month).centered())
|
||||
.bars(&bars)
|
||||
})
|
||||
.collect();
|
||||
let title = Line::from("Weather (Horizontal)").centered();
|
||||
BarChart::default()
|
||||
.block(Block::new().title(title))
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, false);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(7)
|
||||
.group_gap(3);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
height: LEGEND_HEIGHT,
|
||||
width: legend_width,
|
||||
y: area.y,
|
||||
x: area.right() - legend_width,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(1)
|
||||
.group_gap(1)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal)
|
||||
}
|
||||
.direction(Direction::Horizontal);
|
||||
|
||||
/// create a yellow to red value based on the value (50-90)
|
||||
fn temperature_style(value: u64) -> Style {
|
||||
let green = (255.0 * (1.0 - (value - 50) as f64 / 40.0)) as u8;
|
||||
let color = Color::Rgb(255, green, 0);
|
||||
Style::new().fg(color)
|
||||
}
|
||||
|
||||
/// Contains functions common to all examples
|
||||
mod terminal {
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
panic,
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
execute,
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// A type alias to simplify the usage of the terminal and make it easier to change the backend
|
||||
// or choice of writer.
|
||||
pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Initialize the terminal by enabling raw mode and entering the alternate screen.
|
||||
///
|
||||
/// This function should be called before the program starts to ensure that the terminal is in
|
||||
/// the correct state for the application.
|
||||
pub fn init() -> io::Result<Terminal> {
|
||||
install_panic_hook();
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout(), EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
for group in groups {
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
/// Restore the terminal by leaving the alternate screen and disabling raw mode.
|
||||
///
|
||||
/// This function should be called before the program exits to ensure that the terminal is
|
||||
/// restored to its original state.
|
||||
pub fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
stdout(),
|
||||
LeaveAlternateScreen,
|
||||
Clear(ClearType::FromCursorDown),
|
||||
)
|
||||
}
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
/// Install a panic hook that restores the terminal before printing the panic.
|
||||
///
|
||||
/// This prevents error messages from being messed up by the terminal state.
|
||||
fn install_panic_hook() {
|
||||
let panic_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = restore();
|
||||
panic_hook(panic_info);
|
||||
}));
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
height: LEGEND_HEIGHT,
|
||||
width: legend_width,
|
||||
y: area.y,
|
||||
x: area.right() - legend_width,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_legend(f: &mut Frame, area: Rect) {
|
||||
let text = vec![
|
||||
Line::from(Span::styled(
|
||||
TOTAL_REVENUE,
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::White),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company A",
|
||||
Style::default().fg(Color::Green),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company B",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company C",
|
||||
Style::default().fg(Color::White),
|
||||
)),
|
||||
];
|
||||
|
||||
let block = Block::bordered().style(Style::default().fg(Color::White));
|
||||
let paragraph = Paragraph::new(text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
@@ -20,22 +20,18 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
prelude::*,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
|
||||
// These type aliases are used to make the code more readable by reducing repetition of the generic
|
||||
@@ -90,7 +86,7 @@ fn handle_events() -> Result<ControlFlow<()>> {
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let (title_area, layout) = calculate_layout(frame.area());
|
||||
let (title_area, layout) = calculate_layout(frame.size());
|
||||
|
||||
render_title(frame, title_area);
|
||||
|
||||
|
||||
@@ -13,20 +13,16 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
||||
Frame, Terminal,
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::calendar::*};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -56,8 +52,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(frame: &mut Frame) {
|
||||
let app_area = frame.area();
|
||||
fn draw(f: &mut Frame) {
|
||||
let app_area = f.size();
|
||||
|
||||
let calarea = Rect {
|
||||
x: app_area.x + 1,
|
||||
@@ -84,7 +80,7 @@ fn draw(frame: &mut Frame) {
|
||||
});
|
||||
for col in cols {
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
frame.render_widget(cal, col);
|
||||
f.render_widget(cal, col);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -170,7 +166,6 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
}
|
||||
|
||||
mod cals {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
|
||||
@@ -13,26 +13,21 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
|
||||
Block, Widget,
|
||||
},
|
||||
Frame, Terminal,
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
@@ -132,7 +127,7 @@ impl App {
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [map, right] = horizontal.areas(frame.area());
|
||||
let [map, right] = horizontal.areas(frame.size());
|
||||
let [pong, boxes] = vertical.areas(right);
|
||||
|
||||
frame.render_widget(self.map_canvas(), map);
|
||||
|
||||
@@ -19,19 +19,14 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols::{self, Marker},
|
||||
text::Span,
|
||||
prelude::*,
|
||||
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -151,18 +146,19 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame, app: &App) {
|
||||
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
|
||||
let [animated_chart, bar_chart] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
|
||||
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
|
||||
let area = frame.size();
|
||||
|
||||
render_animated_chart(frame, animated_chart, app);
|
||||
render_barchart(frame, bar_chart);
|
||||
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||
let [chart1, bottom] = vertical.areas(area);
|
||||
let [line_chart, scatter] = horizontal.areas(bottom);
|
||||
|
||||
render_chart1(frame, chart1, app);
|
||||
render_line_chart(frame, line_chart);
|
||||
render_scatter(frame, scatter);
|
||||
}
|
||||
|
||||
fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
||||
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
@@ -188,7 +184,7 @@ fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(Block::bordered())
|
||||
.block(Block::bordered().title("Chart 1".cyan().bold()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
@@ -200,58 +196,13 @@ fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(["-20".bold(), "0".into(), "20".bold()])
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||
let dataset = Dataset::default()
|
||||
.marker(symbols::Marker::HalfBlock)
|
||||
.style(Style::new().fg(Color::Blue))
|
||||
.graph_type(GraphType::Bar)
|
||||
// a bell curve
|
||||
.data(&[
|
||||
(0., 0.4),
|
||||
(10., 2.9),
|
||||
(20., 13.5),
|
||||
(30., 41.1),
|
||||
(40., 80.1),
|
||||
(50., 100.0),
|
||||
(60., 80.1),
|
||||
(70., 41.1),
|
||||
(80., 13.5),
|
||||
(90., 2.9),
|
||||
(100., 0.4),
|
||||
]);
|
||||
|
||||
let chart = Chart::new(vec![dataset])
|
||||
.block(
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Bar chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 100.0])
|
||||
.labels(["0".bold(), "50".into(), "100.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 100.0])
|
||||
.labels(["0".bold(), "50".into(), "100.0".bold()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
frame.render_widget(chart, bar_chart);
|
||||
}
|
||||
|
||||
fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("Line from only 2 points".italic())
|
||||
@@ -273,14 +224,14 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
.title("X Axis")
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
@@ -323,14 +274,14 @@ fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
.title("Year")
|
||||
.bounds([1960., 2020.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(["1960", "1990", "2020"]),
|
||||
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Cost")
|
||||
.bounds([0., 75000.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(["0", "37 500", "75 000"]),
|
||||
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
|
||||
@@ -23,19 +23,15 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
@@ -70,7 +66,7 @@ fn ui(frame: &mut Frame) {
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(frame.area());
|
||||
.split(frame.size());
|
||||
|
||||
render_named_colors(frame, layout[0]);
|
||||
render_indexed_colors(frame, layout[1]);
|
||||
|
||||
@@ -33,21 +33,13 @@ use std::{
|
||||
};
|
||||
|
||||
use color_eyre::{config::HookBuilder, eyre, Result};
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout, Position, Rect},
|
||||
style::Color,
|
||||
text::Text,
|
||||
widgets::Widget,
|
||||
Terminal,
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
@@ -113,7 +105,7 @@ impl App {
|
||||
/// This is the main event loop for the app.
|
||||
pub fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.size()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -149,7 +141,8 @@ impl App {
|
||||
/// to update the colors to render.
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Constraint::*;
|
||||
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||
Text::from("colors_rgb example. Press q to quit")
|
||||
@@ -224,7 +217,7 @@ impl Widget for &mut ColorsWidget {
|
||||
// pixel below it
|
||||
let fg = colors[yi * 2][xi];
|
||||
let bg = colors[yi * 2 + 1][xi];
|
||||
buf[Position::new(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
self.frame_count += 1;
|
||||
|
||||
@@ -13,30 +13,23 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Flex, Layout, Rect,
|
||||
},
|
||||
style::{
|
||||
palette::tailwind::{BLUE, SKY, SLATE, STONE},
|
||||
Color, Style, Stylize,
|
||||
},
|
||||
symbols::{self, line},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Paragraph, Widget, Wrap},
|
||||
Terminal,
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind::*,
|
||||
symbols::line,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
@@ -125,28 +118,29 @@ impl App {
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
use KeyCode::*;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
|
||||
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
KeyCode::Char('x') => self.delete_block(),
|
||||
KeyCode::Char('a') => self.insert_block(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
|
||||
Char('q') | Esc => self.exit(),
|
||||
Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||
Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||
Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||
Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||
Char('+') => self.increment_spacing(),
|
||||
Char('-') => self.decrement_spacing(),
|
||||
Char('x') => self.delete_block(),
|
||||
Char('a') => self.insert_block(),
|
||||
Char('k') | Up => self.increment_value(),
|
||||
Char('j') | Down => self.decrement_value(),
|
||||
Char('h') | Left => self.prev_block(),
|
||||
Char('l') | Right => self.next_block(),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
|
||||
@@ -13,30 +13,17 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Layout, Rect,
|
||||
},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||
Tabs, Widget,
|
||||
},
|
||||
Terminal,
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const SPACER_HEIGHT: u16 = 0;
|
||||
@@ -86,6 +73,9 @@ fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
// increase the cache size to avoid flickering for indeterminate layouts
|
||||
Layout::init_cache(100);
|
||||
|
||||
App::default().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
@@ -112,23 +102,24 @@ impl App {
|
||||
}
|
||||
|
||||
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
use KeyCode::*;
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
Char('h') | Left => self.previous(),
|
||||
Char('j') | Down => self.down(),
|
||||
Char('k') | Up => self.up(),
|
||||
Char('g') | Home => self.top(),
|
||||
Char('G') | End => self.bottom(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -247,7 +238,7 @@ impl App {
|
||||
for (i, cell) in visible_content.enumerate() {
|
||||
let x = i as u16 % area.width;
|
||||
let y = i as u16 / area.width;
|
||||
buf[(area.x + x, area.y + y)] = cell;
|
||||
*buf.get_mut(area.x + x, area.y + y) = cell;
|
||||
}
|
||||
|
||||
if scrollbar_needed {
|
||||
|
||||
@@ -15,23 +15,15 @@
|
||||
|
||||
use std::{error::Error, io, ops::ControlFlow, time::Duration};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Line,
|
||||
widgets::{Paragraph, Widget},
|
||||
Frame, Terminal,
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A custom widget that renders a button with a label, theme and state.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -203,7 +195,7 @@ fn ui(frame: &mut Frame, states: [State; 3]) {
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [title, buttons, help, _] = vertical.areas(frame.area());
|
||||
let [title, buttons, help, _] = vertical.areas(frame.size());
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||
|
||||
@@ -122,8 +122,8 @@ pub struct TabsState<'a> {
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> Self {
|
||||
Self { titles, index: 0 }
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
|
||||
@@ -4,15 +4,12 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
Terminal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
},
|
||||
Terminal,
|
||||
use ratatui::prelude::*;
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
@@ -3,13 +3,10 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::TermwizBackend,
|
||||
termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
},
|
||||
Terminal,
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{self, Span},
|
||||
widgets::{
|
||||
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
|
||||
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
|
||||
Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.area());
|
||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.size());
|
||||
let tabs = app
|
||||
.tabs
|
||||
.titles
|
||||
@@ -83,7 +76,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::new().title("LineGauge:"))
|
||||
.filled_style(Style::default().fg(Color::Magenta))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
@@ -221,7 +214,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels([
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Color,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Tabs, Widget},
|
||||
Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
use crate::{
|
||||
destroy,
|
||||
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
|
||||
term, THEME,
|
||||
};
|
||||
use crate::{destroy, tabs::*, term, THEME};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct App {
|
||||
@@ -71,7 +59,7 @@ impl App {
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.render_widget(self, frame.area());
|
||||
frame.render_widget(self, frame.size());
|
||||
if self.mode == Mode::Destroy {
|
||||
destroy::destroy(frame);
|
||||
}
|
||||
@@ -94,13 +82,14 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_key_press(&mut self, key: KeyEvent) {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.prev(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
|
||||
Char('q') | Esc => self.mode = Mode::Quit,
|
||||
Char('h') | Left => self.prev_tab(),
|
||||
Char('l') | Right => self.next_tab(),
|
||||
Char('k') | Up => self.prev(),
|
||||
Char('j') | Down => self.next(),
|
||||
Char('d') | Delete => self.destroy(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
815
examples/demo2/big_text.rs
Normal file
815
examples/demo2/big_text.rs
Normal file
@@ -0,0 +1,815 @@
|
||||
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
|
||||
//! glyphs from the [font8x8] crate.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! # Installation
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui tui-big-text
|
||||
//! ```
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Create a [`BigText`] widget using `BigTextBuilder` and pass it to [`Frame::render_widget`] to
|
||||
//! render be rendered. The builder allows you to customize the [`Style`] of the widget and the
|
||||
//! [`PixelSize`] of the glyphs. The [`PixelSize`] can be used to control how many character cells
|
||||
//! are used to represent a single pixel of the 8x8 font.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use anyhow::Result;
|
||||
//! use ratatui::prelude::*;
|
||||
//! use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
//!
|
||||
//! fn render(frame: &mut Frame) -> Result<()> {
|
||||
//! let big_text = BigTextBuilder::default()
|
||||
//! .pixel_size(PixelSize::Full)
|
||||
//! .style(Style::new().blue())
|
||||
//! .lines(vec![
|
||||
//! "Hello".red().into(),
|
||||
//! "World".white().into(),
|
||||
//! "~~~~~".into(),
|
||||
//! ])
|
||||
//! .build()?;
|
||||
//! frame.render_widget(big_text, frame.size());
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [tui-big-text]: https://crates.io/crates/tui-big-text
|
||||
//! [Ratatui]: https://crates.io/crates/ratatui
|
||||
//! [font8x8]: https://crates.io/crates/font8x8
|
||||
//! [`BigText`]: crate::BigText
|
||||
//! [`PixelSize`]: crate::PixelSize
|
||||
//! [`Frame::render_widget`]: ratatui::Frame::render_widget
|
||||
//! [`Style`]: ratatui::style::Style
|
||||
|
||||
use std::cmp::min;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use font8x8::UnicodeFonts;
|
||||
use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub enum PixelSize {
|
||||
#[default]
|
||||
/// A pixel from the 8x8 font is represented by a full character cell in the terminal.
|
||||
Full,
|
||||
/// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the
|
||||
/// terminal.
|
||||
HalfHeight,
|
||||
/// A pixel from the 8x8 font is represented by a half (left/right) character cell in the
|
||||
/// terminal.
|
||||
HalfWidth,
|
||||
/// A pixel from the 8x8 font is represented by a quadrant of a character cell in the terminal.
|
||||
Quadrant,
|
||||
}
|
||||
|
||||
/// Displays one or more lines of text using 8x8 pixel characters.
|
||||
///
|
||||
/// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate.
|
||||
///
|
||||
/// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be.
|
||||
/// Currently a pixel of the 8x8 font can be represented by one full or half
|
||||
/// (horizontal/vertical/both) character cell of the terminal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
///
|
||||
/// BigText::builder()
|
||||
/// .pixel_size(PixelSize::Full)
|
||||
/// .style(Style::new().white())
|
||||
/// .lines(vec![
|
||||
/// "Hello".red().into(),
|
||||
/// "World".blue().into(),
|
||||
/// "=====".into(),
|
||||
/// ])
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// Renders:
|
||||
///
|
||||
/// ```plain
|
||||
/// ██ ██ ███ ███
|
||||
/// ██ ██ ██ ██
|
||||
/// ██ ██ ████ ██ ██ ████
|
||||
/// ██████ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ██████ ██ ██ ██ ██
|
||||
/// ██ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ████ ████ ████ ████
|
||||
///
|
||||
/// ██ ██ ███ ███
|
||||
/// ██ ██ ██ ██
|
||||
/// ██ ██ ████ ██ ███ ██ ██
|
||||
/// ██ █ ██ ██ ██ ███ ██ ██ █████
|
||||
/// ███████ ██ ██ ██ ██ ██ ██ ██
|
||||
/// ███ ███ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ████ ████ ████ ███ ██
|
||||
///
|
||||
/// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██
|
||||
/// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███
|
||||
/// ```
|
||||
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct BigText<'a> {
|
||||
/// The text to display
|
||||
#[builder(setter(into))]
|
||||
lines: Vec<Line<'a>>,
|
||||
|
||||
/// The style of the widget
|
||||
///
|
||||
/// Defaults to `Style::default()`
|
||||
#[builder(default)]
|
||||
style: Style,
|
||||
|
||||
/// The size of single glyphs
|
||||
///
|
||||
/// Defaults to `BigTextSize::default()` (=> `BigTextSize::Full`)
|
||||
#[builder(default)]
|
||||
pixel_size: PixelSize,
|
||||
}
|
||||
|
||||
impl Widget for BigText<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = layout(area, self.pixel_size);
|
||||
for (line, line_layout) in self.lines.iter().zip(layout) {
|
||||
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
|
||||
render_symbol(&g, cell, buf, self.pixel_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
|
||||
const fn cells_per_glyph(size: PixelSize) -> (u16, u16) {
|
||||
match size {
|
||||
PixelSize::Full => (8, 8),
|
||||
PixelSize::HalfHeight => (8, 4),
|
||||
PixelSize::HalfWidth => (4, 8),
|
||||
PixelSize::Quadrant => (4, 4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s
|
||||
/// representing the rows of cells.
|
||||
/// The size of each cell depends on given font size
|
||||
fn layout(
|
||||
area: Rect,
|
||||
pixel_size: PixelSize,
|
||||
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
(area.top()..area.bottom())
|
||||
.step_by(height as usize)
|
||||
.map(move |y| {
|
||||
(area.left()..area.right())
|
||||
.step_by(width as usize)
|
||||
.map(move |x| {
|
||||
let width = min(area.right() - x, width);
|
||||
let height = min(area.bottom() - y, height);
|
||||
Rect::new(x, y, width, height)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
|
||||
/// `BITMAPS` array and setting the corresponding cells in the buffer.
|
||||
fn render_symbol(grapheme: &StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
|
||||
buf.set_style(area, grapheme.style);
|
||||
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
|
||||
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
|
||||
render_glyph(glyph, area, buf, pixel_size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two vertical "pixels"
|
||||
const fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
match top {
|
||||
0 => match bottom {
|
||||
0 => ' ',
|
||||
_ => '▄',
|
||||
},
|
||||
_ => match bottom {
|
||||
0 => '▀',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two horizontal "pixels"
|
||||
const fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
match left {
|
||||
0 => match right {
|
||||
0 => ' ',
|
||||
_ => '▐',
|
||||
},
|
||||
_ => match right {
|
||||
0 => '▌',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for 2x2 "pixels"
|
||||
const fn get_symbol_half_size(
|
||||
top_left: u8,
|
||||
top_right: u8,
|
||||
bottom_left: u8,
|
||||
bottom_right: u8,
|
||||
) -> char {
|
||||
const QUADRANT_SYMBOLS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 << 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 << 2 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 << 3 } else { 0 };
|
||||
|
||||
QUADRANT_SYMBOLS[top_left + top_right + bottom_left + bottom_right]
|
||||
}
|
||||
|
||||
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
|
||||
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
|
||||
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);
|
||||
let glyph_horizontal_bit_selector = (0..8).step_by(8 / width as usize);
|
||||
|
||||
for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) {
|
||||
for (col, x) in glyph_horizontal_bit_selector
|
||||
.clone()
|
||||
.zip(area.left()..area.right())
|
||||
{
|
||||
let cell = buf.get_mut(x, y);
|
||||
let symbol_character = match pixel_size {
|
||||
PixelSize::Full => match glyph[row] & (1 << col) {
|
||||
0 => ' ',
|
||||
_ => '█',
|
||||
},
|
||||
PixelSize::HalfHeight => {
|
||||
let top = glyph[row] & (1 << col);
|
||||
let bottom = glyph[row + 1] & (1 << col);
|
||||
get_symbol_half_height(top, bottom)
|
||||
}
|
||||
PixelSize::HalfWidth => {
|
||||
let left = glyph[row] & (1 << col);
|
||||
let right = glyph[row] & (1 << (col + 1));
|
||||
get_symbol_half_width(left, right)
|
||||
}
|
||||
PixelSize::Quadrant => {
|
||||
let top_left = glyph[row] & (1 << col);
|
||||
let top_right = glyph[row] & (1 << (col + 1));
|
||||
let bottom_left = glyph[row + 1] & (1 << col);
|
||||
let bottom_right = glyph[row + 1] & (1 << (col + 1));
|
||||
get_symbol_half_size(top_left, top_right, bottom_left, bottom_right)
|
||||
}
|
||||
};
|
||||
cell.set_char(symbol_character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[test]
|
||||
fn build() -> Result<()> {
|
||||
let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])];
|
||||
let style = Style::new().green();
|
||||
let pixel_size = PixelSize::default();
|
||||
assert_eq!(
|
||||
BigTextBuilder::default()
|
||||
.lines(lines.clone())
|
||||
.style(style)
|
||||
.build()?,
|
||||
BigText {
|
||||
lines,
|
||||
style,
|
||||
pixel_size
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ ██ ███ ████ ██ ",
|
||||
"██ ██ ██ ██ ",
|
||||
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
|
||||
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
|
||||
" █████ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"██████ █ ███",
|
||||
"█ ██ █ ██ ██",
|
||||
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
|
||||
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"██ ██ ███ █ ██ ",
|
||||
"███ ███ ██ ██ ",
|
||||
"███████ ██ ██ ██ █████ ███ ",
|
||||
"███████ ██ ██ ██ ██ ██ ",
|
||||
"██ █ ██ ██ ██ ██ ██ ██ ",
|
||||
"██ ██ ██ ██ ██ ██ █ ██ ",
|
||||
"██ ██ ███ ██ ████ ██ ████ ",
|
||||
" ",
|
||||
"████ ██ ",
|
||||
" ██ ",
|
||||
" ██ ███ █████ ████ █████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ██ █ ██ ██ ██ ██████ ████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
"███████ ████ ██ ██ ████ █████ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ █ ███ ███ ".bold(),
|
||||
"██ ██ ██ ██ ██ ".bold(),
|
||||
"███ █████ ██ ██ ██ ████ ██ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
|
||||
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
|
||||
" █████ ".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ████ ██ ",
|
||||
" █████ ██ ██ █████ ",
|
||||
" ██ ██ ██████ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ",
|
||||
"███ ██ ████ ███ ██ ",
|
||||
" ",
|
||||
" ████ ",
|
||||
" ██ ██ ",
|
||||
"██ ██ ███ ████ ████ █████ ",
|
||||
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" █████ ████ ████ ████ ██ ██ ",
|
||||
" ",
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ████ ",
|
||||
" █████ ██ ██ ██ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ██████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ",
|
||||
"██████ ████ ███ ██ ████ ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
|
||||
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
|
||||
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
|
||||
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"█▀██▀█ ▄█ ▀██",
|
||||
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
|
||||
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
|
||||
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
|
||||
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
|
||||
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
|
||||
"▀██▀ ▀▀ ",
|
||||
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
|
||||
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
|
||||
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
|
||||
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
|
||||
" ▄█▀▀█▄ ",
|
||||
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
|
||||
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
|
||||
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
|
||||
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
|
||||
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ █ ▐█ ██ █ ",
|
||||
"█ █ █ ▐▌ ",
|
||||
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
|
||||
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
|
||||
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
|
||||
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
|
||||
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
|
||||
" ██▌ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"███ ▐ ▐█",
|
||||
"▌█▐ █ █",
|
||||
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
|
||||
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
|
||||
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
|
||||
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"█ ▐▌ ▐█ ▐ █ ",
|
||||
"█▌█▌ █ █ ",
|
||||
"███▌█ █ █ ▐██ ▐█ ",
|
||||
"███▌█ █ █ █ █ ",
|
||||
"█▐▐▌█ █ █ █ █ ",
|
||||
"█ ▐▌█ █ █ █▐ █ ",
|
||||
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
|
||||
" ",
|
||||
"██ █ ",
|
||||
"▐▌ ",
|
||||
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
|
||||
"▐▌ █ █ █ █ █ █ ",
|
||||
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
|
||||
"▐▌▐▌ █ █ █ █ █ ",
|
||||
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ ▐ ▐█ ▐█ ".bold(),
|
||||
"█ █ █ █ █ ".bold(),
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
|
||||
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
|
||||
" ▐█ █ █ █ █ ███ █ █ ".bold(),
|
||||
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
|
||||
" ██▌ ".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌▐█▌ █ ",
|
||||
"▐██ █ █ ▐██ ",
|
||||
"▐▌█ ███ █ █ ",
|
||||
"▐▌▐▌█ █ █ ",
|
||||
"█▌▐▌▐█▌ ▐█▐▌ ",
|
||||
" ",
|
||||
" ██ ",
|
||||
"▐▌▐▌ ",
|
||||
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
|
||||
"█ ▐█▐▌█ █ █ █ █ █ ",
|
||||
"█ █▌▐▌▐▌███ ███ █ █ ",
|
||||
"▐▌▐▌▐▌ █ █ █ █ ",
|
||||
" ██▌██ ▐█▌ ▐█▌ █ █ ",
|
||||
" ",
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌ █ █ █ ▐█▌ ",
|
||||
"▐██ █ █ █ █ █ ",
|
||||
"▐▌▐▌ █ █ █ ███ ",
|
||||
"▐▌▐▌ █ █ █ █ ",
|
||||
"███ ▐█▌ ▐█▐▌▐█▌ ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_half_size_symbols() -> Result<()> {
|
||||
assert_eq!(get_symbol_half_size(0, 0, 0, 0), ' ');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 0, 0), '▘');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 0, 0), '▝');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 0, 0), '▀');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 1, 0), '▖');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 1, 0), '▌');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 1, 0), '▞');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 1, 0), '▛');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 0, 1), '▗');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 0, 1), '▚');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 0, 1), '▐');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 0, 1), '▜');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 1, 1), '▄');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 1, 1), '▙');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 1, 1), '▟');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 1, 1), '█');
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
|
||||
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
|
||||
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
|
||||
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▛█▜ ▟ ▝█",
|
||||
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
|
||||
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"█▖▟▌ ▝█ ▟ ▀ ",
|
||||
"███▌█ █ █ ▝█▀ ▝█ ",
|
||||
"█▝▐▌█ █ █ █▗ █ ",
|
||||
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
|
||||
"▜▛ ▀ ",
|
||||
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
|
||||
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
|
||||
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▜▛▜▖ ▝█ ",
|
||||
"▐▙▟▘▟▀▙ ▗▄█ ",
|
||||
"▐▌▜▖█▀▀ █ █ ",
|
||||
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
|
||||
"▗▛▜▖ ",
|
||||
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
|
||||
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
|
||||
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
|
||||
"▜▛▜▖▝█ ",
|
||||
"▐▙▟▘ █ █ █ ▟▀▙ ",
|
||||
"▐▌▐▌ █ █ █ █▀▀ ",
|
||||
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use palette::{IntoColor, Okhsv, Srgb};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
/// A widget that renders a color swatch of RGB colors.
|
||||
///
|
||||
@@ -19,7 +19,7 @@ impl Widget for RgbSwatch {
|
||||
let hue = xi as f32 * 360.0 / f32::from(area.width);
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
|
||||
buf[(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use rand::Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Flex, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
text::Text,
|
||||
widgets::Widget,
|
||||
Frame,
|
||||
};
|
||||
use ratatui::{buffer::Cell, layout::Flex, prelude::*};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::big_text::{BigTextBuilder, PixelSize};
|
||||
|
||||
/// delay the start of the animation so it doesn't start immediately
|
||||
const DELAY: usize = 120;
|
||||
const DELAY: usize = 240;
|
||||
/// higher means more pixels per frame are modified in the animation
|
||||
const DRIP_SPEED: usize = 500;
|
||||
const DRIP_SPEED: usize = 50;
|
||||
/// delay the start of the text animation so it doesn't start immediately after the initial delay
|
||||
const TEXT_DELAY: usize = 180;
|
||||
const TEXT_DELAY: usize = 240;
|
||||
|
||||
/// Destroy mode activated by pressing `d`
|
||||
pub fn destroy(frame: &mut Frame<'_>) {
|
||||
@@ -23,7 +19,7 @@ pub fn destroy(frame: &mut Frame<'_>) {
|
||||
return;
|
||||
}
|
||||
|
||||
let area = frame.area();
|
||||
let area = frame.size();
|
||||
let buf = frame.buffer_mut();
|
||||
|
||||
drip(frame_count, area, buf);
|
||||
@@ -49,7 +45,7 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
for _ in 0..pixel_count {
|
||||
let src_x = rng.gen_range(0..area.width);
|
||||
let src_y = rng.gen_range(1..area.height - 2);
|
||||
let src = buf[(src_x, src_y)].clone();
|
||||
let src = buf.get_mut(src_x, src_y).clone();
|
||||
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
|
||||
if rng.gen_ratio(1, 100) {
|
||||
let dest_x = rng
|
||||
@@ -57,20 +53,21 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
.clamp(area.left(), area.right() - 1);
|
||||
let dest_y = area.top() + 1;
|
||||
|
||||
let dest = &mut buf[(dest_x, dest_y)];
|
||||
let dest = buf.get_mut(dest_x, dest_y);
|
||||
// copy the cell to the new location about 1/10 of the time blank out the cell the rest
|
||||
// of the time. This has the effect of gradually removing the pixels from the screen.
|
||||
if rng.gen_ratio(1, 10) {
|
||||
*dest = src;
|
||||
} else {
|
||||
dest.reset();
|
||||
*dest = Cell::default();
|
||||
}
|
||||
} else {
|
||||
// move the pixel down one row
|
||||
let dest_x = src_x;
|
||||
let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
|
||||
// copy the cell to the new location
|
||||
buf[(dest_x, dest_y)] = src;
|
||||
let dest = buf.get_mut(dest_x, dest_y);
|
||||
*dest = src;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,25 +80,26 @@ fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let logo = indoc::indoc! {"
|
||||
██████ ████ ██████ ████ ██████ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██████ ████████ ██ ████████ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ████ ██
|
||||
"};
|
||||
let logo_text = Text::styled(logo, Color::Rgb(255, 255, 255));
|
||||
let area = centered_rect(area, logo_text.width() as u16, logo_text.height() as u16);
|
||||
let line = "RATATUI";
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines([line.into()])
|
||||
.pixel_size(PixelSize::Full)
|
||||
.style(Style::new().fg(Color::Rgb(255, 0, 0)))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// the font size is 8x8 for each character and we have 1 line
|
||||
let area = centered_rect(area, line.width() as u16 * 8, 8);
|
||||
|
||||
let mask_buf = &mut Buffer::empty(area);
|
||||
logo_text.render(area, mask_buf);
|
||||
big_text.render(area, mask_buf);
|
||||
|
||||
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
|
||||
|
||||
for row in area.rows() {
|
||||
for col in row.columns() {
|
||||
let cell = &mut buf[(col.x, col.y)];
|
||||
let mask_cell = &mut mask_buf[(col.x, col.y)];
|
||||
let cell = buf.get_mut(col.x, col.y);
|
||||
let mask_cell = mask_buf.get(col.x, col.y);
|
||||
cell.set_symbol(mask_cell.symbol());
|
||||
|
||||
// blend the mask cell color with the cell color
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(
|
||||
clippy::enum_glob_use,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate
|
||||
clippy::must_use_candidate,
|
||||
clippy::wildcard_imports
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod big_text;
|
||||
mod colors;
|
||||
mod destroy;
|
||||
mod errors;
|
||||
@@ -27,17 +30,16 @@ mod tabs;
|
||||
mod term;
|
||||
mod theme;
|
||||
|
||||
pub use app::*;
|
||||
use color_eyre::Result;
|
||||
|
||||
pub use self::{
|
||||
colors::{color_from_oklab, RgbSwatch},
|
||||
theme::THEME,
|
||||
};
|
||||
pub use colors::*;
|
||||
pub use term::*;
|
||||
pub use theme::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
errors::init_hooks()?;
|
||||
let terminal = &mut term::init()?;
|
||||
app::run(terminal)?;
|
||||
App::default().run(terminal)?;
|
||||
term::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
@@ -68,16 +64,20 @@ impl Widget for AboutTab {
|
||||
}
|
||||
|
||||
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
});
|
||||
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 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 \
|
||||
@@ -108,7 +108,7 @@ pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
} else {
|
||||
THEME.logo.rat_eye_alt
|
||||
};
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
@@ -116,7 +116,7 @@ pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
|
||||
let x = area.left() + x as u16;
|
||||
let y = area.top() + y as u16;
|
||||
let cell = &mut buf[(x, y)];
|
||||
let cell = buf.get_mut(x, y);
|
||||
let rat_color = THEME.logo.rat;
|
||||
let term_color = THEME.logo.term;
|
||||
match (ch1, ch2) {
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{Styled, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
|
||||
Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
|
||||
},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -68,7 +59,7 @@ impl EmailTab {
|
||||
impl Widget for EmailTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
StatefulWidget, Table, TableState, Widget, Wrap,
|
||||
},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
@@ -114,7 +105,7 @@ impl RecipeTab {
|
||||
impl Widget for RecipeTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
@@ -133,7 +124,7 @@ impl Widget for RecipeTab {
|
||||
};
|
||||
render_scrollbar(self.row_index, scrollbar_area, buf);
|
||||
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Styled, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{self, Canvas, Map, MapResolution, Points},
|
||||
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
Sparkline, StatefulWidget, Table, TableState, Widget,
|
||||
},
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -33,7 +26,7 @@ impl TracerouteTab {
|
||||
impl Widget for TracerouteTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
@@ -111,7 +104,7 @@ fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let theme = THEME.traceroute.map;
|
||||
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
|
||||
let map = Map {
|
||||
resolution: MapResolution::High,
|
||||
resolution: canvas::MapResolution::High,
|
||||
color: theme.color,
|
||||
};
|
||||
Canvas::default()
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
use itertools::Itertools;
|
||||
use palette::Okhsv;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols,
|
||||
widgets::{
|
||||
calendar::{CalendarEventStore, Monthly},
|
||||
Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget,
|
||||
},
|
||||
prelude::*,
|
||||
widgets::{calendar::CalendarEventStore, *},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -34,14 +28,14 @@ impl WeatherTab {
|
||||
impl Widget for WeatherTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
|
||||
let area = area.inner(Margin {
|
||||
let area = area.inner(&Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
@@ -65,7 +59,7 @@ impl Widget for WeatherTab {
|
||||
|
||||
fn render_calendar(area: Rect, buf: &mut Buffer) {
|
||||
let date = OffsetDateTime::now_utc().date();
|
||||
Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
|
||||
calendar::Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
|
||||
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||
.show_month_header(Style::new().bold())
|
||||
.show_weekdays_header(Style::new().italic())
|
||||
@@ -146,8 +140,8 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||
let hue = 90.0 - (percent as f32 * 0.6);
|
||||
let value = Okhsv::max_value();
|
||||
let filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let label = if percent < 100.0 {
|
||||
format!("Downloading: {percent}%")
|
||||
} else {
|
||||
@@ -157,8 +151,7 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
.ratio(percent / 100.0)
|
||||
.label(label)
|
||||
.style(Style::new().light_blue())
|
||||
.filled_style(Style::new().fg(filled_color))
|
||||
.unfilled_style(Style::new().fg(unfilled_color))
|
||||
.gauge_style(Style::new().fg(fg).bg(bg))
|
||||
.line_set(symbols::line::THICK)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
@@ -4,16 +4,12 @@ use std::{
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::Rect,
|
||||
Terminal, TerminalOptions, Viewport,
|
||||
use crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
pub fn init() -> Result<Terminal<impl Backend>> {
|
||||
// this size is to match the size of the terminal when running the demo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
pub struct Theme {
|
||||
pub root: Style,
|
||||
|
||||
@@ -15,18 +15,13 @@
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
/// Example code for lib.rs
|
||||
@@ -57,10 +52,11 @@ fn main() -> io::Result<()> {
|
||||
fn hello_world(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.area(),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
fn handle_events() -> io::Result<bool> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
@@ -79,7 +75,7 @@ fn layout(frame: &mut Frame) {
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
|
||||
let [title_bar, main_area, status_bar] = vertical.areas(frame.area());
|
||||
let [title_bar, main_area, status_bar] = vertical.areas(frame.size());
|
||||
let [left, right] = horizontal.areas(main_area);
|
||||
|
||||
frame.render_widget(
|
||||
@@ -102,7 +98,7 @@ fn styling(frame: &mut Frame) {
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(frame.area());
|
||||
.split(frame.size());
|
||||
|
||||
let span1 = Span::raw("Hello ");
|
||||
let span2 = Span::styled(
|
||||
|
||||
@@ -13,33 +13,22 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
num::NonZeroUsize,
|
||||
};
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{
|
||||
Alignment,
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Flex, Layout, Rect,
|
||||
},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
symbols::{self, line},
|
||||
text::{Line, Text},
|
||||
widgets::{
|
||||
block::Title, Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
StatefulWidget, Tabs, Widget,
|
||||
},
|
||||
Terminal,
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
symbols::line,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
@@ -159,9 +148,7 @@ enum SelectedTab {
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// assuming the user changes spacing about a 100 times or so
|
||||
Layout::init_cache(
|
||||
NonZeroUsize::new(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100).unwrap(),
|
||||
);
|
||||
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
@@ -185,22 +172,23 @@ impl App {
|
||||
}
|
||||
|
||||
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
use KeyCode::*;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
Char('h') | Left => self.previous(),
|
||||
Char('j') | Down => self.down(),
|
||||
Char('k') | Up => self.up(),
|
||||
Char('g') | Home => self.top(),
|
||||
Char('G') | End => self.bottom(),
|
||||
Char('+') => self.increment_spacing(),
|
||||
Char('-') => self.decrement_spacing(),
|
||||
_ => (),
|
||||
},
|
||||
_ => {}
|
||||
@@ -346,7 +334,7 @@ impl App {
|
||||
for (i, cell) in visible_content.enumerate() {
|
||||
let x = i as u16 % area.width;
|
||||
let y = i as u16 / area.width;
|
||||
buf[(area.x + x, area.y + y)] = cell;
|
||||
*buf.get_mut(area.x + x, area.y + y) = cell;
|
||||
}
|
||||
|
||||
if scrollbar_needed {
|
||||
@@ -376,7 +364,7 @@ impl SelectedTab {
|
||||
|
||||
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
use tailwind::{INDIGO, ORANGE, SKY};
|
||||
use tailwind::*;
|
||||
let text = value.to_string();
|
||||
let color = match value {
|
||||
Self::Legacy => ORANGE.c400,
|
||||
@@ -521,7 +509,7 @@ impl Example {
|
||||
}
|
||||
|
||||
const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
use tailwind::{BLUE, SLATE};
|
||||
use tailwind::*;
|
||||
match constraint {
|
||||
Constraint::Min(_) => BLUE.c900,
|
||||
Constraint::Max(_) => BLUE.c800,
|
||||
|
||||
@@ -13,22 +13,20 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Style, Stylize},
|
||||
text::Span,
|
||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph, Widget},
|
||||
Terminal,
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
|
||||
};
|
||||
|
||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||
@@ -74,7 +72,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.area()))?;
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -101,9 +99,10 @@ impl App {
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
Char(' ') | Enter => self.start(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +123,7 @@ impl App {
|
||||
impl Widget for &App {
|
||||
#[allow(clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
use Constraint::*;
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, gauge_area, footer_area] = layout.areas(area);
|
||||
|
||||
|
||||
@@ -18,36 +18,33 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
widgets::Paragraph,
|
||||
Frame, Terminal,
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
/// teardown of a terminal application.
|
||||
///
|
||||
/// This example does not handle events or update the application state. It just draws a greeting
|
||||
/// and exits when the user presses 'q'.
|
||||
/// A more robust application would probably want to handle errors and ensure that the terminal is
|
||||
/// restored to a sane state before exiting. This example does not do that. It also does not handle
|
||||
/// events or update the application state. It just draws a greeting and exits when the user
|
||||
/// presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?; // augment errors / panics with easy to read messages
|
||||
let mut terminal = init_terminal().context("setup failed")?;
|
||||
let result = run(&mut terminal).context("app loop failed");
|
||||
restore_terminal();
|
||||
result
|
||||
let mut terminal = setup_terminal().context("setup failed")?;
|
||||
run(&mut terminal).context("app loop failed")?;
|
||||
restore_terminal(&mut terminal).context("restore terminal failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
|
||||
/// hide the cursor. This example does not handle errors.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
set_panic_hook();
|
||||
/// hide the cursor. This example does not handle errors. A more robust application would probably
|
||||
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode().context("failed to enable raw mode")?;
|
||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
||||
@@ -56,23 +53,11 @@ fn init_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
|
||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
||||
/// the cursor.
|
||||
fn restore_terminal() {
|
||||
// There's not a lot we can do if these fail, so we just print an error message.
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
eprintln!("Error disabling raw mode: {err}");
|
||||
}
|
||||
if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) {
|
||||
eprintln!("Error leaving alternate screen: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the default panic hook with one that restores the terminal before panicking.
|
||||
fn set_panic_hook() {
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
restore_terminal();
|
||||
hook(panic_info);
|
||||
}));
|
||||
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode().context("failed to disable raw mode")?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
||||
.context("unable to switch to main screen")?;
|
||||
terminal.show_cursor().context("unable to show cursor")
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
@@ -81,7 +66,7 @@ fn set_panic_hook() {
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render_app)?;
|
||||
terminal.draw(crate::render_app)?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
@@ -93,15 +78,13 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
/// draws a greeting.
|
||||
fn render_app(frame: &mut Frame) {
|
||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.area());
|
||||
frame.render_widget(greeting, frame.size());
|
||||
}
|
||||
|
||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
||||
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
|
||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms. This allows you
|
||||
/// to do other work in the application loop, such as updating the application state, without
|
||||
/// blocking the event loop for too long.
|
||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
//! # [Ratatui] Hyperlink examplew
|
||||
//!
|
||||
//! Shows how to use [OSC 8] to create hyperlinks in the terminal.
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
panic,
|
||||
};
|
||||
|
||||
use color_eyre::{
|
||||
config::{EyreHook, HookBuilder, PanicHook},
|
||||
eyre, Result,
|
||||
};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::WidgetRef};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_handling()?;
|
||||
let mut terminal = init_terminal()?;
|
||||
let app = App::new();
|
||||
app.run(&mut terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct App {
|
||||
hyperlink: Hyperlink<'static>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
|
||||
let hyperlink = Hyperlink::new(text, "https://example.com");
|
||||
Self { hyperlink }
|
||||
}
|
||||
|
||||
fn run(self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
|
||||
///
|
||||
/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
struct Hyperlink<'content> {
|
||||
text: Text<'content>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl<'content> Hyperlink<'content> {
|
||||
fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
url: url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Hyperlink<'_> {
|
||||
fn render_ref(&self, area: Rect, buffer: &mut Buffer) {
|
||||
self.text.render_ref(area, buffer);
|
||||
|
||||
// this is a hacky workaround for https://github.com/ratatui-org/ratatui/issues/902, a bug
|
||||
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
|
||||
// works by rendering the hyperlink as a series of 2-character chunks, which is the
|
||||
// calculated width of the hyperlink text.
|
||||
for (i, two_chars) in self
|
||||
.text
|
||||
.to_string()
|
||||
.chars()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let text = two_chars.collect::<String>();
|
||||
let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
|
||||
buffer[(area.x + i as u16 * 2, area.y)].set_symbol(hyperlink.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the terminal with raw mode and alternate screen.
|
||||
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
Terminal::new(CrosstermBackend::new(stdout()))
|
||||
}
|
||||
|
||||
/// Restore the terminal to its original state.
|
||||
fn restore_terminal() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(stdout(), LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize error handling with color-eyre.
|
||||
pub fn init_error_handling() -> Result<()> {
|
||||
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
|
||||
set_panic_hook(panic_hook);
|
||||
set_error_hook(eyre_hook)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before printing the panic.
|
||||
fn set_panic_hook(panic_hook: PanicHook) {
|
||||
let panic_hook = panic_hook.into_panic_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = restore_terminal();
|
||||
panic_hook(panic_info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Install an error hook that restores the terminal before printing the error.
|
||||
fn set_error_hook(eyre_hook: EyreHook) -> Result<()> {
|
||||
let eyre_hook = eyre_hook.into_eyre_hook();
|
||||
eyre::set_hook(Box::new(move |error| {
|
||||
let _ = restore_terminal();
|
||||
eyre_hook(error)
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
@@ -23,15 +25,7 @@ use std::{
|
||||
};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{block, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
@@ -240,7 +234,7 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let area = f.area();
|
||||
let area = f.size();
|
||||
|
||||
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, area);
|
||||
@@ -254,7 +248,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let progress = LineGauge::default()
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, progress_area);
|
||||
|
||||
@@ -13,25 +13,20 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{
|
||||
Constraint,
|
||||
Constraint::{Length, Max, Min, Percentage, Ratio},
|
||||
Layout, Rect,
|
||||
},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
layout::Constraint::*,
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -80,7 +75,7 @@ fn ui(frame: &mut Frame) {
|
||||
Length(50), // examples
|
||||
Min(0), // fills remaining space
|
||||
]);
|
||||
let [text_area, examples_area, _] = vertical.areas(frame.area());
|
||||
let [text_area, examples_area, _] = vertical.areas(frame.size());
|
||||
|
||||
// title
|
||||
frame.render_widget(
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
//! # [Ratatui] Line Gauge example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Style, Stylize},
|
||||
widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
progress_columns: u16,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Started,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
self.update(terminal.size()?.width);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.area()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, terminal_width: u16) {
|
||||
if self.state != AppState::Started {
|
||||
return;
|
||||
}
|
||||
|
||||
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
|
||||
self.progress = f64::from(self.progress_columns) / f64::from(terminal_width);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
let timeout = Duration::from_secs_f32(1.0 / 20.0);
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&mut self) {
|
||||
self.state = AppState::Started;
|
||||
}
|
||||
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, main_area, footer_area] = layout.areas(area);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 3); 3]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area] = layout.areas(main_area);
|
||||
|
||||
header().render(header_area, buf);
|
||||
footer().render(footer_area, buf);
|
||||
|
||||
self.render_gauge1(gauge1_area, buf);
|
||||
self.render_gauge2(gauge2_area, buf);
|
||||
self.render_gauge3(gauge3_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn header() -> impl Widget {
|
||||
Paragraph::new("Ratatui Line Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
}
|
||||
|
||||
fn footer() -> impl Widget {
|
||||
Paragraph::new("Press ENTER / SPACE to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Blue / red only foreground");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.unfilled_style(Style::default().fg(Color::Red))
|
||||
.label("Foreground:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Blue / red only background");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(Style::default().fg(Color::Blue).bg(Color::Blue))
|
||||
.unfilled_style(Style::default().fg(Color::Red).bg(Color::Red))
|
||||
.label("Background:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Fully styled with background");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(
|
||||
Style::default()
|
||||
.fg(tailwind::BLUE.c400)
|
||||
.bg(tailwind::BLUE.c600),
|
||||
)
|
||||
.unfilled_style(
|
||||
Style::default()
|
||||
.fg(tailwind::RED.c400)
|
||||
.bg(tailwind::RED.c800),
|
||||
)
|
||||
.label("Both:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::NONE)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.padding(Padding::vertical(1))
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
535
examples/list.rs
535
examples/list.rs
@@ -13,323 +13,350 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{error::Error, io};
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{
|
||||
palette::tailwind::{BLUE, GREEN, SLATE},
|
||||
Color, Modifier, Style, Stylize,
|
||||
},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
|
||||
StatefulWidget, Widget, Wrap,
|
||||
},
|
||||
Terminal,
|
||||
use std::{error::Error, io, io::stdout};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
|
||||
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
|
||||
const NORMAL_ROW_BG: Color = SLATE.c950;
|
||||
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
|
||||
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
|
||||
const TEXT_FG_COLOR: Color = SLATE.c200;
|
||||
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
||||
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
|
||||
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
|
||||
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
|
||||
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
|
||||
const TEXT_COLOR: Color = tailwind::SLATE.c200;
|
||||
const COMPLETED_TEXT_COLOR: Color = tailwind::GREEN.c500;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
tui::init_error_hooks()?;
|
||||
let terminal = tui::init_terminal()?;
|
||||
|
||||
let mut app = App::default();
|
||||
app.run(terminal)?;
|
||||
|
||||
tui::restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
|
||||
/// which is a wrapper around `ListState`. Keeping track of the state lets us render the
|
||||
/// associated widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events. Check
|
||||
/// the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
todo_list: TodoList,
|
||||
}
|
||||
|
||||
struct TodoList {
|
||||
items: Vec<TodoItem>,
|
||||
state: ListState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TodoItem {
|
||||
todo: String,
|
||||
info: String,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Copy, Clone)]
|
||||
enum Status {
|
||||
Todo,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
struct TodoItem<'a> {
|
||||
todo: &'a str,
|
||||
info: &'a str,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
struct StatefulList<'a> {
|
||||
state: ListState,
|
||||
items: Vec<TodoItem<'a>>,
|
||||
last_selected: Option<usize>,
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
items: StatefulList<'a>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
// create app and run it
|
||||
App::new().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
should_exit: false,
|
||||
todo_list: TodoList::from_iter([
|
||||
(Status::Todo, "Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust"),
|
||||
(Status::Completed, "Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui."),
|
||||
(Status::Todo, "Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!"),
|
||||
(Status::Todo, "Walk with your dog", "Max is bored, go walk with him!"),
|
||||
(Status::Completed, "Pay the bills", "Pay the train subscription!!!"),
|
||||
(Status::Completed, "Refactor list example", "If you see this info that means I completed this task!"),
|
||||
items: StatefulList::with_items([
|
||||
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
|
||||
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
|
||||
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
|
||||
("Walk with your dog", "Max is bored, go walk with him!", Status::Todo),
|
||||
("Pay the bills", "Pay the train subscription!!!", Status::Completed),
|
||||
("Refactor list example", "If you see this info that means I completed this task!", Status::Completed),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(Status, &'static str, &'static str)> for TodoList {
|
||||
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
|
||||
let items = iter
|
||||
.into_iter()
|
||||
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
|
||||
.collect();
|
||||
let state = ListState::default();
|
||||
Self { items, state }
|
||||
}
|
||||
}
|
||||
|
||||
impl TodoItem {
|
||||
fn new(status: Status, todo: &str, info: &str) -> Self {
|
||||
Self {
|
||||
status,
|
||||
todo: todo.to_string(),
|
||||
info: info.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|f| f.render_widget(&mut *self, f.area()))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
self.handle_key(key);
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.select_none(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.select_next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.select_previous(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.select_first(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.select_last(),
|
||||
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
|
||||
self.toggle_status();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_none(&mut self) {
|
||||
self.todo_list.state.select(None);
|
||||
}
|
||||
|
||||
fn select_next(&mut self) {
|
||||
self.todo_list.state.select_next();
|
||||
}
|
||||
fn select_previous(&mut self) {
|
||||
self.todo_list.state.select_previous();
|
||||
}
|
||||
|
||||
fn select_first(&mut self) {
|
||||
self.todo_list.state.select_first();
|
||||
}
|
||||
|
||||
fn select_last(&mut self) {
|
||||
self.todo_list.state.select_last();
|
||||
}
|
||||
|
||||
/// Changes the status of the selected list item
|
||||
fn toggle_status(&mut self) {
|
||||
if let Some(i) = self.todo_list.state.selected() {
|
||||
self.todo_list.items[i].status = match self.todo_list.items[i].status {
|
||||
fn change_status(&mut self) {
|
||||
if let Some(i) = self.items.state.selected() {
|
||||
self.items.items[i].status = match self.items.items[i].status {
|
||||
Status::Completed => Status::Todo,
|
||||
Status::Todo => Status::Completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn go_top(&mut self) {
|
||||
self.items.state.select(Some(0));
|
||||
}
|
||||
|
||||
fn go_bottom(&mut self) {
|
||||
self.items.state.select(Some(self.items.items.len() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
impl App<'_> {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
|
||||
loop {
|
||||
self.draw(&mut terminal)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('h') | Left => self.items.unselect(),
|
||||
Char('j') | Down => self.items.next(),
|
||||
Char('k') | Up => self.items.previous(),
|
||||
Char('l') | Right | Enter => self.change_status(),
|
||||
Char('g') => self.go_top(),
|
||||
Char('G') => self.go_bottom(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [header_area, main_area, footer_area] = Layout::vertical([
|
||||
// Create a space for header, todo list and the footer.
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(2),
|
||||
]);
|
||||
let [header_area, rest_area, footer_area] = vertical.areas(area);
|
||||
|
||||
let [list_area, item_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
|
||||
// Create two chunks with equal vertical screen space. One for the list and the other for
|
||||
// the info block.
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
|
||||
|
||||
App::render_header(header_area, buf);
|
||||
App::render_footer(footer_area, buf);
|
||||
self.render_list(list_area, buf);
|
||||
self.render_selected_item(item_area, buf);
|
||||
render_title(header_area, buf);
|
||||
self.render_todo(upper_item_list_area, buf);
|
||||
self.render_info(lower_item_list_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendering logic for the app
|
||||
impl App {
|
||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
impl App<'_> {
|
||||
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// We create two blocks, one is for the header (outer) and the other is for list (inner).
|
||||
let outer_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO List")
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
// We get the inner area from outer_block. We'll use this area later to render the table.
|
||||
let outer_area = area;
|
||||
let inner_area = outer_block.inner(outer_area);
|
||||
|
||||
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
let block = Block::new()
|
||||
.title(Line::raw("TODO List").centered())
|
||||
.borders(Borders::TOP)
|
||||
.border_set(symbols::border::EMPTY)
|
||||
.border_style(TODO_HEADER_STYLE)
|
||||
.bg(NORMAL_ROW_BG);
|
||||
// We can render the header in outer_area.
|
||||
outer_block.render(outer_area, buf);
|
||||
|
||||
// Iterate through all elements in the `items` and stylize them.
|
||||
let items: Vec<ListItem> = self
|
||||
.todo_list
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, todo_item)| {
|
||||
let color = alternate_colors(i);
|
||||
ListItem::from(todo_item).bg(color)
|
||||
})
|
||||
.map(|(i, todo_item)| todo_item.to_list_item(i))
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let list = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(SELECTED_STYLE)
|
||||
let items = List::new(items)
|
||||
.block(inner_block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(SELECTED_STYLE_FG),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
|
||||
// same method name `render`.
|
||||
StatefulWidget::render(list, area, buf, &mut self.todo_list.state);
|
||||
// We can now render the item list
|
||||
// (look careful we are using StatefulWidget's render.)
|
||||
// ratatui::widgets::StatefulWidget::render as stateful_render
|
||||
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
|
||||
}
|
||||
|
||||
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_info(&self, area: Rect, buf: &mut Buffer) {
|
||||
// We get the info depending on the item's state.
|
||||
let info = if let Some(i) = self.todo_list.state.selected() {
|
||||
match self.todo_list.items[i].status {
|
||||
Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info),
|
||||
Status::Todo => format!("☐ TODO: {}", self.todo_list.items[i].info),
|
||||
let info = if let Some(i) = self.items.state.selected() {
|
||||
match self.items.items[i].status {
|
||||
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
|
||||
Status::Todo => "TODO: ".to_string() + self.items.items[i].info,
|
||||
}
|
||||
} else {
|
||||
"Nothing selected...".to_string()
|
||||
"Nothing to see here...".to_string()
|
||||
};
|
||||
|
||||
// We show the list item's info under the list in this paragraph
|
||||
let block = Block::new()
|
||||
.title(Line::raw("TODO Info").centered())
|
||||
.borders(Borders::TOP)
|
||||
.border_set(symbols::border::EMPTY)
|
||||
.border_style(TODO_HEADER_STYLE)
|
||||
.bg(NORMAL_ROW_BG)
|
||||
.padding(Padding::horizontal(1));
|
||||
let outer_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO Info")
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.padding(Padding::horizontal(1))
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// This is a similar process to what we did for list. outer_info_area will be used for
|
||||
// header inner_info_area will be used for the list info.
|
||||
let outer_info_area = area;
|
||||
let inner_info_area = outer_info_block.inner(outer_info_area);
|
||||
|
||||
// We can render the header. Inner info will be rendered later
|
||||
outer_info_block.render(outer_info_area, buf);
|
||||
|
||||
let info_paragraph = Paragraph::new(info)
|
||||
.block(inner_info_block)
|
||||
.fg(TEXT_COLOR)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
// We can now render the item info
|
||||
Paragraph::new(info)
|
||||
.block(block)
|
||||
.fg(TEXT_FG_COLOR)
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(area, buf);
|
||||
info_paragraph.render(inner_info_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
const fn alternate_colors(i: usize) -> Color {
|
||||
if i % 2 == 0 {
|
||||
NORMAL_ROW_BG
|
||||
} else {
|
||||
ALT_ROW_BG_COLOR
|
||||
}
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl From<&TodoItem> for ListItem<'_> {
|
||||
fn from(value: &TodoItem) -> Self {
|
||||
let line = match value.status {
|
||||
Status::Todo => Line::styled(format!(" ☐ {}", value.todo), TEXT_FG_COLOR),
|
||||
Status::Completed => {
|
||||
Line::styled(format!(" ✓ {}", value.todo), COMPLETED_TEXT_FG_COLOR)
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl StatefulList<'_> {
|
||||
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: items.iter().map(TodoItem::from).collect(),
|
||||
last_selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
ListItem::new(line)
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
let offset = self.state.offset();
|
||||
self.last_selected = self.state.selected();
|
||||
self.state.select(None);
|
||||
*self.state.offset_mut() = offset;
|
||||
}
|
||||
}
|
||||
|
||||
mod tui {
|
||||
use std::{io, io::stdout};
|
||||
impl TodoItem<'_> {
|
||||
fn to_list_item(&self, index: usize) -> ListItem {
|
||||
let bg_color = match index % 2 {
|
||||
0 => NORMAL_ROW_COLOR,
|
||||
_ => ALT_ROW_COLOR,
|
||||
};
|
||||
let line = match self.status {
|
||||
Status::Todo => Line::styled(format!(" ☐ {}", self.todo), TEXT_COLOR),
|
||||
Status::Completed => Line::styled(
|
||||
format!(" ✓ {}", self.todo),
|
||||
(COMPLETED_TEXT_COLOR, bg_color),
|
||||
),
|
||||
};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
pub fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Terminal::new(CrosstermBackend::new(stdout()))
|
||||
}
|
||||
|
||||
pub fn restore_terminal() -> io::Result<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
disable_raw_mode()
|
||||
ListItem::new(line).bg(bg_color)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&(&'a str, &'a str, Status)> for TodoItem<'a> {
|
||||
fn from((todo, info, status): &(&'a str, &'a str, Status)) -> Self {
|
||||
Self {
|
||||
todo,
|
||||
info,
|
||||
status: *status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//! # [Ratatui] Minimal example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
text::Text,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
|
||||
enable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -25,20 +25,13 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::Paragraph,
|
||||
Frame, Terminal,
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -68,7 +61,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [text_area, main_area] = vertical.areas(frame.area());
|
||||
let [text_area, main_area] = vertical.areas(frame.size());
|
||||
frame.render_widget(
|
||||
Paragraph::new("Note: not all terminals support all modifiers")
|
||||
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
|
||||
@@ -31,15 +31,13 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
text::Line,
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
@@ -148,5 +146,5 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
f.render_widget(paragraph, f.area());
|
||||
f.render_widget(paragraph, f.size());
|
||||
}
|
||||
|
||||
@@ -14,218 +14,150 @@
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self},
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::event::KeyEventKind;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Widget, Wrap},
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
use self::common::{init_terminal, install_hooks, restore_terminal, Tui};
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
install_hooks()?;
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = App::new();
|
||||
app.run(&mut terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
scroll: u16,
|
||||
last_tick: Instant,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// The duration between each tick.
|
||||
const TICK_RATE: Duration = Duration::from_millis(250);
|
||||
|
||||
/// Create a new instance of the app.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
should_exit: false,
|
||||
scroll: 0,
|
||||
last_tick: Instant::now(),
|
||||
}
|
||||
const fn new() -> Self {
|
||||
Self { scroll: 0 }
|
||||
}
|
||||
|
||||
/// Run the app until the user exits.
|
||||
fn run(&mut self, terminal: &mut Tui) -> io::Result<()> {
|
||||
while !self.should_exit {
|
||||
self.draw(terminal)?;
|
||||
self.handle_events()?;
|
||||
if self.last_tick.elapsed() >= Self::TICK_RATE {
|
||||
self.on_tick();
|
||||
self.last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
/// Draw the app to the terminal.
|
||||
fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle events from the terminal.
|
||||
fn handle_events(&mut self) -> io::Result<()> {
|
||||
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
|
||||
while event::poll(timeout)? {
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the app state on each tick.
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll = (self.scroll + 1) % 10;
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Default alignment (Left), no wrap"))
|
||||
.gray()
|
||||
.render(areas[0], buf);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Default alignment (Left), with wrap"))
|
||||
.gray()
|
||||
.wrap(Wrap { trim: true })
|
||||
.render(areas[1], buf);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Right alignment, with wrap"))
|
||||
.gray()
|
||||
.right_aligned()
|
||||
.wrap(Wrap { trim: true })
|
||||
.render(areas[2], buf);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Center alignment, with wrap, with scroll"))
|
||||
.gray()
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((self.scroll, 0))
|
||||
.render(areas[3], buf);
|
||||
}
|
||||
}
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
/// Create a bordered block with a title.
|
||||
fn title_block(title: &str) -> Block {
|
||||
Block::bordered()
|
||||
.gray()
|
||||
.title(title.bold().into_centered_line())
|
||||
}
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
/// Create some lines to display in the paragraph.
|
||||
fn create_lines(area: Rect) -> Vec<Line<'static>> {
|
||||
let short_line = "A long line to demonstrate line wrapping. ";
|
||||
let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
|
||||
let mut styled_spans = vec![];
|
||||
for span in [
|
||||
"Styled".blue(),
|
||||
"Spans".red().on_white(),
|
||||
"Bold".bold(),
|
||||
"Italic".italic(),
|
||||
"Underlined".underlined(),
|
||||
"Strikethrough".crossed_out(),
|
||||
] {
|
||||
styled_spans.push(span);
|
||||
styled_spans.push(" ".into());
|
||||
}
|
||||
vec![
|
||||
Line::raw("Unstyled Line"),
|
||||
Line::raw("Styled Line").black().on_red().bold().italic(),
|
||||
Line::from(styled_spans),
|
||||
Line::from(long_line.green().italic()),
|
||||
Line::from_iter([
|
||||
let block = Block::new().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_blue()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.on_green()),
|
||||
Line::from("This is a line".green().italic()),
|
||||
Line::from(vec![
|
||||
"Masked text: ".into(),
|
||||
Span::styled(Masked::new("my secret password", '*'), Color::Red),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
/// A module for common functionality used in the examples.
|
||||
mod common {
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
panic,
|
||||
};
|
||||
|
||||
use color_eyre::{
|
||||
config::{EyreHook, HookBuilder, PanicHook},
|
||||
eyre,
|
||||
};
|
||||
use crossterm::ExecutableCommand;
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
// A simple alias for the terminal type used in this example.
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Initialize the terminal and enter alternate screen mode.
|
||||
pub fn init_terminal() -> io::Result<Tui> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
Terminal::new(backend)
|
||||
}
|
||||
|
||||
/// Restore the terminal to its original state.
|
||||
pub fn restore_terminal() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Installs hooks for panic and error handling.
|
||||
///
|
||||
/// Makes the app resilient to panics and errors by restoring the terminal before printing the
|
||||
/// panic or error message. This prevents error messages from being messed up by the terminal
|
||||
/// state.
|
||||
pub fn install_hooks() -> color_eyre::Result<()> {
|
||||
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
|
||||
install_panic_hook(panic_hook);
|
||||
install_error_hook(eyre_hook)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before printing the panic.
|
||||
fn install_panic_hook(panic_hook: PanicHook) {
|
||||
let panic_hook = panic_hook.into_panic_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = restore_terminal();
|
||||
panic_hook(panic_info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Install an error hook that restores the terminal before printing the error.
|
||||
fn install_error_hook(eyre_hook: EyreHook) -> color_eyre::Result<()> {
|
||||
let eyre_hook = eyre_hook.into_eyre_hook();
|
||||
eyre::set_hook(Box::new(move |error| {
|
||||
let _ = restore_terminal();
|
||||
eyre_hook(error)
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::bordered()
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, layout[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, layout[1]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.right_aligned()
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, layout[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, layout[3]);
|
||||
}
|
||||
|
||||
@@ -18,17 +18,14 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
prelude::*,
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -86,7 +83,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let area = f.area();
|
||||
let area = f.size();
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [instructions, content] = vertical.areas(area);
|
||||
|
||||
@@ -19,14 +19,10 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
|
||||
widgets::Paragraph,
|
||||
Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
@@ -64,7 +60,9 @@ fn logo() -> String {
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let mut terminal = init()?;
|
||||
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
|
||||
terminal.draw(|frame| {
|
||||
frame.render_widget(Paragraph::new(logo()), frame.size());
|
||||
})?;
|
||||
sleep(Duration::from_secs(5));
|
||||
restore()?;
|
||||
println!();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
@@ -21,20 +22,12 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Margin},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols::scrollbar,
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
Frame, Terminal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
@@ -119,11 +112,11 @@ fn run_app<B: Backend>(
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let area = f.area();
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4);
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let chunks = Layout::vertical([
|
||||
@@ -133,7 +126,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(area);
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
@@ -193,7 +186,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(Margin {
|
||||
chunks[2].inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 0,
|
||||
}),
|
||||
@@ -211,7 +204,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(Margin {
|
||||
chunks[3].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
@@ -229,7 +222,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(Margin {
|
||||
chunks[4].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
|
||||
@@ -19,21 +19,18 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style},
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -151,7 +148,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(f.area());
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
|
||||
@@ -13,25 +13,17 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Stylize},
|
||||
text::{Line, Text},
|
||||
widgets::{
|
||||
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
||||
ScrollbarState, Table, TableState,
|
||||
},
|
||||
Frame, Terminal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use style::palette::tailwind;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -220,12 +212,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => app.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.previous(),
|
||||
KeyCode::Char('l') | KeyCode::Right => app.next_color(),
|
||||
KeyCode::Char('h') | KeyCode::Left => app.previous_color(),
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('j') | Down => app.next(),
|
||||
Char('k') | Up => app.previous(),
|
||||
Char('l') | Right => app.next_color(),
|
||||
Char('h') | Left => app.previous_color(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +227,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.area());
|
||||
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size());
|
||||
|
||||
app.set_colors();
|
||||
|
||||
@@ -325,7 +318,7 @@ fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None),
|
||||
area.inner(Margin {
|
||||
area.inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
}),
|
||||
|
||||
@@ -13,24 +13,17 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
|
||||
|
||||
use std::io::stdout;
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{Block, Padding, Paragraph, Tabs, Widget},
|
||||
Terminal,
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -77,17 +70,18 @@ impl App {
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_tab(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
Char('l') | Right => self.next_tab(),
|
||||
Char('h') | Left => self.previous_tab(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +120,7 @@ impl SelectedTab {
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
use Constraint::*;
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
//! # [Ratatui] Tracing example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// A simple example demonstrating how to use the [tracing] with Ratatui to log to a file.
|
||||
//
|
||||
// This example demonstrates how to use the [tracing] crate with Ratatui to log to a file. The
|
||||
// example sets up a simple logger that logs to a file named `tracing.log` in the current directory.
|
||||
//
|
||||
// Run the example with `cargo run --example tracing` and then view the `tracing.log` file to see
|
||||
// the logs. To see more logs, you can run the example with `RUST_LOG=tracing=debug cargo run
|
||||
// --example`
|
||||
//
|
||||
// For a helpful widget that handles logging, see the [tui-logger] crate.
|
||||
//
|
||||
// [tracing]: https://crates.io/crates/tracing
|
||||
// [tui-logger]: https://crates.io/crates/tui-logger
|
||||
|
||||
use std::{fs::File, io::stdout, panic, time::Duration};
|
||||
|
||||
use color_eyre::{
|
||||
config::HookBuilder,
|
||||
eyre::{self, Context},
|
||||
Result,
|
||||
};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
widgets::{Block, Paragraph},
|
||||
Terminal,
|
||||
};
|
||||
use tracing::{debug, info, instrument, trace, Level};
|
||||
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let _guard = init_tracing()?;
|
||||
info!("Starting tracing example");
|
||||
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut events = vec![]; // a buffer to store the recent events to display in the UI
|
||||
while !should_exit(&events) {
|
||||
handle_events(&mut events)?;
|
||||
terminal.draw(|frame| ui(frame, &events))?;
|
||||
}
|
||||
restore_terminal()?;
|
||||
info!("Exiting tracing example");
|
||||
println!("See the tracing.log file for the logs");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_exit(events: &[Event]) -> bool {
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, Event::Key(key) if key.code == KeyCode::Char('q')))
|
||||
}
|
||||
|
||||
/// Handle events and insert them into the events vector keeping only the last 10 events
|
||||
#[instrument(skip(events))]
|
||||
fn handle_events(events: &mut Vec<Event>) -> Result<()> {
|
||||
// Render the UI at least once every 100ms
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
let event = event::read()?;
|
||||
debug!(?event);
|
||||
events.insert(0, event);
|
||||
}
|
||||
events.truncate(10);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn ui(frame: &mut ratatui::Frame, events: &[Event]) {
|
||||
// To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing`
|
||||
trace!(frame_count = frame.count(), event_count = events.len());
|
||||
let events = events.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
|
||||
let paragraph = Paragraph::new(events.join("\n"))
|
||||
.block(Block::bordered().title("Tracing example. Press 'q' to quit."));
|
||||
frame.render_widget(paragraph, frame.area());
|
||||
}
|
||||
|
||||
/// Initialize the tracing subscriber to log to a file
|
||||
///
|
||||
/// This function initializes the tracing subscriber to log to a file named `tracing.log` in the
|
||||
/// current directory. The function returns a [`WorkerGuard`] that must be kept alive for the
|
||||
/// duration of the program to ensure that logs are flushed to the file on shutdown. The logs are
|
||||
/// written in a non-blocking fashion to ensure that the logs do not block the main thread.
|
||||
fn init_tracing() -> Result<WorkerGuard> {
|
||||
let file = File::create("tracing.log").wrap_err("failed to create tracing.log")?;
|
||||
let (non_blocking, guard) = non_blocking(file);
|
||||
|
||||
// By default, the subscriber is configured to log all events with a level of `DEBUG` or higher,
|
||||
// but this can be changed by setting the `RUST_LOG` environment variable.
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(Level::DEBUG.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(non_blocking)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
/// Initialize the error hooks to ensure that the terminal is restored to a sane state before
|
||||
/// exiting
|
||||
fn init_error_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
debug!("terminal initialized");
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn restore_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
debug!("terminal restored");
|
||||
Ok(())
|
||||
}
|
||||
@@ -29,18 +29,14 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Position},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
prelude::*,
|
||||
widgets::{Block, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
enum InputMode {
|
||||
@@ -90,7 +86,7 @@ impl App {
|
||||
///
|
||||
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate
|
||||
/// the byte index based on the index of the character.
|
||||
fn byte_index(&self) -> usize {
|
||||
fn byte_index(&mut self) -> usize {
|
||||
self.input
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
@@ -209,7 +205,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]);
|
||||
let [help_area, input_area, messages_area] = vertical.areas(f.area());
|
||||
let [help_area, input_area, messages_area] = vertical.areas(f.size());
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
@@ -245,19 +241,22 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.block(Block::bordered().title("Input"));
|
||||
f.render_widget(input, input_area);
|
||||
match app.input_mode {
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
InputMode::Normal => {}
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
{}
|
||||
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
InputMode::Editing => f.set_cursor_position(Position::new(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
input_area.x + app.character_index as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
input_area.y + 1,
|
||||
)),
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
f.set_cursor(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
input_area.x + app.character_index as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
input_area.y + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let messages: Vec<ListItem> = app
|
||||
|
||||
@@ -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/barchart.tape`
|
||||
Output "target/barchart-grouped.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1000
|
||||
Hide
|
||||
Type "cargo run --example=barchart-grouped"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 1s
|
||||
@@ -3,10 +3,10 @@
|
||||
Output "target/barchart.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=barchart"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 1s
|
||||
Sleep 5s
|
||||
|
||||
@@ -1,30 +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/constraint-explorer.tape`
|
||||
Output "target/constraint-explorer.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set FontSize 18
|
||||
Set Width 1200
|
||||
Set Height 950
|
||||
Hide
|
||||
Type "cargo run --example=constraint-explorer --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Set TypingSpeed 2s
|
||||
Type "1"
|
||||
Type "2"
|
||||
Right
|
||||
Type "4"
|
||||
Type "5"
|
||||
Up
|
||||
Up
|
||||
Down
|
||||
Down
|
||||
Right
|
||||
Set TypingSpeed 0.5s
|
||||
Type "++++++++"
|
||||
Type "--------"
|
||||
Type "aaa"
|
||||
Sleep 2s
|
||||
Type "xxx"
|
||||
Hide
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/demo2-destroy.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/demo.tape`
|
||||
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
|
||||
Output "target/demo2-destroy.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
@@ -10,9 +10,9 @@ Set Width 1120
|
||||
Set Height 480
|
||||
Set Padding 0
|
||||
Hide
|
||||
Type "cargo run --example demo2 --features crossterm,palette,widget-calendar"
|
||||
Type "cargo run --example demo2 --features crossterm,widget-calendar"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Type "d"
|
||||
Sleep 20s
|
||||
Sleep 30s
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/demo2-social.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/demo.tape`
|
||||
|
||||
Output "target/demo2-social.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/demo2.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/demo.tape`
|
||||
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
|
||||
Output "target/demo2.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/docsrs.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/demo.tape`
|
||||
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
|
||||
Output "target/docsrs.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/flex.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/layout.tape`
|
||||
Output "target/flex.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
|
||||
@@ -1,15 +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/hyperlink.tape`
|
||||
Output "target/hyperlink.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
Type "cargo run --example=hyperlink --features=crossterm,unstable-widget-ref"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Show
|
||||
Sleep 1s
|
||||
Hide
|
||||
Type "q"
|
||||
|
||||
@@ -1,14 +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/line_gauge.tape`
|
||||
Output "target/line_gauge.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 850
|
||||
Hide
|
||||
Type "cargo run --example=line_gauge --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
Enter 1
|
||||
Sleep 15s
|
||||
@@ -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/minimal.tape`
|
||||
Output "target/minimal.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
Type "cargo run --example=minimal --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/ratatui-logo.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/popup.tape`
|
||||
Output "target/ratatui-logo.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 550
|
||||
|
||||
@@ -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/tracing.tape`
|
||||
Output "target/tracing.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Type "RUST_LOG=trace cargo run --example=tracing" Enter
|
||||
Sleep 1s
|
||||
Type @100ms "jjjjq"
|
||||
Sleep 1s
|
||||
Type "cat tracing.log" Enter
|
||||
Sleep 10s
|
||||
@@ -1,9 +1,6 @@
|
||||
# configuration for https://rust-lang.github.io/rustfmt/
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_matchers=true
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
normalize_doc_attributes=true
|
||||
use_field_init_shorthand=true
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
|
||||
@@ -104,10 +104,7 @@ use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
};
|
||||
use crate::{buffer::Cell, layout::Size, prelude::Rect};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
@@ -195,25 +192,25 @@ pub trait Backend {
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`show_cursor`]: Self::show_cursor
|
||||
/// [`show_cursor`]: Backend::show_cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
///
|
||||
/// See [`hide_cursor`] for an example.
|
||||
///
|
||||
/// [`hide_cursor`]: Self::hide_cursor
|
||||
/// [`hide_cursor`]: Backend::hide_cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// The returned tuple contains the x and y coordinates of the cursor.
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
/// The returned tuple contains the x and y coordinates of the cursor. The origin
|
||||
/// (0, 0) is at the top left corner of the screen.
|
||||
///
|
||||
/// See [`set_cursor_position`] for an example.
|
||||
/// See [`set_cursor`] for an example.
|
||||
///
|
||||
/// [`set_cursor_position`]: Self::set_cursor_position
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position>;
|
||||
/// [`set_cursor`]: Backend::set_cursor
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
@@ -223,31 +220,14 @@ pub trait Backend {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
|
||||
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
|
||||
/// backend.set_cursor(10, 20)?;
|
||||
/// assert_eq!(backend.get_cursor()?, (10, 20));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
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 = "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))
|
||||
}
|
||||
|
||||
/// 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 = "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 })
|
||||
}
|
||||
/// [`get_cursor`]: Backend::get_cursor
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
///
|
||||
@@ -281,7 +261,7 @@ pub trait Backend {
|
||||
/// This method will return an error if the terminal screen could not be cleared. It will also
|
||||
/// return an error if the `clear_type` is not supported by the backend.
|
||||
///
|
||||
/// [`clear`]: Self::clear
|
||||
/// [`clear`]: Backend::clear
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
@@ -295,19 +275,19 @@ pub trait Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Size`].
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
|
||||
///
|
||||
/// The returned [`Size`] contains the width and height of the terminal screen.
|
||||
/// The returned [`Rect`] contains the width and height of the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// assert_eq!(backend.size()?, Size::new(80, 25));
|
||||
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn size(&self) -> io::Result<Size>;
|
||||
fn size(&self) -> io::Result<Rect>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
|
||||
///
|
||||
|
||||
@@ -6,20 +6,21 @@ use std::io::{self, Write};
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors, ContentStyle,
|
||||
Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors,
|
||||
ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
},
|
||||
layout::{Position, Size},
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
@@ -44,15 +45,11 @@ use crate::{
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// crossterm::{
|
||||
/// terminal::{
|
||||
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
/// },
|
||||
/// ExecutableCommand,
|
||||
/// },
|
||||
/// prelude::*,
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
/// };
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let mut backend = CrosstermBackend::new(stdout());
|
||||
/// // or
|
||||
@@ -103,27 +100,6 @@ where
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
///
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
@@ -154,13 +130,13 @@ where
|
||||
#[cfg(feature = "underline-color")]
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = None;
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
@@ -212,14 +188,12 @@ where
|
||||
execute!(self.writer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map(|(x, y)| Position { x, y })
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
@@ -247,9 +221,9 @@ where
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Size { width, height })
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
|
||||
@@ -9,12 +9,13 @@ use std::{
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use termion::{color as tcolor, style as tstyle};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
termion::{self, color as tcolor, color::Color as _, style as tstyle},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
@@ -39,10 +40,8 @@ use crate::{
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
|
||||
/// };
|
||||
/// use ratatui::prelude::*;
|
||||
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let mut backend = TermionBackend::new(writer);
|
||||
@@ -86,26 +85,6 @@ where
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for TermionBackend<W>
|
||||
@@ -157,13 +136,11 @@ where
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer)
|
||||
.map(|(x, y)| Position { x: x - 1, y: y - 1 })
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.writer.flush()
|
||||
}
|
||||
@@ -178,13 +155,13 @@ where
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = None;
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
write!(
|
||||
string,
|
||||
@@ -216,9 +193,9 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = termion::terminal_size()?;
|
||||
Ok(Size::new(terminal.0, terminal.1))
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
@@ -246,6 +223,7 @@ struct ModifierDiff {
|
||||
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_fg(f),
|
||||
Color::Black => termion::color::Black.write_fg(f),
|
||||
@@ -271,6 +249,7 @@ impl fmt::Display for Fg {
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_bg(f),
|
||||
Color::Black => termion::color::Black.write_bg(f),
|
||||
@@ -296,7 +275,7 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color:ident) => {
|
||||
($termion_color:ident, $color: ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
@@ -437,7 +416,7 @@ impl fmt::Display for ModifierDiff {
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier:ident) => {
|
||||
($termion_modifier:ident, $modifier: ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
|
||||
@@ -7,18 +7,20 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
@@ -191,16 +193,12 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<crate::layout::Position> {
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok((x as u16, y as u16).into())
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<crate::layout::Position>>(
|
||||
&mut self,
|
||||
position: P,
|
||||
) -> io::Result<()> {
|
||||
let crate::layout::Position { x, y } = position.into();
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
@@ -215,9 +213,9 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Size::new(u16_max(cols), u16_max(rows)))
|
||||
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user