Compare commits

..

4 Commits

Author SHA1 Message Date
Josh McKinney
48924e9cb9 fix: add ansi_buffer example 2024-04-25 20:01:22 -07:00
Josh McKinney
ee85bcfb43 fix: flip order of formatter and value to write 2024-04-25 19:52:22 -07:00
Josh McKinney
9f9a3047c4 fix: add area param to AnsiStringBuffer::render_ref 2024-04-25 19:41:29 -07:00
Josh McKinney
0d54ca06f8 feat: add WidgetExt trait for debugging widgets
Importing the `WidgetExt` trait allows users to easily get a string
representation of a widget with ANSI escape sequences for the terminal.
This is useful for debugging and testing widgets.

```rust
use ratatui::{prelude::*, widgets::widget_ext::WidgetExt};

fn main() {
    let greeting = Text::from(vec![
        Line::styled("Hello", Color::Blue),
        Line::styled("World ", Color::Green),
    ]);
    println!("{}", greeting.to_ansi_string(5, 2));
}
```

Fixes: https://github.com/ratatui-org/ratatui/issues/1045
2024-04-25 19:20:45 -07:00
294 changed files with 23452 additions and 37518 deletions

16
.cargo-husky/hooks/pre-push Executable file
View 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

View File

@@ -1,2 +0,0 @@
[alias]
xtask = "run --package xtask --"

9
.github/CODEOWNERS vendored
View File

@@ -1,11 +1,8 @@
# See <https://help.github.com/articles/about-codeowners/>
# See https://help.github.com/articles/about-codeowners/
# for more info about CODEOWNERS file
# It uses the same pattern rule for gitignore file
# <https://git-scm.com/docs/gitignore#_pattern_format>
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @ratatui/maintainers
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271 @EdJoPaTo

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: ratatui
open_collective: ratatui

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'Type: Enhancement'
labels: enhancement
assignees: ''
---

View File

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

View File

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

View File

@@ -1,56 +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: dawidd6/action-download-artifact@v6
with:
name: ${{ env.BENCHMARK_RESULTS }}
run_id: ${{ github.event.workflow_run.id }}
- name: Download PR Event
uses: dawidd6/action-download-artifact@v6
with:
name: ${{ env.PR_EVENT }}
run_id: ${{ github.event.workflow_run.id }}
- name: Export PR Event Data
uses: actions/github-script@v7
with:
script: |
let fs = require('fs');
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
core.exportVariable("PR_HEAD", prEvent.pull_request.head.ref);
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
core.exportVariable("PR_NUMBER", prEvent.number);
- uses: bencherdev/bencher@main
- name: Track Benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch "$PR_HEAD" \
--start-point "$PR_BASE" \
--start-point-hash "$PR_BASE_SHA" \
--start-point-clone-thresholds \
--start-point-reset \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number "$PR_NUMBER" \
--file "$BENCHMARK_RESULTS"

View File

