├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── cato.png ├── paper.png └── src ├── default.syncat ├── dirs.rs ├── main.rs ├── printer.rs ├── str_width.rs ├── table.rs ├── termpix.rs └── words.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: foxfriends 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: foxfriends 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk -------------------------------------------------------------------------------- /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 = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "aligned-vec" 22 | version = "0.5.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" 25 | 26 | [[package]] 27 | name = "ansi_term" 28 | version = "0.12.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 31 | dependencies = [ 32 | "winapi", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 79 | dependencies = [ 80 | "anstyle", 81 | "windows-sys", 82 | ] 83 | 84 | [[package]] 85 | name = "anyhow" 86 | version = "1.0.95" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 89 | 90 | [[package]] 91 | name = "arbitrary" 92 | version = "1.4.1" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 95 | 96 | [[package]] 97 | name = "arg_enum_proc_macro" 98 | version = "0.3.4" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 101 | dependencies = [ 102 | "proc-macro2", 103 | "quote", 104 | "syn", 105 | ] 106 | 107 | [[package]] 108 | name = "arrayvec" 109 | version = "0.7.6" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 112 | 113 | [[package]] 114 | name = "autocfg" 115 | version = "1.4.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 118 | 119 | [[package]] 120 | name = "av1-grain" 121 | version = "0.2.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" 124 | dependencies = [ 125 | "anyhow", 126 | "arrayvec", 127 | "log", 128 | "nom", 129 | "num-rational", 130 | "v_frame", 131 | ] 132 | 133 | [[package]] 134 | name = "avif-serialize" 135 | version = "0.8.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" 138 | dependencies = [ 139 | "arrayvec", 140 | ] 141 | 142 | [[package]] 143 | name = "bit_field" 144 | version = "0.10.2" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 147 | 148 | [[package]] 149 | name = "bitflags" 150 | version = "1.3.2" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 153 | 154 | [[package]] 155 | name = "bitflags" 156 | version = "2.6.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 159 | 160 | [[package]] 161 | name = "bitstream-io" 162 | version = "2.6.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" 165 | 166 | [[package]] 167 | name = "built" 168 | version = "0.7.5" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" 171 | 172 | [[package]] 173 | name = "bumpalo" 174 | version = "3.16.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 177 | 178 | [[package]] 179 | name = "bytemuck" 180 | version = "1.21.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" 183 | 184 | [[package]] 185 | name = "byteorder" 186 | version = "1.5.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 189 | 190 | [[package]] 191 | name = "byteorder-lite" 192 | version = "0.1.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 195 | 196 | [[package]] 197 | name = "cc" 198 | version = "1.2.7" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" 201 | dependencies = [ 202 | "jobserver", 203 | "libc", 204 | "shlex", 205 | ] 206 | 207 | [[package]] 208 | name = "cfg-expr" 209 | version = "0.15.8" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" 212 | dependencies = [ 213 | "smallvec", 214 | "target-lexicon", 215 | ] 216 | 217 | [[package]] 218 | name = "cfg-if" 219 | version = "1.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 222 | 223 | [[package]] 224 | name = "cjk" 225 | version = "0.2.5" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "8c5581b1ff873217332789ccc8956c1c68bcf9d693e7e5c6bcc677897fda5257" 228 | dependencies = [ 229 | "hex", 230 | "lazy_static", 231 | "unicode-blocks", 232 | "widestring", 233 | ] 234 | 235 | [[package]] 236 | name = "clap" 237 | version = "4.5.23" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 240 | dependencies = [ 241 | "clap_builder", 242 | "clap_derive", 243 | ] 244 | 245 | [[package]] 246 | name = "clap_builder" 247 | version = "4.5.23" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 250 | dependencies = [ 251 | "anstream", 252 | "anstyle", 253 | "clap_lex", 254 | "strsim", 255 | ] 256 | 257 | [[package]] 258 | name = "clap_complete" 259 | version = "4.5.40" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9" 262 | dependencies = [ 263 | "clap", 264 | ] 265 | 266 | [[package]] 267 | name = "clap_derive" 268 | version = "4.5.18" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 271 | dependencies = [ 272 | "heck", 273 | "proc-macro2", 274 | "quote", 275 | "syn", 276 | ] 277 | 278 | [[package]] 279 | name = "clap_lex" 280 | version = "0.7.4" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 283 | 284 | [[package]] 285 | name = "color_quant" 286 | version = "1.1.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 289 | 290 | [[package]] 291 | name = "colorchoice" 292 | version = "1.0.3" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 295 | 296 | [[package]] 297 | name = "console" 298 | version = "0.15.10" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" 301 | dependencies = [ 302 | "encode_unicode", 303 | "libc", 304 | "once_cell", 305 | "unicode-width 0.2.0", 306 | "windows-sys", 307 | ] 308 | 309 | [[package]] 310 | name = "crc32fast" 311 | version = "1.4.2" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 314 | dependencies = [ 315 | "cfg-if", 316 | ] 317 | 318 | [[package]] 319 | name = "crossbeam-deque" 320 | version = "0.8.6" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 323 | dependencies = [ 324 | "crossbeam-epoch", 325 | "crossbeam-utils", 326 | ] 327 | 328 | [[package]] 329 | name = "crossbeam-epoch" 330 | version = "0.9.18" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 333 | dependencies = [ 334 | "crossbeam-utils", 335 | ] 336 | 337 | [[package]] 338 | name = "crossbeam-utils" 339 | version = "0.8.21" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 342 | 343 | [[package]] 344 | name = "crunchy" 345 | version = "0.2.2" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 348 | 349 | [[package]] 350 | name = "directories-next" 351 | version = "2.0.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" 354 | dependencies = [ 355 | "cfg-if", 356 | "dirs-sys-next", 357 | ] 358 | 359 | [[package]] 360 | name = "dirs-sys-next" 361 | version = "0.1.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 364 | dependencies = [ 365 | "libc", 366 | "redox_users", 367 | "winapi", 368 | ] 369 | 370 | [[package]] 371 | name = "either" 372 | version = "1.13.0" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 375 | 376 | [[package]] 377 | name = "encode_unicode" 378 | version = "1.0.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 381 | 382 | [[package]] 383 | name = "enquote" 384 | version = "1.1.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" 387 | dependencies = [ 388 | "thiserror", 389 | ] 390 | 391 | [[package]] 392 | name = "equivalent" 393 | version = "1.0.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 396 | 397 | [[package]] 398 | name = "errno" 399 | version = "0.3.10" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 402 | dependencies = [ 403 | "libc", 404 | "windows-sys", 405 | ] 406 | 407 | [[package]] 408 | name = "exr" 409 | version = "1.73.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 412 | dependencies = [ 413 | "bit_field", 414 | "half", 415 | "lebe", 416 | "miniz_oxide", 417 | "rayon-core", 418 | "smallvec", 419 | "zune-inflate", 420 | ] 421 | 422 | [[package]] 423 | name = "fdeflate" 424 | version = "0.3.7" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 427 | dependencies = [ 428 | "simd-adler32", 429 | ] 430 | 431 | [[package]] 432 | name = "flate2" 433 | version = "1.0.35" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 436 | dependencies = [ 437 | "crc32fast", 438 | "miniz_oxide", 439 | ] 440 | 441 | [[package]] 442 | name = "getopts" 443 | version = "0.2.21" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 446 | dependencies = [ 447 | "unicode-width 0.1.14", 448 | ] 449 | 450 | [[package]] 451 | name = "getrandom" 452 | version = "0.2.15" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 455 | dependencies = [ 456 | "cfg-if", 457 | "libc", 458 | "wasi", 459 | ] 460 | 461 | [[package]] 462 | name = "gif" 463 | version = "0.13.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" 466 | dependencies = [ 467 | "color_quant", 468 | "weezl", 469 | ] 470 | 471 | [[package]] 472 | name = "half" 473 | version = "2.4.1" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 476 | dependencies = [ 477 | "cfg-if", 478 | "crunchy", 479 | ] 480 | 481 | [[package]] 482 | name = "hashbrown" 483 | version = "0.15.2" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 486 | 487 | [[package]] 488 | name = "heck" 489 | version = "0.5.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 492 | 493 | [[package]] 494 | name = "hex" 495 | version = "0.4.3" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 498 | 499 | [[package]] 500 | name = "image" 501 | version = "0.25.5" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" 504 | dependencies = [ 505 | "bytemuck", 506 | "byteorder-lite", 507 | "color_quant", 508 | "exr", 509 | "gif", 510 | "image-webp", 511 | "num-traits", 512 | "png", 513 | "qoi", 514 | "ravif", 515 | "rayon", 516 | "rgb", 517 | "tiff", 518 | "zune-core", 519 | "zune-jpeg", 520 | ] 521 | 522 | [[package]] 523 | name = "image-webp" 524 | version = "0.2.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" 527 | dependencies = [ 528 | "byteorder-lite", 529 | "quick-error", 530 | ] 531 | 532 | [[package]] 533 | name = "imgref" 534 | version = "1.11.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" 537 | 538 | [[package]] 539 | name = "indexmap" 540 | version = "2.7.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 543 | dependencies = [ 544 | "equivalent", 545 | "hashbrown", 546 | ] 547 | 548 | [[package]] 549 | name = "interpolate_name" 550 | version = "0.2.4" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 553 | dependencies = [ 554 | "proc-macro2", 555 | "quote", 556 | "syn", 557 | ] 558 | 559 | [[package]] 560 | name = "is_terminal_polyfill" 561 | version = "1.70.1" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 564 | 565 | [[package]] 566 | name = "itertools" 567 | version = "0.12.1" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 570 | dependencies = [ 571 | "either", 572 | ] 573 | 574 | [[package]] 575 | name = "jobserver" 576 | version = "0.1.32" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 579 | dependencies = [ 580 | "libc", 581 | ] 582 | 583 | [[package]] 584 | name = "jpeg-decoder" 585 | version = "0.3.1" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 588 | 589 | [[package]] 590 | name = "lazy_static" 591 | version = "1.5.0" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 594 | 595 | [[package]] 596 | name = "lebe" 597 | version = "0.5.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 600 | 601 | [[package]] 602 | name = "libc" 603 | version = "0.2.169" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 606 | 607 | [[package]] 608 | name = "libfuzzer-sys" 609 | version = "0.4.8" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" 612 | dependencies = [ 613 | "arbitrary", 614 | "cc", 615 | ] 616 | 617 | [[package]] 618 | name = "libredox" 619 | version = "0.1.3" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 622 | dependencies = [ 623 | "bitflags 2.6.0", 624 | "libc", 625 | ] 626 | 627 | [[package]] 628 | name = "linux-raw-sys" 629 | version = "0.4.14" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 632 | 633 | [[package]] 634 | name = "log" 635 | version = "0.4.22" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 638 | 639 | [[package]] 640 | name = "loop9" 641 | version = "0.1.5" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 644 | dependencies = [ 645 | "imgref", 646 | ] 647 | 648 | [[package]] 649 | name = "maybe-rayon" 650 | version = "0.1.1" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 653 | dependencies = [ 654 | "cfg-if", 655 | "rayon", 656 | ] 657 | 658 | [[package]] 659 | name = "memchr" 660 | version = "2.7.4" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 663 | 664 | [[package]] 665 | name = "minimal-lexical" 666 | version = "0.2.1" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 669 | 670 | [[package]] 671 | name = "miniz_oxide" 672 | version = "0.8.2" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" 675 | dependencies = [ 676 | "adler2", 677 | "simd-adler32", 678 | ] 679 | 680 | [[package]] 681 | name = "new_debug_unreachable" 682 | version = "1.0.6" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 685 | 686 | [[package]] 687 | name = "nom" 688 | version = "7.1.3" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 691 | dependencies = [ 692 | "memchr", 693 | "minimal-lexical", 694 | ] 695 | 696 | [[package]] 697 | name = "noop_proc_macro" 698 | version = "0.3.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 701 | 702 | [[package]] 703 | name = "num-bigint" 704 | version = "0.4.6" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 707 | dependencies = [ 708 | "num-integer", 709 | "num-traits", 710 | ] 711 | 712 | [[package]] 713 | name = "num-derive" 714 | version = "0.4.2" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 717 | dependencies = [ 718 | "proc-macro2", 719 | "quote", 720 | "syn", 721 | ] 722 | 723 | [[package]] 724 | name = "num-integer" 725 | version = "0.1.46" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 728 | dependencies = [ 729 | "num-traits", 730 | ] 731 | 732 | [[package]] 733 | name = "num-rational" 734 | version = "0.4.2" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 737 | dependencies = [ 738 | "num-bigint", 739 | "num-integer", 740 | "num-traits", 741 | ] 742 | 743 | [[package]] 744 | name = "num-traits" 745 | version = "0.2.19" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 748 | dependencies = [ 749 | "autocfg", 750 | ] 751 | 752 | [[package]] 753 | name = "once_cell" 754 | version = "1.20.2" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 757 | 758 | [[package]] 759 | name = "paper-terminal" 760 | version = "3.1.0" 761 | dependencies = [ 762 | "ansi_term", 763 | "cjk", 764 | "clap", 765 | "clap_complete", 766 | "console", 767 | "directories-next", 768 | "image", 769 | "pulldown-cmark", 770 | "syncat-stylesheet", 771 | "terminal_size", 772 | "unicode-width 0.2.0", 773 | ] 774 | 775 | [[package]] 776 | name = "paste" 777 | version = "1.0.15" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 780 | 781 | [[package]] 782 | name = "pkg-config" 783 | version = "0.3.31" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 786 | 787 | [[package]] 788 | name = "png" 789 | version = "0.17.16" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 792 | dependencies = [ 793 | "bitflags 1.3.2", 794 | "crc32fast", 795 | "fdeflate", 796 | "flate2", 797 | "miniz_oxide", 798 | ] 799 | 800 | [[package]] 801 | name = "ppv-lite86" 802 | version = "0.2.20" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 805 | dependencies = [ 806 | "zerocopy", 807 | ] 808 | 809 | [[package]] 810 | name = "proc-macro2" 811 | version = "1.0.92" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 814 | dependencies = [ 815 | "unicode-ident", 816 | ] 817 | 818 | [[package]] 819 | name = "profiling" 820 | version = "1.0.16" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" 823 | dependencies = [ 824 | "profiling-procmacros", 825 | ] 826 | 827 | [[package]] 828 | name = "profiling-procmacros" 829 | version = "1.0.16" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" 832 | dependencies = [ 833 | "quote", 834 | "syn", 835 | ] 836 | 837 | [[package]] 838 | name = "pulldown-cmark" 839 | version = "0.12.2" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" 842 | dependencies = [ 843 | "bitflags 2.6.0", 844 | "getopts", 845 | "memchr", 846 | "pulldown-cmark-escape", 847 | "unicase", 848 | ] 849 | 850 | [[package]] 851 | name = "pulldown-cmark-escape" 852 | version = "0.11.0" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" 855 | 856 | [[package]] 857 | name = "qoi" 858 | version = "0.4.1" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 861 | dependencies = [ 862 | "bytemuck", 863 | ] 864 | 865 | [[package]] 866 | name = "quick-error" 867 | version = "2.0.1" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 870 | 871 | [[package]] 872 | name = "quote" 873 | version = "1.0.38" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 876 | dependencies = [ 877 | "proc-macro2", 878 | ] 879 | 880 | [[package]] 881 | name = "rand" 882 | version = "0.8.5" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 885 | dependencies = [ 886 | "libc", 887 | "rand_chacha", 888 | "rand_core", 889 | ] 890 | 891 | [[package]] 892 | name = "rand_chacha" 893 | version = "0.3.1" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 896 | dependencies = [ 897 | "ppv-lite86", 898 | "rand_core", 899 | ] 900 | 901 | [[package]] 902 | name = "rand_core" 903 | version = "0.6.4" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 906 | dependencies = [ 907 | "getrandom", 908 | ] 909 | 910 | [[package]] 911 | name = "rav1e" 912 | version = "0.7.1" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" 915 | dependencies = [ 916 | "arbitrary", 917 | "arg_enum_proc_macro", 918 | "arrayvec", 919 | "av1-grain", 920 | "bitstream-io", 921 | "built", 922 | "cfg-if", 923 | "interpolate_name", 924 | "itertools", 925 | "libc", 926 | "libfuzzer-sys", 927 | "log", 928 | "maybe-rayon", 929 | "new_debug_unreachable", 930 | "noop_proc_macro", 931 | "num-derive", 932 | "num-traits", 933 | "once_cell", 934 | "paste", 935 | "profiling", 936 | "rand", 937 | "rand_chacha", 938 | "simd_helpers", 939 | "system-deps", 940 | "thiserror", 941 | "v_frame", 942 | "wasm-bindgen", 943 | ] 944 | 945 | [[package]] 946 | name = "ravif" 947 | version = "0.11.11" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" 950 | dependencies = [ 951 | "avif-serialize", 952 | "imgref", 953 | "loop9", 954 | "quick-error", 955 | "rav1e", 956 | "rayon", 957 | "rgb", 958 | ] 959 | 960 | [[package]] 961 | name = "rayon" 962 | version = "1.10.0" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 965 | dependencies = [ 966 | "either", 967 | "rayon-core", 968 | ] 969 | 970 | [[package]] 971 | name = "rayon-core" 972 | version = "1.12.1" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 975 | dependencies = [ 976 | "crossbeam-deque", 977 | "crossbeam-utils", 978 | ] 979 | 980 | [[package]] 981 | name = "redox_users" 982 | version = "0.4.6" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 985 | dependencies = [ 986 | "getrandom", 987 | "libredox", 988 | "thiserror", 989 | ] 990 | 991 | [[package]] 992 | name = "regex" 993 | version = "1.11.1" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 996 | dependencies = [ 997 | "aho-corasick", 998 | "memchr", 999 | "regex-automata", 1000 | "regex-syntax", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "regex-automata" 1005 | version = "0.4.9" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1008 | dependencies = [ 1009 | "aho-corasick", 1010 | "memchr", 1011 | "regex-syntax", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "regex-syntax" 1016 | version = "0.8.5" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1019 | 1020 | [[package]] 1021 | name = "rgb" 1022 | version = "0.8.50" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" 1025 | 1026 | [[package]] 1027 | name = "rustix" 1028 | version = "0.38.42" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 1031 | dependencies = [ 1032 | "bitflags 2.6.0", 1033 | "errno", 1034 | "libc", 1035 | "linux-raw-sys", 1036 | "windows-sys", 1037 | ] 1038 | 1039 | [[package]] 1040 | name = "serde" 1041 | version = "1.0.217" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1044 | dependencies = [ 1045 | "serde_derive", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "serde_derive" 1050 | version = "1.0.217" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1053 | dependencies = [ 1054 | "proc-macro2", 1055 | "quote", 1056 | "syn", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "serde_spanned" 1061 | version = "0.6.8" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1064 | dependencies = [ 1065 | "serde", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "shlex" 1070 | version = "1.3.0" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1073 | 1074 | [[package]] 1075 | name = "simd-adler32" 1076 | version = "0.3.7" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1079 | 1080 | [[package]] 1081 | name = "simd_helpers" 1082 | version = "0.1.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 1085 | dependencies = [ 1086 | "quote", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "smallvec" 1091 | version = "1.13.2" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1094 | 1095 | [[package]] 1096 | name = "strsim" 1097 | version = "0.11.1" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1100 | 1101 | [[package]] 1102 | name = "syn" 1103 | version = "2.0.94" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" 1106 | dependencies = [ 1107 | "proc-macro2", 1108 | "quote", 1109 | "unicode-ident", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "syncat-stylesheet" 1114 | version = "3.5.0" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "9a0ad46e0dea7c8a1affa928824a3eaf1e24812fb9f91921b00eb2587c63ef29" 1117 | dependencies = [ 1118 | "ansi_term", 1119 | "cc", 1120 | "enquote", 1121 | "hex", 1122 | "regex", 1123 | "tree-sitter", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "system-deps" 1128 | version = "6.2.2" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" 1131 | dependencies = [ 1132 | "cfg-expr", 1133 | "heck", 1134 | "pkg-config", 1135 | "toml", 1136 | "version-compare", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "target-lexicon" 1141 | version = "0.12.16" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 1144 | 1145 | [[package]] 1146 | name = "terminal_size" 1147 | version = "0.4.1" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" 1150 | dependencies = [ 1151 | "rustix", 1152 | "windows-sys", 1153 | ] 1154 | 1155 | [[package]] 1156 | name = "thiserror" 1157 | version = "1.0.69" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1160 | dependencies = [ 1161 | "thiserror-impl", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "thiserror-impl" 1166 | version = "1.0.69" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1169 | dependencies = [ 1170 | "proc-macro2", 1171 | "quote", 1172 | "syn", 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "tiff" 1177 | version = "0.9.1" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 1180 | dependencies = [ 1181 | "flate2", 1182 | "jpeg-decoder", 1183 | "weezl", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "toml" 1188 | version = "0.8.19" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1191 | dependencies = [ 1192 | "serde", 1193 | "serde_spanned", 1194 | "toml_datetime", 1195 | "toml_edit", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "toml_datetime" 1200 | version = "0.6.8" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1203 | dependencies = [ 1204 | "serde", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "toml_edit" 1209 | version = "0.22.22" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 1212 | dependencies = [ 1213 | "indexmap", 1214 | "serde", 1215 | "serde_spanned", 1216 | "toml_datetime", 1217 | "winnow", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "tree-sitter" 1222 | version = "0.20.10" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" 1225 | dependencies = [ 1226 | "cc", 1227 | "regex", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "unicase" 1232 | version = "2.8.1" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1235 | 1236 | [[package]] 1237 | name = "unicode-blocks" 1238 | version = "0.1.9" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "6b12e05d9e06373163a9bb6bb8c263c261b396643a99445fe6b9811fd376581b" 1241 | 1242 | [[package]] 1243 | name = "unicode-ident" 1244 | version = "1.0.14" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1247 | 1248 | [[package]] 1249 | name = "unicode-width" 1250 | version = "0.1.14" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1253 | 1254 | [[package]] 1255 | name = "unicode-width" 1256 | version = "0.2.0" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1259 | 1260 | [[package]] 1261 | name = "utf8parse" 1262 | version = "0.2.2" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1265 | 1266 | [[package]] 1267 | name = "v_frame" 1268 | version = "0.3.8" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" 1271 | dependencies = [ 1272 | "aligned-vec", 1273 | "num-traits", 1274 | "wasm-bindgen", 1275 | ] 1276 | 1277 | [[package]] 1278 | name = "version-compare" 1279 | version = "0.2.0" 1280 | source = "registry+https://github.com/rust-lang/crates.io-index" 1281 | checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 1282 | 1283 | [[package]] 1284 | name = "wasi" 1285 | version = "0.11.0+wasi-snapshot-preview1" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1288 | 1289 | [[package]] 1290 | name = "wasm-bindgen" 1291 | version = "0.2.99" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 1294 | dependencies = [ 1295 | "cfg-if", 1296 | "once_cell", 1297 | "wasm-bindgen-macro", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "wasm-bindgen-backend" 1302 | version = "0.2.99" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 1305 | dependencies = [ 1306 | "bumpalo", 1307 | "log", 1308 | "proc-macro2", 1309 | "quote", 1310 | "syn", 1311 | "wasm-bindgen-shared", 1312 | ] 1313 | 1314 | [[package]] 1315 | name = "wasm-bindgen-macro" 1316 | version = "0.2.99" 1317 | source = "registry+https://github.com/rust-lang/crates.io-index" 1318 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 1319 | dependencies = [ 1320 | "quote", 1321 | "wasm-bindgen-macro-support", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "wasm-bindgen-macro-support" 1326 | version = "0.2.99" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 1329 | dependencies = [ 1330 | "proc-macro2", 1331 | "quote", 1332 | "syn", 1333 | "wasm-bindgen-backend", 1334 | "wasm-bindgen-shared", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "wasm-bindgen-shared" 1339 | version = "0.2.99" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 1342 | 1343 | [[package]] 1344 | name = "weezl" 1345 | version = "0.1.8" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" 1348 | 1349 | [[package]] 1350 | name = "widestring" 1351 | version = "0.4.3" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" 1354 | 1355 | [[package]] 1356 | name = "winapi" 1357 | version = "0.3.9" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1360 | dependencies = [ 1361 | "winapi-i686-pc-windows-gnu", 1362 | "winapi-x86_64-pc-windows-gnu", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "winapi-i686-pc-windows-gnu" 1367 | version = "0.4.0" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1370 | 1371 | [[package]] 1372 | name = "winapi-x86_64-pc-windows-gnu" 1373 | version = "0.4.0" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1376 | 1377 | [[package]] 1378 | name = "windows-sys" 1379 | version = "0.59.0" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1382 | dependencies = [ 1383 | "windows-targets", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "windows-targets" 1388 | version = "0.52.6" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1391 | dependencies = [ 1392 | "windows_aarch64_gnullvm", 1393 | "windows_aarch64_msvc", 1394 | "windows_i686_gnu", 1395 | "windows_i686_gnullvm", 1396 | "windows_i686_msvc", 1397 | "windows_x86_64_gnu", 1398 | "windows_x86_64_gnullvm", 1399 | "windows_x86_64_msvc", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "windows_aarch64_gnullvm" 1404 | version = "0.52.6" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1407 | 1408 | [[package]] 1409 | name = "windows_aarch64_msvc" 1410 | version = "0.52.6" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1413 | 1414 | [[package]] 1415 | name = "windows_i686_gnu" 1416 | version = "0.52.6" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1419 | 1420 | [[package]] 1421 | name = "windows_i686_gnullvm" 1422 | version = "0.52.6" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1425 | 1426 | [[package]] 1427 | name = "windows_i686_msvc" 1428 | version = "0.52.6" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1431 | 1432 | [[package]] 1433 | name = "windows_x86_64_gnu" 1434 | version = "0.52.6" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1437 | 1438 | [[package]] 1439 | name = "windows_x86_64_gnullvm" 1440 | version = "0.52.6" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1443 | 1444 | [[package]] 1445 | name = "windows_x86_64_msvc" 1446 | version = "0.52.6" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1449 | 1450 | [[package]] 1451 | name = "winnow" 1452 | version = "0.6.22" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" 1455 | dependencies = [ 1456 | "memchr", 1457 | ] 1458 | 1459 | [[package]] 1460 | name = "zerocopy" 1461 | version = "0.7.35" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1464 | dependencies = [ 1465 | "byteorder", 1466 | "zerocopy-derive", 1467 | ] 1468 | 1469 | [[package]] 1470 | name = "zerocopy-derive" 1471 | version = "0.7.35" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1474 | dependencies = [ 1475 | "proc-macro2", 1476 | "quote", 1477 | "syn", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "zune-core" 1482 | version = "0.4.12" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 1485 | 1486 | [[package]] 1487 | name = "zune-inflate" 1488 | version = "0.2.54" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 1491 | dependencies = [ 1492 | "simd-adler32", 1493 | ] 1494 | 1495 | [[package]] 1496 | name = "zune-jpeg" 1497 | version = "0.4.14" 1498 | source = "registry+https://github.com/rust-lang/crates.io-index" 1499 | checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" 1500 | dependencies = [ 1501 | "zune-core", 1502 | ] 1503 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paper-terminal" 3 | description = """ 4 | Writes a file to a paper in your terminal. Especially if that file is Markdown. 5 | """ 6 | repository = "https://github.com/foxfriends/paper-terminal" 7 | homepage = "https://github.com/foxfriends/paper-terminal" 8 | license = "MIT" 9 | readme = "README.md" 10 | version = "3.1.0" 11 | authors = ["Cameron Eldridge "] 12 | edition = "2024" 13 | categories = ["command-line-utilities"] 14 | 15 | [[bin]] 16 | name = "paper" 17 | path = "src/main.rs" 18 | 19 | [dependencies] 20 | clap = { version = "4.4", features = ["derive"] } 21 | terminal_size = "0.4" 22 | pulldown-cmark = "0.12" 23 | ansi_term = "0.12" 24 | image = "0.25" 25 | console = { version = "0.15", features = ["unicode-width"] } 26 | directories-next = "2.0" 27 | syncat-stylesheet = { version = "3.5.0", features = ["ansi_term"] } 28 | unicode-width = "0.2" 29 | cjk = "0.2" 30 | clap_complete = "4.5.40" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [syncat]: https://github.com/foxfriends/syncat 2 | [syncat stylesheets]: https://github.com/foxfriends/syncat-themes 3 | [foxfriends/paper-terminal]: https://github.com/foxfriends/paper-terminal 4 | [ttscoff/mdless]: https://github.com/ttscoff/mdless 5 | [lunaryorn/mdcat]: https://github.com/lunaryorn/mdcat 6 | 7 | # Paper terminal 8 | 9 | [![dependency status](https://deps.rs/repo/github/foxfriends/paper-terminal/status.svg)](https://deps.rs/repo/github/foxfriends/paper-terminal) 10 | 11 | 12 | 13 | See [paper.png](./paper.png) to see what this looks like! 14 | 15 | Writes a file to a paper in your terminal. *Especially* if that file is Markdown! Features supported 16 | include: 17 | 18 | 1. The usual text, and paragraphs with automatic line-wrapping. You can manually wrap with 19 | hard breaks as expected. 20 | 21 | Otherwise, paragraphs will be nicely spaced. 22 | 2. Headings 23 | 3. __Bold__ / *Italic* / *__Bold and Italic__* / ~~Strikethrough~~ 24 | 4. Lists 25 | * Ordered 26 | * Unordered 27 | * Nested 28 | 29 | Definition 30 | : This is a definition 31 | 32 | 5. Rules 33 | 6. `Inline code` 34 | 7. Code blocks, with [syncat][] integration for syntax highlighting. Note that you must install 35 | syncat and make the syncat executable available on your path for this to work. 36 | ```rust 37 | fn main() { 38 | println!("Hello world"); 39 | } 40 | ``` 41 | 8. Blockquotes 42 | 43 | > Blockquotes 44 | > > And even nested block quotes 45 | 46 | > [!IMPORTANT] 47 | > Also alert blockquotes 48 | 49 | 9. And even images! Here's a photo of my cat 50 | 51 | ![My cat. His name is Cato](./cato.png) 52 | 53 | 10. Task lists: 54 | - [x] Easy 55 | - [ ] Hard 56 | 11. Footnotes[^ft] 57 | 58 | [^ft]: This is the footnote! 59 | 60 | 12. Tables 61 | 62 | ## Comparison with other command line Markdown renderers 63 | 64 | Not a very good comparison... this is more of an example of a table! 65 | 66 | | Tool | CommonMark | Paper | Paging | Wrapping | Syntax | Images | Tables | Looks good\* | 67 | | :------------------- | :--------- | :---- | :----- | :------- | :--------- | :-------- | :----- | :----------- | 68 | | [foxfriends/paper-terminal][] | Yes | Yes | No | Yes | syncat | Pixelated | Yes | Yes | 69 | | [ttscoff/mdless][] | Yes | No | Yes | No | pygmentize | Sometimes | Yes | No | 70 | | [lunaryorn/mdcat][] | Yes | No | No | No | syntect | Sometimes | No | No | 71 | 72 | \* subjective 73 | 74 | ## Styling 75 | 76 | Paper uses [syncat stylesheets][] to allow full customization of styling. See the default stylesheet (`src/default.syncat`) 77 | as an example of how this works. To override the default styles, create `paper.syncat` in your active syncat theme. 78 | 79 | * Different scopes are represented as nodes, inspired by the corresponding HTML tag names. 80 | 81 | * `h1` through `h6` 82 | * `strong` 83 | * `emphasis` 84 | * `strikethrough` 85 | * `code` 86 | * `blockquote` (alert style blockquotes are represented separately, using the custom tags `note-blockquote`, `tip-blockquote`, `important-blockquote`, `warning-blockquote`, and `caution-blockquote`) 87 | * `ul`, `ol`, `li` 88 | * `dl`, `dt`, `dd` 89 | * `footnote-ref`, `footnote-def`, `footnote` 90 | * `table` 91 | * `caption` 92 | * `link` 93 | 94 | * The paper and shadow can be matched with `paper` and `shadow`. Styles applied to `paper` are applied to everything. 95 | * The `"prefix"` and `"suffix"` tokens can be used to match the decorations 96 | * List item bullets 97 | * Blockquote (and alert) markers 98 | * Code block margins 99 | * The `"lang-tag"` token matches the language name written in the bottom corner of the code block 100 | * You can apply styles to code blocks with a specific language by using the language name as the token 101 | 102 | For now, the prefix/suffix contents are not customizable, but this may be added in future if it is desired. 103 | 104 | ## Installation 105 | 106 | Paper can be installed from crates.io using Cargo: 107 | 108 | ```bash 109 | cargo install paper-terminal 110 | ``` 111 | 112 | ## Usage 113 | 114 | ```bash 115 | # Print the help 116 | paper --help 117 | 118 | # Render README.md 119 | paper README.md 120 | 121 | # Render README.md, with syntax highlighting 122 | paper README.md -s 123 | ``` 124 | 125 | ``` 126 | Prints papers in your terminal 127 | 128 | Usage: paper [OPTIONS] [FILE]... 129 | 130 | Arguments: 131 | [FILE]... Files to print 132 | 133 | Options: 134 | -m, --margin Margin (shortcut for horizontal and vertical margin set to the same value) [default: 6] 135 | --h-margin Horizontal margin (overrides --margin) 136 | --v-margin Vertical margin (overrides --margin) 137 | -w, --width The width of the paper (including the space used for the margin) [default: 92] 138 | -p, --plain Don't parse as Markdown, just render the plain text on a paper 139 | -t, --tab-length The length to consider tabs as [default: 4] 140 | -U, --hide-urls Hide link URLs 141 | -I, --no-images Disable drawing images 142 | -l, --left Position paper on the left edge of the terminal, instead of centred 143 | -r, --right Position paper on the right edge of the terminal, instead of centred 144 | -s, --syncat Use syncat to highlight code blocks. Requires you have syncat installed 145 | --dev Print in debug mode 146 | --completions Generate shell completions [possible values: bash, elvish, fish, powershell, zsh] 147 | -h, --help Print help 148 | ``` 149 | -------------------------------------------------------------------------------- /cato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxfriends/paper-terminal/0f272698c8b11808932feddd3ae7e883aa61f881/cato.png -------------------------------------------------------------------------------- /paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxfriends/paper-terminal/0f272698c8b11808932feddd3ae7e883aa61f881/paper.png -------------------------------------------------------------------------------- /src/default.syncat: -------------------------------------------------------------------------------- 1 | shadow { 2 | background-color: brblack; 3 | } 4 | 5 | li & "prefix", 6 | blockquote & "prefix" { 7 | color: brblack; 8 | dim: true; 9 | } 10 | 11 | note-blockquote & "prefix" { 12 | color: blue; 13 | } 14 | 15 | tip-blockquote & "prefix" { 16 | color: green; 17 | } 18 | 19 | important-blockquote & "prefix" { 20 | color: purple; 21 | } 22 | 23 | warning-blockquote & "prefix" { 24 | color: yellow; 25 | } 26 | 27 | caution-blockquote & "prefix" { 28 | color: red; 29 | } 30 | 31 | rule { 32 | color: black; 33 | } 34 | 35 | strong { 36 | bold: true; 37 | } 38 | 39 | emphasis { 40 | italic: true; 41 | } 42 | 43 | strikethrough { 44 | strikethrough: true; 45 | } 46 | 47 | link { 48 | underline: true; 49 | } 50 | 51 | caption { 52 | underline: true; 53 | } 54 | 55 | h1 { 56 | bold: true; 57 | } 58 | 59 | h2 { 60 | bold: true; 61 | } 62 | 63 | h3 { 64 | bold: true; 65 | underline: true; 66 | } 67 | 68 | h4 { 69 | bold: true; 70 | underline: true; 71 | dim: true; 72 | } 73 | 74 | h5 { 75 | underline: true; 76 | } 77 | 78 | h6 { 79 | underline: true; 80 | dim: true; 81 | } 82 | 83 | dt { 84 | bold: true 85 | } 86 | 87 | code { 88 | background-color: black; 89 | color: white; 90 | } 91 | 92 | footnote-def, footnote-ref { 93 | color: brblack; 94 | dim: true; 95 | } 96 | 97 | codeblock *, 98 | codeblock * "prefix", 99 | codeblock * "suffix", { 100 | background-color: black; 101 | color: white; 102 | } 103 | 104 | codeblock * "lang-tag" { 105 | color: white; 106 | dim: true; 107 | } 108 | 109 | paper { 110 | background-color: white; 111 | color: black; 112 | } 113 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use directories_next::ProjectDirs; 2 | use std::path::PathBuf; 3 | 4 | fn syncat_directories() -> ProjectDirs { 5 | ProjectDirs::from("com", "cameldridge", "syncat").unwrap() 6 | } 7 | 8 | pub fn syncat_config() -> PathBuf { 9 | syncat_directories().config_dir().to_owned() 10 | } 11 | 12 | pub fn active_color() -> PathBuf { 13 | syncat_config().join("style").join("active") 14 | } 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::Style; 2 | use clap::{CommandFactory, Parser as _}; 3 | use clap_complete::Shell; 4 | use console::strip_ansi_codes; 5 | use pulldown_cmark::{Options, Parser}; 6 | use std::convert::TryInto; 7 | use std::fs; 8 | use std::io::{self, Read}; 9 | use std::path::PathBuf; 10 | use syncat_stylesheet::Stylesheet; 11 | use terminal_size::{Width, terminal_size}; 12 | 13 | mod dirs; 14 | mod printer; 15 | mod str_width; 16 | mod table; 17 | mod termpix; 18 | mod words; 19 | 20 | use printer::Printer; 21 | use str_width::str_width; 22 | use words::Words; 23 | 24 | /// Prints papers in your terminal 25 | #[derive(clap::Parser, Debug)] 26 | #[clap(name = "paper")] 27 | #[clap(rename_all = "kebab-case")] 28 | pub struct Opts { 29 | /// Margin (shortcut for horizontal and vertical margin set to the same value) 30 | #[structopt(short = 'm', long, default_value = "6")] 31 | pub margin: usize, 32 | 33 | /// Horizontal margin (overrides --margin) 34 | #[structopt(long)] 35 | pub h_margin: Option, 36 | 37 | /// Vertical margin (overrides --margin) 38 | #[structopt(long)] 39 | pub v_margin: Option, 40 | 41 | /// The width of the paper (including the space used for the margin) 42 | #[structopt(short = 'w', long, default_value = "92")] 43 | pub width: usize, 44 | 45 | /// Don't parse as Markdown, just render the plain text on a paper 46 | #[structopt(short = 'p', long)] 47 | pub plain: bool, 48 | 49 | /// The length to consider tabs as. 50 | #[structopt(short, long, default_value = "4")] 51 | pub tab_length: usize, 52 | 53 | /// Hide link URLs 54 | #[structopt(short = 'U', long)] 55 | pub hide_urls: bool, 56 | 57 | /// Disable drawing images 58 | #[structopt(short = 'I', long)] 59 | pub no_images: bool, 60 | 61 | /// Position paper on the left edge of the terminal, instead of centred. 62 | #[structopt(short = 'l', long)] 63 | pub left: bool, 64 | 65 | /// Position paper on the right edge of the terminal, instead of centred. 66 | #[structopt(short = 'r', long)] 67 | pub right: bool, 68 | 69 | /// Use syncat to highlight code blocks. Requires you have syncat installed. 70 | #[structopt(short, long)] 71 | pub syncat: bool, 72 | 73 | /// Print in debug mode 74 | #[structopt(long)] 75 | pub dev: bool, 76 | 77 | /// Files to print 78 | #[structopt(name = "FILE")] 79 | pub files: Vec, 80 | 81 | /// Generate shell completions 82 | #[structopt(long)] 83 | completions: Option, 84 | } 85 | 86 | fn normalize(tab_len: usize, source: &str) -> String { 87 | source 88 | .lines() 89 | .map(|line| { 90 | let mut len = 0; 91 | let line = strip_ansi_codes(line); 92 | if line.contains('\t') { 93 | line.chars() 94 | .flat_map(|ch| { 95 | if ch == '\t' { 96 | let missing = tab_len - (len % tab_len); 97 | len += missing; 98 | vec![' '; missing] 99 | } else { 100 | len += 1; 101 | vec![ch] 102 | } 103 | }) 104 | .collect::() 105 | .into() 106 | } else { 107 | line 108 | } 109 | }) 110 | .map(|line| format!("{}\n", line)) 111 | .collect::() 112 | } 113 | 114 | fn print(opts: Opts, sources: I) 115 | where 116 | I: Iterator>, 117 | { 118 | let h_margin = opts.h_margin.unwrap_or(opts.margin); 119 | let v_margin = opts.v_margin.unwrap_or(opts.margin); 120 | let terminal_width = terminal_size() 121 | .map(|(Width(width), _)| width) 122 | .unwrap_or(opts.width as u16) as usize; 123 | let width = usize::min(opts.width, terminal_width - 1); 124 | 125 | if width < h_margin * 2 + 40 { 126 | eprintln!("The width is too short!"); 127 | return; 128 | } 129 | 130 | let left_space = match (opts.left, opts.right) { 131 | (true, false) => "".to_owned(), 132 | (false, true) => " ".repeat(terminal_width.saturating_sub(width) - 1), 133 | _ => " ".repeat((terminal_width.saturating_sub(width)) / 2), 134 | }; 135 | 136 | let stylesheet = Stylesheet::from_file(dirs::active_color().join("paper.syncat")) 137 | .unwrap_or_else(|_| { 138 | include_str!("default.syncat") 139 | .parse::() 140 | .unwrap() 141 | }); 142 | let paper_style: Style = stylesheet 143 | .style(&"paper".into()) 144 | .unwrap_or_default() 145 | .try_into() 146 | .unwrap_or_default(); 147 | let shadow_style: Style = stylesheet 148 | .style(&"shadow".into()) 149 | .unwrap_or_default() 150 | .try_into() 151 | .unwrap_or_default(); 152 | let blank_line = format!("{}", paper_style.paint(" ".repeat(width))); 153 | let end_shadow = format!("{}", shadow_style.paint(" ")); 154 | let margin = format!("{}", paper_style.paint(" ".repeat(h_margin))); 155 | let available_width = width - 2 * h_margin; 156 | for source in sources { 157 | let source = match source { 158 | Ok(source) => normalize(opts.tab_length, &source), 159 | Err(error) => { 160 | println!("{}", error); 161 | continue; 162 | } 163 | }; 164 | if opts.plain { 165 | println!("{}{}", left_space, blank_line); 166 | for _ in 0..v_margin { 167 | println!("{}{}{}", left_space, blank_line, end_shadow); 168 | } 169 | 170 | for line in source.lines() { 171 | let mut buffer = String::new(); 172 | let mut indent = None; 173 | for word in Words::preserving_whitespace(line) { 174 | if str_width(&buffer) + str_width(&word) > available_width { 175 | println!( 176 | "{}{}{}{}{}{}", 177 | left_space, 178 | margin, 179 | paper_style.paint(&buffer), 180 | paper_style.paint( 181 | " ".repeat(available_width.saturating_sub(str_width(&buffer))) 182 | ), 183 | margin, 184 | shadow_style.paint(" "), 185 | ); 186 | buffer.clear(); 187 | } 188 | if buffer.is_empty() { 189 | if indent.is_none() { 190 | let indent_len = 191 | word.chars().take_while(|ch| ch.is_whitespace()).count(); 192 | indent = Some(word[0..indent_len].to_string()); 193 | } 194 | buffer.push_str(indent.as_ref().unwrap()); 195 | buffer.push_str(word.trim()); 196 | } else { 197 | buffer.push_str(&word); 198 | } 199 | } 200 | println!( 201 | "{}{}{}{}{}{}", 202 | left_space, 203 | margin, 204 | paper_style.paint(&buffer), 205 | paper_style 206 | .paint(" ".repeat(available_width.saturating_sub(str_width(&buffer)))), 207 | margin, 208 | shadow_style.paint(" "), 209 | ); 210 | } 211 | for _ in 0..v_margin { 212 | println!("{}{}{}", left_space, blank_line, end_shadow); 213 | } 214 | println!("{} {}", left_space, shadow_style.paint(" ".repeat(width))); 215 | } else if opts.dev { 216 | let parser = Parser::new_ext(&source, Options::all()); 217 | for event in parser { 218 | println!("{:?}", event); 219 | } 220 | } else { 221 | let parser = Parser::new_ext(&source, Options::all()); 222 | println!("{}{}", left_space, blank_line); 223 | for _ in 0..v_margin { 224 | println!("{}{}{}", left_space, blank_line, end_shadow); 225 | } 226 | 227 | let mut printer = 228 | Printer::new(&left_space, &margin, available_width, &stylesheet, &opts); 229 | for event in parser { 230 | printer.handle(event); 231 | } 232 | 233 | for _ in 0..v_margin { 234 | println!("{}{}{}", left_space, blank_line, end_shadow); 235 | } 236 | println!("{} {}", left_space, shadow_style.paint(" ".repeat(width))); 237 | } 238 | } 239 | } 240 | 241 | fn main() { 242 | let opts = Opts::parse(); 243 | 244 | if opts.completions.is_some() { 245 | let shell = opts.completions.or_else(Shell::from_env).unwrap(); 246 | let mut opts = Opts::command(); 247 | let name = opts.get_name().to_string(); 248 | clap_complete::generate(shell, &mut opts, name, &mut std::io::stdout()); 249 | std::process::exit(0); 250 | } 251 | 252 | if opts.files.is_empty() { 253 | let mut string = String::new(); 254 | io::stdin().read_to_string(&mut string).unwrap(); 255 | print(opts, vec![Ok(string)].into_iter()); 256 | } else { 257 | let sources = opts 258 | .files 259 | .clone() 260 | .into_iter() 261 | .map(|path| fs::read_to_string(&path)); 262 | print(opts, sources); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/printer.rs: -------------------------------------------------------------------------------- 1 | use crate::str_width; 2 | use crate::table::Table; 3 | use crate::termpix; 4 | use crate::words::Words; 5 | use ansi_term::Style; 6 | use console::AnsiCodeIterator; 7 | use image::{self, GenericImageView as _}; 8 | use pulldown_cmark::{Alignment, BlockQuoteKind, CodeBlockKind, Event, HeadingLevel, Tag, TagEnd}; 9 | use std::convert::{TryFrom, TryInto}; 10 | use std::io::{Read as _, Write as _}; 11 | use std::process::{Command, Stdio}; 12 | use syncat_stylesheet::{Query, Stylesheet}; 13 | 14 | #[derive(Debug, PartialEq)] 15 | enum Scope { 16 | Paper, 17 | Indent, 18 | Italic, 19 | Bold, 20 | Strikethrough, 21 | Link { dest_url: String, title: String }, 22 | Caption, 23 | FootnoteDefinition, 24 | FootnoteReference, 25 | FootnoteContent, 26 | List(Option), 27 | DefinitionList, 28 | Term, 29 | Definition, 30 | ListItem(Option, bool), 31 | Code, 32 | CodeBlock(String), 33 | BlockQuote(Option), 34 | Table(Vec), 35 | TableHead, 36 | TableRow, 37 | TableCell, 38 | Heading(HeadingLevel), 39 | } 40 | 41 | impl Scope { 42 | fn prefix_len(&self) -> usize { 43 | match self { 44 | Scope::Indent => 4, 45 | Scope::FootnoteContent => 4, 46 | Scope::ListItem(..) => 4, 47 | Scope::CodeBlock(..) => 2, 48 | Scope::BlockQuote(..) => 4, 49 | Scope::Heading(HeadingLevel::H2) => 5, 50 | Scope::Heading(..) => 4, 51 | _ => 0, 52 | } 53 | } 54 | 55 | fn prefix(&mut self) -> String { 56 | match self { 57 | Scope::Indent => " ".to_owned(), 58 | Scope::FootnoteContent => " ".to_owned(), 59 | Scope::ListItem(Some(index), handled) => { 60 | if *handled { 61 | " ".to_owned() 62 | } else { 63 | *handled = true; 64 | format!("{: <4}", format!("{}.", index)) 65 | } 66 | } 67 | Scope::ListItem(None, handled) => { 68 | if *handled { 69 | " ".to_owned() 70 | } else { 71 | *handled = true; 72 | "• ".to_owned() 73 | } 74 | } 75 | Scope::CodeBlock(..) => " ".to_owned(), 76 | Scope::BlockQuote(..) => "┃ ".to_owned(), 77 | Scope::Heading(HeadingLevel::H2) => "├─── ".to_owned(), 78 | Scope::Heading(..) => " ".to_owned(), 79 | Scope::Definition => " ".to_owned(), 80 | _ => String::new(), 81 | } 82 | } 83 | 84 | fn suffix_len(&self) -> usize { 85 | match self { 86 | Scope::CodeBlock(..) => 2, 87 | Scope::Heading(HeadingLevel::H2) => 5, 88 | Scope::Heading(..) => 4, 89 | _ => 0, 90 | } 91 | } 92 | 93 | fn suffix(&mut self) -> String { 94 | match self { 95 | Scope::CodeBlock(..) => " ".to_owned(), 96 | Scope::Heading(HeadingLevel::H2) => " ───┤".to_owned(), 97 | Scope::Heading(..) => " ".to_owned(), 98 | _ => String::new(), 99 | } 100 | } 101 | 102 | fn name(&self) -> &'static str { 103 | use Scope::*; 104 | match self { 105 | Paper => "paper", 106 | Indent => "indent", 107 | Italic => "emphasis", 108 | Bold => "strong", 109 | Strikethrough => "strikethrough", 110 | Link { .. } => "link", 111 | Caption => "caption", 112 | FootnoteDefinition => "footnote-def", 113 | FootnoteReference => "footnote-ref", 114 | FootnoteContent => "footnote", 115 | List(Some(..)) => "ol", 116 | List(None) => "ul", 117 | DefinitionList => "dl", 118 | Term => "dt", 119 | Definition => "dd", 120 | ListItem(..) => "li", 121 | Code => "code", 122 | CodeBlock(..) => "codeblock", 123 | BlockQuote(None) => "blockquote", 124 | BlockQuote(Some(BlockQuoteKind::Note)) => "note-blockquote", 125 | BlockQuote(Some(BlockQuoteKind::Tip)) => "tip-blockquote", 126 | BlockQuote(Some(BlockQuoteKind::Important)) => "important-blockquote", 127 | BlockQuote(Some(BlockQuoteKind::Warning)) => "warning-blockquote", 128 | BlockQuote(Some(BlockQuoteKind::Caution)) => "caution-blockquote", 129 | Table(..) => "table", 130 | TableHead => "th", 131 | TableRow => "tr", 132 | TableCell => "td", 133 | Heading(HeadingLevel::H1) => "h1", 134 | Heading(HeadingLevel::H2) => "h2", 135 | Heading(HeadingLevel::H3) => "h3", 136 | Heading(HeadingLevel::H4) => "h4", 137 | Heading(HeadingLevel::H5) => "h5", 138 | Heading(HeadingLevel::H6) => "h6", 139 | } 140 | } 141 | } 142 | 143 | pub struct Printer<'a> { 144 | centering: &'a str, 145 | margin: &'a str, 146 | stylesheet: &'a Stylesheet, 147 | opts: &'a crate::Opts, 148 | width: usize, 149 | buffer: String, 150 | table: (Vec, Vec>), 151 | content: String, 152 | scope: Vec, 153 | empty_queued: bool, 154 | } 155 | 156 | impl<'a> Printer<'a> { 157 | pub fn new( 158 | centering: &'a str, 159 | margin: &'a str, 160 | width: usize, 161 | stylesheet: &'a Stylesheet, 162 | opts: &'a crate::Opts, 163 | ) -> Printer<'a> { 164 | Printer { 165 | centering, 166 | margin, 167 | width, 168 | stylesheet, 169 | opts, 170 | buffer: String::new(), 171 | table: (vec![], vec![]), 172 | content: String::new(), 173 | scope: vec![Scope::Paper], 174 | empty_queued: false, 175 | } 176 | } 177 | 178 | fn prefix_len(&self) -> usize { 179 | self.scope 180 | .iter() 181 | .fold(0, |len, scope| len + scope.prefix_len()) 182 | } 183 | 184 | fn suffix_len(&self) -> usize { 185 | self.scope 186 | .iter() 187 | .fold(0, |len, scope| len + scope.suffix_len()) 188 | } 189 | 190 | fn prefix(&mut self) -> (String, usize) { 191 | self.prefix2(None) 192 | } 193 | 194 | fn prefix2(&mut self, extra_scopes: Option<&[&str]>) -> (String, usize) { 195 | let stylesheet = self.stylesheet; 196 | self.scope 197 | .iter_mut() 198 | .scan(vec![], |scopes, scope| { 199 | scopes.push(scope.name()); 200 | let prefix = scope.prefix(); 201 | let mut all_scopes = scopes.clone(); 202 | all_scopes.append(&mut extra_scopes.unwrap_or(&[]).to_vec()); 203 | let style = Self::resolve_scopes(&stylesheet, &all_scopes, Some("prefix")); 204 | Some((format!("{}", style.paint(&prefix)), str_width(&prefix))) 205 | }) 206 | .fold((String::new(), 0), |(s, c), (s2, c2)| (s + &s2, c + c2)) 207 | } 208 | 209 | fn suffix(&mut self) -> (String, usize) { 210 | self.suffix2(None) 211 | } 212 | 213 | fn suffix2(&mut self, extra_scopes: Option<&[&str]>) -> (String, usize) { 214 | let stylesheet = self.stylesheet; 215 | self.scope 216 | .iter_mut() 217 | .scan(vec![], |scopes, scope| { 218 | scopes.push(scope.name()); 219 | let suffix = scope.suffix(); 220 | let mut all_scopes = scopes.clone(); 221 | all_scopes.append(&mut extra_scopes.unwrap_or(&[]).to_vec()); 222 | let style = Self::resolve_scopes(&stylesheet, &all_scopes, Some("suffix")); 223 | Some((format!("{}", style.paint(&suffix)), str_width(&suffix))) 224 | }) 225 | .fold((String::new(), 0), |(s, c), (s2, c2)| (s2 + &s, c + c2)) 226 | } 227 | 228 | fn style3(&self, extra_scopes: Option<&[&str]>, token: Option<&str>) -> Style { 229 | let mut scope_names: Vec<_> = self.scope.iter().map(Scope::name).collect(); 230 | if let Some(extras) = extra_scopes { 231 | scope_names.append(&mut extras.to_vec()); 232 | } 233 | Self::resolve_scopes(&self.stylesheet, &scope_names, token) 234 | } 235 | 236 | fn resolve_scopes(stylesheet: &Stylesheet, scopes: &[&str], token: Option<&str>) -> Style { 237 | if scopes.is_empty() { 238 | return Style::default(); 239 | } 240 | let mut query = Query::new(scopes[0], token.unwrap_or(scopes[0])); 241 | let mut index = vec![]; 242 | for scope in &scopes[1..] { 243 | query[&index[..]].add_child(Query::new(*scope, token.unwrap_or(scope))); 244 | index.push(0); 245 | } 246 | stylesheet 247 | .style(&query) 248 | .unwrap_or_default() 249 | .try_into() 250 | .unwrap_or_default() 251 | } 252 | 253 | fn style2(&self, token: Option<&str>) -> Style { 254 | self.style3(None, token) 255 | } 256 | 257 | fn style(&self) -> Style { 258 | self.style2(None) 259 | } 260 | 261 | fn shadow(&self) -> String { 262 | format!( 263 | "{}", 264 | Style::try_from(self.stylesheet.style(&"shadow".into()).unwrap_or_default()) 265 | .unwrap_or_default() 266 | .paint(" ") 267 | ) 268 | } 269 | 270 | fn paper_style(&self) -> Style { 271 | Style::try_from(self.stylesheet.style(&"paper".into()).unwrap_or_default()) 272 | .unwrap_or_default() 273 | } 274 | 275 | fn queue_empty(&mut self) { 276 | self.empty_queued = true; 277 | } 278 | 279 | fn empty(&mut self) { 280 | let (prefix, prefix_len) = self.prefix(); 281 | let (suffix, suffix_len) = self.suffix(); 282 | println!( 283 | "{}{}{}{}{}{}{}", 284 | self.centering, 285 | self.margin, 286 | prefix, 287 | self.paper_style().paint( 288 | " ".repeat( 289 | self.width 290 | .saturating_sub(prefix_len) 291 | .saturating_sub(suffix_len) 292 | ) 293 | ), 294 | suffix, 295 | self.margin, 296 | self.shadow(), 297 | ); 298 | self.empty_queued = false; 299 | } 300 | 301 | fn print_rule(&mut self) { 302 | let (prefix, prefix_len) = self.prefix(); 303 | let (suffix, suffix_len) = self.suffix(); 304 | println!( 305 | "{}{}{}{}{}{}{}", 306 | self.centering, 307 | self.margin, 308 | prefix, 309 | self.style().paint( 310 | "─".repeat( 311 | self.width 312 | .saturating_sub(prefix_len) 313 | .saturating_sub(suffix_len) 314 | ) 315 | ), 316 | suffix, 317 | self.margin, 318 | self.shadow(), 319 | ); 320 | } 321 | 322 | fn print_table(&mut self) { 323 | let alignments = if let Some(Scope::Table(alignments)) = self.scope.last() { 324 | alignments 325 | } else { 326 | return; 327 | }; 328 | let (heading, rows) = std::mem::replace(&mut self.table, (vec![], vec![])); 329 | let available_width = self 330 | .width 331 | .saturating_sub(self.prefix_len()) 332 | .saturating_sub(self.suffix_len()); 333 | let table_str = 334 | Table::new(heading, rows, available_width).print(self.paper_style(), alignments); 335 | for line in table_str.lines() { 336 | let (prefix, _) = self.prefix(); 337 | let (suffix, _) = self.suffix(); 338 | println!( 339 | "{}{}{}{}{}{}{}{}", 340 | self.centering, 341 | self.margin, 342 | line, 343 | prefix, 344 | self.paper_style() 345 | .paint(" ".repeat(available_width.saturating_sub(str_width(line)))), 346 | suffix, 347 | self.margin, 348 | self.shadow(), 349 | ); 350 | } 351 | } 352 | 353 | fn flush_buffer(&mut self) { 354 | match self.scope.last() { 355 | Some(Scope::CodeBlock(lang)) => { 356 | let language_context = if lang.is_empty() || !self.opts.syncat { 357 | String::from("txt") 358 | } else { 359 | lang.to_owned() 360 | }; 361 | let style = self.style3(Some(&[&language_context[..]]), None); 362 | let lang = lang.to_owned(); 363 | let mut first_prefix = Some(self.prefix2(Some(&[&language_context[..]]))); 364 | let mut first_suffix = Some(self.suffix2(Some(&[&language_context[..]]))); 365 | 366 | let available_width = self 367 | .width 368 | .saturating_sub(first_prefix.as_ref().unwrap().1) 369 | .saturating_sub(first_suffix.as_ref().unwrap().1); 370 | let buffer = std::mem::replace(&mut self.buffer, String::new()); 371 | let buffer = if self.opts.syncat { 372 | let syncat = Command::new("syncat") 373 | .args(&["-l", &lang, "-w", &available_width.to_string()]) 374 | .stdin(Stdio::piped()) 375 | .stdout(Stdio::piped()) 376 | .spawn(); 377 | match syncat { 378 | Ok(syncat) => { 379 | { 380 | let mut stdin = syncat.stdin.unwrap(); 381 | write!(stdin, "{}", buffer).unwrap(); 382 | } 383 | let mut output = String::new(); 384 | syncat.stdout.unwrap().read_to_string(&mut output).unwrap(); 385 | output 386 | } 387 | Err(error) => { 388 | eprintln!("{}", error); 389 | buffer.to_owned() 390 | } 391 | } 392 | } else { 393 | buffer 394 | .lines() 395 | .map(|mut line| { 396 | let mut output = String::new(); 397 | while str_width(&line) > available_width { 398 | let not_too_wide = { 399 | let mut acc = 0; 400 | move |ch: &char| { 401 | acc += str_width(&ch.to_string()); 402 | acc < available_width 403 | } 404 | }; 405 | let prefix = 406 | line.chars().take_while(not_too_wide).collect::(); 407 | output = format!("{}{}\n", output, prefix); 408 | line = &line[prefix.len()..]; 409 | } 410 | format!( 411 | "{}{}{}\n", 412 | output, 413 | line, 414 | " ".repeat(available_width.saturating_sub(str_width(&line))) 415 | ) 416 | }) 417 | .collect() 418 | }; 419 | 420 | let (prefix, _) = first_prefix 421 | .take() 422 | .unwrap_or_else(|| self.prefix2(Some(&[&language_context[..]]))); 423 | let (suffix, _) = first_suffix 424 | .take() 425 | .unwrap_or_else(|| self.suffix2(Some(&[&language_context[..]]))); 426 | println!( 427 | "{}{}{}{}{}{}{}", 428 | self.centering, 429 | self.margin, 430 | prefix, 431 | style.paint(" ".repeat(available_width)), 432 | suffix, 433 | self.margin, 434 | self.shadow(), 435 | ); 436 | 437 | for line in buffer.lines() { 438 | let width = str_width(line); 439 | let (prefix, _) = self.prefix2(Some(&[&language_context[..]])); 440 | let (suffix, _) = self.suffix2(Some(&[&language_context[..]])); 441 | print!( 442 | "{}{}{}{}", 443 | self.centering, 444 | self.margin, 445 | prefix, 446 | style.prefix(), 447 | ); 448 | for (s, is_ansi) in AnsiCodeIterator::new(line) { 449 | if is_ansi { 450 | if s == "\u{1b}[0m" { 451 | print!("{}{}", s, style.prefix()); 452 | } else { 453 | print!("{}{}", style.prefix(), s); 454 | } 455 | } else { 456 | print!("{}", s); 457 | } 458 | } 459 | println!( 460 | "{}{}{}{}", 461 | style.paint(" ".repeat(available_width.saturating_sub(width))), 462 | suffix, 463 | self.margin, 464 | self.shadow(), 465 | ); 466 | } 467 | 468 | let (prefix, _) = first_prefix 469 | .take() 470 | .unwrap_or_else(|| self.prefix2(Some(&[&language_context[..]]))); 471 | let (suffix, _) = first_suffix 472 | .take() 473 | .unwrap_or_else(|| self.suffix2(Some(&[&language_context[..]]))); 474 | println!( 475 | "{}{}{}{}{}{}{}", 476 | self.centering, 477 | self.margin, 478 | prefix, 479 | format!( 480 | "{}{}", 481 | style.paint(" ".repeat(available_width.saturating_sub(str_width(&lang)))), 482 | self.style3(Some(&[&language_context[..]]), Some("lang-tag")) 483 | .paint(lang) 484 | ), 485 | suffix, 486 | self.margin, 487 | self.shadow(), 488 | ); 489 | } 490 | _ => {} 491 | } 492 | } 493 | 494 | fn flush(&mut self) { 495 | if !self.buffer.is_empty() { 496 | return; 497 | } 498 | if self 499 | .scope 500 | .iter() 501 | .find(|scope| { 502 | if let Scope::Table(..) = scope { 503 | true 504 | } else { 505 | false 506 | } 507 | }) 508 | .is_some() 509 | { 510 | return; 511 | } 512 | if self.content.is_empty() { 513 | return; 514 | } 515 | let (prefix, prefix_len) = self.prefix(); 516 | let (suffix, suffix_len) = self.suffix(); 517 | println!( 518 | "{}{}{}{}{}{}{}{}", 519 | self.centering, 520 | self.margin, 521 | prefix, 522 | self.content, 523 | suffix, 524 | self.paper_style().paint( 525 | " ".repeat( 526 | self.width 527 | .saturating_sub(str_width(&self.content)) 528 | .saturating_sub(prefix_len) 529 | .saturating_sub(suffix_len) 530 | ) 531 | ), 532 | self.margin, 533 | self.shadow(), 534 | ); 535 | self.content.clear(); 536 | } 537 | 538 | fn target(&mut self) -> &mut String { 539 | if self 540 | .scope 541 | .iter() 542 | .find(|scope| *scope == &Scope::TableHead) 543 | .is_some() 544 | { 545 | self.table.0.last_mut().unwrap() 546 | } else if self 547 | .scope 548 | .iter() 549 | .find(|scope| *scope == &Scope::TableRow) 550 | .is_some() 551 | { 552 | self.table.1.last_mut().unwrap().last_mut().unwrap() 553 | } else { 554 | &mut self.content 555 | } 556 | } 557 | 558 | fn handle_text(&mut self, text: S) 559 | where 560 | S: AsRef, 561 | { 562 | let s = text.as_ref(); 563 | if let Some(Scope::CodeBlock(..)) = self.scope.last() { 564 | self.buffer += s; 565 | return; 566 | } 567 | let style = self.style(); 568 | for word in Words::new(s) { 569 | if str_width(&self.content) + word.len() + self.prefix_len() + self.suffix_len() 570 | > self.width 571 | { 572 | self.flush(); 573 | } 574 | let mut word = if self.target().is_empty() { 575 | word.trim() 576 | } else { 577 | &word 578 | }; 579 | let available_len = self 580 | .width 581 | .saturating_sub(self.prefix_len()) 582 | .saturating_sub(self.suffix_len()); 583 | while str_width(&self.content) + str_width(&word) > available_len { 584 | let part = word.chars().take(available_len).collect::(); 585 | self.target().push_str(&format!("{}", style.paint(&part))); 586 | word = &word[part.len()..]; 587 | self.flush(); 588 | } 589 | self.target().push_str(&format!("{}", style.paint(word))); 590 | } 591 | } 592 | 593 | pub fn handle(&mut self, event: Event) { 594 | match event { 595 | Event::Start(tag) => { 596 | if self.empty_queued { 597 | // TODO: queue an empty after an item's initial text when there's a block 598 | self.empty(); 599 | } 600 | match tag { 601 | Tag::MetadataBlock(..) => self.scope.push(Scope::CodeBlock("".to_owned())), 602 | Tag::HtmlBlock => {} 603 | Tag::Paragraph => { 604 | self.flush(); 605 | } 606 | Tag::Heading { 607 | level: HeadingLevel::H1, 608 | .. 609 | } => { 610 | self.flush(); 611 | self.print_rule(); 612 | self.scope.push(Scope::Heading(HeadingLevel::H1)); 613 | } 614 | Tag::Heading { level, .. } => { 615 | self.flush(); 616 | self.scope.push(Scope::Heading(level)); 617 | } 618 | Tag::BlockQuote(kind) => { 619 | self.flush(); 620 | self.scope.push(Scope::BlockQuote(kind)); 621 | match kind { 622 | None => {} 623 | Some(BlockQuoteKind::Note) => { 624 | let style = Self::resolve_scopes( 625 | &self.stylesheet, 626 | &["note-blockquote"], 627 | Some("prefix"), 628 | ); 629 | self.handle_text(&format!( 630 | "{} {}", 631 | style.paint("󰋽"), 632 | style.paint("Note") 633 | )); 634 | } 635 | Some(BlockQuoteKind::Tip) => { 636 | let style = Self::resolve_scopes( 637 | &self.stylesheet, 638 | &["tip-blockquote"], 639 | Some("prefix"), 640 | ); 641 | self.handle_text(&format!( 642 | "{} {}", 643 | style.paint("󰌶"), 644 | style.paint("Tip") 645 | )); 646 | } 647 | Some(BlockQuoteKind::Important) => { 648 | let style = Self::resolve_scopes( 649 | &self.stylesheet, 650 | &["important-blockquote"], 651 | Some("prefix"), 652 | ); 653 | self.handle_text(&format!( 654 | "{} {}", 655 | style.paint("󱋉"), 656 | style.paint("Important") 657 | )); 658 | } 659 | Some(BlockQuoteKind::Warning) => { 660 | let style = Self::resolve_scopes( 661 | &self.stylesheet, 662 | &["warning-blockquote"], 663 | Some("prefix"), 664 | ); 665 | self.handle_text(&format!( 666 | "{} {}", 667 | style.paint("󰀪"), 668 | style.paint("Warning") 669 | )); 670 | } 671 | Some(BlockQuoteKind::Caution) => { 672 | let style = Self::resolve_scopes( 673 | &self.stylesheet, 674 | &["caution-blockquote"], 675 | Some("prefix"), 676 | ); 677 | self.handle_text(&format!( 678 | "{} {}", 679 | style.paint("󰳦"), 680 | style.paint("Caution") 681 | )); 682 | } 683 | } 684 | } 685 | Tag::CodeBlock(CodeBlockKind::Indented) => { 686 | self.flush(); 687 | self.scope.push(Scope::CodeBlock("".to_owned())); 688 | } 689 | Tag::CodeBlock(CodeBlockKind::Fenced(language)) => { 690 | self.flush(); 691 | self.scope.push(Scope::CodeBlock(language.into_string())); 692 | } 693 | Tag::List(start_index) => { 694 | self.flush(); 695 | self.scope.push(Scope::List(start_index)); 696 | } 697 | Tag::DefinitionList => { 698 | self.flush(); 699 | self.scope.push(Scope::DefinitionList); 700 | } 701 | Tag::DefinitionListTitle => { 702 | self.flush(); 703 | self.scope.push(Scope::Term); 704 | } 705 | Tag::DefinitionListDefinition => { 706 | self.flush(); 707 | self.scope.push(Scope::Definition); 708 | } 709 | Tag::Item => { 710 | self.flush(); 711 | if let Some(&Scope::List(index)) = self.scope.last() { 712 | self.scope.push(Scope::ListItem(index, false)); 713 | } else { 714 | self.scope.push(Scope::ListItem(None, false)); 715 | } 716 | } 717 | Tag::FootnoteDefinition(text) => { 718 | self.flush(); 719 | self.scope.push(Scope::FootnoteDefinition); 720 | self.handle_text(&format!("{}:", text)); 721 | self.scope.pop(); 722 | self.flush(); 723 | self.scope.push(Scope::FootnoteContent); 724 | } 725 | Tag::Table(columns) => self.scope.push(Scope::Table(columns)), 726 | Tag::TableHead => { 727 | self.scope.push(Scope::TableHead); 728 | } 729 | Tag::TableRow => { 730 | self.scope.push(Scope::TableRow); 731 | self.table.1.push(vec![]); 732 | } 733 | Tag::TableCell => { 734 | self.scope.push(Scope::TableCell); 735 | if self 736 | .scope 737 | .iter() 738 | .find(|scope| *scope == &Scope::TableHead) 739 | .is_some() 740 | { 741 | self.table.0.push(String::new()); 742 | } else { 743 | self.table.1.last_mut().unwrap().push(String::new()); 744 | } 745 | } 746 | Tag::Emphasis => { 747 | self.scope.push(Scope::Italic); 748 | } 749 | Tag::Strong => { 750 | self.scope.push(Scope::Bold); 751 | } 752 | Tag::Strikethrough => { 753 | self.scope.push(Scope::Strikethrough); 754 | } 755 | Tag::Link { 756 | dest_url, title, .. 757 | } => { 758 | self.scope.push(Scope::Link { 759 | dest_url: dest_url.into_string(), 760 | title: title.into_string(), 761 | }); 762 | } 763 | Tag::Image { 764 | dest_url, title, .. 765 | } => { 766 | self.flush(); 767 | 768 | if !self.opts.no_images { 769 | let available_width = self 770 | .width 771 | .saturating_sub(self.prefix_len()) 772 | .saturating_sub(self.suffix_len()); 773 | match image::open(dest_url.as_ref()) { 774 | Ok(image) => { 775 | let (mut width, mut height) = image.dimensions(); 776 | if width > available_width as u32 { 777 | let scale = available_width as f64 / width as f64; 778 | width = (width as f64 * scale) as u32; 779 | height = (height as f64 * scale) as u32; 780 | } 781 | let mut vec = vec![]; 782 | termpix::print_image(image, true, width, height, &mut vec); 783 | let string = String::from_utf8(vec).unwrap(); 784 | 785 | for line in string.lines() { 786 | let (prefix, _) = self.prefix(); 787 | let (suffix, _) = self.suffix(); 788 | println!( 789 | "{}{}{}{}{}{}{}", 790 | self.centering, 791 | self.margin, 792 | prefix, 793 | line, 794 | suffix, 795 | self.margin, 796 | self.shadow(), 797 | ); 798 | } 799 | 800 | self.scope.push(Scope::Indent); 801 | self.scope.push(Scope::Caption); 802 | self.handle_text(title); 803 | } 804 | Err(error) => { 805 | self.handle_text("Cannot open image "); 806 | self.scope.push(Scope::Indent); 807 | self.scope.push(Scope::Link { 808 | dest_url: "".to_owned(), 809 | title: "".to_owned(), 810 | }); 811 | self.handle_text(dest_url); 812 | self.scope.pop(); 813 | self.handle_text(&format!(": {}", error)); 814 | self.scope.push(Scope::Caption); 815 | self.flush(); 816 | } 817 | } 818 | } else { 819 | self.scope.push(Scope::Indent); 820 | self.handle_text("[Image"); 821 | if !title.is_empty() { 822 | self.handle_text(": "); 823 | self.scope.push(Scope::Caption); 824 | self.handle_text(title); 825 | self.scope.pop(); 826 | } 827 | if !dest_url.is_empty() && !self.opts.hide_urls { 828 | self.handle_text(" <"); 829 | self.scope.push(Scope::Link { 830 | dest_url: "".to_owned(), 831 | title: "".to_owned(), 832 | }); 833 | self.handle_text(dest_url); 834 | self.scope.pop(); 835 | self.handle_text(">"); 836 | } 837 | self.handle_text("]"); 838 | self.scope.push(Scope::Caption); 839 | self.flush(); 840 | } 841 | } 842 | } 843 | } 844 | 845 | Event::End(tag) => match tag { 846 | TagEnd::Paragraph => { 847 | self.flush(); 848 | self.queue_empty(); 849 | } 850 | TagEnd::Heading(HeadingLevel::H1) => { 851 | self.flush(); 852 | self.scope.pop(); 853 | self.print_rule(); 854 | self.queue_empty(); 855 | } 856 | TagEnd::Heading(_) => { 857 | self.flush(); 858 | self.scope.pop(); 859 | self.queue_empty(); 860 | } 861 | TagEnd::List(..) => { 862 | self.flush(); 863 | self.scope.pop(); 864 | self.queue_empty(); 865 | } 866 | TagEnd::DefinitionList => { 867 | self.flush(); 868 | self.scope.pop(); 869 | self.queue_empty(); 870 | } 871 | TagEnd::DefinitionListTitle => { 872 | self.flush(); 873 | self.scope.pop(); 874 | } 875 | TagEnd::DefinitionListDefinition => { 876 | self.flush(); 877 | self.scope.pop(); 878 | self.queue_empty(); 879 | } 880 | TagEnd::Item => { 881 | self.flush(); 882 | self.scope.pop(); 883 | if let Some(Scope::List(index)) = self.scope.last_mut() { 884 | *index = index.map(|x| x + 1); 885 | } 886 | } 887 | TagEnd::BlockQuote(..) => { 888 | self.flush(); 889 | self.scope.pop(); 890 | self.queue_empty(); 891 | } 892 | TagEnd::Table => { 893 | self.print_table(); 894 | self.scope.pop(); 895 | self.queue_empty(); 896 | } 897 | TagEnd::HtmlBlock => {} 898 | TagEnd::CodeBlock => { 899 | self.flush_buffer(); 900 | self.scope.pop(); 901 | self.queue_empty(); 902 | } 903 | TagEnd::Link => { 904 | let Scope::Link { dest_url, title } = self.scope.pop().unwrap() else { 905 | panic!() 906 | }; 907 | if !title.is_empty() && !dest_url.is_empty() && !self.opts.hide_urls { 908 | self.handle_text(format!(" <{}: {}>", title, dest_url)); 909 | } else if !dest_url.is_empty() && !self.opts.hide_urls { 910 | self.handle_text(format!(" <{}>", dest_url)); 911 | } else if !title.is_empty() { 912 | self.handle_text(format!(" <{}>", title)); 913 | } 914 | } 915 | TagEnd::Image => { 916 | self.flush(); 917 | self.scope.pop(); 918 | self.scope.pop(); 919 | self.queue_empty(); 920 | } 921 | TagEnd::FootnoteDefinition => { 922 | self.flush(); 923 | self.scope.pop(); 924 | self.queue_empty(); 925 | } 926 | _ => { 927 | self.scope.pop(); 928 | } 929 | }, 930 | Event::Rule => { 931 | self.flush(); 932 | self.print_rule(); 933 | } 934 | Event::Text(text) => { 935 | self.handle_text(text); 936 | } 937 | Event::Code(text) => { 938 | self.scope.push(Scope::Code); 939 | self.handle_text(text); 940 | self.scope.pop(); 941 | } 942 | Event::Html(_text) => { /* not rendered */ } 943 | Event::InlineHtml(_text) => { /* not rendered */ } 944 | Event::InlineMath(text) | Event::DisplayMath(text) => { 945 | self.scope.push(Scope::Code); 946 | self.handle_text(text); 947 | self.scope.pop(); 948 | } 949 | Event::FootnoteReference(text) => { 950 | self.scope.push(Scope::FootnoteReference); 951 | self.handle_text(&format!("[{}]", text)); 952 | self.scope.pop(); 953 | } 954 | Event::SoftBreak => { 955 | self.handle_text(" "); 956 | } 957 | Event::HardBreak => { 958 | self.flush(); 959 | } 960 | Event::TaskListMarker(checked) => { 961 | self.handle_text(if checked { "[✓] " } else { "[ ] " }); 962 | } 963 | } 964 | } 965 | } 966 | -------------------------------------------------------------------------------- /src/str_width.rs: -------------------------------------------------------------------------------- 1 | use console::strip_ansi_codes; 2 | use unicode_width::UnicodeWidthChar; 3 | 4 | pub fn str_width(s: &str) -> usize { 5 | strip_ansi_codes(s) 6 | .chars() 7 | .flat_map(|ch| { 8 | if cjk::is_cjk_codepoint(ch) { 9 | UnicodeWidthChar::width_cjk(ch) 10 | } else { 11 | UnicodeWidthChar::width(ch) 12 | } 13 | }) 14 | .sum() 15 | } 16 | -------------------------------------------------------------------------------- /src/table.rs: -------------------------------------------------------------------------------- 1 | use crate::str_width; 2 | use crate::words::Words; 3 | use ansi_term::Style; 4 | use console::strip_ansi_codes; 5 | use pulldown_cmark::Alignment; 6 | use std::io::Write; 7 | 8 | pub struct Table { 9 | titles: Vec, 10 | rows: Vec>, 11 | width: usize, 12 | } 13 | 14 | impl Table { 15 | pub fn new(titles: Vec, rows: Vec>, width: usize) -> Self { 16 | Table { 17 | titles, 18 | rows, 19 | width, 20 | } 21 | } 22 | 23 | pub fn print(self, paper_style: Style, alignment: &[Alignment]) -> String { 24 | let Table { 25 | titles, 26 | rows, 27 | width, 28 | } = self; 29 | 30 | // NOTE: for now, styling is not supported within tables because that gets really hard 31 | let titles = titles 32 | .iter() 33 | .map(|title| strip_ansi_codes(title).trim().to_string()) 34 | .collect::>(); 35 | let rows = rows 36 | .iter() 37 | .map(|row| { 38 | row.iter() 39 | .map(|cell| strip_ansi_codes(cell).trim().to_string()) 40 | .collect() 41 | }) 42 | .collect::>>(); 43 | 44 | let num_cols = usize::max( 45 | titles.len(), 46 | rows.iter().map(|row| row.len()).max().unwrap_or(0), 47 | ); 48 | 49 | let mut title_longest_words = titles 50 | .iter() 51 | .map(|title| { 52 | Words::new(title) 53 | .map(|word| str_width(word.trim())) 54 | .max() 55 | .unwrap_or(0) 56 | }) 57 | .collect::>(); 58 | title_longest_words.resize(num_cols, 0); 59 | let longest_words = rows 60 | .iter() 61 | .map(|row| { 62 | row.iter() 63 | .map(|cell| { 64 | Words::new(cell) 65 | .map(|word| str_width(word.trim())) 66 | .max() 67 | .unwrap_or(0) 68 | }) 69 | .collect::>() 70 | }) 71 | .fold(title_longest_words, |mut chars, row| { 72 | for i in 0..row.len() { 73 | chars[i] = usize::max(chars[i], row[i]); 74 | } 75 | chars 76 | }); 77 | 78 | let mut title_chars = titles 79 | .iter() 80 | .map(|title| title.lines().map(str_width).max().unwrap_or(0)) 81 | .collect::>(); 82 | title_chars.resize(num_cols, 0); 83 | let max_chars_per_col = rows 84 | .iter() 85 | .map(|row| { 86 | row.iter() 87 | .map(|cell| cell.lines().map(str_width).max().unwrap_or(0)) 88 | .collect::>() 89 | }) 90 | .fold(title_chars.clone(), |mut chars, row| { 91 | for i in 0..row.len() { 92 | chars[i] = usize::max(1, usize::max(chars[i], row[i])); 93 | } 94 | chars 95 | }); 96 | 97 | let total_chars: usize = max_chars_per_col.iter().sum(); 98 | let max_chars_width = width.saturating_sub(4 + (num_cols - 1) * 3); 99 | let col_widths = if total_chars < max_chars_width { 100 | max_chars_per_col 101 | } else { 102 | max_chars_per_col 103 | .into_iter() 104 | .enumerate() 105 | .map(|(i, chars)| { 106 | usize::max( 107 | longest_words[i], 108 | (max_chars_width as f64 * chars as f64 / total_chars as f64) as usize, 109 | ) 110 | }) 111 | .collect() 112 | }; 113 | if col_widths.iter().sum::() > max_chars_width { 114 | return format!("{}", paper_style.paint("[Table too large to fit]")); 115 | } 116 | 117 | let mut buffer = vec![]; 118 | print_separator(&mut buffer, &col_widths, '─', '┌', '┬', '┐', paper_style); 119 | if !titles.is_empty() { 120 | print_row(&mut buffer, &col_widths, alignment, &titles, paper_style); 121 | print_separator(&mut buffer, &col_widths, '═', '╞', '╪', '╡', paper_style); 122 | } 123 | let row_count = rows.len(); 124 | for (i, row) in rows.into_iter().enumerate() { 125 | print_row(&mut buffer, &col_widths, alignment, &row, paper_style); 126 | if i != row_count - 1 { 127 | print_separator(&mut buffer, &col_widths, '─', '├', '┼', '┤', paper_style); 128 | } 129 | } 130 | print_separator(&mut buffer, &col_widths, '─', '└', '┴', '┘', paper_style); 131 | 132 | String::from_utf8(buffer).unwrap() 133 | } 134 | } 135 | 136 | fn print_row( 137 | w: &mut W, 138 | cols: &[usize], 139 | alignment: &[Alignment], 140 | row: &[String], 141 | paper_style: Style, 142 | ) { 143 | let mut row_words = row.into_iter().map(|s| Words::new(s)).collect::>(); 144 | loop { 145 | let mut done = true; 146 | write!(w, "{}", paper_style.paint("│")).unwrap(); 147 | for (i, words) in row_words.iter_mut().enumerate() { 148 | let mut line = match words.next() { 149 | Some(line) => line.trim().to_string(), 150 | None => { 151 | write!( 152 | w, 153 | "{}", 154 | paper_style.paint(format!(" {: { 163 | if str_width(&line) + str_width(&next) <= cols[i] { 164 | line += &next; 165 | } else { 166 | words.undo(); 167 | done = false; 168 | break; 169 | } 170 | } 171 | None => break, 172 | }; 173 | } 174 | line = line.trim().to_string(); 175 | let padded = if alignment[i] == Alignment::Center { 176 | format!( 177 | " {: ^width$} │", 178 | line, 179 | width = cols[i] - (line.len().saturating_sub(str_width(&line))) 180 | ) 181 | } else if alignment[i] == Alignment::Right { 182 | format!( 183 | " {: >width$} │", 184 | line, 185 | width = cols[i] - (line.len().saturating_sub(str_width(&line))) 186 | ) 187 | } else { 188 | format!( 189 | " {: ( 204 | w: &mut W, 205 | cols: &[usize], 206 | mid: char, 207 | left: char, 208 | cross: char, 209 | right: char, 210 | paper_style: Style, 211 | ) { 212 | let line = cols 213 | .iter() 214 | .map(|width| mid.to_string().repeat(*width)) 215 | .collect::>() 216 | .join(&format!("{}{}{}", mid, cross, mid)); 217 | write!( 218 | w, 219 | "{}\n", 220 | paper_style.paint(format!("{}{}{}{}{}", left, mid, line, mid, right)) 221 | ) 222 | .unwrap(); 223 | } 224 | -------------------------------------------------------------------------------- /src/termpix.rs: -------------------------------------------------------------------------------- 1 | //! This module is being used temporarily until someone publishes termpix to crates.io 2 | use ansi_term::ANSIStrings; 3 | use ansi_term::Colour::Fixed; 4 | use image::{ 5 | Pixel, 6 | imageops::{self, FilterType}, 7 | }; 8 | use std::io::Write; 9 | 10 | pub fn print_image( 11 | img: image::DynamicImage, 12 | true_colour: bool, 13 | width: u32, 14 | height: u32, 15 | w: &mut W, 16 | ) { 17 | let img = imageops::resize(&img, width, height, FilterType::Nearest); 18 | 19 | if !true_colour { 20 | for y in 0..height { 21 | //TODO: inc by 2 instead 22 | if y % 2 == 1 || y + 1 == height { 23 | continue; 24 | } 25 | 26 | let row: Vec<_> = (0..width) 27 | .map(|x| { 28 | let mut top = img[(x, y)]; 29 | let mut bottom = img[(x, y + 1)]; 30 | blend_alpha(&mut top); 31 | blend_alpha(&mut bottom); 32 | let top_colour = find_colour_index(top.to_rgb().channels()); 33 | let bottom_colour = find_colour_index(bottom.to_rgb().channels()); 34 | Fixed(bottom_colour).on(Fixed(top_colour)).paint("▄") 35 | }) 36 | .collect(); 37 | 38 | write!(w, "{}\n", ANSIStrings(&row)).ok(); 39 | } 40 | } else { 41 | let mut row = Vec::new(); 42 | for y in 0..height { 43 | //TODO: inc by 2 instead 44 | if y % 2 == 1 || y + 1 == height { 45 | continue; 46 | } 47 | 48 | for x in 0..width { 49 | let mut top = img[(x, y)]; 50 | let mut bottom = img[(x, y + 1)]; 51 | blend_alpha(&mut top); 52 | blend_alpha(&mut bottom); 53 | write!( 54 | row, 55 | "\x1b[48;2;{};{};{}m\x1b[38;2;{};{};{}m▄", 56 | top[0], top[1], top[2], bottom[0], bottom[1], bottom[2] 57 | ) 58 | .unwrap(); 59 | } 60 | 61 | write!(row, "\x1b[m\n").unwrap(); 62 | w.write(&row).unwrap(); 63 | row.clear(); 64 | } 65 | } 66 | } 67 | 68 | fn find_colour_index(pixel: &[u8]) -> u8 { 69 | let mut best = 0; 70 | let mut best_distance = 255 * 255 * 3 + 1; 71 | for i in 16..255 { 72 | let ansi_colour = ANSI_COLOURS[i]; 73 | let dr = ansi_colour[0] - pixel[0] as i32; 74 | let dg = ansi_colour[1] - pixel[1] as i32; 75 | let db = ansi_colour[2] - pixel[2] as i32; 76 | let distance = dr * dr + dg * dg + db * db; 77 | 78 | if distance < best_distance { 79 | best_distance = distance; 80 | best = i as u8; 81 | } 82 | } 83 | 84 | return best; 85 | } 86 | 87 | fn blend_alpha(pixel: &mut image::Rgba) { 88 | let alpha = pixel[3] as i32 as f32 / 255.0; 89 | pixel[0] = (alpha * (pixel[0] as i32 as f32) + (1.0 - alpha) * 38.0) as u8; 90 | pixel[1] = (alpha * (pixel[1] as i32 as f32) + (1.0 - alpha) * 38.0) as u8; 91 | pixel[2] = (alpha * (pixel[2] as i32 as f32) + (1.0 - alpha) * 38.0) as u8; 92 | } 93 | 94 | #[rustfmt::skip] 95 | static ANSI_COLOURS: [[i32; 3]; 256] = [ 96 | [ 0x00, 0x00, 0x00 ],[ 0x80, 0x00, 0x00 ],[ 0x00, 0x80, 0x00 ],[ 0x80, 0x80, 0x00 ],[ 0x00, 0x00, 0x80 ], 97 | [ 0x80, 0x00, 0x80 ],[ 0x00, 0x80, 0x80 ],[ 0xc0, 0xc0, 0xc0 ],[ 0x80, 0x80, 0x80 ],[ 0xff, 0x00, 0x00 ], 98 | [ 0x00, 0xff, 0x00 ],[ 0xff, 0xff, 0x00 ],[ 0x00, 0x00, 0xff ],[ 0xff, 0x00, 0xff ],[ 0x00, 0xff, 0xff ], 99 | [ 0xff, 0xff, 0xff ],[ 0x00, 0x00, 0x00 ],[ 0x00, 0x00, 0x5f ],[ 0x00, 0x00, 0x87 ],[ 0x00, 0x00, 0xaf ], 100 | [ 0x00, 0x00, 0xd7 ],[ 0x00, 0x00, 0xff ],[ 0x00, 0x5f, 0x00 ],[ 0x00, 0x5f, 0x5f ],[ 0x00, 0x5f, 0x87 ], 101 | [ 0x00, 0x5f, 0xaf ],[ 0x00, 0x5f, 0xd7 ],[ 0x00, 0x5f, 0xff ],[ 0x00, 0x87, 0x00 ],[ 0x00, 0x87, 0x5f ], 102 | [ 0x00, 0x87, 0x87 ],[ 0x00, 0x87, 0xaf ],[ 0x00, 0x87, 0xd7 ],[ 0x00, 0x87, 0xff ],[ 0x00, 0xaf, 0x00 ], 103 | [ 0x00, 0xaf, 0x5f ],[ 0x00, 0xaf, 0x87 ],[ 0x00, 0xaf, 0xaf ],[ 0x00, 0xaf, 0xd7 ],[ 0x00, 0xaf, 0xff ], 104 | [ 0x00, 0xd7, 0x00 ],[ 0x00, 0xd7, 0x5f ],[ 0x00, 0xd7, 0x87 ],[ 0x00, 0xd7, 0xaf ],[ 0x00, 0xd7, 0xd7 ], 105 | [ 0x00, 0xd7, 0xff ],[ 0x00, 0xff, 0x00 ],[ 0x00, 0xff, 0x5f ],[ 0x00, 0xff, 0x87 ],[ 0x00, 0xff, 0xaf ], 106 | [ 0x00, 0xff, 0xd7 ],[ 0x00, 0xff, 0xff ],[ 0x5f, 0x00, 0x00 ],[ 0x5f, 0x00, 0x5f ],[ 0x5f, 0x00, 0x87 ], 107 | [ 0x5f, 0x00, 0xaf ],[ 0x5f, 0x00, 0xd7 ],[ 0x5f, 0x00, 0xff ],[ 0x5f, 0x5f, 0x00 ],[ 0x5f, 0x5f, 0x5f ], 108 | [ 0x5f, 0x5f, 0x87 ],[ 0x5f, 0x5f, 0xaf ],[ 0x5f, 0x5f, 0xd7 ],[ 0x5f, 0x5f, 0xff ],[ 0x5f, 0x87, 0x00 ], 109 | [ 0x5f, 0x87, 0x5f ],[ 0x5f, 0x87, 0x87 ],[ 0x5f, 0x87, 0xaf ],[ 0x5f, 0x87, 0xd7 ],[ 0x5f, 0x87, 0xff ], 110 | [ 0x5f, 0xaf, 0x00 ],[ 0x5f, 0xaf, 0x5f ],[ 0x5f, 0xaf, 0x87 ],[ 0x5f, 0xaf, 0xaf ],[ 0x5f, 0xaf, 0xd7 ], 111 | [ 0x5f, 0xaf, 0xff ],[ 0x5f, 0xd7, 0x00 ],[ 0x5f, 0xd7, 0x5f ],[ 0x5f, 0xd7, 0x87 ],[ 0x5f, 0xd7, 0xaf ], 112 | [ 0x5f, 0xd7, 0xd7 ],[ 0x5f, 0xd7, 0xff ],[ 0x5f, 0xff, 0x00 ],[ 0x5f, 0xff, 0x5f ],[ 0x5f, 0xff, 0x87 ], 113 | [ 0x5f, 0xff, 0xaf ],[ 0x5f, 0xff, 0xd7 ],[ 0x5f, 0xff, 0xff ],[ 0x87, 0x00, 0x00 ],[ 0x87, 0x00, 0x5f ], 114 | [ 0x87, 0x00, 0x87 ],[ 0x87, 0x00, 0xaf ],[ 0x87, 0x00, 0xd7 ],[ 0x87, 0x00, 0xff ],[ 0x87, 0x5f, 0x00 ], 115 | [ 0x87, 0x5f, 0x5f ],[ 0x87, 0x5f, 0x87 ],[ 0x87, 0x5f, 0xaf ],[ 0x87, 0x5f, 0xd7 ],[ 0x87, 0x5f, 0xff ], 116 | [ 0x87, 0x87, 0x00 ],[ 0x87, 0x87, 0x5f ],[ 0x87, 0x87, 0x87 ],[ 0x87, 0x87, 0xaf ],[ 0x87, 0x87, 0xd7 ], 117 | [ 0x87, 0x87, 0xff ],[ 0x87, 0xaf, 0x00 ],[ 0x87, 0xaf, 0x5f ],[ 0x87, 0xaf, 0x87 ],[ 0x87, 0xaf, 0xaf ], 118 | [ 0x87, 0xaf, 0xd7 ],[ 0x87, 0xaf, 0xff ],[ 0x87, 0xd7, 0x00 ],[ 0x87, 0xd7, 0x5f ],[ 0x87, 0xd7, 0x87 ], 119 | [ 0x87, 0xd7, 0xaf ],[ 0x87, 0xd7, 0xd7 ],[ 0x87, 0xd7, 0xff ],[ 0x87, 0xff, 0x00 ],[ 0x87, 0xff, 0x5f ], 120 | [ 0x87, 0xff, 0x87 ],[ 0x87, 0xff, 0xaf ],[ 0x87, 0xff, 0xd7 ],[ 0x87, 0xff, 0xff ],[ 0xaf, 0x00, 0x00 ], 121 | [ 0xaf, 0x00, 0x5f ],[ 0xaf, 0x00, 0x87 ],[ 0xaf, 0x00, 0xaf ],[ 0xaf, 0x00, 0xd7 ],[ 0xaf, 0x00, 0xff ], 122 | [ 0xaf, 0x5f, 0x00 ],[ 0xaf, 0x5f, 0x5f ],[ 0xaf, 0x5f, 0x87 ],[ 0xaf, 0x5f, 0xaf ],[ 0xaf, 0x5f, 0xd7 ], 123 | [ 0xaf, 0x5f, 0xff ],[ 0xaf, 0x87, 0x00 ],[ 0xaf, 0x87, 0x5f ],[ 0xaf, 0x87, 0x87 ],[ 0xaf, 0x87, 0xaf ], 124 | [ 0xaf, 0x87, 0xd7 ],[ 0xaf, 0x87, 0xff ],[ 0xaf, 0xaf, 0x00 ],[ 0xaf, 0xaf, 0x5f ],[ 0xaf, 0xaf, 0x87 ], 125 | [ 0xaf, 0xaf, 0xaf ],[ 0xaf, 0xaf, 0xd7 ],[ 0xaf, 0xaf, 0xff ],[ 0xaf, 0xd7, 0x00 ],[ 0xaf, 0xd7, 0x5f ], 126 | [ 0xaf, 0xd7, 0x87 ],[ 0xaf, 0xd7, 0xaf ],[ 0xaf, 0xd7, 0xd7 ],[ 0xaf, 0xd7, 0xff ],[ 0xaf, 0xff, 0x00 ], 127 | [ 0xaf, 0xff, 0x5f ],[ 0xaf, 0xff, 0x87 ],[ 0xaf, 0xff, 0xaf ],[ 0xaf, 0xff, 0xd7 ],[ 0xaf, 0xff, 0xff ], 128 | [ 0xd7, 0x00, 0x00 ],[ 0xd7, 0x00, 0x5f ],[ 0xd7, 0x00, 0x87 ],[ 0xd7, 0x00, 0xaf ],[ 0xd7, 0x00, 0xd7 ], 129 | [ 0xd7, 0x00, 0xff ],[ 0xd7, 0x5f, 0x00 ],[ 0xd7, 0x5f, 0x5f ],[ 0xd7, 0x5f, 0x87 ],[ 0xd7, 0x5f, 0xaf ], 130 | [ 0xd7, 0x5f, 0xd7 ],[ 0xd7, 0x5f, 0xff ],[ 0xd7, 0x87, 0x00 ],[ 0xd7, 0x87, 0x5f ],[ 0xd7, 0x87, 0x87 ], 131 | [ 0xd7, 0x87, 0xaf ],[ 0xd7, 0x87, 0xd7 ],[ 0xd7, 0x87, 0xff ],[ 0xd7, 0xaf, 0x00 ],[ 0xd7, 0xaf, 0x5f ], 132 | [ 0xd7, 0xaf, 0x87 ],[ 0xd7, 0xaf, 0xaf ],[ 0xd7, 0xaf, 0xd7 ],[ 0xd7, 0xaf, 0xff ],[ 0xd7, 0xd7, 0x00 ], 133 | [ 0xd7, 0xd7, 0x5f ],[ 0xd7, 0xd7, 0x87 ],[ 0xd7, 0xd7, 0xaf ],[ 0xd7, 0xd7, 0xd7 ],[ 0xd7, 0xd7, 0xff ], 134 | [ 0xd7, 0xff, 0x00 ],[ 0xd7, 0xff, 0x5f ],[ 0xd7, 0xff, 0x87 ],[ 0xd7, 0xff, 0xaf ],[ 0xd7, 0xff, 0xd7 ], 135 | [ 0xd7, 0xff, 0xff ],[ 0xff, 0x00, 0x00 ],[ 0xff, 0x00, 0x5f ],[ 0xff, 0x00, 0x87 ],[ 0xff, 0x00, 0xaf ], 136 | [ 0xff, 0x00, 0xd7 ],[ 0xff, 0x00, 0xff ],[ 0xff, 0x5f, 0x00 ],[ 0xff, 0x5f, 0x5f ],[ 0xff, 0x5f, 0x87 ], 137 | [ 0xff, 0x5f, 0xaf ],[ 0xff, 0x5f, 0xd7 ],[ 0xff, 0x5f, 0xff ],[ 0xff, 0x87, 0x00 ],[ 0xff, 0x87, 0x5f ], 138 | [ 0xff, 0x87, 0x87 ],[ 0xff, 0x87, 0xaf ],[ 0xff, 0x87, 0xd7 ],[ 0xff, 0x87, 0xff ],[ 0xff, 0xaf, 0x00 ], 139 | [ 0xff, 0xaf, 0x5f ],[ 0xff, 0xaf, 0x87 ],[ 0xff, 0xaf, 0xaf ],[ 0xff, 0xaf, 0xd7 ],[ 0xff, 0xaf, 0xff ], 140 | [ 0xff, 0xd7, 0x00 ],[ 0xff, 0xd7, 0x5f ],[ 0xff, 0xd7, 0x87 ],[ 0xff, 0xd7, 0xaf ],[ 0xff, 0xd7, 0xd7 ], 141 | [ 0xff, 0xd7, 0xff ],[ 0xff, 0xff, 0x00 ],[ 0xff, 0xff, 0x5f ],[ 0xff, 0xff, 0x87 ],[ 0xff, 0xff, 0xaf ], 142 | [ 0xff, 0xff, 0xd7 ],[ 0xff, 0xff, 0xff ],[ 0x08, 0x08, 0x08 ],[ 0x12, 0x12, 0x12 ],[ 0x1c, 0x1c, 0x1c ], 143 | [ 0x26, 0x26, 0x26 ],[ 0x30, 0x30, 0x30 ],[ 0x3a, 0x3a, 0x3a ],[ 0x44, 0x44, 0x44 ],[ 0x4e, 0x4e, 0x4e ], 144 | [ 0x58, 0x58, 0x58 ],[ 0x60, 0x60, 0x60 ],[ 0x66, 0x66, 0x66 ],[ 0x76, 0x76, 0x76 ],[ 0x80, 0x80, 0x80 ], 145 | [ 0x8a, 0x8a, 0x8a ],[ 0x94, 0x94, 0x94 ],[ 0x9e, 0x9e, 0x9e ],[ 0xa8, 0xa8, 0xa8 ],[ 0xb2, 0xb2, 0xb2 ], 146 | [ 0xbc, 0xbc, 0xbc ],[ 0xc6, 0xc6, 0xc6 ],[ 0xd0, 0xd0, 0xd0 ],[ 0xda, 0xda, 0xda ],[ 0xe4, 0xe4, 0xe4 ], 147 | [ 0xee, 0xee, 0xee ]]; 148 | -------------------------------------------------------------------------------- /src/words.rs: -------------------------------------------------------------------------------- 1 | use cjk::is_cjk_codepoint; 2 | 3 | pub struct Words> { 4 | source: S, 5 | position: usize, 6 | previous: usize, 7 | preserve_whitespace: bool, 8 | } 9 | 10 | impl> Words { 11 | pub fn new(source: S) -> Self { 12 | Self { 13 | source, 14 | previous: 0, 15 | position: 0, 16 | preserve_whitespace: false, 17 | } 18 | } 19 | 20 | pub fn preserving_whitespace(source: S) -> Self { 21 | Self { 22 | source, 23 | previous: 0, 24 | position: 0, 25 | preserve_whitespace: true, 26 | } 27 | } 28 | } 29 | 30 | impl> Words { 31 | pub fn undo(&mut self) { 32 | self.position = self.previous; 33 | } 34 | } 35 | 36 | // NOTE: this almost certainly does some extra processing... but for my sanity, 37 | // we accept that 38 | fn may_end_word_cjk(ch: char) -> bool { 39 | // simplified chinese 40 | !"$(£¥·'\"〈《「『【〔〖〝﹙﹛$(.[{£¥" 41 | .chars() 42 | .any(|c| c == ch) 43 | // traditional chinese 44 | && !"([{£¥'\"‵〈《「『〔〝︴﹙﹛({︵︷︹︻︽︿﹁﹃﹏" 45 | .chars() 46 | .any(|c| c == ch) 47 | // japanese 48 | && !"([{〔〈《「『【〘〖〝'\"⦅«" 49 | .chars() 50 | .any(|c| c == ch) 51 | // japanese inseparable 52 | && !"—...‥〳〴〵" 53 | .chars() 54 | .any(|c| c == ch) 55 | // korean 56 | && !"$([\\{£¥'\"々〇〉》」〔$([{⦆¥₩ #" 57 | .chars() 58 | .any(|c| c == ch) 59 | } 60 | 61 | fn may_start_word_cjk(ch: char) -> bool { 62 | // simplified chinese 63 | !"!%),.:;?]}¢°·'\"†‡›℃∶、。〃〆〕〗〞﹚﹜!"%'),.:;?!]}~" 64 | .chars() 65 | .any(|c| c == ch) 66 | // traditional chinese 67 | && !"!),.:;?]}¢·–— '\"• 、。〆〞〕〉》」︰︱︲︳﹐﹑﹒﹓﹔﹕﹖﹘﹚﹜!),.:;?︶︸︺︼︾﹀﹂﹗]|}、" 68 | .chars() 69 | .any(|c| c == ch) 70 | // japenese 71 | && !")]}〕〉》」』】〙〗〟'\"⦆»" 72 | .chars() 73 | .any(|c| c == ch) 74 | && !"ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻" 75 | .chars() 76 | .any(|c| c == ch) 77 | && !"‐゠–〜? ! ‼ ⁇ ⁈ ⁉・、:;,。." 78 | .chars() 79 | .any(|c| c == ch) 80 | // japanese inseparable 81 | && !"—...‥〳〴〵" 82 | .chars() 83 | .any(|c| c == ch) 84 | // korean 85 | && !"!%),.:;?]}¢°'\"†‡℃〆〈《「『〕!%),.:;?]}" 86 | .chars() 87 | .any(|c| c == ch) 88 | } 89 | 90 | impl> Iterator for Words { 91 | type Item = String; 92 | 93 | fn next(&mut self) -> Option { 94 | self.previous = self.position; 95 | let chars: Vec = self.source.as_ref().chars().skip(self.position).collect(); 96 | let mut start = 0; 97 | while start < chars.len() && chars[start].is_whitespace() { 98 | start += 1; 99 | } 100 | self.position += start; 101 | if start == chars.len() { 102 | if chars.len() == 0 { 103 | return None; 104 | } else if self.preserve_whitespace { 105 | return Some(chars[..].into_iter().collect()); 106 | } else { 107 | return Some(" ".to_string()); 108 | } 109 | } 110 | let mut len = 0; 111 | while start + len < chars.len() { 112 | if chars[start + len] == '-' { 113 | len += 1; 114 | break; 115 | } 116 | if chars[start + len].is_whitespace() { 117 | break; 118 | } 119 | if len != 0 120 | // Before or after cjk characters, we can usually break line, unless it's one of the exceptions. 121 | // I got the exceptions off Wikipedia: 122 | // https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages 123 | && (is_cjk_codepoint(chars[start + len - 1]) || is_cjk_codepoint(chars[start + len])) 124 | && may_end_word_cjk(chars[start + len - 1]) 125 | && may_start_word_cjk(chars[start + len]) 126 | { 127 | break; 128 | } 129 | len += 1; 130 | } 131 | self.position += len; 132 | if chars[0].is_whitespace() { 133 | if self.preserve_whitespace { 134 | return Some(chars[0..start + len].into_iter().collect::()); 135 | } else { 136 | return Some( 137 | String::from(" ") + &chars[start..start + len].into_iter().collect::(), 138 | ); 139 | } 140 | } else { 141 | return Some(chars[start..start + len].into_iter().collect::()); 142 | } 143 | } 144 | } 145 | --------------------------------------------------------------------------------