├── .editorconfig ├── .envrc ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── demo │ ├── input.tera │ ├── justfile │ └── output │ │ ├── frappe.md │ │ ├── latte.md │ │ ├── macchiato.md │ │ └── mocha.md ├── frontmatter │ ├── input.tera │ ├── justfile │ └── output │ │ ├── frappe.md │ │ ├── latte.md │ │ ├── macchiato.md │ │ └── mocha.md ├── multi-output │ ├── single-accent │ │ ├── example.tera │ │ ├── output-frappe-red.txt │ │ ├── output-latte-red.txt │ │ ├── output-macchiato-red.txt │ │ └── output-mocha-red.txt │ └── single-flavor │ │ ├── example.tera │ │ ├── output-a.txt │ │ └── output-b.txt └── single-file │ ├── overrides │ ├── input.tera │ ├── justfile │ └── output.md │ └── simple │ ├── input.tera │ ├── justfile │ └── output.md ├── release-please-config.json ├── renovate.json ├── src ├── cli.rs ├── context.rs ├── filters.rs ├── frontmatter.rs ├── functions.rs ├── lib.rs ├── main.rs ├── markdown.rs ├── matrix.rs ├── models.rs └── templating.rs └── tests ├── cli.rs ├── encodings.rs └── fixtures ├── encodings ├── README.md ├── utf16be.tera ├── utf16le.tera ├── utf8.tera └── utf8bom.tera ├── errors.tera ├── formats.tera ├── hexformat ├── custom.tera ├── custom.txt ├── default.tera └── default.txt ├── multi ├── multi.md └── multi.tera ├── multifile.tera ├── read_file ├── abc.txt ├── read_file.md └── read_file.tera ├── single ├── single.md └── single.tera ├── singlefile-multiflavor.tera └── singlefile-singleflavor.tera /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # EditorConfig is awesome: https://EditorConfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_size = 2 10 | indent_style = space 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # go 16 | [*.go] 17 | indent_style = tab 18 | indent_size = 4 19 | 20 | # python 21 | [*.{ini,py,py.tpl,rst}] 22 | indent_size = 4 23 | 24 | # rust 25 | [*.rs] 26 | indent_size = 4 27 | 28 | # documentation, utils 29 | [*.{md,mdx,diff}] 30 | trim_trailing_whitespace = false 31 | 32 | # windows shell scripts 33 | [*.{cmd,bat,ps1}] 34 | end_of_line = crlf 35 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if has nix_direnv_version; then 2 | use flake 3 | fi 4 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | paths: ["**.rs", "**.toml", "**.lock"] 6 | push: 7 | branches: [main] 8 | paths: ["**.rs", "**.toml", "**.lock"] 9 | 10 | jobs: 11 | rust: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions-rust-lang/setup-rust-toolchain@v1 16 | - uses: actions/cache@v4 17 | with: 18 | path: | 19 | ~/.cargo/registry 20 | ~/.cargo/git 21 | target 22 | key: ${{ runner.os }}-cargo-${{ hashFiles('./Cargo.lock') }}-test 23 | 24 | - name: clippy 25 | run: cargo clippy 26 | 27 | - name: rustfmt check 28 | run: cargo fmt --all --check 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: googleapis/release-please-action@v4 17 | id: release 18 | outputs: 19 | release_created: ${{ steps.release.outputs.release_created }} 20 | tag_name: ${{ steps.release.outputs.tag_name }} 21 | 22 | release: 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | defaults: 31 | run: 32 | shell: bash 33 | 34 | env: 35 | EXECUTABLE: "whiskers" 36 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 37 | EXE_SUFFIX: ${{ matrix.os == 'windows-latest' && '.exe' || '' }} 38 | 39 | needs: release-please 40 | if: ${{ needs.release-please.outputs.release_created }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions-rust-lang/setup-rust-toolchain@v1 44 | - uses: actions/cache@v4 45 | with: 46 | path: | 47 | ~/.cargo/registry 48 | ~/.cargo/git 49 | target 50 | key: ${{ runner.os }}-cargo-${{ hashFiles('./Cargo.lock') }}-release 51 | 52 | - name: Build 53 | id: build 54 | run: | 55 | cargo build --release --locked 56 | cargo test --release --locked 57 | 58 | export BINARY_NAME="${EXECUTABLE}-$(rustc --version --verbose | grep host | cut -d ' ' -f 2)${EXE_SUFFIX}" 59 | mv "target/release/${EXECUTABLE}${EXE_SUFFIX}" "./target/${BINARY_NAME}" 60 | echo "binary=target/${BINARY_NAME}" >> $GITHUB_OUTPUT 61 | 62 | - name: Publish to crates.io 63 | if: ${{ matrix.os == 'ubuntu-latest' }} 64 | env: 65 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 66 | run: cargo publish 67 | 68 | - name: Upload to release 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | run: gh release upload "${{ needs.release-please.outputs.tag_name }}" ${{ steps.build.outputs.binary }} 72 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | paths: ["**.rs", "**.toml", "**.lock"] 6 | push: 7 | branches: [main] 8 | paths: ["**.rs", "**.toml", "**.lock"] 9 | 10 | jobs: 11 | rust: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions-rust-lang/setup-rust-toolchain@v1 19 | - uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: ${{ runner.os }}-cargo-${{ hashFiles('./Cargo.lock') }}-test 26 | 27 | - name: cargo test 28 | run: cargo test --all-features 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .direnv/ 3 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.5.1" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.5.1](https://github.com/catppuccin/whiskers/compare/v2.5.0...v2.5.1) (2024-10-12) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **check:** wait until all diffs are shown before exiting ([#53](https://github.com/catppuccin/whiskers/issues/53)) ([66d6ffe](https://github.com/catppuccin/whiskers/commit/66d6ffe392eddef1f16634e33517cddff198aff1)) 9 | * **deps:** update rust crate anyhow to v1.0.89 ([#46](https://github.com/catppuccin/whiskers/issues/46)) ([b0e9c20](https://github.com/catppuccin/whiskers/commit/b0e9c20105d492f4d4c1d544a58adb322b1542fe)) 10 | * **deps:** update rust crate clap to v4.5.20 ([#47](https://github.com/catppuccin/whiskers/issues/47)) ([e80e369](https://github.com/catppuccin/whiskers/commit/e80e36937c2a462a8cfb7bda1c47efa1b378111d)) 11 | * **deps:** update rust crate indexmap to v2.6.0 ([#50](https://github.com/catppuccin/whiskers/issues/50)) ([c579f55](https://github.com/catppuccin/whiskers/commit/c579f55399cc7ac50feaf37908a53c41bbda19d8)) 12 | * **deps:** update rust crate serde to v1.0.210 ([#48](https://github.com/catppuccin/whiskers/issues/48)) ([3ddbf6e](https://github.com/catppuccin/whiskers/commit/3ddbf6e1173f39ce3056996e1a4459e21cdb575f)) 13 | * **deps:** update rust crate serde_json to v1.0.128 ([#49](https://github.com/catppuccin/whiskers/issues/49)) ([00de490](https://github.com/catppuccin/whiskers/commit/00de49069d1e1a4d39688221bf98449bd266e014)) 14 | * **deps:** update rust crate tempfile to v3.13.0 ([#52](https://github.com/catppuccin/whiskers/issues/52)) ([ac953a3](https://github.com/catppuccin/whiskers/commit/ac953a3a39fbb9386f3dfcfd9ad1356ba473affc)) 15 | * **deps:** update rust crate thiserror to v1.0.64 ([#51](https://github.com/catppuccin/whiskers/issues/51)) ([f96deac](https://github.com/catppuccin/whiskers/commit/f96deac4962393aeeabdb36ee5a4abd39b714d9c)) 16 | 17 | ## [2.5.0](https://github.com/catppuccin/whiskers/compare/v2.4.0...v2.5.0) (2024-09-08) 18 | 19 | 20 | ### Features 21 | 22 | * add signed & unsigned integer colour repr ([#27](https://github.com/catppuccin/whiskers/issues/27)) ([6d2f354](https://github.com/catppuccin/whiskers/commit/6d2f354c6665578c37231c2d8689aa0c7059bcce)) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update rust crate clap to v4.5.16 ([#35](https://github.com/catppuccin/whiskers/issues/35)) ([088fc13](https://github.com/catppuccin/whiskers/commit/088fc13867612b7477ec7ff55f69390487927cf5)) 28 | * **deps:** update rust crate clap to v4.5.8 ([#24](https://github.com/catppuccin/whiskers/issues/24)) ([aa47392](https://github.com/catppuccin/whiskers/commit/aa47392358650675e3206b1cb14417f8bf19a81a)) 29 | * **deps:** update rust crate clap to v4.5.9 ([#30](https://github.com/catppuccin/whiskers/issues/30)) ([b138161](https://github.com/catppuccin/whiskers/commit/b1381614ec574db02141d062f6ff230438a290a0)) 30 | * **deps:** update rust crate clap-stdin to 0.5.0 ([#28](https://github.com/catppuccin/whiskers/issues/28)) ([7e123b7](https://github.com/catppuccin/whiskers/commit/7e123b78a2cf0f88962fc14401d156488db1f0ed)) 31 | * **deps:** update rust crate indexmap to v2.3.0 ([#39](https://github.com/catppuccin/whiskers/issues/39)) ([43d31ff](https://github.com/catppuccin/whiskers/commit/43d31ff336f9e9e02a97cc6d47a725098cfa82fe)) 32 | * **deps:** update rust crate indexmap to v2.4.0 ([#44](https://github.com/catppuccin/whiskers/issues/44)) ([09d6d4a](https://github.com/catppuccin/whiskers/commit/09d6d4a32d1020a1f70f679c2acea776817af5bf)) 33 | * **deps:** update rust crate lzma-rust to v0.1.7 ([#33](https://github.com/catppuccin/whiskers/issues/33)) ([644bfbf](https://github.com/catppuccin/whiskers/commit/644bfbf765353817af232047eb92e8b2d4ba9569)) 34 | * **deps:** update rust crate serde to v1.0.204 ([#29](https://github.com/catppuccin/whiskers/issues/29)) ([f81f0b2](https://github.com/catppuccin/whiskers/commit/f81f0b20defcb85de241d6596d8ef62590c1535f)) 35 | * **deps:** update rust crate serde to v1.0.209 ([#41](https://github.com/catppuccin/whiskers/issues/41)) ([d0ca928](https://github.com/catppuccin/whiskers/commit/d0ca92874d84fec6b7fa321dc4df0fb160992187)) 36 | * **deps:** update rust crate serde_json to v1.0.118 ([#21](https://github.com/catppuccin/whiskers/issues/21)) ([9c493fb](https://github.com/catppuccin/whiskers/commit/9c493fb22fabbbd69b3e4c28faf8c132883f5872)) 37 | * **deps:** update rust crate serde_json to v1.0.119 ([#25](https://github.com/catppuccin/whiskers/issues/25)) ([a80a579](https://github.com/catppuccin/whiskers/commit/a80a579af755abbe329e83de898f44c1599f75a8)) 38 | * **deps:** update rust crate serde_json to v1.0.120 ([#26](https://github.com/catppuccin/whiskers/issues/26)) ([007eeae](https://github.com/catppuccin/whiskers/commit/007eeaedb7863ebb3b41c6e2c88446ae7d3f6179)) 39 | * **deps:** update rust crate serde_json to v1.0.122 ([#38](https://github.com/catppuccin/whiskers/issues/38)) ([89f9a8d](https://github.com/catppuccin/whiskers/commit/89f9a8d304a75520e166378d541f86b5e2143693)) 40 | * **deps:** update rust crate serde_json to v1.0.127 ([#43](https://github.com/catppuccin/whiskers/issues/43)) ([0d79de3](https://github.com/catppuccin/whiskers/commit/0d79de3b35a17b481fe8d4f5220386b51b7bd614)) 41 | * **deps:** update rust crate tempfile to v3.12.0 ([#40](https://github.com/catppuccin/whiskers/issues/40)) ([269e8ed](https://github.com/catppuccin/whiskers/commit/269e8ed8bed34531b39ebd8ab6dd50093f951987)) 42 | * **deps:** update rust crate thiserror to v1.0.62 ([#31](https://github.com/catppuccin/whiskers/issues/31)) ([c169d03](https://github.com/catppuccin/whiskers/commit/c169d03f308d831e54974c069fe6637d04c4a095)) 43 | * **deps:** update rust crate thiserror to v1.0.63 ([#34](https://github.com/catppuccin/whiskers/issues/34)) ([855169c](https://github.com/catppuccin/whiskers/commit/855169c3681f0b9f53faeb869a90b01c0987ba1c)) 44 | 45 | ## [2.4.0](https://github.com/catppuccin/whiskers/compare/v2.3.0...v2.4.0) (2024-06-14) 46 | 47 | 48 | ### Features 49 | 50 | * add custom hex formatting ([#18](https://github.com/catppuccin/whiskers/issues/18)) ([dfc700e](https://github.com/catppuccin/whiskers/commit/dfc700e749c3c57f31f7e0fa676a78c10bc0f6cb)) 51 | * nix flake ([e02f0d4](https://github.com/catppuccin/whiskers/commit/e02f0d436c75cbf5721374d0c4260bb19a5ef955)) 52 | * **whiskers:** tidy up output formats, general clean up ([#235](https://github.com/catppuccin/whiskers/issues/235)) ([9ad3f14](https://github.com/catppuccin/whiskers/commit/9ad3f1499b23ce6b26e7d0ef05cd1cc50716f3af)) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **deps:** update rust crate clap to v4.5.6 ([#11](https://github.com/catppuccin/whiskers/issues/11)) ([73c6550](https://github.com/catppuccin/whiskers/commit/73c65503a9204d006db4a9f4682725c65b35a7ad)) 58 | * **deps:** update rust crate clap to v4.5.7 ([#17](https://github.com/catppuccin/whiskers/issues/17)) ([7f6c57b](https://github.com/catppuccin/whiskers/commit/7f6c57b6f4afc4405795bb3e791626d29ea13ea0)) 59 | 60 | ## [2.3.0](https://github.com/catppuccin/toolbox/compare/whiskers-v2.2.0...whiskers-v2.3.0) (2024-05-27) 61 | 62 | 63 | ### Features 64 | 65 | * **whiskers:** add --list-flavors and --list-accents ([#219](https://github.com/catppuccin/toolbox/issues/219)) ([2a53432](https://github.com/catppuccin/toolbox/commit/2a534326a8b44027628044d6f6a8e84e1824144a)) 66 | * **whiskers:** add css_* functions as filters ([#230](https://github.com/catppuccin/toolbox/issues/230)) ([e2e7f22](https://github.com/catppuccin/toolbox/commit/e2e7f22b88bafd89f2da63f869e3c895abdb5545)) 67 | * **whiskers:** add read_file function ([#217](https://github.com/catppuccin/toolbox/issues/217)) ([c00881a](https://github.com/catppuccin/toolbox/commit/c00881a0c67806b0f8572693728c1ac1bc5586c5)) 68 | 69 | ## [2.2.0](https://github.com/catppuccin/toolbox/compare/whiskers-v2.1.1...whiskers-v2.2.0) (2024-05-26) 70 | 71 | 72 | ### Features 73 | 74 | * **whiskers:** add colors to context in single-flavor matrix ([#223](https://github.com/catppuccin/toolbox/issues/223)) ([37217cc](https://github.com/catppuccin/toolbox/commit/37217cc221c9800614402ddc2c7a09b4f57f3b4b)) 75 | * **whiskers:** support `emoji` and `order` properties ([#227](https://github.com/catppuccin/toolbox/issues/227)) ([365b2f6](https://github.com/catppuccin/toolbox/commit/365b2f6f9a9e3c1ff691643ebd7b5e5cf25cbd9b)) 76 | * **whiskers:** write results to `filename` in single file mode ([#218](https://github.com/catppuccin/toolbox/issues/218)) ([4715155](https://github.com/catppuccin/toolbox/commit/47151550bdad323c8c8793601dd3f3848a2a87c6)) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * **deps:** update rust crate itertools to 0.13.0 ([#220](https://github.com/catppuccin/toolbox/issues/220)) ([6840a88](https://github.com/catppuccin/toolbox/commit/6840a887ae42b366ca2c5ec2cf7fb7194d405eae)) 82 | 83 | ## [2.1.1](https://github.com/catppuccin/toolbox/compare/whiskers-v2.1.0...whiskers-v2.1.1) (2024-05-20) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * **deps:** update rust crate anyhow to 1.0.82 ([#187](https://github.com/catppuccin/toolbox/issues/187)) ([42779a4](https://github.com/catppuccin/toolbox/commit/42779a4e78b43028b4823116f9e6812046cc1a78)) 89 | * **deps:** update rust crate base64 to 0.22.1 ([#188](https://github.com/catppuccin/toolbox/issues/188)) ([74c103f](https://github.com/catppuccin/toolbox/commit/74c103f192b3916fc3863e664de1c9f26f64e08c)) 90 | * **deps:** update rust crate clap to 4.5.4 ([#189](https://github.com/catppuccin/toolbox/issues/189)) ([52d5aa4](https://github.com/catppuccin/toolbox/commit/52d5aa42b0e9a6085b22da37580912a55c442477)) 91 | * **deps:** update rust crate css-colors to 1.0.1 ([#192](https://github.com/catppuccin/toolbox/issues/192)) ([b5d84fd](https://github.com/catppuccin/toolbox/commit/b5d84fde430563a293f864b1f10580eca6881770)) 92 | * **deps:** update rust crate encoding_rs_io to 0.1.7 ([#195](https://github.com/catppuccin/toolbox/issues/195)) ([770d8c4](https://github.com/catppuccin/toolbox/commit/770d8c4cecaf1d379010b3e3098740a5c45fc318)) 93 | * **deps:** update rust crate indexmap to 2.2.6 ([#197](https://github.com/catppuccin/toolbox/issues/197)) ([58be625](https://github.com/catppuccin/toolbox/commit/58be625815b71b43dfecd37b9b88ef50f4a62de3)) 94 | * **deps:** update rust crate itertools to 0.12.1 ([#198](https://github.com/catppuccin/toolbox/issues/198)) ([9b662d8](https://github.com/catppuccin/toolbox/commit/9b662d866263364033e55c3f4e9ddaa5d4a12bb4)) 95 | * **deps:** update rust crate lzma-rust to 0.1.6 ([#199](https://github.com/catppuccin/toolbox/issues/199)) ([ae06a6f](https://github.com/catppuccin/toolbox/commit/ae06a6fc5dbcb280362dbec62ff3c7924618c639)) 96 | * **deps:** update rust crate rmp-serde to 1.3.0 ([#185](https://github.com/catppuccin/toolbox/issues/185)) ([6722334](https://github.com/catppuccin/toolbox/commit/6722334591411c6af3cfcba42fc159309798a110)) 97 | * **deps:** update rust crate semver to 1.0.22 ([#201](https://github.com/catppuccin/toolbox/issues/201)) ([ea25560](https://github.com/catppuccin/toolbox/commit/ea255603d532bd2dcd725541f61277672bd5c08a)) 98 | * **deps:** update rust crate serde to 1.0.200 ([#202](https://github.com/catppuccin/toolbox/issues/202)) ([f6b6f36](https://github.com/catppuccin/toolbox/commit/f6b6f361b92c5ba42a7285a75563aceb50990cee)) 99 | * **deps:** update rust crate serde_json to 1.0.116 ([#203](https://github.com/catppuccin/toolbox/issues/203)) ([2540310](https://github.com/catppuccin/toolbox/commit/2540310ad00e0c2d78c3402e9e5f0e38336292d2)) 100 | * **deps:** update rust crate serde_yaml to 0.9.34 ([#204](https://github.com/catppuccin/toolbox/issues/204)) ([75235ba](https://github.com/catppuccin/toolbox/commit/75235ba5053792ffaa492c62f0c1ff108e09a02e)) 101 | * **deps:** update rust crate tempfile to 3.10.1 ([#205](https://github.com/catppuccin/toolbox/issues/205)) ([1589be5](https://github.com/catppuccin/toolbox/commit/1589be5969f8d4b456d8ee6a329938885dc6c6af)) 102 | * **deps:** update rust crate tera to 1.19.1 ([#206](https://github.com/catppuccin/toolbox/issues/206)) ([fc912f8](https://github.com/catppuccin/toolbox/commit/fc912f827db0729a64cc33c3dc769423479ab916)) 103 | * **whiskers:** avoid excessive whitespace trimming ([#216](https://github.com/catppuccin/toolbox/issues/216)) ([59bc0f6](https://github.com/catppuccin/toolbox/commit/59bc0f6db2c1399ad9c0d893f47253d95243901c)) 104 | 105 | ## [2.1.0](https://github.com/catppuccin/toolbox/compare/whiskers-v2.0.2...whiskers-v2.1.0) (2024-04-27) 106 | 107 | 108 | ### Features 109 | 110 | * binstall support ([#173](https://github.com/catppuccin/toolbox/issues/173)) ([2ae0c33](https://github.com/catppuccin/toolbox/commit/2ae0c33b9b6c577cacbeed02e6a68873194597ab)) 111 | * **whiskers:** allow overrides of any matrix iterable ([#171](https://github.com/catppuccin/toolbox/issues/171)) ([052482d](https://github.com/catppuccin/toolbox/commit/052482d8c702b4747ef97a507ca8d749e4a75b76)) 112 | * **whiskers:** create needed parent directories by default ([0cd4327](https://github.com/catppuccin/toolbox/commit/0cd432754dfc112dababd2db9b2061175cc0b123)) 113 | * **whiskers:** support alternative template file encodings ([a966a83](https://github.com/catppuccin/toolbox/commit/a966a83fa00464d01e8ede3e9760abb5712817d7)) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * **deps:** update rust crate rmp-serde to 1.2 ([#180](https://github.com/catppuccin/toolbox/issues/180)) ([6148435](https://github.com/catppuccin/toolbox/commit/6148435e940b82f3edfa65c02cb9e20a48cb7de5)) 119 | 120 | ## [2.0.2](https://github.com/catppuccin/toolbox/compare/whiskers-v2.0.1...whiskers-v2.0.2) (2024-04-02) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * put flavors/flavor into context even when matrix is used ([#169](https://github.com/catppuccin/toolbox/issues/169)) ([58dc4b5](https://github.com/catppuccin/toolbox/commit/58dc4b5663b4a37abaa29f0b43995d6c4de201ee)) 126 | 127 | ## [2.0.1](https://github.com/catppuccin/toolbox/compare/whiskers-v2.0.0...whiskers-v2.0.1) (2024-04-01) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * **whiskers:** fix frontmatter/version detection on windows ([ac38530](https://github.com/catppuccin/toolbox/commit/ac38530909bf6421f87002423f968e04420f9a0e)) 133 | 134 | ## [2.0.0](https://github.com/catppuccin/toolbox/compare/whiskers-v1.1.4...whiskers-v2.0.0) (2024-03-31) 135 | 136 | 137 | ### ⚠ BREAKING CHANGES 138 | 139 | * switch to tera & rich context variables 140 | 141 | ### Features 142 | 143 | * switch to tera & rich context variables ([bdf0dc5](https://github.com/catppuccin/toolbox/commit/bdf0dc54b0271c26ea5522e105a562ef946e46bd)) 144 | * **whiskers:** add colors object in context ([#104](https://github.com/catppuccin/toolbox/issues/104)) ([0f08acc](https://github.com/catppuccin/toolbox/commit/0f08acc98b77fb8ef2c62cf6d1e842afcc0265bf)) 145 | * **whiskers:** add rgba to hex helpers ([#120](https://github.com/catppuccin/toolbox/issues/120)) ([31ffd9e](https://github.com/catppuccin/toolbox/commit/31ffd9e2bc806fcbd9f0c14653c93c17a91ba6c7)) 146 | * **whiskers:** enforce semver versioning in frontmatter ([bdf0dc5](https://github.com/catppuccin/toolbox/commit/bdf0dc54b0271c26ea5522e105a562ef946e46bd)) 147 | * **whiskers:** update catppuccin to v2 & add `flavorName` var ([#137](https://github.com/catppuccin/toolbox/issues/137)) ([8e60740](https://github.com/catppuccin/toolbox/commit/8e607401c48447f368e4beb59157b34ace1c4a85)) 148 | 149 | 150 | ### Bug Fixes 151 | 152 | * **deps:** update rust crate catppuccin to 1.4 ([#124](https://github.com/catppuccin/toolbox/issues/124)) ([0b6de43](https://github.com/catppuccin/toolbox/commit/0b6de43b4817fa4e34fcebe5fde81159d9103a8c)) 153 | * **deps:** update rust crate catppuccin to 2.1 ([#145](https://github.com/catppuccin/toolbox/issues/145)) ([0eb1fd7](https://github.com/catppuccin/toolbox/commit/0eb1fd78420f6257a1ed11ee71af7e54d02b5c2c)) 154 | * **deps:** update rust crate catppuccin to 2.2 ([#146](https://github.com/catppuccin/toolbox/issues/146)) ([fe9fd1a](https://github.com/catppuccin/toolbox/commit/fe9fd1a8be8c2179b2d0c136b5ce324bae5b2c28)) 155 | * **deps:** update rust crate clap to 4.5 ([#127](https://github.com/catppuccin/toolbox/issues/127)) ([20d4047](https://github.com/catppuccin/toolbox/commit/20d40479bbf3345f2b1038c736a07ccb4c6efda9)) 156 | * **deps:** update rust crate clap-stdin to 0.4.0 ([#96](https://github.com/catppuccin/toolbox/issues/96)) ([dc6c017](https://github.com/catppuccin/toolbox/commit/dc6c0177cedbde090d63993587f6360722c0ed65)) 157 | * **deps:** update rust crate indexmap to 2.2.3 ([#126](https://github.com/catppuccin/toolbox/issues/126)) ([70bfca0](https://github.com/catppuccin/toolbox/commit/70bfca0dbc060e85be291ad230d617bc8c7f9c5e)) 158 | * **deps:** update rust crate tempfile to 3.10 ([#117](https://github.com/catppuccin/toolbox/issues/117)) ([c8846f6](https://github.com/catppuccin/toolbox/commit/c8846f6b038c69aa42a85cdaa46b1ae378f869ba)) 159 | * **whiskers:** use block context in darklight helper ([#144](https://github.com/catppuccin/toolbox/issues/144)) ([486a747](https://github.com/catppuccin/toolbox/commit/486a74772ebb159913063f668dd1f015e8418129)) 160 | 161 | ## [1.1.4](https://github.com/catppuccin/toolbox/compare/whiskers-v1.1.3...whiskers-v1.1.4) (2023-12-10) 162 | 163 | 164 | ### Miscellaneous Chores 165 | 166 | * **whiskers:** release as 1.1.4 ([0edb5ff](https://github.com/catppuccin/toolbox/commit/0edb5ff8bd2474eb6954a5a5539b27679873d2fc)) 167 | 168 | ## [1.1.3](https://github.com/catppuccin/toolbox/compare/whiskers-v1.1.2...whiskers-v1.1.3) (2023-12-10) 169 | 170 | 171 | ### Bug Fixes 172 | 173 | * use hex -> rgb(a) without hsla where possible ([#95](https://github.com/catppuccin/toolbox/issues/95)) ([c7c095f](https://github.com/catppuccin/toolbox/commit/c7c095ff7d14d4b43065b4a81c45e9e5354c87c6)) 174 | 175 | ## [1.1.2](https://github.com/catppuccin/toolbox/compare/whiskers-v1.1.1...whiskers-v1.1.2) (2023-11-23) 176 | 177 | 178 | ### Bug Fixes 179 | 180 | * **deps:** update rust crate base64 to 0.21.5 ([#70](https://github.com/catppuccin/toolbox/issues/70)) ([be92614](https://github.com/catppuccin/toolbox/commit/be9261407e181a3cbf2bb88be871727ebd88dc3e)) 181 | * **deps:** update rust crate clap to 4.4.7 ([#71](https://github.com/catppuccin/toolbox/issues/71)) ([21cdc5d](https://github.com/catppuccin/toolbox/commit/21cdc5d1e51f2145758c49e8fff83a426ee72cee)) 182 | * **deps:** update rust crate clap to 4.4.8 ([#84](https://github.com/catppuccin/toolbox/issues/84)) ([efac5e3](https://github.com/catppuccin/toolbox/commit/efac5e3548521d5bdcaa83f49c8775bfab20dda2)) 183 | * **deps:** update rust crate handlebars to 4.5.0 ([#79](https://github.com/catppuccin/toolbox/issues/79)) ([cbd1cb7](https://github.com/catppuccin/toolbox/commit/cbd1cb7fdebb9e7f7deb57ed2cae9055a5623e56)) 184 | * **deps:** update rust crate serde to 1.0.192 ([#74](https://github.com/catppuccin/toolbox/issues/74)) ([02676a9](https://github.com/catppuccin/toolbox/commit/02676a91c57123b8b77b92a4f15fe9c4b2925b22)) 185 | * **deps:** update rust crate serde to 1.0.193 ([#86](https://github.com/catppuccin/toolbox/issues/86)) ([020f291](https://github.com/catppuccin/toolbox/commit/020f2910ade722dfa3d3a358f8e6baa7feacd29a)) 186 | * **deps:** update rust crate serde_json to 1.0.108 ([#75](https://github.com/catppuccin/toolbox/issues/75)) ([e9effd0](https://github.com/catppuccin/toolbox/commit/e9effd05376c041ac0605fde6bdc0e8f614de558)) 187 | * **deps:** update rust crate serde_yaml to 0.9.27 ([#76](https://github.com/catppuccin/toolbox/issues/76)) ([9eb5470](https://github.com/catppuccin/toolbox/commit/9eb54703ff49c9ee06b8be63396dddfca6a60f2c)) 188 | 189 | ## [1.1.1](https://github.com/catppuccin/toolbox/compare/whiskers-v1.1.0...whiskers-v1.1.1) (2023-10-28) 190 | 191 | 192 | ### Miscellaneous Chores 193 | 194 | * **whiskers:** release as 1.1.1 ([9033840](https://github.com/catppuccin/toolbox/commit/9033840c0b9cf591b7a35e5f595e044925f1cb2b)) 195 | 196 | ## [1.1.0](https://github.com/catppuccin/toolbox/compare/whiskers-v1.0.3...whiskers-v1.1.0) (2023-10-28) 197 | 198 | Re-released as 1.1.1 because the CI to publish the binary/crate failed. 199 | 200 | ### Features 201 | 202 | * **whiskers:** add check mode with diff view ([6bb415e](https://github.com/catppuccin/toolbox/commit/6bb415e87921f8db1266edde15737ac7bb24bd90)) 203 | * **whiskers:** add version flag ([6bb415e](https://github.com/catppuccin/toolbox/commit/6bb415e87921f8db1266edde15737ac7bb24bd90)) 204 | 205 | ## [1.0.3](https://github.com/catppuccin/toolbox/compare/whiskers-v1.0.2...whiskers-v1.0.3) (2023-10-28) 206 | 207 | 208 | ### Miscellaneous Chores 209 | 210 | * **whiskers:** release as 1.0.3 ([5bd49bf](https://github.com/catppuccin/toolbox/commit/5bd49bfd1ef6b5b3e9618e6c7f8b4550e5b564ca)) 211 | 212 | ## [1.0.2](https://github.com/catppuccin/toolbox/compare/whiskers-v1.0.1...whiskers-v1.0.2) (2023-10-27) 213 | 214 | 215 | ### Miscellaneous Chores 216 | 217 | * **whiskers:** release as 1.0.2 ([d20e4b6](https://github.com/catppuccin/toolbox/commit/d20e4b6be08d85c26ea5896767d6b10988185e22)) 218 | 219 | ## [1.0.1](https://github.com/catppuccin/toolbox/compare/whiskers-v1.0.0...whiskers-v1.0.1) (2023-10-27) 220 | 221 | 222 | ### Miscellaneous Chores 223 | 224 | * **whiskers:** release as 1.0.1 ([92b9409](https://github.com/catppuccin/toolbox/commit/92b9409b67047d0f58a4255b8bed638a112cd54d)) 225 | 226 | ## [1.0.0](https://github.com/catppuccin/toolbox/compare/whiskers-v1.0.0...whiskers-v1.0.0) (2023-10-27) 227 | 228 | 229 | ### Features 230 | 231 | * add whiskers ([#46](https://github.com/catppuccin/toolbox/issues/46)) ([9c4a5bb](https://github.com/catppuccin/toolbox/commit/9c4a5bb84563e1af57a5ab8670f550b2fbcf21e9)) 232 | 233 | 234 | ### Miscellaneous Chores 235 | 236 | * **whiskers:** release as 1.0.0 ([c8e15ce](https://github.com/catppuccin/toolbox/commit/c8e15ce96aa04a835da970de5355b60c2b7b213d)) 237 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "catppuccin-whiskers" 3 | version = "2.5.1" 4 | authors = ["backwardspy "] 5 | edition = "2021" 6 | description = "😾 Soothing port creation tool for the high-spirited!" 7 | readme = "README.md" 8 | homepage = "https://github.com/catppuccin/whiskers/tree/main/whiskers" 9 | repository = "https://github.com/catppuccin/whiskers" 10 | license = "MIT" 11 | 12 | [profile.release] 13 | lto = true 14 | opt-level = "z" 15 | strip = true 16 | 17 | [lib] 18 | name = "whiskers" 19 | path = "src/lib.rs" 20 | 21 | [[bin]] 22 | name = "whiskers" 23 | path = "src/main.rs" 24 | 25 | [package.metadata.binstall] 26 | pkg-url = "{ repo }/releases/download/v{ version }/whiskers-{ target }{ archive-suffix }" 27 | pkg-fmt = "bin" 28 | 29 | [lints.clippy] 30 | all = { level = "warn", priority = -1 } 31 | pedantic = { level = "warn", priority = -1 } 32 | nursery = { level = "warn", priority = -1 } 33 | unwrap_used = "warn" 34 | missing_errors_doc = "allow" 35 | implicit_hasher = "allow" 36 | cast_possible_truncation = "allow" 37 | cast_sign_loss = "allow" 38 | 39 | [dependencies] 40 | anyhow = "1.0.95" 41 | base64 = "0.22.1" 42 | catppuccin = { version = "2.4.0", features = ["serde", "css-colors"] } 43 | clap = { version = "4.5.30", features = ["derive"] } 44 | clap-stdin = "0.6.0" 45 | css-colors = "1.0.1" 46 | detect-newline-style = "0.1.2" 47 | encoding_rs_io = "0.1.7" 48 | indexmap = { version = "2.7.1", features = ["serde"] } 49 | itertools = "0.14.0" 50 | lzma-rust = "0.1.6" 51 | rmp-serde = "1.3.0" 52 | semver = { version = "1.0.25", features = ["serde"] } 53 | serde = { version = "1.0.217", features = ["derive"] } 54 | serde_json = "1.0.138" 55 | serde_yaml = "0.9.34" 56 | tempfile = "3.17.1" 57 | tera = { version = "1.19.1", features = ["preserve_order"] } 58 | thiserror = "2.0.11" 59 | 60 | [dev-dependencies] 61 | assert_cmd = "2.0.14" 62 | predicates = "3.1.3" 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Catppuccin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo
3 | 4 | Catppuccin Whiskers 5 | 6 |

