diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 00000000..35049cbc
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --package xtask --"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ccb9d4b2..da1ae863 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,6 @@ 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
@@ -21,46 +20,58 @@ concurrency:
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
# without having to wait for the comprehensive matrix of tests to complete.
jobs:
- rustfmt:
+ # Lint the formatting of the codebase.
+ lint-formatting:
+ name: Check Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- with:
- components: rustfmt
- - run: cargo +nightly fmt --all --check
+ with: { components: rustfmt }
+ - run: cargo xtask lint-formatting
- typos:
+ # Check for typos in the codebase.
+ # See
+ 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
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
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
- clippy:
+ # 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: taiki-e/install-action@cargo-make
+ with: { components: clippy }
- uses: Swatinem/rust-cache@v2
- - run: cargo make clippy
+ - run: cargo xtask lint-clippy
- markdownlint:
+ # Run markdownlint on all markdown files in the repository.
+ lint-markdown:
+ name: Check Markdown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -70,24 +81,27 @@ jobs:
'**/*.md'
'!target'
+ # Run cargo coverage. This will generate a coverage report and upload it to codecov.
+ #
coverage:
+ name: Coverage Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- - uses: taiki-e/install-action@v2
- with:
- tool: cargo-llvm-cov,cargo-make
+ - uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- - run: cargo make coverage
+ - run: cargo xtask coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
+ # Run cargo check. This is a fast way to catch any obvious errors in the code.
check:
+ name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
strategy:
fail-fast: false
matrix:
@@ -99,13 +113,13 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- - uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- - run: cargo make check
- env:
- RUST_BACKTRACE: full
+ - run: cargo xtask check
+ # Run cargo rustdoc with the same options that would be used by docs.rs, taking into account the
+ # package.metadata.docs.rs configured in Cargo.toml. https://github.com/dtolnay/cargo-docs-rs
lint-docs:
+ name: Check Docs
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
@@ -114,47 +128,48 @@ jobs:
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
- # Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
- # the package.metadata.docs.rs configured in Cargo.toml.
- # https://github.com/dtolnay/cargo-docs-rs
- - run: cargo +nightly docs-rs -p ratatui
+ - run: cargo xtask lint-docs
- test-doc:
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- runs-on: ${{ matrix.os }}
+ # 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: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- - run: cargo make test-doc
- env:
- RUST_BACKTRACE: full
+ - run: cargo xtask test-docs
- test:
+ # 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 }}
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@master
- with:
- toolchain: ${{ matrix.toolchain }}
- - uses: taiki-e/install-action@v2
- with:
- tool: cargo-make,nextest
+ - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- - run: cargo make test-backend ${{ matrix.backend }}
- env:
- RUST_BACKTRACE: full
+ - run: cargo xtask test-backend ${{ matrix.backend }}
diff --git a/Cargo.lock b/Cargo.lock
index 3138f4a1..1c965aae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -53,12 +53,55 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+[[package]]
+name = "anstream"
+version = "0.6.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "anyhow"
version = "1.0.90"
@@ -242,6 +285,38 @@ version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+[[package]]
+name = "camino"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver 1.0.23",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
[[package]]
name = "cassowary"
version = "0.3.0"
@@ -331,6 +406,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap-cargo"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2ea69cefa96b848b73ad516ad1d59a195cdf9263087d977f648a818c8b43e"
+dependencies = [
+ "anstyle",
+ "cargo_metadata",
+ "clap",
+]
+
+[[package]]
+name = "clap-verbosity-flag"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e099138e1807662ff75e2cebe4ae2287add879245574489f9b1588eb5e5564ed"
+dependencies = [
+ "clap",
+ "log",
]
[[package]]
@@ -339,8 +436,22 @@ version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
+ "anstream",
"anstyle",
"clap_lex",
+ "strsim 0.11.1",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.85",
]
[[package]]
@@ -385,6 +496,12 @@ dependencies = [
"tracing-error",
]
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
[[package]]
name = "compact_str"
version = "0.8.0"
@@ -607,6 +724,18 @@ dependencies = [
"litrs",
]
+[[package]]
+name = "duct"
+version = "0.13.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c"
+dependencies = [
+ "libc",
+ "once_cell",
+ "os_pipe",
+ "shared_child",
+]
+
[[package]]
name = "either"
version = "1.13.0"
@@ -1109,6 +1238,12 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1490,6 +1625,16 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "os_pipe"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "overload"
version = "0.1.1"
@@ -2294,6 +2439,9 @@ name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+dependencies = [
+ "serde",
+]
[[package]]
name = "semver-parser"
@@ -2378,6 +2526,16 @@ dependencies = [
"lazy_static",
]
+[[package]]
+name = "shared_child"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -2505,6 +2663,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
[[package]]
name = "strum"
version = "0.26.3"
@@ -3205,7 +3369,7 @@ checksum = "dfb128bacfa86734e07681fb6068e34c144698e84ee022d6e009145d1abb77b5"
dependencies = [
"log",
"ordered-float",
- "strsim",
+ "strsim 0.10.0",
"thiserror",
"wezterm-dynamic-derive",
]
@@ -3364,6 +3528,21 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "xtask"
+version = "0.0.0"
+dependencies = [
+ "cargo_metadata",
+ "clap",
+ "clap-cargo",
+ "clap-verbosity-flag",
+ "color-eyre",
+ "duct",
+ "tracing",
+ "tracing-log",
+ "tracing-subscriber",
+]
+
[[package]]
name = "yansi"
version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 27b91829..67cae2b6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[workspace]
resolver = "2"
-members = ["ratatui"]
+members = ["ratatui", "xtask"]
+default-members = ["ratatui"]
[workspace.package]
authors = ["Florian Dehau ", "The Ratatui Developers"]
diff --git a/xtask/Cargo.lock b/xtask/Cargo.lock
new file mode 100644
index 00000000..9b71ead2
--- /dev/null
+++ b/xtask/Cargo.lock
@@ -0,0 +1,248 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anstream"
+version = "0.6.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap-cargo"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2ea69cefa96b848b73ad516ad1d59a195cdf9263087d977f648a818c8b43e"
+dependencies = [
+ "anstyle",
+ "clap",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "xtask"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "clap-cargo",
+]
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
new file mode 100644
index 00000000..3b8210f6
--- /dev/null
+++ b/xtask/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "xtask"
+edition = "2021"
+publish = false
+license.workspace = true
+
+[dependencies]
+cargo_metadata = "0.18.1"
+clap = { version = "4.5.20", features = ["derive"] }
+clap-cargo = { version = "0.14.1", features = ["cargo_metadata"] }
+clap-verbosity-flag = "2.2.2"
+color-eyre = "0.6.3"
+duct = "0.13.7"
+tracing = "0.1.40"
+tracing-log = "0.2.0"
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
diff --git a/xtask/README.md b/xtask/README.md
new file mode 100644
index 00000000..3890acdb
--- /dev/null
+++ b/xtask/README.md
@@ -0,0 +1,5 @@
+# xtask for ratatui
+
+See for details
+
+run with `cargo xtask ...`
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 00000000..64342373
--- /dev/null
+++ b/xtask/src/main.rs
@@ -0,0 +1,329 @@
+use std::{fmt::Debug, io, process::Output, vec};
+
+use cargo_metadata::MetadataCommand;
+use clap::{Parser, Subcommand, ValueEnum};
+use clap_verbosity_flag::{InfoLevel, Verbosity};
+use color_eyre::{eyre::Context, Result};
+use duct::cmd;
+use tracing::level_filters::LevelFilter;
+use tracing_log::AsTrace;
+
+fn main() -> Result<()> {
+ color_eyre::install()?;
+ let args = Args::parse();
+ tracing_subscriber::fmt()
+ .with_max_level(args.log_level())
+ .without_time()
+ .init();
+
+ match args.run() {
+ Ok(_) => (),
+ Err(err) => {
+ tracing::error!("{err}");
+ std::process::exit(1);
+ }
+ }
+ Ok(())
+}
+
+#[derive(Debug, Parser)]
+#[command(bin_name = "cargo xtask", styles = clap_cargo::style::CLAP_STYLING)]
+struct Args {
+ #[command(subcommand)]
+ command: Command,
+
+ #[command(flatten)]
+ verbosity: Verbosity,
+}
+
+impl Args {
+ fn run(self) -> Result<()> {
+ self.command.run()
+ }
+
+ fn log_level(&self) -> LevelFilter {
+ self.verbosity.log_level_filter().as_trace()
+ }
+}
+
+#[derive(Clone, Debug, Subcommand)]
+enum Command {
+ /// Run CI checks (lint, build, test)
+ CI,
+
+ /// Build the project
+ #[command(visible_alias = "b")]
+ Build,
+
+ /// Run cargo check
+ #[command(visible_alias = "c")]
+ Check,
+
+ /// Generate code coverage report
+ #[command(visible_alias = "cov")]
+ Coverage,
+
+ /// Lint formatting, typos, clippy, and docs
+ #[command(visible_alias = "l")]
+ Lint,
+
+ /// Run clippy on the project
+ #[command(visible_alias = "cl")]
+ LintClippy,
+
+ /// Check documentation for errors and warnings
+ #[command(visible_alias = "d")]
+ LintDocs,
+
+ /// Check for formatting issues in the project
+ #[command(visible_alias = "lf")]
+ LintFormatting,
+
+ /// Lint markdown files
+ #[command(visible_alias = "md")]
+ LintMarkdown,
+
+ /// Check for typos in the project
+ #[command(visible_alias = "lt")]
+ LintTypos,
+
+ /// Fix clippy warnings in the project
+ #[command(visible_alias = "fc")]
+ FixClippy,
+
+ /// Fix formatting issues in the project
+ #[command(visible_alias = "fmt")]
+ FixFormatting,
+
+ /// Fix typos in the project
+ #[command(visible_alias = "typos")]
+ FixTypos,
+
+ /// Run tests
+ #[command(visible_alias = "t")]
+ Test,
+
+ /// Test backend
+ #[command(visible_alias = "tb")]
+ TestBackend { backend: Backend },
+
+ /// Run doc tests
+ #[command(visible_alias = "td")]
+ TestDocs,
+
+ /// Run lib tests
+ #[command(visible_alias = "tl")]
+ TestLibs,
+}
+
+#[derive(Clone, Debug, ValueEnum, PartialEq, Eq)]
+enum Backend {
+ Crossterm,
+ Termion,
+ Termwiz,
+}
+
+impl Command {
+ fn run(self) -> Result<()> {
+ match self {
+ Command::CI => ci(),
+ Command::Build => build(),
+ Command::Check => check(),
+ Command::Coverage => coverage(),
+ Command::Lint => lint(),
+ Command::LintClippy => lint_clippy(),
+ Command::LintDocs => lint_docs(),
+ Command::LintFormatting => lint_format(),
+ Command::LintTypos => lint_typos(),
+ Command::LintMarkdown => lint_markdown(),
+ Command::FixClippy => fix_clippy(),
+ Command::FixFormatting => fix_format(),
+ Command::FixTypos => fix_typos(),
+ Command::Test => test(),
+ Command::TestBackend { backend } => test_backend(backend),
+ Command::TestDocs => test_docs(),
+ Command::TestLibs => test_libs(),
+ }
+ }
+}
+
+/// Run CI checks (lint, build, test)
+fn ci() -> Result<()> {
+ lint()?;
+ build()?;
+ test()?;
+ Ok(())
+}
+
+/// Build the project
+fn build() -> Result<()> {
+ run_cargo(vec!["build", "--all-targets", "--all-features"])
+}
+
+/// Run cargo check
+fn check() -> Result<()> {
+ run_cargo(vec!["check", "--all-targets", "--all-features"])
+}
+
+/// Generate code coverage report
+fn coverage() -> Result<()> {
+ run_cargo(vec![
+ "llvm-cov",
+ "--lcov",
+ "--output-path",
+ "target/lcov.info",
+ "--all-features",
+ ])
+}
+
+/// Lint formatting, typos, clippy, and docs (and a soft fail on markdown)
+fn lint() -> Result<()> {
+ lint_clippy()?;
+ lint_docs()?;
+ lint_format()?;
+ lint_typos()?;
+ if let Err(err) = lint_markdown() {
+ tracing::warn!("known issue: markdownlint is currently noisy and can be ignored: {err}");
+ }
+ Ok(())
+}
+
+/// Run clippy on the project
+fn lint_clippy() -> Result<()> {
+ run_cargo(vec![
+ "clippy",
+ "--all-targets",
+ "--all-features",
+ "--tests",
+ "--benches",
+ "--",
+ "-D",
+ "warnings",
+ ])
+}
+
+/// Fix clippy warnings in the project
+fn fix_clippy() -> Result<()> {
+ run_cargo(vec![
+ "clippy",
+ "--all-targets",
+ "--all-features",
+ "--tests",
+ "--benches",
+ "--",
+ "-D",
+ "warnings",
+ "--fix",
+ ])
+}
+
+/// Check that docs build without errors using flags for docs.rs
+fn lint_docs() -> Result<()> {
+ let meta = MetadataCommand::new()
+ .exec()
+ .wrap_err("failed to get cargo metadata")?;
+ for package in meta.workspace_default_packages() {
+ run_cargo_nightly(vec!["docs-rs", "--package", &package.name])?;
+ }
+ Ok(())
+}
+
+/// Lint formatting issues in the project
+fn lint_format() -> Result<()> {
+ run_cargo_nightly(vec!["fmt", "--all", "--check"])
+}
+
+/// Fix formatting issues in the project
+fn fix_format() -> Result<()> {
+ run_cargo_nightly(vec!["fmt", "--all"])
+}
+
+/// Lint markdown files using [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)
+fn lint_markdown() -> Result<()> {
+ cmd!("markdownlint-cli2", "**/*.md", "!target").run_with_trace()?;
+ Ok(())
+}
+
+/// Check for typos in the project using [typos-cli](https://github.com/crate-ci/typos/)
+fn lint_typos() -> Result<()> {
+ cmd!("typos").run_with_trace()?;
+ Ok(())
+}
+
+/// Fix typos in the project
+fn fix_typos() -> Result<()> {
+ cmd!("typos", "-w").run_with_trace()?;
+ Ok(())
+}
+
+/// Run tests for libs, backends, and docs
+fn test() -> Result<()> {
+ test_libs()?;
+ test_backend(Backend::Crossterm)?;
+ test_backend(Backend::Termion)?;
+ test_backend(Backend::Termwiz)?;
+ test_docs()?; // run last because it's slow
+ Ok(())
+}
+
+/// Run tests for the specified backend
+fn test_backend(backend: Backend) -> Result<()> {
+ if cfg!(windows) && backend == Backend::Termion {
+ tracing::error!("termion backend is not supported on Windows");
+ }
+ let backend = match backend {
+ Backend::Crossterm => "crossterm",
+ Backend::Termion => "termion",
+ Backend::Termwiz => "termwiz",
+ };
+ run_cargo(vec![
+ "test",
+ "--all-targets",
+ "--no-default-features",
+ "--features",
+ backend,
+ ])
+}
+
+/// Run doc tests for the workspace's default packages
+fn test_docs() -> Result<()> {
+ run_cargo(vec!["test", "--doc", "--all-features"])
+}
+
+/// Run lib tests for the workspace's default packages
+fn test_libs() -> Result<()> {
+ run_cargo(vec!["test", "--all-targets", "--all-features"])
+}
+
+/// Run a cargo subcommand with the default toolchain
+fn run_cargo(args: Vec<&str>) -> Result<()> {
+ cmd("cargo", args).run_with_trace()?;
+ Ok(())
+}
+
+/// Run a cargo subcommand with the nightly toolchain
+fn run_cargo_nightly(args: Vec<&str>) -> Result<()> {
+ cmd("cargo", args)
+ // CARGO env var is set because we're running in a cargo subcommand
+ .env_remove("CARGO")
+ .env("RUSTUP_TOOLCHAIN", "nightly")
+ .run_with_trace()?;
+ Ok(())
+}
+
+/// An extension trait for `duct::Expression` that logs the command being run
+/// before running it.
+trait ExpressionExt {
+ /// Run the command and log the command being run
+ fn run_with_trace(&self) -> io::Result