├── .cargo └── config.toml ├── .github ├── codeowners ├── renovate.json └── workflows │ └── ci-cd.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── license ├── readme.md ├── rust-toolchain └── src ├── bin └── chara.rs ├── bubbles.rs ├── charas ├── aya.chara ├── chocobo.chara ├── cirno.chara ├── clefairy.chara ├── cow.chara ├── eevee.chara ├── ferris.chara ├── ferris1.chara ├── flareon.chara ├── goldeen.chara ├── growlithe.chara ├── kirby.chara ├── kitten.chara ├── mario.chara ├── mew.chara ├── nemo.chara ├── pikachu.chara ├── piplup.chara ├── psyduck.chara ├── remilia-scarlet.chara ├── seaking.chara ├── togepi.chara ├── tux.chara └── wartortle.chara ├── errors.rs └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | 4 | [target.aarch64-linux-android] 5 | linker = "aarch64-linux-android33-clang++" 6 | 7 | [profile.release] 8 | strip = "symbols" 9 | lto = true 10 | codegen-units = 1 11 | opt-level = "s" 12 | -------------------------------------------------------------------------------- /.github/codeowners: -------------------------------------------------------------------------------- 1 | * @latipun7 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>latipun7/library//configs/renovate/default"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: ⚙️🚀 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: [main] 8 | tags: ["*"] 9 | 10 | concurrency: 11 | group: ci-cd-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | code-quality: 16 | name: 🦀 Code Quality 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: 🛎️ Checkout 20 | uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 21 | 22 | - name: 🦀 Install Rust 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | components: clippy, rustfmt 26 | 27 | - name: ♻️ Manage Cache 28 | uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 29 | with: 30 | path: | 31 | ~/.cargo/ 32 | target/ 33 | key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} 34 | restore-keys: | 35 | cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} 36 | cargo-${{ runner.os }}- 37 | 38 | - name: 🎨 Check Formatting 39 | run: cargo fmt --check --all 40 | 41 | - name: 📎 Check Linting 42 | run: cargo clippy --locked --all-targets --all-features -- -D warnings 43 | 44 | - name: 🧪 Run Tests 45 | run: cargo test --locked --all-targets --all-features 46 | 47 | build-artifacts: 48 | name: ⚙️ Build (${{ matrix.artifact-name }}) 49 | needs: [code-quality] 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | include: 55 | - os: ubuntu-latest 56 | artifact-name: chara-x86_64-unknown-linux-gnu 57 | cargo-target: x86_64-unknown-linux-gnu 58 | - os: ubuntu-latest 59 | artifact-name: chara-aarch64-unknown-linux-gnu 60 | cargo-target: aarch64-unknown-linux-gnu 61 | linker: gcc-aarch64-linux-gnu 62 | - os: ubuntu-latest 63 | artifact-name: chara-aarch64-linux-android 64 | cargo-target: aarch64-linux-android 65 | - os: macos-latest 66 | artifact-name: chara-x86_64-apple-darwin 67 | cargo-target: x86_64-apple-darwin 68 | - os: windows-latest 69 | artifact-name: chara-x86_64-pc-windows-gnu 70 | cargo-target: x86_64-pc-windows-gnu 71 | 72 | steps: 73 | - name: 🛎️ Checkout 74 | uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 75 | 76 | - name: 🦀 Install Rust 77 | uses: dtolnay/rust-toolchain@stable 78 | with: 79 | target: ${{ matrix.cargo-target }} 80 | 81 | - name: 🔗 Install Linker packages 82 | if: matrix.linker != '' 83 | run: | 84 | sudo apt-get -y update 85 | sudo apt-get -y install ${{ matrix.linker }} 86 | 87 | - name: 🛣️ Set Linker Path 88 | if: matrix.cargo-target == 'aarch64-linux-android' 89 | run: echo "$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH 90 | 91 | - name: ♻️ Manage Build Cache 92 | uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 93 | with: 94 | path: | 95 | ~/.cargo/ 96 | target/ 97 | key: cargo-${{ matrix.artifact-name }}-${{ hashFiles('**/Cargo.lock') }} 98 | restore-keys: | 99 | cargo-${{ matrix.artifact-name }}-${{ hashFiles('**/Cargo.lock') }} 100 | cargo-${{ matrix.artifact-name }}- 101 | 102 | - name: 🛠️ Build Binary 103 | run: cargo build --locked --release --target ${{ matrix.cargo-target }} 104 | 105 | - name: 📁 Setup Archive + Extension 106 | shell: bash 107 | run: | 108 | mkdir -p staging 109 | if [ "${{ matrix.os }}" = "windows-latest" ]; then 110 | cp "target/${{ matrix.cargo-target }}/release/chara.exe" staging/ 111 | cd staging 112 | 7z a ../${{ matrix.artifact-name }}.zip * 113 | else 114 | cp "target/${{ matrix.cargo-target }}/release/chara" staging/ 115 | cd staging 116 | zip ../${{ matrix.artifact-name }}.zip * 117 | fi 118 | 119 | - name: ⬆️ Upload Binary Artifact 120 | uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 121 | with: 122 | name: ${{ matrix.artifact-name }} 123 | path: ${{ matrix.artifact-name }}.zip 124 | retention-days: 5 125 | 126 | release: 127 | name: 🚀 Create Release 128 | if: github.ref_type == 'tag' 129 | needs: [build-artifacts] 130 | runs-on: ubuntu-latest 131 | 132 | steps: 133 | - name: ⬇️ Download All Binary Artifacts 134 | uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 135 | 136 | - name: 🗃️Create Release 137 | uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2 138 | env: 139 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 140 | with: 141 | generate_release_notes: true 142 | files: chara-*/*.zip 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys 0.59.0", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.7" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell", 61 | "windows-sys 0.59.0", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "1.3.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 69 | 70 | [[package]] 71 | name = "block-buffer" 72 | version = "0.10.4" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 75 | dependencies = [ 76 | "generic-array", 77 | ] 78 | 79 | [[package]] 80 | name = "byteorder" 81 | version = "1.5.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 84 | 85 | [[package]] 86 | name = "cfg-if" 87 | version = "1.0.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 90 | 91 | [[package]] 92 | name = "charasay" 93 | version = "3.3.0" 94 | dependencies = [ 95 | "clap", 96 | "clap_complete", 97 | "rand", 98 | "regex", 99 | "rust-embed", 100 | "strip-ansi-escapes", 101 | "textwrap", 102 | "unicode-width 0.2.0", 103 | ] 104 | 105 | [[package]] 106 | name = "clap" 107 | version = "4.5.27" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" 110 | dependencies = [ 111 | "clap_builder", 112 | "clap_derive", 113 | ] 114 | 115 | [[package]] 116 | name = "clap_builder" 117 | version = "4.5.27" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" 120 | dependencies = [ 121 | "anstream", 122 | "anstyle", 123 | "clap_lex", 124 | "strsim", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_complete" 129 | version = "4.5.42" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" 132 | dependencies = [ 133 | "clap", 134 | ] 135 | 136 | [[package]] 137 | name = "clap_derive" 138 | version = "4.5.24" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" 141 | dependencies = [ 142 | "heck", 143 | "proc-macro2", 144 | "quote", 145 | "syn", 146 | ] 147 | 148 | [[package]] 149 | name = "clap_lex" 150 | version = "0.7.4" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 153 | 154 | [[package]] 155 | name = "colorchoice" 156 | version = "1.0.3" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 159 | 160 | [[package]] 161 | name = "cpufeatures" 162 | version = "0.2.16" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" 165 | dependencies = [ 166 | "libc", 167 | ] 168 | 169 | [[package]] 170 | name = "crypto-common" 171 | version = "0.1.6" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 174 | dependencies = [ 175 | "generic-array", 176 | "typenum", 177 | ] 178 | 179 | [[package]] 180 | name = "digest" 181 | version = "0.10.7" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 184 | dependencies = [ 185 | "block-buffer", 186 | "crypto-common", 187 | ] 188 | 189 | [[package]] 190 | name = "errno" 191 | version = "0.3.10" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 194 | dependencies = [ 195 | "libc", 196 | "windows-sys 0.59.0", 197 | ] 198 | 199 | [[package]] 200 | name = "generic-array" 201 | version = "0.14.7" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 204 | dependencies = [ 205 | "typenum", 206 | "version_check", 207 | ] 208 | 209 | [[package]] 210 | name = "getrandom" 211 | version = "0.2.15" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 214 | dependencies = [ 215 | "cfg-if", 216 | "libc", 217 | "wasi", 218 | ] 219 | 220 | [[package]] 221 | name = "heck" 222 | version = "0.5.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 225 | 226 | [[package]] 227 | name = "hermit-abi" 228 | version = "0.3.9" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 231 | 232 | [[package]] 233 | name = "io-lifetimes" 234 | version = "1.0.11" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 237 | dependencies = [ 238 | "hermit-abi", 239 | "libc", 240 | "windows-sys 0.48.0", 241 | ] 242 | 243 | [[package]] 244 | name = "is_terminal_polyfill" 245 | version = "1.70.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 248 | 249 | [[package]] 250 | name = "libc" 251 | version = "0.2.169" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 254 | 255 | [[package]] 256 | name = "linux-raw-sys" 257 | version = "0.3.8" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 260 | 261 | [[package]] 262 | name = "memchr" 263 | version = "2.7.4" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 266 | 267 | [[package]] 268 | name = "once_cell" 269 | version = "1.20.2" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 272 | 273 | [[package]] 274 | name = "ppv-lite86" 275 | version = "0.2.20" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 278 | dependencies = [ 279 | "zerocopy", 280 | ] 281 | 282 | [[package]] 283 | name = "proc-macro2" 284 | version = "1.0.93" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 287 | dependencies = [ 288 | "unicode-ident", 289 | ] 290 | 291 | [[package]] 292 | name = "quote" 293 | version = "1.0.38" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 296 | dependencies = [ 297 | "proc-macro2", 298 | ] 299 | 300 | [[package]] 301 | name = "rand" 302 | version = "0.8.5" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 305 | dependencies = [ 306 | "libc", 307 | "rand_chacha", 308 | "rand_core", 309 | ] 310 | 311 | [[package]] 312 | name = "rand_chacha" 313 | version = "0.3.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 316 | dependencies = [ 317 | "ppv-lite86", 318 | "rand_core", 319 | ] 320 | 321 | [[package]] 322 | name = "rand_core" 323 | version = "0.6.4" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 326 | dependencies = [ 327 | "getrandom", 328 | ] 329 | 330 | [[package]] 331 | name = "regex" 332 | version = "1.11.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 335 | dependencies = [ 336 | "aho-corasick", 337 | "memchr", 338 | "regex-automata", 339 | "regex-syntax", 340 | ] 341 | 342 | [[package]] 343 | name = "regex-automata" 344 | version = "0.4.9" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 347 | dependencies = [ 348 | "aho-corasick", 349 | "memchr", 350 | "regex-syntax", 351 | ] 352 | 353 | [[package]] 354 | name = "regex-syntax" 355 | version = "0.8.5" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 358 | 359 | [[package]] 360 | name = "rust-embed" 361 | version = "8.5.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" 364 | dependencies = [ 365 | "rust-embed-impl", 366 | "rust-embed-utils", 367 | "walkdir", 368 | ] 369 | 370 | [[package]] 371 | name = "rust-embed-impl" 372 | version = "8.5.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" 375 | dependencies = [ 376 | "proc-macro2", 377 | "quote", 378 | "rust-embed-utils", 379 | "syn", 380 | "walkdir", 381 | ] 382 | 383 | [[package]] 384 | name = "rust-embed-utils" 385 | version = "8.5.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" 388 | dependencies = [ 389 | "sha2", 390 | "walkdir", 391 | ] 392 | 393 | [[package]] 394 | name = "rustix" 395 | version = "0.37.28" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" 398 | dependencies = [ 399 | "bitflags", 400 | "errno", 401 | "io-lifetimes", 402 | "libc", 403 | "linux-raw-sys", 404 | "windows-sys 0.48.0", 405 | ] 406 | 407 | [[package]] 408 | name = "same-file" 409 | version = "1.0.6" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 412 | dependencies = [ 413 | "winapi-util", 414 | ] 415 | 416 | [[package]] 417 | name = "sha2" 418 | version = "0.10.8" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 421 | dependencies = [ 422 | "cfg-if", 423 | "cpufeatures", 424 | "digest", 425 | ] 426 | 427 | [[package]] 428 | name = "smawk" 429 | version = "0.3.2" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 432 | 433 | [[package]] 434 | name = "strip-ansi-escapes" 435 | version = "0.2.1" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" 438 | dependencies = [ 439 | "vte", 440 | ] 441 | 442 | [[package]] 443 | name = "strsim" 444 | version = "0.11.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 447 | 448 | [[package]] 449 | name = "syn" 450 | version = "2.0.96" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 453 | dependencies = [ 454 | "proc-macro2", 455 | "quote", 456 | "unicode-ident", 457 | ] 458 | 459 | [[package]] 460 | name = "terminal_size" 461 | version = "0.2.6" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" 464 | dependencies = [ 465 | "rustix", 466 | "windows-sys 0.48.0", 467 | ] 468 | 469 | [[package]] 470 | name = "textwrap" 471 | version = "0.16.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 474 | dependencies = [ 475 | "smawk", 476 | "terminal_size", 477 | "unicode-linebreak", 478 | "unicode-width 0.1.14", 479 | ] 480 | 481 | [[package]] 482 | name = "typenum" 483 | version = "1.17.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 486 | 487 | [[package]] 488 | name = "unicode-ident" 489 | version = "1.0.14" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 492 | 493 | [[package]] 494 | name = "unicode-linebreak" 495 | version = "0.1.5" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 498 | 499 | [[package]] 500 | name = "unicode-width" 501 | version = "0.1.14" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 504 | 505 | [[package]] 506 | name = "unicode-width" 507 | version = "0.2.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 510 | 511 | [[package]] 512 | name = "utf8parse" 513 | version = "0.2.2" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 516 | 517 | [[package]] 518 | name = "version_check" 519 | version = "0.9.5" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 522 | 523 | [[package]] 524 | name = "vte" 525 | version = "0.14.1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" 528 | dependencies = [ 529 | "memchr", 530 | ] 531 | 532 | [[package]] 533 | name = "walkdir" 534 | version = "2.5.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 537 | dependencies = [ 538 | "same-file", 539 | "winapi-util", 540 | ] 541 | 542 | [[package]] 543 | name = "wasi" 544 | version = "0.11.0+wasi-snapshot-preview1" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 547 | 548 | [[package]] 549 | name = "winapi-util" 550 | version = "0.1.9" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 553 | dependencies = [ 554 | "windows-sys 0.59.0", 555 | ] 556 | 557 | [[package]] 558 | name = "windows-sys" 559 | version = "0.48.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 562 | dependencies = [ 563 | "windows-targets 0.48.5", 564 | ] 565 | 566 | [[package]] 567 | name = "windows-sys" 568 | version = "0.59.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 571 | dependencies = [ 572 | "windows-targets 0.52.6", 573 | ] 574 | 575 | [[package]] 576 | name = "windows-targets" 577 | version = "0.48.5" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 580 | dependencies = [ 581 | "windows_aarch64_gnullvm 0.48.5", 582 | "windows_aarch64_msvc 0.48.5", 583 | "windows_i686_gnu 0.48.5", 584 | "windows_i686_msvc 0.48.5", 585 | "windows_x86_64_gnu 0.48.5", 586 | "windows_x86_64_gnullvm 0.48.5", 587 | "windows_x86_64_msvc 0.48.5", 588 | ] 589 | 590 | [[package]] 591 | name = "windows-targets" 592 | version = "0.52.6" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 595 | dependencies = [ 596 | "windows_aarch64_gnullvm 0.52.6", 597 | "windows_aarch64_msvc 0.52.6", 598 | "windows_i686_gnu 0.52.6", 599 | "windows_i686_gnullvm", 600 | "windows_i686_msvc 0.52.6", 601 | "windows_x86_64_gnu 0.52.6", 602 | "windows_x86_64_gnullvm 0.52.6", 603 | "windows_x86_64_msvc 0.52.6", 604 | ] 605 | 606 | [[package]] 607 | name = "windows_aarch64_gnullvm" 608 | version = "0.48.5" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 611 | 612 | [[package]] 613 | name = "windows_aarch64_gnullvm" 614 | version = "0.52.6" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 617 | 618 | [[package]] 619 | name = "windows_aarch64_msvc" 620 | version = "0.48.5" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 623 | 624 | [[package]] 625 | name = "windows_aarch64_msvc" 626 | version = "0.52.6" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 629 | 630 | [[package]] 631 | name = "windows_i686_gnu" 632 | version = "0.48.5" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 635 | 636 | [[package]] 637 | name = "windows_i686_gnu" 638 | version = "0.52.6" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 641 | 642 | [[package]] 643 | name = "windows_i686_gnullvm" 644 | version = "0.52.6" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 647 | 648 | [[package]] 649 | name = "windows_i686_msvc" 650 | version = "0.48.5" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 653 | 654 | [[package]] 655 | name = "windows_i686_msvc" 656 | version = "0.52.6" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 659 | 660 | [[package]] 661 | name = "windows_x86_64_gnu" 662 | version = "0.48.5" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 665 | 666 | [[package]] 667 | name = "windows_x86_64_gnu" 668 | version = "0.52.6" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 671 | 672 | [[package]] 673 | name = "windows_x86_64_gnullvm" 674 | version = "0.48.5" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 677 | 678 | [[package]] 679 | name = "windows_x86_64_gnullvm" 680 | version = "0.52.6" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 683 | 684 | [[package]] 685 | name = "windows_x86_64_msvc" 686 | version = "0.48.5" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 689 | 690 | [[package]] 691 | name = "windows_x86_64_msvc" 692 | version = "0.52.6" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 695 | 696 | [[package]] 697 | name = "zerocopy" 698 | version = "0.7.35" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 701 | dependencies = [ 702 | "byteorder", 703 | "zerocopy-derive", 704 | ] 705 | 706 | [[package]] 707 | name = "zerocopy-derive" 708 | version = "0.7.35" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 711 | dependencies = [ 712 | "proc-macro2", 713 | "quote", 714 | "syn", 715 | ] 716 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charasay" 3 | version = "3.3.0" 4 | authors = ["Latif Sulistyo "] 5 | edition = "2021" 6 | description = "The future of cowsay 🐮! Colorful characters saying something 🗨️." 7 | repository = "https://github.com/latipun7/charasay" 8 | license = "MIT" 9 | readme = "readme.md" 10 | keywords = ["cowsay", "print", "ansi"] 11 | categories = [ 12 | "command-line-utilities", 13 | "value-formatting", 14 | "text-processing", 15 | "visualization", 16 | "rendering", 17 | ] 18 | 19 | [[bin]] 20 | name = "chara" 21 | 22 | [dependencies] 23 | clap_complete = "4.5.42" 24 | clap = { version = "4.5.27", features = ["derive"] } 25 | rust-embed = { version = "8.5.0", features = ["debug-embed"] } 26 | textwrap = { version = "0.16.1", features = ["terminal_size"] } 27 | unicode-width = "0.2.0" 28 | regex = "1.11.1" 29 | rand = "0.8.5" 30 | strip-ansi-escapes = "0.2.1" 31 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Latif Sulistyo 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 | # `charasay 🐮` 2 | 3 | [![Discord][discord-image]][discord-url] 4 | [![GitHub Workflow Status][workflow-image]][workflow-url] 5 | 6 | > **🐈 The future of cowsay 🐮! Colorful characters saying something 🗨️.** 7 | > 8 | > Re-engineered cowsay in rust 🦀. Display colorful ANSI arts saying something 9 | > in your terminal 💻. 10 | 11 | ![Default character](https://user-images.githubusercontent.com/20012970/222370473-8a61c85f-7a14-49a4-a61f-44540d959286.png) 12 | 13 | ## Motivation 14 | 15 | I use terminal emulator almost every day. I stare at it so much. I need some 16 | entertainment in terminal, so I found [`ponysay`][ponysay] which is beautiful 17 | and giving my terminal some colors. But `ponysay` kind of bloated for me since 18 | I don't display all those ponies. 19 | 20 | So, I want to make my own minimal tool to make my terminal so colorful and 21 | display the character that I like. This chance is a great time to learn `rust`. 22 | This project is mainly for me to learn rust and hopefully I get some feedback 23 | while this make us all happy 😁. 24 | 25 | ## Installation 26 | 27 | ### AUR 28 | 29 | For Arch Linux, package available via AUR. Example install this with AUR helper: 30 | 31 | ```console 32 | yay -S charasay 33 | ``` 34 | 35 | or 36 | 37 | ```console 38 | yay -S charasay-bin 39 | ``` 40 | 41 | ### Cargo 42 | 43 | If you have `rustup` or `cargo`, this tool available on crates.io. Install this with: 44 | 45 | ```console 46 | cargo install charasay 47 | ``` 48 | 49 | ### Manual 50 | 51 | Just donwload from the [release page](https://github.com/latipun7/charasay/releases) 52 | for your compatible Operating System, then extract the zip archive, give permission 53 | to execute on extracted file, then place it on your `PATH`. 54 | 55 | Alternatively, clone this repository, then build this with `cargo build --release`. 56 | 57 | ### Prerequisites 58 | 59 | To display characters, your terminal needs to support true color (24-bit color). 60 | Unicode fonts are needed to render the border of speech bubble. 61 | 62 | ## Usage 63 | 64 | ### Display Default Character to Say Something 65 | 66 | Run `chara say something that motivating.` It would display colorful cow saying 67 | `something that motivating.`. 68 | 69 | If message is empty, it would accept from standard input, piping would works: 70 | `fortune | chara say`. 71 | 72 | ### Display Different Character 73 | 74 | Run `chara say -f ferris "Hello rustaceans!"`. 75 | 76 | It could display external `.chara` files: `chara say -f ~/path/test.chara "Nice"`. 77 | 78 | > Note: `.chara` files could be generated from PNG file. 79 | > 80 | > I want to implement this builtin in this tool. For now, you could generate 81 | > `.cow` file with [Cowsay file converter][cowsay-converter] then rename `.cow` 82 | > into `.chara`. 83 | 84 | ### Shell Completions 85 | 86 | Shell completions also available with `chara completions` which would print out 87 | completions script to standard output. Please consult to your shell documentation 88 | on how to add completions. 89 | 90 | ### Consult to Help Command 91 | 92 | For updated usage please consult to help command. 93 | 94 | ```console 95 | $ chara --help 96 | The future of cowsay 🐮! Colorful characters saying something 🗨️. 97 | 98 | Usage: chara 99 | 100 | Commands: 101 | say Make the character say something 102 | completions Generate completions for shell. Default to current shell 103 | convert TODO: Convert pixel-arts PNG to chara files 104 | help Print this message or the help of the given subcommand(s) 105 | 106 | Options: 107 | -h, --help Print help 108 | -V, --version Print version 109 | ``` 110 | 111 | ```console 112 | $ chara help say 113 | Make the character say something 114 | 115 | Usage: chara say [OPTIONS] [MESSAGE]... 116 | 117 | Arguments: 118 | [MESSAGE]... Messages that chara want to say/think. If empty, read from STDIN 119 | 120 | Options: 121 | -r, --random Choose random chara 122 | -a, --all Print all available chara 123 | -t, --think Make chara only thinking about it, not saying it 124 | -w, --width Max width of speech bubble. Default to terminal width 125 | -f, --file Which chara should say/think 126 | -h, --help Print help 127 | ``` 128 | 129 | ![Ferris](https://user-images.githubusercontent.com/20012970/222370485-3d43052f-977a-441e-a0c2-efc538d8e693.png) 130 | 131 | ## Hacking to the Gate~! 🧑‍💻🎶 132 | 133 | [MIT License](./license) © Latif Sulistyo 134 | 135 | ### Acknowledgements 136 | 137 | - All pixel-art artist on [each chara files](./src/charas/) 138 | - [**@charc0al**][cowsay-converter] for cowsay file converter 139 | - Rustaceans 🦀 140 | 141 | 142 | 143 | [discord-image]: https://img.shields.io/discord/758271814153011201?label=Developers%20Indonesia&logo=discord&style=flat-square 144 | [discord-url]: https://discord.gg/njSj2Nq "Chat and discuss at Developers Indonesia" 145 | [workflow-image]: https://img.shields.io/github/actions/workflow/status/latipun7/charasay/ci-cd.yml?label=CI%2FCD&logo=github-actions&style=flat-square 146 | [workflow-url]: https://github.com/latipun7/charasay/actions "GitHub Actions" 147 | [ponysay]: https://github.com/erkin/ponysay "Ponysay GitHub Repository" 148 | [cowsay-converter]: https://charc0al.github.io/cowsay-files/converter/ "Cowsay File Converter" 149 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | profile = "default" 4 | components = ["rust-analyzer"] 5 | -------------------------------------------------------------------------------- /src/bin/chara.rs: -------------------------------------------------------------------------------- 1 | use charasay::errors::CustomError; 2 | use std::{ 3 | error::Error, 4 | io::{stdin, stdout, Read}, 5 | path::PathBuf, 6 | }; 7 | 8 | use charasay::{bubbles::BubbleType, format_character, print_character, Chara, BUILTIN_CHARA}; 9 | use clap::{Args, Command, CommandFactory, Parser, Subcommand}; 10 | use clap_complete::{generate, Generator, Shell}; 11 | use textwrap::termwidth; 12 | 13 | const BORDER_WIDTH: usize = 6; 14 | 15 | #[derive(Parser, Debug)] 16 | #[command(author, version, about, long_about, name = "chara")] 17 | struct Cli { 18 | #[command(subcommand)] 19 | command: Commands, 20 | } 21 | 22 | #[derive(Subcommand, Debug)] 23 | enum Commands { 24 | /// Make the character say something. Default to cow. 25 | Say { 26 | /// Messages that chara want to say/think. If empty, read from standard input. 27 | message: Vec, 28 | 29 | /// Choose bubble type to use. Default to round. 30 | #[arg(short = 't', long, value_enum)] 31 | bubble_type: Option, 32 | 33 | /// Max width of speech bubble. Default to terminal width. 34 | #[arg(short, long)] 35 | width: Option, 36 | 37 | #[command(flatten)] 38 | charas: Charas, 39 | }, 40 | 41 | /// Generate completions for shell. Default to current shell. 42 | Completions { 43 | /// Shell syntax to use. Infer current shell when missing, fallback to bash. 44 | #[arg(short, long, value_enum)] 45 | shell: Option, 46 | }, 47 | 48 | /// List all built-in charas. 49 | List, 50 | 51 | /// Print only the character. Default to cow. 52 | Print { 53 | #[command(flatten)] 54 | charas: Charas, 55 | }, 56 | 57 | /// TODO: Convert pixel-arts PNG to chara files. 58 | Convert { 59 | /// PNG file path. 60 | image: PathBuf, 61 | }, 62 | } 63 | 64 | #[derive(Args, Debug)] 65 | #[group(multiple = false)] 66 | struct Charas { 67 | /// Choose built-in chara. 68 | #[arg(short, long, value_parser = BUILTIN_CHARA)] 69 | chara: Option, 70 | 71 | /// Choose custom chara file. 72 | #[arg(short, long)] 73 | file: Option, 74 | 75 | /// Choose random chara. 76 | #[arg(short, long)] 77 | random: bool, 78 | 79 | /// Print all built-in charas. 80 | #[arg(short, long)] 81 | all: bool, 82 | } 83 | 84 | fn print_completions(gen: G, cmd: &mut Command) { 85 | generate(gen, cmd, cmd.get_name().to_string(), &mut stdout()); 86 | } 87 | 88 | fn print_all_characters( 89 | messages: &str, 90 | max_width: usize, 91 | bubble_type: BubbleType, 92 | ) -> Result<(), Box> { 93 | let charas = BUILTIN_CHARA; 94 | for chara in charas { 95 | println!("\n\n{}", chara); 96 | println!( 97 | "{}", 98 | format_character( 99 | messages, 100 | &Chara::Builtin(chara.to_string()), 101 | max_width, 102 | bubble_type 103 | )? 104 | ); 105 | } 106 | Ok(()) 107 | } 108 | 109 | fn print_random_character( 110 | messages: &str, 111 | max_width: usize, 112 | bubble_type: BubbleType, 113 | ) -> Result<(), Box> { 114 | let chara = Chara::Random; 115 | println!( 116 | "{}", 117 | format_character(messages, &chara, max_width, bubble_type)? 118 | ); 119 | Ok(()) 120 | } 121 | 122 | fn print_specified_character( 123 | messages: &str, 124 | chara_name: &str, 125 | max_width: usize, 126 | bubble_type: BubbleType, 127 | ) -> Result<(), Box> { 128 | let chara = Chara::Builtin(chara_name.to_string()); 129 | println!( 130 | "{}", 131 | format_character(messages, &chara, max_width, bubble_type)? 132 | ); 133 | Ok(()) 134 | } 135 | 136 | fn print_character_from_file( 137 | messages: &str, 138 | file_path: &str, 139 | max_width: usize, 140 | bubble_type: BubbleType, 141 | ) -> Result<(), Box> { 142 | let chara = Chara::File(file_path.into()); 143 | println!( 144 | "{}", 145 | format_character(messages, &chara, max_width, bubble_type)? 146 | ); 147 | Ok(()) 148 | } 149 | 150 | fn print_characters( 151 | charas: Charas, 152 | messages: String, 153 | max_width: usize, 154 | bubble_type: BubbleType, 155 | ) -> Result<(), Box> { 156 | match charas { 157 | Charas { all: true, .. } => { 158 | // Print all built-in characters 159 | print_all_characters(&messages, max_width, bubble_type)?; 160 | } 161 | Charas { random: true, .. } => { 162 | // Print a random character 163 | print_random_character(&messages, max_width, bubble_type)?; 164 | } 165 | Charas { chara: Some(s), .. } => { 166 | // Print the specified character 167 | print_specified_character(&messages, &s, max_width, bubble_type)?; 168 | } 169 | Charas { 170 | file: Some(path), .. 171 | } => { 172 | // Print the character from a file 173 | print_character_from_file(&messages, path.to_str().unwrap(), max_width, bubble_type)?; 174 | } 175 | _ => { 176 | // Print the default character (cow) 177 | let chara = Chara::Builtin("cow".to_string()); 178 | println!( 179 | "{}", 180 | format_character(&messages, &chara, max_width, bubble_type)? 181 | ); 182 | } 183 | } 184 | Ok(()) 185 | } 186 | 187 | fn read_input(message: Vec) -> Result { 188 | let mut messages = message.join(" "); 189 | 190 | if messages.is_empty() { 191 | let mut buffer = String::new(); 192 | 193 | if let Err(err) = stdin().read_to_string(&mut buffer) { 194 | return Err(CustomError::IoError(err)); 195 | } 196 | 197 | messages = buffer.trim_end().to_string(); 198 | } 199 | 200 | Ok(messages) 201 | } 202 | 203 | fn main() -> Result<(), Box> { 204 | let cli = Cli::parse(); 205 | 206 | match cli.command { 207 | Commands::Say { 208 | message, 209 | bubble_type, 210 | width, 211 | charas, 212 | } => { 213 | let messages = match read_input(message) { 214 | Ok(s) => s, 215 | Err(err) => { 216 | eprintln!("Failed to read input: {:#?}", err); 217 | std::process::exit(1); 218 | } 219 | }; 220 | 221 | let max_width = width.unwrap_or(termwidth() - BORDER_WIDTH); 222 | let bubble_type = match bubble_type { 223 | Some(bt) => bt, 224 | None => BubbleType::Round, 225 | }; 226 | 227 | print_characters(charas, messages, max_width, bubble_type)?; 228 | } 229 | 230 | Commands::Completions { shell } => { 231 | let mut cmd = Cli::command(); 232 | let gen = match shell { 233 | Some(s) => s, 234 | None => Shell::from_env().unwrap_or(Shell::Bash), 235 | }; 236 | 237 | print_completions(gen, &mut cmd); 238 | } 239 | 240 | Commands::List => { 241 | let charas = BUILTIN_CHARA.join(" "); 242 | println!("{}", charas) 243 | } 244 | 245 | Commands::Print { charas } => { 246 | let chara = match (charas.all, charas.random, charas.chara, charas.file) { 247 | (true, _, _, _) => Chara::All, 248 | (_, true, _, _) => Chara::Random, 249 | (_, _, Some(s), _) => Chara::Builtin(s), 250 | (_, _, _, Some(path)) => Chara::File(path), 251 | _ => Chara::Builtin("cow".to_string()), 252 | }; 253 | 254 | println!("{}", print_character(&chara)); 255 | } 256 | 257 | Commands::Convert { image: _ } => todo!(), 258 | } 259 | Ok(()) 260 | } 261 | -------------------------------------------------------------------------------- /src/bubbles.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, str::from_utf8}; 2 | 3 | use clap::ValueEnum; 4 | use strip_ansi_escapes::strip; 5 | use textwrap::fill; 6 | use unicode_width::UnicodeWidthStr; 7 | 8 | #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, ValueEnum)] 9 | pub enum BubbleType { 10 | Think, 11 | Round, 12 | Cowsay, 13 | Ascii, 14 | Unicode, 15 | } 16 | 17 | const THINK_BUBBLE: SpeechBubble = SpeechBubble { 18 | corner_top_left: "(", 19 | top: "⁀", 20 | corner_top_right: ")\n", 21 | top_right: " )\n", 22 | right: " )\n", 23 | bottom_right: " )\n", 24 | corner_bottom_right: ")\n", 25 | bottom: "‿", 26 | corner_bottom_left: "(", 27 | bottom_left: "( ", 28 | left: "( ", 29 | top_left: "( ", 30 | short_left: "( ", 31 | short_right: " )\n", 32 | }; 33 | 34 | const ROUND_BUBBLE: SpeechBubble = SpeechBubble { 35 | corner_top_left: "╭", 36 | top: "─", 37 | corner_top_right: "╮\n", 38 | top_right: " │\n", 39 | right: " │\n", 40 | bottom_right: " │\n", 41 | corner_bottom_right: "╯\n", 42 | bottom: "─", 43 | corner_bottom_left: "╰", 44 | bottom_left: "│ ", 45 | left: "│ ", 46 | top_left: "│ ", 47 | short_left: "│ ", 48 | short_right: " │\n", 49 | }; 50 | 51 | const COWSAY_BUBBLE: SpeechBubble = SpeechBubble { 52 | corner_top_left: " ", 53 | top: "_", 54 | corner_top_right: " \n", 55 | top_right: " \\\n", 56 | right: " |\n", 57 | bottom_right: " /\n", 58 | corner_bottom_right: " \n", 59 | bottom: "-", 60 | corner_bottom_left: " ", 61 | bottom_left: "\\ ", 62 | left: "| ", 63 | top_left: "/ ", 64 | short_left: "< ", 65 | short_right: " >\n", 66 | }; 67 | 68 | const ASCII_BUBBLE: SpeechBubble = SpeechBubble { 69 | corner_top_left: " ", 70 | top: "_", 71 | corner_top_right: " \n", 72 | top_right: " \\\n", 73 | right: " |\n", 74 | bottom_right: " |\n", 75 | corner_bottom_right: "/\n", 76 | bottom: "_", 77 | corner_bottom_left: "\\", 78 | bottom_left: "| ", 79 | left: "| ", 80 | top_left: "/ ", 81 | short_left: "/ ", 82 | short_right: " \\\n", 83 | }; 84 | 85 | const UNICODE_BUBBLE: SpeechBubble = SpeechBubble { 86 | corner_top_left: "┌", 87 | top: "─", 88 | corner_top_right: "┐\n", 89 | top_right: " │\n", 90 | right: " │\n", 91 | bottom_right: " │\n", 92 | corner_bottom_right: "┘\n", 93 | bottom: "─", 94 | corner_bottom_left: "└", 95 | bottom_left: "│ ", 96 | left: "│ ", 97 | top_left: "│ ", 98 | short_left: "│ ", 99 | short_right: " │\n", 100 | }; 101 | 102 | #[derive(Debug)] 103 | pub struct SpeechBubble { 104 | corner_top_left: &'static str, 105 | top: &'static str, 106 | corner_top_right: &'static str, 107 | top_right: &'static str, 108 | right: &'static str, 109 | bottom_right: &'static str, 110 | corner_bottom_right: &'static str, 111 | bottom: &'static str, 112 | corner_bottom_left: &'static str, 113 | bottom_left: &'static str, 114 | left: &'static str, 115 | top_left: &'static str, 116 | short_left: &'static str, 117 | short_right: &'static str, 118 | } 119 | 120 | impl SpeechBubble { 121 | pub fn new(bubble_type: BubbleType) -> Self { 122 | match bubble_type { 123 | BubbleType::Think => THINK_BUBBLE, 124 | BubbleType::Round => ROUND_BUBBLE, 125 | BubbleType::Cowsay => COWSAY_BUBBLE, 126 | BubbleType::Ascii => ASCII_BUBBLE, 127 | BubbleType::Unicode => UNICODE_BUBBLE, 128 | } 129 | } 130 | 131 | fn line_len(line: &str) -> Result> { 132 | let stripped = strip(line); 133 | let text = from_utf8(stripped.as_slice()); 134 | 135 | Ok(text.map(UnicodeWidthStr::width).unwrap_or(0)) 136 | } 137 | 138 | fn longest_line(lines: &[&str]) -> Result> { 139 | let line_lengths = lines 140 | .iter() 141 | .map(|line| Self::line_len(line)) 142 | .collect::, _>>()?; 143 | Ok(line_lengths.into_iter().max().unwrap_or(0)) 144 | } 145 | 146 | pub fn create(self, messages: &str, max_width: &usize) -> Result> { 147 | const SPACE: &str = " "; 148 | let wrapped = fill(messages, *max_width).replace('\t', " "); 149 | let lines: Vec<&str> = wrapped.lines().collect(); 150 | let line_count = lines.len(); 151 | let actual_width = Self::longest_line(&lines)?; 152 | 153 | let total_size_buffer = (actual_width + 5) * 2 + line_count * (actual_width + 6); 154 | 155 | let mut write_buffer = Vec::with_capacity(total_size_buffer); 156 | 157 | // draw top box border 158 | write_buffer.push(self.corner_top_left); 159 | for _ in 0..(actual_width + 4) { 160 | write_buffer.push(self.top); 161 | } 162 | write_buffer.push(self.corner_top_right); 163 | 164 | // draw inner borders & messages 165 | for (i, line) in lines.into_iter().enumerate() { 166 | let left_border = match (line_count, i) { 167 | (1, _) => self.short_left, 168 | (_, 0) => self.top_left, 169 | (_, i) if i == line_count - 1 => self.bottom_left, 170 | _ => self.left, 171 | }; 172 | write_buffer.push(left_border); 173 | 174 | let line_len = Self::line_len(line)?; 175 | write_buffer.push(line); 176 | write_buffer.resize(write_buffer.len() + actual_width - line_len, SPACE); 177 | 178 | let right_border = match (line_count, i) { 179 | (1, _) => self.short_right, 180 | (_, 0) => self.top_right, 181 | (_, i) if i == line_count - 1 => self.bottom_right, 182 | _ => self.right, 183 | }; 184 | write_buffer.push(right_border); 185 | } 186 | 187 | // draw bottom box border 188 | write_buffer.push(self.corner_bottom_left); 189 | for _ in 0..(actual_width + 4) { 190 | write_buffer.push(self.bottom); 191 | } 192 | write_buffer.push(self.corner_bottom_right); 193 | 194 | Ok(write_buffer.join("")) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/charas/aya.chara: -------------------------------------------------------------------------------- 1 | # Aya (Touhou) - true color 2 | # by Unknown - https://m.minecraft.novaskin.me/skin/5196393402/Aya-Touhou-Pixel-Art 3 | 4 | $x = "\e[49m "; #reset color 5 | $t = "$thoughts"; 6 | $a = "\e[48;2;133;1;33m "; 7 | $b = "\e[48;2;199;1;33m "; 8 | $c = "\e[48;2;246;33;98m "; 9 | $d = "\e[48;2;48;49;49m "; 10 | $e = "\e[48;2;99;99;90m "; 11 | $f = "\e[48;2;181;180;172m "; 12 | $g = "\e[48;2;181;180;133m "; 13 | $h = "\e[48;2;246;247;230m "; 14 | $i = "\e[48;2;24;24;24m "; 15 | $j = "\e[48;2;231;149;133m "; 16 | $k = "\e[48;2;247;230;149m "; 17 | $l = "\e[48;2;133;132;83m "; 18 | $m = "\e[48;2;255;254;254m "; 19 | $n = "\e[48;2;246;181;149m "; 20 | $o = "\e[48;2;214;0;82m "; 21 | $p = "\e[48;2;231;133;67m "; 22 | $q = "\e[48;2;80;81;81m "; 23 | $r = "\e[48;2;199;199;206m "; 24 | $s = "\e[48;2;254;230;90m "; 25 | $u = "\e[48;2;33;32;33m "; 26 | 27 | $the_chara = < fmt::Result { 14 | match self { 15 | // Format the error message as "IO error: " 16 | CustomError::IoError(err) => write!(f, "IO error: {}", err), 17 | // Format the error message as "UTF-8 error: " 18 | CustomError::Utf8Error(err) => write!(f, "UTF-8 error: {}", err), 19 | } 20 | } 21 | } 22 | 23 | impl Error for CustomError {} 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, error::Error, fs::File, io::Read, path::PathBuf, str::from_utf8}; 2 | 3 | use rand::seq::SliceRandom; 4 | use regex::Regex; 5 | use rust_embed::RustEmbed; 6 | 7 | use crate::bubbles::{BubbleType, SpeechBubble}; 8 | 9 | pub mod bubbles; 10 | pub mod errors; 11 | 12 | #[derive(RustEmbed, Debug)] 13 | #[folder = "src/charas"] 14 | struct Asset; 15 | 16 | /// Source chara to load, either builtin or from external file. 17 | #[derive(Debug)] 18 | pub enum Chara { 19 | All, 20 | Builtin(String), 21 | File(PathBuf), 22 | Raw(String), 23 | Random, 24 | } 25 | 26 | /// All built-in characters name. 27 | pub const BUILTIN_CHARA: [&str; 24] = [ 28 | "aya", 29 | "chocobo", 30 | "cirno", 31 | "clefairy", 32 | "cow", 33 | "eevee", 34 | "ferris", 35 | "ferris1", 36 | "flareon", 37 | "goldeen", 38 | "growlithe", 39 | "kirby", 40 | "kitten", 41 | "mario", 42 | "mew", 43 | "nemo", 44 | "pikachu", 45 | "piplup", 46 | "psyduck", 47 | "remilia-scarlet", 48 | "seaking", 49 | "togepi", 50 | "tux", 51 | "wartortle", 52 | ]; 53 | 54 | fn load_raw_chara_string(chara: &Chara) -> String { 55 | let mut raw_chara = String::new(); 56 | 57 | match chara { 58 | Chara::File(s) => { 59 | let mut file = File::open(s).unwrap_or_else(|err| todo!("Log ERROR: {:#?}", err)); 60 | file.read_to_string(&mut raw_chara) 61 | .unwrap_or_else(|err| todo!("Log ERROR: {:#?}", err)); 62 | } 63 | 64 | Chara::Builtin(s) => { 65 | let name = format!("{}.chara", s); 66 | let asset = Asset::get(&name).unwrap(); 67 | raw_chara = from_utf8(&asset.data) 68 | .unwrap_or_else(|err| todo!("Log ERROR: {:#?}", err)) 69 | .to_string(); 70 | } 71 | 72 | Chara::Raw(s) => { 73 | raw_chara = s.to_string(); 74 | } 75 | 76 | Chara::All => { 77 | let charas = Asset::iter() 78 | .map(|file| { 79 | let name = file.trim_end_matches(".chara"); 80 | let asset = Asset::get(&file).unwrap(); 81 | format!("{} 👇\n{}", name, String::from_utf8_lossy(&asset.data)) 82 | }) 83 | .collect::>(); 84 | raw_chara = charas.join("\n+\n"); 85 | } 86 | 87 | Chara::Random => { 88 | let charas = Asset::iter().collect::>(); 89 | let choosen_chara = charas.choose(&mut rand::thread_rng()).unwrap().clone(); 90 | let asset = Asset::get(&choosen_chara).unwrap(); 91 | raw_chara = from_utf8(&asset.data) 92 | .unwrap_or_else(|err| todo!("Log ERROR: {:#?}", err)) 93 | .to_string(); 94 | } 95 | } 96 | 97 | raw_chara 98 | } 99 | 100 | fn strip_chara_string(raw_chara: &str) -> String { 101 | raw_chara 102 | .split('\n') 103 | .filter(|line| { 104 | !line.starts_with('#') 105 | && !line.starts_with("$x") 106 | && !line.contains("$thoughts") 107 | && !line.is_empty() 108 | }) 109 | .collect::>() 110 | .join("\n") 111 | .replace("\\e", "\x1B") 112 | } 113 | 114 | fn parse_character(chara: &Chara, voice_line: &str) -> String { 115 | let raw_chara = load_raw_chara_string(chara); 116 | let stripped_chara = strip_chara_string(&raw_chara); 117 | let charas = stripped_chara.split('+').collect::>(); 118 | let mut parsed = String::new(); 119 | 120 | let re = Regex::new(r"(?\$\w).*=.*(?\x1B\[.*m\s*).;").unwrap(); 121 | for chara in charas { 122 | // extract variable definition to HashMap 123 | let replacers: Vec> = re 124 | .captures_iter(chara) 125 | .map(|cap| { 126 | re.capture_names() 127 | .flatten() 128 | .filter_map(|n| Some((n, cap.name(n)?.as_str()))) 129 | .collect() 130 | }) 131 | .collect(); 132 | 133 | let mut chara_body = chara 134 | .split('\n') 135 | .filter(|line| !line.contains('=') && !line.contains("EOC")) 136 | .collect::>() 137 | .join("\n") 138 | .trim_end() 139 | .replace("$x", "\x1B[49m ") 140 | .replace("$t", voice_line); 141 | 142 | // replace variable from character's body with actual value 143 | for replacer in replacers { 144 | chara_body = chara_body.replace( 145 | replacer.get("var").copied().unwrap(), 146 | replacer.get("val").copied().unwrap(), 147 | ); 148 | } 149 | 150 | parsed.push_str(&format!("{}\n\n\n", &chara_body)) 151 | } 152 | 153 | parsed.trim_end().to_string() 154 | } 155 | 156 | /// Format arguments to form complete charasay 157 | pub fn format_character( 158 | messages: &str, 159 | chara: &Chara, 160 | max_width: usize, 161 | bubble_type: BubbleType, 162 | ) -> Result> { 163 | let voice_line: &str; 164 | let bubble_type = match bubble_type { 165 | BubbleType::Think => { 166 | voice_line = "o "; 167 | BubbleType::Think 168 | } 169 | BubbleType::Round => { 170 | voice_line = "╲ "; 171 | BubbleType::Round 172 | } 173 | BubbleType::Cowsay => { 174 | voice_line = "\\ "; 175 | BubbleType::Cowsay 176 | } 177 | BubbleType::Ascii => { 178 | voice_line = "\\ "; 179 | BubbleType::Ascii 180 | } 181 | BubbleType::Unicode => { 182 | voice_line = "╲ "; 183 | BubbleType::Unicode 184 | } 185 | }; 186 | 187 | let speech_bubble = SpeechBubble::new(bubble_type); 188 | let speech = speech_bubble.create(messages, &max_width)?; 189 | let character = parse_character(chara, voice_line); 190 | 191 | Ok(format!("{}{}", speech, character)) 192 | } 193 | 194 | /// Print only the character 195 | pub fn print_character(chara: &Chara) -> String { 196 | parse_character(chara, " ") 197 | } 198 | --------------------------------------------------------------------------------