7 | 8 |

9 | 10 | 11 | 12 |

13 | 14 |   15 | 16 | Whiskers is a port creation helper tool that is custom-built for Catppuccin, 17 | allowing developers to define template files which the palette can be injected 18 | into. 19 | 20 | ## Installation 21 | 22 | You can install Whiskers using one of the methods below: 23 | 24 | | Installation Method | Instructions | 25 | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------- | 26 | | crates.io | `cargo install catppuccin-whiskers` | 27 | | Source | `cargo install --git https://github.com/catppuccin/whiskers catppuccin-whiskers` | 28 | | Homebrew | `brew install catppuccin/tap/whiskers` | 29 | | Nix | `nix profile install github:catppuccin/nix@whiskers`
`nix run github:catppuccin/nix#whiskers -- ` | 30 | | Binaries
(Windows, MacOS & Linux) | Available from the [latest GitHub release](https://github.com/catppuccin/whiskers/releases). | 31 | | AUR | Install [catppuccin-whiskers-bin](https://aur.archlinux.org/packages/catppuccin-whiskers-bin) | 32 | 33 | ## Usage 34 | 35 | ```console 36 | $ whiskers --help 37 | Soothing port creation tool for the high-spirited! 38 | 39 | Usage: whiskers [OPTIONS] [TEMPLATE] 40 | 41 | Arguments: 42 | [TEMPLATE] 43 | Path to the template file, or - for stdin 44 | 45 | Options: 46 | -f, --flavor 47 | Render a single flavor instead of all four 48 | 49 | [possible values: latte, frappe, macchiato, mocha] 50 | 51 | --color-overrides 52 | Set color overrides 53 | 54 | --overrides 55 | Set frontmatter overrides 56 | 57 | --check [] 58 | Instead of creating an output, check it against an example 59 | 60 | In single-output mode, a path to the example file must be provided. In multi-output mode, no path is required and, if one is provided, it will be ignored. 61 | 62 | --dry-run 63 | Dry run, don't write anything to disk 64 | 65 | --list-functions 66 | List all Tera filters and functions 67 | 68 | --list-flavors 69 | List the Catppuccin flavors 70 | 71 | --list-accents 72 | List the Catppuccin accent colors 73 | 74 | -o, --output-format 75 | Output format of --list-functions 76 | 77 | [default: json] 78 | [possible values: json, yaml, markdown, markdown-table, plain] 79 | 80 | -h, --help 81 | Print help (see a summary with '-h') 82 | 83 | -V, --version 84 | Print version 85 | ``` 86 | 87 | ## Template 88 | 89 | Please familiarize yourself with [Tera](https://keats.github.io/tera/), 90 | which is the templating engine used in Whiskers. 91 | 92 | ### Naming Convention 93 | 94 | Whiskers imposes no restrictions on template names. However, we recommend you use one the following options: 95 | 96 | - `.tera` in the repo root for ports that only need one template. 97 | - For example the [lazygit](https://github.com/catppuccin/lazygit) port uses [`lazygit.tera`](https://github.com/catppuccin/lazygit/blob/main/lazygit.tera) 98 | - `templates/.tera` especially for ports that have multiple templates. 99 | - For example, a port that generates files called `ui.cfg` and `palette.cfg` could use `templates/ui.tera` and `templates/palette.tera` respectively. 100 | 101 | These conventions exist to make it easier for contributors to find templates and to give code editors a hint about the correct file type. 102 | 103 | ### Context Variables 104 | 105 | The following variables are available for use in your templates: 106 | 107 | #### Single-Flavor Mode 108 | 109 | | Variable | Description | 110 | | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | 111 | | `flavor` ([`Flavor`](#flavor)) | The flavor being templated. | 112 | | `rosewater`, `flamingo`, `pink`, [etc.](https://github.com/catppuccin/catppuccin#-palette) ([`Color`](#color)) | All colors of the flavor being templated. | 113 | | Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | 114 | 115 | #### Multi-Flavor Mode 116 | 117 | | Variable | Description | 118 | | --------------------------------------------- | ---------------------------------------------------------------------------------- | 119 | | `flavors` (Map\) | An array containing all of the named flavors, with every other context variable. | 120 | | Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | 121 | 122 | #### Types 123 | 124 | These types are designed to closely match the [palette.json](https://github.com/catppuccin/palette/blob/main/palette.json). 125 | 126 | ##### Flavor 127 | 128 | | Field | Type | Description | Examples | 129 | | ------------ | -------------------- | ------------------------------------------------------ | ----------------------------------------------- | 130 | | `name` | `String` | The name of the flavor. | `"Latte"`, `"Frappé"`, `"Macchiato"`, `"Mocha"` | 131 | | `identifier` | `String` | The identifier of the flavor. | `"latte"`, `"frappe"`, `"macchiato"`, `"mocha"` | 132 | | `emoji` | `char` | Emoji associated with the flavor. | `'🌻'`, `'🪴'`, `'🌺'`, `'🌿'` | 133 | | `order` | `u32` | Order of the flavor in the palette spec. | `0` to `3` | 134 | | `dark` | `bool` | Whether the flavor is dark. | `false` for Latte, `true` for others | 135 | | `light` | `bool` | Whether the flavor is light. | `true` for Latte, `false` for others | 136 | | `colors` | `Map` | A map of color identifiers to their respective values. | | 137 | 138 | ##### Color 139 | 140 | | Field | Type | Description | Examples | 141 | | ------------ | -------- | ----------------------------------------------- | -------------------------------------- | 142 | | `name` | `String` | The name of the color. | `"Rosewater"`, `"Surface 0"`, `"Base"` | 143 | | `identifier` | `String` | The identifier of the color. | `"rosewater"`, `"surface0"`, `"base"` | 144 | | `order` | `u32` | Order of the color in the palette spec. | `0` to `25` | 145 | | `accent` | `bool` | Whether the color is an accent color. | | 146 | | `hex` | `String` | The color in hexadecimal format. | `"1e1e2e"` | 147 | | `int24` | `u32` | Big-endian 24-bit color in RGB order. | `1973806` | 148 | | `uint32` | `u32` | Big-endian unsigned 32-bit color in ARGB order. | `4280163886` | 149 | | `sint32` | `i32` | Big-endian signed 32-bit color in ARGB order. | `-14803410` | 150 | | `rgb` | `RGB` | The color in RGB format. | | 151 | | `hsl` | `HSL` | The color in HSL format. | | 152 | | `opacity` | `u8` | The opacity of the color. | `0` to `255` | 153 | 154 | ##### RGB 155 | 156 | | Field | Type | Description | 157 | | ----- | ---- | ------------------------------- | 158 | | `r` | `u8` | The red channel of the color. | 159 | | `g` | `u8` | The green channel of the color. | 160 | | `b` | `u8` | The blue channel of the color. | 161 | 162 | ##### HSL 163 | 164 | | Field | Type | Description | 165 | | ----- | ----- | ---------------------------- | 166 | | `h` | `u16` | The hue of the color. | 167 | | `s` | `u8` | The saturation of the color. | 168 | | `l` | `u8` | The lightness of the color. | 169 | 170 | ### Functions 171 | 172 | | Name | Description | Examples | 173 | | ----------- | ------------------------------------------------------------------------------ | --------------------------------------------------- | 174 | | `if` | Return one value if a condition is true, and another if it's false | `if(cond=true, t=1, f=0)` ⇒ `1` | 175 | | `object` | Create an object from the input | `object(a=1, b=2)` ⇒ `{a: 1, b: 2}` | 176 | | `css_rgb` | Convert a color to an RGB CSS string | `css_rgb(color=red)` ⇒ `rgb(210, 15, 57)` | 177 | | `css_rgba` | Convert a color to an RGBA CSS string | `css_rgba(color=red)` ⇒ `rgba(210, 15, 57, 1.00)` | 178 | | `css_hsl` | Convert a color to an HSL CSS string | `css_hsl(color=red)` ⇒ `hsl(347, 87%, 44%)` | 179 | | `css_hsla` | Convert a color to an HSLA CSS string | `css_hsla(color=red)` ⇒ `hsla(347, 87%, 44%, 1.00)` | 180 | | `read_file` | Read and include the contents of a file, path is relative to the template file | `read_file(path="abc.txt")` ⇒ `abc` | 181 | 182 | ### Filters 183 | 184 | | Name | Description | Examples | 185 | | ---------------- | ---------------------------------------------------------------- | ------------------------------------------------ | 186 | | `add` | Add a value to a color | `red \| add(hue=30)` ⇒ `#ff6666` | 187 | | `sub` | Subtract a value from a color | `red \| sub(hue=30)` ⇒ `#d30f9b` | 188 | | `mod` | Modify a color | `red \| mod(lightness=80)` ⇒ `#f8a0b3` | 189 | | `mix` | Mix two colors together | `red \| mix(color=base, amount=0.5)` ⇒ `#e08097` | 190 | | `urlencode_lzma` | Serialize an object into a URL-safe string with LZMA compression | `red \| urlencode_lzma` ⇒ `#ff6666` | 191 | | `trunc` | Truncate a number to a certain number of places | `1.123456 \| trunc(places=3)` ⇒ `1.123` | 192 | | `css_rgb` | Convert a color to an RGB CSS string | `red \| css_rgb` ⇒ `rgb(210, 15, 57)` | 193 | | `css_rgba` | Convert a color to an RGBA CSS string | `red \| css_rgba` ⇒ `rgba(210, 15, 57, 1.00)` | 194 | | `css_hsl` | Convert a color to an HSL CSS string | `red \| css_hsl` ⇒ `hsl(347, 87%, 44%)` | 195 | | `css_hsla` | Convert a color to an HSLA CSS string | `red \| css_hsla` ⇒ `hsla(347, 87%, 44%, 1.00)` | 196 | 197 | > [!NOTE] 198 | > You also have access to all of Tera's own built-in filters and functions. 199 | > See [the Tera documentation](https://keats.github.io/tera/docs/#built-ins) for 200 | > more information. 201 | 202 | ## Frontmatter 203 | 204 | Whiskers templates may include a frontmatter section at the top of the file. 205 | 206 | The frontmatter is a YAML block that contains metadata about the template. If 207 | present, the frontmatter section must be the first thing in the file and must 208 | take the form of valid YAML set between triple-dashed lines. 209 | 210 | ### Template Version 211 | 212 | The most important frontmatter key is the Whiskers version requirement. This key 213 | allows Whiskers to ensure it is rendering a template that it can understand. 214 | 215 | Syntax: 216 | 217 | ```yaml 218 | --- 219 | whiskers: 220 | version: "^2.5.1" 221 | --- 222 | ... standard template content goes here ... 223 | ``` 224 | 225 | Whiskers supports specifying version requirements in a number of ways: 226 | 227 | - `^X.Y.Z`: exactly `X`, with any minor and patch version >= `Y.Z`. **This is the 228 | recommended approach unless a more specific constraint is required.** 229 | - `~X.Y.Z`: exactly `X.Y`, with any patch version >= `Z`. 230 | - `=X.Y.Z`: only version `X.Y.Z`. 231 | - `=X.Y` or `=X`: any version matching `X.Y.*` or `X.*.*`. 232 | - `>ver`: any version newer than `ver`, not including `ver`. 233 | - `>=ver`: version `ver` or newer. 234 | - ` `. 462 | 463 | ```console 464 | $ whiskers theme.tera latte --check themes/latte.cfg 465 | (no output, exit code 0) 466 | 467 | $ whiskers theme.tera latte --check themes/latte.cfg 468 | Templating would result in changes. 469 | 4c4 470 | < accent is #ea76cb 471 | --- 472 | > accent is #40a02b 473 | 474 | (exit code 1) 475 | ``` 476 | 477 | ## Editor Support 478 | 479 | Tera support can be installed in Neovim as of v0.11 and with the latest nvim-treesitter. Support in Helix is unreleased but available on the master branch as of 2025-02-02. For Zed users, we recommend the [Tera extension for Zed](https://github.com/uncenter/zed-tera). 480 | 481 | For Visual Studio Code users we recommend the [Better Jinja](https://marketplace.visualstudio.com/items?itemName=samuelcolvin.jinjahtml) extension. 482 | 483 | ## Further Reading 484 | 485 | - See the [examples](examples) directory which further showcase the utilities 486 | and power of Whiskers. 487 | - See the RFC, 488 | [CAT-0003-Whiskers](https://github.com/catppuccin/community/blob/main/rfc/CAT-0003-Whiskers.md), 489 | to understand the motivation behind creating Whiskers. 490 | 491 |   492 | 493 |

494 |

Copyright © 2023-present Catppuccin Org 495 |

496 | -------------------------------------------------------------------------------- /examples/demo/input.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0.0" 4 | --- 5 | ## Demo 6 | 7 | **flavor:** {{ flavor.name }} 8 | 9 | ### Colours 10 | {% set lightred = red | add(lightness=10) -%} 11 | {% set darkred = red | sub(lightness=10) -%} 12 | {% set palered = red | mix(color=base, amount=0.3) -%} 13 | {% set fadered1 = red | mod(opacity=0.6) -%} 14 | {% set fadered2 = red | mod(opacity=0.5) -%} 15 | 16 | - **red:** #{{ red.hex }} / {{ css_rgb(color=red) }} / {{ css_hsl(color=red) }} 17 | - **components:** r: {{ red.rgb.r }} / {{ red.rgb.r / 255 | trunc(places=2) }}, g: {{ red.rgb.g }} / {{ red.rgb.g / 255 | trunc(places=2) }}, b: {{ red.rgb.b }} / {{ red.rgb.b / 255 | trunc(places=2) }} 18 | - **alpha:** {{ fadered1.opacity }} / {{ fadered1.opacity / 255 | trunc(places=2) }} 19 | - **10% lighter:** #{{ lightred.hex }} / {{ css_rgb(color=lightred) }} / {{ css_hsl(color=lightred) }} 20 | - **10% darker:** #{{ darkred.hex }} / {{ css_rgb(color=darkred) }} / {{ css_hsl(color=darkred) }} 21 | 22 | - **30% mix with base:** #{{ palered.hex }} / {{ css_rgb(color=palered) }} / {{ css_hsl(color=palered) }} 23 | 24 | - **50% opacity:** #{{ fadered2.hex }} / {{ css_rgba(color=fadered2) }} / {{ css_hsla(color=fadered2) }} 25 | 26 | ### Conditionals 27 | 28 | this is a {{ if(cond=flavor.dark, t="dark", f="light") }} theme 29 | -------------------------------------------------------------------------------- /examples/demo/justfile: -------------------------------------------------------------------------------- 1 | # Print out all recipes when running `just` 2 | _default: 3 | @just --list 4 | 5 | # Variables 6 | output := "output" 7 | whiskers_cmd := "cargo run --bin whiskers --" 8 | 9 | # Create the output directory 10 | setup: 11 | mkdir -p {{output}} 12 | 13 | # Remove all files in the output directory 14 | clean: 15 | rm -fv {{output}}/*.md 16 | 17 | # Generate a single flavor, e.g. "mocha" 18 | gen flavor: 19 | @{{whiskers_cmd}} input.tera -f {{flavor}} > {{output}}/{{flavor}}.md 20 | 21 | # Generate all four flavors 22 | all: setup (gen "latte") (gen "frappe") (gen "macchiato") (gen "mocha") 23 | -------------------------------------------------------------------------------- /examples/demo/output/frappe.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | **flavor:** Frappé 4 | 5 | ### Colours 6 | - **red:** #e78284 / rgb(231, 130, 132) / hsl(359, 68%, 71%) 7 | - **components:** r: 231 / 0.91, g: 130 / 0.51, b: 132 / 0.52 8 | - **alpha:** 153 / 0.60 9 | - **10% lighter:** #f0aeb0 / rgb(240, 174, 176) / hsl(359, 68%, 81%) 10 | - **10% darker:** #df5759 / rgb(223, 87, 89) / hsl(359, 68%, 61%) 11 | 12 | - **30% mix with base:** #684b59 / rgb(104, 75, 89) / hsl(331, 16%, 35%) 13 | 14 | - **50% opacity:** #e7828480 / rgba(231, 130, 132, 0.50) / hsla(359, 68%, 71%, 0.50) 15 | 16 | ### Conditionals 17 | 18 | this is a dark theme 19 | -------------------------------------------------------------------------------- /examples/demo/output/latte.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | **flavor:** Latte 4 | 5 | ### Colours 6 | - **red:** #d20f39 / rgb(210, 15, 57) / hsl(347, 87%, 44%) 7 | - **components:** r: 210 / 0.82, g: 15 / 0.06, b: 57 / 0.22 8 | - **alpha:** 153 / 0.60 9 | - **10% lighter:** #f02652 / rgb(240, 38, 82) / hsl(347, 87%, 55%) 10 | - **10% darker:** #a20c2c / rgb(162, 12, 44) / hsl(347, 87%, 34%) 11 | 12 | - **30% mix with base:** #e6adbc / rgb(230, 173, 188) / hsl(344, 53%, 79%) 13 | 14 | - **50% opacity:** #d20f3980 / rgba(210, 15, 57, 0.50) / hsla(347, 87%, 44%, 0.50) 15 | 16 | ### Conditionals 17 | 18 | this is a light theme 19 | -------------------------------------------------------------------------------- /examples/demo/output/macchiato.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | **flavor:** Macchiato 4 | 5 | ### Colours 6 | - **red:** #ed8796 / rgb(237, 135, 150) / hsl(351, 74%, 73%) 7 | - **components:** r: 237 / 0.93, g: 135 / 0.53, b: 150 / 0.59 8 | - **alpha:** 153 / 0.60 9 | - **10% lighter:** #f4b4be / rgb(244, 180, 190) / hsl(351, 74%, 83%) 10 | - **10% darker:** #e65a6f / rgb(230, 90, 111) / hsl(351, 74%, 63%) 11 | 12 | - **30% mix with base:** #614455 / rgb(97, 68, 85) / hsl(325, 18%, 33%) 13 | 14 | - **50% opacity:** #ed879680 / rgba(237, 135, 150, 0.50) / hsla(351, 74%, 73%, 0.50) 15 | 16 | ### Conditionals 17 | 18 | this is a dark theme 19 | -------------------------------------------------------------------------------- /examples/demo/output/mocha.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | **flavor:** Mocha 4 | 5 | ### Colours 6 | - **red:** #f38ba8 / rgb(243, 139, 168) / hsl(343, 81%, 75%) 7 | - **components:** r: 243 / 0.95, g: 139 / 0.55, b: 168 / 0.66 8 | - **alpha:** 153 / 0.60 9 | - **10% lighter:** #f8bacc / rgb(248, 186, 204) / hsl(343, 81%, 85%) 10 | - **10% darker:** #ee5c85 / rgb(238, 92, 133) / hsl(343, 81%, 65%) 11 | 12 | - **30% mix with base:** #5e3f53 / rgb(94, 63, 83) / hsl(321, 20%, 31%) 13 | 14 | - **50% opacity:** #f38ba880 / rgba(243, 139, 168, 0.50) / hsla(343, 81%, 75%, 0.50) 15 | 16 | ### Conditionals 17 | 18 | this is a dark theme 19 | -------------------------------------------------------------------------------- /examples/frontmatter/input.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0.0" 4 | dark_accent: mauve 5 | light_accent: pink 6 | --- 7 | {% set parent = if(cond=flavor.dark, t='darcula', f='default') -%} 8 | {% set accent = if(cond=flavor.dark, t=dark_accent, f=light_accent) -%} 9 | 10 | ## Demo With Frontmatter 11 | 12 | **flavor:** {{ flavor.name }} 13 | 14 | This file also contains variables that have been defined in the frontmatter, as shown below: 15 | 16 | ### Frontmatter Variables 17 | 18 | - **parent** is {{ parent }} 19 | - **accent** is #{{ flavor.colors[accent].hex }} 20 | 21 | ### Colours 22 | 23 | - **red:** #{{ red.hex }} / {{ css_rgb(color=red) }} / {{ css_hsl(color=red) }} 24 | - **components:** r: {{ red.rgb.r }} / {{ red.rgb.r / 255 | trunc(places=2) }}, g: {{ red.rgb.g }} / {{ red.rgb.g / 255 | trunc(places=2) }}, b: {{ red.rgb.b }} / {{ red.rgb.b / 255 | trunc(places=2) }} 25 | - **alpha:** {{ red.opacity }} / {{ red.opacity / 255 | trunc(places=2) }} 26 | {% set lightred = red | add(lightness=10) -%} 27 | - **10% lighter:** #{{ lightred.hex }} / {{ css_rgb(color=lightred) }} / {{ css_hsl(color=lightred) }} 28 | {% set darkred = red | sub(lightness=10) -%} 29 | - **10% darker:** #{{ darkred.hex }} / {{ css_rgb(color=darkred) }} / {{ css_hsl(color=darkred) }} 30 | 31 | {% set palered = red | mix(color=base, amount=0.3) -%} 32 | - **30% mix with base:** #{{ palered.hex }} / {{ css_rgb(color=palered) }} / {{ css_hsl(color=palered) }} 33 | 34 | {% set fadered = red | mod(opacity=0.5) -%} 35 | - **50% opacity:** #{{ fadered.hex }} / {{ css_rgba(color=fadered) }} / {{ css_hsla(color=fadered) }} 36 | 37 | ### Conditionals 38 | 39 | this is a {{ if(cond=flavor.dark, t="dark", f="light") }} theme 40 | -------------------------------------------------------------------------------- /examples/frontmatter/justfile: -------------------------------------------------------------------------------- 1 | # Print out all recipes when running `just` 2 | _default: 3 | @just --list 4 | 5 | # Variables 6 | output := "output" 7 | whiskers_cmd := "cargo run --bin whiskers --" 8 | 9 | # Create the output directory 10 | setup: 11 | mkdir -p {{output}} 12 | 13 | # Remove all files in the output directory 14 | clean: 15 | rm -fv {{output}}/*.md 16 | 17 | # Generate a single flavor, e.g. "mocha" 18 | gen flavor: 19 | @{{whiskers_cmd}} input.tera --flavor {{flavor}} > {{output}}/{{flavor}}.md 20 | 21 | # Generate all four flavors 22 | all: setup (gen "latte") (gen "frappe") (gen "macchiato") (gen "mocha") 23 | -------------------------------------------------------------------------------- /examples/frontmatter/output/frappe.md: -------------------------------------------------------------------------------- 1 | ## Demo With Frontmatter 2 | 3 | **flavor:** Frappé 4 | 5 | This file also contains variables that have been defined in the frontmatter, as shown below: 6 | 7 | ### Frontmatter Variables 8 | 9 | - **parent** is darcula 10 | - **accent** is #ca9ee6 11 | 12 | ### Colours 13 | 14 | - **red:** #e78284 / rgb(231, 130, 132) / hsl(359, 68%, 71%) 15 | - **components:** r: 231 / 0.91, g: 130 / 0.51, b: 132 / 0.52 16 | - **alpha:** 255 / 1.00 17 | - **10% lighter:** #f0aeb0 / rgb(240, 174, 176) / hsl(359, 68%, 81%) 18 | - **10% darker:** #df5759 / rgb(223, 87, 89) / hsl(359, 68%, 61%) 19 | 20 | - **30% mix with base:** #684b59 / rgb(104, 75, 89) / hsl(331, 16%, 35%) 21 | 22 | - **50% opacity:** #e7828480 / rgba(231, 130, 132, 0.50) / hsla(359, 68%, 71%, 0.50) 23 | 24 | ### Conditionals 25 | 26 | this is a dark theme 27 | -------------------------------------------------------------------------------- /examples/frontmatter/output/latte.md: -------------------------------------------------------------------------------- 1 | ## Demo With Frontmatter 2 | 3 | **flavor:** Latte 4 | 5 | This file also contains variables that have been defined in the frontmatter, as shown below: 6 | 7 | ### Frontmatter Variables 8 | 9 | - **parent** is default 10 | - **accent** is #ea76cb 11 | 12 | ### Colours 13 | 14 | - **red:** #d20f39 / rgb(210, 15, 57) / hsl(347, 87%, 44%) 15 | - **components:** r: 210 / 0.82, g: 15 / 0.06, b: 57 / 0.22 16 | - **alpha:** 255 / 1.00 17 | - **10% lighter:** #f02652 / rgb(240, 38, 82) / hsl(347, 87%, 55%) 18 | - **10% darker:** #a20c2c / rgb(162, 12, 44) / hsl(347, 87%, 34%) 19 | 20 | - **30% mix with base:** #e6adbc / rgb(230, 173, 188) / hsl(344, 53%, 79%) 21 | 22 | - **50% opacity:** #d20f3980 / rgba(210, 15, 57, 0.50) / hsla(347, 87%, 44%, 0.50) 23 | 24 | ### Conditionals 25 | 26 | this is a light theme 27 | -------------------------------------------------------------------------------- /examples/frontmatter/output/macchiato.md: -------------------------------------------------------------------------------- 1 | ## Demo With Frontmatter 2 | 3 | **flavor:** Macchiato 4 | 5 | This file also contains variables that have been defined in the frontmatter, as shown below: 6 | 7 | ### Frontmatter Variables 8 | 9 | - **parent** is darcula 10 | - **accent** is #c6a0f6 11 | 12 | ### Colours 13 | 14 | - **red:** #ed8796 / rgb(237, 135, 150) / hsl(351, 74%, 73%) 15 | - **components:** r: 237 / 0.93, g: 135 / 0.53, b: 150 / 0.59 16 | - **alpha:** 255 / 1.00 17 | - **10% lighter:** #f4b4be / rgb(244, 180, 190) / hsl(351, 74%, 83%) 18 | - **10% darker:** #e65a6f / rgb(230, 90, 111) / hsl(351, 74%, 63%) 19 | 20 | - **30% mix with base:** #614455 / rgb(97, 68, 85) / hsl(325, 18%, 33%) 21 | 22 | - **50% opacity:** #ed879680 / rgba(237, 135, 150, 0.50) / hsla(351, 74%, 73%, 0.50) 23 | 24 | ### Conditionals 25 | 26 | this is a dark theme 27 | -------------------------------------------------------------------------------- /examples/frontmatter/output/mocha.md: -------------------------------------------------------------------------------- 1 | ## Demo With Frontmatter 2 | 3 | **flavor:** Mocha 4 | 5 | This file also contains variables that have been defined in the frontmatter, as shown below: 6 | 7 | ### Frontmatter Variables 8 | 9 | - **parent** is darcula 10 | - **accent** is #cba6f7 11 | 12 | ### Colours 13 | 14 | - **red:** #f38ba8 / rgb(243, 139, 168) / hsl(343, 81%, 75%) 15 | - **components:** r: 243 / 0.95, g: 139 / 0.55, b: 168 / 0.66 16 | - **alpha:** 255 / 1.00 17 | - **10% lighter:** #f8bacc / rgb(248, 186, 204) / hsl(343, 81%, 85%) 18 | - **10% darker:** #ee5c85 / rgb(238, 92, 133) / hsl(343, 81%, 65%) 19 | 20 | - **30% mix with base:** #5e3f53 / rgb(94, 63, 83) / hsl(321, 20%, 31%) 21 | 22 | - **50% opacity:** #f38ba880 / rgba(243, 139, 168, 0.50) / hsla(343, 81%, 75%, 0.50) 23 | 24 | ### Conditionals 25 | 26 | this is a dark theme 27 | -------------------------------------------------------------------------------- /examples/multi-output/single-accent/example.tera: -------------------------------------------------------------------------------- 1 | --- 2 | # use `--overrides '{"accent": "red"}'` for example 3 | whiskers: 4 | version: "2.0.0" 5 | matrix: 6 | - flavor 7 | - accent 8 | filename: 'output-{{flavor.identifier}}-{{accent}}.txt' 9 | --- 10 | {{flavor.name}} accent {{accent}} 11 | -------------------------------------------------------------------------------- /examples/multi-output/single-accent/output-frappe-red.txt: -------------------------------------------------------------------------------- 1 | Frappé accent red 2 | -------------------------------------------------------------------------------- /examples/multi-output/single-accent/output-latte-red.txt: -------------------------------------------------------------------------------- 1 | Latte accent red 2 | -------------------------------------------------------------------------------- /examples/multi-output/single-accent/output-macchiato-red.txt: -------------------------------------------------------------------------------- 1 | Macchiato accent red 2 | -------------------------------------------------------------------------------- /examples/multi-output/single-accent/output-mocha-red.txt: -------------------------------------------------------------------------------- 1 | Mocha accent red 2 | -------------------------------------------------------------------------------- /examples/multi-output/single-flavor/example.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0.0" 4 | matrix: 5 | - variant: [a, b] 6 | filename: 'output-{{variant}}.txt' 7 | --- 8 | {{flavor.name}} variant {{variant}} 9 | -------------------------------------------------------------------------------- /examples/multi-output/single-flavor/output-a.txt: -------------------------------------------------------------------------------- 1 | Latte variant a 2 | -------------------------------------------------------------------------------- /examples/multi-output/single-flavor/output-b.txt: -------------------------------------------------------------------------------- 1 | Latte variant b 2 | -------------------------------------------------------------------------------- /examples/single-file/overrides/input.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0.0" 4 | filename: "output.md" 5 | 6 | # Set default accent color 7 | accent: "mauve" 8 | 9 | # Set custom variables 10 | user: "@sgoudham" 11 | 12 | overrides: 13 | latte: 14 | user: "@backwardspy" 15 | accent: "pink" 16 | emoji: "🌻" 17 | frappe: 18 | user: "@nullishamy" 19 | accent: "blue" 20 | emoji: "🪴" 21 | macchiato: 22 | emoji: "🌺" 23 | mocha: 24 | user: "@nekowinston" 25 | accent: "sky" 26 | emoji: "🌿" 27 | --- 28 | # Single File With Overrides 29 | {% for id, flavor in flavors %} 30 | {% set o = overrides[id] -%} 31 | {% set user = o | get(key="user", default=user) -%} 32 | {% set accent = o | get(key="accent", default=accent) -%} 33 | ## {{o.emoji}} {{flavor.name}} 34 | 35 | {{user}}'s favourite hex code is #{{flavor.colors[accent].hex}} 36 | {% endfor %} 37 | -------------------------------------------------------------------------------- /examples/single-file/overrides/justfile: -------------------------------------------------------------------------------- 1 | # Print out all recipes when running `just` 2 | _default: 3 | @just --list 4 | 5 | # Variables 6 | whiskers_cmd := "cargo run --bin whiskers --" 7 | 8 | # Generate a single file containing all four flavors 9 | gen: 10 | @{{whiskers_cmd}} input.tera 11 | -------------------------------------------------------------------------------- /examples/single-file/overrides/output.md: -------------------------------------------------------------------------------- 1 | # Single File With Overrides 2 | 3 | ## 🌻 Latte 4 | 5 | @backwardspy's favourite hex code is #ea76cb 6 | 7 | ## 🪴 Frappé 8 | 9 | @nullishamy's favourite hex code is #8caaee 10 | 11 | ## 🌺 Macchiato 12 | 13 | @sgoudham's favourite hex code is #c6a0f6 14 | 15 | ## 🌿 Mocha 16 | 17 | @nekowinston's favourite hex code is #89dceb 18 | -------------------------------------------------------------------------------- /examples/single-file/simple/input.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0.0" 4 | filename: "output.md" 5 | --- 6 | # Catppuccin Palette v0.2.0 7 | 8 | {% for _, flavor in flavors -%} 9 |
10 | {{flavor.name}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {%- for _, color in flavor.colors %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {%- endfor %} 28 |
LabelsHexRGBHSL
{{color.name}}#{{color.hex}}{{css_rgb(color=color)}}{{css_hsl(color=color)}}
29 |
30 | {% endfor %} 31 | -------------------------------------------------------------------------------- /examples/single-file/simple/justfile: -------------------------------------------------------------------------------- 1 | # Print out all recipes when running `just` 2 | _default: 3 | @just --list 4 | 5 | # Variables 6 | whiskers_cmd := "cargo run --bin whiskers --" 7 | 8 | # Generate a single file containing all four flavors 9 | gen: 10 | @{{whiskers_cmd}} input.tera 11 | -------------------------------------------------------------------------------- /examples/single-file/simple/output.md: -------------------------------------------------------------------------------- 1 | # Catppuccin Palette v0.2.0 2 | 3 |
4 | Latte 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 |
LabelsHexRGBHSL
Rosewater#dc8a78rgb(220, 138, 120)hsl(11, 59%, 67%)
Flamingo#dd7878rgb(221, 120, 120)hsl(0, 60%, 67%)
Pink#ea76cbrgb(234, 118, 203)hsl(316, 73%, 69%)
Mauve#8839efrgb(136, 57, 239)hsl(266, 85%, 58%)
Red#d20f39rgb(210, 15, 57)hsl(347, 87%, 44%)
Maroon#e64553rgb(230, 69, 83)hsl(355, 76%, 59%)
Peach#fe640brgb(254, 100, 11)hsl(22, 99%, 52%)
Yellow#df8e1drgb(223, 142, 29)hsl(35, 77%, 49%)
Green#40a02brgb(64, 160, 43)hsl(109, 58%, 40%)
Teal#179299rgb(23, 146, 153)hsl(183, 74%, 35%)
Sky#04a5e5rgb(4, 165, 229)hsl(197, 96%, 46%)
Sapphire#209fb5rgb(32, 159, 181)hsl(189, 70%, 42%)
Blue#1e66f5rgb(30, 102, 245)hsl(220, 91%, 54%)
Lavender#7287fdrgb(114, 135, 253)hsl(231, 97%, 72%)
Text#4c4f69rgb(76, 79, 105)hsl(234, 16%, 36%)
Subtext 1#5c5f77rgb(92, 95, 119)hsl(233, 13%, 42%)
Subtext 0#6c6f85rgb(108, 111, 133)hsl(233, 10%, 47%)
Overlay 2#7c7f93rgb(124, 127, 147)hsl(232, 10%, 53%)
Overlay 1#8c8fa1rgb(140, 143, 161)hsl(231, 10%, 59%)
Overlay 0#9ca0b0rgb(156, 160, 176)hsl(228, 11%, 65%)
Surface 2#acb0bergb(172, 176, 190)hsl(227, 12%, 71%)
Surface 1#bcc0ccrgb(188, 192, 204)hsl(225, 14%, 77%)
Surface 0#ccd0dargb(204, 208, 218)hsl(223, 16%, 83%)
Base#eff1f5rgb(239, 241, 245)hsl(220, 23%, 95%)
Mantle#e6e9efrgb(230, 233, 239)hsl(220, 22%, 92%)
Crust#dce0e8rgb(220, 224, 232)hsl(220, 21%, 89%)
196 |
197 |
198 | Frappé 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 |
LabelsHexRGBHSL
Rosewater#f2d5cfrgb(242, 213, 207)hsl(10, 57%, 88%)
Flamingo#eebebergb(238, 190, 190)hsl(0, 58%, 84%)
Pink#f4b8e4rgb(244, 184, 228)hsl(316, 73%, 84%)
Mauve#ca9ee6rgb(202, 158, 230)hsl(277, 59%, 76%)
Red#e78284rgb(231, 130, 132)hsl(359, 68%, 71%)
Maroon#ea999crgb(234, 153, 156)hsl(358, 66%, 76%)
Peach#ef9f76rgb(239, 159, 118)hsl(20, 79%, 70%)
Yellow#e5c890rgb(229, 200, 144)hsl(40, 62%, 73%)
Green#a6d189rgb(166, 209, 137)hsl(96, 44%, 68%)
Teal#81c8bergb(129, 200, 190)hsl(172, 39%, 65%)
Sky#99d1dbrgb(153, 209, 219)hsl(189, 48%, 73%)
Sapphire#85c1dcrgb(133, 193, 220)hsl(199, 55%, 69%)
Blue#8caaeergb(140, 170, 238)hsl(222, 74%, 74%)
Lavender#babbf1rgb(186, 187, 241)hsl(239, 66%, 84%)
Text#c6d0f5rgb(198, 208, 245)hsl(227, 70%, 87%)
Subtext 1#b5bfe2rgb(181, 191, 226)hsl(227, 44%, 80%)
Subtext 0#a5adcergb(165, 173, 206)hsl(228, 29%, 73%)
Overlay 2#949cbbrgb(148, 156, 187)hsl(228, 22%, 66%)
Overlay 1#838ba7rgb(131, 139, 167)hsl(227, 17%, 58%)
Overlay 0#737994rgb(115, 121, 148)hsl(229, 13%, 52%)
Surface 2#626880rgb(98, 104, 128)hsl(228, 13%, 44%)
Surface 1#51576drgb(81, 87, 109)hsl(227, 15%, 37%)
Surface 0#414559rgb(65, 69, 89)hsl(230, 16%, 30%)
Base#303446rgb(48, 52, 70)hsl(229, 19%, 23%)
Mantle#292c3crgb(41, 44, 60)hsl(231, 19%, 20%)
Crust#232634rgb(35, 38, 52)hsl(229, 20%, 17%)
390 |
391 |
392 | Macchiato 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 |
LabelsHexRGBHSL
Rosewater#f4dbd6rgb(244, 219, 214)hsl(10, 58%, 90%)
Flamingo#f0c6c6rgb(240, 198, 198)hsl(0, 58%, 86%)
Pink#f5bde6rgb(245, 189, 230)hsl(316, 74%, 85%)
Mauve#c6a0f6rgb(198, 160, 246)hsl(267, 83%, 80%)
Red#ed8796rgb(237, 135, 150)hsl(351, 74%, 73%)
Maroon#ee99a0rgb(238, 153, 160)hsl(355, 71%, 77%)
Peach#f5a97frgb(245, 169, 127)hsl(21, 85%, 73%)
Yellow#eed49frgb(238, 212, 159)hsl(40, 70%, 78%)
Green#a6da95rgb(166, 218, 149)hsl(105, 48%, 72%)
Teal#8bd5cargb(139, 213, 202)hsl(171, 47%, 69%)
Sky#91d7e3rgb(145, 215, 227)hsl(189, 60%, 73%)
Sapphire#7dc4e4rgb(125, 196, 228)hsl(199, 65%, 69%)
Blue#8aadf4rgb(138, 173, 244)hsl(220, 83%, 75%)
Lavender#b7bdf8rgb(183, 189, 248)hsl(234, 82%, 85%)
Text#cad3f5rgb(202, 211, 245)hsl(227, 68%, 88%)
Subtext 1#b8c0e0rgb(184, 192, 224)hsl(228, 39%, 80%)
Subtext 0#a5adcbrgb(165, 173, 203)hsl(227, 27%, 72%)
Overlay 2#939ab7rgb(147, 154, 183)hsl(228, 20%, 65%)
Overlay 1#8087a2rgb(128, 135, 162)hsl(228, 15%, 57%)
Overlay 0#6e738drgb(110, 115, 141)hsl(230, 12%, 49%)
Surface 2#5b6078rgb(91, 96, 120)hsl(230, 14%, 42%)
Surface 1#494d64rgb(73, 77, 100)hsl(231, 16%, 34%)
Surface 0#363a4frgb(54, 58, 79)hsl(230, 19%, 26%)
Base#24273argb(36, 39, 58)hsl(232, 24%, 18%)
Mantle#1e2030rgb(30, 32, 48)hsl(233, 23%, 15%)
Crust#181926rgb(24, 25, 38)hsl(236, 23%, 12%)
584 |
585 |
586 | Mocha 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 |
LabelsHexRGBHSL
Rosewater#f5e0dcrgb(245, 224, 220)hsl(10, 56%, 91%)
Flamingo#f2cdcdrgb(242, 205, 205)hsl(0, 59%, 88%)
Pink#f5c2e7rgb(245, 194, 231)hsl(316, 72%, 86%)
Mauve#cba6f7rgb(203, 166, 247)hsl(267, 84%, 81%)
Red#f38ba8rgb(243, 139, 168)hsl(343, 81%, 75%)
Maroon#eba0acrgb(235, 160, 172)hsl(350, 65%, 78%)
Peach#fab387rgb(250, 179, 135)hsl(23, 92%, 76%)
Yellow#f9e2afrgb(249, 226, 175)hsl(41, 86%, 83%)
Green#a6e3a1rgb(166, 227, 161)hsl(115, 54%, 76%)
Teal#94e2d5rgb(148, 226, 213)hsl(170, 57%, 73%)
Sky#89dcebrgb(137, 220, 235)hsl(189, 71%, 73%)
Sapphire#74c7ecrgb(116, 199, 236)hsl(199, 76%, 69%)
Blue#89b4fargb(137, 180, 250)hsl(217, 92%, 76%)
Lavender#b4befergb(180, 190, 254)hsl(232, 97%, 85%)
Text#cdd6f4rgb(205, 214, 244)hsl(226, 64%, 88%)
Subtext 1#bac2dergb(186, 194, 222)hsl(227, 35%, 80%)
Subtext 0#a6adc8rgb(166, 173, 200)hsl(228, 24%, 72%)
Overlay 2#9399b2rgb(147, 153, 178)hsl(228, 17%, 64%)
Overlay 1#7f849crgb(127, 132, 156)hsl(230, 13%, 56%)
Overlay 0#6c7086rgb(108, 112, 134)hsl(231, 11%, 47%)
Surface 2#585b70rgb(88, 91, 112)hsl(233, 12%, 39%)
Surface 1#45475argb(69, 71, 90)hsl(234, 13%, 31%)
Surface 0#313244rgb(49, 50, 68)hsl(237, 16%, 23%)
Base#1e1e2ergb(30, 30, 46)hsl(240, 21%, 15%)
Mantle#181825rgb(24, 24, 37)hsl(240, 21%, 12%)
Crust#11111brgb(17, 17, 27)hsl(240, 23%, 9%)
778 |
779 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "last-release-sha": "46f20d199d6c8424e4680c9133a4e7e57aab3d88", 4 | "packages": { 5 | ".": { 6 | "package-name": "", 7 | "release-type": "rust" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use clap::Parser; 7 | use clap_stdin::FileOrStdin; 8 | 9 | type ValueMap = HashMap; 10 | 11 | #[derive(Parser, Debug)] 12 | #[command(version, about)] 13 | #[allow(clippy::struct_excessive_bools)] // not a problem for cli flags 14 | pub struct Args { 15 | /// Path to the template file, or - for stdin 16 | #[arg(required_unless_present_any = ["list_functions", "list_flavors", "list_accents"])] 17 | pub template: Option, 18 | 19 | /// Render a single flavor instead of all four 20 | #[arg(long, short)] 21 | pub flavor: Option, 22 | 23 | /// Set color overrides 24 | #[arg(long, value_parser = json_map::)] 25 | pub color_overrides: Option, 26 | 27 | /// Set frontmatter overrides 28 | #[arg(long, value_parser = json_map::)] 29 | pub overrides: Option, 30 | 31 | /// Instead of creating an output, check it against an example 32 | /// 33 | /// In single-output mode, a path to the example file must be provided. 34 | /// In multi-output mode, no path is required and, if one is provided, it 35 | /// will be ignored. 36 | #[arg(long, value_name = "EXAMPLE_PATH")] 37 | pub check: Option>, 38 | 39 | /// Dry run, don't write anything to disk 40 | #[arg(long)] 41 | pub dry_run: bool, 42 | 43 | /// List all Tera filters and functions 44 | #[arg(long)] 45 | pub list_functions: bool, 46 | 47 | /// List the Catppuccin flavors 48 | #[arg(long)] 49 | pub list_flavors: bool, 50 | 51 | /// List the Catppuccin accent colors 52 | #[arg(long)] 53 | pub list_accents: bool, 54 | 55 | /// Output format of --list-functions 56 | #[arg(short, long, default_value = "json")] 57 | pub output_format: OutputFormat, 58 | } 59 | 60 | #[derive(Debug, thiserror::Error)] 61 | enum Error { 62 | #[error("Invalid JSON literal argument: {message}")] 63 | InvalidJsonLiteralArg { message: String }, 64 | 65 | #[error("Invalid JSON file argument: {message}")] 66 | InvalidJsonFileArg { message: String }, 67 | 68 | #[error("Failed to read file: {path}")] 69 | ReadFile { 70 | path: String, 71 | #[source] 72 | source: std::io::Error, 73 | }, 74 | } 75 | 76 | #[derive(Copy, Clone, Debug, clap::ValueEnum)] 77 | pub enum Flavor { 78 | Latte, 79 | Frappe, 80 | Macchiato, 81 | Mocha, 82 | } 83 | 84 | impl From for catppuccin::FlavorName { 85 | fn from(val: Flavor) -> Self { 86 | match val { 87 | Flavor::Latte => Self::Latte, 88 | Flavor::Frappe => Self::Frappe, 89 | Flavor::Macchiato => Self::Macchiato, 90 | Flavor::Mocha => Self::Mocha, 91 | } 92 | } 93 | } 94 | 95 | #[derive(Clone, Debug, serde::Deserialize)] 96 | pub struct ColorOverrides { 97 | #[serde(default)] 98 | pub all: HashMap, 99 | #[serde(default)] 100 | pub latte: HashMap, 101 | #[serde(default)] 102 | pub frappe: HashMap, 103 | #[serde(default)] 104 | pub macchiato: HashMap, 105 | #[serde(default)] 106 | pub mocha: HashMap, 107 | } 108 | 109 | #[derive(Clone, Copy, Debug, clap::ValueEnum)] 110 | pub enum OutputFormat { 111 | Json, 112 | Yaml, 113 | Markdown, 114 | Plain, 115 | 116 | /// Deprecated, now equivalent to `Markdown` 117 | #[clap(hide = true)] 118 | MarkdownTable, 119 | } 120 | 121 | fn json_map(s: &str) -> Result 122 | where 123 | T: serde::de::DeserializeOwned, 124 | { 125 | if Path::new(s).is_file() { 126 | let s = std::fs::read_to_string(s).map_err(|e| Error::ReadFile { 127 | path: s.to_string(), 128 | source: e, 129 | })?; 130 | serde_json::from_str(&s).map_err(|e| Error::InvalidJsonFileArg { 131 | message: e.to_string(), 132 | }) 133 | } else { 134 | serde_json::from_str(s).map_err(|e| Error::InvalidJsonLiteralArg { 135 | message: e.to_string(), 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | /// Recursively merge two tera values into one. 2 | #[must_use] 3 | pub fn merge_values(a: &tera::Value, b: &tera::Value) -> tera::Value { 4 | match (a, b) { 5 | // if both are objects, merge them 6 | (tera::Value::Object(a), tera::Value::Object(b)) => { 7 | let mut result = a.clone(); 8 | for (k, v) in b { 9 | result.insert( 10 | k.clone(), 11 | merge_values(a.get(k).unwrap_or(&tera::Value::Null), v), 12 | ); 13 | } 14 | tera::Value::Object(result) 15 | } 16 | // otherwise, use the second value 17 | (_, b) => b.clone(), 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use serde_json::json; 24 | 25 | use super::*; 26 | 27 | #[test] 28 | fn test_merge_values() { 29 | let a = tera::to_value(&json!({ 30 | "a": 1, 31 | "b": { 32 | "c": 2, 33 | "d": 3, 34 | }, 35 | })) 36 | .expect("test value is always valid"); 37 | let b = tera::to_value(&json!({ 38 | "b": { 39 | "c": 4, 40 | "e": 5, 41 | }, 42 | "f": 6, 43 | })) 44 | .expect("test value is always valid"); 45 | let result = merge_values(&a, &b); 46 | assert_eq!( 47 | result, 48 | tera::to_value(&json!({ 49 | "a": 1, 50 | "b": { 51 | "c": 4, 52 | "d": 3, 53 | "e": 5, 54 | }, 55 | "f": 6, 56 | })) 57 | .expect("test value is always valid") 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/filters.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | io::Write, 4 | }; 5 | 6 | use base64::Engine as _; 7 | 8 | use crate::models::Color; 9 | 10 | pub fn mix( 11 | value: &tera::Value, 12 | args: &HashMap, 13 | ) -> Result { 14 | let base: Color = tera::from_value(value.clone())?; 15 | let blend: Color = tera::from_value( 16 | args.get("color") 17 | .ok_or_else(|| tera::Error::msg("blend color is required"))? 18 | .clone(), 19 | )?; 20 | let amount = args 21 | .get("amount") 22 | .ok_or_else(|| tera::Error::msg("blend amount is required"))? 23 | .as_f64() 24 | .ok_or_else(|| tera::Error::msg("blend amount must be a number"))?; 25 | 26 | let result = Color::mix(&base, &blend, amount)?; 27 | 28 | Ok(tera::to_value(result)?) 29 | } 30 | 31 | pub fn modify( 32 | value: &tera::Value, 33 | args: &HashMap, 34 | ) -> Result { 35 | let color: Color = tera::from_value(value.clone())?; 36 | if let Some(hue) = args.get("hue") { 37 | let hue = tera::from_value(hue.clone())?; 38 | Ok(tera::to_value(color.mod_hue(hue)?)?) 39 | } else if let Some(saturation) = args.get("saturation") { 40 | let saturation = tera::from_value(saturation.clone())?; 41 | Ok(tera::to_value(color.mod_saturation(saturation)?)?) 42 | } else if let Some(lightness) = args.get("lightness") { 43 | let lightness = tera::from_value(lightness.clone())?; 44 | Ok(tera::to_value(color.mod_lightness(lightness)?)?) 45 | } else if let Some(opacity) = args.get("opacity") { 46 | let opacity = tera::from_value(opacity.clone())?; 47 | Ok(tera::to_value(color.mod_opacity(opacity)?)?) 48 | } else { 49 | Ok(value.clone()) 50 | } 51 | } 52 | 53 | pub fn add( 54 | value: &tera::Value, 55 | args: &HashMap, 56 | ) -> Result { 57 | let color: Color = tera::from_value(value.clone())?; 58 | if let Some(hue) = args.get("hue") { 59 | let hue = tera::from_value(hue.clone())?; 60 | Ok(tera::to_value(color.add_hue(hue)?)?) 61 | } else if let Some(saturation) = args.get("saturation") { 62 | let saturation = tera::from_value(saturation.clone())?; 63 | Ok(tera::to_value(color.add_saturation(saturation)?)?) 64 | } else if let Some(lightness) = args.get("lightness") { 65 | let lightness = tera::from_value(lightness.clone())?; 66 | Ok(tera::to_value(color.add_lightness(lightness)?)?) 67 | } else if let Some(opacity) = args.get("opacity") { 68 | let opacity = tera::from_value(opacity.clone())?; 69 | Ok(tera::to_value(color.add_opacity(opacity)?)?) 70 | } else { 71 | Ok(value.clone()) 72 | } 73 | } 74 | 75 | pub fn sub( 76 | value: &tera::Value, 77 | args: &HashMap, 78 | ) -> Result { 79 | let color: Color = tera::from_value(value.clone())?; 80 | if let Some(hue) = args.get("hue") { 81 | let hue = tera::from_value(hue.clone())?; 82 | Ok(tera::to_value(color.sub_hue(hue)?)?) 83 | } else if let Some(saturation) = args.get("saturation") { 84 | let saturation = tera::from_value(saturation.clone())?; 85 | Ok(tera::to_value(color.sub_saturation(saturation)?)?) 86 | } else if let Some(lightness) = args.get("lightness") { 87 | let lightness = tera::from_value(lightness.clone())?; 88 | Ok(tera::to_value(color.sub_lightness(lightness)?)?) 89 | } else if let Some(opacity) = args.get("opacity") { 90 | let opacity = tera::from_value(opacity.clone())?; 91 | Ok(tera::to_value(color.sub_opacity(opacity)?)?) 92 | } else { 93 | Ok(value.clone()) 94 | } 95 | } 96 | 97 | pub fn urlencode_lzma( 98 | value: &tera::Value, 99 | _args: &HashMap, 100 | ) -> Result { 101 | // encode the data with the following process: 102 | // 1. messagepack the data 103 | // 2. compress the messagepacked data with lzma (v1, preset 9) 104 | // 3. urlsafe base64 encode the compressed data 105 | let value: BTreeMap = tera::from_value(value.clone())?; 106 | let packed = rmp_serde::to_vec(&value).map_err(|e| tera::Error::msg(e.to_string()))?; 107 | let mut options = lzma_rust::LZMA2Options::with_preset(9); 108 | options.dict_size = lzma_rust::LZMA2Options::DICT_SIZE_DEFAULT; 109 | let mut compressed = Vec::new(); 110 | let mut writer = lzma_rust::LZMAWriter::new( 111 | lzma_rust::CountingWriter::new(&mut compressed), 112 | &options, 113 | true, 114 | false, 115 | Some(packed.len() as u64), 116 | )?; 117 | writer.write_all(&packed)?; 118 | let _ = writer.write(&[])?; 119 | let encoded = base64::engine::general_purpose::URL_SAFE.encode(compressed); 120 | Ok(tera::to_value(encoded)?) 121 | } 122 | 123 | pub fn trunc( 124 | value: &tera::Value, 125 | args: &HashMap, 126 | ) -> Result { 127 | let value: f64 = tera::from_value(value.clone())?; 128 | let places: usize = tera::from_value( 129 | args.get("places") 130 | .ok_or_else(|| tera::Error::msg("number of places is required"))? 131 | .clone(), 132 | )?; 133 | Ok(tera::to_value(format!("{value:.places$}"))?) 134 | } 135 | 136 | pub fn css_rgb( 137 | value: &tera::Value, 138 | _args: &HashMap, 139 | ) -> Result { 140 | let color: Color = tera::from_value(value.clone())?; 141 | let color: css_colors::RGB = (&color).into(); 142 | Ok(tera::to_value(color.to_string())?) 143 | } 144 | 145 | pub fn css_rgba( 146 | value: &tera::Value, 147 | _args: &HashMap, 148 | ) -> Result { 149 | let color: Color = tera::from_value(value.clone())?; 150 | let color: css_colors::RGBA = (&color).into(); 151 | Ok(tera::to_value(color.to_string())?) 152 | } 153 | 154 | pub fn css_hsl( 155 | value: &tera::Value, 156 | _args: &HashMap, 157 | ) -> Result { 158 | let color: Color = tera::from_value(value.clone())?; 159 | let color: css_colors::HSL = (&color).into(); 160 | Ok(tera::to_value(color.to_string())?) 161 | } 162 | 163 | pub fn css_hsla( 164 | value: &tera::Value, 165 | _args: &HashMap, 166 | ) -> Result { 167 | let color: Color = tera::from_value(value.clone())?; 168 | let color: css_colors::HSLA = (&color).into(); 169 | Ok(tera::to_value(color.to_string())?) 170 | } 171 | -------------------------------------------------------------------------------- /src/frontmatter.rs: -------------------------------------------------------------------------------- 1 | use detect_newline_style::LineEnding; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug)] 5 | pub struct Document { 6 | pub frontmatter: HashMap, 7 | pub body: String, 8 | } 9 | 10 | #[derive(Debug, thiserror::Error)] 11 | pub enum Error { 12 | #[error("Invalid YAML frontmatter (L{line}:{column}) : {message}")] 13 | InvalidYaml { 14 | line: usize, 15 | column: usize, 16 | message: String, 17 | }, 18 | } 19 | 20 | pub fn parse(input: &str) -> Result { 21 | let Some((frontmatter, body)) = split(input) else { 22 | // no frontmatter to parse 23 | return Ok(Document { 24 | frontmatter: HashMap::new(), 25 | body: input.to_string(), 26 | }); 27 | }; 28 | 29 | Ok(Document { 30 | frontmatter: serde_yaml::from_str(frontmatter).map_err(|e| Error::InvalidYaml { 31 | line: e.location().map(|l| l.line()).unwrap_or_default(), 32 | column: e.location().map(|l| l.column()).unwrap_or_default(), 33 | message: e.to_string(), 34 | })?, 35 | body: body.to_string(), 36 | }) 37 | } 38 | 39 | fn split(template: &str) -> Option<(&str, &str)> { 40 | // we consider a template to possibly have frontmatter iff: 41 | // * line 0 is "---" 42 | // * there is another "---" on another line 43 | let template = template.trim_start(); 44 | let eol = LineEnding::find(template, LineEnding::LF).to_string(); 45 | let sep = "---".to_string() + &eol; 46 | if !template.starts_with(&sep) { 47 | return None; 48 | } 49 | 50 | template[sep.len()..] 51 | .split_once(&sep) 52 | .map(|(a, b)| (a.trim(), b)) 53 | } 54 | -------------------------------------------------------------------------------- /src/functions.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, HashMap}, 3 | fs, 4 | path::PathBuf, 5 | }; 6 | 7 | use crate::models::Color; 8 | 9 | pub fn if_fn(args: &HashMap) -> Result { 10 | let cond = args 11 | .get("cond") 12 | .ok_or_else(|| tera::Error::msg("cond is required"))? 13 | .as_bool() 14 | .ok_or_else(|| tera::Error::msg("cond must be a boolean"))?; 15 | let t = args 16 | .get("t") 17 | .ok_or_else(|| tera::Error::msg("t is required"))? 18 | .clone(); 19 | let f = args 20 | .get("f") 21 | .ok_or_else(|| tera::Error::msg("f is required"))? 22 | .clone(); 23 | 24 | Ok(if cond { t } else { f }) 25 | } 26 | 27 | pub fn object(args: &HashMap) -> Result { 28 | // sorting the args gives us stable output 29 | let args: BTreeMap<_, _> = args.iter().collect(); 30 | Ok(tera::to_value(args)?) 31 | } 32 | 33 | pub fn css_rgb(args: &HashMap) -> Result { 34 | let color: Color = tera::from_value( 35 | args.get("color") 36 | .ok_or_else(|| tera::Error::msg("color is required"))? 37 | .clone(), 38 | )?; 39 | 40 | let color: css_colors::RGB = (&color).into(); 41 | Ok(tera::to_value(color.to_string())?) 42 | } 43 | 44 | pub fn css_rgba(args: &HashMap) -> Result { 45 | let color: Color = tera::from_value( 46 | args.get("color") 47 | .ok_or_else(|| tera::Error::msg("color is required"))? 48 | .clone(), 49 | )?; 50 | let color: css_colors::RGBA = (&color).into(); 51 | Ok(tera::to_value(color.to_string())?) 52 | } 53 | 54 | pub fn css_hsl(args: &HashMap) -> Result { 55 | let color: Color = tera::from_value( 56 | args.get("color") 57 | .ok_or_else(|| tera::Error::msg("color is required"))? 58 | .clone(), 59 | )?; 60 | 61 | let color: css_colors::HSL = (&color).into(); 62 | Ok(tera::to_value(color.to_string())?) 63 | } 64 | 65 | pub fn css_hsla(args: &HashMap) -> Result { 66 | let color: Color = tera::from_value( 67 | args.get("color") 68 | .ok_or_else(|| tera::Error::msg("color is required"))? 69 | .clone(), 70 | )?; 71 | let color: css_colors::HSLA = (&color).into(); 72 | Ok(tera::to_value(color.to_string())?) 73 | } 74 | 75 | pub fn read_file_handler( 76 | template_directory: PathBuf, 77 | ) -> impl Fn(&HashMap) -> Result { 78 | move |args| -> Result { 79 | let path: String = tera::from_value( 80 | args.get("path") 81 | .ok_or_else(|| tera::Error::msg("path is required"))? 82 | .clone(), 83 | )?; 84 | let path = template_directory.join(path); 85 | let contents = fs::read_to_string(&path) 86 | .map_err(|_| format!("Failed to open file {}", path.display()))?; 87 | Ok(tera::to_value(contents)?) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod context; 3 | pub mod filters; 4 | pub mod frontmatter; 5 | pub mod functions; 6 | pub mod markdown; 7 | pub mod matrix; 8 | pub mod models; 9 | pub mod templating; 10 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_map::Entry, HashMap}, 3 | env, 4 | io::{Read, Write as _}, 5 | path::{Path, PathBuf}, 6 | process::{self, exit}, 7 | }; 8 | 9 | use anyhow::{anyhow, Context as _}; 10 | use catppuccin::FlavorName; 11 | use clap::Parser as _; 12 | use encoding_rs_io::DecodeReaderBytes; 13 | use itertools::Itertools; 14 | use whiskers::{ 15 | cli::{Args, OutputFormat}, 16 | context::merge_values, 17 | frontmatter, markdown, 18 | matrix::{self, Matrix}, 19 | models::{self, HEX_FORMAT}, 20 | templating, 21 | }; 22 | 23 | const FRONTMATTER_OPTIONS_SECTION: &str = "whiskers"; 24 | 25 | fn default_hex_format() -> String { 26 | "{{r}}{{g}}{{b}}{{z}}".to_string() 27 | } 28 | 29 | #[derive(Default, Debug, serde::Deserialize)] 30 | struct TemplateOptions { 31 | version: Option<(semver::VersionReq, String)>, 32 | matrix: Option, 33 | filename: Option, 34 | hex_format: String, 35 | } 36 | 37 | impl TemplateOptions { 38 | fn from_frontmatter( 39 | frontmatter: &HashMap, 40 | only_flavor: Option, 41 | ) -> anyhow::Result { 42 | // a `TemplateOptions` object before matrix transformation 43 | #[derive(serde::Deserialize)] 44 | struct RawTemplateOptions { 45 | version: Option, 46 | matrix: Option>, 47 | filename: Option, 48 | hex_format: Option, 49 | hex_prefix: Option, 50 | #[serde(default)] 51 | capitalize_hex: bool, 52 | } 53 | 54 | if let Some(opts_section) = frontmatter.get(FRONTMATTER_OPTIONS_SECTION) { 55 | let raw_opts: RawTemplateOptions = tera::from_value(opts_section.clone()) 56 | .context("Frontmatter `whiskers` section is invalid")?; 57 | 58 | let matrix = raw_opts 59 | .matrix 60 | .map(|m| matrix::from_values(m, only_flavor)) 61 | .transpose() 62 | .context("Frontmatter matrix is invalid")?; 63 | 64 | // if there's no hex_format but there is hex_prefix and/or capitalize_hex, 65 | // we can construct a hex_format from those. 66 | let hex_format = if let Some(hex_format) = raw_opts.hex_format { 67 | hex_format 68 | } else { 69 | // throw a deprecation warning for hex_prefix and capitalize_hex 70 | if raw_opts.hex_prefix.is_some() { 71 | eprintln!("warning: `hex_prefix` is deprecated and will be removed in a future version. Use `hex_format` instead."); 72 | } 73 | 74 | if raw_opts.capitalize_hex { 75 | eprintln!("warning: `capitalize_hex` is deprecated and will be removed in a future version. Use `hex_format` instead."); 76 | } 77 | 78 | let prefix = raw_opts.hex_prefix.unwrap_or_default(); 79 | let components = default_hex_format(); 80 | if raw_opts.capitalize_hex { 81 | format!("{prefix}{}", components.to_uppercase()) 82 | } else { 83 | format!("{prefix}{components}") 84 | } 85 | }; 86 | 87 | Ok(Self { 88 | version: raw_opts.version.map(|version| { 89 | ( 90 | version, 91 | opts_section["version"].as_str().map(String::from).expect( 92 | "version string is guaranteed to be Some if `raw_opts.version` is Some", 93 | ), 94 | ) 95 | }), 96 | matrix, 97 | filename: raw_opts.filename, 98 | hex_format, 99 | }) 100 | } else { 101 | Ok(Self { 102 | hex_format: default_hex_format(), 103 | ..Default::default() 104 | }) 105 | } 106 | } 107 | } 108 | 109 | fn main() -> anyhow::Result<()> { 110 | // parse command-line arguments & template frontmatter 111 | let args = Args::parse(); 112 | handle_list_flags(&args); 113 | 114 | let template_arg = args 115 | .template 116 | .clone() 117 | .expect("args.template is guaranteed by clap to be set"); 118 | let template_from_stdin = template_arg.is_stdin(); 119 | let template_name = template_name(&template_arg); 120 | let template_directory = 121 | template_directory(&template_arg).context("Template file does not exist")?; 122 | 123 | let mut decoder = DecodeReaderBytes::new( 124 | template_arg 125 | .into_reader() 126 | .context("Failed to open template file")?, 127 | ); 128 | let mut template = String::new(); 129 | decoder 130 | .read_to_string(&mut template) 131 | .context("Template could not be read")?; 132 | 133 | let doc = frontmatter::parse(&template).context("Frontmatter is invalid")?; 134 | let mut template_opts = 135 | TemplateOptions::from_frontmatter(&doc.frontmatter, args.flavor.map(Into::into)) 136 | .context("Could not get template options from frontmatter")?; 137 | 138 | if !template_from_stdin && !template_is_compatible(&template_opts) { 139 | std::process::exit(1); 140 | } 141 | 142 | // merge frontmatter with command-line overrides and add to Tera context 143 | let mut frontmatter = doc.frontmatter; 144 | if let Some(ref overrides) = args.overrides { 145 | for (key, value) in overrides { 146 | frontmatter 147 | .entry(key.clone()) 148 | .and_modify(|v| { 149 | *v = merge_values(v, value); 150 | }) 151 | .or_insert( 152 | tera::to_value(value) 153 | .with_context(|| format!("Value of {key} override is invalid"))?, 154 | ); 155 | 156 | // overrides also work on matrix iterables 157 | if let Some(ref mut matrix) = template_opts.matrix { 158 | override_matrix(matrix, value, key)?; 159 | } 160 | } 161 | } 162 | let mut ctx = tera::Context::new(); 163 | for (key, value) in &frontmatter { 164 | ctx.insert(key, &value); 165 | } 166 | 167 | HEX_FORMAT 168 | .set(template_opts.hex_format) 169 | .expect("can always set HEX_FORMAT"); 170 | 171 | // build the palette and add it to the templating context 172 | let palette = models::build_palette(args.color_overrides.as_ref()) 173 | .context("Palette context cannot be built")?; 174 | 175 | ctx.insert("flavors", &palette.flavors); 176 | if let Some(flavor) = args.flavor { 177 | let flavor: catppuccin::FlavorName = flavor.into(); 178 | let flavor = &palette.flavors[flavor.identifier()]; 179 | ctx.insert("flavor", flavor); 180 | 181 | // also throw in the flavor's colors for convenience 182 | for (_, color) in flavor { 183 | ctx.insert(&color.identifier, &color); 184 | } 185 | } 186 | 187 | // build the Tera engine 188 | let mut tera = templating::make_engine(&template_directory); 189 | tera.add_raw_template(&template_name, &doc.body) 190 | .context("Template is invalid")?; 191 | 192 | if let Some(matrix) = template_opts.matrix { 193 | let Some(filename_template) = template_opts.filename else { 194 | anyhow::bail!("Filename template is required for multi-output render"); 195 | }; 196 | render_multi_output( 197 | matrix, 198 | &filename_template, 199 | &ctx, 200 | &palette, 201 | &tera, 202 | &template_name, 203 | &args, 204 | ) 205 | .context("Multi-output render failed")?; 206 | } else { 207 | let check = args 208 | .check 209 | .map(|c| { 210 | c.ok_or_else(|| anyhow!("--check requires a file argument in single-output mode")) 211 | }) 212 | .transpose()?; 213 | 214 | render_single_output( 215 | &ctx, 216 | &tera, 217 | &template_name, 218 | check, 219 | template_opts.filename, 220 | args.dry_run, 221 | ) 222 | .context("Single-output render failed")?; 223 | } 224 | 225 | Ok(()) 226 | } 227 | 228 | fn handle_list_flags(args: &Args) { 229 | if args.list_functions { 230 | list_functions(args.output_format); 231 | exit(0); 232 | } 233 | 234 | if args.list_flavors { 235 | list_flavors(args.output_format); 236 | exit(0); 237 | } 238 | 239 | if args.list_accents { 240 | list_accents(args.output_format); 241 | exit(0); 242 | } 243 | } 244 | 245 | fn override_matrix( 246 | matrix: &mut Matrix, 247 | value: &tera::Value, 248 | key: &str, 249 | ) -> Result<(), anyhow::Error> { 250 | let Entry::Occupied(e) = matrix.entry(key.to_string()) else { 251 | return Ok(()); 252 | }; 253 | 254 | // if the override is a list, we can just replace the iterable. 255 | if let Some(value_list) = value.as_array() { 256 | let value_list = value_list 257 | .iter() 258 | .map(|v| v.as_str().map(ToString::to_string)) 259 | .collect::>>() 260 | .context("Override value is not a list of strings")?; 261 | *e.into_mut() = value_list; 262 | } 263 | // if the override is a string, we instead replace the iterable with a 264 | // single-element list containing the string. 265 | else if let Some(value_string) = value.as_str() { 266 | *e.into_mut() = vec![value_string.to_string()]; 267 | } 268 | 269 | Ok(()) 270 | } 271 | 272 | fn list_functions(format: OutputFormat) { 273 | let functions = templating::all_functions(); 274 | let filters = templating::all_filters(); 275 | println!( 276 | "{}", 277 | match format { 278 | OutputFormat::Json | OutputFormat::Yaml => { 279 | let output = serde_json::json!({ 280 | "functions": functions, 281 | "filters": filters, 282 | }); 283 | 284 | if matches!(format, OutputFormat::Json) { 285 | serde_json::to_string_pretty(&output).expect("output is guaranteed to be valid") 286 | } else { 287 | serde_yaml::to_string(&output).expect("output is guaranteed to be valid") 288 | } 289 | } 290 | OutputFormat::Markdown | OutputFormat::MarkdownTable => { 291 | format!( 292 | "{}\n\n{}", 293 | markdown::display_as_table(&functions, "Functions"), 294 | markdown::display_as_table(&filters, "Filters") 295 | ) 296 | } 297 | OutputFormat::Plain => { 298 | let mut list = filters 299 | .iter() 300 | .map(|f| f.name.clone()) 301 | .collect::>(); 302 | 303 | list.extend(functions.iter().map(|f| f.name.clone())); 304 | 305 | list.join("\n") 306 | } 307 | } 308 | ); 309 | } 310 | 311 | fn list_flavors(format: OutputFormat) { 312 | // we want all the flavor info minus the colors 313 | #[derive(serde::Serialize)] 314 | struct FlavorInfo { 315 | identifier: String, 316 | name: String, 317 | emoji: char, 318 | order: u32, 319 | dark: bool, 320 | } 321 | 322 | impl markdown::TableDisplay for FlavorInfo { 323 | fn table_headings() -> Box<[String]> { 324 | vec![ 325 | "Identifier".to_string(), 326 | "Name".to_string(), 327 | "Dark".to_string(), 328 | "Emoji".to_string(), 329 | ] 330 | .into_boxed_slice() 331 | } 332 | 333 | fn table_row(&self) -> Box<[String]> { 334 | vec![ 335 | self.identifier.clone(), 336 | self.name.clone(), 337 | self.dark.to_string(), 338 | self.emoji.to_string(), 339 | ] 340 | .into_boxed_slice() 341 | } 342 | } 343 | 344 | let flavors = catppuccin::PALETTE 345 | .all_flavors() 346 | .into_iter() 347 | .map(|f| FlavorInfo { 348 | identifier: f.identifier().to_string(), 349 | name: f.name.to_string(), 350 | emoji: f.emoji, 351 | order: f.order, 352 | dark: f.dark, 353 | }) 354 | .collect::>(); 355 | 356 | println!( 357 | "{}", 358 | match format { 359 | // for structured data, we output the full flavor info objects 360 | OutputFormat::Json | OutputFormat::Yaml => { 361 | if matches!(format, OutputFormat::Json) { 362 | serde_json::to_string_pretty(&flavors) 363 | .expect("flavors are guaranteed to be valid json") 364 | } else { 365 | serde_yaml::to_string(&flavors) 366 | .expect("flavors are guaranteed to be valid yaml") 367 | } 368 | } 369 | // for plain output, we just list the flavor identifiers 370 | OutputFormat::Plain => { 371 | flavors.iter().map(|f| &f.identifier).join("\n") 372 | } 373 | // and finally for human-readable markdown, we list the flavor names 374 | OutputFormat::Markdown | OutputFormat::MarkdownTable => { 375 | markdown::display_as_table(&flavors, "Flavors") 376 | } 377 | } 378 | ); 379 | } 380 | 381 | fn list_accents(format: OutputFormat) { 382 | let accents = catppuccin::PALETTE 383 | .latte 384 | .colors 385 | .all_colors() 386 | .into_iter() 387 | .filter(|c| c.accent) 388 | .collect::>(); 389 | 390 | println!( 391 | "{}", 392 | match format { 393 | // for structured data, we can include both name and identifier of each color 394 | OutputFormat::Json | OutputFormat::Yaml => { 395 | let accents = accents 396 | .into_iter() 397 | .map(|c| { 398 | serde_json::json!({ 399 | "name": c.name, 400 | "identifier": c.identifier(), 401 | }) 402 | }) 403 | .collect::>(); 404 | if matches!(format, OutputFormat::Json) { 405 | serde_json::to_string_pretty(&accents) 406 | .expect("accents are guaranteed to be valid json") 407 | } else { 408 | serde_yaml::to_string(&accents) 409 | .expect("accents are guaranteed to be valid yaml") 410 | } 411 | } 412 | // for plain output, we just list the identifiers 413 | OutputFormat::Plain => { 414 | accents 415 | .into_iter() 416 | .map(catppuccin::Color::identifier) 417 | .join("\n") 418 | } 419 | // and finally for human-readable markdown, we list the names 420 | OutputFormat::Markdown | OutputFormat::MarkdownTable => { 421 | markdown::display_as_list( 422 | &accents.into_iter().map(|c| c.name).collect::>(), 423 | "Accents", 424 | ) 425 | } 426 | } 427 | ); 428 | } 429 | 430 | fn template_name(template: &clap_stdin::FileOrStdin) -> String { 431 | if template.is_stdin() { 432 | "template".to_string() 433 | } else { 434 | Path::new(template.filename()).file_name().map_or_else( 435 | || "template".to_string(), 436 | |name| name.to_string_lossy().to_string(), 437 | ) 438 | } 439 | } 440 | 441 | fn template_directory(template: &clap_stdin::FileOrStdin) -> anyhow::Result { 442 | if template.is_stdin() { 443 | Ok(std::env::current_dir()?) 444 | } else { 445 | Ok(Path::new(template.filename()) 446 | .canonicalize()? 447 | .parent() 448 | .expect("file path must have a parent") 449 | .to_owned()) 450 | } 451 | } 452 | 453 | fn template_is_compatible(template_opts: &TemplateOptions) -> bool { 454 | let whiskers_version = semver::Version::parse(env!("CARGO_PKG_VERSION")) 455 | .expect("CARGO_PKG_VERSION is always valid"); 456 | if let Some((template_version, template_version_raw)) = &template_opts.version { 457 | // warn if the template is using an implicit constraint instead of an explicit one 458 | // i.e. `version: "2.5.1"` instead of `version: "^2.5.1"` 459 | if let &[comp] = &template_version.comparators.as_slice() { 460 | if comp.op == semver::Op::Caret && !template_version_raw.starts_with('^') { 461 | eprintln!("warning: Template specifies an implicit constraint of {template_version_raw}, consider explicitly specifying ^{template_version_raw} instead"); 462 | } 463 | } 464 | 465 | if !template_version.matches(&whiskers_version) { 466 | eprintln!( 467 | "error: This template requires a version of Whiskers compatible with \ 468 | \"{template_version}\", but you are running Whiskers \ 469 | {whiskers_version} which is not compatible with this \ 470 | requirement." 471 | ); 472 | return false; 473 | } 474 | } else { 475 | eprintln!("warning: No Whiskers version requirement specified in template."); 476 | eprintln!("This template may not be compatible with this version of Whiskers."); 477 | eprintln!(); 478 | eprintln!("To fix this, specify a Whiskers version requirement in the template frontmatter as follows:"); 479 | eprintln!(); 480 | eprintln!("---"); 481 | eprintln!("whiskers:"); 482 | eprintln!(" version: \"^{whiskers_version}\""); 483 | eprintln!("---"); 484 | eprintln!(); 485 | }; 486 | 487 | true 488 | } 489 | 490 | fn write_template(dry_run: bool, filename: &str, result: String) -> Result<(), anyhow::Error> { 491 | let filename = Path::new(&filename); 492 | 493 | if dry_run || cfg!(test) { 494 | println!( 495 | "Would write {} bytes into {}", 496 | result.len(), 497 | filename.display() 498 | ); 499 | } else { 500 | maybe_create_parents(filename)?; 501 | std::fs::write(filename, result) 502 | .with_context(|| format!("Couldn't write to {}", filename.display()))?; 503 | } 504 | 505 | Ok(()) 506 | } 507 | 508 | fn render_single_output( 509 | ctx: &tera::Context, 510 | tera: &tera::Tera, 511 | template_name: &str, 512 | check: Option, 513 | filename: Option, 514 | dry_run: bool, 515 | ) -> Result<(), anyhow::Error> { 516 | let result = tera 517 | .render(template_name, ctx) 518 | .context("Template render failed")?; 519 | 520 | if let Some(path) = check { 521 | if matches!( 522 | check_result_with_file(&path, &result).context("Check mode failed")?, 523 | CheckResult::Fail 524 | ) { 525 | std::process::exit(1); 526 | } 527 | } else if let Some(filename) = filename { 528 | write_template(dry_run, &filename, result)?; 529 | } else { 530 | print!("{result}"); 531 | } 532 | 533 | Ok(()) 534 | } 535 | 536 | fn render_multi_output( 537 | matrix: HashMap>, 538 | filename_template: &str, 539 | ctx: &tera::Context, 540 | palette: &models::Palette, 541 | tera: &tera::Tera, 542 | template_name: &str, 543 | args: &Args, 544 | ) -> Result<(), anyhow::Error> { 545 | let iterables = matrix 546 | .into_iter() 547 | .map(|(key, iterable)| iterable.into_iter().map(move |v| (key.clone(), v))) 548 | .multi_cartesian_product() 549 | .collect::>(); 550 | let mut check_results: Vec = Vec::with_capacity(iterables.len()); 551 | 552 | for iterable in iterables { 553 | let mut ctx = ctx.clone(); 554 | for (key, value) in iterable { 555 | // expand flavor automatically to prevent requiring: 556 | // `{% set flavor = flavors[flavor] %}` 557 | // at the top of every template. 558 | if key == "flavor" { 559 | let flavor: catppuccin::FlavorName = value.parse()?; 560 | let flavor = &palette.flavors[flavor.identifier()]; 561 | ctx.insert("flavor", flavor); 562 | 563 | // also throw in the flavor's colors for convenience 564 | for (_, color) in flavor { 565 | ctx.insert(&color.identifier, &color); 566 | } 567 | } else { 568 | ctx.insert(key, &value); 569 | } 570 | } 571 | let result = tera 572 | .render(template_name, &ctx) 573 | .context("Main template render failed")?; 574 | let filename = tera::Tera::one_off(filename_template, &ctx, false) 575 | .context("Filename template render failed")?; 576 | 577 | if args.check.is_some() { 578 | check_results 579 | .push(check_result_with_file(&filename, &result).context("Check mode failed")?); 580 | } else { 581 | write_template(args.dry_run, &filename, result)?; 582 | } 583 | } 584 | 585 | if check_results.iter().any(|r| matches!(r, CheckResult::Fail)) { 586 | std::process::exit(1); 587 | } 588 | 589 | Ok(()) 590 | } 591 | 592 | fn maybe_create_parents(filename: &Path) -> anyhow::Result<()> { 593 | if let Some(parent) = filename.parent() { 594 | std::fs::create_dir_all(parent).with_context(|| { 595 | format!( 596 | "Couldn't create parent directories for {}", 597 | filename.display() 598 | ) 599 | })?; 600 | }; 601 | Ok(()) 602 | } 603 | 604 | #[must_use] 605 | enum CheckResult { 606 | Pass, 607 | Fail, 608 | } 609 | 610 | fn check_result_with_file

(path: &P, result: &str) -> anyhow::Result 611 | where 612 | P: AsRef, 613 | { 614 | let path = path.as_ref(); 615 | let expected = std::fs::read_to_string(path).with_context(|| { 616 | format!( 617 | "error: Couldn't read {} for comparison against result", 618 | path.display() 619 | ) 620 | })?; 621 | if *result == expected { 622 | Ok(CheckResult::Pass) 623 | } else { 624 | eprintln!("error: Output does not match {}", path.display()); 625 | invoke_difftool(result, path)?; 626 | Ok(CheckResult::Fail) 627 | } 628 | } 629 | 630 | fn invoke_difftool

(actual: &str, expected_path: P) -> anyhow::Result<()> 631 | where 632 | P: AsRef, 633 | { 634 | let expected_path = expected_path.as_ref(); 635 | let tool = env::var("DIFFTOOL").unwrap_or_else(|_| "diff".to_string()); 636 | 637 | let mut actual_file = tempfile::NamedTempFile::new()?; 638 | write!(&mut actual_file, "{actual}")?; 639 | if let Ok(mut child) = process::Command::new(tool) 640 | .args([actual_file.path(), expected_path]) 641 | .spawn() 642 | { 643 | child.wait()?; 644 | } else { 645 | eprintln!("warning: Can't display diff, try setting $DIFFTOOL."); 646 | } 647 | 648 | Ok(()) 649 | } 650 | -------------------------------------------------------------------------------- /src/markdown.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use itertools::Itertools as _; 4 | 5 | pub trait TableDisplay { 6 | fn table_headings() -> Box<[String]>; 7 | fn table_row(&self) -> Box<[String]>; 8 | } 9 | 10 | pub fn display_as_list(items: &[T], heading: &str) -> String { 11 | let items = items.iter().map(|item| format!("* {item}")).join("\n"); 12 | format!("### {heading}\n\n{items}") 13 | } 14 | 15 | pub fn display_as_table(items: &[T], heading: &str) -> String { 16 | let mut result = String::new(); 17 | let rows = items.iter().map(T::table_row).collect::>(); 18 | 19 | // calculate a max width for each heading based on the longest row in the column 20 | let headings = T::table_headings(); 21 | let headings = headings 22 | .iter() 23 | .enumerate() 24 | .map(|(i, heading)| { 25 | let max_width = rows 26 | .iter() 27 | .map(|row| row[i].len()) 28 | .chain(std::iter::once(heading.len())) 29 | .max() 30 | .unwrap_or(heading.len()); 31 | (heading, max_width) 32 | }) 33 | .collect::>(); 34 | 35 | // add the section heading 36 | result.push_str(&format!("### {heading}\n\n")); 37 | 38 | // add the table headings 39 | result.push_str(&format!( 40 | "| {} |\n", 41 | headings 42 | .iter() 43 | .map(|(heading, max_width)| format!("{heading:>; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum Error { 9 | #[error("Unknown magic iterable: {name}")] 10 | UnknownIterable { name: String }, 11 | 12 | #[error("Invalid matrix array object element: must have a single key and an array of strings as value")] 13 | InvalidObjectElement, 14 | 15 | #[error("Invalid matrix array element: must be a string or object")] 16 | InvalidElement, 17 | } 18 | 19 | // matrix in frontmatter is a list of strings or objects. 20 | // objects must have a single key and an array of strings as the value. 21 | // string array elements are substituted with the array from `iterables`. 22 | pub fn from_values( 23 | values: Vec, 24 | only_flavor: Option, 25 | ) -> Result { 26 | let iterables = magic_iterables(only_flavor); 27 | values 28 | .into_iter() 29 | .map(|v| match v { 30 | tera::Value::String(s) => { 31 | let iterable = iterables 32 | .get(s.as_str()) 33 | .ok_or_else(|| Error::UnknownIterable { name: s.clone() })?; 34 | Ok((s, iterable.clone())) 35 | } 36 | tera::Value::Object(o) => { 37 | let (key, value) = o.into_iter().next().ok_or(Error::InvalidObjectElement)?; 38 | let value: Vec = 39 | tera::from_value(value).map_err(|_| Error::InvalidObjectElement)?; 40 | Ok((key, value)) 41 | } 42 | _ => Err(Error::InvalidElement), 43 | }) 44 | .collect::>() 45 | } 46 | 47 | fn magic_iterables(only_flavor: Option) -> HashMap<&'static str, Vec> { 48 | HashMap::from([ 49 | ( 50 | "flavor", 51 | only_flavor.map_or_else( 52 | || { 53 | catppuccin::PALETTE 54 | .into_iter() 55 | .map(|flavor| flavor.identifier().to_string()) 56 | .collect::>() 57 | }, 58 | |flavor| vec![flavor.identifier().to_string()], 59 | ), 60 | ), 61 | ("accent", ctp_accents()), 62 | ]) 63 | } 64 | 65 | fn ctp_accents() -> Vec { 66 | catppuccin::PALETTE 67 | .latte 68 | .colors 69 | .iter() 70 | .filter(|c| c.accent) 71 | .map(|c| c.name.identifier().to_string()) 72 | .collect() 73 | } 74 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use css_colors::Color as _; 4 | use indexmap::IndexMap; 5 | use serde_json::json; 6 | use tera::Tera; 7 | 8 | use crate::cli::ColorOverrides; 9 | 10 | // a frankenstein mix of Catppuccin & css_colors types to get all the 11 | // functionality we want. 12 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] 13 | pub struct Palette { 14 | pub flavors: IndexMap, 15 | } 16 | 17 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] 18 | pub struct Flavor { 19 | pub name: String, 20 | pub identifier: String, 21 | pub emoji: char, 22 | pub order: u32, 23 | pub dark: bool, 24 | pub light: bool, 25 | pub colors: IndexMap, 26 | } 27 | 28 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] 29 | pub struct Color { 30 | pub name: String, 31 | pub identifier: String, 32 | pub order: u32, 33 | pub accent: bool, 34 | pub hex: String, 35 | pub int24: u32, 36 | pub uint32: u32, 37 | pub sint32: i32, 38 | pub rgb: RGB, 39 | pub hsl: HSL, 40 | pub opacity: u8, 41 | } 42 | 43 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] 44 | pub struct RGB { 45 | pub r: u8, 46 | pub g: u8, 47 | pub b: u8, 48 | } 49 | 50 | #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] 51 | pub struct HSL { 52 | pub h: u16, 53 | pub s: f32, 54 | pub l: f32, 55 | } 56 | 57 | #[derive(Debug, thiserror::Error)] 58 | pub enum Error { 59 | #[error("Hex formatting failed: {0}")] 60 | HexFormat(#[from] tera::Error), 61 | #[error("Failed to parse hex color: {0}")] 62 | ParseHex(#[from] std::num::ParseIntError), 63 | } 64 | 65 | // we have many functions that need to know how to format hex colors. 66 | // they can't know this at build time, as the format may be provided by the template. 67 | // we have little to no available state in many of these functions to store this information. 68 | // these possible solutions were evaluated: 69 | // 1. pass the format string to every function that needs it. this is cumbersome and error-prone. 70 | // 2. store the format string in the `Colour` struct, thus duplicating it for every color. this is wasteful. 71 | // 3. store it in a global static, and initialize it when the template frontmatter is read. 72 | // we opted for the third option, with a convenience macro for accessing it. 73 | pub static HEX_FORMAT: OnceLock = OnceLock::new(); 74 | macro_rules! format_hex { 75 | ($r:expr, $g:expr, $b: expr, $a: expr) => { 76 | format_hex( 77 | $r, 78 | $g, 79 | $b, 80 | $a, 81 | &*HEX_FORMAT.get().expect("HEX_FORMAT was never set"), 82 | ) 83 | }; 84 | } 85 | 86 | /// attempt to canonicalize a hex string, using the provided format string. 87 | fn format_hex(r: u8, g: u8, b: u8, a: u8, hex_format: &str) -> tera::Result { 88 | Tera::one_off( 89 | hex_format, 90 | &tera::Context::from_serialize(json!({ 91 | "r": format!("{r:02x}"), 92 | "g": format!("{g:02x}"), 93 | "b": format!("{b:02x}"), 94 | "a": format!("{a:02x}"), 95 | "z": if a == 0xFF { String::new() } else { format!("{a:02x}") }, 96 | "R": format!("{r:02X}"), 97 | "G": format!("{g:02X}"), 98 | "B": format!("{b:02X}"), 99 | "A": format!("{a:02X}"), 100 | "Z": if a == 0xFF { String::new() } else { format!("{a:02X}") }, 101 | })) 102 | .expect("hardcoded context is always valid"), 103 | true, 104 | ) 105 | } 106 | 107 | /// produce three values from a given rgb value and opacity: 108 | /// 1. a 24-bit unsigned integer with the format `0xRRGGBB` 109 | /// 2. a 32-bit unsigned integer with the format `0xAARRGGBB` 110 | /// 3. a 32-bit signed integer with the format `0xAARRGGBB` 111 | /// 112 | /// opacity is optional, and defaults to `0xFF`. 113 | fn rgb_to_ints(rgb: &RGB, opacity: Option) -> (u32, u32, i32) { 114 | let opacity = opacity.unwrap_or(0xFF); 115 | let uint24 = u32::from_be_bytes([0x00, rgb.r, rgb.g, rgb.b]); 116 | let uint32 = u32::from_be_bytes([opacity, rgb.r, rgb.g, rgb.b]); 117 | #[allow(clippy::cast_possible_wrap)] 118 | (uint24, uint32, uint32 as i32) 119 | } 120 | 121 | fn color_from_hex_override(hex: &str, blueprint: &catppuccin::Color) -> Result { 122 | let i = u32::from_str_radix(hex, 16)?; 123 | let rgb = RGB { 124 | r: ((i >> 16) & 0xFF) as u8, 125 | g: ((i >> 8) & 0xFF) as u8, 126 | b: (i & 0xFF) as u8, 127 | }; 128 | let hsl = css_colors::rgb(rgb.r, rgb.g, rgb.b).to_hsl(); 129 | let hex = format_hex!(rgb.r, rgb.g, rgb.b, 0xFF)?; 130 | let (int24, uint32, sint32) = rgb_to_ints(&rgb, None); 131 | Ok(Color { 132 | name: blueprint.name.to_string(), 133 | identifier: blueprint.name.identifier().to_string(), 134 | order: blueprint.order, 135 | accent: blueprint.accent, 136 | hex, 137 | int24, 138 | uint32, 139 | sint32, 140 | rgb, 141 | hsl: HSL { 142 | h: hsl.h.degrees(), 143 | s: hsl.s.as_f32(), 144 | l: hsl.l.as_f32(), 145 | }, 146 | opacity: 0xFF, 147 | }) 148 | } 149 | 150 | fn color_from_catppuccin(color: &catppuccin::Color) -> tera::Result { 151 | let hex = format_hex!(color.rgb.r, color.rgb.g, color.rgb.b, 0xFF)?; 152 | let rgb: RGB = color.rgb.into(); 153 | let (int24, uint32, sint32) = rgb_to_ints(&rgb, None); 154 | Ok(Color { 155 | name: color.name.to_string(), 156 | identifier: color.name.identifier().to_string(), 157 | order: color.order, 158 | accent: color.accent, 159 | hex, 160 | int24, 161 | uint32, 162 | sint32, 163 | rgb: RGB { 164 | r: color.rgb.r, 165 | g: color.rgb.g, 166 | b: color.rgb.b, 167 | }, 168 | hsl: HSL { 169 | h: color.hsl.h.round() as u16, 170 | s: color.hsl.s as f32, 171 | l: color.hsl.l as f32, 172 | }, 173 | opacity: 255, 174 | }) 175 | } 176 | 177 | /// Build a [`Palette`] from [`catppuccin::PALETTE`], optionally applying color overrides. 178 | pub fn build_palette(color_overrides: Option<&ColorOverrides>) -> Result { 179 | // make a `Color` from a `catppuccin::Color`, taking into account `color_overrides`. 180 | // overrides apply in this order: 181 | // 1. base color 182 | // 2. "all" override 183 | // 3. flavor override 184 | let make_color = 185 | |color: &catppuccin::Color, flavor_name: catppuccin::FlavorName| -> Result { 186 | let flavor_override = color_overrides 187 | .map(|co| match flavor_name { 188 | catppuccin::FlavorName::Latte => &co.latte, 189 | catppuccin::FlavorName::Frappe => &co.frappe, 190 | catppuccin::FlavorName::Macchiato => &co.macchiato, 191 | catppuccin::FlavorName::Mocha => &co.mocha, 192 | }) 193 | .and_then(|o| o.get(color.name.identifier()).cloned()) 194 | .map(|s| color_from_hex_override(&s, color)) 195 | .transpose()?; 196 | 197 | let all_override = color_overrides 198 | .and_then(|co| co.all.get(color.name.identifier()).cloned()) 199 | .map(|s| color_from_hex_override(&s, color)) 200 | .transpose()?; 201 | 202 | let base_color = color_from_catppuccin(color)?; 203 | 204 | Ok(flavor_override.or(all_override).unwrap_or(base_color)) 205 | }; 206 | 207 | let mut flavors = IndexMap::new(); 208 | for flavor in &catppuccin::PALETTE { 209 | let mut colors = IndexMap::new(); 210 | for color in flavor { 211 | colors.insert( 212 | color.name.identifier().to_string(), 213 | make_color(color, flavor.name)?, 214 | ); 215 | } 216 | flavors.insert( 217 | flavor.name.identifier().to_string(), 218 | Flavor { 219 | name: flavor.name.to_string(), 220 | identifier: flavor.name.identifier().to_string(), 221 | emoji: flavor.emoji, 222 | order: flavor.order, 223 | dark: flavor.dark, 224 | light: !flavor.dark, 225 | colors, 226 | }, 227 | ); 228 | } 229 | Ok(Palette { flavors }) 230 | } 231 | 232 | impl Palette { 233 | #[must_use] 234 | pub fn iter(&self) -> indexmap::map::Iter { 235 | self.flavors.iter() 236 | } 237 | } 238 | 239 | impl<'a> IntoIterator for &'a Palette { 240 | type Item = (&'a String, &'a Flavor); 241 | type IntoIter = indexmap::map::Iter<'a, String, Flavor>; 242 | 243 | fn into_iter(self) -> Self::IntoIter { 244 | self.iter() 245 | } 246 | } 247 | 248 | impl Flavor { 249 | #[must_use] 250 | pub fn iter(&self) -> indexmap::map::Iter { 251 | self.colors.iter() 252 | } 253 | } 254 | 255 | impl<'a> IntoIterator for &'a Flavor { 256 | type Item = (&'a String, &'a Color); 257 | type IntoIter = indexmap::map::Iter<'a, String, Color>; 258 | 259 | fn into_iter(self) -> Self::IntoIter { 260 | self.iter() 261 | } 262 | } 263 | 264 | fn rgb_to_hex(rgb: &RGB, opacity: u8) -> tera::Result { 265 | format_hex!(rgb.r, rgb.g, rgb.b, opacity) 266 | } 267 | 268 | impl Color { 269 | fn from_hsla(hsla: css_colors::HSLA, blueprint: &Self) -> tera::Result { 270 | let rgb = hsla.to_rgb(); 271 | let rgb = RGB { 272 | r: rgb.r.as_u8(), 273 | g: rgb.g.as_u8(), 274 | b: rgb.b.as_u8(), 275 | }; 276 | let hsl = HSL { 277 | h: hsla.h.degrees(), 278 | s: hsla.s.as_f32(), 279 | l: hsla.l.as_f32(), 280 | }; 281 | let opacity = hsla.a.as_u8(); 282 | let (int24, uint32, sint32) = rgb_to_ints(&rgb, Some(opacity)); 283 | Ok(Self { 284 | name: blueprint.name.clone(), 285 | identifier: blueprint.identifier.clone(), 286 | order: blueprint.order, 287 | accent: blueprint.accent, 288 | hex: rgb_to_hex(&rgb, opacity)?, 289 | int24, 290 | uint32, 291 | sint32, 292 | rgb, 293 | hsl, 294 | opacity, 295 | }) 296 | } 297 | 298 | fn from_rgba(rgba: css_colors::RGBA, blueprint: &Self) -> tera::Result { 299 | let hsl = rgba.to_hsl(); 300 | let rgb = RGB { 301 | r: rgba.r.as_u8(), 302 | g: rgba.g.as_u8(), 303 | b: rgba.b.as_u8(), 304 | }; 305 | let hsl = HSL { 306 | h: hsl.h.degrees(), 307 | s: hsl.s.as_f32(), 308 | l: hsl.l.as_f32(), 309 | }; 310 | let opacity = rgba.a.as_u8(); 311 | let (int24, uint32, sint32) = rgb_to_ints(&rgb, Some(opacity)); 312 | Ok(Self { 313 | name: blueprint.name.clone(), 314 | identifier: blueprint.identifier.clone(), 315 | order: blueprint.order, 316 | accent: blueprint.accent, 317 | hex: rgb_to_hex(&rgb, opacity)?, 318 | int24, 319 | uint32, 320 | sint32, 321 | rgb, 322 | hsl, 323 | opacity, 324 | }) 325 | } 326 | 327 | pub fn mix(base: &Self, blend: &Self, amount: f64) -> tera::Result { 328 | let amount = (amount * 100.0).clamp(0.0, 100.0).round() as u8; 329 | let blueprint = base; 330 | let base: css_colors::RGBA = base.into(); 331 | let base = base.to_rgba(); 332 | let blend: css_colors::RGBA = blend.into(); 333 | let result = base.mix(blend, css_colors::percent(amount)); 334 | Self::from_rgba(result, blueprint) 335 | } 336 | 337 | pub fn mod_hue(&self, hue: i32) -> tera::Result { 338 | let mut hsl: css_colors::HSL = self.into(); 339 | hsl.h = css_colors::deg(hue); 340 | Self::from_hsla(hsl.to_hsla(), self) 341 | } 342 | 343 | pub fn add_hue(&self, hue: i32) -> tera::Result { 344 | let hsl: css_colors::HSL = self.into(); 345 | let hsl = hsl.spin(css_colors::deg(hue)); 346 | Self::from_hsla(hsl.to_hsla(), self) 347 | } 348 | 349 | pub fn sub_hue(&self, hue: i32) -> tera::Result { 350 | let hsl: css_colors::HSL = self.into(); 351 | let hsl = hsl.spin(-css_colors::deg(hue)); 352 | Self::from_hsla(hsl.to_hsla(), self) 353 | } 354 | 355 | pub fn mod_saturation(&self, saturation: u8) -> tera::Result { 356 | let mut hsl: css_colors::HSL = self.into(); 357 | hsl.s = css_colors::percent(saturation); 358 | Self::from_hsla(hsl.to_hsla(), self) 359 | } 360 | 361 | pub fn add_saturation(&self, saturation: u8) -> tera::Result { 362 | let hsl: css_colors::HSL = self.into(); 363 | let hsl = hsl.saturate(css_colors::percent(saturation)); 364 | Self::from_hsla(hsl.to_hsla(), self) 365 | } 366 | 367 | pub fn sub_saturation(&self, saturation: u8) -> tera::Result { 368 | let hsl: css_colors::HSL = self.into(); 369 | let hsl = hsl.desaturate(css_colors::percent(saturation)); 370 | Self::from_hsla(hsl.to_hsla(), self) 371 | } 372 | 373 | pub fn mod_lightness(&self, lightness: u8) -> tera::Result { 374 | let mut hsl: css_colors::HSL = self.into(); 375 | hsl.l = css_colors::percent(lightness); 376 | Self::from_hsla(hsl.to_hsla(), self) 377 | } 378 | 379 | pub fn add_lightness(&self, lightness: u8) -> tera::Result { 380 | let hsl: css_colors::HSL = self.into(); 381 | let hsl = hsl.lighten(css_colors::percent(lightness)); 382 | Self::from_hsla(hsl.to_hsla(), self) 383 | } 384 | 385 | pub fn sub_lightness(&self, lightness: u8) -> tera::Result { 386 | let hsl: css_colors::HSL = self.into(); 387 | let hsl = hsl.darken(css_colors::percent(lightness)); 388 | Self::from_hsla(hsl.to_hsla(), self) 389 | } 390 | 391 | pub fn mod_opacity(&self, opacity: f32) -> tera::Result { 392 | let opacity = (opacity * 255.0).round() as u8; 393 | let (int24, uint32, sint32) = rgb_to_ints(&self.rgb, Some(opacity)); 394 | Ok(Self { 395 | opacity, 396 | hex: rgb_to_hex(&self.rgb, opacity)?, 397 | int24, 398 | uint32, 399 | sint32, 400 | ..self.clone() 401 | }) 402 | } 403 | 404 | pub fn add_opacity(&self, opacity: f32) -> tera::Result { 405 | let opacity = (opacity * 255.0).round() as u8; 406 | let opacity = self.opacity.saturating_add(opacity); 407 | let (int24, uint32, sint32) = rgb_to_ints(&self.rgb, Some(opacity)); 408 | Ok(Self { 409 | opacity, 410 | hex: rgb_to_hex(&self.rgb, opacity)?, 411 | int24, 412 | uint32, 413 | sint32, 414 | ..self.clone() 415 | }) 416 | } 417 | 418 | pub fn sub_opacity(&self, opacity: f32) -> tera::Result { 419 | let opacity = (opacity * 255.0).round() as u8; 420 | let opacity = self.opacity.saturating_sub(opacity); 421 | let (int24, uint32, sint32) = rgb_to_ints(&self.rgb, Some(opacity)); 422 | Ok(Self { 423 | opacity, 424 | hex: rgb_to_hex(&self.rgb, opacity)?, 425 | int24, 426 | uint32, 427 | sint32, 428 | ..self.clone() 429 | }) 430 | } 431 | } 432 | 433 | impl From<&Color> for css_colors::RGB { 434 | fn from(c: &Color) -> Self { 435 | Self { 436 | r: css_colors::Ratio::from_u8(c.rgb.r), 437 | g: css_colors::Ratio::from_u8(c.rgb.g), 438 | b: css_colors::Ratio::from_u8(c.rgb.b), 439 | } 440 | } 441 | } 442 | 443 | impl From<&Color> for css_colors::RGBA { 444 | fn from(c: &Color) -> Self { 445 | Self { 446 | r: css_colors::Ratio::from_u8(c.rgb.r), 447 | g: css_colors::Ratio::from_u8(c.rgb.g), 448 | b: css_colors::Ratio::from_u8(c.rgb.b), 449 | a: css_colors::Ratio::from_u8(c.opacity), 450 | } 451 | } 452 | } 453 | 454 | impl From<&Color> for css_colors::HSL { 455 | fn from(c: &Color) -> Self { 456 | Self { 457 | h: css_colors::Angle::new(c.hsl.h), 458 | s: css_colors::Ratio::from_f32(c.hsl.s), 459 | l: css_colors::Ratio::from_f32(c.hsl.l), 460 | } 461 | } 462 | } 463 | 464 | impl From<&Color> for css_colors::HSLA { 465 | fn from(c: &Color) -> Self { 466 | Self { 467 | h: css_colors::Angle::new(c.hsl.h), 468 | s: css_colors::Ratio::from_f32(c.hsl.s), 469 | l: css_colors::Ratio::from_f32(c.hsl.l), 470 | a: css_colors::Ratio::from_u8(c.opacity), 471 | } 472 | } 473 | } 474 | 475 | impl From for RGB { 476 | fn from(rgb: catppuccin::Rgb) -> Self { 477 | Self { 478 | r: rgb.r, 479 | g: rgb.g, 480 | b: rgb.b, 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/templating.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use indexmap::IndexMap; 4 | use itertools::Itertools as _; 5 | 6 | use crate::{filters, functions, markdown}; 7 | 8 | /// Allows creation of a [`FilterExample`] with the following syntax: 9 | /// 10 | /// `function_example!(mix(base=base, blend=red, amount=0.5) => "#804040")` 11 | macro_rules! function_example { 12 | ($name:ident($($key:ident = $value:tt),*) => $output:expr) => { 13 | $crate::templating::FunctionExample { 14 | inputs: { 15 | let mut map = indexmap::IndexMap::new(); 16 | $(map.insert(stringify!($key).to_string(), stringify!($value).to_string());)* 17 | map 18 | }, 19 | output: $output.to_string(), 20 | } 21 | }; 22 | } 23 | 24 | /// Allows creation of a [`FilterExample`] with the following syntax: 25 | /// 26 | /// `filter_example!(red | add(hue=30)) => "#ff6666")` 27 | macro_rules! filter_example { 28 | ($value:tt | $name:ident => $output:expr) => { 29 | $crate::templating::FilterExample { 30 | value: stringify!($value).to_string(), 31 | inputs: indexmap::IndexMap::new(), 32 | output: $output.to_string(), 33 | } 34 | }; 35 | ($value:tt | $name:ident($($key:ident = $arg_value:tt),*) => $output:expr) => { 36 | $crate::templating::FilterExample { 37 | value: stringify!($value).to_string(), 38 | inputs: { 39 | let mut map = indexmap::IndexMap::new(); 40 | $(map.insert(stringify!($key).to_string(), stringify!($arg_value).to_string());)* 41 | map 42 | }, 43 | output: $output.to_string(), 44 | } 45 | }; 46 | } 47 | 48 | pub fn make_engine(template_directory: &Path) -> tera::Tera { 49 | let mut tera = tera::Tera::default(); 50 | tera.register_filter("add", filters::add); 51 | tera.register_filter("sub", filters::sub); 52 | tera.register_filter("mod", filters::modify); 53 | tera.register_filter("urlencode_lzma", filters::urlencode_lzma); 54 | tera.register_filter("trunc", filters::trunc); 55 | tera.register_filter("mix", filters::mix); 56 | tera.register_filter("css_rgb", filters::css_rgb); 57 | tera.register_filter("css_rgba", filters::css_rgba); 58 | tera.register_filter("css_hsl", filters::css_hsl); 59 | tera.register_filter("css_hsla", filters::css_hsla); 60 | tera.register_function("if", functions::if_fn); 61 | tera.register_function("object", functions::object); 62 | tera.register_function("css_rgb", functions::css_rgb); 63 | tera.register_function("css_rgba", functions::css_rgba); 64 | tera.register_function("css_hsl", functions::css_hsl); 65 | tera.register_function("css_hsla", functions::css_hsla); 66 | tera.register_function( 67 | "read_file", 68 | functions::read_file_handler(template_directory.to_owned()), 69 | ); 70 | tera 71 | } 72 | 73 | #[must_use] 74 | pub fn all_functions() -> Vec { 75 | vec![ 76 | Function { 77 | name: "if".to_string(), 78 | description: "Return one value if a condition is true, and another if it's false" 79 | .to_string(), 80 | examples: vec![ 81 | function_example!(if(cond=true, t=1, f=0) => "1"), 82 | function_example!(if(cond=false, t=1, f=0) => "0"), 83 | ], 84 | }, 85 | Function { 86 | name: "object".to_string(), 87 | description: "Create an object from the input".to_string(), 88 | examples: vec![ 89 | function_example!(object(a=1, b=2) => "{a: 1, b: 2}"), 90 | function_example!(object(a=1, b=2) => "{a: 1, b: 2}"), 91 | ], 92 | }, 93 | Function { 94 | name: "css_rgb".to_string(), 95 | description: "Convert a color to an RGB CSS string".to_string(), 96 | examples: vec![function_example!(css_rgb(color=red) => "rgb(210, 15, 57)")], 97 | }, 98 | Function { 99 | name: "css_rgba".to_string(), 100 | description: "Convert a color to an RGBA CSS string".to_string(), 101 | examples: vec![function_example!(css_rgba(color=red) => "rgba(210, 15, 57, 1.00)")], 102 | }, 103 | Function { 104 | name: "css_hsl".to_string(), 105 | description: "Convert a color to an HSL CSS string".to_string(), 106 | examples: vec![function_example!(css_hsl(color=red) => "hsl(347, 87%, 44%)")], 107 | }, 108 | Function { 109 | name: "css_hsla".to_string(), 110 | description: "Convert a color to an HSLA CSS string".to_string(), 111 | examples: vec![function_example!(css_hsla(color=red) => "hsla(347, 87%, 44%, 1.00)")], 112 | }, 113 | Function { 114 | name: "read_file".to_string(), 115 | description: 116 | "Read and include the contents of a file, path is relative to the template file" 117 | .to_string(), 118 | examples: vec![function_example!(read_file(path="abc.txt") => "abc")], 119 | }, 120 | ] 121 | } 122 | 123 | #[must_use] 124 | pub fn all_filters() -> Vec { 125 | vec![ 126 | Filter { 127 | name: "add".to_string(), 128 | description: "Add a value to a color".to_string(), 129 | examples: vec![ 130 | filter_example!(red | add(hue=30) => "#ff6666"), 131 | filter_example!(red | add(saturation=0.5) => "#ff6666"), 132 | ], 133 | }, 134 | Filter { 135 | name: "sub".to_string(), 136 | description: "Subtract a value from a color".to_string(), 137 | examples: vec![ 138 | filter_example!(red | sub(hue=30) => "#d30f9b"), 139 | filter_example!(red | sub(saturation=60) => "#8f5360"), 140 | ], 141 | }, 142 | Filter { 143 | name: "mod".to_string(), 144 | description: "Modify a color".to_string(), 145 | examples: vec![ 146 | filter_example!(red | mod(lightness=80) => "#f8a0b3"), 147 | filter_example!(red | mod(opacity=0.5) => "#d20f3980"), 148 | ], 149 | }, 150 | Filter { 151 | name: "mix".to_string(), 152 | description: "Mix two colors together".to_string(), 153 | examples: vec![filter_example!(red | mix(color=base, amount=0.5) => "#e08097")], 154 | }, 155 | Filter { 156 | name: "urlencode_lzma".to_string(), 157 | description: "Serialize an object into a URL-safe string with LZMA compression" 158 | .to_string(), 159 | examples: vec![ 160 | filter_example!(red | urlencode_lzma => "#ff6666"), 161 | filter_example!(some_object | urlencode_lzma => "XQAAgAAEAAAAAAAAAABAqEggMAAAAA=="), 162 | ], 163 | }, 164 | Filter { 165 | name: "trunc".to_string(), 166 | description: "Truncate a number to a certain number of places".to_string(), 167 | examples: vec![filter_example!(1.123456 | trunc(places=3) => "1.123")], 168 | }, 169 | Filter { 170 | name: "css_rgb".to_string(), 171 | description: "Convert a color to an RGB CSS string".to_string(), 172 | examples: vec![filter_example!(red | css_rgb => "rgb(210, 15, 57)")], 173 | }, 174 | Filter { 175 | name: "css_rgba".to_string(), 176 | description: "Convert a color to an RGBA CSS string".to_string(), 177 | examples: vec![filter_example!(red | css_rgba => "rgba(210, 15, 57, 1.00)")], 178 | }, 179 | Filter { 180 | name: "css_hsl".to_string(), 181 | description: "Convert a color to an HSL CSS string".to_string(), 182 | examples: vec![filter_example!(red | css_hsl => "hsl(347, 87%, 44%)")], 183 | }, 184 | Filter { 185 | name: "css_hsla".to_string(), 186 | description: "Convert a color to an HSLA CSS string".to_string(), 187 | examples: vec![filter_example!(red | css_hsla => "hsla(347, 87%, 44%, 1.00)")], 188 | }, 189 | ] 190 | } 191 | 192 | #[derive(serde::Serialize)] 193 | pub struct Function { 194 | pub name: String, 195 | pub description: String, 196 | pub examples: Vec, 197 | } 198 | 199 | #[derive(serde::Serialize)] 200 | pub struct Filter { 201 | pub name: String, 202 | pub description: String, 203 | pub examples: Vec, 204 | } 205 | 206 | #[derive(serde::Serialize)] 207 | pub struct FunctionExample { 208 | pub inputs: IndexMap, 209 | pub output: String, 210 | } 211 | 212 | #[derive(serde::Serialize)] 213 | pub struct FilterExample { 214 | pub value: String, 215 | pub inputs: IndexMap, 216 | pub output: String, 217 | } 218 | 219 | impl markdown::TableDisplay for Function { 220 | fn table_headings() -> Box<[String]> { 221 | Box::new([ 222 | "Name".to_string(), 223 | "Description".to_string(), 224 | "Examples".to_string(), 225 | ]) 226 | } 227 | 228 | fn table_row(&self) -> Box<[String]> { 229 | Box::new([ 230 | format!("`{}`", self.name), 231 | self.description.clone(), 232 | if self.examples.is_empty() { 233 | "None".to_string() 234 | } else { 235 | self.examples.first().map_or_else(String::new, |example| { 236 | format!( 237 | "`{name}({input})` ⇒ `{output}`", 238 | name = self.name, 239 | input = example 240 | .inputs 241 | .iter() 242 | .map(|(k, v)| format!("{k}={v}")) 243 | .join(", "), 244 | output = example.output 245 | ) 246 | }) 247 | }, 248 | ]) 249 | } 250 | } 251 | 252 | impl markdown::TableDisplay for Filter { 253 | fn table_headings() -> Box<[String]> { 254 | Box::new([ 255 | "Name".to_string(), 256 | "Description".to_string(), 257 | "Examples".to_string(), 258 | ]) 259 | } 260 | 261 | fn table_row(&self) -> Box<[String]> { 262 | Box::new([ 263 | format!("`{}`", self.name), 264 | self.description.clone(), 265 | if self.examples.is_empty() { 266 | "None".to_string() 267 | } else { 268 | self.examples.first().map_or_else(String::new, |example| { 269 | if example.inputs.is_empty() { 270 | format!( 271 | "`{value} \\| {name}` ⇒ `{output}`", 272 | value = example.value, 273 | name = self.name, 274 | output = example.output 275 | ) 276 | } else { 277 | format!( 278 | "`{value} \\| {name}({input})` ⇒ `{output}`", 279 | value = example.value, 280 | name = self.name, 281 | input = example 282 | .inputs 283 | .iter() 284 | .map(|(k, v)| format!("{k}={v}")) 285 | .join(", "), 286 | output = example.output 287 | ) 288 | } 289 | }) 290 | }, 291 | ]) 292 | } 293 | } 294 | 295 | #[cfg(test)] 296 | mod tests { 297 | #[test] 298 | fn function_example_with_single_arg() { 299 | let example = function_example!(mix(base=base) => "#804040"); 300 | assert_eq!(example.inputs["base"], "base"); 301 | assert_eq!(example.output, "#804040"); 302 | } 303 | 304 | #[test] 305 | fn function_example_with_multiple_args() { 306 | let example = function_example!(mix(base=base, blend=red, amount=0.5) => "#804040"); 307 | assert_eq!(example.inputs["base"], "base"); 308 | assert_eq!(example.inputs["blend"], "red"); 309 | assert_eq!(example.inputs["amount"], "0.5"); 310 | assert_eq!(example.output, "#804040"); 311 | } 312 | 313 | #[test] 314 | fn filter_example_with_no_args() { 315 | let example = filter_example!(red | add => "#ff6666"); 316 | assert_eq!(example.value, "red"); 317 | assert_eq!(example.inputs.len(), 0); 318 | assert_eq!(example.output, "#ff6666"); 319 | } 320 | 321 | #[test] 322 | fn filter_example_with_single_arg() { 323 | let example = filter_example!(red | add(hue=30) => "#ff6666"); 324 | assert_eq!(example.value, "red"); 325 | assert_eq!(example.inputs["hue"], "30"); 326 | assert_eq!(example.output, "#ff6666"); 327 | } 328 | 329 | #[test] 330 | fn filter_example_with_multiple_args() { 331 | let example = filter_example!(red | add(hue=30, saturation=0.5) => "#ff6666"); 332 | assert_eq!(example.value, "red"); 333 | assert_eq!(example.inputs["hue"], "30"); 334 | assert_eq!(example.inputs["saturation"], "0.5"); 335 | assert_eq!(example.output, "#ff6666"); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod happy_path { 3 | use assert_cmd::Command; 4 | use predicates::prelude::{predicate, PredicateBooleanExt}; 5 | 6 | /// Test that the CLI can render a single-flavor template file 7 | #[test] 8 | fn test_single() { 9 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 10 | let assert = cmd 11 | .args(["tests/fixtures/single/single.tera", "-f", "latte"]) 12 | .assert(); 13 | assert 14 | .success() 15 | .stdout(include_str!("fixtures/single/single.md")); 16 | } 17 | 18 | /// Test that the CLI can render a multi-flavor template file 19 | #[test] 20 | fn test_multi() { 21 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 22 | let assert = cmd.args(["tests/fixtures/multi/multi.tera"]).assert(); 23 | assert 24 | .success() 25 | .stdout(include_str!("fixtures/multi/multi.md")); 26 | } 27 | 28 | /// Test that the CLI can render a multi-flavor matrix template 29 | #[test] 30 | fn test_multifile_render() { 31 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 32 | let assert = cmd 33 | .args(["--dry-run", "tests/fixtures/multifile.tera"]) 34 | .assert(); 35 | assert.success().stdout(predicate::str::contains( 36 | "catppuccin-macchiato-yellow-no-italics.ini", 37 | )); 38 | } 39 | 40 | /// Test that the CLI can render a template which uses `read_file` 41 | #[test] 42 | fn test_read_file() { 43 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 44 | let assert = cmd 45 | .args(["tests/fixtures/read_file/read_file.tera", "-f", "latte"]) 46 | .assert(); 47 | assert 48 | .success() 49 | .stdout(include_str!("fixtures/read_file/read_file.md")); 50 | } 51 | 52 | /// Test that the CLI can render colours in specific formats 53 | #[test] 54 | fn test_formats() { 55 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 56 | let assert = cmd 57 | .args(["tests/fixtures/formats.tera", "-f", "latte"]) 58 | .assert(); 59 | assert.success().stdout( 60 | predicate::str::contains("24-bit red: 13766457") 61 | .and(predicate::str::contains("unsigned 32-bit red: 4291956537")) 62 | .and(predicate::str::contains("signed 32-bit red: -3010759")), 63 | ); 64 | } 65 | 66 | /// Test that the CLI can render a UTF-8 template file 67 | #[test] 68 | fn test_utf8() { 69 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 70 | let assert = cmd.args(["tests/fixtures/encodings/utf8.tera"]).assert(); 71 | assert 72 | .success() 73 | .stdout(predicate::str::contains("it worked!")); 74 | } 75 | 76 | /// Test that the CLI can render a UTF-8 with BOM template file 77 | #[test] 78 | fn test_utf8_bom() { 79 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 80 | let assert = cmd.args(["tests/fixtures/encodings/utf8bom.tera"]).assert(); 81 | assert 82 | .success() 83 | .stdout(predicate::str::contains("it worked!")); 84 | } 85 | 86 | /// Test that the CLI can render a UTF-16 BE template file 87 | #[test] 88 | fn test_utf16be() { 89 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 90 | let assert = cmd.args(["tests/fixtures/encodings/utf16be.tera"]).assert(); 91 | assert 92 | .success() 93 | .stdout(predicate::str::contains("it worked!")); 94 | } 95 | 96 | /// Test that the CLI can render a UTF-16 LE template file 97 | #[test] 98 | fn test_utf16le() { 99 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 100 | let assert = cmd.args(["tests/fixtures/encodings/utf16le.tera"]).assert(); 101 | assert 102 | .success() 103 | .stdout(predicate::str::contains("it worked!")); 104 | } 105 | 106 | /// Test that the default hex format is rrggbb and full alpha is hidden 107 | #[test] 108 | fn test_default_hex_format() { 109 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 110 | let assert = cmd.args(["tests/fixtures/hexformat/default.tera"]).assert(); 111 | assert 112 | .success() 113 | .stdout(include_str!("fixtures/hexformat/default.txt")); 114 | } 115 | 116 | /// Test that the CLI can render a template with a custom hex format 117 | #[test] 118 | fn test_custom_hex_format() { 119 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 120 | let assert = cmd 121 | .args(["tests/fixtures/hexformat/custom.tera", "-f", "latte"]) 122 | .assert(); 123 | assert 124 | .success() 125 | .stdout(include_str!("fixtures/hexformat/custom.txt")); 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod sad_path { 131 | use assert_cmd::Command; 132 | use predicates::prelude::predicate; 133 | 134 | #[test] 135 | fn nonexistent_template_file() { 136 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 137 | cmd.arg("test/file/doesnt/exist"); 138 | cmd.assert() 139 | .failure() 140 | .stderr(predicate::str::contains("Template file does not exist")); 141 | } 142 | 143 | #[test] 144 | fn invalid_flavor() { 145 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 146 | cmd.arg("tests/fixtures/single/single.tera") 147 | .args(["--flavor", "invalid"]); 148 | cmd.assert().failure().stderr(predicate::str::contains( 149 | "error: invalid value 'invalid' for '--flavor '", 150 | )); 151 | } 152 | 153 | #[test] 154 | fn template_contains_invalid_syntax() { 155 | let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); 156 | cmd.arg("tests/fixtures/errors.tera").args(["-f", "mocha"]); 157 | cmd.assert() 158 | .failure() 159 | .stderr(predicate::str::contains("Error: Template is invalid")); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/encodings.rs: -------------------------------------------------------------------------------- 1 | //! tests that ensure the special encoding fixtures are left untouched 2 | #[test] 3 | fn utf8() { 4 | let bytes = &include_bytes!("fixtures/encodings/utf8.tera")[..3]; 5 | assert_eq!( 6 | bytes, b"---", 7 | "fixtures/encodings/utf8.tera needs to be re-encoded to UTF-8" 8 | ); 9 | } 10 | 11 | #[test] 12 | fn utf8bom() { 13 | let bytes = &include_bytes!("fixtures/encodings/utf8bom.tera")[..6]; 14 | assert_eq!( 15 | bytes, b"\xEF\xBB\xBF---", 16 | "fixtures/encodings/utf8bom.tera needs to be re-encoded to UTF-8 with BOM" 17 | ); 18 | } 19 | 20 | #[test] 21 | fn utf16be() { 22 | let bytes = &include_bytes!("fixtures/encodings/utf16be.tera")[..2]; 23 | assert_eq!( 24 | bytes, b"\xFE\xFF", 25 | "fixtures/encodings/utf16be.tera needs to be re-encoded to UTF-16 BE" 26 | ); 27 | } 28 | 29 | #[test] 30 | fn utf16le() { 31 | let bytes = &include_bytes!("fixtures/encodings/utf16le.tera")[..2]; 32 | assert_eq!( 33 | bytes, b"\xFF\xFE", 34 | "fixtures/encodings/utf16le.tera needs to be re-encoded to UTF-16 LE" 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/encodings/README.md: -------------------------------------------------------------------------------- 1 | The fixtures in this directory are encoded in various formats to test the encoding detection and decoding capabilities of Whiskers. 2 | 3 | Some text editors like to normalize the encoding of files when saving them. Please be careful not to change them unintentionally. 4 | 5 | There are tests in `tests/encodings.rs` that ensure these fixtures are not unintentionally changed. 6 | -------------------------------------------------------------------------------- /tests/fixtures/encodings/utf16be.tera: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/whiskers/e14005e15e0ad481c2f9f523da8ea12dd24227dc/tests/fixtures/encodings/utf16be.tera -------------------------------------------------------------------------------- /tests/fixtures/encodings/utf16le.tera: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catppuccin/whiskers/e14005e15e0ad481c2f9f523da8ea12dd24227dc/tests/fixtures/encodings/utf16le.tera -------------------------------------------------------------------------------- /tests/fixtures/encodings/utf8.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0" 4 | --- 5 | it worked! 6 | -------------------------------------------------------------------------------- /tests/fixtures/encodings/utf8bom.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0" 4 | --- 5 | it worked! 6 | -------------------------------------------------------------------------------- /tests/fixtures/errors.tera: -------------------------------------------------------------------------------- 1 | a: b 2 | --- 3 | this file is total junk 4 | {{ nonexistent }} 5 | {{ these words dont work }} 6 | -------------------------------------------------------------------------------- /tests/fixtures/formats.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: "2.0.0" 4 | --- 5 | 24-bit red: {{red.int24}} 6 | unsigned 32-bit red: {{red.uint32}} 7 | signed 32-bit red: {{red.sint32}} -------------------------------------------------------------------------------- /tests/fixtures/hexformat/custom.tera: -------------------------------------------------------------------------------- 1 | --- 2 | # test a custom hex format 3 | whiskers: 4 | version: "2" 5 | hex_format: "0x{{B}}{{G}}{{R}}{{A}}" 6 | --- 7 | {%- set translucent_red = flavors.latte.colors.red | mod(opacity=0.5) -%} 8 | {{flavors.latte.colors.red.hex}} == 0x390FD2FF 9 | {{flavors.macchiato.colors.sky.hex}} == 0xE3D791FF 10 | {{translucent_red.hex}} == 0x390FD280 11 | -------------------------------------------------------------------------------- /tests/fixtures/hexformat/custom.txt: -------------------------------------------------------------------------------- 1 | 0x390FD2FF == 0x390FD2FF 2 | 0xE3D791FF == 0xE3D791FF 3 | 0x390FD280 == 0x390FD280 4 | -------------------------------------------------------------------------------- /tests/fixtures/hexformat/default.tera: -------------------------------------------------------------------------------- 1 | --- 2 | # test the default hex format 3 | whiskers: 4 | version: "2" 5 | --- 6 | {%- set translucent_red = flavors.latte.colors.red | mod(opacity=0.5) -%} 7 | {{flavors.latte.colors.red.hex}} == d20f39 8 | {{flavors.macchiato.colors.sky.hex}} == 91d7e3 9 | {{translucent_red.hex}} == d20f3980 10 | -------------------------------------------------------------------------------- /tests/fixtures/hexformat/default.txt: -------------------------------------------------------------------------------- 1 | d20f39 == d20f39 2 | 91d7e3 == 91d7e3 3 | d20f3980 == d20f3980 4 | -------------------------------------------------------------------------------- /tests/fixtures/multi/multi.md: -------------------------------------------------------------------------------- 1 | 2 | # Latte 3 | 4 | | Color | Hex | RGB | HSL | 5 | |-------|-----|-----|-----| 6 | Rosewater | #dc8a78 | rgb(220, 138, 120) | hsl(11, 59%, 67%) | 7 | Flamingo | #dd7878 | rgb(221, 120, 120) | hsl(0, 60%, 67%) | 8 | Pink | #ea76cb | rgb(234, 118, 203) | hsl(316, 73%, 69%) | 9 | Mauve | #8839ef | rgb(136, 57, 239) | hsl(266, 85%, 58%) | 10 | Red | #d20f39 | rgb(210, 15, 57) | hsl(347, 87%, 44%) | 11 | Maroon | #e64553 | rgb(230, 69, 83) | hsl(355, 76%, 59%) | 12 | Peach | #fe640b | rgb(254, 100, 11) | hsl(22, 99%, 52%) | 13 | Yellow | #df8e1d | rgb(223, 142, 29) | hsl(35, 77%, 49%) | 14 | Green | #40a02b | rgb(64, 160, 43) | hsl(109, 58%, 40%) | 15 | Teal | #179299 | rgb(23, 146, 153) | hsl(183, 74%, 35%) | 16 | Sky | #04a5e5 | rgb(4, 165, 229) | hsl(197, 97%, 46%) | 17 | Sapphire | #209fb5 | rgb(32, 159, 181) | hsl(189, 70%, 42%) | 18 | Blue | #1e66f5 | rgb(30, 102, 245) | hsl(220, 91%, 54%) | 19 | Lavender | #7287fd | rgb(114, 135, 253) | hsl(231, 97%, 72%) | 20 | Text | #4c4f69 | rgb(76, 79, 105) | hsl(234, 16%, 35%) | 21 | Subtext 1 | #5c5f77 | rgb(92, 95, 119) | hsl(233, 13%, 41%) | 22 | Subtext 0 | #6c6f85 | rgb(108, 111, 133) | hsl(233, 10%, 47%) | 23 | Overlay 2 | #7c7f93 | rgb(124, 127, 147) | hsl(232, 10%, 53%) | 24 | Overlay 1 | #8c8fa1 | rgb(140, 143, 161) | hsl(231, 10%, 59%) | 25 | Overlay 0 | #9ca0b0 | rgb(156, 160, 176) | hsl(228, 11%, 65%) | 26 | Surface 2 | #acb0be | rgb(172, 176, 190) | hsl(227, 12%, 71%) | 27 | Surface 1 | #bcc0cc | rgb(188, 192, 204) | hsl(225, 14%, 77%) | 28 | Surface 0 | #ccd0da | rgb(204, 208, 218) | hsl(223, 16%, 83%) | 29 | Base | #eff1f5 | rgb(239, 241, 245) | hsl(220, 23%, 95%) | 30 | Mantle | #e6e9ef | rgb(230, 233, 239) | hsl(220, 22%, 92%) | 31 | Crust | #dce0e8 | rgb(220, 224, 232) | hsl(220, 21%, 89%) | 32 | 33 | # Frappé 34 | 35 | | Color | Hex | RGB | HSL | 36 | |-------|-----|-----|-----| 37 | Rosewater | #f2d5cf | rgb(242, 213, 207) | hsl(10, 57%, 88%) | 38 | Flamingo | #eebebe | rgb(238, 190, 190) | hsl(0, 59%, 84%) | 39 | Pink | #f4b8e4 | rgb(244, 184, 228) | hsl(316, 73%, 84%) | 40 | Mauve | #ca9ee6 | rgb(202, 158, 230) | hsl(277, 59%, 76%) | 41 | Red | #e78284 | rgb(231, 130, 132) | hsl(359, 68%, 71%) | 42 | Maroon | #ea999c | rgb(234, 153, 156) | hsl(358, 66%, 76%) | 43 | Peach | #ef9f76 | rgb(239, 159, 118) | hsl(20, 79%, 70%) | 44 | Yellow | #e5c890 | rgb(229, 200, 144) | hsl(40, 62%, 73%) | 45 | Green | #a6d189 | rgb(166, 209, 137) | hsl(96, 44%, 68%) | 46 | Teal | #81c8be | rgb(129, 200, 190) | hsl(172, 39%, 65%) | 47 | Sky | #99d1db | rgb(153, 209, 219) | hsl(189, 48%, 73%) | 48 | Sapphire | #85c1dc | rgb(133, 193, 220) | hsl(199, 55%, 69%) | 49 | Blue | #8caaee | rgb(140, 170, 238) | hsl(222, 74%, 74%) | 50 | Lavender | #babbf1 | rgb(186, 187, 241) | hsl(239, 66%, 84%) | 51 | Text | #c6d0f5 | rgb(198, 208, 245) | hsl(227, 70%, 87%) | 52 | Subtext 1 | #b5bfe2 | rgb(181, 191, 226) | hsl(227, 44%, 80%) | 53 | Subtext 0 | #a5adce | rgb(165, 173, 206) | hsl(228, 29%, 73%) | 54 | Overlay 2 | #949cbb | rgb(148, 156, 187) | hsl(228, 22%, 66%) | 55 | Overlay 1 | #838ba7 | rgb(131, 139, 167) | hsl(227, 17%, 58%) | 56 | Overlay 0 | #737994 | rgb(115, 121, 148) | hsl(229, 13%, 52%) | 57 | Surface 2 | #626880 | rgb(98, 104, 128) | hsl(228, 13%, 44%) | 58 | Surface 1 | #51576d | rgb(81, 87, 109) | hsl(227, 15%, 37%) | 59 | Surface 0 | #414559 | rgb(65, 69, 89) | hsl(230, 16%, 30%) | 60 | Base | #303446 | rgb(48, 52, 70) | hsl(229, 19%, 23%) | 61 | Mantle | #292c3c | rgb(41, 44, 60) | hsl(231, 19%, 20%) | 62 | Crust | #232634 | rgb(35, 38, 52) | hsl(229, 20%, 17%) | 63 | 64 | # Macchiato 65 | 66 | | Color | Hex | RGB | HSL | 67 | |-------|-----|-----|-----| 68 | Rosewater | #f4dbd6 | rgb(244, 219, 214) | hsl(10, 58%, 90%) | 69 | Flamingo | #f0c6c6 | rgb(240, 198, 198) | hsl(0, 58%, 86%) | 70 | Pink | #f5bde6 | rgb(245, 189, 230) | hsl(316, 74%, 85%) | 71 | Mauve | #c6a0f6 | rgb(198, 160, 246) | hsl(267, 83%, 80%) | 72 | Red | #ed8796 | rgb(237, 135, 150) | hsl(351, 74%, 73%) | 73 | Maroon | #ee99a0 | rgb(238, 153, 160) | hsl(355, 71%, 77%) | 74 | Peach | #f5a97f | rgb(245, 169, 127) | hsl(21, 86%, 73%) | 75 | Yellow | #eed49f | rgb(238, 212, 159) | hsl(40, 70%, 78%) | 76 | Green | #a6da95 | rgb(166, 218, 149) | hsl(105, 48%, 72%) | 77 | Teal | #8bd5ca | rgb(139, 213, 202) | hsl(171, 47%, 69%) | 78 | Sky | #91d7e3 | rgb(145, 215, 227) | hsl(189, 59%, 73%) | 79 | Sapphire | #7dc4e4 | rgb(125, 196, 228) | hsl(199, 66%, 69%) | 80 | Blue | #8aadf4 | rgb(138, 173, 244) | hsl(220, 83%, 75%) | 81 | Lavender | #b7bdf8 | rgb(183, 189, 248) | hsl(234, 82%, 85%) | 82 | Text | #cad3f5 | rgb(202, 211, 245) | hsl(227, 68%, 88%) | 83 | Subtext 1 | #b8c0e0 | rgb(184, 192, 224) | hsl(228, 39%, 80%) | 84 | Subtext 0 | #a5adcb | rgb(165, 173, 203) | hsl(227, 27%, 72%) | 85 | Overlay 2 | #939ab7 | rgb(147, 154, 183) | hsl(228, 20%, 65%) | 86 | Overlay 1 | #8087a2 | rgb(128, 135, 162) | hsl(228, 15%, 57%) | 87 | Overlay 0 | #6e738d | rgb(110, 115, 141) | hsl(230, 12%, 49%) | 88 | Surface 2 | #5b6078 | rgb(91, 96, 120) | hsl(230, 14%, 41%) | 89 | Surface 1 | #494d64 | rgb(73, 77, 100) | hsl(231, 16%, 34%) | 90 | Surface 0 | #363a4f | rgb(54, 58, 79) | hsl(230, 19%, 26%) | 91 | Base | #24273a | rgb(36, 39, 58) | hsl(232, 23%, 18%) | 92 | Mantle | #1e2030 | rgb(30, 32, 48) | hsl(233, 23%, 15%) | 93 | Crust | #181926 | rgb(24, 25, 38) | hsl(236, 23%, 12%) | 94 | 95 | # Mocha 96 | 97 | | Color | Hex | RGB | HSL | 98 | |-------|-----|-----|-----| 99 | Rosewater | #f5e0dc | rgb(245, 224, 220) | hsl(10, 56%, 91%) | 100 | Flamingo | #f2cdcd | rgb(242, 205, 205) | hsl(0, 59%, 88%) | 101 | Pink | #f5c2e7 | rgb(245, 194, 231) | hsl(316, 72%, 86%) | 102 | Mauve | #cba6f7 | rgb(203, 166, 247) | hsl(267, 84%, 81%) | 103 | Red | #f38ba8 | rgb(243, 139, 168) | hsl(343, 81%, 75%) | 104 | Maroon | #eba0ac | rgb(235, 160, 172) | hsl(350, 65%, 77%) | 105 | Peach | #fab387 | rgb(250, 179, 135) | hsl(23, 92%, 75%) | 106 | Yellow | #f9e2af | rgb(249, 226, 175) | hsl(41, 86%, 83%) | 107 | Green | #a6e3a1 | rgb(166, 227, 161) | hsl(115, 54%, 76%) | 108 | Teal | #94e2d5 | rgb(148, 226, 213) | hsl(170, 57%, 73%) | 109 | Sky | #89dceb | rgb(137, 220, 235) | hsl(189, 71%, 73%) | 110 | Sapphire | #74c7ec | rgb(116, 199, 236) | hsl(199, 76%, 69%) | 111 | Blue | #89b4fa | rgb(137, 180, 250) | hsl(217, 92%, 76%) | 112 | Lavender | #b4befe | rgb(180, 190, 254) | hsl(232, 97%, 85%) | 113 | Text | #cdd6f4 | rgb(205, 214, 244) | hsl(226, 64%, 88%) | 114 | Subtext 1 | #bac2de | rgb(186, 194, 222) | hsl(227, 35%, 80%) | 115 | Subtext 0 | #a6adc8 | rgb(166, 173, 200) | hsl(228, 24%, 72%) | 116 | Overlay 2 | #9399b2 | rgb(147, 153, 178) | hsl(228, 17%, 64%) | 117 | Overlay 1 | #7f849c | rgb(127, 132, 156) | hsl(230, 13%, 55%) | 118 | Overlay 0 | #6c7086 | rgb(108, 112, 134) | hsl(231, 11%, 47%) | 119 | Surface 2 | #585b70 | rgb(88, 91, 112) | hsl(233, 12%, 39%) | 120 | Surface 1 | #45475a | rgb(69, 71, 90) | hsl(234, 13%, 31%) | 121 | Surface 0 | #313244 | rgb(49, 50, 68) | hsl(237, 16%, 23%) | 122 | Base | #1e1e2e | rgb(30, 30, 46) | hsl(240, 21%, 15%) | 123 | Mantle | #181825 | rgb(24, 24, 37) | hsl(240, 21%, 12%) | 124 | Crust | #11111b | rgb(17, 17, 27) | hsl(240, 23%, 9%) | 125 | -------------------------------------------------------------------------------- /tests/fixtures/multi/multi.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: 2.0.0 4 | --- 5 | {%- macro css_rgb(v) -%} 6 | rgb({{ v.r }}, {{ v.g }}, {{ v.b }}) 7 | {%- endmacro -%} 8 | 9 | {%- macro css_hsl(v) -%} 10 | hsl({{ v.h | round }}, {{ v.s * 100 | round }}%, {{ v.l * 100 | round }}%) 11 | {%- endmacro -%} 12 | 13 | {% for flavor_key, flavor in flavors %} 14 | # {{ flavor.name }} 15 | 16 | | Color | Hex | RGB | HSL | 17 | |-------|-----|-----|-----| 18 | {%- for color_key, color in flavor.colors %} 19 | {{ color.name }} | #{{ color.hex }} | {{ self::css_rgb(v=color.rgb) }} | {{ self::css_hsl(v=color.hsl) }} | 20 | 21 | {%- endfor %} 22 | {% endfor -%} 23 | -------------------------------------------------------------------------------- /tests/fixtures/multifile.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: 2.0.0 4 | matrix: 5 | - variant: ["normal", "no-italics"] 6 | - flavor 7 | - accent 8 | filename: "catppuccin-{{flavor.identifier}}-{{accent}}-{{variant}}.ini" 9 | --- 10 | # Catppuccin {{flavor.name}}{% if variant == "no-italics" %} (no italics){% endif %} 11 | [theme] 12 | {{accent}}: #{{flavor.colors[accent].hex}} 13 | rosewater: {{rosewater.hex}} 14 | -------------------------------------------------------------------------------- /tests/fixtures/read_file/abc.txt: -------------------------------------------------------------------------------- 1 | Aute tempor minim eiusmod. 2 | -------------------------------------------------------------------------------- /tests/fixtures/read_file/read_file.md: -------------------------------------------------------------------------------- 1 | Aute tempor minim eiusmod. 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 Catppuccin 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /tests/fixtures/read_file/read_file.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: 2.0.0 4 | --- 5 | 6 | {{- read_file(path="abc.txt") }} 7 | {{ read_file(path="../../../LICENSE") -}} 8 | -------------------------------------------------------------------------------- /tests/fixtures/single/single.md: -------------------------------------------------------------------------------- 1 | # Latte 2 | 3 | | Color | Hex | RGB | HSL | 4 | |-------|-----|-----|-----| 5 | Rosewater | #dc8a78 | rgb(220, 138, 120) | hsl(11, 59%, 67%) | 6 | Flamingo | #dd7878 | rgb(221, 120, 120) | hsl(0, 60%, 67%) | 7 | Pink | #ea76cb | rgb(234, 118, 203) | hsl(316, 73%, 69%) | 8 | Mauve | #8839ef | rgb(136, 57, 239) | hsl(266, 85%, 58%) | 9 | Red | #d20f39 | rgb(210, 15, 57) | hsl(347, 87%, 44%) | 10 | Maroon | #e64553 | rgb(230, 69, 83) | hsl(355, 76%, 59%) | 11 | Peach | #fe640b | rgb(254, 100, 11) | hsl(22, 99%, 52%) | 12 | Yellow | #df8e1d | rgb(223, 142, 29) | hsl(35, 77%, 49%) | 13 | Green | #40a02b | rgb(64, 160, 43) | hsl(109, 58%, 40%) | 14 | Teal | #179299 | rgb(23, 146, 153) | hsl(183, 74%, 35%) | 15 | Sky | #04a5e5 | rgb(4, 165, 229) | hsl(197, 97%, 46%) | 16 | Sapphire | #209fb5 | rgb(32, 159, 181) | hsl(189, 70%, 42%) | 17 | Blue | #1e66f5 | rgb(30, 102, 245) | hsl(220, 91%, 54%) | 18 | Lavender | #7287fd | rgb(114, 135, 253) | hsl(231, 97%, 72%) | 19 | Text | #4c4f69 | rgb(76, 79, 105) | hsl(234, 16%, 35%) | 20 | Subtext 1 | #5c5f77 | rgb(92, 95, 119) | hsl(233, 13%, 41%) | 21 | Subtext 0 | #6c6f85 | rgb(108, 111, 133) | hsl(233, 10%, 47%) | 22 | Overlay 2 | #7c7f93 | rgb(124, 127, 147) | hsl(232, 10%, 53%) | 23 | Overlay 1 | #8c8fa1 | rgb(140, 143, 161) | hsl(231, 10%, 59%) | 24 | Overlay 0 | #9ca0b0 | rgb(156, 160, 176) | hsl(228, 11%, 65%) | 25 | Surface 2 | #acb0be | rgb(172, 176, 190) | hsl(227, 12%, 71%) | 26 | Surface 1 | #bcc0cc | rgb(188, 192, 204) | hsl(225, 14%, 77%) | 27 | Surface 0 | #ccd0da | rgb(204, 208, 218) | hsl(223, 16%, 83%) | 28 | Base | #eff1f5 | rgb(239, 241, 245) | hsl(220, 23%, 95%) | 29 | Mantle | #e6e9ef | rgb(230, 233, 239) | hsl(220, 22%, 92%) | 30 | Crust | #dce0e8 | rgb(220, 224, 232) | hsl(220, 21%, 89%) | 31 | 32 | red: #d20f39 / hsl(347, 87%, 44%) 33 | orangey: #d3470f / hsl(17, 87%, 44%) 34 | green: #40a02b / hsl(109, 58%, 40%) 35 | dark green: #205016 // hsl(109, 58%, 20%) 36 | -------------------------------------------------------------------------------- /tests/fixtures/single/single.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: 2.0.0 4 | --- 5 | {%- macro css_rgb(v) -%} 6 | rgb({{ v.r }}, {{ v.g }}, {{ v.b }}) 7 | {%- endmacro -%} 8 | 9 | {%- macro css_hsl(v) -%} 10 | hsl({{ v.h | round }}, {{ v.s * 100 | round }}%, {{ v.l * 100 | round }}%) 11 | {%- endmacro -%} 12 | 13 | # {{ flavor.name }} 14 | 15 | | Color | Hex | RGB | HSL | 16 | |-------|-----|-----|-----| 17 | {%- for _, color in flavor.colors %} 18 | {{ color.name }} | #{{ color.hex }} | {{ self::css_rgb(v=color.rgb) }} | {{ self::css_hsl(v=color.hsl) }} | 19 | {%- endfor %} 20 | 21 | {% set orange = red | add(hue=30) -%} 22 | {% set darkgreen = green | sub(lightness=20) -%} 23 | red: #{{ red.hex }} / {{ self::css_hsl(v=red.hsl) }} 24 | orangey: #{{ orange.hex }} / {{ self::css_hsl(v=orange.hsl) }} 25 | green: #{{ green.hex }} / {{ self::css_hsl(v=green.hsl) }} 26 | dark green: #{{ darkgreen.hex }} // {{ self::css_hsl(v=darkgreen.hsl) }} 27 | -------------------------------------------------------------------------------- /tests/fixtures/singlefile-multiflavor.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: 2.0.0 4 | accent: mauve 5 | --- 6 | {% for _, flavor in flavors %} 7 | # Catppuccin {{flavor.name}} 8 | [theme] 9 | {{accent}}: #{{flavor.colors[accent].hex}} 10 | {% endfor %} 11 | -------------------------------------------------------------------------------- /tests/fixtures/singlefile-singleflavor.tera: -------------------------------------------------------------------------------- 1 | --- 2 | whiskers: 3 | version: 2.0.0 4 | accent: mauve 5 | --- 6 | # Catppuccin {{flavor.name}} 7 | [theme] 8 | {{accent}}: #{{flavor.colors[accent].hex}} 9 | --------------------------------------------------------------------------------