@@ -1,4 +1,4 @@
name: Release alpha version
name: Continuous Deployment
on:
workflow_dispatch:
@@ -6,6 +6,9 @@ on:
# At 00:00 on Saturday
# https://crontab.guru/#0_0_*_*_6
- cron: "0 0 * * 6"
push:
tags:
- "v*.*.*"
defaults:
run:
@@ -17,6 +20,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
@@ -26,14 +30,14 @@ jobs:
- name: Calculate the next release
run: .github/workflows/calculate-alpha-release.bash
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
uses: orhun/git-cliff-action@v3
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
@@ -46,3 +50,17 @@ jobs:
tag: ${{ env.NEXT_TAG }}
prerelease: true
bodyFile: BODY.md
publish-stable:
name: Create a stable release
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
merge_group:
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
@@ -16,92 +17,76 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting,
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
# without having to wait for the comprehensive matrix of tests to complete.
env:
# don't install husky hooks during CI as they are only needed for for pre-push
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
jobs:
# Lint the formatting of the codebase.
lint-formatting:
name: Check Formatting
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- run: cargo xtask lint-formatting
# Check for typos in the codebase.
# See <https://github.com/crate-ci/typos/>
lint-typos:
name: Check Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
# Check for any disallowed dependencies in the codebase due to license / security issues.
# See <https://github.com/EmbarkStudios/cargo-deny>
dependencies:
name: Check Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
# Check for any unused dependencies in the codebase.
# See <https://github.com/bnjbvr/cargo-machete/>
cargo-machete:
name: Check Unused Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.7.0
# Run cargo clippy.
lint-clippy:
name: Check Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { components: clippy }
- uses: Swatinem/rust-cache@v2
- run: cargo xtask lint-clippy
# Run markdownlint on all markdown files in the repository.
lint-markdown:
name: Check Markdown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v18
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
globs: |
'**/*.md'
'!target'
components: rustfmt
- 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
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
# <https://app.codecov.io/gh/ratatui/ratatui>
coverage:
name: Coverage Report
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- 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
- 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:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- run: cargo xtask coverage
- uses: codecov/codecov-action@v5
- name: Install cargo-llvm-cov and cargo-make
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# Run cargo check. This is a fast way to catch any obvious errors in the code.
check:
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
strategy:
fail-fast: false
matrix:
@@ -109,77 +94,67 @@ jobs:
toolchain: ["1.74.0", "stable"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: Swatinem/rust-cache@v2
- run: cargo xtask check
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make check
run: cargo make check
env:
RUST_BACKTRACE: full
# Check if README.md is up-to-date with the crate's documentation.
check-readme:
name: Check README
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-rdme
- run: cargo xtask check-readme
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account the
# package.metadata.docs.rs configured in Cargo.toml. https://github.com/dtolnay/cargo-docs-rs
lint-docs:
name: Check Docs
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
- run: cargo xtask lint-docs
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
# compile, or any other issues in the documentation.
test-docs:
name: Test Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-docs
# Run cargo test on the libraries of the crate.
test-libs:
name: Test Libs ${{ matrix.toolchain }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain: ["1.74.0", "stable"]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-libs
# Run cargo test on all the backends.
test-backends:
name: Test ${{matrix.backend}} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
test-doc:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- 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
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test docs
run: cargo make test-doc
env:
RUST_BACKTRACE: full
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
backend: [crossterm, termion, termwiz]
exclude:
# termion is not supported on windows
- os: windows-latest
backend: termion
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-backend ${{ matrix.backend }}
- 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
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env:
RUST_BACKTRACE: full

View File

@@ -1,45 +0,0 @@
name: Release stable version
on:
push:
tags:
- "v*.*.*"
jobs:
publish-stable:
name: Create an stable release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
prerelease: false
bodyFile: BODY.md
publish-crate:
name: Publish crate
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
target
Cargo.lock
*.log
*.rs.rustfmt
.gdb_history

View File

@@ -4,39 +4,12 @@ This document contains a list of breaking changes in each version and some notes
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
GitHub with a [breaking change] label.
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary
This is a quick summary of the sections below:
- [Unreleased](#unreleased)
- The `From` impls for backend types are now replaced with more specific traits
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
- Removed public fields from `Rect` iterators
- `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>`
- `Color::from_hsl` is now behind the `palette` feature
- [v0.28.0](#v0280)
- `Backend::size` returns `Size` instead of `Rect`
- `Backend` 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`
@@ -74,403 +47,11 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## Unreleased (0.30.0)
### `WidgetRef` no longer has a blanket implementation of Widget
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
type.
```diff
-impl WidgetRef for Foo {
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
+impl Widget for &Foo {
+ fn render(self, area: Rect, buf: &mut Buffer)
}
```
### The `From` impls for backend types are now replaced with more specific traits [#1464]
[#1464]: https://github.com/ratatui/ratatui/pull/1464
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
once the crossterm types are moved to a separate crate.
```diff
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
let crossterm_color = crossterm::style::Color::Black;
- let ratatui_color = crossterm_color.into();
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
- let crossterm_color = ratatui_color.into();
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
+ let crossterm_color = ratatui_color.into_crossterm();
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
- let ratatui_modifier = crossterm_attribute.into();
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
- let crossterm_attribute = ratatui_modifier.into();
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
```
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
Crossterm and the various Termion and Termwiz types as well.
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
[#1471]: https://github.com/ratatui/ratatui/pull/1471
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
for `Bar::label()`:
```diff
- Bar::default().label("foo".into());
+ Bar::default().label("foo");
```
for `BarGroup::label()`:
```diff
- BarGroup::default().label("bar".into());
+ BarGroup::default().label("bar");
```
### `Bar::text_value` now accepts `Into<String>` ([#1471])
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
for `Bar::text_value()`:
```diff
- Bar::default().text_value("foobar".into());
+ Bar::default().text_value("foobar");
```
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
[#1326]: https://github.com/ratatui/ratatui/pull/1326
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
instead of `&[u64]`.
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
data points differently, depending on whether they are present or absent.
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
addition to any other styling applied to the `Sparkline`.
Several `From` implementations have been added to `SparklineBar` to support existing callers who
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
If you encounter any type inference issues, you may need to provide an explicit type for the data
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
`into()` to convert it to form that can be used as a `SparklineBar`:
```diff
let value = 1u8;
- Sparkline::default().data(&[value.into()]);
+ Sparkline::default().data(&[u64::from(value)]);
```
As a consequence of this change, the `data` method is no longer a `const fn`.
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
[#1418]: https://github.com/ratatui/ratatui/pull/1418
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
single `palette::Hsl` value and is gated behind a `palette` feature flag.
```diff
- Color::from_hsl(360.0, 100.0, 100.0)
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
```
### Removed public fields from `Rect` iterators ([#1358], [#1424])
[#1358]: https://github.com/ratatui/ratatui/pull/1358
[#1424]: https://github.com/ratatui/ratatui/pull/1424
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
These fields were not intended to be public and should not have been accessed directly.
### `Rect::area()` now returns u32 instead of u16 ([#1378])
[#1378]: https://github.com/ratatui/ratatui/pull/1378
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
### `Line` now implements `From<Cow<str>` ([#1373])
[#1373]: https://github.com/ratatui/ratatui/pull/1373
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }
let foo = Foo { ... };
let line = Line::from(foo); // now fails due to now ambiguous inferred type
// replace with e.g.
let line = Line::from(String::from(foo));
```
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
[#1413]: https://github.com/ratatui/ratatui/pull/1413
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
any code already using parameter type inference:
```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
[#1331]: https://github.com/ratatui/ratatui/pull/1331
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
Also, the serialized output of the `TableState` will now include the "selected_column" field.
Software that manually parse the serialized the output (with anything other than the `Serialize`
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
implementation on the state.
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
[#1254]: https://github.com/ratatui/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/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/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/ratatui/pull/1173
[#1283]: https://github.com/ratatui/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/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/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/ratatui/pull/1234
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui/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/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui/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/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/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/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/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/ratatui/releases/tag/v0.26.0)
## [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])
[#881]: https://github.com/ratatui/ratatui/pull/881
[#881]: https://github.com/ratatui-org/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
@@ -491,9 +72,9 @@ existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Le
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui/ratatui/pull/774
[#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.
@@ -508,9 +89,9 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui/ratatui/pull/776
[#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
@@ -524,7 +105,7 @@ by removing the call to `.collect()`.
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui/ratatui/pull/751
[#751]: https://github.com/ratatui-org/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
@@ -534,7 +115,7 @@ To use the previous default values, call `table.segment_size(Default::default())
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui/ratatui/pull/754
[#754]: https://github.com/ratatui-org/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
@@ -551,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
@@ -560,7 +143,7 @@ The following example shows how to migrate for `Line`, but the same applies for
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui/ratatui/pull/720
[#720]: https://github.com/ratatui-org/ratatui/pull/720
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
@@ -568,7 +151,7 @@ longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui/ratatui/pull/708
[#708]: https://github.com/ratatui-org/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
@@ -592,11 +175,11 @@ the `Span::style` field.
.alignment(Alignment::Left);
```
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0)
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui/ratatui/pull/691
[#691]: https://github.com/ratatui-org/ratatui/pull/691
These items were deprecated since 0.10.
@@ -610,7 +193,7 @@ These items were deprecated since 0.10.
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui/ratatui/pull/672
[#672]: https://github.com/ratatui-org/ratatui/pull/672
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
@@ -625,7 +208,7 @@ E.g.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui/ratatui/pull/635
[#635]: https://github.com/ratatui-org/ratatui/pull/635
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
@@ -637,7 +220,7 @@ widget in the default configuration would not show any indication of the selecte
### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui/ratatui/pull/664
[#664]: https://github.com/ratatui-org/ratatui/pull/664
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
@@ -663,7 +246,7 @@ or complex, it may be convenient to replace `Table::new` with `Table::default().
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui/ratatui/pull/663
[#663]: https://github.com/ratatui-org/ratatui/pull/663
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
@@ -679,7 +262,7 @@ E.g.
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui/ratatui/pull/557
[#557]: https://github.com/ratatui-org/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor.
@@ -696,18 +279,18 @@ let layout = layout::default()
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0)
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui/ratatui/pull/456
[#456]: https://github.com/ratatui-org/ratatui/pull/456
In order to support larger content lengths, the `position`, `content_length` and
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
[#529]: https://github.com/ratatui/ratatui/issues/529
[#529]: https://github.com/ratatui-org/ratatui/issues/529
Applications can now set custom borders on a `Block` by calling `border_set()`. The
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
@@ -721,7 +304,7 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui/ratatui/issues/530
[#530]: https://github.com/ratatui-org/ratatui/issues/530
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
@@ -735,7 +318,7 @@ instance of a Frame. E.g.:
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui/ratatui/issues/466
[#466]: https://github.com/ratatui-org/ratatui/issues/466
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
@@ -753,7 +336,7 @@ longer compile. E.g.
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui/ratatui/issues/426
[#426]: https://github.com/ratatui-org/ratatui/issues/426
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`.
@@ -766,11 +349,11 @@ longer compile. E.g.
+ buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0)
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui/ratatui/issues/360
[#360]: https://github.com/ratatui-org/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
@@ -782,7 +365,7 @@ The track symbol of `Scrollbar` is now optional, this method now takes an option
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui/ratatui/issues/330
[#330]: https://github.com/ratatui-org/ratatui/issues/330
The symbols for defining scrollbars have been moved to the `symbols` module from the
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
@@ -796,31 +379,31 @@ new module locations. E.g.:
### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui/ratatui/issues/361
[#361]: https://github.com/ratatui-org/ratatui/issues/361
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205])
[#205]: https://github.com/ratatui/ratatui/issues/205
[#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/ratatui/releases/tag/v0.21.0)
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui/ratatui/issues/171
[#171]: https://github.com/ratatui-org/ratatui/issues/171
The minimum supported rust version is now 1.65.0.
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
[#114]: https://github.com/ratatui/ratatui/issues/114
[#114]: https://github.com/ratatui-org/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
@@ -837,7 +420,7 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui/ratatui/issues/168
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
@@ -851,7 +434,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui/ratatui/issues/133
[#133]: https://github.com/ratatui-org/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
@@ -863,20 +446,20 @@ the existing code.
+ let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0)
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
[Changelog](./CHANGELOG.md) for more details.
### MSRV is update to 1.63.0 ([#80])
[#80]: https://github.com/ratatui/ratatui/issues/80
[#80]: https://github.com/ratatui-org/ratatui/issues/80
The minimum supported rust version is 1.63.0
### List no longer ignores empty string in items ([#42])
[#42]: https://github.com/ratatui/ratatui/issues/42
[#42]: https://github.com/ratatui-org/ratatui/issues/42
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
need to manually filter empty items prior to display.

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,7 +8,7 @@ creating a new issue before making the change, or starting a discussion on
## Reporting issues
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
please check that it has not already been reported by searching for some related keywords. Please
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
found.
@@ -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
@@ -31,8 +31,8 @@ guarantee that the behavior is unchanged.
### Code formatting
Run `cargo xtask format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
### Search `tui-rs` for similar work
@@ -56,9 +56,11 @@ documented.
### Run CI tests before pushing a PR
Running `cargo xtask ci` before pushing will perform the same checks that we do in the CI process.
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
@@ -71,22 +73,22 @@ in GitHub docs.
### Setup
TL;DR: Clone the repo and build it using `cargo xtask`.
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
Ratatui is an ordinary Rust project where common tasks are managed with
[cargo-xtask](https://github.com/matklad/cargo-xtask). It wraps common `cargo` commands with sane
[cargo-make](https://github.com/sagiegurari/cargo-make/). It wraps common `cargo` commands with sane
defaults depending on your platform of choice. Building the project should be as easy as running
`cargo xtask build`.
`cargo make build`.
```shell
git clone https://github.com/ratatui/ratatui.git
git clone https://github.com/ratatui-org/ratatui.git
cd ratatui
cargo xtask build
cargo make build
```
### Tests
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
@@ -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,26 +165,26 @@ 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/ratatui/discussions/66) for more about the decision.
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
## Continuous Integration
We use GitHub Actions for the CI where we perform the following checks:
We use Github Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass.
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo xtask ci` which will offer you
You can also check most of those things yourself locally using `cargo make ci` which will offer you
a shorter feedback loop than pushing to github.
## Relationship with `tui-rs`
@@ -191,12 +193,12 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
([@fdehau](https://github.com/fdehau)).
The original repository contains all the issues, PRs, and discussion that were raised originally, and
The original repository contains all the issues, PRs and discussion that were raised originally, and
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
We imported all the PRs from the original repository, implemented many of the smaller ones, and
We imported all the PRs from the original repository and implemented many of the smaller ones and
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
We have documented the current state of those PRs, and anyone is welcome to pick them up and
continue the work on them.

3953
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,338 @@
[workspace]
resolver = "2"
members = ["ratatui", "ratatui-*", "xtask"]
default-members = [
"ratatui",
"ratatui-core",
"ratatui-crossterm",
# this is not included as it doesn't compile on windows
# "ratatui-termion",
"ratatui-termwiz",
"ratatui-widgets",
]
[workspace.package]
[package]
name = "ratatui"
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/"
repository = "https://github.com/ratatui/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"]
categories = ["command-line-interface"]
repository = "https://github.com/ratatui-org/ratatui"
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
exclude = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
autoexamples = true
edition = "2021"
rust-version = "1.74.0"
[workspace.dependencies]
bitflags = "2.6.0"
color-eyre = "0.6.3"
crossterm = "0.28.1"
document-features = "0.2.7"
indoc = "2.0.5"
instability = "0.3.3"
itertools = "0.13.0"
pretty_assertions = "1.4.1"
ratatui = { path = "ratatui", version = "0.30.0-alpha.0" }
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.0" }
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.0" }
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.0" }
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.0" }
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.0" }
rstest = "0.23.0"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
strum = { version = "0.26.3", features = ["derive"] }
termwiz = { version = "0.22.0" }
unicode-segmentation = "1.12.0"
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
unicode-width = "=0.2.0"
termion = "4.0.0"
[badges]
# Improve benchmark consistency
[profile.bench]
codegen-units = 1
lto = true
[dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.22.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.26", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.12.0"
stability = "0.2.0"
compact_str = "0.7.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"] }
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.19.0"
serde_json = "1.0.109"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
cargo = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
wildcard_imports = "allow"
# nursery or restricted
as_underscore = "warn"
deref_by_slicing = "warn"
else_if_without_else = "warn"
empty_line_after_doc_comments = "warn"
equatable_if_let = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
map_err_ignore = "warn"
missing_const_for_fn = "warn"
mixed_read_write_in_expression = "warn"
mod_module_files = "warn"
needless_raw_strings = "warn"
redundant_type_annotations = "warn"
rest_pat_in_fully_bound_structs = "warn"
string_lit_chars_any = "warn"
string_to_string = "warn"
use_self = "warn"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
## 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 and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## 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].
## 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 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`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
#! 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 = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
## 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` and `StatefulWidgetRef` traits which are experimental and may change in
## the future.
unstable-widget-ref = []
## Enables the `WidgetExt` trait which is experimental and may change in the future.
unstable-widget-ext = []
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[[bench]]
name = "barchart"
harness = false
[[bench]]
name = "block"
harness = false
[[bench]]
name = "list"
harness = false
[lib]
bench = false
[[bench]]
name = "paragraph"
harness = false
[[bench]]
name = "sparkline"
harness = false
[[example]]
name = "ansi_buffer"
required-features = ["unstable-widget-ext", "unstable-widget-ref"]
doc-scrape-examples = true
[[example]]
name = "block"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "canvas"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "chart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "colors"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "colors_rgb"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "demo"
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
doc-scrape-examples = false
[[example]]
name = "demo2"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "docsrs"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hello_world"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "layout"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "modifiers"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "panic"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "paragraph"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "popup"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "ratatui-logo"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "scrollbar"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "sparkline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "table"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tabs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "user_input"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "widget_ext"
required-features = ["unstable-widget-ext"]
doc-scrape-examples = true
[[test]]
name = "state_serde"
required-features = ["serde"]

View File

@@ -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)
## Past Maintainers
- [fdehau](https://github.com/fdehau)
- [mindoodoo](https://github.com/mindoodoo)
- [sayanarijit](https://github.com/sayanarijit)
- [EdJoPaTo](https://github.com/EdJoPaTo)

174
Makefile.toml Normal file
View File

@@ -0,0 +1,174 @@
# configuration for https://github.com/sagiegurari/cargo-make
[config]
skip_core_tasks = true
[env]
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = ["lint-style", "clippy", "check", "test"]
[tasks.lint-style]
description = "Lint code style (formatting, typos, docs)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.lint-format]
description = "Lint code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[tasks.format]
description = "Fix code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all"]
[tasks.lint-typos]
description = "Run typo checks"
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.lint-docs]
description = "Check documentation for errors and warnings"
toolchain = "nightly"
command = "cargo"
args = [
"rustdoc",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
]
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = [
"check",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.build]
description = "Compile the project"
command = "cargo"
args = [
"build",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.clippy]
description = "Run Clippy for linting"
command = "cargo"
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-D",
"warnings",
]
[tasks.install-nextest]
description = "Install cargo-nextest"
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
[tasks.test]
description = "Run tests"
run_task = { name = ["test-lib", "test-doc"] }
[tasks.test-lib]
description = "Run default tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"nextest",
"run",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"nextest",
"run",
"--all-targets",
"--no-default-features",
"--features",
"${ALL_FEATURES},${@}",
]
[tasks.coverage]
description = "Generate code coverage report"
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}",
"--features",
"all-widgets",
]
[tasks.build-examples]
description = "Compile project examples"
command = "cargo"
args = ["build", "--examples", "--release", "--features", "all-widgets"]
[tasks.run-examples]
description = "Run project examples"
dependencies = ["build-examples"]
script = '''
#!@duckscript
files = glob_array ./examples/*.rs
for file in ${files}
name = basename ${file}
name = substring ${name} -3
set_env TUI_EXAMPLE_NAME ${name}
cm_run_task run-example
end
'''

489
README.md
View File

@@ -2,19 +2,16 @@
<summary>Table of Contents</summary>
- [Ratatui](#ratatui)
- [Quick Start](#quickstart)
- [Other documentation](#other-documentation)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other Documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
@@ -24,14 +21,14 @@
<!-- cargo-rdme start -->
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
<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]
@@ -44,7 +41,7 @@ Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
## Quickstart
## Installation
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
@@ -52,47 +49,9 @@ Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
cargo add ratatui crossterm
```
Then you can create a simple "Hello World" application:
```rust
use crossterm::event::{self, Event};
use ratatui::{text::Text, Frame};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal.draw(draw).expect("failed to draw frame");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
fn draw(frame: &mut Frame) {
let text = Text::raw("Hello World!");
frame.render_widget(text, frame.area());
}
```
The full code for this example which contains a little more detail is in the [Examples]
directory. For more guidance on different ways to structure your application see the
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
## 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.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
## Introduction
@@ -102,71 +61,57 @@ This is in contrast to the retained mode style of rendering where widgets are up
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
## Quickstart
The following example demonstrates the minimal amount of code necessary to setup a terminal and
render "Hello World!". The full code for this example which contains a little more detail is in
the [Examples] directory. For more guidance on different ways to structure your application see
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
Every application built with `ratatui` needs to implement the following steps:
- Initialize the terminal
- A main loop that:
- Draws the UI
- Handles input events
- A main loop to:
- Handle input events
- Draw the UI
- Restore the terminal state
The library contains a [`prelude`] module that re-exports the most commonly used traits and
types for convenience. Most examples in the documentation will use this instead of showing the
full path of each type.
### Initialize and restore the terminal
The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
the screen, hide the cursor, etc. There are backend implementations for [Crossterm], [Termion]
and [Termwiz].
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
[Termwiz].
The simplest way to initialize the terminal is to use the [`init`] function which returns a
[`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
and sets up a panic hook that restores the terminal in case of panic. This instance can then be
used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
terminal to its original state.
```rust
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
```
See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
the alternate screen and raw mode.
Most applications should enter the Alternate Screen when starting and leave it when exiting and
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
module] and the [Backends] section of the [Ratatui Website] for more info.
### Drawing the UI
Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
method takes a closure that is called with a [`Frame`] instance. The [`Frame`] provides the size
of the area to draw to and allows the app to render any [`Widget`] using the provided
[`render_widget`] method. After this closure returns, a diff is performed and only the changes
are drawn to the terminal. See the [Widgets] section of the [Ratatui Website] for more info.
The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
```rust
use ratatui::{widgets::Paragraph, Frame};
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
loop {
terminal.draw(|frame| draw(frame))?;
if handle_events()? {
break Ok(());
}
}
}
fn draw(frame: &mut Frame) {
let text = Paragraph::new("Hello World!");
frame.render_widget(text, frame.area());
}
```
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. See the [Widgets] section of the [Ratatui Website]
for more info.
### Handling events
@@ -175,23 +120,58 @@ calling backend library methods directly. See the [Handling Events] section of t
Website] for more info. For example, if you are using [Crossterm], you can use the
[`crossterm::event`] module to handle events.
```rust
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
### Example
fn handle_events() -> std::io::Result<bool> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') => return Ok(true),
// handle other key events
_ => {}
},
// handle other events
_ => {}
```rust
use std::io::{self, stdout};
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()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
@@ -200,36 +180,46 @@ 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 draw(frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
fn ui(frame: &mut Frame) {
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],
);
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [title_area, main_area, status_area] = vertical.areas(frame.area());
let horizontal = Layout::horizontal([Fill(1); 2]);
let [left_area, right_area] = horizontal.areas(main_area);
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::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
}
```
Running this example produces the following output:
```text
Title Bar───────────────────────────────────
┌Left────────────────┐┌Right───────────────┐
│ ││ │
└────────────────────┘└────────────────────┘
Status Bar──────────────────────────────────
```
![docsrs-layout]
## Text and styling
@@ -244,44 +234,55 @@ 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 draw(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
fn ui(frame: &mut Frame) {
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]);
}
```
Running this example produces the following output:
![docsrs-styling]
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
@@ -292,18 +293,21 @@ fn draw(frame: &mut Frame) {
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[templates]: https://github.com/ratatui/templates/
[Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
[templates]: https://github.com/ratatui-org/templates/
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Docs]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
@@ -322,58 +326,121 @@ fn draw(frame: &mut Frame) {
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui
[GitHub Sponsors]: https://github.com/sponsors/ratatui
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[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
[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
[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
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
<!-- cargo-rdme end -->
## Contributing
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
the community forked the project and created this crate. We look forward to continuing the work
started by Florian 🚀
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
We have also recently launched the [Ratatui Forum][Forum], 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 [contributing](./CONTRIBUTING.md) guidelines, especially if you are
interested in working on a PR or issue opened in the previous repository.
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.
## Built with Ratatui
## Rust version requirements
Ratatui has a number of built-in [widgets](https://docs.rs/ratatui/latest/ratatui/widgets/), as well
as many contributed by external contributors. Check out the [Showcase](https://ratatui.rs/showcase/)
section of the website, or the [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) repo
for a curated list of awesome apps/libraries built with `ratatui`!
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Widgets
### Built in
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
rendering [points, lines, shapes and a world
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each widget has an associated example which can be found in the [Examples] folder. Run each example
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui-org/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & crossterm
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
with a React/Elm inspired approach
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
Tui-realm
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Widget for tree data
structures.
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
windows and their rendering
- [tui-textarea](https://github.com/rhysd/tui-textarea) — Simple yet powerful multi-line text editor
widget supporting several key shortcuts, undo/redo, text search, etc.
- [tui-input](https://github.com/sayanarijit/tui-input) — TUI input library supporting multiple
backends and tui-rs.
- [tui-term](https://github.com/a-kenji/tui-term) — A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
## Apps
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
## Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) or
[iocraft](https://github.com/ccbrown/iocraft/) for an alternative solutions
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
to build text user interfaces in Rust.
## Acknowledgments
None of this could be possible without [**Florian Dehau**](https://github.com/fdehau) who originally
created [tui-rs] which inspired many Rust TUIs.
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui organization.
awesome logo** for the ratatui project and ratatui-org organization.
## License

View File

@@ -1,12 +1,5 @@
# Creating a Release
Our release strategy is:
> Release major versions with detailed summaries when necessary, while releasing minor versions
> weekly or as needed without extensive announcements.
>
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
@@ -19,7 +12,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
```
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
@@ -28,16 +21,16 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[0.x.y]`
1. Create a new tag: `git tag -a v[X.Y.Z]`
1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui/ratatui/actions) workflow to
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
finish.
## Alpha Releases
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
and can be manually be created when necessary by triggering the [Continuous
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
Deployment](https://github.com/ratatui-org/ratatui/actions/workflows/cd.yml) workflow.
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
@@ -47,5 +40,5 @@ These releases will have whatever happened to be in main at the time of release,
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases.
See [#147](https://github.com/ratatui/ratatui/issues/147) and
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.
See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
[#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.

View File

@@ -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/ratatui/security/advisories/new>
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new

View File

@@ -8,66 +8,58 @@
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--all-features"]
command = ["cargo", "check", "--all-features", "--color", "always"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--all-features"]
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
need_stdout = false
[jobs.check-crossterm]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"crossterm",
]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
need_stdout = false
[jobs.check-termion]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"termion",
]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
need_stdout = false
[jobs.check-termwiz]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"termwiz",
]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
need_stdout = false
[jobs.clippy]
command = ["cargo", "clippy", "--all-targets"]
command = [
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
need_stdout = false
[jobs.test]
command = ["cargo", "test", "--all-features"]
command = [
"cargo", "test",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.test-unit]
command = ["cargo", "test", "--lib", "--all-features"]
command = [
"cargo", "test",
"--lib",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = [
"cargo",
"+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
]
env.RUSTDOCFLAGS = "--cfg docsrs"
@@ -77,12 +69,10 @@ need_stdout = false
# to the previous job
[jobs.doc-open]
command = [
"cargo",
"+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
"--open",
]
@@ -92,34 +82,19 @@ on_success = "job:doc" # so that we don't open the browser at each change
[jobs.coverage]
command = [
"cargo",
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--all-features",
"--color", "always",
]
[jobs.coverage-unit-tests-only]
command = [
"cargo",
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--lib",
"--all-features",
]
[jobs.hack]
command = [
"cargo",
"hack",
"test",
"--lib",
"--each-feature",
# "--all-targets",
"--workspace",
"--color", "always",
]
# You may define here keybindings that would be specific to
@@ -127,7 +102,7 @@ command = [
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
ctrl-h = "job:hack"
# alt-m = "job:my-job"
ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz"

View File

@@ -1,8 +1,9 @@
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion};
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::{Direction, Rect},
layout::Rect,
prelude::Direction,
widgets::{Bar, BarChart, BarGroup, Widget},
};
@@ -15,7 +16,7 @@ fn barchart(c: &mut Criterion) {
let data: Vec<Bar> = (0..data_count)
.map(|i| {
Bar::default()
.label(format!("B{i}"))
.label(format!("B{i}").into())
.value(rng.gen_range(0..data_count))
})
.collect();
@@ -58,7 +59,7 @@ fn barchart(c: &mut Criterion) {
fn render(bencher: &mut Bencher, barchart: &BarChart) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| barchart.clone(),
|bench_barchart| {
@@ -69,3 +70,4 @@ fn render(bencher: &mut Bencher, barchart: &BarChart) {
}
criterion_group!(benches, barchart);
criterion_main!(benches);

View File

@@ -1,9 +1,11 @@
use criterion::{criterion_group, BatchSize, Bencher, Criterion};
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
text::Line,
widgets::{Block, Padding, Widget},
widgets::{
block::{Position, Title},
Block, Borders, Padding, Widget,
},
};
/// Benchmark for rendering a block.
@@ -27,10 +29,15 @@ fn block(c: &mut Criterion) {
// Render with all features
group.bench_with_input(
format!("render_all_feature/{width}x{height}"),
&Block::bordered()
.padding(Padding::new(5, 5, 2, 2))
&Block::new()
.borders(Borders::ALL)
.title("test title")
.title_bottom(Line::from("bottom left title").alignment(Alignment::Right)),
.title(
Title::from("bottom left title")
.alignment(Alignment::Right)
.position(Position::Bottom),
)
.padding(Padding::new(5, 5, 2, 2)),
|b, block| render(b, block, buffer_size),
);
}
@@ -42,7 +49,7 @@ fn block(c: &mut Criterion) {
fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
let mut buffer = Buffer::empty(size);
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| block.to_owned(),
|bench_block| {
@@ -53,3 +60,4 @@ fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
}
criterion_group!(benches, block);
criterion_main!(benches);

View File

@@ -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,
@@ -45,7 +45,7 @@ fn list(c: &mut Criterion) {
fn render(bencher: &mut Bencher, list: &List) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
@@ -59,7 +59,7 @@ fn render(bencher: &mut Bencher, list: &List) {
fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
@@ -70,3 +70,4 @@ fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
}
criterion_group!(benches, list);
criterion_main!(benches);

View File

@@ -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,
@@ -70,7 +72,7 @@ fn paragraph(c: &mut Criterion) {
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| paragraph.to_owned(),
|bench_paragraph| {
@@ -92,3 +94,4 @@ fn random_lines(count: u16) -> String {
}
criterion_group!(benches, paragraph);
criterion_main!(benches);

View File

@@ -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,
@@ -31,7 +31,7 @@ fn sparkline(c: &mut Criterion) {
fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| sparkline.clone(),
|bench_sparkline| {
@@ -42,3 +42,4 @@ fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
}
criterion_group!(benches, sparkline);
criterion_main!(benches);

View File

@@ -2,7 +2,7 @@
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui"
owner = "ratatui-org"
repo = "ratatui"
[changelog]
@@ -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
@@ -24,15 +22,27 @@ body = """
{%- if not version %}
## [unreleased]
{% else -%}
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
## [{{ version }}](https://github.com/ratatui-org/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif -%}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}
````text {#- 4 backticks escape any backticks in body #}
{{commit.body | indent(prefix=" ") }}
````
{%- endif %}
{%- for footer in commit.footers %}
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}
@@ -77,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
@@ -121,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

View File

@@ -1,24 +1,4 @@
avoid-breaking-exported-api = false
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
# ratatui -> bitflags v2.3
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
# (also, memoffset, syn, nix, strsim, windows-sys
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"memoffset",
"nix",
"strsim",
"syn",
"windows-sys",
"windows-targets",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"unicode-width",
]
allowed-duplicate-crates = ["bitflags"]

View File

@@ -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",
@@ -11,11 +13,11 @@ allow = [
"MIT",
"Unicode-DFS-2016",
"WTFPL",
"Zlib",
]
[advisories]
version = 2
unmaintained = "deny"
yanked = "deny"
[bans]
multiple-versions = "allow"

View File

@@ -10,39 +10,19 @@ This folder might use unreleased code. View the examples for the latest release
> There are a few workaround for this problem:
>
> - View the examples as they were when the latest version was release by selecting the tag that
> matches that version. E.g. <https://github.com/ratatui/ratatui/tree/v0.26.1/examples>.
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.26.1/examples>.
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
> allows you to select any previous tagged version.
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
> the dependency, or remotely by adding `git = "https://github.com/ratatui/ratatui"`
> the dependency, or remotely by adding `git = "https://github.com/ratatui-org/ratatui"`
>
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
>
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
> `git-cliff -u` against a cloned version of this repository.
## Design choices
The examples contain some opinionated choices in order to make it easier for newer rustaceans to
easily be productive in creating applications:
- Each example has an App struct, with methods that implement a main loop, handle events and drawing
the UI.
- We use color_eyre for handling errors and panics. See [How to use color-eyre with Ratatui] on the
website for more information about this.
- Common code is not extracted into a separate file. This makes each example self-contained and easy
to read as a whole.
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)
@@ -170,31 +139,7 @@ cargo run --example=colors_rgb --features=crossterm
Note: VHs renders full screen animations poorly, so this is a screen capture rather than the output
of the VHS tape.
<https://github.com/ratatui/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
```
<https://github.com/ratatui-org/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
## Custom Widget
@@ -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).
@@ -441,38 +332,30 @@ examples/vhs/generate.bash
```
-->
[barchart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/barchart.gif?raw=true
[barchart-grouped.gif]: https://github.com/ratatui/ratatui/blob/images/examples/barchart-grouped.gif?raw=true
[block.gif]: https://github.com/ratatui/ratatui/blob/images/examples/block.gif?raw=true
[calendar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/calendar.gif?raw=true
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/examples/canvas.gif?raw=true
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/chart.gif?raw=true
[colors.gif]: https://github.com/ratatui/ratatui/blob/images/examples/colors.gif?raw=true
[constraint-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
[constraints.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraints.gif?raw=true
[custom_widget.gif]: https://github.com/ratatui/ratatui/blob/images/examples/custom_widget.gif?raw=true
[demo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo.gif?raw=true
[demo2.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif?raw=true
[flex.gif]: https://github.com/ratatui/ratatui/blob/images/examples/flex.gif?raw=true
[gauge.gif]: https://github.com/ratatui/ratatui/blob/images/examples/gauge.gif?raw=true
[hello_world.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hello_world.gif?raw=true
[hyperlink.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hyperlink.gif?raw=true
[inline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/inline.gif?raw=true
[layout.gif]: https://github.com/ratatui/ratatui/blob/images/examples/layout.gif?raw=true
[list.gif]: https://github.com/ratatui/ratatui/blob/images/examples/list.gif?raw=true
[line_gauge.gif]: https://github.com/ratatui/ratatui/blob/images/examples/line_gauge.gif?raw=true
[minimal.gif]: https://github.com/ratatui/ratatui/blob/images/examples/minimal.gif?raw=true
[modifiers.gif]: https://github.com/ratatui/ratatui/blob/images/examples/modifiers.gif?raw=true
[panic.gif]: https://github.com/ratatui/ratatui/blob/images/examples/panic.gif?raw=true
[paragraph.gif]: https://github.com/ratatui/ratatui/blob/images/examples/paragraph.gif?raw=true
[popup.gif]: https://github.com/ratatui/ratatui/blob/images/examples/popup.gif?raw=true
[ratatui-logo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/scrollbar.gif?raw=true
[sparkline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/sparkline.gif?raw=true
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.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
[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
[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
[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
[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
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
[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/ratatui/blob/images/examples/tabs.gif?raw=true
[tracing.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tracing.gif?raw=true
[user_input.gif]: https://github.com/ratatui/ratatui/blob/images/examples/user_input.gif?raw=true
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.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
[BREAKING-CHANGES.md]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
[BREAKING-CHANGES.md]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md

8
examples/ansi_buffer.rs Normal file
View File

@@ -0,0 +1,8 @@
use ratatui::{prelude::*, widgets::ansi_string_buffer::AnsiStringBuffer};
fn main() {
let mut buf = AnsiStringBuffer::new(5, 2);
buf.render_ref(&Line::styled("Hello", Color::Blue), Rect::new(0, 0, 5, 1));
buf.render_ref(&Line::styled("World", Color::Green), Rect::new(0, 1, 5, 1));
println!("{buf}");
}

300
examples/barchart.rs Normal file
View File

@@ -0,0 +1,300 @@
//! # [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::{
error::Error,
io,
time::{Duration, Instant},
};
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, Borders, Paragraph},
};
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
}
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();
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(())
}
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.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
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::default().title("Data1").borders(Borders::ALL))
.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);
}
#[allow(clippy::cast_precision_loss)]
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
app.months
.iter()
.enumerate()
.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()
}
#[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::default().title("Data1").borders(Borders::ALL))
.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::default().title("Data1").borders(Borders::ALL))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
.direction(Direction::Horizontal);
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);
}
}
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(vec![Span::styled(
"- Company C",
Style::default().fg(Color::White),
)]),
];
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}

261
examples/block.rs Normal file
View File

@@ -0,0 +1,261 @@
//! # [Ratatui] Block 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::{
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
};
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let result = run(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
}
fn setup_terminal() -> Result<Terminal> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
loop {
terminal.draw(ui)?;
if handle_events()?.is_break() {
return Ok(());
}
}
}
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(ControlFlow::Break(()));
}
}
}
Ok(ControlFlow::Continue(()))
}
fn ui(frame: &mut Frame) {
let (title_area, layout) = calculate_layout(frame.size());
render_title(frame, title_area);
let paragraph = placeholder_paragraph();
render_borders(&paragraph, Borders::ALL, frame, layout[0][0]);
render_borders(&paragraph, Borders::NONE, frame, layout[0][1]);
render_borders(&paragraph, Borders::LEFT, frame, layout[1][0]);
render_borders(&paragraph, Borders::RIGHT, frame, layout[1][1]);
render_borders(&paragraph, Borders::TOP, frame, layout[2][0]);
render_borders(&paragraph, Borders::BOTTOM, frame, layout[2][1]);
render_border_type(&paragraph, BorderType::Plain, frame, layout[3][0]);
render_border_type(&paragraph, BorderType::Rounded, frame, layout[3][1]);
render_border_type(&paragraph, BorderType::Double, frame, layout[4][0]);
render_border_type(&paragraph, BorderType::Thick, frame, layout[4][1]);
render_styled_block(&paragraph, frame, layout[5][0]);
render_styled_borders(&paragraph, frame, layout[5][1]);
render_styled_title(&paragraph, frame, layout[6][0]);
render_styled_title_content(&paragraph, frame, layout[6][1]);
render_multiple_titles(&paragraph, frame, layout[7][0]);
render_multiple_title_positions(&paragraph, frame, layout[7][1]);
render_padding(&paragraph, frame, layout[8][0]);
render_nested_blocks(&paragraph, frame, layout[8][1]);
}
/// Calculate the layout of the UI elements.
///
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let block_layout = Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = main_layout.areas(area);
let main_areas = block_layout
.split(main_area)
.iter()
.map(|&area| {
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})
.collect_vec();
(title_area, main_areas)
}
fn render_title(frame: &mut Frame, area: Rect) {
frame.render_widget(
Paragraph::new("Block example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center),
area,
);
}
fn placeholder_paragraph() -> Paragraph<'static> {
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
}
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(border)
.title(format!("Borders::{border:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_border_type(
paragraph: &Paragraph,
border_type: BorderType,
frame: &mut Frame,
area: Rect,
) {
let block = Block::new()
.borders(Borders::ALL)
.border_type(border_type)
.title(format!("BorderType::{border_type:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::new().blue().on_white().bold().italic())
.title("Styled borders");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.style(Style::new().blue().on_white().bold().italic())
.title("Styled block");
frame.render_widget(paragraph.clone().block(block), area);
}
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let title = Line::from(vec![
"Styled ".blue().on_white().bold().italic(),
"title content".red().on_white().bold().italic(),
]);
let block = Block::new().borders(Borders::ALL).title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Multiple".blue().on_white().bold().italic())
.title("Titles".red().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title(
Title::from("top left")
.position(Position::Top)
.alignment(Alignment::Left),
)
.title(
Title::from("top center")
.position(Position::Top)
.alignment(Alignment::Center),
)
.title(
Title::from("top right")
.position(Position::Top)
.alignment(Alignment::Right),
)
.title(
Title::from("bottom left")
.position(Position::Bottom)
.alignment(Alignment::Left),
)
.title(
Title::from("bottom center")
.position(Position::Bottom)
.alignment(Alignment::Center),
)
.title(
Title::from("bottom right")
.position(Position::Bottom)
.alignment(Alignment::Right),
);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Padding")
.padding(Padding::new(5, 10, 1, 2));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
let inner = outer_block.inner(area);
frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner);
}

View File

@@ -9,44 +9,58 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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 color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Margin},
style::{Color, Modifier, Style},
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
DefaultTerminal, Frame,
#![allow(clippy::wildcard_imports)]
use std::{error::Error, io};
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<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn main() -> Result<(), Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
let _ = terminal.draw(draw);
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break Ok(());
}
#[allow(clippy::single_match)]
match key.code {
KeyCode::Char(_) => {
break;
}
_ => {}
};
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn draw(frame: &mut Frame) {
let area = frame.area().inner(Margin {
vertical: 1,
horizontal: 1,
});
fn draw(f: &mut Frame) {
let app_area = f.size();
let calarea = Rect {
x: app_area.x + 1,
y: app_area.y + 1,
height: app_area.height - 1,
width: app_area.width - 1,
};
let mut start = OffsetDateTime::now_local()
.unwrap()
@@ -58,7 +72,7 @@ fn draw(frame: &mut Frame) {
let list = make_dates(start.year());
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea);
let cols = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row)
@@ -66,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();
}
}
@@ -152,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> {

View File

@@ -9,45 +9,32 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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
#![allow(clippy::wildcard_imports)]
use std::{
io::stdout,
io::{self, stdout, Stdout},
time::{Duration, Instant},
};
use color_eyre::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, KeyEventKind},
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, MouseEventKind},
layout::{Constraint, Layout, Position, Rect},
style::{Color, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle},
Block, Widget,
},
DefaultTerminal, Frame,
prelude::*,
widgets::{canvas::*, *},
};
fn main() -> Result<()> {
color_eyre::install()?;
stdout().execute(EnableMouseCapture)?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
stdout().execute(DisableMouseCapture)?;
app_result
fn main() -> io::Result<()> {
App::run()
}
struct App {
exit: bool,
x: f64,
y: f64,
ball: Circle,
@@ -56,14 +43,11 @@ struct App {
vy: f64,
tick_count: u64,
marker: Marker,
points: Vec<Position>,
is_drawing: bool,
}
impl App {
const fn new() -> Self {
fn new() -> Self {
Self {
exit: false,
x: 0.0,
y: 0.0,
ball: Circle {
@@ -77,56 +61,36 @@ impl App {
vy: 1.0,
tick_count: 0,
marker: Marker::Dot,
points: vec![],
is_drawing: false,
}
}
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(16);
pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = Self::new();
let mut last_tick = Instant::now();
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
let tick_rate = Duration::from_millis(16);
loop {
let _ = terminal.draw(|frame| app.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
match event::read()? {
Event::Key(key) => self.handle_key_press(key),
Event::Mouse(event) => self.handle_mouse_event(event),
_ => (),
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
self.on_tick();
app.on_tick();
last_tick = Instant::now();
}
}
Ok(())
}
fn handle_key_press(&mut self, key: event::KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
}
match key.code {
KeyCode::Char('q') => self.exit = true,
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
}
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
match event.kind {
MouseEventKind::Down(_) => self.is_drawing = true,
MouseEventKind::Up(_) => self.is_drawing = false,
MouseEventKind::Drag(_) => {
self.points.push(Position::new(event.column, event.row));
}
_ => {}
}
restore_terminal()
}
fn on_tick(&mut self) {
@@ -159,23 +123,21 @@ impl App {
self.ball.y += self.vy;
}
fn draw(&self, frame: &mut Frame) {
fn ui(&self, frame: &mut Frame) {
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [left, right] = horizontal.areas(frame.area());
let [draw, map] = vertical.areas(left);
let [map, right] = horizontal.areas(frame.size());
let [pong, boxes] = vertical.areas(right);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.draw_canvas(draw), draw);
frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes);
}
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("World"))
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
@@ -188,33 +150,9 @@ impl App {
.y_bounds([-90.0, 90.0])
}
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("Draw here"))
.marker(self.marker)
.x_bounds([0.0, f64::from(area.width)])
.y_bounds([0.0, f64::from(area.height)])
.paint(move |ctx| {
let points = self
.points
.iter()
.map(|p| {
(
f64::from(p.x) - f64::from(area.left()),
f64::from(area.bottom()) - f64::from(p.y),
)
})
.collect_vec();
ctx.draw(&Points {
coords: &points,
color: Color::White,
});
})
}
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("Pong"))
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
@@ -229,7 +167,7 @@ impl App {
let bottom = 0.0;
let top = f64::from(area.height).mul_add(2.0, -4.0);
Canvas::default()
.block(Block::bordered().title("Rects"))
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])
@@ -261,3 +199,15 @@ impl App {
})
}
}
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -9,38 +9,25 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
text::{Line, Span},
widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
DefaultTerminal, Frame,
use std::{
error::Error,
io,
time::{Duration, Instant},
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
struct App {
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
data2: Vec<(f64, f64)>,
window: [f64; 2],
}
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{block::Title, Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition},
};
#[derive(Clone)]
struct SinSignal {
@@ -70,6 +57,14 @@ impl Iterator for SinSignal {
}
}
struct App {
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
data2: Vec<(f64, f64)>,
window: [f64; 2],
}
impl App {
fn new() -> Self {
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
@@ -85,27 +80,6 @@ impl App {
}
}
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(250);
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| self.draw(frame))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
self.on_tick();
last_tick = Instant::now();
}
}
}
fn on_tick(&mut self) {
self.data1.drain(0..5);
self.data1.extend(self.signal1.by_ref().take(5));
@@ -116,105 +90,124 @@ impl App {
self.window[0] += 1.0;
self.window[1] += 1.0;
}
}
fn draw(&self, frame: &mut Frame) {
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
let [animated_chart, bar_chart] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
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)?;
self.render_animated_chart(frame, animated_chart);
render_barchart(frame, bar_chart);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
// 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:?}");
}
fn render_animated_chart(&self, frame: &mut Frame, area: Rect) {
let x_labels = vec![
Span::styled(
format!("{}", self.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
Span::styled(
format!("{}", self.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&self.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&self.data2),
];
Ok(())
}
let chart = Chart::new(datasets)
.block(Block::bordered())
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(self.window),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
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))?;
frame.render_widget(chart, area);
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(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
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),
]);
fn ui(frame: &mut Frame, app: &App) {
let area = frame.size();
let chart = Chart::new(vec![dataset])
.block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered()))
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_chart1(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(["0".bold(), "50".into(), "100.0".bold()]),
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(app.window),
)
.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)));
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
frame.render_widget(chart, bar_chart);
f.render_widget(chart, area);
}
fn render_line_chart(frame: &mut Frame, area: Rect) {
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic())
.marker(symbols::Marker::Braille)
@@ -223,28 +216,36 @@ fn render_line_chart(frame: &mut Frame, area: Rect) {
.data(&[(1., 1.), (4., 4.)])];
let chart = Chart::new(datasets)
.block(Block::bordered().title(Line::from("Line chart").cyan().bold().centered()))
.block(
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
.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)));
frame.render_widget(chart, area);
f.render_widget(chart, area);
}
fn render_scatter(frame: &mut Frame, area: Rect) {
fn render_scatter(f: &mut Frame, area: Rect) {
let datasets = vec![
Dataset::default()
.name("Heavy")
@@ -267,24 +268,30 @@ fn render_scatter(frame: &mut Frame, area: Rect) {
];
let chart = Chart::new(datasets)
.block(Block::bordered().title(Line::from("Scatter chart").cyan().bold().centered()))
.block(
Block::new().borders(Borders::all()).title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
.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)));
frame.render_widget(chart, area);
f.render_widget(chart, area);
}
// Data from https://ourworldindata.org/space-exploration-satellites

View File

@@ -9,50 +9,64 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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
// This example shows all the colors supported by ratatui. It will render a grid of foreground
// and background colors with their names and indexes.
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, Borders, Paragraph},
DefaultTerminal, Frame,
use std::{
error::Error,
io::{self, Stdout},
result,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
}
fn draw(frame: &mut Frame) {
fn ui(frame: &mut Frame) {
let layout = Layout::vertical([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.area());
.split(frame.size());
render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]);
@@ -99,16 +113,19 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
let areas = vertical.iter().flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
});
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) {
let layout = Layout::vertical([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
.collect_vec();
for (i, &fg) in NAMED_COLORS.iter().enumerate() {
let color_name = fg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, area);
frame.render_widget(paragraph, layout[i]);
}
}
@@ -117,16 +134,19 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
let areas = vertical.iter().flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
});
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) {
let layout = Layout::vertical([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
.collect_vec();
for (i, &bg) in NAMED_COLORS.iter().enumerate() {
let color_name = bg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, area);
frame.render_widget(paragraph, layout[i]);
}
}
@@ -210,12 +230,12 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
}
fn title_block(title: String) -> Block<'static> {
Block::new()
Block::default()
.borders(Borders::TOP)
.title_alignment(Alignment::Center)
.border_style(Style::new().dark_gray())
.title_style(Style::new().reset())
.title(title)
.title_alignment(Alignment::Center)
.title_style(Style::new().reset())
}
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
@@ -247,3 +267,20 @@ fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
frame.render_widget(paragraph, layout[i as usize - 232]);
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

View File

@@ -9,9 +9,9 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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
// This example shows the full range of RGB colors that can be displayed in the terminal.
//
@@ -26,27 +26,20 @@
// is useful when the state is only used by the widget and doesn't need to be shared with
// other widgets.
use std::time::{Duration, Instant};
use color_eyre::Result;
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Position, Rect},
style::Color,
text::Text,
widgets::Widget,
DefaultTerminal,
use std::{
io::stdout,
panic,
time::{Duration, Instant},
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
use color_eyre::{config::HookBuilder, eyre, Result};
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 {
@@ -98,13 +91,21 @@ struct ColorsWidget {
frame_count: usize,
}
fn main() -> Result<()> {
install_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
/// Run the app
///
/// This is the main event loop for the app.
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
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(())
@@ -140,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")
@@ -215,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;
@@ -254,3 +256,36 @@ impl ColorsWidget {
}
}
}
/// Install `color_eyre` panic and error hooks
///
/// The hooks restore the terminal to a usable state before printing the error message.
fn install_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(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -9,38 +9,30 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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 color_eyre::Result;
#![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::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect,
},
style::{
palette::tailwind::{BLUE, SKY, SLATE, STONE},
Color, Style, Stylize,
},
symbols::{self, line},
text::{Line, Span, Text},
widgets::{Block, Paragraph, Widget, Wrap},
DefaultTerminal,
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind::*,
symbols::line,
widgets::{Block, Paragraph, Wrap},
};
use strum::{Display, EnumIter, FromRepr};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default)]
struct App {
mode: AppMode,
@@ -91,13 +83,21 @@ struct ConstraintBlock {
/// ```
struct SpacerBlock;
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
// App behaviour
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.insert_test_defaults();
while self.is_running() {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
@@ -117,24 +117,30 @@ impl App {
self.mode == AppMode::Running
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
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(),
_ => {}
},
_ => {}
@@ -346,8 +352,13 @@ impl App {
}
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
let constraints = self.constraints.iter().map(|_| Constraint::Fill(1));
let blocks = Layout::horizontal(constraints).split(area);
let blocks = Layout::horizontal(
self.constraints
.iter()
.map(|_| Constraint::Fill(1))
.collect_vec(),
)
.split(area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
@@ -425,7 +436,7 @@ impl ConstraintBlock {
} else {
main_color
};
Block::new()
Block::default()
.fg(Self::TEXT_COLOR)
.bg(selected_color)
.render(area, buf);
@@ -604,3 +615,32 @@ impl ConstraintName {
}
}
}
fn init_error_hooks() -> 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() -> 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() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -9,27 +9,21 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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 color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols,
text::Line,
widgets::{
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
Tabs, Widget,
},
DefaultTerminal,
#![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},
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;
@@ -46,14 +40,6 @@ const RATIO_COLOR: Color = tailwind::SLATE.c900;
// priority 4
const FILL_COLOR: Color = tailwind::SLATE.c950;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default, Clone, Copy)]
struct App {
selected_tab: SelectedTab,
@@ -83,11 +69,25 @@ enum AppState {
Quit,
}
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()?;
Ok(())
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.update_max_scroll_offset();
while self.is_running() {
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
@@ -101,19 +101,22 @@ impl App {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
return Ok(());
}
use KeyCode::*;
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(),
_ => (),
}
}
@@ -191,7 +194,7 @@ impl App {
);
Paragraph::new(width_bar.dark_gray())
.centered()
.block(Block::new().padding(Padding {
.block(Block::default().padding(Padding {
left: 0,
right: 0,
top: 1,
@@ -232,7 +235,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 {
@@ -403,3 +406,32 @@ impl Example {
Paragraph::new(text).centered().block(block)
}
}
fn init_error_hooks() -> 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() -> 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() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -9,40 +9,21 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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, ops::ControlFlow, time::Duration};
use std::{error::Error, io, ops::ControlFlow, time::Duration};
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
layout::{Constraint, Layout, Rect},
style::{Color, Style},
text::Line,
widgets::{Paragraph, Widget},
DefaultTerminal, Frame,
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?;
let app_result = run(terminal);
ratatui::restore();
if let Err(err) = execute!(stdout(), DisableMouseCapture) {
eprintln!("Error disabling mouse capture: {err}");
}
app_result
}
use ratatui::{prelude::*, widgets::Paragraph};
/// A custom widget that renders a button with a label, theme and state.
#[derive(Debug, Clone)]
@@ -154,11 +135,38 @@ impl Button<'_> {
}
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
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 res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selected_button: usize = 0;
let mut button_states = [State::Selected, State::Normal, State::Normal];
loop {
terminal.draw(|frame| draw(frame, button_states))?;
terminal.draw(|frame| ui(frame, button_states))?;
if !event::poll(Duration::from_millis(100))? {
continue;
}
@@ -180,14 +188,14 @@ fn run(mut terminal: DefaultTerminal) -> Result<()> {
Ok(())
}
fn draw(frame: &mut Frame, states: [State; 3]) {
fn ui(frame: &mut Frame, states: [State; 3]) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
]);
let [title, buttons, help, _] = vertical.areas(frame.area());
let [title, buttons, help, _] = vertical.areas(frame.size());
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),

View File

@@ -122,8 +122,8 @@ pub struct TabsState<'a> {
}
impl<'a> TabsState<'a> {
pub const 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();

View File

@@ -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};
@@ -26,7 +23,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate);
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
@@ -37,7 +34,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
)?;
terminal.show_cursor()?;
if let Err(err) = app_result {
if let Err(err) = res {
println!("{err:?}");
}
@@ -51,10 +48,10 @@ fn run_app<B: Backend>(
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {

View File

@@ -9,9 +9,9 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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::{error::Error, time::Duration};
@@ -20,7 +20,7 @@ use argh::FromArgs;
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(all(not(windows), feature = "termion"))]
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termwiz")]
mod termwiz;
@@ -43,13 +43,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")]
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))]
#[cfg(feature = "termion")]
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(
feature = "termwiz",
not(feature = "crossterm"),
not(feature = "termion")
))]
#[cfg(feature = "termwiz")]
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
Ok(())
}

View File

@@ -1,15 +1,11 @@
#![allow(dead_code)]
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};
@@ -39,7 +35,7 @@ fn run_app<B: Backend>(
) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate);
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv()? {
Event::Input(key) => match key {

View File

@@ -1,16 +1,12 @@
#![allow(dead_code)]
use std::{
error::Error,
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};
@@ -22,12 +18,12 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
// create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate);
let res = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
if let Err(err) = app_result {
if let Err(err) = res {
println!("{err:?}");
}
@@ -41,7 +37,7 @@ fn run_app(
) -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(input) = terminal

View File

@@ -1,64 +1,57 @@
#[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(frame: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.size());
let tabs = app
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.block(Block::bordered().title(app.title))
.block(Block::default().borders(Borders::ALL).title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
frame.render_widget(tabs, chunks[0]);
f.render_widget(tabs, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(frame, app, chunks[1]),
1 => draw_second_tab(frame, app, chunks[1]),
2 => draw_third_tab(frame, app, chunks[1]),
0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(f, app, chunks[1]),
2 => draw_third_tab(f, app, chunks[1]),
_ => {}
};
}
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) {
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
draw_gauges(frame, app, chunks[0]);
draw_charts(frame, app, chunks[1]);
draw_text(frame, chunks[2]);
draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]);
draw_text(f, chunks[2]);
}
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(2),
Constraint::Length(1),
])
.margin(1)
.split(area);
let block = Block::bordered().title("Graphs");
frame.render_widget(block, area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::new().title("Gauge:"))
.block(Block::default().title("Gauge:"))
.gauge_style(
Style::default()
.fg(Color::Magenta)
@@ -68,10 +61,10 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
.use_unicode(app.enhanced_graphics)
.label(label)
.ratio(app.progress);
frame.render_widget(gauge, chunks[0]);
f.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::new().title("Sparkline:"))
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics {
@@ -79,22 +72,22 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
} else {
symbols::bar::THREE_LEVELS
});
frame.render_widget(sparkline, chunks[1]);
f.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::new().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta))
.block(Block::default().title("LineGauge:"))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
symbols::line::NORMAL
})
.ratio(app.progress);
frame.render_widget(line_gauge, chunks[2]);
f.render_widget(line_gauge, chunks[2]);
}
#[allow(clippy::too_many_lines)]
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
@@ -117,10 +110,10 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::bordered().title("List"))
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::Blue);
@@ -145,12 +138,12 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::bordered().title("List"));
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
let barchart = BarChart::default()
.block(Block::bordered().title("Bar chart"))
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
@@ -167,7 +160,7 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
)
.label_style(Style::default().fg(Color::Yellow))
.bar_style(Style::default().fg(Color::Green));
frame.render_widget(barchart, chunks[1]);
f.render_widget(barchart, chunks[1]);
}
if app.show_chart {
let x_labels = vec![
@@ -202,12 +195,14 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
];
let chart = Chart::new(datasets)
.block(
Block::bordered().title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Block::default()
.title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
@@ -221,17 +216,17 @@ fn draw_charts(frame: &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)),
]),
);
frame.render_widget(chart, chunks[1]);
f.render_widget(chart, chunks[1]);
}
}
fn draw_text(frame: &mut Frame, area: Rect) {
fn draw_text(f: &mut Frame, area: Rect) {
let text = vec![
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
text::Line::from(""),
@@ -259,17 +254,17 @@ fn draw_text(frame: &mut Frame, area: Rect) {
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::bordered().title(Span::styled(
let block = Block::default().borders(Borders::ALL).title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
frame.render_widget(paragraph, area);
f.render_widget(paragraph, area);
}
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
let up_style = Style::default().fg(Color::Green);
@@ -297,11 +292,11 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::bordered().title("Servers"));
frame.render_widget(table, chunks[0]);
.block(Block::default().title("Servers").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
let map = Canvas::default()
.block(Block::bordered().title("World"))
.block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
@@ -352,10 +347,10 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
frame.render_widget(map, chunks[1]);
f.render_widget(map, chunks[1]);
}
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
let colors = [
Color::Reset,
@@ -395,6 +390,6 @@ fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
Constraint::Ratio(1, 3),
],
)
.block(Block::bordered().title("Colors"));
frame.render_widget(table, chunks[0]);
.block(Block::default().title("Colors").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
}

View File

@@ -1,24 +1,12 @@
use std::time::Duration;
use color_eyre::{eyre::Context, Result};
use crossterm::event;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::Color,
text::{Line, Span},
widgets::{Block, Tabs, Widget},
DefaultTerminal, Frame,
};
use ratatui::{prelude::*, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{
destroy,
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
THEME,
};
use crate::{destroy, tabs::*, term, THEME};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct App {
@@ -49,13 +37,15 @@ enum Tab {
Weather,
}
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
App::default().run(terminal)
}
impl App {
/// Run the app until the user quits.
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
while self.is_running() {
terminal
.draw(|frame| self.draw(frame))
.wrap_err("terminal.draw")?;
self.draw(terminal)?;
self.handle_events()?;
}
Ok(())
@@ -66,11 +56,16 @@ impl App {
}
/// Draw a single frame of the app.
fn draw(&self, frame: &mut Frame) {
frame.render_widget(self, frame.area());
if self.mode == Mode::Destroy {
destroy::destroy(frame);
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal
.draw(|frame| {
frame.render_widget(self, frame.size());
if self.mode == Mode::Destroy {
destroy::destroy(frame);
}
})
.wrap_err("terminal.draw")?;
Ok(())
}
/// Handle events from the terminal.
@@ -79,24 +74,22 @@ impl App {
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f64(1.0 / 50.0);
if !event::poll(timeout)? {
return Ok(());
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
match term::next_event(timeout)? {
Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
_ => {}
}
Ok(())
}
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(),
_ => {}
};
}

821
examples/demo2/big_text.rs Normal file
View File

@@ -0,0 +1,821 @@
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
//! glyphs from the [font8x8] crate.
//!
//! ![Hello World example](https://vhs.charm.sh/vhs-2UxNc2SJgiNqHoowbsXAMW.gif)
//!
//! # 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 ratatui::assert_buffer_eq;
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(vec![
" ████ ██ ███ ████ ██ ",
"██ ██ ██ ██ ",
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
" █████ ",
]);
assert_buffer_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(vec![
"██████ █ ███",
"█ ██ █ ██ ██",
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
]);
assert_buffer_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(vec![
"██ ██ ███ █ ██ ",
"███ ███ ██ ██ ",
"███████ ██ ██ ██ █████ ███ ",
"███████ ██ ██ ██ ██ ██ ",
"██ █ ██ ██ ██ ██ ██ ██ ",
"██ ██ ██ ██ ██ ██ █ ██ ",
"██ ██ ███ ██ ████ ██ ████ ",
" ",
"████ ██ ",
" ██ ",
" ██ ███ █████ ████ █████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" ██ █ ██ ██ ██ ██████ ████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
"███████ ████ ██ ██ ████ █████ ",
" ",
]);
assert_buffer_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 mut expected = Buffer::with_lines(vec![
" ████ █ ███ ███ ",
"██ ██ ██ ██ ██ ",
"███ █████ ██ ██ ██ ████ ██ ",
" ███ ██ ██ ██ ██ ██ ██ █████ ",
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
" ████ ██ ██ ████ ████ ███ ██ ",
" █████ ",
]);
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
assert_buffer_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(vec![
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ████ ██ ",
" █████ ██ ██ █████ ",
" ██ ██ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ",
"███ ██ ████ ███ ██ ",
" ",
" ████ ",
" ██ ██ ",
"██ ██ ███ ████ ████ █████ ",
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" █████ ████ ████ ████ ██ ██ ",
" ",
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ██ ██ ██ ████ ",
" █████ ██ ██ ██ ██ ██ ",
" ██ ██ ██ ██ ██ ██████ ",
" ██ ██ ██ ██ ██ ██ ",
"██████ ████ ███ ██ ████ ",
" ",
]);
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_buffer_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(vec![
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
]);
assert_buffer_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(vec![
"█▀██▀█ ▄█ ▀██",
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
]);
assert_buffer_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(vec![
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
"▀██▀ ▀▀ ",
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
]);
assert_buffer_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 mut expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
]);
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
assert_buffer_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(vec![
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
" ▄█▀▀█▄ ",
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
]);
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_buffer_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(vec![
"▐█▌ █ ▐█ ██ █ ",
"█ █ █ ▐▌ ",
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
" ██▌ ",
]);
assert_buffer_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(vec![
"███ ▐ ▐█",
"▌█▐ █ █",
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
]);
assert_buffer_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(vec![
"█ ▐▌ ▐█ ▐ █ ",
"█▌█▌ █ █ ",
"███▌█ █ █ ▐██ ▐█ ",
"███▌█ █ █ █ █ ",
"█▐▐▌█ █ █ █ █ ",
"█ ▐▌█ █ █ █▐ █ ",
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
" ",
"██ █ ",
"▐▌ ",
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
"▐▌ █ █ █ █ █ █ ",
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
"▐▌▐▌ █ █ █ █ █ ",
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
" ",
]);
assert_buffer_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 mut expected = Buffer::with_lines(vec![
"▐█▌ ▐ ▐█ ▐█ ",
"█ █ █ █ █ ",
"█▌ ▐██ █ █ █ ▐█▌ █ ",
"▐█ █ █ █ █ █ █ ▐██ ",
" ▐█ █ █ █ █ ███ █ █ ",
"█ █ █▐ ▐██ █ █ █ █ ",
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
" ██▌ ",
]);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
assert_buffer_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(vec![
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌▐█▌ █ ",
"▐██ █ █ ▐██ ",
"▐▌█ ███ █ █ ",
"▐▌▐▌█ █ █ ",
"█▌▐▌▐█▌ ▐█▐▌ ",
" ",
" ██ ",
"▐▌▐▌ ",
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
"█ ▐█▐▌█ █ █ █ █ █ ",
"█ █▌▐▌▐▌███ ███ █ █ ",
"▐▌▐▌▐▌ █ █ █ █ ",
" ██▌██ ▐█▌ ▐█▌ █ █ ",
" ",
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌ █ █ █ ▐█▌ ",
"▐██ █ █ █ █ █ ",
"▐▌▐▌ █ █ █ ███ ",
"▐▌▐▌ █ █ █ █ ",
"███ ▐█▌ ▐█▐▌▐█▌ ",
" ",
]);
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_buffer_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(vec![
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
]);
assert_buffer_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(vec![
"▛█▜ ▟ ▝█",
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
]);
assert_buffer_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(vec![
"█▖▟▌ ▝█ ▟ ▀ ",
"███▌█ █ █ ▝█▀ ▝█ ",
"█▝▐▌█ █ █ █▗ █ ",
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
"▜▛ ▀ ",
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
]);
assert_buffer_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 mut expected = Buffer::with_lines(vec![
"▟▀▙ ▟ ▝█ ▝█ ",
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
]);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
assert_buffer_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(vec![
"▜▛▜▖ ▝█ ",
"▐▙▟▘▟▀▙ ▗▄█ ",
"▐▌▜▖█▀▀ █ █ ",
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
"▗▛▜▖ ",
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
"▜▛▜▖▝█ ",
"▐▙▟▘ █ █ █ ▟▀▙ ",
"▐▌▐▌ █ █ █ █▀▀ ",
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
]);
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_buffer_eq!(buf, expected);
Ok(())
}
}

View File

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

View File

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

18
examples/demo2/errors.rs Normal file
View File

@@ -0,0 +1,18 @@
use color_eyre::{config::HookBuilder, Result};
use crate::term;
pub fn init_hooks() -> 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 _ = term::restore();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = term::restore();
panic(info);
}));
Ok(())
}

45
examples/demo2/main.rs Normal file
View File

@@ -0,0 +1,45 @@
//! # [Ratatui] Demo2 example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-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
#![allow(
clippy::enum_glob_use,
clippy::missing_errors_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::wildcard_imports
)]
mod app;
mod big_text;
mod colors;
mod destroy;
mod errors;
mod tabs;
mod term;
mod theme;
pub use app::*;
use color_eyre::Result;
pub use colors::*;
pub use term::*;
pub use theme::*;
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
App::default().run(terminal)?;
term::restore()?;
Ok(())
}

View File

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

View File

@@ -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,
});
@@ -96,10 +87,13 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.map(|e| e.from.width())
.max()
.unwrap_or_default();
let items = EMAILS.iter().map(|e| {
let from = format!("{:width$}", e.from, width = from_width).into();
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
});
let items = EMAILS
.iter()
.map(|e| {
let from = format!("{:width$}", e.from, width = from_width).into();
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
})
.collect_vec();
let mut state = ListState::default().with_selected(Some(selected_index));
StatefulWidget::render(
List::new(items)

View File

@@ -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,
});
@@ -164,7 +155,7 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
.block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.row_highlight_style(Style::new().light_yellow()),
.highlight_style(Style::new().light_yellow()),
area,
buf,
&mut state,

View File

@@ -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,
});
@@ -52,15 +45,18 @@ impl Widget for TracerouteTab {
fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = HOPS.iter().map(|hop| Row::new(vec![hop.host, hop.address]));
let block = Block::new()
.padding(Padding::new(1, 1, 1, 1))
let rows = HOPS
.iter()
.map(|hop| Row::new(vec![hop.host, hop.address]))
.collect_vec();
let block = Block::default()
.title("Traceroute bad.horse".bold().white())
.title_alignment(Alignment::Center)
.title("Traceroute bad.horse".bold().white());
.padding(Padding::new(1, 1, 1, 1));
StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.row_highlight_style(THEME.traceroute.selected)
.highlight_style(THEME.traceroute.selected)
.block(block),
area,
buf,
@@ -99,7 +95,7 @@ pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
.title_alignment(Alignment::Center)
.border_type(BorderType::Thick),
)
.data(data)
.data(&data)
.style(THEME.traceroute.ping)
.render(area, buf);
}
@@ -108,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()

View File

@@ -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())
@@ -88,7 +82,7 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
Bar::default()
.value(value)
// This doesn't actually render correctly as the text is too wide for the bar
// See https://github.com/ratatui/ratatui/issues/513 for more info
// See https://github.com/ratatui-org/ratatui/issues/513 for more info
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
.text_value(format!("{value}°"))
.style(if value > 70 {
@@ -101,7 +95,7 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
} else {
Style::new().fg(Color::DarkGray).bg(Color::Yellow).bold()
})
.label(label)
.label(label.into())
})
.collect_vec();
let group = BarGroup::default().bars(&data);
@@ -115,15 +109,15 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
let bg = Color::Rgb(32, 48, 96);
let data = [
Bar::default().text_value("Winter 37-51").value(51),
Bar::default().text_value("Spring 40-65").value(65),
Bar::default().text_value("Summer 54-77").value(77),
Bar::default().text_value("Winter 37-51".into()).value(51),
Bar::default().text_value("Spring 40-65".into()).value(65),
Bar::default().text_value("Summer 54-77".into()).value(77),
Bar::default()
.text_value("Fall 41-71")
.text_value("Fall 41-71".into())
.value(71)
.value_style(Style::new().bold()), // current season
];
let group = BarGroup::default().label("GPU").bars(&data);
let group = BarGroup::default().label("GPU".into()).bars(&data);
BarChart::default()
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
.direction(Direction::Horizontal)
@@ -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);
}

42
examples/demo2/term.rs Normal file
View File

@@ -0,0 +1,42 @@
use std::{
io::{self, stdout},
time::Duration,
};
use color_eyre::{eyre::WrapErr, Result};
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
// using vhs in a 1280x640 sized window (github social preview size)
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
};
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
enable_raw_mode().context("enable raw mode")?;
stdout()
.execute(EnterAlternateScreen)
.wrap_err("enter alternate screen")?;
Ok(terminal)
}
pub fn restore() -> Result<()> {
disable_raw_mode().context("disable raw mode")?;
stdout()
.execute(LeaveAlternateScreen)
.wrap_err("leave alternate screen")?;
Ok(())
}
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
let event = event::read()?;
Ok(Some(event))
}

View File

@@ -1,4 +1,4 @@
use ratatui::style::{Color, Modifier, Style};
use ratatui::prelude::*;
pub struct Theme {
pub root: Style,

View File

@@ -9,62 +9,66 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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 color_eyre::Result;
use std::io::{self, stdout};
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
prelude::*,
widgets::{Block, Borders, Paragraph},
DefaultTerminal, Frame,
};
/// Example code for lib.rs
///
/// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file.
fn main() -> Result<()> {
color_eyre::install()?;
let first_arg = std::env::args().nth(1).unwrap_or_default();
let terminal = ratatui::init();
let app_result = run(terminal, &first_arg);
ratatui::restore();
app_result
}
fn main() -> io::Result<()> {
let arg = std::env::args().nth(1).unwrap_or_default();
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
fn run(mut terminal: DefaultTerminal, first_arg: &str) -> Result<()> {
let mut should_quit = false;
while !should_quit {
terminal.draw(match first_arg {
terminal.draw(match arg.as_str() {
"layout" => layout,
"styling" => styling,
_ => hello_world,
})?;
should_quit = handle_events()?;
}
Ok(())
}
fn handle_events() -> std::io::Result<bool> {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
Ok(false)
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn hello_world(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.area(),
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
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()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
fn layout(frame: &mut Frame) {
let vertical = Layout::vertical([
Constraint::Length(1),
@@ -72,7 +76,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(
@@ -83,8 +87,8 @@ fn layout(frame: &mut Frame) {
Block::new().borders(Borders::TOP).title("Status Bar"),
status_bar,
);
frame.render_widget(Block::bordered().title("Left"), left);
frame.render_widget(Block::bordered().title("Right"), right);
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
}
fn styling(frame: &mut Frame) {
@@ -95,7 +99,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(

View File

@@ -9,40 +9,29 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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::num::NonZeroUsize;
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
use color_eyre::Result;
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::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Alignment,
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect,
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols::{self, line},
text::{Line, Text},
widgets::{
Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs,
Widget,
},
DefaultTerminal,
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind,
symbols::line,
widgets::{block::Title, *},
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
(
"Min(u16) takes any excess space always",
@@ -157,18 +146,23 @@ enum SelectedTab {
SpaceBetween,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
// increase the layout cache to account for the number of layout events. This ensures that
// layout is not generally reprocessed on every frame (which would lead to possible janky
// results when there are more than one possible solution to the requested layout). This
// assumes the user changes spacing about a 100 times or so.
let cache_size = EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100;
Layout::init_cache(NonZeroUsize::new(cache_size).unwrap());
fn main() -> Result<()> {
// assuming the user changes spacing about a 100 times or so
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
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<()> {
self.draw(&mut terminal)?;
while self.is_running() {
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
self.handle_events()?;
self.draw(&mut terminal)?;
}
Ok(())
}
@@ -177,18 +171,24 @@ impl App {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
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(),
_ => (),
},
_ => {}
@@ -273,7 +273,7 @@ impl App {
fn tabs(self) -> impl Widget {
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title("Flex Layouts ".bold())
.title(Title::from("Flex Layouts ".bold()))
.title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing ");
Tabs::new(tab_titles)
.block(block)
@@ -334,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 {
@@ -364,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,
@@ -509,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,
@@ -520,6 +520,35 @@ const fn color_for_constraint(constraint: Constraint) -> Color {
}
}
fn init_error_hooks() -> 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() -> 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() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn get_description_height(s: &str) -> u16 {
if s.is_empty() {

View File

@@ -9,21 +9,24 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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::time::Duration;
#![allow(clippy::enum_glob_use)]
use color_eyre::Result;
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::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Padding, Paragraph, Widget},
DefaultTerminal,
prelude::*,
style::palette::tailwind,
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
};
const GAUGE1_COLOR: Color = tailwind::RED.c800;
@@ -51,23 +54,28 @@ enum AppState {
}
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while self.state != AppState::Quitting {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
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.size()))?;
Ok(())
}
fn update(&mut self, terminal_width: u16) {
if self.state != AppState::Started {
return;
@@ -91,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(),
_ => {}
}
}
@@ -114,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);
@@ -196,10 +205,39 @@ impl App {
}
fn title_block(title: &str) -> Block {
let title = Line::from(title).centered();
Block::new()
.borders(Borders::NONE)
.padding(Padding::vertical(1))
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(())
}

95
examples/hello_world.rs Normal file
View File

@@ -0,0 +1,95 @@
//! # [Ratatui] Hello World 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::{self, Stdout},
time::Duration,
};
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.
///
/// 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<()> {
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. 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")?;
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
}
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
/// the cursor.
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
/// state. This example exits when the user presses 'q'. Other styles of application loops are
/// possible, for example, you could have multiple application states and switch between them based
/// on events, or you could have a single application state and update it based on events.
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
loop {
terminal.draw(crate::render_app)?;
if should_quit()? {
break;
}
}
Ok(())
}
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
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.
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")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}

View File

@@ -9,64 +9,37 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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
#![allow(clippy::wildcard_imports)]
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
use color_eyre::Result;
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::Backend,
crossterm::event,
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(8),
});
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
let app_result = run(&mut terminal, workers, downloads, rx);
ratatui::restore();
app_result
}
use ratatui::{prelude::*, widgets::*};
const NUM_DOWNLOADS: usize = 10;
type DownloadId = usize;
type WorkerId = usize;
enum Event {
Input(event::KeyEvent),
Input(crossterm::event::KeyEvent),
Tick,
Resize,
DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId),
}
struct Downloads {
pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
@@ -90,20 +63,52 @@ impl Downloads {
}
}
}
struct DownloadInProgress {
id: DownloadId,
started_at: Instant,
progress: f64,
}
struct Download {
id: DownloadId,
size: usize,
}
struct Worker {
id: WorkerId,
tx: mpsc::Sender<Download>,
}
fn main() -> Result<(), Box<dyn Error>> {
crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
run_app(&mut terminal, workers, downloads, rx)?;
crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;
Ok(())
}
fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
@@ -111,10 +116,10 @@ fn input_handling(tx: mpsc::Sender<Event>) {
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout).unwrap() {
match event::read().unwrap() {
event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
if crossterm::event::poll(timeout).unwrap() {
match crossterm::event::read().unwrap() {
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
_ => {}
};
}
@@ -167,22 +172,22 @@ fn downloads() -> Downloads {
}
#[allow(clippy::needless_pass_by_value)]
fn run(
terminal: &mut Terminal<impl Backend>,
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
workers: Vec<Worker>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
) -> Result<()> {
) -> Result<(), Box<dyn Error>> {
let mut redraw = true;
loop {
if redraw {
terminal.draw(|frame| draw(frame, &downloads))?;
terminal.draw(|f| ui(f, &downloads))?;
}
redraw = true;
match rx.recv()? {
Event::Input(event) => {
if event.code == event::KeyCode::Char('q') {
if event.code == crossterm::event::KeyCode::Char('q') {
break;
}
}
@@ -228,11 +233,11 @@ fn run(
Ok(())
}
fn draw(frame: &mut Frame, downloads: &Downloads) {
let area = frame.area();
fn ui(f: &mut Frame, downloads: &Downloads) {
let area = f.size();
let block = Block::new().title(Line::from("Progress").centered());
frame.render_widget(block, area);
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
@@ -243,10 +248,10 @@ fn draw(frame: &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);
frame.render_widget(progress, progress_area);
f.render_widget(progress, progress_area);
// in progress downloads
let items: Vec<ListItem> = downloads
@@ -269,7 +274,7 @@ fn draw(frame: &mut Frame, downloads: &Downloads) {
})
.collect();
let list = List::new(items);
frame.render_widget(list, list_area);
f.render_widget(list, list_area);
#[allow(clippy::cast_possible_truncation)]
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
@@ -279,7 +284,7 @@ fn draw(frame: &mut Frame, downloads: &Downloads) {
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
continue;
}
frame.render_widget(
f.render_widget(
gauge,
Rect {
x: gauge_area.left(),

View File

@@ -9,50 +9,73 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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
#![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::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, Paragraph},
DefaultTerminal, Frame,
layout::Constraint::*,
prelude::*,
widgets::{Block, Borders, Paragraph},
};
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
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 res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(draw)?;
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break Ok(());
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
#[allow(clippy::too_many_lines)]
fn draw(frame: &mut Frame) {
fn ui(frame: &mut Frame) {
let vertical = Layout::vertical([
Length(4), // text
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(
@@ -74,28 +97,31 @@ fn draw(frame: &mut Frame) {
Min(0), // fills remaining space
])
.split(examples_area);
let example_areas = example_rows.iter().flat_map(|area| {
Layout::horizontal([
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
let example_areas = example_rows
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
});
.flat_map(|area| {
Layout::horizontal([
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
})
.collect_vec();
// the examples are a cartesian product of the following constraints
// e.g. Len/Len, Len/Min, Len/Max, Len/Perc, Len/Ratio, Min/Len, Min/Min, ...
let examples = [
(
"Len",
[
vec![
Length(0),
Length(2),
Length(3),
@@ -104,11 +130,17 @@ fn draw(frame: &mut Frame) {
Length(15),
],
),
("Min", [Min(0), Min(2), Min(3), Min(6), Min(10), Min(15)]),
("Max", [Max(0), Max(2), Max(3), Max(6), Max(10), Max(15)]),
(
"Min",
vec![Min(0), Min(2), Min(3), Min(6), Min(10), Min(15)],
),
(
"Max",
vec![Max(0), Max(2), Max(3), Max(6), Max(10), Max(15)],
),
(
"Perc",
[
vec![
Percentage(0),
Percentage(25),
Percentage(50),
@@ -119,7 +151,7 @@ fn draw(frame: &mut Frame) {
),
(
"Ratio",
[
vec![
Ratio(0, 4),
Ratio(1, 4),
Ratio(2, 4),
@@ -130,15 +162,24 @@ fn draw(frame: &mut Frame) {
),
];
for ((a, b), area) in examples
for (i, (a, b)) in examples
.iter()
.cartesian_product(examples.iter())
.zip(example_areas)
.enumerate()
{
let (name_a, examples_a) = a;
let (name_b, examples_b) = b;
let constraints = examples_a.iter().copied().zip(examples_b.iter().copied());
render_example_combination(frame, area, &format!("{name_a}/{name_b}"), constraints);
let constraints = examples_a
.iter()
.copied()
.zip(examples_b.iter().copied())
.collect_vec();
render_example_combination(
frame,
example_areas[i],
&format!("{name_a}/{name_b}"),
constraints,
);
}
}
@@ -147,17 +188,18 @@ fn render_example_combination(
frame: &mut Frame,
area: Rect,
title: &str,
constraints: impl ExactSizeIterator<Item = (Constraint, Constraint)>,
constraints: Vec<(Constraint, Constraint)>,
) {
let block = Block::bordered()
let block = Block::default()
.title(title.gray())
.style(Style::reset())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
for ((a, b), &area) in constraints.into_iter().zip(layout.iter()) {
render_single_example(frame, area, vec![a, b, Min(0)]);
for (i, (a, b)) in constraints.into_iter().enumerate() {
render_single_example(frame, layout[i], vec![a, b, Min(0)]);
}
// This is to make it easy to visually see the alignment of the examples
// with the constraints.

362
examples/list.rs Normal file
View File

@@ -0,0 +1,362 @@
//! # [Ratatui] List 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
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
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_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;
#[derive(Copy, Clone)]
enum Status {
Todo,
Completed,
}
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 {
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),
]),
}
}
/// Changes the status of the selected list item
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 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) {
// Create a space for header, todo list and the footer.
let vertical = Layout::vertical([
Constraint::Length(2),
Constraint::Min(0),
Constraint::Length(2),
]);
let [header_area, rest_area, footer_area] = vertical.areas(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);
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);
}
}
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::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG)
.title("TODO List")
.title_alignment(Alignment::Center);
let inner_block = Block::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(NORMAL_ROW_COLOR);
// 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);
// 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
.items
.items
.iter()
.enumerate()
.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 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 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_info(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state.
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 to see here...".to_string()
};
// We show the list item's info under the list in this paragraph
let outer_info_block = Block::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG)
.title("TODO Info")
.title_alignment(Alignment::Center);
let inner_info_block = Block::default()
.borders(Borders::NONE)
.bg(NORMAL_ROW_COLOR)
.padding(Padding::horizontal(1));
// 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
info_paragraph.render(inner_info_area, buf);
}
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
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),
};
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;
}
}
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),
),
};
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,
}
}
}

View File

@@ -9,50 +9,59 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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
// This example is useful for testing how your terminal emulator handles different modifiers.
// It will render a grid of combinations of foreground and background colors with all
// modifiers applied to them.
use std::{error::Error, iter::once, result};
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::Line,
widgets::Paragraph,
DefaultTerminal, Frame,
use std::{
error::Error,
io::{self, Stdout},
iter::once,
result,
time::Duration,
};
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>>;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
}
fn draw(frame: &mut Frame) {
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)),
@@ -98,3 +107,20 @@ fn draw(frame: &mut Frame) {
}
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

152
examples/panic.rs Normal file
View File

@@ -0,0 +1,152 @@
//! # [Ratatui] Panic Hook 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
//! How to use a panic hook to reset the terminal before printing the panic to
//! the terminal.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the
//! terminal manually at the end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the
//! terminal at the end of `main` won't do us any good. Instead, we need to
//! make sure to set up a panic hook that first resets the terminal before
//! handling the panic. This both reuses the standard panic hook to ensure a
//! consistent panic handling UX and properly resets the terminal to not
//! distort the output.
//!
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Default)]
struct App {
hook_enabled: bool,
}
impl App {
fn chain_hook(&mut self) {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
reset_terminal().unwrap();
original_hook(panic);
}));
self.hook_enabled = true;
}
}
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
/// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
/// Resets the terminal.
fn reset_terminal() -> Result<()> {
disable_raw_mode()?;
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Runs the TUI loop.
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('p') => {
panic!("intentional demo panic");
}
KeyCode::Char('e') => {
app.chain_hook();
}
_ => {
return Ok(());
}
}
}
}
}
/// Render the TUI.
fn ui(f: &mut Frame, app: &App) {
let text = vec![
if app.hook_enabled {
Line::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Line::from("HOOK IS CURRENTLY **DISABLED**")
},
Line::from(""),
Line::from("press `p` to panic"),
Line::from("press `e` to enable the terminal-resetting panic hook"),
Line::from("press any other key to quit without panic"),
Line::from(""),
Line::from("when you panic without the chained hook,"),
Line::from("you will likely have to reset your terminal afterwards"),
Line::from("with the `reset` command"),
Line::from(""),
Line::from("with the chained panic hook enabled,"),
Line::from("you should see the panic report as you would without ratatui"),
Line::from(""),
Line::from("try first without the panic handler to see the difference"),
];
let b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
let p = Paragraph::new(text).block(b).centered();
f.render_widget(p, f.size());
}

164
examples/paragraph.rs Normal file
View File

@@ -0,0 +1,164 @@
//! # [Ratatui] Paragraph 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::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph, Wrap},
};
struct App {
scroll: u16,
}
impl App {
const fn new() -> Self {
Self { scroll: 0 }
}
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:?}");
}
Ok(())
}
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.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
// 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');
let block = Block::default().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("password", '*'),
Style::default().fg(Color::Red),
),
]),
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.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]);
}

130
examples/popup.rs Normal file
View File

@@ -0,0 +1,130 @@
//! # [Ratatui] Popup 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
// See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog
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::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
struct App {
show_popup: bool,
}
impl App {
const fn new() -> Self {
Self { show_popup: false }
}
}
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 app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let area = f.size();
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = vertical.areas(area);
let text = if app.show_popup {
"Press p to close the popup"
} else {
"Press p to show the popup"
};
let paragraph = Paragraph::new(text.slow_blink())
.centered()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, instructions);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
f.render_widget(block, content);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, area);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
}
}
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}

83
examples/ratatui-logo.rs Normal file
View File

@@ -0,0 +1,83 @@
//! # [Ratatui] Logo 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::{self, stdout},
thread::sleep,
time::Duration,
};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use indoc::indoc;
use itertools::izip;
use ratatui::{prelude::*, widgets::Paragraph};
/// A fun example of using half block characters to draw a logo
#[allow(clippy::many_single_char_names)]
fn logo() -> String {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"};
let a = indoc! {"
▄▄
█▄▄█
█ █
"};
let t = indoc! {"
▄▄▄
"};
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"};
let i = indoc! {"
"};
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
.collect::<Vec<_>>()
.join("\n")
}
fn main() -> io::Result<()> {
let mut terminal = init()?;
terminal.draw(|frame| {
frame.render_widget(Paragraph::new(logo()), frame.size());
})?;
sleep(Duration::from_secs(5));
restore()?;
println!();
Ok(())
}
fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
};
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
}

231
examples/scrollbar.rs Normal file
View File

@@ -0,0 +1,231 @@
//! # [Ratatui] Scrollbar 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
#![warn(clippy::pedantic)]
#![allow(clippy::wildcard_imports)]
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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 {
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_state: ScrollbarState,
pub vertical_scroll: usize,
pub horizontal_scroll: usize,
}
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::default();
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(())
}
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, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('k') | KeyCode::Up => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('h') | KeyCode::Left => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
KeyCode::Char('l') | KeyCode::Right => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
// 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');
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.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_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let title = Block::new()
.title_alignment(Alignment::Center)
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block("Vertical scrollbar with arrows"))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows, without track symbol and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
.end_symbol(None),
chunks[2].inner(&Margin {
vertical: 1,
horizontal: 0,
}),
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar without arrows & custom thumb and track symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
}

180
examples/sparkline.rs Normal file
View File

@@ -0,0 +1,180 @@
//! # [Ratatui] Sparkline 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::{
error::Error,
io,
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::{
prelude::*,
widgets::{Block, Borders, Sparkline},
};
#[derive(Clone)]
struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
data2: Vec<u64>,
data3: Vec<u64>,
}
impl App {
fn new() -> Self {
let mut signal = RandomSignal::new(0, 100);
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
Self {
signal,
data1,
data2,
data3,
}
}
fn on_tick(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
let value = self.signal.next().unwrap();
self.data2.pop();
self.data2.insert(0, value);
let value = self.signal.next().unwrap();
self.data3.pop();
self.data3.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();
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(())
}
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.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
}

368
examples/table.rs Normal file
View File

@@ -0,0 +1,368 @@
//! # [Ratatui] Table 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
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
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 itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use style::palette::tailwind;
use unicode_width::UnicodeWidthStr;
const PALETTES: [tailwind::Palette; 4] = [
tailwind::BLUE,
tailwind::EMERALD,
tailwind::INDIGO,
tailwind::RED,
];
const INFO_TEXT: &str =
"(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color";
const ITEM_HEIGHT: usize = 4;
struct TableColors {
buffer_bg: Color,
header_bg: Color,
header_fg: Color,
row_fg: Color,
selected_style_fg: Color,
normal_row_color: Color,
alt_row_color: Color,
footer_border_color: Color,
}
impl TableColors {
const fn new(color: &tailwind::Palette) -> Self {
Self {
buffer_bg: tailwind::SLATE.c950,
header_bg: color.c900,
header_fg: tailwind::SLATE.c200,
row_fg: tailwind::SLATE.c200,
selected_style_fg: color.c400,
normal_row_color: tailwind::SLATE.c950,
alt_row_color: tailwind::SLATE.c900,
footer_border_color: color.c400,
}
}
}
struct Data {
name: String,
address: String,
email: String,
}
impl Data {
const fn ref_array(&self) -> [&String; 3] {
[&self.name, &self.address, &self.email]
}
fn name(&self) -> &str {
&self.name
}
fn address(&self) -> &str {
&self.address
}
fn email(&self) -> &str {
&self.email
}
}
struct App {
state: TableState,
items: Vec<Data>,
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
colors: TableColors,
color_index: usize,
}
impl App {
fn new() -> Self {
let data_vec = generate_fake_names();
Self {
state: TableState::default().with_selected(0),
longest_item_lens: constraint_len_calculator(&data_vec),
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
colors: TableColors::new(&PALETTES[0]),
color_index: 0,
items: data_vec,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
}
pub fn next_color(&mut self) {
self.color_index = (self.color_index + 1) % PALETTES.len();
}
pub fn previous_color(&mut self) {
let count = PALETTES.len();
self.color_index = (self.color_index + count - 1) % count;
}
pub fn set_colors(&mut self) {
self.colors = TableColors::new(&PALETTES[self.color_index]);
}
}
fn generate_fake_names() -> Vec<Data> {
use fakeit::{address, contact, name};
(0..20)
.map(|_| {
let name = name::full();
let address = format!(
"{}\n{}, {} {}",
address::street(),
address::city(),
address::state(),
address::zip()
);
let email = contact::email();
Data {
name,
address,
email,
}
})
.sorted_by(|a, b| a.name.cmp(&b.name))
.collect_vec()
}
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 app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
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(),
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size());
app.set_colors();
render_table(f, app, rects[0]);
render_scrollbar(f, app, rects[0]);
render_footer(f, app, rects[1]);
}
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
let header_style = Style::default()
.fg(app.colors.header_fg)
.bg(app.colors.header_bg);
let selected_style = Style::default()
.add_modifier(Modifier::REVERSED)
.fg(app.colors.selected_style_fg);
let header = ["Name", "Address", "Email"]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(header_style)
.height(1);
let rows = app.items.iter().enumerate().map(|(i, data)| {
let color = match i % 2 {
0 => app.colors.normal_row_color,
_ => app.colors.alt_row_color,
};
let item = data.ref_array();
item.into_iter()
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
.collect::<Row>()
.style(Style::new().fg(app.colors.row_fg).bg(color))
.height(4)
});
let bar = "";
let t = Table::new(
rows,
[
// + 1 is for padding.
Constraint::Length(app.longest_item_lens.0 + 1),
Constraint::Min(app.longest_item_lens.1 + 1),
Constraint::Min(app.longest_item_lens.2),
],
)
.header(header)
.highlight_style(selected_style)
.highlight_symbol(Text::from(vec![
"".into(),
bar.into(),
bar.into(),
"".into(),
]))
.bg(app.colors.buffer_bg)
.highlight_spacing(HighlightSpacing::Always);
f.render_stateful_widget(t, area, &mut app.state);
}
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
let name_len = items
.iter()
.map(Data::name)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let address_len = items
.iter()
.map(Data::address)
.flat_map(str::lines)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let email_len = items
.iter()
.map(Data::email)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
#[allow(clippy::cast_possible_truncation)]
(name_len as u16, address_len as u16, email_len as u16)
}
fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None),
area.inner(&Margin {
vertical: 1,
horizontal: 1,
}),
&mut app.scroll_state,
);
}
fn render_footer(f: &mut Frame, app: &App, area: Rect) {
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
.centered()
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(app.colors.footer_border_color))
.border_type(BorderType::Double),
);
f.render_widget(info_footer, area);
}
#[cfg(test)]
mod tests {
use crate::Data;
#[test]
fn constraint_len_calculator() {
let test_data = vec![
Data {
name: "Emirhan Tala".to_string(),
address: "Cambridgelaan 6XX\n3584 XX Utrecht".to_string(),
email: "tala.emirhan@gmail.com".to_string(),
},
Data {
name: "thistextis26characterslong".to_string(),
address: "this line is 31 characters long\nbottom line is 33 characters long"
.to_string(),
email: "thisemailis40caharacterslong@ratatui.com".to_string(),
},
];
let (longest_name_len, longest_address_len, longest_email_len) =
crate::constraint_len_calculator(&test_data);
assert_eq!(26, longest_name_len);
assert_eq!(33, longest_address_len);
assert_eq!(40, longest_email_len);
}
}

View File

@@ -9,31 +9,23 @@
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//! [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 color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::{palette::tailwind, Color, Stylize},
symbols,
text::Line,
widgets::{Block, Padding, Paragraph, Tabs, Widget},
DefaultTerminal,
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use std::io::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::{prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default)]
struct App {
state: AppState,
@@ -60,22 +52,36 @@ enum SelectedTab {
Tab4,
}
fn main() -> Result<()> {
init_error_hooks()?;
let mut terminal = init_terminal()?;
App::default().run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
while self.state == AppState::Running {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.draw(terminal)?;
self.handle_events()?;
}
Ok(())
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
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(),
_ => {}
}
}
@@ -114,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);
@@ -199,7 +205,8 @@ impl SelectedTab {
/// A block surrounding the tab's content
fn block(self) -> Block<'static> {
Block::bordered()
Block::default()
.borders(Borders::ALL)
.border_set(symbols::border::PROPORTIONAL_TALL)
.padding(Padding::horizontal(1))
.border_style(self.palette().c700)
@@ -214,3 +221,32 @@ impl SelectedTab {
}
}
}
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(())
}

262
examples/user_input.rs Normal file
View File

@@ -0,0 +1,262 @@
//! # [Ratatui] User Input 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 handle user input. This is a bit out of the scope of
// the library as it does not provide any input handling out of the box. However, it may helps
// some to get started.
//
// This is a very simple example:
// * An input box always focused. Every character you type is registered here.
// * An entered character is inserted at the cursor position.
// * Pressing Backspace erases the left character before the cursor position
// * Pressing Enter pushes the current input in the history of previous messages. **Note: ** as
// this is a relatively simple example unicode characters are unsupported and their use will
// result in undefined behaviour.
//
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
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::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph},
};
enum InputMode {
Normal,
Editing,
}
/// App holds the state of the application
struct App {
/// Current value of the input box
input: String,
/// Position of cursor in the editor area.
cursor_position: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: Vec<String>,
}
impl App {
const fn new() -> Self {
Self {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cursor_position: 0,
}
}
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
self.input.insert(self.cursor_position, new_char);
self.move_cursor_right();
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.cursor_position != 0;
if is_not_cursor_leftmost {
// Method "remove" is not used on the saved text for deleting the selected char.
// Reason: Using remove on String works on bytes instead of the chars.
// Using remove would require special care because of char boundaries.
let current_index = self.cursor_position;
let from_left_to_current_index = current_index - 1;
// Getting all characters before the selected character.
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
// Getting all characters after selected character.
let after_char_to_delete = self.input.chars().skip(current_index);
// Put all characters together except the selected one.
// By leaving the selected one out, it is forgotten and therefore deleted.
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
}
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.len())
}
fn reset_cursor(&mut self) {
self.cursor_position = 0;
}
fn submit_message(&mut self) {
self.messages.push(self.input.clone());
self.input.clear();
self.reset_cursor();
}
}
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 app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Enter => app.submit_message(),
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Backspace => {
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
InputMode::Editing => {}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]);
let [help_area, input_area, messages_area] = vertical.areas(f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
"Press ".into(),
"q".bold(),
" to exit, ".into(),
"e".bold(),
" to start editing.".bold(),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
"Press ".into(),
"Esc".bold(),
" to stop editing, ".into(),
"Enter".bold(),
" to record the message".into(),
],
Style::default(),
),
};
let text = Text::from(Line::from(msg)).patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, help_area);
let input = Paragraph::new(app.input.as_str())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, input_area);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
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.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
);
}
}
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = Line::from(Span::raw(format!("{i}: {m}")));
ListItem::new(content)
})
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, messages_area);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More