├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── blocks.gif ├── blocks.png ├── iterm.gif ├── options.gif └── top_quality.png └── src ├── lib.rs ├── main.rs ├── options.rs ├── previewer ├── blocks.rs ├── iterm.rs ├── kitty.rs ├── mod.rs └── sixel.rs ├── result.rs ├── support.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build files 2 | debug/ 3 | target/ 4 | 5 | # Ignore backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # MSVC Windows builds of rustc generate these, which store debugging information 9 | *.pdb 10 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "ansi_colours" 13 | version = "1.2.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "7db9d9767fde724f83933a716ee182539788f293828244e9d999695ce0f7ba1e" 16 | 17 | [[package]] 18 | name = "autocfg" 19 | version = "1.1.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 22 | 23 | [[package]] 24 | name = "base64" 25 | version = "0.21.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" 28 | 29 | [[package]] 30 | name = "bit_field" 31 | version = "0.10.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "1.3.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 40 | 41 | [[package]] 42 | name = "bumpalo" 43 | version = "3.12.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 46 | 47 | [[package]] 48 | name = "bytemuck" 49 | version = "1.13.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" 52 | 53 | [[package]] 54 | name = "byteorder" 55 | version = "1.4.3" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 58 | 59 | [[package]] 60 | name = "cc" 61 | version = "1.0.79" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 70 | 71 | [[package]] 72 | name = "clap" 73 | version = "4.1.10" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "ce38afc168d8665cfc75c7b1dd9672e50716a137f433f070991619744a67342a" 76 | dependencies = [ 77 | "bitflags", 78 | "clap_derive", 79 | "clap_lex", 80 | "is-terminal", 81 | "once_cell", 82 | "strsim", 83 | "termcolor", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_derive" 88 | version = "4.1.9" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" 91 | dependencies = [ 92 | "heck", 93 | "proc-macro-error", 94 | "proc-macro2", 95 | "quote", 96 | "syn", 97 | ] 98 | 99 | [[package]] 100 | name = "clap_lex" 101 | version = "0.3.3" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" 104 | dependencies = [ 105 | "os_str_bytes", 106 | ] 107 | 108 | [[package]] 109 | name = "color_quant" 110 | version = "1.1.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 113 | 114 | [[package]] 115 | name = "console" 116 | version = "0.15.5" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 119 | dependencies = [ 120 | "encode_unicode", 121 | "lazy_static", 122 | "libc", 123 | "windows-sys 0.42.0", 124 | ] 125 | 126 | [[package]] 127 | name = "crc32fast" 128 | version = "1.3.2" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 131 | dependencies = [ 132 | "cfg-if", 133 | ] 134 | 135 | [[package]] 136 | name = "crossbeam-channel" 137 | version = "0.5.7" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" 140 | dependencies = [ 141 | "cfg-if", 142 | "crossbeam-utils", 143 | ] 144 | 145 | [[package]] 146 | name = "crossbeam-deque" 147 | version = "0.8.3" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 150 | dependencies = [ 151 | "cfg-if", 152 | "crossbeam-epoch", 153 | "crossbeam-utils", 154 | ] 155 | 156 | [[package]] 157 | name = "crossbeam-epoch" 158 | version = "0.9.14" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" 161 | dependencies = [ 162 | "autocfg", 163 | "cfg-if", 164 | "crossbeam-utils", 165 | "memoffset", 166 | "scopeguard", 167 | ] 168 | 169 | [[package]] 170 | name = "crossbeam-utils" 171 | version = "0.8.15" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" 174 | dependencies = [ 175 | "cfg-if", 176 | ] 177 | 178 | [[package]] 179 | name = "crunchy" 180 | version = "0.2.2" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 183 | 184 | [[package]] 185 | name = "ctrlc" 186 | version = "3.2.5" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" 189 | dependencies = [ 190 | "nix", 191 | "windows-sys 0.45.0", 192 | ] 193 | 194 | [[package]] 195 | name = "either" 196 | version = "1.8.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 199 | 200 | [[package]] 201 | name = "encode_unicode" 202 | version = "0.3.6" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 205 | 206 | [[package]] 207 | name = "errno" 208 | version = "0.2.8" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 211 | dependencies = [ 212 | "errno-dragonfly", 213 | "libc", 214 | "winapi", 215 | ] 216 | 217 | [[package]] 218 | name = "errno-dragonfly" 219 | version = "0.1.2" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 222 | dependencies = [ 223 | "cc", 224 | "libc", 225 | ] 226 | 227 | [[package]] 228 | name = "exr" 229 | version = "1.6.3" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" 232 | dependencies = [ 233 | "bit_field", 234 | "flume", 235 | "half", 236 | "lebe", 237 | "miniz_oxide", 238 | "rayon-core", 239 | "smallvec", 240 | "zune-inflate", 241 | ] 242 | 243 | [[package]] 244 | name = "fastrand" 245 | version = "1.9.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 248 | dependencies = [ 249 | "instant", 250 | ] 251 | 252 | [[package]] 253 | name = "flate2" 254 | version = "1.0.25" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 257 | dependencies = [ 258 | "crc32fast", 259 | "miniz_oxide", 260 | ] 261 | 262 | [[package]] 263 | name = "flume" 264 | version = "0.10.14" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" 267 | dependencies = [ 268 | "futures-core", 269 | "futures-sink", 270 | "nanorand", 271 | "pin-project", 272 | "spin", 273 | ] 274 | 275 | [[package]] 276 | name = "futures-core" 277 | version = "0.3.27" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" 280 | 281 | [[package]] 282 | name = "futures-sink" 283 | version = "0.3.27" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" 286 | 287 | [[package]] 288 | name = "getrandom" 289 | version = "0.2.8" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 292 | dependencies = [ 293 | "cfg-if", 294 | "js-sys", 295 | "libc", 296 | "wasi", 297 | "wasm-bindgen", 298 | ] 299 | 300 | [[package]] 301 | name = "gif" 302 | version = "0.11.4" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" 305 | dependencies = [ 306 | "color_quant", 307 | "weezl", 308 | ] 309 | 310 | [[package]] 311 | name = "glob" 312 | version = "0.3.1" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 315 | 316 | [[package]] 317 | name = "half" 318 | version = "2.2.1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" 321 | dependencies = [ 322 | "crunchy", 323 | ] 324 | 325 | [[package]] 326 | name = "heck" 327 | version = "0.4.1" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 330 | 331 | [[package]] 332 | name = "hermit-abi" 333 | version = "0.2.6" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 336 | dependencies = [ 337 | "libc", 338 | ] 339 | 340 | [[package]] 341 | name = "hermit-abi" 342 | version = "0.3.1" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 345 | 346 | [[package]] 347 | name = "image" 348 | version = "0.24.5" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" 351 | dependencies = [ 352 | "bytemuck", 353 | "byteorder", 354 | "color_quant", 355 | "exr", 356 | "gif", 357 | "jpeg-decoder", 358 | "num-rational", 359 | "num-traits", 360 | "png", 361 | "scoped_threadpool", 362 | "tiff", 363 | ] 364 | 365 | [[package]] 366 | name = "imagesize" 367 | version = "0.11.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf" 370 | 371 | [[package]] 372 | name = "instant" 373 | version = "0.1.12" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 376 | dependencies = [ 377 | "cfg-if", 378 | ] 379 | 380 | [[package]] 381 | name = "io-lifetimes" 382 | version = "1.0.7" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "76e86b86ae312accbf05ade23ce76b625e0e47a255712b7414037385a1c05380" 385 | dependencies = [ 386 | "hermit-abi 0.3.1", 387 | "libc", 388 | "windows-sys 0.45.0", 389 | ] 390 | 391 | [[package]] 392 | name = "is-terminal" 393 | version = "0.4.4" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" 396 | dependencies = [ 397 | "hermit-abi 0.3.1", 398 | "io-lifetimes", 399 | "rustix", 400 | "windows-sys 0.45.0", 401 | ] 402 | 403 | [[package]] 404 | name = "jpeg-decoder" 405 | version = "0.3.0" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" 408 | dependencies = [ 409 | "rayon", 410 | ] 411 | 412 | [[package]] 413 | name = "js-sys" 414 | version = "0.3.61" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 417 | dependencies = [ 418 | "wasm-bindgen", 419 | ] 420 | 421 | [[package]] 422 | name = "lazy_static" 423 | version = "1.4.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 426 | 427 | [[package]] 428 | name = "lebe" 429 | version = "0.5.2" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 432 | 433 | [[package]] 434 | name = "libc" 435 | version = "0.2.140" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 438 | 439 | [[package]] 440 | name = "linux-raw-sys" 441 | version = "0.1.4" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 444 | 445 | [[package]] 446 | name = "lock_api" 447 | version = "0.4.9" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 450 | dependencies = [ 451 | "autocfg", 452 | "scopeguard", 453 | ] 454 | 455 | [[package]] 456 | name = "log" 457 | version = "0.4.17" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 460 | dependencies = [ 461 | "cfg-if", 462 | ] 463 | 464 | [[package]] 465 | name = "make-cmd" 466 | version = "0.1.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" 469 | 470 | [[package]] 471 | name = "memoffset" 472 | version = "0.8.0" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" 475 | dependencies = [ 476 | "autocfg", 477 | ] 478 | 479 | [[package]] 480 | name = "miniz_oxide" 481 | version = "0.6.2" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 484 | dependencies = [ 485 | "adler", 486 | ] 487 | 488 | [[package]] 489 | name = "nanorand" 490 | version = "0.7.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 493 | dependencies = [ 494 | "getrandom", 495 | ] 496 | 497 | [[package]] 498 | name = "nix" 499 | version = "0.26.2" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" 502 | dependencies = [ 503 | "bitflags", 504 | "cfg-if", 505 | "libc", 506 | "static_assertions", 507 | ] 508 | 509 | [[package]] 510 | name = "num-integer" 511 | version = "0.1.45" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 514 | dependencies = [ 515 | "autocfg", 516 | "num-traits", 517 | ] 518 | 519 | [[package]] 520 | name = "num-rational" 521 | version = "0.4.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" 524 | dependencies = [ 525 | "autocfg", 526 | "num-integer", 527 | "num-traits", 528 | ] 529 | 530 | [[package]] 531 | name = "num-traits" 532 | version = "0.2.15" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 535 | dependencies = [ 536 | "autocfg", 537 | ] 538 | 539 | [[package]] 540 | name = "num_cpus" 541 | version = "1.15.0" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 544 | dependencies = [ 545 | "hermit-abi 0.2.6", 546 | "libc", 547 | ] 548 | 549 | [[package]] 550 | name = "once_cell" 551 | version = "1.17.1" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 554 | 555 | [[package]] 556 | name = "os_str_bytes" 557 | version = "6.4.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" 560 | 561 | [[package]] 562 | name = "pic" 563 | version = "0.1.4" 564 | dependencies = [ 565 | "ansi_colours", 566 | "base64", 567 | "clap", 568 | "console", 569 | "crossbeam-channel", 570 | "ctrlc", 571 | "image", 572 | "imagesize", 573 | "libc", 574 | "sixel-rs", 575 | "tempfile", 576 | "wild", 577 | ] 578 | 579 | [[package]] 580 | name = "pin-project" 581 | version = "1.0.12" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 584 | dependencies = [ 585 | "pin-project-internal", 586 | ] 587 | 588 | [[package]] 589 | name = "pin-project-internal" 590 | version = "1.0.12" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 593 | dependencies = [ 594 | "proc-macro2", 595 | "quote", 596 | "syn", 597 | ] 598 | 599 | [[package]] 600 | name = "png" 601 | version = "0.17.7" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" 604 | dependencies = [ 605 | "bitflags", 606 | "crc32fast", 607 | "flate2", 608 | "miniz_oxide", 609 | ] 610 | 611 | [[package]] 612 | name = "proc-macro-error" 613 | version = "1.0.4" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 616 | dependencies = [ 617 | "proc-macro-error-attr", 618 | "proc-macro2", 619 | "quote", 620 | "syn", 621 | "version_check", 622 | ] 623 | 624 | [[package]] 625 | name = "proc-macro-error-attr" 626 | version = "1.0.4" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 629 | dependencies = [ 630 | "proc-macro2", 631 | "quote", 632 | "version_check", 633 | ] 634 | 635 | [[package]] 636 | name = "proc-macro2" 637 | version = "1.0.52" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" 640 | dependencies = [ 641 | "unicode-ident", 642 | ] 643 | 644 | [[package]] 645 | name = "quote" 646 | version = "1.0.26" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 649 | dependencies = [ 650 | "proc-macro2", 651 | ] 652 | 653 | [[package]] 654 | name = "rayon" 655 | version = "1.7.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" 658 | dependencies = [ 659 | "either", 660 | "rayon-core", 661 | ] 662 | 663 | [[package]] 664 | name = "rayon-core" 665 | version = "1.11.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" 668 | dependencies = [ 669 | "crossbeam-channel", 670 | "crossbeam-deque", 671 | "crossbeam-utils", 672 | "num_cpus", 673 | ] 674 | 675 | [[package]] 676 | name = "redox_syscall" 677 | version = "0.2.16" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 680 | dependencies = [ 681 | "bitflags", 682 | ] 683 | 684 | [[package]] 685 | name = "rustix" 686 | version = "0.36.9" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" 689 | dependencies = [ 690 | "bitflags", 691 | "errno", 692 | "io-lifetimes", 693 | "libc", 694 | "linux-raw-sys", 695 | "windows-sys 0.45.0", 696 | ] 697 | 698 | [[package]] 699 | name = "scoped_threadpool" 700 | version = "0.1.9" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 703 | 704 | [[package]] 705 | name = "scopeguard" 706 | version = "1.1.0" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 709 | 710 | [[package]] 711 | name = "simd-adler32" 712 | version = "0.3.5" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" 715 | 716 | [[package]] 717 | name = "sixel-rs" 718 | version = "0.3.3" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "cfa95c014543113a192d906e5971d0c8d1e8b4cc1e61026539687a7016644ce5" 721 | dependencies = [ 722 | "sixel-sys", 723 | ] 724 | 725 | [[package]] 726 | name = "sixel-sys" 727 | version = "0.3.1" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "fb46e0cd5569bf910390844174a5a99d52dd40681fff92228d221d9f8bf87dea" 730 | dependencies = [ 731 | "make-cmd", 732 | ] 733 | 734 | [[package]] 735 | name = "smallvec" 736 | version = "1.10.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 739 | 740 | [[package]] 741 | name = "spin" 742 | version = "0.9.6" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" 745 | dependencies = [ 746 | "lock_api", 747 | ] 748 | 749 | [[package]] 750 | name = "static_assertions" 751 | version = "1.1.0" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 754 | 755 | [[package]] 756 | name = "strsim" 757 | version = "0.10.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 760 | 761 | [[package]] 762 | name = "syn" 763 | version = "1.0.109" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 766 | dependencies = [ 767 | "proc-macro2", 768 | "quote", 769 | "unicode-ident", 770 | ] 771 | 772 | [[package]] 773 | name = "tempfile" 774 | version = "3.4.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 777 | dependencies = [ 778 | "cfg-if", 779 | "fastrand", 780 | "redox_syscall", 781 | "rustix", 782 | "windows-sys 0.42.0", 783 | ] 784 | 785 | [[package]] 786 | name = "termcolor" 787 | version = "1.2.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 790 | dependencies = [ 791 | "winapi-util", 792 | ] 793 | 794 | [[package]] 795 | name = "tiff" 796 | version = "0.8.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" 799 | dependencies = [ 800 | "flate2", 801 | "jpeg-decoder", 802 | "weezl", 803 | ] 804 | 805 | [[package]] 806 | name = "unicode-ident" 807 | version = "1.0.8" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 810 | 811 | [[package]] 812 | name = "version_check" 813 | version = "0.9.4" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 816 | 817 | [[package]] 818 | name = "wasi" 819 | version = "0.11.0+wasi-snapshot-preview1" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 822 | 823 | [[package]] 824 | name = "wasm-bindgen" 825 | version = "0.2.84" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" 828 | dependencies = [ 829 | "cfg-if", 830 | "wasm-bindgen-macro", 831 | ] 832 | 833 | [[package]] 834 | name = "wasm-bindgen-backend" 835 | version = "0.2.84" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" 838 | dependencies = [ 839 | "bumpalo", 840 | "log", 841 | "once_cell", 842 | "proc-macro2", 843 | "quote", 844 | "syn", 845 | "wasm-bindgen-shared", 846 | ] 847 | 848 | [[package]] 849 | name = "wasm-bindgen-macro" 850 | version = "0.2.84" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" 853 | dependencies = [ 854 | "quote", 855 | "wasm-bindgen-macro-support", 856 | ] 857 | 858 | [[package]] 859 | name = "wasm-bindgen-macro-support" 860 | version = "0.2.84" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" 863 | dependencies = [ 864 | "proc-macro2", 865 | "quote", 866 | "syn", 867 | "wasm-bindgen-backend", 868 | "wasm-bindgen-shared", 869 | ] 870 | 871 | [[package]] 872 | name = "wasm-bindgen-shared" 873 | version = "0.2.84" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" 876 | 877 | [[package]] 878 | name = "weezl" 879 | version = "0.1.7" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" 882 | 883 | [[package]] 884 | name = "wild" 885 | version = "2.1.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "05b116685a6be0c52f5a103334cbff26db643826c7b3735fc0a3ba9871310a74" 888 | dependencies = [ 889 | "glob", 890 | ] 891 | 892 | [[package]] 893 | name = "winapi" 894 | version = "0.3.9" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 897 | dependencies = [ 898 | "winapi-i686-pc-windows-gnu", 899 | "winapi-x86_64-pc-windows-gnu", 900 | ] 901 | 902 | [[package]] 903 | name = "winapi-i686-pc-windows-gnu" 904 | version = "0.4.0" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 907 | 908 | [[package]] 909 | name = "winapi-util" 910 | version = "0.1.5" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 913 | dependencies = [ 914 | "winapi", 915 | ] 916 | 917 | [[package]] 918 | name = "winapi-x86_64-pc-windows-gnu" 919 | version = "0.4.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 922 | 923 | [[package]] 924 | name = "windows-sys" 925 | version = "0.42.0" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 928 | dependencies = [ 929 | "windows_aarch64_gnullvm", 930 | "windows_aarch64_msvc", 931 | "windows_i686_gnu", 932 | "windows_i686_msvc", 933 | "windows_x86_64_gnu", 934 | "windows_x86_64_gnullvm", 935 | "windows_x86_64_msvc", 936 | ] 937 | 938 | [[package]] 939 | name = "windows-sys" 940 | version = "0.45.0" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 943 | dependencies = [ 944 | "windows-targets", 945 | ] 946 | 947 | [[package]] 948 | name = "windows-targets" 949 | version = "0.42.2" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 952 | dependencies = [ 953 | "windows_aarch64_gnullvm", 954 | "windows_aarch64_msvc", 955 | "windows_i686_gnu", 956 | "windows_i686_msvc", 957 | "windows_x86_64_gnu", 958 | "windows_x86_64_gnullvm", 959 | "windows_x86_64_msvc", 960 | ] 961 | 962 | [[package]] 963 | name = "windows_aarch64_gnullvm" 964 | version = "0.42.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 967 | 968 | [[package]] 969 | name = "windows_aarch64_msvc" 970 | version = "0.42.2" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 973 | 974 | [[package]] 975 | name = "windows_i686_gnu" 976 | version = "0.42.2" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 979 | 980 | [[package]] 981 | name = "windows_i686_msvc" 982 | version = "0.42.2" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 985 | 986 | [[package]] 987 | name = "windows_x86_64_gnu" 988 | version = "0.42.2" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 991 | 992 | [[package]] 993 | name = "windows_x86_64_gnullvm" 994 | version = "0.42.2" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 997 | 998 | [[package]] 999 | name = "windows_x86_64_msvc" 1000 | version = "0.42.2" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1003 | 1004 | [[package]] 1005 | name = "zune-inflate" 1006 | version = "0.2.51" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" 1009 | dependencies = [ 1010 | "simd-adler32", 1011 | ] 1012 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pic" 3 | version = "0.1.4" 4 | edition = "2021" 5 | authors = ["Emanuel "] 6 | description = "Preview Image in CLI." 7 | repository = "https://github.com/emsquid/pic" 8 | readme = "README.md" 9 | license = "MIT" 10 | exclude = ["examples"] 11 | 12 | [lib] 13 | name = "pic" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | ansi_colours = { version = "1.2.1", default-features = false } 18 | base64 = "0.21.0" 19 | clap = { version = "4.1.1", features = ["derive"] } 20 | console = { version = "0.15.5", default-features = false } 21 | crossbeam-channel = "0.5.6" 22 | ctrlc = "3.2.4" 23 | image = "0.24.5" 24 | imagesize = "0.11.0" 25 | libc = "0.2.139" 26 | sixel-rs = "0.3.3" 27 | tempfile = "3.3.0" 28 | wild = "2.1.0" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 emanuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PIC 📷 2 | 3 | PIC (**P**review **I**mage in **C**LI ) is a lightweight Rust tool to preview images in your terminal! 4 |
5 | With support for various image protocols ([`Kitty`](https://sw.kovidgoyal.net/kitty/graphics-protocol/), [`Sixel`](https://saitoha.github.io/libsixel/), [`iTerm`](https://iterm2.com/documentation-images.html)) it works in several terminals, and can still use Unicode blocks in case your terminal isn't supported. 6 | PIC also provides a library for you to use in your own tools! 7 | 8 | ## Features 9 | 10 | - Choose your favourite protocols 11 | * Kitty graphics with multiple actions available (`load`/`clear`) 12 | * Sixel which works in a lot of terminals 13 | * iTerm which displays GIFs incredibly well 14 | * Unicode blocks with truecolor/ansi256 support otherwise 15 | - Customization 16 | * `--x` and `--y` options to choose where to display your image 17 | * `--cols` and `--rows` options to choose the size of your image (always tries preserving ratio) 18 | * `--upscale` option to preview image at full wanted size if needed 19 | * `--static` and `--loop` options to interact with GIFs 20 | * `--protocol` option to choose a protocol 21 | * `--load` `--display` and `--clear` options to interact with Kitty protocol 22 | 23 | ## Installation 24 | 25 | ### From source (Recommended) 26 | 27 | Prerequisites 28 | - [Git](https://git-scm.com/downloads) 29 | - [Rust toolchain](https://www.rust-lang.org/tools/install) 30 | 31 | Command line instructions 32 | ```bash 33 | # Clone the repository 34 | git clone https://github.com/emsquid/pic 35 | 36 | # Build and install 37 | cargo install --path pic 38 | 39 | # Use freely 40 | pic Images/YourFavouriteImage.png --cols 13 ... 41 | ``` 42 | 43 | ### From Cargo 44 | 45 | Prerequisites 46 | - [Rust toolchain](https://www.rust-lang.org/tools/install) 47 | 48 | Command line instructions 49 | ```bash 50 | # Build and install 51 | cargo install pic 52 | 53 | # Use freely again 54 | pic Images/YourFavouriteImage.png --cols 13 ... 55 | ``` 56 | 57 | ### As a library 58 | 59 | Prerequisites 60 | - [Rust toolchain](https://www.rust-lang.org/tools/install) 61 | 62 | Command line instructions 63 | ```bash 64 | # Add the dependency in your project directory 65 | cargo add pic 66 | ``` 67 | 68 | ## Examples 69 | 70 | Blocks & Top quality previewing 71 | 72 | ![demo](examples/blocks.png) 73 | ![demo](examples/top_quality.png) 74 | 75 | Wide choice of options 76 | 77 | ![options](examples/options.gif) 78 | 79 | Really nice GIFs in iTerm 80 | 81 | ![iterm](examples/iterm.gif) 82 | 83 | And also nice in Blocks 84 | 85 | ![gotcha](examples/blocks.gif) 86 | 87 | ## Command line usage 88 | 89 | ``` 90 | Preview Image in CLI. 91 | 92 | Usage: pic [OPTIONS] [PATH]... 93 | 94 | Arguments: 95 | [PATH]... Image(s) to preview 96 | 97 | Options: 98 | -p, --protocol Previewing protocol to use [possible values: kitty, sixel, iterm, blocks] 99 | -x, --x x position (0 is left) 100 | -y, --y y position (0 is top) 101 | -c, --cols Number of cols to fit the preview in 102 | -r, --rows Number of rows to fit the preview in 103 | --spacing Spacing between images if more than one file is provided 104 | -u, --upscale Upscale image if needed 105 | -n, --no-newline Don't print newline after preview 106 | -s, --static Only show first frame of GIFs 107 | -l, --loop Loop GIFs infinitely 108 | --load Load image with the given id (kitty only) 109 | --display Display image with the given id (kitty only) 110 | --clear Clear image with the given id (0 for all) (kitty only) 111 | -h, --help Print help 112 | -V, --version Print version 113 | ``` 114 | 115 | ## Library usage 116 | 117 | ```rust 118 | use pic 119 | 120 | fn main() { 121 | // Choose images to preview 122 | let path1 = std::path::PathBuf::from("Picture/MyFavImage.png"); 123 | let mut options = pic::options::Options::new(vec![path1]); 124 | 125 | // Set your options 126 | options.set_position(Some(10), None); 127 | options.set_size(Some(50), Some(50)); 128 | options.upscale(); 129 | 130 | // Preview 131 | if let Err(err) = pic::previewer::preview(&mut std::io::stdout(), &mut options) { 132 | eprintln!("{err}"); 133 | }; 134 | } 135 | ``` 136 | 137 | ## Notes 138 | 139 | - `Sixel` protocol may require [libsixel](https://github.com/saitoha/libsixel) to be installed 140 | - `iTerm` protocol always loop GIFs, except if `--static` is specified 141 | 142 | ## Progress 143 | 144 | Help would be greatly appreciated 145 | 146 | - Documentation 147 | * [ ] Write a greater README 148 | * [ ] Make releases/packages (publish on crates.io) 149 | - Protocols support 150 | * [ ] Preview GIFs with Kitty protocol 151 | * [x] Preview GIFs with Unicode blocks 152 | * [ ] Work on handling transparency/GIFs with Sixel protocol (GIFs work but don't render well) 153 | * [ ] Improve protocol support checking (need to test in various terminal) 154 | - Miscellaneous 155 | * [ ] Implement caching somehow 156 | * [ ] Show cooler error messages 157 | * [ ] Write tests (I guess I need to do that...) 158 | -------------------------------------------------------------------------------- /examples/blocks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emsquid/pic/dbea6f605aa367847ec0ae6b9aa4372a881b71d9/examples/blocks.gif -------------------------------------------------------------------------------- /examples/blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emsquid/pic/dbea6f605aa367847ec0ae6b9aa4372a881b71d9/examples/blocks.png -------------------------------------------------------------------------------- /examples/iterm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emsquid/pic/dbea6f605aa367847ec0ae6b9aa4372a881b71d9/examples/iterm.gif -------------------------------------------------------------------------------- /examples/options.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emsquid/pic/dbea6f605aa367847ec0ae6b9aa4372a881b71d9/examples/options.gif -------------------------------------------------------------------------------- /examples/top_quality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emsquid/pic/dbea6f605aa367847ec0ae6b9aa4372a881b71d9/examples/top_quality.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Options needed to preview an image 2 | pub mod options; 3 | /// Previewing functions 4 | pub mod previewer; 5 | /// Results from previewing 6 | pub mod result; 7 | /// Previewing protocol support checking 8 | pub mod support; 9 | /// A bunch of utils 10 | pub mod utils; 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | fn main() { 4 | let mut stdout = std::io::stdout(); 5 | let mut options = pic::options::Options::parse_from(wild::args()); 6 | 7 | if let Err(err) = pic::previewer::preview(&mut stdout, &mut options) { 8 | eprintln!("{err}"); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use crate::support::Protocol; 2 | use clap::{arg, command, Parser}; 3 | use std::path::PathBuf; 4 | 5 | /// Options for previewing an image in terminal 6 | #[derive(Parser)] 7 | #[command(author, version, about)] 8 | pub struct Options { 9 | /// Image(s) to preview 10 | #[arg(num_args(1..))] 11 | pub path: Vec, 12 | 13 | /// Previewing protocol to use 14 | #[arg(short, long)] 15 | pub protocol: Option, 16 | /// x position (0 is left) 17 | #[arg(short, long)] 18 | pub x: Option, 19 | /// y position (0 is top) 20 | #[arg(short, long)] 21 | pub y: Option, 22 | /// Number of cols to fit the preview in 23 | #[arg(short, long)] 24 | pub cols: Option, 25 | /// Number of rows to fit the preview in 26 | #[arg(short, long)] 27 | pub rows: Option, 28 | /// Spacing between images if more than one file is provided 29 | #[arg(long)] 30 | pub spacing: Option, 31 | /// Upscale image if needed 32 | #[arg(short, long)] 33 | pub upscale: bool, 34 | /// Don't print newline after preview 35 | #[arg(short, long)] 36 | pub no_newline: bool, 37 | /// Only show first frame of GIFs 38 | #[arg(short = 's', long = "static", conflicts_with("gif_loop"))] 39 | pub gif_static: bool, 40 | /// Loop GIFs infinitely 41 | #[arg(short = 'l', long = "loop")] 42 | pub gif_loop: bool, 43 | 44 | /// Load image with the given id (kitty only) 45 | #[arg(long, value_name = "ID")] 46 | pub load: Option, 47 | /// Display image with the given id (kitty only) 48 | #[arg(long, value_name = "ID")] 49 | pub display: Option, 50 | /// Clear image with the given id (0 for all) (kitty only) 51 | #[arg(long, value_name = "ID")] 52 | pub clear: Option, 53 | } 54 | 55 | impl Options { 56 | /// New options for images 57 | pub fn new(path: Vec) -> Self { 58 | Self { 59 | path, 60 | protocol: None, 61 | x: None, 62 | y: None, 63 | cols: None, 64 | rows: None, 65 | spacing: None, 66 | upscale: false, 67 | gif_static: false, 68 | gif_loop: false, 69 | no_newline: false, 70 | load: None, 71 | display: None, 72 | clear: None, 73 | } 74 | } 75 | 76 | /// Set position of images in the terminal 77 | pub fn set_position(&mut self, x: Option, y: Option) { 78 | self.x = x; 79 | self.y = y; 80 | } 81 | 82 | /// Set size of images in the terminal 83 | pub fn set_size(&mut self, cols: Option, rows: Option) { 84 | self.cols = cols; 85 | self.rows = rows; 86 | } 87 | 88 | /// Set spacing of images in the terminal 89 | pub fn set_spacing(&mut self, spacing: Option) { 90 | self.spacing = spacing; 91 | } 92 | 93 | /// Upscale images 94 | pub fn upscale(&mut self) { 95 | self.upscale = true; 96 | } 97 | 98 | /// No newline after image 99 | pub fn no_newline(&mut self) { 100 | self.no_newline = true; 101 | } 102 | 103 | /// Set GIFs to be static 104 | pub fn set_static(&mut self) { 105 | self.gif_static = true; 106 | self.gif_loop = false; 107 | } 108 | 109 | /// Set GIFs to loop 110 | pub fn set_loop(&mut self) { 111 | self.gif_static = false; 112 | self.gif_loop = true; 113 | } 114 | 115 | /// Set options for kitty 116 | pub fn set_kitty(&mut self, load: Option, display: Option, clear: Option) { 117 | if self.protocol == Some(Protocol::Kitty) { 118 | self.load = load; 119 | self.display = display; 120 | self.clear = clear; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/previewer/blocks.rs: -------------------------------------------------------------------------------- 1 | use crate::options::Options; 2 | use crate::result::Result; 3 | use crate::utils::{ 4 | ansi_color, fit_in_bounds, handle_spacing, hide_cursor, move_cursor, move_cursor_up, 5 | pixel_is_transparent, resize, show_cursor, CtrlcHandler, TermSize, 6 | }; 7 | use crossbeam_channel::select; 8 | use image::codecs::gif::GifDecoder; 9 | use image::{AnimationDecoder, DynamicImage, ImageFormat}; 10 | use std::fs::File; 11 | use std::io::{Read, Write}; 12 | use std::path::PathBuf; 13 | // use std::thread; 14 | use std::time::Duration; 15 | 16 | const ANSI_CLEAR: &str = "\x1b[m"; 17 | const TOP_BLOCK: &str = "\u{2580}"; 18 | const BOTTOM_BLOCK: &str = "\u{2584}"; 19 | 20 | fn write_color_block(stdout: &mut impl Write, block: &str, ansi_bg: &str, ansi_fg: &str) -> Result { 21 | stdout.write_all(format!("{ansi_bg}{ansi_fg}{block}{ANSI_CLEAR}").as_bytes())?; 22 | stdout.flush()?; 23 | Ok(()) 24 | } 25 | 26 | /// this function should only print a 'ready to display' frame 27 | fn display_frame(stdout: &mut impl Write, image: &DynamicImage, options: &Options) -> Result { 28 | let rgba = image.to_rgba8(); 29 | let term_size = TermSize::from_ioctl()?; 30 | 31 | move_cursor(stdout, options.x, options.y)?; 32 | let mut backgrounds = vec![[0; 4]; rgba.width() as usize]; 33 | for (r, row) in rgba.enumerate_rows() { 34 | let is_bg = r % 2 == 0; 35 | 36 | for (c, pixel) in row.enumerate() { 37 | let overflow_cols = (c as u32) + options.x.unwrap_or(0) >= term_size.cols; 38 | 39 | if !overflow_cols { 40 | if is_bg { 41 | backgrounds[c] = pixel.2 .0; 42 | } else { 43 | let rgb_fg = pixel.2 .0; 44 | let rgb_bg = backgrounds[c]; 45 | 46 | match (pixel_is_transparent(rgb_fg), pixel_is_transparent(rgb_bg)) { 47 | (true, true) => write_color_block(stdout, " ", "", "")?, 48 | (true, false) => { 49 | let ansi_fg = ansi_color(rgb_bg, false); 50 | write_color_block(stdout, TOP_BLOCK, "", &ansi_fg)?; 51 | } 52 | (false, true) => { 53 | let ansi_fg = ansi_color(rgb_fg, false); 54 | write_color_block(stdout, BOTTOM_BLOCK, "", &ansi_fg)?; 55 | } 56 | (false, false) => { 57 | let ansi_bg = ansi_color(rgb_bg, true); 58 | let ansi_fg = ansi_color(rgb_fg, false); 59 | write_color_block(stdout, BOTTOM_BLOCK, &ansi_bg, &ansi_fg)?; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | if is_bg { 67 | move_cursor(stdout, options.x, None)?; 68 | } else if r != (rgba.height() - 1) as u32 || !options.no_newline { 69 | stdout.write_all(b"\n")?; 70 | }; 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | fn display_image(stdout: &mut impl Write, buffer: &[u8], options: &Options) -> Result { 77 | let image = image::load_from_memory(buffer)?; 78 | let (width, height) = (image.width(), image.height()); 79 | let (cols, rows) = fit_in_bounds(width, height, options.cols, options.rows, options.upscale)?; 80 | 81 | display_frame(stdout, &resize(&image, cols, rows * 2), options) 82 | } 83 | 84 | fn display_gif(stdout: &mut impl Write, buffer: &[u8], options: &Options) -> Result { 85 | if options.gif_static { 86 | display_image(stdout, buffer, options) 87 | } else { 88 | let frames: Vec<(Duration, DynamicImage)> = GifDecoder::new(buffer)? 89 | .into_frames() 90 | .collect_frames()? 91 | .iter() 92 | .map(|frame| { 93 | let delay = Duration::from(frame.delay()); 94 | let image = &DynamicImage::ImageRgba8(frame.clone().into_buffer()); 95 | let (width, height) = (image.width(), image.height()); 96 | let (cols, rows) = 97 | fit_in_bounds(width, height, options.cols, options.rows, options.upscale) 98 | .unwrap_or_default(); 99 | 100 | (delay, resize(image, cols, rows * 2)) 101 | }) 102 | .collect(); 103 | 104 | // Prevents cursor flickering 105 | let handler = CtrlcHandler::new()?; 106 | hide_cursor(stdout)?; 107 | 108 | // We need to move cursor up, except on very first frame 109 | let mut first_frame = true; 110 | 111 | 'gif: loop { 112 | for (delay, frame) in &frames { 113 | select! { 114 | default(*delay) => { 115 | if first_frame { 116 | first_frame = false; 117 | } else { 118 | move_cursor_up(stdout, frame.height() / 2 - 1)?; 119 | } 120 | 121 | display_frame(stdout, frame, options)?; 122 | }, 123 | recv(handler.receiver) -> _ => { 124 | break 'gif; 125 | } 126 | } 127 | } 128 | 129 | if !options.gif_loop { 130 | break 'gif; 131 | } 132 | } 133 | 134 | show_cursor(stdout)?; 135 | handler.sender.send(true)?; 136 | Ok(()) 137 | } 138 | } 139 | 140 | pub fn preview(stdout: &mut impl Write, image_path: &PathBuf, options: &Options) -> Result { 141 | let mut image = File::open(image_path)?; 142 | let mut buffer = Vec::new(); 143 | image.read_to_end(&mut buffer)?; 144 | 145 | match image::guess_format(&buffer)? { 146 | ImageFormat::Gif => display_gif(stdout, &buffer, options)?, 147 | _ => display_image(stdout, &buffer, options)?, 148 | } 149 | 150 | handle_spacing(stdout, options.spacing)?; 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /src/previewer/iterm.rs: -------------------------------------------------------------------------------- 1 | use crate::options::Options; 2 | use crate::result::Result; 3 | use crate::utils::{convert_to_image_buffer, fit_in_bounds, handle_spacing, move_cursor}; 4 | use base64::{engine::general_purpose, Engine as _}; 5 | use image::ImageFormat; 6 | use std::fs::File; 7 | use std::io::{Read, Write}; 8 | use std::path::PathBuf; 9 | 10 | fn display(stdout: &mut impl Write, image_path: &PathBuf, options: &mut Options) -> Result { 11 | let mut image = File::open(image_path)?; 12 | let mut buffer = Vec::new(); 13 | image.read_to_end(&mut buffer)?; 14 | 15 | let image_size = imagesize::size(image_path)?; 16 | let (width, height) = (image_size.width as u32, image_size.height as u32); 17 | let (cols, rows) = fit_in_bounds(width, height, options.cols, options.rows, options.upscale)?; 18 | 19 | let data = match (image::guess_format(&buffer)?, options.gif_static) { 20 | (ImageFormat::Gif, true) => { 21 | let gif = image::load_from_memory(&buffer)?; 22 | general_purpose::STANDARD.encode(convert_to_image_buffer(&gif, width, height)?) 23 | } 24 | _ => general_purpose::STANDARD.encode(buffer), 25 | }; 26 | 27 | let command = format!("\x1b]1337;File=width={cols};height={rows};inline=1;:{data}\x07"); 28 | 29 | move_cursor(stdout, options.x, options.y)?; 30 | stdout.write_all(command.as_bytes())?; 31 | 32 | if !options.no_newline { 33 | stdout.write_all(b"\r")?; 34 | } 35 | 36 | stdout.flush()?; 37 | Ok(()) 38 | } 39 | 40 | pub fn preview(stdout: &mut impl Write, image_path: &PathBuf, options: &mut Options) -> Result { 41 | display(stdout, image_path, options)?; 42 | handle_spacing(stdout, options.spacing)?; 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /src/previewer/kitty.rs: -------------------------------------------------------------------------------- 1 | use crate::options::Options; 2 | use crate::result::Result; 3 | use crate::utils::{ 4 | create_temp_file, fit_in_bounds, handle_spacing, move_cursor, save_in_temp_file, 5 | }; 6 | use base64::{engine::general_purpose, Engine as _}; 7 | use image::io::Reader; 8 | use std::io::Write; 9 | use std::path::PathBuf; 10 | 11 | const KITTY_PREFIX: &str = "pic.tty-graphics-protocol."; 12 | const PROTOCOL_START: &str = "\x1b_G"; 13 | const PROTOCOL_END: &str = "\x1b\\"; 14 | 15 | fn send_graphics_command( 16 | stdout: &mut impl Write, 17 | command: &str, 18 | payload: Option<&str>, 19 | newline: bool, 20 | ) -> Result { 21 | let data = general_purpose::STANDARD.encode(payload.unwrap_or_default()); 22 | let command = format!("{PROTOCOL_START}{command};{data}{PROTOCOL_END}"); 23 | 24 | stdout.write_all(command.as_bytes())?; 25 | if newline { 26 | stdout.write_all(b"\n")?; 27 | } 28 | stdout.flush()?; 29 | Ok(()) 30 | } 31 | 32 | fn clear(stdout: &mut impl Write, id: u32, options: &Options) -> Result { 33 | if id == 0 { 34 | send_graphics_command(stdout, "a=d,d=a", None, !options.no_newline) 35 | } else { 36 | send_graphics_command( 37 | stdout, 38 | &format!("a=d,d=i,i={id}"), 39 | None, 40 | !options.no_newline, 41 | ) 42 | } 43 | } 44 | 45 | fn load(stdout: &mut impl Write, id: u32, image_path: &PathBuf, options: &Options) -> Result { 46 | let image = Reader::open(image_path)? 47 | .with_guessed_format()? 48 | .decode()? 49 | .to_rgba8(); 50 | let (width, height) = image.dimensions(); 51 | let (mut tempfile, pathbuf) = create_temp_file(KITTY_PREFIX)?; 52 | save_in_temp_file(image.as_raw(), &mut tempfile)?; 53 | 54 | let command = format!("a=t,t=t,f=32,s={width},v={height},i={id},q=2"); 55 | send_graphics_command(stdout, &command, pathbuf.to_str(), !options.no_newline) 56 | } 57 | 58 | fn display( 59 | stdout: &mut impl Write, 60 | id: Option, 61 | image_path: &PathBuf, 62 | options: &Options, 63 | ) -> Result { 64 | let (mut tempfile, pathbuf) = create_temp_file(KITTY_PREFIX)?; 65 | let (command, payload) = if let Some(id) = id { 66 | let image_size = imagesize::size(image_path)?; 67 | let (width, height) = (image_size.width as u32, image_size.height as u32); 68 | let (cols, rows) = 69 | fit_in_bounds(width, height, options.cols, options.rows, options.upscale)?; 70 | 71 | let command = format!("a=p,c={cols},r={rows},i={id},q=2"); 72 | (command, None) 73 | } else { 74 | let image = Reader::open(image_path)? 75 | .with_guessed_format()? 76 | .decode()? 77 | .to_rgba8(); 78 | let (width, height) = image.dimensions(); 79 | let (cols, rows) = 80 | fit_in_bounds(width, height, options.cols, options.rows, options.upscale)?; 81 | save_in_temp_file(image.as_raw(), &mut tempfile)?; 82 | 83 | let command = format!("a=T,t=t,I=13,f=32,s={width},v={height},c={cols},r={rows},q=2",); 84 | (command, pathbuf.to_str()) 85 | }; 86 | 87 | move_cursor(stdout, options.x, options.y)?; 88 | send_graphics_command(stdout, &command, payload, !options.no_newline)?; 89 | 90 | Ok(()) 91 | } 92 | 93 | pub fn preview(stdout: &mut impl Write, image_path: &PathBuf, options: &Options) -> Result { 94 | if let Some(id) = options.clear { 95 | clear(stdout, id, options)?; 96 | } 97 | 98 | match (options.load, options.display) { 99 | (Some(id_load), Some(id_display)) => { 100 | load(stdout, id_load, image_path, options)?; 101 | display(stdout, Some(id_display), image_path, options)?; 102 | } 103 | (Some(id), None) => load(stdout, id, image_path, options)?, 104 | (None, Some(id)) => display(stdout, Some(id), image_path, options)?, 105 | (None, None) => display(stdout, None, image_path, options)?, 106 | } 107 | handle_spacing(stdout, options.spacing)?; 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /src/previewer/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::options::Options; 2 | use crate::result::Result; 3 | use crate::support::Protocol; 4 | use std::io::Write; 5 | 6 | mod blocks; 7 | mod iterm; 8 | mod kitty; 9 | mod sixel; 10 | 11 | /// Preview an image to stdout with the given options 12 | pub fn preview(stdout: &mut impl Write, options: &mut Options) -> Result { 13 | let protocol = Protocol::choose(options); 14 | let image_paths = options.path.clone(); 15 | // If there is more than one path, render `-y` flag useless 16 | // TODO: Does not work if the only path is a directory 17 | if options.y.is_some() && image_paths.len() > 1 { 18 | options.y = None; 19 | // Notify about spacing flag 20 | } 21 | 22 | for image_path in &image_paths { 23 | if image_path.is_dir() { 24 | continue; 25 | } 26 | 27 | match protocol { 28 | Protocol::Kitty => kitty::preview(stdout, image_path, options)?, 29 | Protocol::Iterm => iterm::preview(stdout, image_path, options)?, 30 | Protocol::Sixel => sixel::preview(stdout, image_path, options)?, 31 | Protocol::Blocks => blocks::preview(stdout, image_path, options)?, 32 | } 33 | } 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /src/previewer/sixel.rs: -------------------------------------------------------------------------------- 1 | use crate::options::Options; 2 | use crate::result::Result; 3 | use crate::utils::{fit_in_bounds, handle_spacing, move_cursor, TermSize}; 4 | use sixel_rs::encoder::Encoder; 5 | use sixel_rs::optflags::{EncodePolicy, ResampleMethod, SizeSpecification::Pixel}; 6 | use std::io::Write; 7 | use std::path::PathBuf; 8 | 9 | pub fn display(stdout: &mut impl Write, image_path: &PathBuf, options: &Options) -> Result { 10 | let image_size = imagesize::size(image_path)?; 11 | let (width, height) = (image_size.width as u32, image_size.height as u32); 12 | let (cols, rows) = fit_in_bounds(width, height, options.cols, options.rows, options.upscale)?; 13 | 14 | let term_size = TermSize::from_ioctl()?; 15 | let (col_size, row_size) = match term_size.get_cell_size() { 16 | Some((0, 0)) | None => (15, 30), 17 | Some((c, r)) => (c, r), 18 | }; 19 | 20 | let encoder = Encoder::new()?; 21 | encoder.set_width(Pixel(u64::from(cols * col_size)))?; 22 | encoder.set_height(Pixel(u64::from(rows * row_size)))?; 23 | encoder.set_resampling(ResampleMethod::Nearest)?; 24 | encoder.set_encode_policy(EncodePolicy::Fast)?; 25 | if options.gif_static { 26 | encoder.use_static()?; 27 | }; 28 | 29 | move_cursor(stdout, options.x, options.y)?; 30 | encoder.encode_file(image_path)?; 31 | stdout.flush()?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn preview(stdout: &mut impl Write, image_path: &PathBuf, options: &Options) -> Result { 37 | display(stdout, image_path, options)?; 38 | handle_spacing(stdout, options.spacing)?; 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/result.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | /// Input/Output error 4 | Io(std::io::Error), 5 | /// Image error 6 | Image(image::error::ImageError), 7 | /// Libsixel error 8 | Sixel(sixel_rs::status::Error), 9 | /// ImageSize error 10 | ImageSize(imagesize::ImageError), 11 | /// Tempfile error 12 | Tempfile(tempfile::PersistError), 13 | /// Threading error 14 | Channel(crossbeam_channel::SendError), 15 | /// CTRL-C error 16 | Ctrlc(ctrlc::Error), 17 | } 18 | 19 | impl std::fmt::Display for Error { 20 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 21 | match self { 22 | Error::Io(err) => write!(f, "IO error: {err}"), 23 | Error::Image(err) => write!(f, "Image error: {err}"), 24 | Error::Sixel(err) => write!(f, "Sixel error: {err:#?}"), 25 | Error::ImageSize(err) => write!(f, "Image size error: {err}"), 26 | Error::Tempfile(err) => write!(f, "Tempfile error: {err}"), 27 | Error::Channel(err) => write!(f, "Channel error: {err}"), 28 | Error::Ctrlc(err) => write!(f, "CTRL-C error: {err}"), 29 | } 30 | } 31 | } 32 | 33 | impl std::error::Error for Error {} 34 | 35 | impl From for Error { 36 | fn from(err: std::io::Error) -> Self { 37 | Error::Io(err) 38 | } 39 | } 40 | impl From for Error { 41 | fn from(err: image::ImageError) -> Self { 42 | Error::Image(err) 43 | } 44 | } 45 | 46 | impl From for Error { 47 | fn from(err: sixel_rs::status::Error) -> Self { 48 | Error::Sixel(err) 49 | } 50 | } 51 | 52 | impl From for Error { 53 | fn from(err: imagesize::ImageError) -> Self { 54 | Error::ImageSize(err) 55 | } 56 | } 57 | 58 | impl From for Error { 59 | fn from(err: tempfile::PersistError) -> Self { 60 | Error::Tempfile(err) 61 | } 62 | } 63 | 64 | impl From> for Error { 65 | fn from(err: crossbeam_channel::SendError) -> Self { 66 | Error::Channel(err) 67 | } 68 | } 69 | 70 | impl From for Error { 71 | fn from(err: ctrlc::Error) -> Self { 72 | Error::Ctrlc(err) 73 | } 74 | } 75 | 76 | pub type Result = std::result::Result; 77 | -------------------------------------------------------------------------------- /src/support.rs: -------------------------------------------------------------------------------- 1 | use crate::{options::Options, result::Result}; 2 | use clap::ValueEnum; 3 | use console::{Key, Term}; 4 | use std::{env, io::Write}; 5 | 6 | // add supported Terminals based on their eventual environment variables 7 | const KITTY_SUPPORTED: [&str; 2] = ["xterm-kitty", "WezTerm"]; 8 | 9 | const SIXEL_SUPPORTED: [&str; 7] = [ 10 | "xterm-256color", 11 | "xterm", 12 | "yaft-256color", 13 | "st-256color", 14 | "foot-extra", 15 | "foot", 16 | "mlterm", 17 | ]; 18 | 19 | const ITERM_SUPPORTED: [&str; 3] = ["iTerm", "WezTerm", "mintty"]; 20 | 21 | /// Supported previewing protocols 22 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 23 | pub enum Protocol { 24 | Kitty, 25 | Sixel, 26 | Iterm, 27 | Blocks, 28 | } 29 | 30 | impl Protocol { 31 | /// Choose the best protocol for previewing 32 | pub fn choose(options: &Options) -> Self { 33 | if let Some(protocol) = options.protocol { 34 | protocol 35 | } else if Protocol::support_iterm() { 36 | Protocol::Iterm 37 | } else if Protocol::support_kitty() { 38 | Protocol::Kitty 39 | } else if Protocol::support_sixel() { 40 | Protocol::Sixel 41 | } else { 42 | Protocol::Blocks 43 | } 44 | } 45 | 46 | fn support_kitty() -> bool { 47 | // Term check 48 | let term = env::var("TERM").unwrap_or_default(); 49 | let program = env::var("TERM_PROGRAM").unwrap_or_default(); 50 | // Attrs check (send a kitty request) 51 | let attrs = vec![vec!["OK"]]; 52 | let kitty_command = b"\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\"; 53 | 54 | (find_match(&KITTY_SUPPORTED, &term) || find_match(&KITTY_SUPPORTED, &program)) 55 | && check_primary_attributes(&attrs, Some(kitty_command)).unwrap_or(false) 56 | } 57 | 58 | fn support_sixel() -> bool { 59 | // Term check 60 | let term = env::var("TERM").unwrap_or_default(); 61 | // Attrs check (4 is for sixel support) 62 | let attrs = vec![vec![";4;", ";4c"]]; 63 | 64 | find_match(&SIXEL_SUPPORTED, &term) 65 | && check_primary_attributes(&attrs, None).unwrap_or(false) 66 | } 67 | 68 | fn support_iterm() -> bool { 69 | // Term check 70 | let program = env::var("TERM_PROGRAM").unwrap_or_default(); 71 | let lc = env::var("LC_TERMINAL").unwrap_or_default(); 72 | 73 | find_match(&ITERM_SUPPORTED, &program) || find_match(&ITERM_SUPPORTED, &lc) 74 | } 75 | } 76 | 77 | impl std::fmt::Display for Protocol { 78 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 79 | match self { 80 | Protocol::Kitty => write!(f, "Kitty graphics protocol"), 81 | Protocol::Sixel => write!(f, "Sixel protocol"), 82 | Protocol::Iterm => write!(f, "iTerm protocol"), 83 | Protocol::Blocks => write!(f, "Unicode blocks"), 84 | } 85 | } 86 | } 87 | 88 | fn find_match(list: &[&str], var: &str) -> bool { 89 | list.iter().any(|s| var.contains(s)) 90 | } 91 | 92 | fn check_primary_attributes(attrs: &[Vec<&str>], subcommand: Option<&[u8]>) -> Result { 93 | let mut stdout = Term::stdout(); 94 | let command = [subcommand.unwrap_or_default(), b"\x1b[c"].concat(); 95 | stdout.write_all(&command)?; 96 | stdout.flush()?; 97 | 98 | let mut response = String::new(); 99 | // what if the terminal doesn't answer ? 100 | while !response.contains('c') { 101 | match stdout.read_key() { 102 | Ok(Key::Char(chr)) => response.push(chr), 103 | Ok(Key::UnknownEscSeq(esc)) => response.extend(esc), 104 | Err(_) => break, 105 | _ => (), 106 | } 107 | } 108 | 109 | // check if each groups of attrs has at least a match 110 | Ok(attrs.iter().all(|group| find_match(group, &response))) 111 | } 112 | 113 | /// Check if the terminal supports truecolor 114 | pub fn truecolor() -> bool { 115 | let colorterm = env::var("COLORTERM").unwrap_or_default(); 116 | matches!(colorterm.as_str(), "truecolor" | "24bit") 117 | } 118 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{result::Result, support}; 2 | use ansi_colours::ansi256_from_rgb; 3 | use crossbeam_channel::{unbounded, Receiver, Sender}; 4 | use image::{codecs::png::PngEncoder, DynamicImage, ImageEncoder}; 5 | use std::{ 6 | fs::File, 7 | io::{Error, Write}, 8 | path::PathBuf, 9 | }; 10 | 11 | pub(crate) struct CtrlcHandler { 12 | pub sender: Sender, 13 | pub receiver: Receiver, 14 | } 15 | 16 | impl CtrlcHandler { 17 | pub fn new() -> Result { 18 | // We use two channels so that they can communicate 19 | let (ctrlc_tx, preview_rx) = unbounded(); 20 | let (preview_tx, ctrlc_rx) = unbounded(); 21 | 22 | ctrlc::set_handler(move || { 23 | ctrlc_tx 24 | .send(true) 25 | .expect("CTRL-C error: Unable to send message"); 26 | 27 | ctrlc_rx 28 | .recv() 29 | .expect("CTRL-C error: No response from main thread"); 30 | std::process::exit(0); 31 | })?; 32 | 33 | Ok(Self { 34 | sender: preview_tx, 35 | receiver: preview_rx, 36 | }) 37 | } 38 | } 39 | 40 | /// Useful handle for terminal size 41 | #[derive(Clone, Default, Debug)] 42 | pub struct TermSize { 43 | /// the amount of visible rows in the pty 44 | pub(crate) rows: u32, 45 | /// the amount of visible columns in the pty 46 | pub(crate) cols: u32, 47 | /// the width of the view in pixels 48 | pub(crate) width: u32, 49 | /// the height of the view in pixels 50 | pub(crate) height: u32, 51 | } 52 | 53 | impl TermSize { 54 | pub fn new(rows: u16, cols: u16, width: u16, height: u16) -> Self { 55 | Self { 56 | rows: u32::from(rows), 57 | cols: u32::from(cols), 58 | width: u32::from(width), 59 | height: u32::from(height), 60 | } 61 | } 62 | 63 | /// Retrieve the size of a terminal cell 64 | pub fn get_cell_size(&self) -> Option<(u32, u32)> { 65 | if self.cols == 0 || self.rows == 0 { 66 | return None; 67 | } 68 | Some((self.width / self.cols, self.height / self.rows)) 69 | } 70 | 71 | /// Create TermSize by getting the terminal size with an IOCTL 72 | pub fn from_ioctl() -> Result { 73 | // TODO: find a way to make that safe 74 | unsafe { 75 | let mut ws = libc::winsize { 76 | ws_row: 0, 77 | ws_col: 0, 78 | ws_xpixel: 0, 79 | ws_ypixel: 0, 80 | }; 81 | let ret = libc::ioctl(0, libc::TIOCGWINSZ, &mut ws); 82 | if ret == 0 { 83 | Ok(TermSize::new( 84 | ws.ws_row, 85 | ws.ws_col, 86 | ws.ws_xpixel, 87 | ws.ws_ypixel, 88 | )) 89 | } else { 90 | Err(Error::last_os_error().into()) 91 | } 92 | } 93 | } 94 | } 95 | 96 | /// Create a temporary file with the given prefix 97 | pub fn create_temp_file(prefix: &str) -> Result<(File, PathBuf)> { 98 | let (tempfile, pathbuf) = tempfile::Builder::new() 99 | .prefix(prefix) 100 | .tempfile_in("/tmp/")? 101 | .keep()?; 102 | 103 | Ok((tempfile, pathbuf)) 104 | } 105 | 106 | /// Save buffer in a temporary file 107 | pub fn save_in_temp_file(buffer: &[u8], file: &mut File) -> Result { 108 | file.write_all(buffer)?; 109 | file.flush()?; 110 | Ok(()) 111 | } 112 | 113 | /// Save terminal cursor position 114 | #[allow(dead_code)] 115 | pub fn save_cursor(stdout: &mut impl Write) -> Result { 116 | stdout.write_all(b"\x1b[s")?; 117 | stdout.flush()?; 118 | Ok(()) 119 | } 120 | 121 | /// Restore terminal cursor position 122 | #[allow(dead_code)] 123 | pub fn restore_cursor(stdout: &mut impl Write) -> Result { 124 | stdout.write_all(b"\x1b[u")?; 125 | stdout.flush()?; 126 | Ok(()) 127 | } 128 | 129 | /// Move terminal cursor up 130 | #[allow(dead_code)] 131 | pub fn move_cursor_up(stdout: &mut impl Write, x: u32) -> Result { 132 | let binding = format!("\x1b[{}A", x + 1); 133 | stdout.write_all(binding.as_bytes())?; 134 | stdout.flush()?; 135 | Ok(()) 136 | } 137 | 138 | /// Move terminal cursor down 139 | #[allow(dead_code)] 140 | pub fn move_cursor_down(stdout: &mut impl Write, x: u32) -> Result { 141 | let binding = format!("\x1b[{}B", x + 1); 142 | stdout.write_all(binding.as_bytes())?; 143 | stdout.flush()?; 144 | Ok(()) 145 | } 146 | 147 | /// Move terminal cursor to the given column 148 | pub fn move_cursor_column(stdout: &mut impl Write, col: u32) -> Result { 149 | let binding = format!("\x1b[{}G", col + 1); 150 | stdout.write_all(binding.as_bytes())?; 151 | stdout.flush()?; 152 | Ok(()) 153 | } 154 | 155 | /// Move terminal cursor to the given row 156 | pub fn move_cursor_row(stdout: &mut impl Write, row: u32) -> Result { 157 | let binding = format!("\x1b[{}d", row + 1); 158 | stdout.write_all(binding.as_bytes())?; 159 | stdout.flush()?; 160 | Ok(()) 161 | } 162 | 163 | /// Move terminal cursor to the given position 164 | pub fn move_cursor_pos(stdout: &mut impl Write, col: u32, row: u32) -> Result { 165 | let binding = format!("\x1b[{};{}H", row + 1, col + 1); 166 | stdout.write_all(binding.as_bytes())?; 167 | stdout.flush()?; 168 | Ok(()) 169 | } 170 | 171 | /// Move terminal cursor eventually given x and y 172 | pub fn move_cursor(stdout: &mut impl Write, col: Option, row: Option) -> Result { 173 | match (col, row) { 174 | (None, None) => Ok(()), 175 | (Some(x), None) => move_cursor_column(stdout, x), 176 | (None, Some(y)) => move_cursor_row(stdout, y), 177 | (Some(x), Some(y)) => move_cursor_pos(stdout, x, y), 178 | } 179 | } 180 | 181 | /// Show terminal cursor 182 | pub fn show_cursor(stdout: &mut impl Write) -> Result { 183 | stdout.write_all(b"\x1b[?25h")?; 184 | stdout.flush()?; 185 | Ok(()) 186 | } 187 | 188 | /// Hide terminal cursor 189 | pub fn hide_cursor(stdout: &mut impl Write) -> Result { 190 | stdout.write_all(b"\x1b[?25l")?; 191 | stdout.flush()?; 192 | Ok(()) 193 | } 194 | 195 | /// Handle spacing between images 196 | pub fn handle_spacing(stdout: &mut impl Write, spacing: Option) -> Result { 197 | if let Some(spacing) = spacing { 198 | stdout.write_all(&b"\n".repeat(spacing as usize))?; 199 | stdout.flush()?; 200 | } 201 | Ok(()) 202 | } 203 | 204 | /// Fit an images into cols and rows bounds 205 | pub fn fit_in_bounds( 206 | width: u32, 207 | height: u32, 208 | cols: Option, 209 | rows: Option, 210 | upscale: bool, 211 | ) -> Result<(u32, u32)> { 212 | let term_size = TermSize::from_ioctl()?; 213 | let (col_size, row_size) = match term_size.get_cell_size() { 214 | Some((0, 0)) | None => (15, 30), 215 | Some((c, r)) => (c, r), 216 | }; 217 | let cols = cols.unwrap_or(term_size.cols); 218 | // Terminal prompt puts the image out of screen (rows - 1) 219 | let rows = rows.unwrap_or(term_size.rows - 1); 220 | 221 | let (bound_width, bound_height) = (cols * col_size, rows * row_size); 222 | 223 | if !upscale && width < bound_width && height < bound_height { 224 | return Ok((width / col_size, height / row_size)); 225 | } 226 | 227 | let w_ratio = width * bound_height; 228 | let h_ratio = bound_width * height; 229 | 230 | if w_ratio >= h_ratio { 231 | Ok(( 232 | cols, 233 | std::cmp::max((height * bound_width) / (width * row_size), 1), 234 | )) 235 | } else { 236 | Ok(( 237 | std::cmp::max((width * bound_height) / (height * col_size), 1), 238 | rows, 239 | )) 240 | } 241 | } 242 | 243 | /// Resize an image 244 | pub fn resize(image: &DynamicImage, width: u32, height: u32) -> DynamicImage { 245 | image.resize_exact(width, height, image::imageops::Triangle) 246 | } 247 | 248 | /// Convert an image to a png buffer 249 | /// image is mainly supposed to be a GIF here 250 | pub fn convert_to_image_buffer(image: &DynamicImage, width: u32, height: u32) -> Result> { 251 | let mut image_buffer = Vec::new(); 252 | PngEncoder::new(&mut image_buffer).write_image( 253 | image.as_bytes(), 254 | width, 255 | height, 256 | image.color(), 257 | )?; 258 | Ok(image_buffer) 259 | } 260 | 261 | /// Assess the transparency of a pixel 262 | pub fn pixel_is_transparent(rgb: [u8; 4]) -> bool { 263 | rgb[3] < 25 264 | } 265 | 266 | /// Convert rgb to ansi_rgb 267 | pub fn ansi_rgb(rgb: [u8; 4], bg: bool) -> String { 268 | if bg { 269 | format!("\x1b[48;2;{};{};{}m", rgb[0], rgb[1], rgb[2]) 270 | } else { 271 | format!("\x1b[38;2;{};{};{}m", rgb[0], rgb[1], rgb[2]) 272 | } 273 | } 274 | 275 | /// Convert rgb to ansi_indexed 276 | pub fn ansi_indexed(rgb: [u8; 4], bg: bool) -> String { 277 | let index = ansi256_from_rgb((rgb[0], rgb[1], rgb[2])); 278 | if bg { 279 | format!("\x1b[48;5;{index}m") 280 | } else { 281 | format!("\x1b[38;5;{index}m") 282 | } 283 | } 284 | 285 | /// Convert rgb to ansi 286 | pub fn ansi_color(rgb: [u8; 4], bg: bool) -> String { 287 | if support::truecolor() { 288 | ansi_rgb(rgb, bg) 289 | } else { 290 | ansi_indexed(rgb, bg) 291 | } 292 | } 293 | --------------------------------------------------------------------------------