From f298468dc896428062113d1141ababc6bd28b972 Mon Sep 17 00:00:00 2001 From: itsjunetime Date: Thu, 16 May 2024 18:23:11 -0600 Subject: [PATCH] Initial commit since things seem pretty solid --- .gitignore | 1 + .gitmodules | 6 + Cargo.lock | 1698 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 20 + ratatui | 1 + ratatui-image | 1 + src/main.rs | 133 ++++ src/renderer.rs | 246 +++++++ src/skip.rs | 21 + src/tui.rs | 403 +++++++++++ 10 files changed, 2530 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 160000 ratatui create mode 160000 ratatui-image create mode 100644 src/main.rs create mode 100644 src/renderer.rs create mode 100644 src/skip.rs create mode 100644 src/tui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d8512a1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "ratatui"] + path = ratatui + url = https://github.com/ratatui-org/ratatui +[submodule "ratatui-image"] + path = ratatui-image + url = https://github.com/benjajaja/ratatui-image diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0dcac79 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1698 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cairo-rs" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ac2a4d0e69036cf0062976f6efcba1aaee3e448594e6514bb2ddf87acce562" +dependencies = [ + "bitflags 2.5.0", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3bb3119664efbd78b5e6c93957447944f16bdbced84c17a9f41c7829b81e64" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "clap_mangen" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +dependencies = [ + "clap", + "roff", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "futures-core", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gio" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be548be810e45dd31d3bbb89c6210980bb7af9bca3ea1292b5f16b75f8e394a7" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4bdbef451b0f0361e7f762987cc6bebd5facab1d535e85a3cf1115dfb08db40" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.52.0", +] + +[[package]] +name = "glib" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0116c428e4841cab183a32a71b900fd6712194c20f9c424f01d2c016c96bd23" +dependencies = [ + "bitflags 2.5.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed782fa3e949c31146671da6e7a227a5e7d354660df1db6d0aac4974dc82a3c" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767d23ead9bbdfcbb1c2242c155c8128a7d13dde7bf69c176f809546135e2282" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3787b0bfacca12bb25f8f822b0dbee9f7e4a86e6469a29976d332d2c14c945b" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icy_sixel" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86858ae800284d596cfdefcb0ad435c3493c12f35367431bbe9b2b3858c1155b" + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-traits", + "png", + "rayon", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", + "rayon", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "libdeflate-sys" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669ea17f9257bcb48c09c7ee4bef3957777504acffac557263e20c11001977bc" +dependencies = [ + "cc", +] + +[[package]] +name = "libdeflater" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dfd6424f7010ee0a3416f1d796d0450e3ad3ac237a237644f728277c4ded016" +dependencies = [ + "libdeflate-sys", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oxipng" +version = "9.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f398c53eb34e0cf71d9e0bc676cfa7c611e3844dd14ab05e92fb7b423c98ecf" +dependencies = [ + "bitvec", + "clap", + "clap_mangen", + "crossbeam-channel", + "indexmap", + "libdeflater", + "log", + "rayon", + "rgb", + "rustc-hash", + "rustc_version", +] + +[[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.1", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "poppler-rs" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9421853a6cc8dfaea2e31bd751fb037abdc3a727f04d0eb10fcf7061f6eff562" +dependencies = [ + "cairo-rs", + "gio", + "glib", + "libc", + "poppler-sys-rs", +] + +[[package]] +name = "poppler-sys-rs" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f6737da38a7bb0126931c4a7b23b7bea517410bd48676f18af6b38c5f88d51" +dependencies = [ + "cairo-sys-rs", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro2" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ratatui" +version = "0.26.2" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-image" +version = "1.0.0" +dependencies = [ + "base64", + "dyn-clone", + "icy_sixel", + "image", + "rand", + "ratatui", + "rustix", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "tdf" +version = "0.1.0" +dependencies = [ + "cairo-rs", + "crossterm", + "futures-util", + "glib", + "image", + "itertools", + "notify", + "oxipng", + "poppler-rs", + "ratatui", + "ratatui-image", + "tokio", +] + +[[package]] +name = "thiserror" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.8", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1dd5044 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "tdf" +version = "0.1.0" +edition = "2021" + +[dependencies] +poppler-rs = { version = "0.23.0" } +cairo-rs = { version = "0.19.4", features = ["png"] } +# ratatui = "0.26.2" +ratatui = { path = "./ratatui" } +# ratatui-image = { version = "1.0.0", features = ["rustix"], default-features = false } +ratatui-image = { path = "./ratatui-image", features = ["rustix"], default-features = false } +crossterm = { version = "0.27.0", features = ["event-stream"] } +image = { version = "0.24.9", features = ["png", "rayon"], default-features = false } +notify = "6.1.1" +tokio = { version = "1.37.0", features = ["rt", "sync", "macros"] } +futures-util = { version = "0.3.30", default-features = false } +glib = "0.19.6" +itertools = "0.12.1" +oxipng = { version = "9.1.1", default-features = false, features = ["parallel"] } diff --git a/ratatui b/ratatui new file mode 160000 index 0000000..9bd89c2 --- /dev/null +++ b/ratatui @@ -0,0 +1 @@ +Subproject commit 9bd89c218afb1f3999dce1bfe6edea5b7442966d diff --git a/ratatui-image b/ratatui-image new file mode 160000 index 0000000..e7be613 --- /dev/null +++ b/ratatui-image @@ -0,0 +1 @@ +Subproject commit e7be6130b498d8dc408da7cff30ca37acb6ee262 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b907f3e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,133 @@ +use std::{path::PathBuf, str::FromStr}; + +use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}}; +use glib::{LogField, LogLevel, LogWriterOutput}; +use notify::{RecursiveMode, Watcher}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use ratatui_image::picker::Picker; +use tui::{InputAction, Tui}; +use futures_util::stream::StreamExt; +use renderer::{RenderInfo, RenderNotif}; + +mod tui; +mod renderer; +mod skip; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + let mut args = std::env::args().skip(1); + let file = args.next().expect("Program requires a file to process"); + let path = PathBuf::from_str(&file)?.canonicalize()?; + + let (watch_tx, render_rx) = tokio::sync::mpsc::channel(1); + let tui_tx = watch_tx.clone(); + + // we need to call this outside the recommended_watcher call because if we call it inside, that + // will be calling it from a thread not owned by the tokio runtime (since it's created by + // calling thread::spawn) and that will cause a panic + let mut watcher = notify::recommended_watcher(move |_| { + // This shouldn't fail to send unless the receiver gets disconnected. If that's happened, + // then like the main thread has panicked or something, so it doesn't matter if this panics + // as well + watch_tx.blocking_send(renderer::RenderNotif::Reload).unwrap(); + })?; + + // We're making this nonrecursive 'cause we're just watching a single file, so there's nothing + // to recurse into + watcher.watch(&path, RecursiveMode::NonRecursive)?; + + let file_path = format!("file://{}", path.clone().into_os_string().to_string_lossy()); + let (render_tx, mut tui_rx) = tokio::sync::mpsc::channel(1); + + // We need to create `picker` on this thread because if we create it on the `renderer` thread, + // it messes up something with user input. Input never makes it to the crossterm thing + let mut picker = Picker::from_termios()?; + picker.guess_protocol(); + + // then we want to spawn off the rendering task + // We need to use the thread::spawn API so that this exists in a thread not owned by tokio, + // since the methods we call in `start_rendering` will panic if called in an async context + std::thread::spawn(move || { renderer::start_rendering(file_path, render_tx, render_rx) }); + + let mut ev_stream = crossterm::event::EventStream::new(); + + let file_name = path.file_name() + .map(|n| n.to_string_lossy()) + .unwrap_or_else(|| "Unknown file".into()) + .to_string(); + let mut tui = tui::Tui::new(file_name, picker); + + let backend = CrosstermBackend::new(std::io::stdout()); + let mut term = Terminal::new(backend)?; + + // poppler has some annoying logging (e.g. if you request a page index out-of-bounds of a + // document's pages, then it will return `None`, but still log to stderr with CRITICAL level), + // so we want to just ignore all logging since this is a tui app. + glib::log_set_writer_func(noop); + + execute!( + term.backend_mut(), + EnterAlternateScreen, + crossterm::cursor::Hide + )?; + enable_raw_mode()?; + + let mut main_area = tui::Tui::main_layout(&term.get_frame()); + tui_tx.send(RenderNotif::Area(main_area[1])).await?; + + loop { + let mut needs_redraw; + + tokio::select! { + // First we check if we have any keystrokes + Some(ev) = ev_stream.next() => { + // If we can't get user input, just crash. + let ev = ev.expect("Couldn't get any user input"); + + needs_redraw = match tui.handle_event(ev) { + None => false, + Some(InputAction::Redraw) => true, + Some(InputAction::QuitApp) => break, + Some(InputAction::JumpingToPage(usize)) => { + tui_tx.send(RenderNotif::JumpToPage(usize)).await?; + true + } + }; + }, + Some(renderer_msg) = tui_rx.recv() => { + match renderer_msg { + Ok(RenderInfo::NumPages(num)) => tui.set_n_pages(num), + Ok(RenderInfo::Page(img, page_num)) => tui.page_ready(img, page_num), + Err(e) => tui.show_error(e) + } + needs_redraw = true; + } + } + + let new_area = Tui::main_layout(&term.get_frame()); + if new_area != main_area { + main_area = new_area; + tui_tx.send(RenderNotif::Area(main_area[1])).await?; + needs_redraw = true; + } + + if needs_redraw { + term.draw(|f| { + tui.render(f, &main_area); + })?; + } + } + + execute!( + term.backend_mut(), + LeaveAlternateScreen, + crossterm::cursor::Show + )?; + disable_raw_mode()?; + + Ok(()) +} + +fn noop(_: LogLevel, _: &[LogField<'_>]) -> LogWriterOutput { + LogWriterOutput::Handled +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..0db71e8 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,246 @@ +use cairo::{Antialias, Format}; +use image::{DynamicImage, ImageFormat}; +use itertools::Itertools; +use oxipng::Options; +use poppler::{Document, Page}; +use ratatui::layout::Rect; +use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}; + +pub enum RenderNotif { + Area(Rect), + JumpToPage(usize), + Reload +} + +#[derive(Debug)] +pub enum RenderError { + Doc(glib::Error), + // Don't like storing an error as a string but it needs to be Send to send to the main thread, + // and it's just going to be shown to the user, so whatever + Render(String) +} + +pub enum RenderInfo { + NumPages(usize), + Page(DynamicImage, usize) +} + +// this function has to be sync (non-async) because the poppler::Document needs to be held during +// most of it, but that's basically just a wrapper around `*c_void` cause it's just a binding to C +// code, so it's !Send and thus can't be held across await points. So we can't call any of the +// async `send` or `recv` methods in this function body, since those create await points. Which +// means we need to call blocking_(send|recv). Those functions panic if called in an async context. +// So here we are. +// Also we just kinda 'unwrap' all of the send/recv calls here 'cause if they return an error, that +// means the other side's disconnected, which means that the main thread has panicked, which means +// we're done. +pub fn start_rendering( + path: String, + sender: Sender>, + mut receiver: Receiver +) { + // first, wait 'til we get told what the current starting area is so that we can set it to + // know what to render to + let mut area; + loop { + if let RenderNotif::Area(r) = receiver.blocking_recv().unwrap() { + area = r; + break; + } + }; + + 'reload: loop { + let doc = match Document::from_file(&path, None) { + Err(e) => { + sender.blocking_send(Err(RenderError::Doc(e))).unwrap(); + return; + }, + Ok(d) => d + }; + + let n_pages = doc.n_pages() as usize; + sender.blocking_send(Ok(RenderInfo::NumPages(n_pages))).unwrap(); + + // We're using this vec of bools to indicate which page numbers have already been rendered, + // to support people jumping to specific pages and having quick rendering results. We + // `split_at_mut` at 0 initially (which bascially makes `right == rendered && left == []`), + // doing basically nothing, but if we get a notification that something has been jumped to, + // then we can split at that page and render at both sides of it + let mut rendered = vec![false; n_pages]; + let mut start_point = 0; + + // This is kinda a weird way of doing this, but if we get a notification that the area + // changed, we want to start re-rending all of the pages, but we don't want to reload the + // document. If there was a mechanism to say 'start this for-loop over' then I would do + // that, but I don't think such a thing exists, so this is our attempt + 'render_pages: loop { + // what we do with a notif is the same regardless of if we're in the middle of + // rendering the list of pages or we're all done + macro_rules! handle_notif { + ($notif:ident) => { + match $notif { + RenderNotif::Reload => continue 'reload, + RenderNotif::Area(new_area) => { + let bigger = new_area.width > area.width || new_area.height > area.height; + area = new_area; + // we only want to re-render pages if the new area is greater than the old + // one, 'cause then we might need sharper images to make it all look good. + // If the new area is smaller, then the same high-quality-rendered images + // will still look fine, so it's ok to leave it. + if bigger { + rendered = vec![false; n_pages]; + continue 'render_pages; + } + }, + RenderNotif::JumpToPage(page) => { + start_point = page; + continue 'render_pages; + } + } + } + } + + let (left, right) = rendered.split_at_mut(start_point); + + let page_iter = right.iter_mut() + .enumerate() + .map(|(idx, p)| (idx + start_point, p)) + .interleave( + left.iter_mut() + .enumerate() + .map(|(idx, p)| (idx - (start_point + 1), p)) + ); + + for (num, rendered) in page_iter { + if *rendered { + continue; + } + + // check if we've been told to change the area that we're rendering to, + // or if we're told to rerender + match receiver.try_recv() { + Err(TryRecvError::Disconnected) => panic!("disconnected :("), + Ok(notif) => handle_notif!(notif), + Err(TryRecvError::Empty) => () + }; + + // We know this is in range 'cause we're iterating over it + let page = doc.page(num as i32).unwrap(); + + // render the page + let to_send = render_single_page(page, area) + .and_then(|img_data| match image::load_from_memory_with_format(&img_data, ImageFormat::Png) { + Ok(img) => { + // TODO find some way to do oxipng stuff maybe. Perchance throw them + // all onto a new thread or whatever. idk. + /*let sender_clone = sender.clone(); + std::thread::spawn(move || { + let optimized = oxipng::optimize_from_memory( + &img_data, + &Options::default() + ).unwrap(); + let img = image::load_from_memory_with_format(&optimized, ImageFormat::Png).unwrap(); + sender_clone.blocking_send(Ok(RenderInfo::Page(img, num))).unwrap(); + });*/ + println!("data is {} while img is {}", img_data.len(), img.as_rgb8().unwrap().as_raw().len()); + Ok(img) + }, + Err(e) => Err(format!("Couldn't create DynamicImage: {e}")) + }).map(|img| RenderInfo::Page(img, num)) + .map_err(RenderError::Render); + + // then send it over + sender.blocking_send(to_send).unwrap(); + + *rendered = true; + }; + // Then once we've rendered all these pages, wait until we get another notification + // that this doc needs to be reloaded + loop { + // This once returned None despite the main thing being still connected (I think, at + // last), so I'm just being safe here + let Some(msg) = receiver.blocking_recv() else { + return + }; + handle_notif!(msg); + } + } + } +} + +fn render_single_page( + page: Page, + area: Rect, +//) -> Result { +) -> Result, String> { + // First, get the font size; the number of pixels (width x height) per font character (I + // think; it's at least something like that) on this terminal screen. + let size = crossterm::terminal::window_size() + .map_err(|e| format!("Couldn't get window size: {e}"))?; + let col_h = size.height / size.rows; + let col_w = size.width / size.columns; + + // then, get the size of the page + let (p_width, p_height) = page.size(); + + // and get its aspect ratio + let p_aspect_ratio = p_width / p_height; + + // Then we get the full pixel dimensions of the area provided to us, and the aspect ratio + // of that area + let area_full_h = (area.height * col_h) as f64; + let area_full_w = (area.width * col_w) as f64; + let area_aspect_ratio = area_full_w / area_full_h; + + // and get the ratio that this page would have to be scaled by to fit perfectly within the + // area provided to us. + // we do this first by comparing the aspec ratio of the page with the aspect ratio of the + // area to fit it within. If the aspect ratio of the page is larger, then we need to scale + // the width of the page to fill perfectly within the height of the area. Otherwise, we + // scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly + // is scaled by the same factor as the dimension that _is_ scaled perfectly. + let scale_factor = if p_aspect_ratio > area_aspect_ratio { + area_full_w as f64 / p_width + } else { + area_full_h as f64 / p_height + }; + + let surface_width = p_width * scale_factor; + let surface_height = p_height * scale_factor; + + let surface = cairo::ImageSurface::create( + Format::ARgb32, + // No matter how big you make these arguments, the image will be drawn at the same + // size. So if you make them really big, the image will be drawn on a quarter of it. If + // you make them really small, the image will cover more than all of the surface. + // + // However, that only stands as long as you don't scale the context that you place this + // surface into. If you scale the dimensions of this image by n, then scale the context + // by that same amount, then it'll still fit perfectly into the context, but be + // rendered at higher quality. + surface_width as i32, + surface_height as i32 + ).map_err(|e| format!("Couldn't create ImageSurface: {e}"))?; + let ctx = cairo::Context::new(surface) + .map_err(|e| format!("Couldn't create Context: {e}"))?; + + ctx.scale(scale_factor, scale_factor); + + // The default background color of PDFs (at least, I think) is white, so we need to set + // that as the background color, then paint, then render. + ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0); + ctx.set_antialias(Antialias::Best); + ctx.paint().map_err(|e| format!("Couldn't paint Context: {e}"))?; + page.render_for_printing(&ctx); + ctx.scale(1. / scale_factor, 1. / scale_factor); + + let mut img_data = Vec::new(); + ctx.target().write_to_png(&mut img_data) + .map_err(|e| format!("Couldn't write surface to png: {e}"))?; + + /*let img = image::load_from_memory_with_format(&img_data, ImageFormat::Png) + .map_err(|e| format!("Couldn't load image from provided data: {e}"))?; + + Ok(img)*/ + Ok(img_data) +} diff --git a/src/skip.rs b/src/skip.rs new file mode 100644 index 0000000..8f99784 --- /dev/null +++ b/src/skip.rs @@ -0,0 +1,21 @@ +use ratatui::widgets::Widget; + +pub struct Skip { + skip: bool +} + +impl Skip { + pub fn new(skip: bool) -> Self { + Self { skip } + } +} + +impl Widget for Skip { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { + for x in area.x..(area.x + area.width) { + for y in area.y..(area.y + area.height) { + buf.get_mut(x, y).skip = self.skip; + } + } + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..35500f3 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,403 @@ +use std::rc::Rc; + +use crossterm::event::{Event, KeyCode, MouseEventKind}; +use image::DynamicImage; +use ratatui::{layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders, Padding}, Frame}; +use ratatui_image::{picker::Picker, protocol::Protocol, Image, Resize}; + +use crate::{renderer::RenderError, skip::Skip}; + +pub struct Tui { + name: String, + page: usize, + error: Option, + input_state: Option, + // Used as a way to track if we need to draw the images, to save ratatui from doing a lot of + // diffing work + last_render: LastRender, + // So we hold the `Picker` here and store the `RenderedImage` as a option between the + // `DynamicImage` and `Box` because the Protocol thing is much less + // space-effecient (since it needs to store like a large string instead of just bytes of data) + // so we want to store them as `DynamicImage`s until they're shown, at which point we render + // them to the `Box` and keep them like that + picker: Picker, + rendered: Vec>, +} + +#[derive(Default, Debug)] +struct LastRender { + rect: Rect, + pages_shown: usize, + unused_width: u16 +} + +enum RenderedImage { + Image(DynamicImage), + Text(Box) +} + +enum InputCommand { + GoToPage(usize) +} + +impl Tui { + pub fn new(name: String, picker: Picker) -> Tui { + Self { + name, + page: 0, + error: None, + input_state: None, + picker, + last_render: LastRender::default(), + rendered: vec![] + } + } + + pub fn main_layout(frame: &Frame<'_>) -> Rc<[Rect]> { + Layout::default() + .constraints([ + Constraint::Length(3), + Constraint::Fill(1), + Constraint::Length(3) + ]) + .horizontal_margin(4) + .vertical_margin(2) + .split(frame.size()) + } + + pub fn render(&mut self, frame: &mut Frame<'_>, main_area: &[Rect]) { + let top_block = Block::new() + .padding(Padding { + right: 2, + left: 2, + ..Padding::default() + }) + .borders(Borders::BOTTOM); + + let top_area = top_block.inner(main_area[0]); + + let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len()); + + let top_layout = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(page_nums_text.len() as u16) + ]).split(top_area); + + let title = Span::styled( + &self.name, + Style::new() + .fg(Color::Cyan) + ); + + let page_nums = Span::styled( + &page_nums_text, + Style::new() + .fg(Color::Cyan) + ); + + frame.render_widget(top_block, main_area[0]); + frame.render_widget(title, top_layout[0]); + frame.render_widget(page_nums, top_layout[1]); + + let bottom_block = Block::new() + .padding(Padding { + top: 1, + right: 2, + left: 2, + bottom: 0 + }) + .borders(Borders::TOP); + let bottom_area = bottom_block.inner(main_area[2]); + + frame.render_widget(bottom_block, main_area[2]); + + let rendered_str = format!( + "Rendered: {}%", + (self.rendered.iter().filter(|i| i.is_some()).count() * 100) / self.rendered.len() + ); + + let bottom_layout = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(rendered_str.len() as u16) + ]).split(bottom_area); + + let rendered_span = Span::styled( + &rendered_str, + Style::new() + .fg(Color::Cyan) + ); + frame.render_widget(rendered_span, bottom_layout[1]); + + if let Some(ref error_str) = self.error { + let span = Span::styled( + format!("Couldn't render a page: {error_str}"), + Style::new() + .fg(Color::Red) + ); + frame.render_widget(span, bottom_layout[0]); + } else if let Some(ref cmd) = self.input_state { + match cmd { + InputCommand::GoToPage(page) => { + let span = Span::styled( + format!("Go to: {page}"), + Style::new() + .fg(Color::Blue) + ); + frame.render_widget(span, bottom_layout[0]); + } + } + } + + let mut img_area = main_area[1]; + + let size = frame.size(); + if size == self.last_render.rect { + // If we haven't resized (and haven't used the Rect as a way to mark that we need to + // resize this time), then go through every element in the buffer where any Image would + // be written and set to skip it so that ratatui doesn't spend a lot of time diffing it + // each re-render + self.last_render.rect = size; + frame.render_widget(Skip::new(true), img_area); + } else { + // here we calculate how many pages can fit in the available area. + let mut test_area_w = img_area.width; + // go through our pages, starting at the first one we want to view + let page_widths = self.rendered[self.page..].iter() + // and get their indices (I know it's offset, we fix it down below when we actually + // render each page) + .enumerate() + // and only take as many as are ready to be rendered + .take_while(|(_, page)| page.is_some()) + // and map it to their width (in cells on the terminal, not pixels) + .flat_map(|(idx, page)| + page.as_ref().map(|img| ( + idx, + match img { + RenderedImage::Image(img) => (img.width() / self.picker.font_size.0 as u32) as u16, + RenderedImage::Text(img) => img.rect().width, + } + )) + ) + // and then take them as long as they won't overflow the available area. + .take_while(|(_, width)| { + match test_area_w.checked_sub(*width) { + Some(new_val) => { + test_area_w = new_val; + true + }, + None => false + } + }) + .collect::>(); + + if page_widths.is_empty() { + // If none are ready to render, just show the loading thing + Self::render_loading_in(frame, img_area) + } else { + let total_width = page_widths + .iter() + .map(|(_, w)| w) + .sum::(); + + self.last_render.pages_shown = page_widths.len(); + + let unused_width = img_area.width - total_width; + self.last_render.unused_width = unused_width; + img_area.x += unused_width / 2; + + for (page_idx, width) in page_widths { + // now, theoretically, when we call this, this page should *not* be None, but we do + // have to account for that possibility since we can't `borrow` the image from self + // when passing it in to `render_single_page` since that would be a mutable + // reference + an immutable reference (and also we need to potentially temporarily + // remove it from the array of rendered pages to replace it with a text-rendered + // image) + self.render_single_page(frame, page_idx + self.page, Rect { width, ..img_area }); + img_area.x += width; + } + // frame.bypass_diff = true; + + // we want to set this at the very end so it doesn't get set somewhere halfway through and + // then the whole diffing thing messes it up + self.last_render.rect = frame.size(); + } + } + } + + fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) { + match self.rendered[page_idx] { + Some(ref page_img) => { + let dyn_img = match page_img { + RenderedImage::Image(_) => { + // Couldn't think of a better way to do this. We need to `take` the + // image out so we can transform it into a RenderedImage::Text, but we + // don't want to `take` it out when it's already a `Text` or `None`... + // idk, maybe i'll think of something better later. + let Some(RenderedImage::Image(img)) = self.rendered[page_idx].take() else { + unreachable!(); + }; + + // We don't actually want to Crop this image, but we've already + // verified (with the ImageSurface stuff) that the image is the correct + // size for the area given, so to save ratatui the work of having to + // resize it, we tell them to crop it to fit. + let dyn_img = match self.picker.new_protocol(img, img_area, Resize::Crop) { + Ok(img) => img, + Err(e) => { + self.error = Some(format!("Couldn't convert DynamicImage to ratatui image: {e}")); + return; + } + }; + + self.rendered[page_idx] = Some(RenderedImage::Text(dyn_img)); + let Some(RenderedImage::Text(ref txt)) = self.rendered[page_idx] else { + unreachable!(); + }; + + txt + } + RenderedImage::Text(ref img) => img, + }; + + frame.render_widget(Image::new(&**dyn_img), img_area); + }, + None => Self::render_loading_in(frame, img_area) + }; + } + + fn render_loading_in(frame: &mut Frame<'_>, area: Rect) { + let loading_str = "Loading..."; + let inner_space = Layout::horizontal([ + Constraint::Length(loading_str.len() as u16), + ]).flex(Flex::Center) + .split(area); + + let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan)); + + frame.render_widget(loading_span, inner_space[0]); + } + + fn change_page(&mut self, change: PageChange, amt: ChangeAmount) -> Option { + let diff = match amt { + ChangeAmount::Single => 1, + ChangeAmount::WholeScreen => self.last_render.pages_shown + }; + + let old = self.page; + match change { + PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)), + PageChange::Prev => self.set_page(self.page.saturating_sub(diff)), + } + + (old != self.page).then_some(InputAction::Redraw) + } + + pub fn set_n_pages(&mut self, n_pages: usize) { + self.rendered = Vec::with_capacity(n_pages); + for _ in 0..n_pages { + self.rendered.push(None); + } + // mark that we need to re-render the images + } + + pub fn page_ready(&mut self, img: DynamicImage, page_num: usize) { + // If this new image woulda fit within the available space on the last render AND it's + // within the range where it might've been rendered with the last shown pages, then reset + // the last rect marker so that all images are forced to redraw on next render and this one + // is drawn with them + let img_w = (img.width() / self.picker.font_size.0 as u32) as u16; + if img_w <= self.last_render.unused_width { + let num_fit = self.last_render.unused_width / img_w; + if page_num >= self.page && (self.page + num_fit as usize) >= page_num { + self.last_render.rect = Rect::default(); + } + } + + // We always just set this here because we handle reloading in the `set_n_pages` function. + // If the document was reloaded, then It'll have the `set_n_pages` called to set the new + // number of pages, so the vec will already be cleared + self.rendered[page_num] = Some(RenderedImage::Image(img)); + + if page_num > 10 { panic!() } + } + + pub fn handle_event(&mut self, ev: Event) -> Option { + match ev { + Event::Key(key) => { + match key.code { + KeyCode::Right | KeyCode::Char('l') => self.change_page(PageChange::Next, ChangeAmount::Single), + KeyCode::Down | KeyCode::Char('j') => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), + KeyCode::Left | KeyCode::Char('h') => self.change_page(PageChange::Prev, ChangeAmount::Single), + KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), + KeyCode::Esc | KeyCode::Char('q') => Some(InputAction::QuitApp), + KeyCode::Char('g') => { + self.input_state = Some(InputCommand::GoToPage(0)); + Some(InputAction::Redraw) + }, + KeyCode::Char(c) => { + let Some(InputCommand::GoToPage(ref mut page)) = self.input_state else { + return None; + }; + + c.to_digit(10) + .map(|input_num| { + *page = (*page * 10) + input_num as usize; + InputAction::Redraw + }) + }, + KeyCode::Enter => self.input_state.take() + .and_then(|cmd| match cmd { + // Only forward the command if it's within range + InputCommand::GoToPage(page) => (page < self.rendered.len()).then(|| { + self.set_page(page); + InputAction::JumpingToPage(page) + }) + }), + _ => None, + } + }, + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::ScrollRight => self.change_page(PageChange::Next, ChangeAmount::Single), + MouseEventKind::ScrollDown => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), + MouseEventKind::ScrollLeft => self.change_page(PageChange::Prev, ChangeAmount::Single), + MouseEventKind::ScrollUp => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), + _ => None, + } + // One of these options is Event::Resize, and we don't care about that because + // we always check, regardless, if the available area for the images has + // changed. + _ => None, + } + } + + pub fn show_error(&mut self, err: RenderError) { + self.error = Some(match err { + RenderError::Doc(e) => format!("Couldn't open document: {e}"), + RenderError::Render(e) => format!("Couldn't render page: {e}") + }); + } + + fn set_page(&mut self, page: usize) { + if page != self.page { + // mark that we need to re-render the images + self.last_render.rect = Rect::default(); + self.page = page; + } + } +} + +pub enum InputAction { + Redraw, + JumpingToPage(usize), + QuitApp +} + +enum PageChange { + Prev, + Next +} + +enum ChangeAmount { + WholeScreen, + Single +}