From e4096c0f25fda209f0349ff177bb47de5c51118b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Sat, 26 Aug 2023 17:28:43 +0800 Subject: [PATCH] feat: make the preview scrollable (#86) --- Cargo.lock | 222 +++++++++---------------- adaptor/src/adaptor.rs | 2 +- app/src/app.rs | 23 ++- app/src/executor.rs | 5 + app/src/manager/folder.rs | 10 +- app/src/manager/preview.rs | 3 +- config/Cargo.toml | 1 - config/preset/keymap.toml | 3 + config/preset/theme.toml | 6 +- config/src/boot/boot.rs | 27 +-- core/Cargo.toml | 1 + core/src/event.rs | 16 +- core/src/external/ffmpegthumbnailer.rs | 12 +- core/src/external/jq.rs | 25 +-- core/src/external/lsar.rs | 44 +++-- core/src/external/pdftoppm.rs | 20 ++- core/src/manager/folder.rs | 10 ++ core/src/manager/manager.rs | 39 ++--- core/src/manager/preview.rs | 174 ------------------- core/src/manager/preview/mod.rs | 5 + core/src/manager/preview/preview.rs | 151 +++++++++++++++++ core/src/manager/preview/provider.rs | 138 +++++++++++++++ core/src/manager/tab.rs | 44 ++++- core/src/tasks/tasks.rs | 2 +- core/src/tasks/workers/precache.rs | 10 +- core/src/which/which.rs | 2 +- shared/Cargo.toml | 2 +- shared/src/errors/mod.rs | 3 + shared/src/errors/peek.rs | 34 ++++ shared/src/lib.rs | 2 + shared/src/mime.rs | 34 ++-- 31 files changed, 645 insertions(+), 425 deletions(-) delete mode 100644 core/src/manager/preview.rs create mode 100644 core/src/manager/preview/mod.rs create mode 100644 core/src/manager/preview/preview.rs create mode 100644 core/src/manager/preview/provider.rs create mode 100644 shared/src/errors/mod.rs create mode 100644 shared/src/errors/peek.rs diff --git a/Cargo.lock b/Cargo.lock index a4927088..a8a371af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -69,24 +69,23 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -103,17 +102,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "1.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -163,9 +162,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -250,9 +249,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -277,9 +276,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.22" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b417ae4361bca3f5de378294fc7472d3c4ed86a5ef9f49e93ae722f432aae8d2" +checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d" dependencies = [ "clap_builder", "clap_derive", @@ -288,9 +287,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.22" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c90dc0f0e42c64bff177ca9d7be6fcc9ddb0f26a6e062174a61c84dd6c644d4" +checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6" dependencies = [ "anstream", "anstyle", @@ -300,9 +299,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" dependencies = [ "heck", "proc-macro2", @@ -312,9 +311,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "clipboard-win" @@ -361,7 +360,6 @@ dependencies = [ "indexmap 2.0.0", "md-5", "ratatui", - "regex", "serde", "shared", "shell-words", @@ -385,6 +383,7 @@ dependencies = [ "notify", "parking_lot", "ratatui", + "regex", "serde", "serde_json", "shared", @@ -515,9 +514,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" [[package]] name = "digest" @@ -547,7 +546,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -562,27 +561,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "error-code" version = "2.3.1" @@ -633,7 +611,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -807,9 +785,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "glob" @@ -954,17 +932,6 @@ dependencies = [ "libc", ] -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "itoa" version = "1.0.9" @@ -1042,12 +1009,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" - [[package]] name = "lock_api" version = "0.4.10" @@ -1122,7 +1083,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1152,19 +1113,20 @@ dependencies = [ [[package]] name = "notify" -version = "6.0.1" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", "walkdir", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -1228,9 +1190,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -1295,7 +1257,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.3", + "windows-targets 0.48.5", ] [[package]] @@ -1332,9 +1294,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1364,9 +1326,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.9" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1536,19 +1498,6 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" -[[package]] -name = "rustix" -version = "0.38.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" -dependencies = [ - "bitflags 2.4.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] - [[package]] name = "ryu" version = "1.0.15" @@ -1578,18 +1527,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", @@ -1694,9 +1643,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -1714,7 +1663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1824,9 +1773,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.25" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" dependencies = [ "deranged", "itoa", @@ -1843,9 +1792,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" dependencies = [ "time-core", ] @@ -1881,7 +1830,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2258,16 +2207,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.3", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.48.5", ] [[package]] @@ -2276,7 +2216,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.3", + "windows-targets 0.48.5", ] [[package]] @@ -2296,17 +2236,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f51fb4c64f8b770a823c043c7fad036323e1c48f55287b7bbb7987b2fcdf3b" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.3", - "windows_aarch64_msvc 0.48.3", - "windows_i686_gnu 0.48.3", - "windows_i686_msvc 0.48.3", - "windows_x86_64_gnu 0.48.3", - "windows_x86_64_gnullvm 0.48.3", - "windows_x86_64_msvc 0.48.3", + "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]] @@ -2317,9 +2257,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde1bb55ae4ce76a597a8566d82c57432bc69c039449d61572a7a353da28f68c" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -2329,9 +2269,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1513e8d48365a78adad7322fd6b5e4c4e99d92a69db8df2d435b25b1f1f286d4" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -2341,9 +2281,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60587c0265d2b842298f5858e1a5d79d146f9ee0c37be5782e92a6eb5e1d7a83" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -2353,9 +2293,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224fe0e0ffff5d2ea6a29f82026c8f43870038a0ffc247aa95a52b47df381ac4" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -2365,9 +2305,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fc52a0f50a088de499712cbc012df7ebd94e2d6eb948435449d76a6287e7ad" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -2377,9 +2317,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2093925509d91ea3d69bcd20238f4c2ecdb1a29d3c281d026a09705d0dd35f3d" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -2389,15 +2329,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.3" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ade45bc8bf02ae2aa34a9d54ba660a1a58204da34ba793c00d83ca3730b5f1" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] diff --git a/adaptor/src/adaptor.rs b/adaptor/src/adaptor.rs index bff94b6f..4a723f37 100644 --- a/adaptor/src/adaptor.rs +++ b/adaptor/src/adaptor.rs @@ -19,7 +19,7 @@ pub struct Adaptor; impl Adaptor { pub async fn image_show(mut path: &Path, rect: Rect) -> Result<()> { - let cache = BOOT.cache(path); + let cache = BOOT.cache(path, 0); if fs::metadata(&cache).await.is_ok() { path = cache.as_path(); } diff --git a/app/src/app.rs b/app/src/app.rs index 2b36a781..4b653c12 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -87,8 +87,8 @@ impl App { fn dispatch_resize(&mut self) { self.cx.manager.current_mut().set_page(true); - self.cx.manager.active_mut().preview_reset_image(); - self.cx.manager.preview(self.cx.image_layer()); + self.cx.manager.active_mut().preview_reset(); + self.cx.manager.peek(true, self.cx.image_layer()); emit!(Render); } @@ -100,7 +100,6 @@ impl App { } else { self.term = Some(Term::start().unwrap()); self.signals.stop_term(false); - self.cx.manager.preview(self.cx.image_layer()); emit!(Render); emit!(Hover); } @@ -152,18 +151,24 @@ impl App { Event::Mimetype(mimes) => { if manager.update_mimetype(mimes, tasks) { emit!(Render); - self.cx.manager.preview(self.cx.image_layer()); + emit!(Peek); } } Event::Hover(file) => { - if file.map(|f| manager.current_mut().hover_force(f)).unwrap_or(false) { + if manager.update_hover(file) { emit!(Render); } - self.cx.manager.preview(self.cx.image_layer()); + emit!(Peek); } - Event::Preview(path, mime, data) => { - manager.update_preview(path, mime, data); - emit!(Render); + Event::Peek(skip, sequent) => { + let b = sequent.is_some(); + manager.active_mut().update_peek(skip as isize, sequent); + self.cx.manager.peek(b, self.cx.image_layer()); + } + Event::Preview(lock) => { + if manager.active_mut().update_preview(lock) { + emit!(Render); + } } Event::Select(opt, tx) => { diff --git a/app/src/executor.rs b/app/src/executor.rs index 909bd952..68702479 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -61,6 +61,11 @@ impl Executor { let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); cx.manager.active_mut().arrow(step) } + "peek" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + cx.manager.active_mut().update_peek(step, None); + cx.manager.peek(true, cx.image_layer()) + } "leave" => cx.manager.active_mut().leave(), "enter" => cx.manager.active_mut().enter(), "back" => cx.manager.active_mut().back(), diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index 6cabdacf..755f120c 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -44,8 +44,14 @@ impl<'a> Folder<'a> { impl<'a> Widget for Folder<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - let window = self.folder.window(); - let mode = self.cx.manager.active().mode(); + let active = self.cx.manager.active(); + let mode = active.mode(); + + let window = if self.is_preview { + self.folder.window_for(active.preview().skip()) + } else { + self.folder.window() + }; let items = window .iter() diff --git a/app/src/manager/preview.rs b/app/src/manager/preview.rs index e7dbbb73..3986cae9 100644 --- a/app/src/manager/preview.rs +++ b/app/src/manager/preview.rs @@ -26,8 +26,7 @@ impl<'a> Widget for Preview<'a> { return; } - match &preview.data { - PreviewData::None => {} + match &preview.lock.as_ref().unwrap().data { PreviewData::Folder => { if let Some(folder) = manager.active().history(hovered) { Folder::new(self.cx, folder).with_preview(true).render(area, buf); diff --git a/config/Cargo.toml b/config/Cargo.toml index 798e566a..81eb60df 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -16,7 +16,6 @@ glob = "^0" indexmap = "^2" md-5 = "^0" ratatui = "^0" -regex = "^1" serde = { version = "^1", features = [ "derive" ] } shell-words = "^1" toml = { version = "^0", features = [ "preserve_order" ] } diff --git a/config/preset/keymap.toml b/config/preset/keymap.toml index ae7cc053..960c26e0 100644 --- a/config/preset/keymap.toml +++ b/config/preset/keymap.toml @@ -19,6 +19,9 @@ keymap = [ { on = [ "H" ], exec = "back" }, { on = [ "L" ], exec = "forward" }, + { on = [ "" ], exec = "peek -5" }, + { on = [ "" ], exec = "peek 5" }, + { on = [ "" ], exec = "arrow -1" }, { on = [ "" ], exec = "arrow 1" }, { on = [ "" ], exec = "leave" }, diff --git a/config/preset/theme.toml b/config/preset/theme.toml index a4cb7061..db081b47 100644 --- a/config/preset/theme.toml +++ b/config/preset/theme.toml @@ -41,13 +41,13 @@ rules = [ { mime = "audio/*", fg = "#F3D398" }, # Archives + { mime = "application/zip", fg = "#CD9EFC" }, + { mime = "application/gzip", fg = "#CD9EFC" }, { mime = "application/x-bzip", fg = "#CD9EFC" }, { mime = "application/x-bzip2", fg = "#CD9EFC" }, - { mime = "application/gzip", fg = "#CD9EFC" }, - { mime = "application/vnd.rar", fg = "#CD9EFC" }, { mime = "application/x-tar", fg = "#CD9EFC" }, - { mime = "application/zip", fg = "#CD9EFC" }, { mime = "application/x-7z-compressed", fg = "#CD9EFC" }, + { mime = "application/x-rar", fg = "#CD9EFC" }, # Fallback { name = "*", fg = "#C8D3F8" }, diff --git a/config/src/boot/boot.rs b/config/src/boot/boot.rs index c5f48c4c..982a1826 100644 --- a/config/src/boot/boot.rs +++ b/config/src/boot/boot.rs @@ -1,4 +1,4 @@ -use std::{env, fs, path::{Path, PathBuf}, time::{self, SystemTime}}; +use std::{env, fs, path::{Path, PathBuf}, process, time::{self, SystemTime}}; use clap::{command, Parser}; use md5::{Digest, Md5}; @@ -30,6 +30,10 @@ struct Args { /// Write the selected files on open emitted by the chooser mode #[arg(long)] chooser_file: Option, + + /// Clear the cache directory + #[arg(long, action)] + clear_cache: bool, } impl Default for Boot { @@ -58,23 +62,22 @@ impl Default for Boot { fs::create_dir_all(&boot.state_dir).unwrap(); } + if args.clear_cache { + println!("Clearing cache directory: {:?}", boot.cache_dir); + fs::remove_dir_all(&boot.cache_dir).unwrap(); + process::exit(0); + } + boot } } impl Boot { #[inline] - pub fn cache(&self, path: &Path) -> PathBuf { - #[cfg(target_os = "windows")] - let h = Md5::new_with_prefix(path.to_string_lossy().as_bytes()); - - #[cfg(not(target_os = "windows"))] - let h = { - use std::os::unix::ffi::OsStrExt; - Md5::new_with_prefix(path.as_os_str().as_bytes()) - }; - - self.cache_dir.join(format!("{:x}", h.finalize())) + pub fn cache(&self, path: &Path, skip: usize) -> PathBuf { + self + .cache_dir + .join(format!("{:x}", Md5::new_with_prefix(format!("{:?}///{}", path, skip)).finalize())) } #[inline] diff --git a/core/Cargo.toml b/core/Cargo.toml index 9997d120..0656a129 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,6 +18,7 @@ natord = "^1" notify = { version = "^6", default-features = false, features = [ "macos_fsevent" ] } parking_lot = "^0" ratatui = "^0" +regex = "^1" serde = "^1" serde_json = "^1" syntect = "^5" diff --git a/core/src/event.rs b/core/src/event.rs index 6f54410e..fd53cd0f 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -6,7 +6,8 @@ use crossterm::event::KeyEvent; use shared::RoCell; use tokio::sync::{mpsc::UnboundedSender, oneshot}; -use super::{files::{File, FilesOp}, input::InputOpt, manager::PreviewData, select::SelectOpt}; +use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt}; +use crate::manager::PreviewLock; static TX: RoCell> = RoCell::new(); @@ -26,7 +27,8 @@ pub enum Event { Pages(usize), Mimetype(BTreeMap), Hover(Option), - Preview(PathBuf, String, PreviewData), + Peek(usize, Option), + Preview(PreviewLock), // Input Select(SelectOpt, oneshot::Sender>), @@ -87,8 +89,14 @@ macro_rules! emit { (Hover($file:expr)) => { $crate::Event::Hover(Some($file)).emit(); }; - (Preview($path:expr, $mime:expr, $data:expr)) => { - $crate::Event::Preview($path, $mime, $data).emit(); + (Peek) => { + $crate::Event::Peek(0, None).emit(); + }; + (Peek($skip:expr, $path:expr)) => { + $crate::Event::Peek($skip, Some($path)).emit(); + }; + (Preview($lock:expr)) => { + $crate::Event::Preview($lock).emit(); }; (Select($opt:expr)) => {{ diff --git a/core/src/external/ffmpegthumbnailer.rs b/core/src/external/ffmpegthumbnailer.rs index 21d48979..98fbb695 100644 --- a/core/src/external/ffmpegthumbnailer.rs +++ b/core/src/external/ffmpegthumbnailer.rs @@ -1,22 +1,28 @@ use std::path::Path; -use anyhow::{bail, Result}; use config::PREVIEW; +use shared::PeekError; use tokio::process::Command; -pub async fn ffmpegthumbnailer(src: &Path, dest: &Path) -> Result<()> { +pub async fn ffmpegthumbnailer(src: &Path, dest: &Path, skip: usize) -> Result<(), PeekError> { + let percentage = 5 + skip; + if percentage > 95 { + return Err(PeekError::Exceed(95 - 5)); + } + let output = Command::new("ffmpegthumbnailer") .arg("-i") .arg(src) .arg("-o") .arg(dest) + .args(["-t", &percentage.to_string()]) .args(["-q", "6", "-c", "jpeg", "-s", &PREVIEW.max_width.to_string()]) .kill_on_drop(true) .output() .await?; if !output.status.success() { - bail!("failed to generate video thumbnail: {}", String::from_utf8_lossy(&output.stderr)); + return Err(String::from_utf8_lossy(&output.stderr).to_string().into()); } Ok(()) } diff --git a/core/src/external/jq.rs b/core/src/external/jq.rs index 39af6398..aa0c0cd5 100644 --- a/core/src/external/jq.rs +++ b/core/src/external/jq.rs @@ -1,10 +1,11 @@ use std::{path::Path, process::Stdio}; -use anyhow::{bail, Result}; +use anyhow::Result; use config::PREVIEW; +use shared::PeekError; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command}; -pub async fn jq(path: &Path, mut lines: usize) -> Result { +pub async fn jq(path: &Path, skip: usize, limit: usize) -> Result { let mut child = Command::new("jq") .args(["-C", "--indent", &PREVIEW.tab_size.to_string(), "."]) .arg(path) @@ -13,20 +14,24 @@ pub async fn jq(path: &Path, mut lines: usize) -> Result { .kill_on_drop(true) .spawn()?; + let mut i = 0; let mut it = BufReader::new(child.stdout.take().unwrap()).lines(); - let mut output = String::new(); + let mut lines = String::new(); while let Ok(Some(line)) = it.next_line().await { - if lines < 1 { + i += 1; + if i > skip + limit { break; + } else if i <= skip { + continue; } - output.push_str(&line); - output.push('\n'); - lines -= 1; + lines.push_str(&line); + lines.push('\n'); } - if output.is_empty() { - bail!("failed to get head of jq"); + if skip > 0 && i < skip + limit { + Err(PeekError::Exceed(i.saturating_sub(limit))) + } else { + Ok(lines) } - Ok(output) } diff --git a/core/src/external/lsar.rs b/core/src/external/lsar.rs index 40ed04f0..3e015d78 100644 --- a/core/src/external/lsar.rs +++ b/core/src/external/lsar.rs @@ -1,8 +1,9 @@ use std::path::Path; -use anyhow::{bail, Result}; +use anyhow::anyhow; use serde::Deserialize; use serde_json::Value; +use shared::PeekError; use tokio::process::Command; #[derive(Debug)] @@ -29,12 +30,13 @@ pub struct LsarFile { pub attributes: Option, } -pub async fn lsar(path: &Path) -> Result> { +#[allow(clippy::manual_map)] +pub async fn lsar(path: &Path, skip: usize, limit: usize) -> Result, PeekError> { let output = Command::new("lsar").args(["-j", "-jss"]).arg(path).kill_on_drop(true).output().await?; if !output.status.success() { - bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr)); + return Err(String::from_utf8_lossy(&output.stderr).to_string().into()); } #[derive(Deserialize)] @@ -44,20 +46,36 @@ pub async fn lsar(path: &Path) -> Result> { } let output = String::from_utf8_lossy(&output.stdout); - let contents = serde_json::from_str::(output.trim())?.contents; + let contents = serde_json::from_str::(output.trim()).map_err(|e| anyhow!(e))?.contents; - let mut files = Vec::with_capacity(contents.len()); + let mut i = 0; + let mut files = Vec::with_capacity(limit); for content in contents { - let mut file = serde_json::from_value::(content.clone())?; - if let Some(p) = content.get("XADPosixPermissions").and_then(|p| p.as_u64()) { - file.attributes = Some(LsarAttr::Posix(p as u16)); - } else if let Some(a) = content.get("XADWindowsFileAttributes").and_then(|a| a.as_u64()) { - file.attributes = Some(LsarAttr::Windows(a as u16)); - } else if let Some(a) = content.get("XADDOSFileAttributes").and_then(|a| a.as_u64()) { - file.attributes = Some(LsarAttr::Dos(a as u8)); + i += 1; + if i > skip + limit { + break; + } else if i <= skip { + continue; } + let attributes = if let Some(p) = content.get("XADPosixPermissions").and_then(|p| p.as_u64()) { + Some(LsarAttr::Posix(p as u16)) + } else if let Some(a) = content.get("XADWindowsFileAttributes").and_then(|a| a.as_u64()) { + Some(LsarAttr::Windows(a as u16)) + } else if let Some(a) = content.get("XADDOSFileAttributes").and_then(|a| a.as_u64()) { + Some(LsarAttr::Dos(a as u8)) + } else { + None + }; + + let mut file = serde_json::from_value::(content).map_err(|e| anyhow!(e))?; + file.attributes = attributes; files.push(file); } - Ok(files) + + if skip > 0 && files.len() < limit { + Err(PeekError::Exceed(i.saturating_sub(limit))) + } else { + Ok(files) + } } diff --git a/core/src/external/pdftoppm.rs b/core/src/external/pdftoppm.rs index 26762860..ded40197 100644 --- a/core/src/external/pdftoppm.rs +++ b/core/src/external/pdftoppm.rs @@ -1,19 +1,29 @@ use std::path::Path; use adaptor::Image; -use anyhow::{bail, Result}; +use regex::Regex; +use shared::PeekError; use tokio::process::Command; -pub async fn pdftoppm(src: &Path, dest: impl AsRef) -> Result<()> { +pub async fn pdftoppm(src: &Path, dest: impl AsRef, skip: usize) -> Result<(), PeekError> { let output = Command::new("pdftoppm") - .args(["-singlefile", "-jpeg", "-jpegopt", "quality=75"]) + .args(["-singlefile", "-jpeg", "-jpegopt", "quality=75", "-f"]) + .arg((skip + 1).to_string()) .arg(src) .kill_on_drop(true) .output() .await?; if !output.status.success() { - bail!("failed to generate PDF thumbnail: {}", String::from_utf8_lossy(&output.stderr)); + let s = String::from_utf8_lossy(&output.stderr); + let pages: usize = Regex::new(r"the last page \((\d+)\)") + .unwrap() + .captures(&s) + .map(|cap| cap[1].parse().unwrap()) + .unwrap_or(0); + + return if pages > 0 { Err(PeekError::Exceed(pages - 1)) } else { Err(s.to_string().into()) }; } - Image::precache_anyway(output.stdout.into(), dest).await + + Ok(Image::precache_anyway(output.stdout.into(), dest).await?) } diff --git a/core/src/manager/folder.rs b/core/src/manager/folder.rs index 9d5bb333..f633e5a0 100644 --- a/core/src/manager/folder.rs +++ b/core/src/manager/folder.rs @@ -108,6 +108,13 @@ impl Folder { self.files.get_range(self.offset..end).unwrap() } + #[inline] + pub fn window_for(&self, offset: usize) -> &Slice { + let start = offset.min(self.files.len().saturating_sub(1)); + let end = (offset + MANAGER.layout.folder_height()).min(self.files.len()); + self.files.get_range(start..end).unwrap() + } + pub fn select(&mut self, idx: Option, state: Option) -> bool { let len = self.files.len(); let mut apply = |idx: usize, state: Option| -> bool { @@ -157,6 +164,9 @@ impl Folder { } impl Folder { + #[inline] + pub fn offset(&self) -> usize { self.offset } + #[inline] pub fn cursor(&self) -> usize { self.cursor } diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 821e2f4f..678fe5ca 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -5,7 +5,7 @@ use config::{BOOT, OPEN}; use shared::{max_common_root, Defer, Term, MIME_DIR}; use tokio::{fs::{self, OpenOptions}, io::{stdin, AsyncReadExt, AsyncWriteExt}}; -use super::{PreviewData, Tab, Tabs, Watcher}; +use super::{Tab, Tabs, Watcher}; use crate::{emit, external::{self, ShellOpt}, files::{File, FilesOp}, input::InputOpt, manager::Folder, select::SelectOpt, tasks::Tasks, Event, BLOCKER}; pub struct Manager { @@ -52,7 +52,7 @@ impl Manager { self.watcher.watch(to_watch); } - pub fn preview(&mut self, show_image: bool) -> bool { + pub fn peek(&mut self, sequent: bool, show_image: bool) -> bool { let Some(hovered) = self.hovered().cloned() else { return self.active_mut().preview.reset(); }; @@ -61,19 +61,23 @@ impl Manager { self.active_mut().preview_reset_image(); } - if hovered.meta.is_dir() { - self.active_mut().preview.go(&hovered.path, MIME_DIR, show_image); - if self.active().history(&hovered.path).is_some() { - emit!(Preview(hovered.path, MIME_DIR.to_owned(), PreviewData::Folder)); - } - } else if let Some(mime) = self.mimetype.get(&hovered.path).cloned() { - self.active_mut().preview.go(&hovered.path, &mime, show_image); + let mime = if hovered.meta.is_dir() { + MIME_DIR.to_owned() + } else if let Some(m) = self.mimetype.get(&hovered.path).cloned() { + m } else { tokio::spawn(async move { if let Ok(mimes) = external::file(&[hovered.path]).await { emit!(Mimetype(mimes)); } }); + return false; + }; + + if sequent { + self.active_mut().preview.sequent(&hovered.path, &mime, show_image); + } else { + self.active_mut().preview.go(&hovered.path, &mime, show_image); } false } @@ -393,19 +397,16 @@ impl Manager { true } - pub fn update_preview(&mut self, path: PathBuf, mime: String, data: PreviewData) -> bool { - let Some(hovered) = self.hovered().map(|h| &h.path) else { - return self.active_mut().preview.reset(); + pub fn update_hover(&mut self, file: Option) -> bool { + let b = file.map(|f| self.current_mut().hover_force(f)).unwrap_or(false); + let Some(hovered) = self.hovered() else { + return b; }; - if path != *hovered { - return false; + if hovered.meta.is_dir() { + self.watcher.trigger_dirs(&[&hovered.path]); } - - let preview = &mut self.active_mut().preview; - preview.lock = Some((path, mime)); - preview.data = data; - true + b } } diff --git a/core/src/manager/preview.rs b/core/src/manager/preview.rs deleted file mode 100644 index 98456dc1..00000000 --- a/core/src/manager/preview.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::{io::BufRead, mem, path::{Path, PathBuf}, sync::{atomic::{AtomicUsize, Ordering}, Arc}}; - -use adaptor::Adaptor; -use anyhow::{anyhow, bail, Result}; -use config::{BOOT, MANAGER, PREVIEW}; -use shared::MimeKind; -use syntect::{easy::HighlightFile, util::as_24_bit_terminal_escaped}; -use tokio::{fs, task::JoinHandle}; - -use crate::{emit, external, files::{Files, FilesOp}, highlighter}; - -#[derive(Default)] -pub struct Preview { - pub lock: Option<(PathBuf, String)>, - pub data: PreviewData, - - incr: Arc, - handle: Option>, -} - -#[derive(Debug, Default)] -pub enum PreviewData { - #[default] - None, - Folder, - Text(String), - Image, -} - -impl Preview { - pub fn go(&mut self, path: &Path, mime: &str, show_image: bool) { - let kind = MimeKind::new(mime); - if !show_image && matches!(kind, MimeKind::Image | MimeKind::Video) { - return; - } else if self.same(path, mime) { - return; - } else { - self.reset(); - } - - let (path, mime) = (path.to_path_buf(), mime.to_owned()); - let incr = self.incr.clone(); - - self.handle = Some(tokio::spawn(async move { - let result = match kind { - MimeKind::Empty => Ok(PreviewData::None), - MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text), - MimeKind::Dir => Self::folder(&path).await, - MimeKind::Image => Self::image(&path).await, - MimeKind::Video => Self::video(&path).await, - MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text), - MimeKind::PDF => Self::pdf(&path).await, - MimeKind::Text => Self::highlight(&path, incr).await.map(PreviewData::Text), - MimeKind::Others => Err(anyhow!("Unsupported mimetype: {mime}")), - }; - - emit!(Preview(path, mime, result.unwrap_or_default())); - })); - } - - pub fn reset(&mut self) -> bool { - self.handle.take().map(|h| h.abort()); - self.incr.fetch_add(1, Ordering::Relaxed); - Adaptor::image_hide(MANAGER.layout.preview_rect()).ok(); - - self.lock = None; - !matches!( - mem::replace(&mut self.data, PreviewData::None), - PreviewData::None | PreviewData::Image - ) - } - - pub fn reset_image(&mut self) -> bool { - self.handle.take().map(|h| h.abort()); - self.incr.fetch_add(1, Ordering::Relaxed); - Adaptor::image_hide(MANAGER.layout.preview_rect()).ok(); - - if matches!(self.data, PreviewData::Image) { - self.lock = None; - self.data = PreviewData::None; - } - false - } - - pub async fn folder(path: &Path) -> Result { - emit!(Files(match Files::read_dir(path).await { - Ok(items) => FilesOp::Read(path.to_path_buf(), items), - Err(_) => FilesOp::IOErr(path.to_path_buf()), - })); - - Ok(PreviewData::Folder) - } - - pub async fn image(path: &Path) -> Result { - Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?; - Ok(PreviewData::Image) - } - - pub async fn video(path: &Path) -> Result { - let cache = BOOT.cache(path); - if fs::metadata(&cache).await.is_err() { - external::ffmpegthumbnailer(path, &cache).await?; - } - - Self::image(&cache).await - } - - pub async fn pdf(path: &Path) -> Result { - let cache = BOOT.cache(path); - if fs::metadata(&cache).await.is_err() { - external::pdftoppm(path, &cache).await?; - } - - Self::image(&cache).await - } - - pub async fn json(path: &Path) -> Result { - external::jq(path, MANAGER.layout.preview_height()).await - } - - pub async fn archive(path: &Path) -> Result { - Ok( - external::lsar(path) - .await? - .into_iter() - .take(MANAGER.layout.preview_height()) - .map(|f| f.name) - .collect::>() - .join("\n"), - ) - } - - pub async fn highlight(path: &Path, incr: Arc) -> Result { - let tick = incr.load(Ordering::Relaxed); - let path = path.to_path_buf(); - let spaces = " ".repeat(PREVIEW.tab_size as usize); - - let (syntaxes, theme) = highlighter(); - tokio::task::spawn_blocking(move || -> Result { - let mut h = HighlightFile::new(path, syntaxes, theme)?; - let mut line = String::new(); - let mut buf = String::new(); - - let mut i = MANAGER.layout.preview_height(); - while i > 0 && h.reader.read_line(&mut line)? > 0 { - if tick != incr.load(Ordering::Relaxed) { - bail!("Preview cancelled"); - } - - i -= 1; - line = line.replace('\t', &spaces); - let regions = h.highlight_lines.highlight_line(&line, syntaxes)?; - buf.push_str(&as_24_bit_terminal_escaped(®ions, false)); - line.clear(); - } - - buf.push_str("\x1b[0m"); - Ok(buf) - }) - .await? - } -} - -impl Preview { - #[inline] - pub fn same(&self, path: &Path, mime: &str) -> bool { - self.lock.as_ref().map(|(p, m)| p == path && m == mime).unwrap_or(false) - } - - #[inline] - pub fn same_path(&self, path: &Path) -> bool { - self.lock.as_ref().map(|(p, _)| p == path).unwrap_or(false) - } -} diff --git a/core/src/manager/preview/mod.rs b/core/src/manager/preview/mod.rs new file mode 100644 index 00000000..3a54a75d --- /dev/null +++ b/core/src/manager/preview/mod.rs @@ -0,0 +1,5 @@ +mod preview; +mod provider; + +pub use preview::*; +use provider::*; diff --git a/core/src/manager/preview/preview.rs b/core/src/manager/preview/preview.rs new file mode 100644 index 00000000..21227a2d --- /dev/null +++ b/core/src/manager/preview/preview.rs @@ -0,0 +1,151 @@ +use std::{path::{Path, PathBuf}, sync::atomic::Ordering}; + +use adaptor::Adaptor; +use config::MANAGER; +use shared::{MimeKind, PeekError}; +use tokio::task::JoinHandle; + +use super::{Provider, INCR}; +use crate::emit; + +#[derive(Default)] +pub struct Preview { + pub lock: Option, + skip: usize, + + handle: Option>, +} + +pub struct PreviewLock { + pub path: PathBuf, + pub mime: String, + pub skip: usize, + pub data: PreviewData, +} + +#[derive(Debug)] +pub enum PreviewData { + Folder, + Text(String), + Image, +} + +impl Preview { + pub fn go(&mut self, path: &Path, mime: &str, show_image: bool) { + let kind = MimeKind::new(mime); + if !show_image && kind.show_as_image() { + return; + } else if self.same(path, mime) { + return; + } + + self.reset(); + if !self.same_path(path) { + self.skip = 0; + } + + let (path, mime, skip) = (path.to_path_buf(), mime.to_owned(), self.skip); + self.handle = Some(tokio::spawn(async move { + match Provider::auto(kind, &path, skip).await { + Ok(data) => { + emit!(Preview(PreviewLock { path, mime, skip, data })); + } + Err(PeekError::Exceed(max)) => { + emit!(Peek(max, path)); + } + _ => {} + } + })); + } + + pub fn sequent(&mut self, path: &Path, mime: &str, show_image: bool) { + let kind = MimeKind::new(mime); + if !show_image && kind.show_as_image() { + return; + } else if self.same(path, mime) { + return; + } + + self.handle.take().map(|h| h.abort()); + INCR.fetch_add(1, Ordering::Relaxed); + + let (path, mime, skip) = (path.to_path_buf(), mime.to_owned(), self.skip); + self.handle = Some(tokio::spawn(async move { + match Provider::auto(kind, &path, skip).await { + Ok(data) => { + emit!(Preview(PreviewLock { path, mime, skip, data })); + } + Err(PeekError::Exceed(max)) => { + emit!(Peek(max, path)); + } + _ => {} + } + })); + } + + #[inline] + pub fn arrow(&mut self, step: isize, absolute: bool) -> bool { + let old = self.skip; + if absolute { + self.skip = step.unsigned_abs(); + } else if let Some(kind) = self.lock.as_ref().map(|l| MimeKind::new(&l.mime)) { + let size = Provider::step_size(kind, step.unsigned_abs()); + self.skip = if step < 0 { old.saturating_sub(size) } else { old + size }; + } + + self.skip != old + } + + pub fn reset(&mut self) -> bool { + self.handle.take().map(|h| h.abort()); + INCR.fetch_add(1, Ordering::Relaxed); + Adaptor::image_hide(MANAGER.layout.preview_rect()).ok(); + + let b = matches!(&self.lock, Some(l) if !l.is_image()); + self.lock = None; + b + } + + pub fn reset_image(&mut self) -> bool { + self.handle.take().map(|h| h.abort()); + INCR.fetch_add(1, Ordering::Relaxed); + Adaptor::image_hide(MANAGER.layout.preview_rect()).ok(); + + if matches!(&self.lock, Some(l) if l.is_image()) { + self.lock = None; + } + false + } +} + +impl Preview { + #[inline] + pub fn lock(&self) -> &Option { &self.lock } + + #[inline] + pub fn skip(&self) -> usize { self.skip } + + #[inline] + pub fn same(&self, path: &Path, mime: &str) -> bool { + if let Some(ref lock) = self.lock { + return lock.path == path && lock.mime == mime && lock.skip == self.skip; + } + false + } + + #[inline] + pub fn same_path(&self, path: &Path) -> bool { + if let Some(ref lock) = self.lock { + return lock.path == path; + } + false + } +} + +impl PreviewLock { + #[inline] + pub fn is_image(&self) -> bool { matches!(self.data, PreviewData::Image) } + + #[inline] + pub fn is_folder(&self) -> bool { matches!(self.data, PreviewData::Folder) } +} diff --git a/core/src/manager/preview/provider.rs b/core/src/manager/preview/provider.rs new file mode 100644 index 00000000..82e3e535 --- /dev/null +++ b/core/src/manager/preview/provider.rs @@ -0,0 +1,138 @@ +use std::{io::BufRead, path::Path, sync::atomic::{AtomicUsize, Ordering}}; + +use adaptor::Adaptor; +use anyhow::anyhow; +use config::{BOOT, MANAGER, PREVIEW}; +use shared::{MimeKind, PeekError}; +use syntect::{easy::HighlightFile, util::as_24_bit_terminal_escaped}; +use tokio::fs; + +use super::PreviewData; +use crate::{emit, external, files::{Files, FilesOp}, highlighter}; + +pub(super) struct Provider; + +pub(super) static INCR: AtomicUsize = AtomicUsize::new(0); + +impl Provider { + pub(super) async fn auto( + kind: MimeKind, + path: &Path, + skip: usize, + ) -> Result { + match kind { + MimeKind::Empty => Err("Empty file".into()), + MimeKind::Archive => Provider::archive(path, skip).await.map(PreviewData::Text), + MimeKind::Dir => Provider::folder(path).await, + MimeKind::Image => Provider::image(path).await, + MimeKind::Video => Provider::video(path, skip).await, + MimeKind::JSON => Provider::json(path, skip).await.map(PreviewData::Text), + MimeKind::PDF => Provider::pdf(path, skip).await, + MimeKind::Text => Provider::highlight(path, skip).await.map(PreviewData::Text), + MimeKind::Others => Err("Unsupported mimetype".into()), + } + } + + pub(super) fn step_size(kind: MimeKind, step: usize) -> usize { + match kind { + MimeKind::Empty => 0, + MimeKind::Archive => step * MANAGER.layout.preview_height() / 10, + MimeKind::Dir => step * MANAGER.layout.preview_height() / 10, + MimeKind::Image => 0, + MimeKind::Video => step, + MimeKind::JSON => step * MANAGER.layout.preview_height() / 10, + MimeKind::PDF => 1, + MimeKind::Text => step * MANAGER.layout.preview_height() / 10, + MimeKind::Others => 0, + } + } + + pub(super) async fn folder(path: &Path) -> Result { + emit!(Files(match Files::read_dir(path).await { + Ok(items) => FilesOp::Read(path.to_path_buf(), items), + Err(_) => FilesOp::IOErr(path.to_path_buf()), + })); + + Ok(PreviewData::Folder) + } + + pub(super) async fn image(path: &Path) -> Result { + Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?; + Ok(PreviewData::Image) + } + + pub(super) async fn video(path: &Path, skip: usize) -> Result { + let cache = BOOT.cache(path, skip); + if fs::metadata(&cache).await.is_err() { + external::ffmpegthumbnailer(path, &cache, skip).await?; + } + + Self::image(&cache).await + } + + pub(super) async fn pdf(path: &Path, skip: usize) -> Result { + let cache = BOOT.cache(path, skip); + if fs::metadata(&cache).await.is_err() { + external::pdftoppm(path, &cache, skip).await?; + } + + Self::image(&cache).await + } + + pub(super) async fn json(path: &Path, skip: usize) -> Result { + external::jq(path, skip, MANAGER.layout.preview_height()).await + } + + pub(super) async fn archive(path: &Path, skip: usize) -> Result { + Ok( + external::lsar(path, skip, MANAGER.layout.preview_height()) + .await? + .into_iter() + .map(|f| f.name) + .collect::>() + .join("\n"), + ) + } + + pub(super) async fn highlight(path: &Path, skip: usize) -> Result { + let tick = INCR.load(Ordering::Relaxed); + let path = path.to_path_buf(); + let spaces = " ".repeat(PREVIEW.tab_size as usize); + + let (syntaxes, theme) = highlighter(); + tokio::task::spawn_blocking(move || -> Result { + let mut h = HighlightFile::new(path, syntaxes, theme)?; + let mut line = String::new(); + let mut buf = String::new(); + + let mut i = 0; + let limit = MANAGER.layout.preview_height(); + while h.reader.read_line(&mut line)? > 0 { + if tick != INCR.load(Ordering::Relaxed) { + return Err("Highlighting cancelled".into()); + } + + i += 1; + if i > skip + limit { + break; + } + + line = line.replace('\t', &spaces); + let regions = h.highlight_lines.highlight_line(&line, syntaxes).map_err(|e| anyhow!(e))?; + + if i > skip { + buf.push_str(&as_24_bit_terminal_escaped(®ions, false)); + } + line.clear(); + } + + if skip > 0 && i < skip + limit { + return Err(PeekError::Exceed(i.saturating_sub(limit))); + } + + buf.push_str("\x1b[0m"); + Ok(buf) + }) + .await? + } +} diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index b059a0b9..f1287162 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -1,12 +1,12 @@ use std::{collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, path::{Path, PathBuf}}; use anyhow::{Error, Result}; -use config::open::Opener; +use config::{open::Opener, MANAGER}; use futures::StreamExt; -use shared::Defer; +use shared::{Defer, MIME_DIR}; use tokio::task::JoinHandle; -use super::{Folder, Mode, Preview}; +use super::{Folder, Mode, Preview, PreviewLock}; use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, Files, FilesOp}, input::InputOpt, Event, BLOCKER}; pub struct Tab { @@ -309,6 +309,41 @@ impl Tab { false } + + pub fn update_peek(&mut self, step: isize, path: Option) { + let Some(ref hovered) = self.current.hovered else { + return; + }; + + if path.as_ref().map(|p| *p != hovered.path).unwrap_or(false) { + return; + } else if !self.preview.arrow(step, path.is_some()) { + return; + } else if !matches!(&self.preview.lock, Some(l) if l.mime == MIME_DIR) { + return; + } + + let path = &self.preview.lock.as_ref().unwrap().path; + if let Some(folder) = self.history(path) { + let max = folder.files.len().saturating_sub(MANAGER.layout.preview_height()); + if self.preview.skip() > max { + self.preview.arrow(max as isize, true); + } + } + } + + pub fn update_preview(&mut self, lock: PreviewLock) -> bool { + let Some(hovered) = self.current.hovered.as_ref().map(|h| &h.path) else { + return self.preview.reset(); + }; + + if lock.path != *hovered { + return false; + } + + self.preview.lock = Some(lock); + true + } } impl Tab { @@ -362,6 +397,9 @@ impl Tab { #[inline] pub fn preview(&self) -> &Preview { &self.preview } + #[inline] + pub fn preview_reset(&mut self) -> bool { self.preview.reset() } + #[inline] pub fn preview_reset_image(&mut self) -> bool { self.preview.reset_image() } } diff --git a/core/src/tasks/tasks.rs b/core/src/tasks/tasks.rs index cb8267f6..2ef1ca95 100644 --- a/core/src/tasks/tasks.rs +++ b/core/src/tasks/tasks.rs @@ -34,7 +34,7 @@ impl Tasks { pub fn toggle(&mut self) -> bool { self.visible = !self.visible; - emit!(Hover); // Show/hide preview for images + emit!(Peek); // Show/hide preview for images true } diff --git a/core/src/tasks/workers/precache.rs b/core/src/tasks/workers/precache.rs index f38e6e42..9f92f211 100644 --- a/core/src/tasks/workers/precache.rs +++ b/core/src/tasks/workers/precache.rs @@ -74,7 +74,7 @@ impl Precache { pub(crate) async fn work(&self, task: &mut PrecacheOp) -> Result<()> { match task { PrecacheOp::Image(task) => { - let cache = BOOT.cache(&task.target); + let cache = BOOT.cache(&task.target, 0); if fs::metadata(&cache).await.is_ok() { return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); } @@ -84,21 +84,21 @@ impl Precache { self.sch.send(TaskOp::Adv(task.id, 1, 0))?; } PrecacheOp::Video(task) => { - let cache = BOOT.cache(&task.target); + let cache = BOOT.cache(&task.target, 0); if fs::metadata(&cache).await.is_ok() { return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); } - external::ffmpegthumbnailer(&task.target, &cache).await.ok(); + external::ffmpegthumbnailer(&task.target, &cache, 0).await.ok(); self.sch.send(TaskOp::Adv(task.id, 1, 0))?; } PrecacheOp::Pdf(task) => { - let cache = BOOT.cache(&task.target); + let cache = BOOT.cache(&task.target, 0); if fs::metadata(&cache).await.is_ok() { return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); } - external::pdftoppm(&task.target, &cache).await.ok(); + external::pdftoppm(&task.target, &cache, 0).await.ok(); self.sch.send(TaskOp::Adv(task.id, 1, 0))?; } } diff --git a/core/src/which/which.rs b/core/src/which/which.rs index 758bb101..a18f7e6d 100644 --- a/core/src/which/which.rs +++ b/core/src/which/which.rs @@ -51,6 +51,6 @@ impl Which { #[inline] fn switch(&mut self, state: bool) { self.visible = state; - emit!(Hover); // Show/hide preview for images + emit!(Peek); // Show/hide preview for images } } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 953218e9..57b327eb 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -9,5 +9,5 @@ crossterm = "^0" futures = "^0" libc = "^0" parking_lot = "^0" -ratatui = { version = "^0" } +ratatui = { version = "^0" } tokio = { version = "^1", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "time", "fs" ] } diff --git a/shared/src/errors/mod.rs b/shared/src/errors/mod.rs new file mode 100644 index 00000000..b70c7117 --- /dev/null +++ b/shared/src/errors/mod.rs @@ -0,0 +1,3 @@ +mod peek; + +pub use peek::*; diff --git a/shared/src/errors/peek.rs b/shared/src/errors/peek.rs new file mode 100644 index 00000000..96f8c805 --- /dev/null +++ b/shared/src/errors/peek.rs @@ -0,0 +1,34 @@ +use std::{error::Error, fmt::{self, Display}}; + +#[derive(Debug)] +pub enum PeekError { + Exceed(usize), + Unexpected(String), +} + +impl Display for PeekError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Exceed(lines) => write!(f, "Exceed: {lines}"), + Self::Unexpected(msg) => write!(f, "Unexpected error: {msg}"), + } + } +} + +impl Error for PeekError {} + +impl From for PeekError { + fn from(error: String) -> Self { Self::Unexpected(error) } +} +impl From<&str> for PeekError { + fn from(error: &str) -> Self { Self::from(error.to_owned()) } +} +impl From for PeekError { + fn from(error: anyhow::Error) -> Self { Self::from(error.to_string()) } +} +impl From for PeekError { + fn from(error: std::io::Error) -> Self { Self::from(error.to_string()) } +} +impl From for PeekError { + fn from(error: tokio::task::JoinError) -> Self { Self::from(error.to_string()) } +} diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 326423e1..6a6a1ed7 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -2,6 +2,7 @@ mod chars; mod defer; +mod errors; mod fns; mod fs; mod mime; @@ -13,6 +14,7 @@ mod time; pub use chars::*; pub use defer::*; +pub use errors::*; pub use fns::*; pub use fs::*; pub use mime::*; diff --git a/shared/src/mime.rs b/shared/src/mime.rs index 04831d6e..cb42d532 100644 --- a/shared/src/mime.rs +++ b/shared/src/mime.rs @@ -18,21 +18,6 @@ pub enum MimeKind { } impl MimeKind { - pub fn valid(s: &str) -> bool { - if s == "inode/x-empty" { - return true; - } - - let parts = s.split('/').collect::>(); - if parts.len() != 2 { - return false; - } - - #[rustfmt::skip] - let b = matches!(parts[0], "application" | "audio" | "example" | "font" | "image" | "message" | "model" | "multipart" | "text" | "video"); - b && !parts[1].is_empty() - } - pub fn new(s: &str) -> Self { if s == MIME_DIR { Self::Dir @@ -61,4 +46,23 @@ impl MimeKind { Self::Others } } + + pub fn valid(s: &str) -> bool { + if s == "inode/x-empty" { + return true; + } + + let parts = s.split('/').collect::>(); + if parts.len() != 2 { + return false; + } + + #[rustfmt::skip] + let b = matches!(parts[0], "application" | "audio" | "example" | "font" | "image" | "message" | "model" | "multipart" | "text" | "video"); + b && !parts[1].is_empty() + } + + pub fn show_as_image(&self) -> bool { + matches!(self, MimeKind::Image | MimeKind::Video | MimeKind::PDF) + } }