├── .github ├── dependabot.yaml └── workflows │ ├── audit.yaml │ ├── clippy.yaml │ ├── docs.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── makeup-ansi ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── makeup-console ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── makeup-macros ├── Cargo.toml └── src │ └── lib.rs └── makeup ├── .gitignore ├── Cargo.toml ├── examples ├── container.rs ├── container_input.rs ├── external_message.rs ├── fps.rs ├── hello.rs ├── input.rs ├── spinner.rs └── wave.rs └── src ├── component.rs ├── components ├── container.rs ├── echo_text.rs ├── fps.rs ├── mod.rs ├── spinner.rs └── text_input.rs ├── input ├── mod.rs └── terminal.rs ├── lib.rs ├── post_office.rs ├── render ├── memory.rs ├── mod.rs └── terminal.rs ├── test ├── diff.rs └── mod.rs ├── ui.rs └── util.rs /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "12:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: "Run cargo audit" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "**.toml" 9 | pull_request: 10 | branches: 11 | - "mistress" 12 | paths: 13 | - "**.rs" 14 | - "**.toml" 15 | 16 | jobs: 17 | run-cargo-audit: 18 | strategy: 19 | matrix: 20 | version: ["stable"] 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - uses: "actions/checkout@v2" 24 | - name: "Install latest stable Rust" 25 | uses: "actions-rs/toolchain@v1" 26 | with: 27 | toolchain: "${{ matrix.version }}" 28 | override: true 29 | - name: "Install cargo-audit" 30 | run: "cargo install cargo-audit" 31 | - uses: "Swatinem/rust-cache@v1" 32 | with: 33 | key: "cargo-audit" 34 | - name: "Run cargo-audit" 35 | run: "cargo audit -q" 36 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yaml: -------------------------------------------------------------------------------- 1 | name: "Run clippy lints" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "**.toml" 9 | pull_request: 10 | branches: 11 | - "mistress" 12 | paths: 13 | - "**.rs" 14 | - "**.toml" 15 | 16 | jobs: 17 | run-clippy: 18 | strategy: 19 | matrix: 20 | version: ["stable", "1.70"] 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - uses: "actions/checkout@v2" 24 | - name: "Install latest stable Rust" 25 | uses: "actions-rs/toolchain@v1" 26 | with: 27 | toolchain: "${{ matrix.version }}" 28 | override: true 29 | components: "clippy" 30 | - uses: "Swatinem/rust-cache@v1" 31 | with: 32 | key: "clippy" 33 | - name: "Run clippy" 34 | run: "cargo clippy --all-targets --all-features -- -D warnings" 35 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: "Build docs" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "**.toml" 9 | pull_request: 10 | branches: 11 | - "mistress" 12 | paths: 13 | - "**.rs" 14 | - "**.toml" 15 | 16 | jobs: 17 | run-clippy: 18 | strategy: 19 | matrix: 20 | version: ["stable", "1.70", "nightly"] 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - uses: "actions/checkout@v2" 24 | - name: "Install latest stable Rust" 25 | uses: "actions-rs/toolchain@v1" 26 | with: 27 | toolchain: "${{ matrix.version }}" 28 | override: true 29 | - uses: "Swatinem/rust-cache@v1" 30 | with: 31 | key: "doc" 32 | - name: "Run cargo doc" 33 | run: "cargo doc --workspace --all-features --examples --no-deps --locked" 34 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: "Run all tests" 2 | on: 3 | push: 4 | branches: 5 | - "mistress" 6 | paths: 7 | - "**.rs" 8 | - "**.toml" 9 | pull_request: 10 | branches: 11 | - "mistress" 12 | paths: 13 | - "**.rs" 14 | - "**.toml" 15 | 16 | jobs: 17 | run-tests: 18 | strategy: 19 | matrix: 20 | version: ["stable", "nightly", "1.70"] 21 | runs-on: "ubuntu-latest" 22 | steps: 23 | - uses: "actions/checkout@v2" 24 | - name: "Install latest stable Rust" 25 | uses: "actions-rs/toolchain@v1" 26 | with: 27 | toolchain: "${{ matrix.version }}" 28 | override: true 29 | - uses: "Swatinem/rust-cache@v1" 30 | with: 31 | key: "clippy" 32 | - name: "Run tests" 33 | run: "cargo test" 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tmp 3 | /graph.svg 4 | /perf.data* 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: "https://github.com/pre-commit/pre-commit-hooks" 4 | rev: "v4.3.0" 5 | hooks: 6 | - id: "check-merge-conflict" 7 | - id: "check-toml" 8 | - id: "check-yaml" 9 | - id: "end-of-file-fixer" 10 | - id: "mixed-line-ending" 11 | - id: "trailing-whitespace" 12 | - repo: "local" 13 | hooks: 14 | - id: "format" 15 | name: "rust: cargo fmt" 16 | entry: "cargo fmt --all --check" 17 | language: "system" 18 | pass_filenames: false 19 | files: ".rs*$" 20 | - id: "clippy" 21 | name: "rust: cargo clippy" 22 | entry: "cargo clippy --all-targets --all-features -- -D warnings" 23 | language: "system" 24 | pass_filenames: false 25 | files: ".rs*$" 26 | - id: "test" 27 | name: "rust: cargo test" 28 | entry: "cargo test" 29 | language: "system" 30 | pass_filenames: false 31 | files: ".rs*$" 32 | - id: "doc" 33 | name: "rust: cargo doc" 34 | entry: "cargo doc --workspace --all-features --examples --no-deps --locked --frozen" 35 | language: "system" 36 | pass_filenames: false 37 | files: ".rs*$" 38 | - id: "audit" 39 | name: "rust: cargo audit" 40 | entry: "cargo audit -q" 41 | language: "system" 42 | pass_filenames: false 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": ["./makeup/Cargo.toml"] 3 | } 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.19.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "arrayvec" 22 | version = "0.7.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 25 | 26 | [[package]] 27 | name = "async-recursion" 28 | version = "1.0.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" 31 | dependencies = [ 32 | "proc-macro2", 33 | "quote", 34 | "syn 2.0.72", 35 | ] 36 | 37 | [[package]] 38 | name = "async-trait" 39 | version = "0.1.81" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" 42 | dependencies = [ 43 | "proc-macro2", 44 | "quote", 45 | "syn 2.0.72", 46 | ] 47 | 48 | [[package]] 49 | name = "autocfg" 50 | version = "1.1.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 53 | 54 | [[package]] 55 | name = "backtrace" 56 | version = "0.3.67" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" 59 | dependencies = [ 60 | "addr2line", 61 | "cc", 62 | "cfg-if", 63 | "libc", 64 | "miniz_oxide", 65 | "object", 66 | "rustc-demangle", 67 | ] 68 | 69 | [[package]] 70 | name = "bitflags" 71 | version = "1.3.2" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 74 | 75 | [[package]] 76 | name = "bitflags" 77 | version = "2.4.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 80 | 81 | [[package]] 82 | name = "bytes" 83 | version = "1.2.1" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 86 | 87 | [[package]] 88 | name = "cc" 89 | version = "1.0.79" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 92 | 93 | [[package]] 94 | name = "cfg-if" 95 | version = "1.0.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 98 | 99 | [[package]] 100 | name = "colorgrad" 101 | version = "0.6.2" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "6a5f405d474b9d05e0a093d3120e77e9bf26461b57a84b40aa2a221ac5617fb6" 104 | dependencies = [ 105 | "csscolorparser", 106 | ] 107 | 108 | [[package]] 109 | name = "csscolorparser" 110 | version = "0.6.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" 113 | dependencies = [ 114 | "phf", 115 | ] 116 | 117 | [[package]] 118 | name = "derivative" 119 | version = "2.2.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 122 | dependencies = [ 123 | "proc-macro2", 124 | "quote", 125 | "syn 1.0.103", 126 | ] 127 | 128 | [[package]] 129 | name = "either" 130 | version = "1.13.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 133 | 134 | [[package]] 135 | name = "eyre" 136 | version = "0.6.12" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 139 | dependencies = [ 140 | "indenter", 141 | "once_cell", 142 | ] 143 | 144 | [[package]] 145 | name = "futures-core" 146 | version = "0.3.30" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 149 | 150 | [[package]] 151 | name = "futures-macro" 152 | version = "0.3.30" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 155 | dependencies = [ 156 | "proc-macro2", 157 | "quote", 158 | "syn 2.0.72", 159 | ] 160 | 161 | [[package]] 162 | name = "futures-task" 163 | version = "0.3.30" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 166 | 167 | [[package]] 168 | name = "futures-util" 169 | version = "0.3.30" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 172 | dependencies = [ 173 | "futures-core", 174 | "futures-macro", 175 | "futures-task", 176 | "pin-project-lite", 177 | "pin-utils", 178 | "slab", 179 | ] 180 | 181 | [[package]] 182 | name = "getrandom" 183 | version = "0.2.8" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 186 | dependencies = [ 187 | "cfg-if", 188 | "libc", 189 | "wasi", 190 | ] 191 | 192 | [[package]] 193 | name = "gimli" 194 | version = "0.27.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" 197 | 198 | [[package]] 199 | name = "grid" 200 | version = "0.10.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" 203 | 204 | [[package]] 205 | name = "heck" 206 | version = "0.4.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 209 | 210 | [[package]] 211 | name = "hermit-abi" 212 | version = "0.3.9" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 215 | 216 | [[package]] 217 | name = "indenter" 218 | version = "0.3.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 221 | 222 | [[package]] 223 | name = "indoc" 224 | version = "2.0.4" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 227 | 228 | [[package]] 229 | name = "libc" 230 | version = "0.2.155" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 233 | 234 | [[package]] 235 | name = "lock_api" 236 | version = "0.4.9" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 239 | dependencies = [ 240 | "autocfg", 241 | "scopeguard", 242 | ] 243 | 244 | [[package]] 245 | name = "makeup" 246 | version = "0.0.8" 247 | dependencies = [ 248 | "async-recursion", 249 | "async-trait", 250 | "colorgrad", 251 | "derivative", 252 | "either", 253 | "eyre", 254 | "futures-util", 255 | "indoc", 256 | "libc", 257 | "makeup-ansi", 258 | "makeup-console", 259 | "makeup-macros", 260 | "rand", 261 | "strum", 262 | "taffy", 263 | "thiserror", 264 | "tokio", 265 | ] 266 | 267 | [[package]] 268 | name = "makeup-ansi" 269 | version = "0.0.3" 270 | dependencies = [ 271 | "eyre", 272 | ] 273 | 274 | [[package]] 275 | name = "makeup-console" 276 | version = "0.0.7" 277 | dependencies = [ 278 | "async-recursion", 279 | "eyre", 280 | "libc", 281 | "makeup-ansi", 282 | "nix", 283 | "thiserror", 284 | "tokio", 285 | ] 286 | 287 | [[package]] 288 | name = "makeup-macros" 289 | version = "0.0.2" 290 | dependencies = [ 291 | "proc-macro2", 292 | "quote", 293 | "syn 2.0.72", 294 | ] 295 | 296 | [[package]] 297 | name = "memchr" 298 | version = "2.5.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 301 | 302 | [[package]] 303 | name = "miniz_oxide" 304 | version = "0.6.2" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 307 | dependencies = [ 308 | "adler", 309 | ] 310 | 311 | [[package]] 312 | name = "mio" 313 | version = "1.0.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 316 | dependencies = [ 317 | "hermit-abi", 318 | "libc", 319 | "wasi", 320 | "windows-sys 0.52.0", 321 | ] 322 | 323 | [[package]] 324 | name = "nix" 325 | version = "0.27.1" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 328 | dependencies = [ 329 | "bitflags 2.4.0", 330 | "cfg-if", 331 | "libc", 332 | ] 333 | 334 | [[package]] 335 | name = "num-traits" 336 | version = "0.2.17" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 339 | dependencies = [ 340 | "autocfg", 341 | ] 342 | 343 | [[package]] 344 | name = "object" 345 | version = "0.30.4" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" 348 | dependencies = [ 349 | "memchr", 350 | ] 351 | 352 | [[package]] 353 | name = "once_cell" 354 | version = "1.19.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 357 | 358 | [[package]] 359 | name = "parking_lot" 360 | version = "0.12.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 363 | dependencies = [ 364 | "lock_api", 365 | "parking_lot_core", 366 | ] 367 | 368 | [[package]] 369 | name = "parking_lot_core" 370 | version = "0.9.4" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" 373 | dependencies = [ 374 | "cfg-if", 375 | "libc", 376 | "redox_syscall", 377 | "smallvec", 378 | "windows-sys 0.42.0", 379 | ] 380 | 381 | [[package]] 382 | name = "phf" 383 | version = "0.11.1" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" 386 | dependencies = [ 387 | "phf_macros", 388 | "phf_shared", 389 | ] 390 | 391 | [[package]] 392 | name = "phf_generator" 393 | version = "0.11.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" 396 | dependencies = [ 397 | "phf_shared", 398 | "rand", 399 | ] 400 | 401 | [[package]] 402 | name = "phf_macros" 403 | version = "0.11.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" 406 | dependencies = [ 407 | "phf_generator", 408 | "phf_shared", 409 | "proc-macro2", 410 | "quote", 411 | "syn 1.0.103", 412 | ] 413 | 414 | [[package]] 415 | name = "phf_shared" 416 | version = "0.11.1" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" 419 | dependencies = [ 420 | "siphasher", 421 | ] 422 | 423 | [[package]] 424 | name = "pin-project-lite" 425 | version = "0.2.12" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" 428 | 429 | [[package]] 430 | name = "pin-utils" 431 | version = "0.1.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 434 | 435 | [[package]] 436 | name = "ppv-lite86" 437 | version = "0.2.17" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 440 | 441 | [[package]] 442 | name = "proc-macro2" 443 | version = "1.0.86" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 446 | dependencies = [ 447 | "unicode-ident", 448 | ] 449 | 450 | [[package]] 451 | name = "quote" 452 | version = "1.0.35" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 455 | dependencies = [ 456 | "proc-macro2", 457 | ] 458 | 459 | [[package]] 460 | name = "rand" 461 | version = "0.8.5" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 464 | dependencies = [ 465 | "libc", 466 | "rand_chacha", 467 | "rand_core", 468 | ] 469 | 470 | [[package]] 471 | name = "rand_chacha" 472 | version = "0.3.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 475 | dependencies = [ 476 | "ppv-lite86", 477 | "rand_core", 478 | ] 479 | 480 | [[package]] 481 | name = "rand_core" 482 | version = "0.6.4" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 485 | dependencies = [ 486 | "getrandom", 487 | ] 488 | 489 | [[package]] 490 | name = "redox_syscall" 491 | version = "0.2.16" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 494 | dependencies = [ 495 | "bitflags 1.3.2", 496 | ] 497 | 498 | [[package]] 499 | name = "rustc-demangle" 500 | version = "0.1.23" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 503 | 504 | [[package]] 505 | name = "rustversion" 506 | version = "1.0.13" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" 509 | 510 | [[package]] 511 | name = "scopeguard" 512 | version = "1.1.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 515 | 516 | [[package]] 517 | name = "signal-hook-registry" 518 | version = "1.4.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 521 | dependencies = [ 522 | "libc", 523 | ] 524 | 525 | [[package]] 526 | name = "siphasher" 527 | version = "0.3.10" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" 530 | 531 | [[package]] 532 | name = "slab" 533 | version = "0.4.7" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 536 | dependencies = [ 537 | "autocfg", 538 | ] 539 | 540 | [[package]] 541 | name = "slotmap" 542 | version = "1.0.7" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" 545 | dependencies = [ 546 | "version_check", 547 | ] 548 | 549 | [[package]] 550 | name = "smallvec" 551 | version = "1.10.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 554 | 555 | [[package]] 556 | name = "socket2" 557 | version = "0.5.5" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 560 | dependencies = [ 561 | "libc", 562 | "windows-sys 0.48.0", 563 | ] 564 | 565 | [[package]] 566 | name = "strum" 567 | version = "0.26.1" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" 570 | dependencies = [ 571 | "strum_macros", 572 | ] 573 | 574 | [[package]] 575 | name = "strum_macros" 576 | version = "0.26.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" 579 | dependencies = [ 580 | "heck", 581 | "proc-macro2", 582 | "quote", 583 | "rustversion", 584 | "syn 2.0.72", 585 | ] 586 | 587 | [[package]] 588 | name = "syn" 589 | version = "1.0.103" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" 592 | dependencies = [ 593 | "proc-macro2", 594 | "quote", 595 | "unicode-ident", 596 | ] 597 | 598 | [[package]] 599 | name = "syn" 600 | version = "2.0.72" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 603 | dependencies = [ 604 | "proc-macro2", 605 | "quote", 606 | "unicode-ident", 607 | ] 608 | 609 | [[package]] 610 | name = "taffy" 611 | version = "0.3.18" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "3c2287b6d7f721ada4cddf61ade5e760b2c6207df041cac9bfaa192897362fd3" 614 | dependencies = [ 615 | "arrayvec", 616 | "grid", 617 | "num-traits", 618 | "slotmap", 619 | ] 620 | 621 | [[package]] 622 | name = "thiserror" 623 | version = "1.0.63" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 626 | dependencies = [ 627 | "thiserror-impl", 628 | ] 629 | 630 | [[package]] 631 | name = "thiserror-impl" 632 | version = "1.0.63" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 635 | dependencies = [ 636 | "proc-macro2", 637 | "quote", 638 | "syn 2.0.72", 639 | ] 640 | 641 | [[package]] 642 | name = "tokio" 643 | version = "1.39.1" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" 646 | dependencies = [ 647 | "backtrace", 648 | "bytes", 649 | "libc", 650 | "mio", 651 | "parking_lot", 652 | "pin-project-lite", 653 | "signal-hook-registry", 654 | "socket2", 655 | "tokio-macros", 656 | "tracing", 657 | "windows-sys 0.52.0", 658 | ] 659 | 660 | [[package]] 661 | name = "tokio-macros" 662 | version = "2.4.0" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 665 | dependencies = [ 666 | "proc-macro2", 667 | "quote", 668 | "syn 2.0.72", 669 | ] 670 | 671 | [[package]] 672 | name = "tracing" 673 | version = "0.1.40" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 676 | dependencies = [ 677 | "pin-project-lite", 678 | "tracing-core", 679 | ] 680 | 681 | [[package]] 682 | name = "tracing-core" 683 | version = "0.1.32" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 686 | dependencies = [ 687 | "once_cell", 688 | ] 689 | 690 | [[package]] 691 | name = "unicode-ident" 692 | version = "1.0.5" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 695 | 696 | [[package]] 697 | name = "version_check" 698 | version = "0.9.4" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 701 | 702 | [[package]] 703 | name = "wasi" 704 | version = "0.11.0+wasi-snapshot-preview1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 707 | 708 | [[package]] 709 | name = "windows-sys" 710 | version = "0.42.0" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 713 | dependencies = [ 714 | "windows_aarch64_gnullvm 0.42.1", 715 | "windows_aarch64_msvc 0.42.1", 716 | "windows_i686_gnu 0.42.1", 717 | "windows_i686_msvc 0.42.1", 718 | "windows_x86_64_gnu 0.42.1", 719 | "windows_x86_64_gnullvm 0.42.1", 720 | "windows_x86_64_msvc 0.42.1", 721 | ] 722 | 723 | [[package]] 724 | name = "windows-sys" 725 | version = "0.48.0" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 728 | dependencies = [ 729 | "windows-targets 0.48.0", 730 | ] 731 | 732 | [[package]] 733 | name = "windows-sys" 734 | version = "0.52.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 737 | dependencies = [ 738 | "windows-targets 0.52.6", 739 | ] 740 | 741 | [[package]] 742 | name = "windows-targets" 743 | version = "0.48.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 746 | dependencies = [ 747 | "windows_aarch64_gnullvm 0.48.0", 748 | "windows_aarch64_msvc 0.48.0", 749 | "windows_i686_gnu 0.48.0", 750 | "windows_i686_msvc 0.48.0", 751 | "windows_x86_64_gnu 0.48.0", 752 | "windows_x86_64_gnullvm 0.48.0", 753 | "windows_x86_64_msvc 0.48.0", 754 | ] 755 | 756 | [[package]] 757 | name = "windows-targets" 758 | version = "0.52.6" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 761 | dependencies = [ 762 | "windows_aarch64_gnullvm 0.52.6", 763 | "windows_aarch64_msvc 0.52.6", 764 | "windows_i686_gnu 0.52.6", 765 | "windows_i686_gnullvm", 766 | "windows_i686_msvc 0.52.6", 767 | "windows_x86_64_gnu 0.52.6", 768 | "windows_x86_64_gnullvm 0.52.6", 769 | "windows_x86_64_msvc 0.52.6", 770 | ] 771 | 772 | [[package]] 773 | name = "windows_aarch64_gnullvm" 774 | version = "0.42.1" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 777 | 778 | [[package]] 779 | name = "windows_aarch64_gnullvm" 780 | version = "0.48.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 783 | 784 | [[package]] 785 | name = "windows_aarch64_gnullvm" 786 | version = "0.52.6" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 789 | 790 | [[package]] 791 | name = "windows_aarch64_msvc" 792 | version = "0.42.1" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 795 | 796 | [[package]] 797 | name = "windows_aarch64_msvc" 798 | version = "0.48.0" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 801 | 802 | [[package]] 803 | name = "windows_aarch64_msvc" 804 | version = "0.52.6" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 807 | 808 | [[package]] 809 | name = "windows_i686_gnu" 810 | version = "0.42.1" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 813 | 814 | [[package]] 815 | name = "windows_i686_gnu" 816 | version = "0.48.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 819 | 820 | [[package]] 821 | name = "windows_i686_gnu" 822 | version = "0.52.6" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 825 | 826 | [[package]] 827 | name = "windows_i686_gnullvm" 828 | version = "0.52.6" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 831 | 832 | [[package]] 833 | name = "windows_i686_msvc" 834 | version = "0.42.1" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 837 | 838 | [[package]] 839 | name = "windows_i686_msvc" 840 | version = "0.48.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 843 | 844 | [[package]] 845 | name = "windows_i686_msvc" 846 | version = "0.52.6" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 849 | 850 | [[package]] 851 | name = "windows_x86_64_gnu" 852 | version = "0.42.1" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 855 | 856 | [[package]] 857 | name = "windows_x86_64_gnu" 858 | version = "0.48.0" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 861 | 862 | [[package]] 863 | name = "windows_x86_64_gnu" 864 | version = "0.52.6" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 867 | 868 | [[package]] 869 | name = "windows_x86_64_gnullvm" 870 | version = "0.42.1" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 873 | 874 | [[package]] 875 | name = "windows_x86_64_gnullvm" 876 | version = "0.48.0" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 879 | 880 | [[package]] 881 | name = "windows_x86_64_gnullvm" 882 | version = "0.52.6" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 885 | 886 | [[package]] 887 | name = "windows_x86_64_msvc" 888 | version = "0.42.1" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 891 | 892 | [[package]] 893 | name = "windows_x86_64_msvc" 894 | version = "0.48.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 897 | 898 | [[package]] 899 | name = "windows_x86_64_msvc" 900 | version = "0.52.6" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 903 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["makeup-ansi", "makeup-console", "makeup-macros", "makeup"] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022-present amy null. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # makeup 2 | 3 | Pretty CLI / TUI interfaces. 4 | 5 | MSRV 1.70. 6 | 7 | 8 | ## READ ME FIRST 9 | 10 | makeup is still early-stage!!! Treat it like the alpha project it is. 11 | 12 | [crates.io](https://crates.io/crates/makeup) 13 | 14 | ### Usage examples 15 | 16 | See [`examples/`](https://github.com/queer/makeup/tree/mistress/makeup/examples)! 17 | 18 | ### Demos 19 | 20 | `cargo run --example wave` 21 | 22 | ## Setup 23 | 24 | Install [pre-commit](https://pre-commit.com/). 25 | 26 | ```bash 27 | pre-commit install 28 | pre-commit autoupdate 29 | cargo install cargo-audit 30 | ``` 31 | 32 | ## Features 33 | 34 | - 60fps by default. 35 | - Input and render are fully decoupled, ie input can NEVER block rendering. 36 | - Message-passing-like architecture 37 | - Components are updated and rendered asynchronously. 38 | - Components must not reference each other directly, but instead communicate 39 | via message passing. 40 | - Component updates are just reading the message queue from the mailbox, and 41 | updating the component's state accordingly. makeup assumes that **any** 42 | potentially-blocking task will be moved out of the update/render loop via 43 | `tokio::spawn` or similar, and managed via message-passing. 44 | - Render-backend-agnostic. 45 | - Render backends are async. 46 | - Default backends are memory and UNIX-compatible terminal. 47 | - Render backends can be implemented for other protocols! 48 | - Provided to the UI on instantiation. 49 | - Ideas: WASM + ``? Multiplex over the network? 50 | -------------------------------------------------------------------------------- /makeup-ansi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "makeup-ansi" 3 | description = "ANSI helper library for makeup" 4 | repository = "https://github.com/queer/makeup" 5 | license = "MIT" 6 | version = "0.0.3" 7 | edition = "2021" 8 | categories = ["command-line-interface"] 9 | keywords = ["ansi"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | eyre = "0.6.12" 15 | -------------------------------------------------------------------------------- /makeup-ansi/README.md: -------------------------------------------------------------------------------- 1 | # makeup-ansi 2 | -------------------------------------------------------------------------------- /makeup-ansi/src/lib.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | 3 | pub mod prelude { 4 | pub use crate::{ 5 | Ansi, Colour, CursorStyle, CursorVisibility, DisplayEraseMode, LineEraseMode, SgrParameter, 6 | }; 7 | } 8 | 9 | /// Convert a string literal to an ANSI escape sequence. 10 | /// See: 11 | #[macro_export] 12 | macro_rules! ansi { 13 | ($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) }; 14 | } 15 | 16 | /// ANSI escape sequences. Can be directly formatted into strings. 17 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 18 | pub enum Ansi { 19 | // Cursor manipulation 20 | /// Set the (x, y) cursor position. 21 | CursorPosition(u64, u64), 22 | /// Set the cursor style. 23 | CursorStyle(CursorStyle), 24 | /// Set the cursor visibility. 25 | CursorVisibility(CursorVisibility), 26 | /// Move the cursor up. 27 | CursorUp(u64), 28 | /// Move the cursor down. 29 | CursorDown(u64), 30 | /// Move the cursor left. 31 | CursorLeft(u64), 32 | /// Move the cursor right. 33 | CursorRight(u64), 34 | /// Move the cursor to the start of line `count` steps down. 35 | CursorNextLine(u64), 36 | /// Move the cursor to the start of line `count` steps up. 37 | CursorPreviousLine(u64), 38 | /// Move the cursor to the column `x`. 39 | CursorHorizontalAbsolute(u64), 40 | /// Save the current position of the cursor. 41 | SaveCursorPosition, 42 | /// Restore the position of the cursor. 43 | RestoreCursorPosition, 44 | 45 | // Text manipulation 46 | /// Erase part of the current display. 47 | EraseInDisplay(DisplayEraseMode), 48 | /// Erase part of the current line. 49 | EraseInLine(LineEraseMode), 50 | /// Scroll the display up. 51 | ScrollUp(u64), 52 | /// Scroll the display down. 53 | ScrollDown(u64), 54 | 55 | // Terminal manipulation 56 | /// Set the terminal size. 57 | /// This is not supported on Windows. 58 | TerminalSize(u64, u64), 59 | /// Set the terminal title. 60 | /// This is not supported on Windows. 61 | TerminalTitle(String), 62 | /// Set the terminal foreground colour. 63 | /// This is not supported on Windows. 64 | TerminalForegroundColour(Colour), 65 | /// Set the terminal background colour. 66 | /// This is not supported on Windows. 67 | TerminalBackgroundColour(Colour), 68 | /// Set attributes on the current terminal. 69 | /// This is not supported on Windows. 70 | /// See: 71 | Sgr(Vec), 72 | } 73 | 74 | impl Ansi { 75 | /// Render this ANSI escape sequence into the given `Write`able. 76 | pub fn render(&self, f: &mut impl std::fmt::Write) -> Result<()> { 77 | match self { 78 | // Cursor 79 | Self::CursorPosition(x, y) => { 80 | write!(f, ansi!("{};{}H"), y + 1, x + 1) 81 | } 82 | Self::CursorStyle(style) => match style { 83 | CursorStyle::Block => { 84 | write!(f, ansi!("2 q")) 85 | } 86 | CursorStyle::Bar => { 87 | write!(f, ansi!("5 q")) 88 | } 89 | CursorStyle::HollowBlock => { 90 | write!(f, ansi!("2 q")) 91 | } 92 | }, 93 | Self::CursorVisibility(visibility) => match visibility { 94 | CursorVisibility::Visible => { 95 | write!(f, ansi!("?25h")) 96 | } 97 | CursorVisibility::Invisible => { 98 | write!(f, ansi!("?25l")) 99 | } 100 | }, 101 | Self::CursorUp(count) => { 102 | write!(f, ansi!("{}A"), count) 103 | } 104 | Self::CursorDown(count) => { 105 | write!(f, ansi!("{}B"), count) 106 | } 107 | Self::CursorLeft(count) => { 108 | write!(f, ansi!("{}D"), count) 109 | } 110 | Self::CursorRight(count) => { 111 | write!(f, ansi!("{}C"), count) 112 | } 113 | Self::CursorNextLine(count) => { 114 | write!(f, ansi!("{}E"), count) 115 | } 116 | Self::CursorPreviousLine(count) => { 117 | write!(f, ansi!("{}F"), count) 118 | } 119 | Self::CursorHorizontalAbsolute(x) => { 120 | write!(f, ansi!("{}G"), x + 1) 121 | } 122 | Self::SaveCursorPosition => { 123 | write!(f, ansi!("s")) 124 | } 125 | Self::RestoreCursorPosition => { 126 | write!(f, ansi!("u")) 127 | } 128 | 129 | // Terminal 130 | Self::EraseInDisplay(mode) => match mode { 131 | DisplayEraseMode::All => { 132 | write!(f, ansi!("2J")) 133 | } 134 | DisplayEraseMode::FromCursorToEnd => { 135 | write!(f, ansi!("0J")) 136 | } 137 | DisplayEraseMode::FromCursorToStart => { 138 | write!(f, ansi!("1J")) 139 | } 140 | DisplayEraseMode::ScrollbackBuffer => { 141 | write!(f, ansi!("3J")) 142 | } 143 | }, 144 | Self::EraseInLine(mode) => match mode { 145 | LineEraseMode::All => { 146 | write!(f, ansi!("2K")) 147 | } 148 | LineEraseMode::FromCursorToEnd => { 149 | write!(f, ansi!("0K")) 150 | } 151 | LineEraseMode::FromCursorToStart => { 152 | write!(f, ansi!("1K")) 153 | } 154 | }, 155 | Self::ScrollUp(count) => { 156 | write!(f, ansi!("{}S"), count) 157 | } 158 | Self::ScrollDown(count) => { 159 | write!(f, ansi!("{}T"), count) 160 | } 161 | Self::TerminalSize(width, height) => { 162 | write!(f, ansi!("8;{};{}t"), height, width) 163 | } 164 | Self::TerminalTitle(title) => { 165 | write!(f, "\x1B]0;{title}\x07") 166 | } 167 | Self::TerminalForegroundColour(colour) => { 168 | write!(f, ansi!("38;5;{}"), colour.index()) 169 | } 170 | Self::TerminalBackgroundColour(colour) => { 171 | write!(f, ansi!("48;5;{}"), colour.index()) 172 | } 173 | Self::Sgr(attributes) => { 174 | let mut first = true; 175 | write!(f, ansi!(""))?; 176 | for attribute in attributes { 177 | if first { 178 | first = false; 179 | } else { 180 | write!(f, ";")?; 181 | } 182 | match attribute { 183 | SgrParameter::Reset => { 184 | write!(f, "0") 185 | } 186 | SgrParameter::Bold => { 187 | write!(f, "1") 188 | } 189 | SgrParameter::Faint => { 190 | write!(f, "2") 191 | } 192 | SgrParameter::Italic => { 193 | write!(f, "3") 194 | } 195 | SgrParameter::Underline => { 196 | write!(f, "4") 197 | } 198 | SgrParameter::Blink => { 199 | write!(f, "5") 200 | } 201 | SgrParameter::RapidBlink => { 202 | write!(f, "6") 203 | } 204 | SgrParameter::ReverseVideo => { 205 | write!(f, "7") 206 | } 207 | SgrParameter::Conceal => { 208 | write!(f, "8") 209 | } 210 | SgrParameter::CrossedOut => { 211 | write!(f, "9") 212 | } 213 | SgrParameter::PrimaryFont => { 214 | write!(f, "10") 215 | } 216 | SgrParameter::AlternativeFont(idx) => { 217 | write!(f, "{}", 10 + idx) 218 | } 219 | SgrParameter::Fraktur => { 220 | write!(f, "20") 221 | } 222 | SgrParameter::DoubleUnderline => { 223 | write!(f, "21") 224 | } 225 | SgrParameter::NormalIntensity => { 226 | write!(f, "22") 227 | } 228 | SgrParameter::NotItalicOrBlackletter => { 229 | write!(f, "23") 230 | } 231 | SgrParameter::NotUnderlined => { 232 | write!(f, "24") 233 | } 234 | SgrParameter::SteadyCursor => { 235 | write!(f, "25") 236 | } 237 | SgrParameter::ProportionalSpacing => { 238 | write!(f, "26") 239 | } 240 | SgrParameter::NotReversed => { 241 | write!(f, "27") 242 | } 243 | SgrParameter::Reveal => { 244 | write!(f, "28") 245 | } 246 | SgrParameter::NotCrossedOut => { 247 | write!(f, "29") 248 | } 249 | SgrParameter::Framed => { 250 | write!(f, "51") 251 | } 252 | SgrParameter::Encircled => { 253 | write!(f, "52") 254 | } 255 | SgrParameter::Overlined => { 256 | write!(f, "53") 257 | } 258 | SgrParameter::NotFramedOrEncircled => { 259 | write!(f, "54") 260 | } 261 | SgrParameter::NotOverlined => { 262 | write!(f, "55") 263 | } 264 | SgrParameter::IdeogramUnderlineOrRightSideLine => { 265 | write!(f, "60") 266 | } 267 | SgrParameter::IdeogramDoubleUnderlineOrDoubleLineOnTheRightSide => { 268 | write!(f, "61") 269 | } 270 | SgrParameter::IdeogramOverlineOrLeftSideLine => { 271 | write!(f, "62") 272 | } 273 | SgrParameter::IdeogramDoubleOverlineOrDoubleLineOnTheLeftSide => { 274 | write!(f, "63") 275 | } 276 | SgrParameter::IdeogramStressMarking => { 277 | write!(f, "64") 278 | } 279 | SgrParameter::IdeogramAttributesOff => { 280 | write!(f, "65") 281 | } 282 | SgrParameter::ForegroundColour(colour) => { 283 | write!(f, "38;5;{}", colour.index()) 284 | } 285 | SgrParameter::BackgroundColour(colour) => { 286 | write!(f, "48;5;{}", colour.index()) 287 | } 288 | SgrParameter::HexForegroundColour(hex) => { 289 | let (r, g, b) = Self::rgb(hex); 290 | write!(f, "38;2;{r};{g};{b}") 291 | } 292 | SgrParameter::HexBackgroundColour(hex) => { 293 | let (r, g, b) = Self::rgb(hex); 294 | write!(f, "48;2;{r};{g};{b}") 295 | } 296 | SgrParameter::DefaultForegroundColour => { 297 | write!(f, "39") 298 | } 299 | SgrParameter::DefaultBackgroundColour => { 300 | write!(f, "49") 301 | } 302 | SgrParameter::DisableProportionalSpacing => { 303 | write!(f, "50") 304 | } 305 | SgrParameter::UnderlineColour(colour) => { 306 | write!(f, "58;5;{}", colour.index()) 307 | } 308 | SgrParameter::HexUnderlineColour(hex) => { 309 | let (r, g, b) = Self::rgb(hex); 310 | write!(f, "58;2;{r};{g};{b}") 311 | } 312 | SgrParameter::DefaultUnderlineColour => { 313 | write!(f, "59") 314 | } 315 | SgrParameter::Superscript => { 316 | write!(f, "73") 317 | } 318 | SgrParameter::Subscript => { 319 | write!(f, "74") 320 | } 321 | SgrParameter::NotSuperscriptOrSubscript => { 322 | write!(f, "75") 323 | } 324 | }?; 325 | } 326 | write!(f, "m") 327 | } 328 | } 329 | .map_err(|e| e.into()) 330 | } 331 | 332 | /// Convert a hex colour to RGB. 333 | fn rgb(hex: &u32) -> (u32, u32, u32) { 334 | let r = (hex >> 16) & 0xFF; 335 | let g = (hex >> 8) & 0xFF; 336 | let b = hex & 0xFF; 337 | (r, g, b) 338 | } 339 | } 340 | 341 | impl std::fmt::Display for Ansi { 342 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 343 | self.render(f).map_err(|_| std::fmt::Error) 344 | } 345 | } 346 | 347 | /// Terminal cursor styles. 348 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 349 | pub enum CursorStyle { 350 | /// The cursor is a block. 351 | Block, 352 | 353 | /// The cursor is a bar. 354 | Bar, 355 | 356 | /// The cursor is a hollow block. 357 | HollowBlock, 358 | } 359 | 360 | /// Terminal cursor visibility. 361 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 362 | pub enum CursorVisibility { 363 | /// The cursor is visible. 364 | Visible, 365 | 366 | /// The cursor is invisible. 367 | Invisible, 368 | } 369 | 370 | /// Default 8-bit colour palette. 371 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] 372 | pub enum Colour { 373 | /// Black. 374 | Black, 375 | 376 | /// Red. 377 | Red, 378 | 379 | /// Green. 380 | Green, 381 | 382 | /// Yellow. 383 | Yellow, 384 | 385 | /// Blue. 386 | Blue, 387 | 388 | /// Magenta. 389 | Magenta, 390 | 391 | /// Cyan. 392 | Cyan, 393 | 394 | /// White. 395 | White, 396 | 397 | /// Bright black. 398 | BrightBlack, 399 | 400 | /// Bright red. 401 | BrightRed, 402 | 403 | /// Bright green. 404 | BrightGreen, 405 | 406 | /// Bright yellow. 407 | BrightYellow, 408 | 409 | /// Bright blue. 410 | BrightBlue, 411 | 412 | /// Bright magenta. 413 | BrightMagenta, 414 | 415 | /// Bright cyan. 416 | BrightCyan, 417 | 418 | /// Bright white. 419 | BrightWhite, 420 | } 421 | 422 | impl Colour { 423 | /// Index in the enum. 424 | pub fn index(&self) -> u64 { 425 | *self as u64 426 | } 427 | } 428 | 429 | /// Erase part or all of the current display. 430 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 431 | pub enum DisplayEraseMode { 432 | /// Erase from the cursor to the end of the display. 433 | FromCursorToEnd, 434 | 435 | /// Erase from the cursor to the start of the display. 436 | FromCursorToStart, 437 | 438 | /// Erase the entire display. 439 | All, 440 | 441 | /// Erase the scrollback buffer. 442 | ScrollbackBuffer, 443 | } 444 | 445 | /// Erase part or all of the current line. Does not move the cursor. 446 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 447 | pub enum LineEraseMode { 448 | /// Erase from the cursor to the end of the line. 449 | FromCursorToEnd, 450 | 451 | /// Erase from the cursor to the start of the line. 452 | FromCursorToStart, 453 | 454 | /// Erase the entire line. 455 | All, 456 | } 457 | 458 | /// See: 459 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 460 | pub enum SgrParameter { 461 | /// Reset all attributes. 462 | Reset, 463 | 464 | /// Bold. 465 | Bold, 466 | 467 | /// Faint. 468 | Faint, 469 | 470 | /// Italic. 471 | Italic, 472 | 473 | /// Underline. 474 | Underline, 475 | 476 | /// Blink. 477 | Blink, 478 | 479 | /// Rapid blink. 480 | RapidBlink, 481 | 482 | /// Reverse video (note: Wikipedia notes inconsistent behaviour). Also 483 | /// known as "invert." 484 | ReverseVideo, 485 | 486 | /// Conceal / hide text (note: Wikipedia notes lack of wide support). 487 | Conceal, 488 | 489 | /// Crossed out. Not supported in Terminal.app. 490 | CrossedOut, 491 | 492 | /// Select the primary font. 493 | PrimaryFont, 494 | 495 | /// Select the alternative font at the given index (N-10). 496 | AlternativeFont(u64), 497 | 498 | /// Fraktur/Gothic mode (note: Wikipedia notes lack of wide support). 499 | Fraktur, 500 | 501 | /// Double underline. Note: On some systems, this may instead disable 502 | /// `Bold`. 503 | DoubleUnderline, 504 | 505 | /// Normal intensity. 506 | NormalIntensity, 507 | 508 | /// Not italic or blackletter. 509 | NotItalicOrBlackletter, 510 | 511 | /// Not underlined. 512 | NotUnderlined, 513 | 514 | /// Steady cursor (not blinking). 515 | SteadyCursor, 516 | 517 | /// Proportional spacing. 518 | /// Note: Wikipedia says: 519 | /// > ITU T.61 and T.416, not known to be used on terminals. 520 | ProportionalSpacing, 521 | 522 | /// Not reversed. 523 | /// Presumably undoes `ReverseVideo`, needs testing. 524 | NotReversed, 525 | 526 | /// Reveal concealed text. 527 | /// Presumably undoes `Conceal`, needs testing. 528 | Reveal, 529 | 530 | /// Not crossed out. 531 | NotCrossedOut, 532 | 533 | /// Set foreground colour to the given colour. 534 | ForegroundColour(Colour), 535 | 536 | /// Set background colour to the given colour. 537 | BackgroundColour(Colour), 538 | 539 | /// Set the foreground colour to the given hex colour. 540 | HexForegroundColour(u32), 541 | 542 | /// Set the background colour to the given hex colour. 543 | HexBackgroundColour(u32), 544 | 545 | /// Presumably resets to the default foreground colour, needs testing. 546 | DefaultForegroundColour, 547 | 548 | /// Presumably resets to the default background colour, needs testing. 549 | DefaultBackgroundColour, 550 | 551 | /// Disable proportional spacing. 552 | DisableProportionalSpacing, 553 | 554 | /// Set the framing (encircled) attribute. 555 | Framed, 556 | 557 | /// Set the encircled attribute. 558 | Encircled, 559 | 560 | /// Set the overlined attribute. 561 | /// Note: Not supported in Terminal.app. 562 | /// Note: On some systems, this may instead enable `Bold`. 563 | Overlined, 564 | 565 | /// Not framed or encircled. 566 | NotFramedOrEncircled, 567 | 568 | /// Not overlined. 569 | NotOverlined, 570 | 571 | /// Set the underline colour. 572 | /// Note: Not in standard, implemented in Kitty, VTE, mintty, iTerm2. 573 | UnderlineColour(Colour), 574 | 575 | /// Set the underline colour to the given hex colour. 576 | /// Note: Not in standard, implemented in Kitty, VTE, mintty, iTerm2. 577 | HexUnderlineColour(u32), 578 | 579 | /// Set the underline colour to the default. 580 | /// Note: Not in standard, implemented in Kitty, VTE, mintty, iTerm2. 581 | DefaultUnderlineColour, 582 | 583 | /// Ideogram underline or right side line. 584 | IdeogramUnderlineOrRightSideLine, 585 | 586 | /// Ideogram double underline or double line on the right side. 587 | IdeogramDoubleUnderlineOrDoubleLineOnTheRightSide, 588 | 589 | /// Ideogram overline or left side line. 590 | IdeogramOverlineOrLeftSideLine, 591 | 592 | /// Ideogram double overline or double line on the left side. 593 | IdeogramDoubleOverlineOrDoubleLineOnTheLeftSide, 594 | 595 | /// Ideogram stress marking. 596 | IdeogramStressMarking, 597 | 598 | /// Ideogram attributes off. 599 | /// Resets: 600 | /// - `IdeogramUnderlineOrRightSideLine` 601 | /// - `IdeogramDoubleUnderlineOrDoubleLineOnTheRightSide` 602 | /// - `IdeogramOverlineOrLeftSideLine` 603 | /// - `IdeogramDoubleOverlineOrDoubleLineOnTheLeftSide` 604 | /// - `IdeogramStressMarking`. 605 | IdeogramAttributesOff, 606 | 607 | /// Implemented only in mintty. 608 | Superscript, 609 | 610 | /// Implemented only in mintty. 611 | Subscript, 612 | 613 | /// Implemented only in mintty. 614 | NotSuperscriptOrSubscript, 615 | } 616 | 617 | #[cfg(test)] 618 | mod tests { 619 | use eyre::Result; 620 | 621 | use super::{Ansi, DisplayEraseMode, SgrParameter}; 622 | 623 | #[test] 624 | fn test_works_as_expected() -> Result<()> { 625 | let mut buffer = String::new(); 626 | Ansi::CursorPosition(0, 0).render(&mut buffer)?; 627 | assert_eq!("\u{1b}[1;1H", buffer); 628 | buffer.clear(); 629 | 630 | Ansi::CursorDown(1).render(&mut buffer)?; 631 | assert_eq!("\u{1b}[1B", buffer); 632 | buffer.clear(); 633 | 634 | Ansi::CursorUp(1).render(&mut buffer)?; 635 | assert_eq!("\u{1b}[1A", buffer); 636 | buffer.clear(); 637 | 638 | Ansi::CursorLeft(1).render(&mut buffer)?; 639 | assert_eq!("\u{1b}[1D", buffer); 640 | buffer.clear(); 641 | 642 | Ansi::CursorRight(1).render(&mut buffer)?; 643 | assert_eq!("\u{1b}[1C", buffer); 644 | buffer.clear(); 645 | 646 | Ansi::CursorNextLine(1).render(&mut buffer)?; 647 | assert_eq!("\u{1b}[1E", buffer); 648 | buffer.clear(); 649 | 650 | Ansi::CursorPreviousLine(1).render(&mut buffer)?; 651 | assert_eq!("\u{1b}[1F", buffer); 652 | buffer.clear(); 653 | 654 | Ansi::CursorHorizontalAbsolute(1).render(&mut buffer)?; 655 | assert_eq!("\u{1b}[2G", buffer); 656 | buffer.clear(); 657 | 658 | Ansi::CursorPosition(1, 1).render(&mut buffer)?; 659 | assert_eq!("\u{1b}[2;2H", buffer); 660 | buffer.clear(); 661 | 662 | Ansi::EraseInDisplay(DisplayEraseMode::All).render(&mut buffer)?; 663 | assert_eq!("\u{1b}[2J", buffer); 664 | buffer.clear(); 665 | 666 | Ansi::Sgr(vec![SgrParameter::HexForegroundColour(0xDB325C)]).render(&mut buffer)?; 667 | assert_eq!("\u{1b}[38;2;219;50;92m", buffer); 668 | buffer.clear(); 669 | 670 | Ansi::Sgr(vec![SgrParameter::HexBackgroundColour(0xDB325C)]).render(&mut buffer)?; 671 | assert_eq!("\u{1b}[48;2;219;50;92m", buffer); 672 | buffer.clear(); 673 | 674 | Ok(()) 675 | } 676 | } 677 | -------------------------------------------------------------------------------- /makeup-console/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "makeup-console" 3 | description = "Console helper library for makeup" 4 | repository = "https://github.com/queer/makeup" 5 | license = "MIT" 6 | version = "0.0.7" 7 | edition = "2021" 8 | categories = ["command-line-interface"] 9 | keywords = ["console"] 10 | 11 | [dependencies] 12 | tokio = { version = "1.39.1", features = ["full"] } 13 | eyre = "0.6.12" 14 | nix = { version = "0.27.1", features = ["poll", "signal", "term", "fs"] } 15 | libc = "0.2.155" 16 | async-recursion = "1.0.5" 17 | thiserror = "1.0.63" 18 | 19 | [dependencies.makeup-ansi] 20 | path = "../makeup-ansi" 21 | version = "0.0.3" 22 | -------------------------------------------------------------------------------- /makeup-console/README.md: -------------------------------------------------------------------------------- 1 | # makeup-console 2 | -------------------------------------------------------------------------------- /makeup-console/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::os::fd::{BorrowedFd, RawFd}; 2 | use std::os::unix::prelude::AsRawFd; 3 | use std::time::Duration; 4 | 5 | use async_recursion::async_recursion; 6 | use eyre::{eyre, Result}; 7 | use nix::poll::{poll, PollFd, PollFlags}; 8 | use nix::sys::select::FdSet; 9 | use nix::sys::signal::Signal; 10 | use nix::sys::signalfd::SigSet; 11 | use nix::sys::termios; 12 | use nix::sys::termios::InputFlags; 13 | use nix::sys::time::TimeSpec; 14 | 15 | #[derive(Debug, Clone)] // TODO: Are clone bounds safe here? 16 | pub struct ConsoleState<'a>(#[doc(hidden)] BorrowedFd<'a>); 17 | 18 | pub async fn init(fd: Option) -> Result> { 19 | // Safety: It's impossible for these to not be valid fds 20 | Ok(ConsoleState(unsafe { 21 | BorrowedFd::borrow_raw(if let Some(fd) = fd { 22 | fd 23 | } else { 24 | std::io::stderr().as_raw_fd() 25 | }) 26 | })) 27 | } 28 | 29 | /// - Check if stdin is a terminal (libc::isatty == 1) 30 | /// - If not, open /dev/tty 31 | /// - Put the terminal in raw input mode 32 | /// - Enable TCSADRAIN 33 | /// - Read a byte 34 | /// - If \x1b, csi, so read next byte 35 | /// - If next byte is [, start reading control sequence 36 | /// - Match next byte 37 | /// - A => up 38 | /// - B => down 39 | /// - C => right 40 | /// - D => left 41 | /// - H => home 42 | /// - F => end 43 | /// - Z => shift-tab 44 | /// - _ => 45 | /// - Match next byte 46 | /// - ~ => 47 | /// - Match next byte 48 | /// - 1 => home 49 | /// - 2 => insert 50 | /// - 3 => delete 51 | /// - 4 => end 52 | /// - 5 => page up 53 | /// - 6 => page down 54 | /// - 7 => home 55 | /// - 8 => end 56 | /// - Else, the escape sequence was unknown 57 | /// - Else, the escape sequence was unknown 58 | /// - Else, if next byte is not [, bail out on unknown control sequence 59 | /// - Else, if there was no next byte, input was 60 | /// - Else, if byte & 224u8 == 192u8, Unicode 2-byte 61 | /// - Else, if byte & 240u8 == 224u8, Unicode 3-byte 62 | /// - Else, if byte & 248u8 == 240u8, Unicode 4-byte 63 | /// - Else: 64 | /// - If byte == \r || byte == \n, 65 | /// - If byte == \t, 66 | /// - If byte == \x7f, 67 | /// - If byte == \x1b, 68 | /// - If byte == \x01, 69 | /// - If byte == \x05, 70 | /// - If byte == \x08, 71 | /// - Else, char = byte 72 | /// - Else, if no byte to read: 73 | /// - If stdin is a terminal, return None 74 | /// - Disable TCSADRAIN 75 | pub async fn next_keypress(state: &ConsoleState<'static>) -> Result> { 76 | let original_termios = termios::tcgetattr(state.0)?; 77 | let mut termios = original_termios.clone(); 78 | 79 | // Note: This is ONLY what termios::cfmakeraw does to input 80 | termios.input_flags &= !(InputFlags::IGNBRK 81 | | InputFlags::BRKINT 82 | | InputFlags::PARMRK 83 | | InputFlags::ISTRIP 84 | | InputFlags::INLCR 85 | | InputFlags::IGNCR 86 | | InputFlags::ICRNL 87 | | InputFlags::IXON); 88 | termios.local_flags &= !(termios::LocalFlags::ECHO 89 | | termios::LocalFlags::ECHONL 90 | | termios::LocalFlags::ICANON 91 | | termios::LocalFlags::ISIG 92 | | termios::LocalFlags::IEXTEN); 93 | termios::tcsetattr(state.0, termios::SetArg::TCSADRAIN, &termios)?; 94 | 95 | let out = read_next_key(&state.0).await; 96 | 97 | termios::tcsetattr(state.0, termios::SetArg::TCSADRAIN, &original_termios)?; 98 | 99 | out 100 | } 101 | 102 | #[async_recursion] 103 | async fn read_next_key(fd: &BorrowedFd<'_>) -> Result> { 104 | match read_char(fd)? { 105 | Some('\x1b') => match read_char(fd)? { 106 | Some('[') => match read_char(fd)? { 107 | Some('A') => Ok(Some(Keypress::Up)), 108 | Some('B') => Ok(Some(Keypress::Down)), 109 | Some('C') => Ok(Some(Keypress::Right)), 110 | Some('D') => Ok(Some(Keypress::Left)), 111 | Some('H') => Ok(Some(Keypress::Home)), 112 | Some('F') => Ok(Some(Keypress::End)), 113 | Some('Z') => Ok(Some(Keypress::ShiftTab)), 114 | Some(byte3) => match read_char(fd)? { 115 | Some('~') => match read_char(fd)? { 116 | Some('1') => Ok(Some(Keypress::Home)), 117 | Some('2') => Ok(Some(Keypress::Insert)), 118 | Some('3') => Ok(Some(Keypress::Delete)), 119 | Some('4') => Ok(Some(Keypress::End)), 120 | Some('5') => Ok(Some(Keypress::PageUp)), 121 | Some('6') => Ok(Some(Keypress::PageDown)), 122 | Some('7') => Ok(Some(Keypress::Home)), 123 | Some('8') => Ok(Some(Keypress::End)), 124 | Some(byte5) => Ok(Some(Keypress::UnknownSequence(vec![ 125 | '\x1b', '[', byte3, '~', byte5, 126 | ]))), 127 | None => Ok(Some(Keypress::UnknownSequence(vec![ 128 | '\x1b', '[', byte3, '~', 129 | ]))), 130 | }, 131 | Some(byte4) => Ok(Some(Keypress::UnknownSequence(vec![ 132 | '\x1b', '[', byte3, byte4, 133 | ]))), 134 | None => Ok(Some(Keypress::UnknownSequence(vec!['\x1b', '[', byte3]))), 135 | }, 136 | None => Ok(Some(Keypress::Escape)), 137 | }, 138 | Some(byte) => Ok(Some(Keypress::UnknownSequence(vec!['\x1b', byte]))), 139 | None => Ok(Some(Keypress::Escape)), 140 | }, 141 | Some('\r') | Some('\n') => Ok(Some(Keypress::Return)), 142 | Some('\t') => Ok(Some(Keypress::Tab)), 143 | Some('\x7f') => Ok(Some(Keypress::Backspace)), 144 | Some('\x01') => Ok(Some(Keypress::Home)), 145 | // ^C 146 | Some('\x03') => Err(ConsoleError::Interrupted.into()), 147 | Some('\x05') => Ok(Some(Keypress::End)), 148 | Some('\x08') => Ok(Some(Keypress::Backspace)), 149 | Some(byte) => { 150 | if (byte as u8) & 224u8 == 192u8 { 151 | let bytes = vec![byte as u8, read_byte(fd)?.unwrap()]; 152 | Ok(Some(Keypress::Char(char_from_utf8(&bytes)?))) 153 | } else if (byte as u8) & 240u8 == 224u8 { 154 | let bytes: Vec = 155 | vec![byte as u8, read_byte(fd)?.unwrap(), read_byte(fd)?.unwrap()]; 156 | Ok(Some(Keypress::Char(char_from_utf8(&bytes)?))) 157 | } else if (byte as u8) & 248u8 == 240u8 { 158 | let bytes: Vec = vec![ 159 | byte as u8, 160 | read_byte(fd)?.unwrap(), 161 | read_byte(fd)?.unwrap(), 162 | read_byte(fd)?.unwrap(), 163 | ]; 164 | Ok(Some(Keypress::Char(char_from_utf8(&bytes)?))) 165 | } else { 166 | Ok(Some(Keypress::Char(byte))) 167 | } 168 | } 169 | None => { 170 | // there is no subsequent byte ready to be read, block and wait for input 171 | let pollfd = PollFd::new(&fd, PollFlags::POLLIN); 172 | let ret = poll(&mut [pollfd], 0)?; 173 | 174 | if ret < 0 { 175 | let last_error = std::io::Error::last_os_error(); 176 | if last_error.kind() == std::io::ErrorKind::Interrupted { 177 | // User probably hit ^C, oops 178 | return Err(ConsoleError::Interrupted.into()); 179 | } else { 180 | return Err(ConsoleError::Io(last_error).into()); 181 | } 182 | } 183 | 184 | Ok(None) 185 | } 186 | } 187 | } 188 | 189 | fn read_byte(fd: &BorrowedFd<'_>) -> Result> { 190 | let mut buf = [0u8; 1]; 191 | let mut read_fds = FdSet::new(); 192 | read_fds.insert(fd); 193 | 194 | let mut signals = SigSet::empty(); 195 | signals.add(Signal::SIGINT); 196 | signals.add(Signal::SIGTERM); 197 | signals.add(Signal::SIGKILL); 198 | 199 | match nix::sys::select::pselect( 200 | fd.as_raw_fd() + 1, 201 | Some(&mut read_fds), 202 | Some(&mut FdSet::new()), 203 | Some(&mut FdSet::new()), 204 | Some(&TimeSpec::new( 205 | 0, 206 | Duration::from_millis(50).as_nanos() as i64, 207 | )), 208 | Some(&signals), 209 | ) { 210 | Ok(0) => Ok(None), 211 | Ok(_) => match nix::unistd::read(fd.as_raw_fd(), &mut buf) { 212 | Ok(0) => Ok(None), 213 | Ok(_) => Ok(Some(buf[0])), 214 | Err(err) => Err(err.into()), 215 | }, 216 | Err(err) => Err(err.into()), 217 | } 218 | } 219 | 220 | fn read_char(fd: &BorrowedFd<'_>) -> Result> { 221 | read_byte(fd).map(|byte| byte.map(|byte| byte as char)) 222 | } 223 | 224 | fn char_from_utf8(buf: &[u8]) -> Result { 225 | let str = std::str::from_utf8(buf)?; 226 | let ch = str.chars().next(); 227 | match ch { 228 | Some(c) => Ok(c), 229 | None => Err(eyre!("invalid utf8 sequence: {:?}", buf)), 230 | } 231 | } 232 | 233 | #[derive(Debug, Clone, PartialEq, Eq)] 234 | pub enum Keypress { 235 | Up, 236 | Down, 237 | Right, 238 | Left, 239 | Home, 240 | End, 241 | ShiftTab, 242 | Insert, 243 | Delete, 244 | PageUp, 245 | PageDown, 246 | Return, 247 | Tab, 248 | Backspace, 249 | Escape, 250 | Char(char), 251 | UnknownSequence(Vec), 252 | } 253 | 254 | #[derive(thiserror::Error, Debug)] 255 | pub enum ConsoleError { 256 | #[error("Interrupted!")] 257 | Interrupted, 258 | #[error("IO error: {0}")] 259 | Io(#[from] std::io::Error), 260 | } 261 | -------------------------------------------------------------------------------- /makeup-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "makeup-macros" 3 | version = "0.0.2" 4 | edition = "2021" 5 | description = "Macro helper library for makeup" 6 | repository = "https://github.com/queer/makeup" 7 | license = "MIT" 8 | readme = "../README.md" 9 | keywords = ["cli"] 10 | categories = ["development-tools"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | proc-macro2 = "1.0.86" 16 | quote = "1.0.34" 17 | syn = { version = "2.0.72", features = ["full"] } 18 | 19 | [lib] 20 | proc-macro = true 21 | -------------------------------------------------------------------------------- /makeup-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use quote::quote; 2 | 3 | #[doc(hidden)] 4 | #[proc_macro] 5 | pub fn __do_check_mail_arms(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 6 | let input = syn::parse::(input).expect("Failed to parse input"); 7 | 8 | let mut left_patterns = Vec::new(); 9 | let mut right_patterns = Vec::new(); 10 | let mut left_arms = Vec::new(); 11 | let mut right_arms = Vec::new(); 12 | 13 | if let syn::Expr::Group(group) = input { 14 | if let syn::Expr::Match(expr_match) = group.expr.as_ref() { 15 | for arm in expr_match.arms.iter() { 16 | let pattern = arm.pat.clone(); 17 | let handler = arm.body.clone(); 18 | 19 | // If pattern is an ident starting with MakeupMessage::, put it in the right arm. 20 | // Otherwise, put it in the left arm. 21 | 22 | let is_makeup_message = match &pattern { 23 | syn::Pat::TupleStruct(ref pat_tuple_struct) => pat_tuple_struct.path.segments 24 | [0] 25 | .ident 26 | .to_string() 27 | .starts_with("MakeupMessage"), 28 | _ => false, 29 | }; 30 | 31 | if is_makeup_message { 32 | right_patterns.push(pattern.clone()); 33 | right_arms.push(handler); 34 | } else { 35 | left_patterns.push(pattern.clone()); 36 | left_arms.push(handler); 37 | } 38 | } 39 | } 40 | } 41 | let output = quote! { 42 | match message { 43 | #(Either::Left(#left_patterns) => #left_arms)* 44 | #(Either::Right(#right_patterns) => #right_arms)* 45 | _ => {} 46 | } 47 | }; 48 | 49 | output.into() 50 | } 51 | -------------------------------------------------------------------------------- /makeup/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /makeup/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "makeup" 3 | description = "Stylish CLIs/TUIs for Rust!" 4 | repository = "https://github.com/queer/makeup" 5 | license = "MIT" 6 | version = "0.0.8" 7 | edition = "2021" 8 | readme = "../README.md" 9 | categories = ["command-line-interface", "rendering", "rendering::engine"] 10 | keywords = ["cli", "tui", "terminal", "color", "60fps"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | async-recursion = "1.0.5" 16 | async-trait = "0.1.81" 17 | either = "1.13.0" 18 | eyre = "0.6.12" 19 | futures-util = "0.3.30" 20 | rand = "0.8.5" 21 | thiserror = "1.0.63" 22 | tokio = { version = "1.39.1", features = ["full", "tracing"] } 23 | libc = "0.2.155" 24 | indoc = "2.0.4" 25 | strum = { version = "0.26.1", features = ["derive"] } 26 | taffy = "0.3.18" 27 | derivative = "2.2.0" 28 | 29 | [dev-dependencies] 30 | colorgrad = "0.6.2" 31 | 32 | [dependencies.makeup-ansi] 33 | path = "../makeup-ansi" 34 | version = "0.0.3" 35 | 36 | [dependencies.makeup-console] 37 | path = "../makeup-console" 38 | version = "0.0.7" 39 | 40 | [dependencies.makeup-macros] 41 | path = "../makeup-macros" 42 | version = "0.0.2" 43 | -------------------------------------------------------------------------------- /makeup/examples/container.rs: -------------------------------------------------------------------------------- 1 | use makeup::components::{Container, EchoText}; 2 | use makeup::input::TerminalInput; 3 | use makeup::render::terminal::TerminalRenderer; 4 | use makeup::MUI; 5 | 6 | use eyre::Result; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let mut root = Container::new_with_style( 11 | vec![ 12 | Box::new(EchoText::new("hello,")), 13 | Box::new(EchoText::new("world!")), 14 | ], 15 | Some(taffy::style::Style { 16 | flex_direction: taffy::style::FlexDirection::Column, 17 | ..Default::default() 18 | }), 19 | ); 20 | let renderer = TerminalRenderer::new(); 21 | let input = TerminalInput::new().await?; 22 | let mui = MUI::<()>::new(&mut root, Box::new(renderer), input)?; 23 | mui.render_once().await?; 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /makeup/examples/container_input.rs: -------------------------------------------------------------------------------- 1 | use makeup::components::{Container, EchoText, TextInput}; 2 | use makeup::input::TerminalInput; 3 | use makeup::render::terminal::TerminalRenderer; 4 | use makeup::MUI; 5 | 6 | use eyre::Result; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let mut root = Container::new_with_style( 11 | vec![ 12 | Box::new(EchoText::new("hello,")), 13 | Box::new(EchoText::new("world!")), 14 | Box::new(TextInput::new("type here")), 15 | ], 16 | Some(taffy::style::Style { 17 | flex_direction: taffy::style::FlexDirection::Column, 18 | ..Default::default() 19 | }), 20 | ); 21 | let renderer = TerminalRenderer::new(); 22 | let input = TerminalInput::new().await?; 23 | let mui = MUI::<()>::new(&mut root, Box::new(renderer), input)?; 24 | mui.render(false).await?; 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /makeup/examples/external_message.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::time::Duration; 3 | 4 | use makeup::components::Fps; 5 | use makeup::input::TerminalInput; 6 | use makeup::render::terminal::TerminalRenderer; 7 | use makeup::MUI; 8 | 9 | use eyre::Result; 10 | use makeup::ui::{RenderState, UiControlMessage}; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let mut root = Fps::new(); 15 | let renderer = TerminalRenderer::new(); 16 | let input = TerminalInput::new().await?; 17 | let mui = Arc::new(MUI::<()>::new(&mut root, Box::new(renderer), input)?); 18 | let stop_mui = mui.clone(); 19 | 20 | 'outer: loop { 21 | // tokio::select! over the mui.render() future and the time::sleep future 22 | tokio::select! { 23 | _ = tokio::time::sleep(Duration::from_secs(1)) => { 24 | stop_mui.send_control(UiControlMessage::StopRendering).await; 25 | } 26 | res = mui.render(true) => { 27 | match res { 28 | Ok(RenderState::Stopped) => { 29 | break 'outer; 30 | } 31 | Ok(_) => {} 32 | Err(e) => { 33 | eprintln!("Error: {e}"); 34 | break 'outer; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /makeup/examples/fps.rs: -------------------------------------------------------------------------------- 1 | use makeup::components::Fps; 2 | use makeup::input::TerminalInput; 3 | use makeup::render::terminal::TerminalRenderer; 4 | use makeup::MUI; 5 | 6 | use eyre::Result; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let mut root = Fps::new(); 11 | let renderer = TerminalRenderer::new(); 12 | let input = TerminalInput::new().await?; 13 | let mui = MUI::<()>::new(&mut root, Box::new(renderer), input)?; 14 | mui.render(true).await?; 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /makeup/examples/hello.rs: -------------------------------------------------------------------------------- 1 | use makeup::components::EchoText; 2 | use makeup::input::TerminalInput; 3 | use makeup::render::terminal::TerminalRenderer; 4 | use makeup::MUI; 5 | 6 | use eyre::Result; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let mut root = EchoText::new("hello, world!"); 11 | let renderer = TerminalRenderer::new(); 12 | let input = TerminalInput::new().await?; 13 | let mui = MUI::<()>::new(&mut root, Box::new(renderer), input)?; 14 | mui.render_once().await?; 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /makeup/examples/input.rs: -------------------------------------------------------------------------------- 1 | use makeup::components::TextInput; 2 | use makeup::input::TerminalInput; 3 | use makeup::render::terminal::TerminalRenderer; 4 | use makeup::MUI; 5 | 6 | use eyre::Result; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | let mut root = TextInput::new("Type some text here"); 11 | let renderer = TerminalRenderer::new(); 12 | let input = TerminalInput::new().await?; 13 | let mui = MUI::<()>::new(&mut root, Box::new(renderer), input)?; 14 | mui.render(false).await?; 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /makeup/examples/spinner.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use makeup::components::Spinner; 4 | use makeup::input::TerminalInput; 5 | use makeup::render::terminal::TerminalRenderer; 6 | use makeup::MUI; 7 | 8 | use eyre::Result; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | let mut root = Spinner::<()>::new( 13 | "hello, world!", 14 | vec!['-', '\\', '|', '/'], 15 | Duration::from_millis(100), 16 | ); 17 | let renderer = TerminalRenderer::new(); 18 | let input = TerminalInput::new().await?; 19 | let mui = MUI::<()>::new(&mut root, Box::new(renderer), input)?; 20 | mui.render(false).await?; 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /makeup/examples/wave.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use async_trait::async_trait; 4 | use colorgrad::Gradient; 5 | use makeup::component::{DrawCommandBatch, Key, MakeupMessage, MakeupUpdate, RenderContext}; 6 | use makeup::input::TerminalInput; 7 | use makeup::render::terminal::TerminalRenderer; 8 | use makeup::{ 9 | check_mail, Ansi, Component, Dimensions, DrawCommand, LineEraseMode, SgrParameter, MUI, 10 | }; 11 | 12 | use eyre::Result; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<()> { 16 | let gradient = colorgrad::CustomGradient::new() 17 | .html_colors(&[ 18 | "#FFB3BA", "#FFDFBA", "#FFFFBA", "#BAFFC9", "#BAE1FF", "#D0BAFF", "#FFBAF2", "#FFB3BA", 19 | ]) 20 | .build()?; 21 | let mut root = Wave::new(gradient); 22 | let renderer = TerminalRenderer::new(); 23 | let input = TerminalInput::new().await?; 24 | let mui = MUI::new(&mut root, Box::new(renderer), input)?; 25 | mui.render(false).await?; 26 | 27 | Ok(()) 28 | } 29 | 30 | const DURATION: Duration = Duration::from_millis(16); 31 | 32 | #[derive(Debug)] 33 | struct Wave { 34 | key: Key, 35 | gradient: Gradient, 36 | step: u64, 37 | started: bool, 38 | } 39 | 40 | impl Wave { 41 | fn new(gradient: Gradient) -> Wave { 42 | Wave { 43 | key: makeup::component::generate_key(), 44 | gradient, 45 | step: 0, 46 | started: false, 47 | } 48 | } 49 | } 50 | 51 | #[async_trait] 52 | impl Component for Wave { 53 | type Message = (); 54 | 55 | fn children(&self) -> Option>>> { 56 | None 57 | } 58 | 59 | fn children_mut(&mut self) -> Option>>> { 60 | None 61 | } 62 | 63 | async fn update(&mut self, ctx: &mut MakeupUpdate) -> Result<()> { 64 | let sender = ctx.sender.clone(); 65 | if !self.started { 66 | self.started = true; 67 | sender.send_makeup_message(self.key(), MakeupMessage::TimerTick(DURATION))?; 68 | } 69 | 70 | check_mail!( 71 | self, 72 | ctx, 73 | match _ { 74 | MakeupMessage::TimerTick(_) => { 75 | self.step += 1; 76 | ctx.sender.send_makeup_message_after( 77 | self.key(), 78 | MakeupMessage::TimerTick(DURATION), 79 | DURATION, 80 | )?; 81 | } 82 | } 83 | ); 84 | 85 | Ok(()) 86 | } 87 | 88 | async fn render(&self, ctx: &RenderContext) -> Result { 89 | let mut commands = vec![]; 90 | 91 | commands.push(DrawCommand::HideCursor); 92 | 93 | let mut colours = self.gradient.colors(ctx.dimensions.1 as usize - 1); 94 | let step = self.step % colours.len() as u64; 95 | colours.rotate_right(step as usize); 96 | 97 | let mut output = String::new(); 98 | for colour in colours.iter() { 99 | let [r, g, b, _] = colour.to_rgba8(); 100 | let r = r as u32; 101 | let g = g as u32; 102 | let b = b as u32; 103 | output += format!( 104 | "{}{}\n", 105 | Ansi::Sgr(vec![SgrParameter::HexForegroundColour( 106 | r << 16 | g << 8 | b 107 | )]), 108 | "█".repeat(ctx.dimensions.0 as usize) 109 | ) 110 | .as_str(); 111 | } 112 | 113 | commands.push(DrawCommand::TextUnderCursor(output)); 114 | commands.push(DrawCommand::EraseCurrentLine( 115 | LineEraseMode::FromCursorToEnd, 116 | )); 117 | commands.push(DrawCommand::TextUnderCursor(format!( 118 | "{:.2}fps ({:.2}fps effective), dimensions {:?}, step {:?} frame {:?}", 119 | ctx.fps, ctx.effective_fps, ctx.dimensions, self.step, ctx.frame_counter, 120 | ))); 121 | 122 | commands.push(DrawCommand::ShowCursor); 123 | 124 | self.batch(commands) 125 | } 126 | 127 | fn key(&self) -> Key { 128 | self.key 129 | } 130 | 131 | fn dimensions(&self) -> Result> { 132 | Ok(None) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /makeup/src/component.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use async_trait::async_trait; 4 | use either::Either; 5 | use eyre::Result; 6 | use makeup_console::Keypress; 7 | use tokio::sync::mpsc::UnboundedSender; 8 | 9 | use crate::post_office::PostOffice; 10 | use crate::{Coordinates, Dimensions, DrawCommand}; 11 | 12 | /// A key that uniquely identifies a [`Component`]. 13 | pub type Key = u64; 14 | 15 | /// A [`Key`]ed batch of [`DrawCommand`]s. 16 | pub type DrawCommandBatch = (Key, Vec); 17 | 18 | /// The exact message type that can be sent to a component. Either the 19 | /// component's associated `Message` type, or a [`MakeupMessage`]. 20 | pub type RawComponentMessage = Either; 21 | 22 | /// The associated `Message` type of a [`Component`]. 23 | pub type ExtractMessageFromComponent = ::Message; 24 | 25 | /// The type of messages that can be sent to the given [`Component`]. 26 | pub type ComponentMessage = RawComponentMessage>; 27 | 28 | /// A mailbox for a component. 29 | pub type Mailbox = Vec>; 30 | 31 | /// An [`UnboundedSender`] that can be used to send messages to a component 32 | /// during updates. 33 | pub type ContextTx = UnboundedSender<(Key, RawComponentMessage)>; 34 | 35 | pub type MakeupUpdate<'a, C> = UpdateContext<'a, ExtractMessageFromComponent>; 36 | 37 | /// The context for a component's update lifecycle. 38 | #[derive(Debug)] 39 | pub struct UpdateContext<'a, M: std::fmt::Debug + Send + Sync + Clone + 'static> { 40 | /// The [`PostOffice`] used for receiving messages. 41 | pub post_office: &'a mut PostOffice, 42 | /// Used for sending messages. 43 | pub sender: MessageSender, 44 | /// The [`Key`] of the currently-focused component. 45 | pub focus: Key, 46 | /// The dimensions of the character grid. 47 | pub dimensions: Dimensions, 48 | } 49 | 50 | impl<'a, M: std::fmt::Debug + Send + Sync + Clone + 'static> UpdateContext<'a, M> { 51 | pub fn new( 52 | post_office: &'a mut PostOffice, 53 | sender: ContextTx, 54 | focus: Key, 55 | dimensions: Dimensions, 56 | ) -> Self { 57 | Self { 58 | post_office, 59 | sender: MessageSender::new(sender, focus), 60 | focus, 61 | dimensions, 62 | } 63 | } 64 | 65 | pub fn sender(&self) -> MessageSender { 66 | self.sender.clone() 67 | } 68 | } 69 | 70 | // TODO: Figure out update propagation so that containers recalculate layout when children change 71 | /// A helper for components to use for message-sending during the update loop. 72 | /// These functions are not on the [`UpdateContext`] itself because the 73 | /// `sender` needs to be able to be moved across threads with a `'static` 74 | /// lifetime, and that's achieved by repeatedly cloning the `sender`. 75 | #[derive(Debug, Clone)] 76 | pub struct MessageSender { 77 | focus: Key, 78 | tx: ContextTx, 79 | } 80 | 81 | impl MessageSender { 82 | pub fn new(tx: ContextTx, focus: Key) -> Self { 83 | Self { tx, focus } 84 | } 85 | 86 | /// Send a message to the given component. 87 | pub fn send_message(&self, key: Key, msg: M) -> Result<()> { 88 | let sender = self.tx.clone(); 89 | tokio::spawn(async move { 90 | sender.send((key, Either::Left(msg))).unwrap(); 91 | }); 92 | Ok(()) 93 | } 94 | 95 | /// Send a [`MakeupMessage`] to the given component. 96 | pub fn send_makeup_message(&self, key: Key, msg: MakeupMessage) -> Result<()> { 97 | let sender = self.tx.clone(); 98 | tokio::spawn(async move { 99 | sender.send((key, Either::Right(msg))).unwrap(); 100 | }); 101 | Ok(()) 102 | } 103 | 104 | /// Send a message to given component after waiting for the given duration. 105 | pub fn send_message_after(&self, key: Key, msg: M, duration: Duration) -> Result<()> { 106 | let sender = self.tx.clone(); 107 | tokio::spawn(async move { 108 | tokio::time::sleep(duration).await; 109 | sender.send((key, Either::Left(msg))).unwrap(); 110 | }); 111 | Ok(()) 112 | } 113 | 114 | /// Send a [`MakeupMessage`] to the given component after waiting for the 115 | /// given duration. 116 | pub fn send_makeup_message_after( 117 | &self, 118 | key: Key, 119 | msg: MakeupMessage, 120 | duration: Duration, 121 | ) -> Result<()> { 122 | let sender = self.tx.clone(); 123 | tokio::spawn(async move { 124 | tokio::time::sleep(duration).await; 125 | sender.send((key, Either::Right(msg))).unwrap(); 126 | }); 127 | Ok(()) 128 | } 129 | 130 | /// Send a message to the currently-focused component. 131 | pub fn send_message_to_focused(&self, msg: M) -> Result<()> { 132 | self.send_message(self.focus, msg) 133 | } 134 | 135 | /// Send a [`MakeupMessage`] to the currently-focused component. 136 | pub fn send_makeup_message_to_focused(&self, msg: MakeupMessage) -> Result<()> { 137 | self.send_makeup_message(self.focus, msg) 138 | } 139 | 140 | /// Send a message to the currently-focused component after waiting for the 141 | /// given duration. 142 | pub fn send_message_to_focused_after(&self, msg: M, duration: Duration) -> Result<()> { 143 | self.send_message_after(self.focus, msg, duration) 144 | } 145 | 146 | /// Send a [`MakeupMessage`] to the currently-focused component after 147 | /// waiting for the given duration. 148 | pub fn send_makeup_message_to_focused_after( 149 | &self, 150 | msg: MakeupMessage, 151 | duration: Duration, 152 | ) -> Result<()> { 153 | self.send_makeup_message_after(self.focus, msg, duration) 154 | } 155 | } 156 | 157 | #[derive(Debug, Clone)] 158 | pub struct RenderContext { 159 | /// How long the previous frame took to render. May not be present. 160 | pub last_frame_time: Option, 161 | /// The number of the current frame. Will only ever increase. 162 | pub frame_counter: u128, 163 | /// The last FPS value. 164 | pub fps: f64, 165 | /// The last effective FPS value. Maybe be larger than `fps`, sometimes 166 | /// significantly so. 167 | pub effective_fps: f64, 168 | /// The coordinates of the cursor in the character grid. 169 | pub cursor: Coordinates, 170 | /// The dimensions of the character grid. 171 | pub dimensions: Dimensions, 172 | /// The [`Key`] of the currently-focused component. 173 | pub focus: Key, 174 | } 175 | 176 | /// A default message that can be sent to a component. Contains a lot of the 177 | /// built-in functionality you would expect: 178 | /// - Timer ticks 179 | /// - Text updates 180 | #[derive(Debug, Clone)] 181 | pub enum MakeupMessage { 182 | TimerTick(Duration), 183 | TextUpdate(String), 184 | Keypress(Keypress), 185 | } 186 | 187 | /// A component in a makeup UI. 188 | /// 189 | /// Component layout is done via flexbox. This means that container-like 190 | /// components CANNOT render text directly on themselves, but must instead have 191 | /// child components for all rendering. 192 | #[async_trait] 193 | pub trait Component: std::fmt::Debug + Send + Sync { 194 | /// The type of messages that can be sent to this component. 195 | type Message: std::fmt::Debug + Send + Sync + Clone; 196 | 197 | /// The children this component has. May be empty when present. 198 | /// 199 | /// **NOTE:** This *intentionally* returns a borrowed box! 200 | #[allow(clippy::borrowed_box)] 201 | fn children(&self) -> Option>>>; 202 | 203 | /// The children this component has, but mutable. May be empty when present. 204 | fn children_mut(&mut self) -> Option>>>; 205 | 206 | /// Process any messages that have been sent to this component. Messages 207 | /// are expected to be process asynchronously, ie. any long-running 208 | /// operations should be [`tokio::spawn`]ed as a task. 209 | async fn update(&mut self, ctx: &mut MakeupUpdate) -> Result<()>; 210 | 211 | /// Render this component. 212 | async fn render(&self, ctx: &RenderContext) -> Result; 213 | 214 | /// A unique key for this component. See [`generate_key`]. 215 | fn key(&self) -> Key; 216 | 217 | /// Batch the given render commands with this component's key. 218 | fn batch(&self, commands: Vec) -> Result { 219 | Ok((self.key(), commands)) 220 | } 221 | 222 | /// The dimensions of this component. Coordinates are calculated 223 | /// automatically by the parent component that manages layout, or are 224 | /// implied by render order. 225 | fn dimensions(&self) -> Result>; 226 | 227 | fn style(&self) -> Option { 228 | None 229 | } 230 | 231 | /// Whether or not this component accepts focus. This is intended to help 232 | /// differentiate between ex. text inputs and labels. 233 | fn accepts_focus(&self) -> bool { 234 | false 235 | } 236 | } 237 | 238 | /// Generate a most-likely-unique key for a component. 239 | pub fn generate_key() -> Key { 240 | rand::random::() 241 | } 242 | -------------------------------------------------------------------------------- /makeup/src/components/container.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use derivative::Derivative; 3 | use eyre::Result; 4 | use taffy::style::Style; 5 | 6 | use crate::component::{DrawCommandBatch, Key, MakeupUpdate, RenderContext}; 7 | use crate::{Component, Dimensions}; 8 | 9 | #[derive(Derivative)] 10 | #[derivative(Debug)] 11 | pub struct Container { 12 | children: Vec>>, 13 | key: Key, 14 | updating: bool, 15 | style: Option