feat: make the preview scrollable (#86)

This commit is contained in:
三咲雅 · Misaki Masa 2023-08-26 17:28:43 +08:00 committed by GitHub
parent 388ad43537
commit e4096c0f25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 645 additions and 425 deletions

222
Cargo.lock generated
View file

@ -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",
]

View file

@ -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();
}

View file

@ -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) => {

View file

@ -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(),

View file

@ -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()

View file

@ -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);

View file

@ -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" ] }

View file

@ -19,6 +19,9 @@ keymap = [
{ on = [ "H" ], exec = "back" },
{ on = [ "L" ], exec = "forward" },
{ on = [ "<C-k>" ], exec = "peek -5" },
{ on = [ "<C-j>" ], exec = "peek 5" },
{ on = [ "<Up>" ], exec = "arrow -1" },
{ on = [ "<Down>" ], exec = "arrow 1" },
{ on = [ "<Left>" ], exec = "leave" },

View file

@ -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" },

View file

@ -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<PathBuf>,
/// 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]

View file

@ -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"

View file

@ -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<UnboundedSender<Event>> = RoCell::new();
@ -26,7 +27,8 @@ pub enum Event {
Pages(usize),
Mimetype(BTreeMap<PathBuf, String>),
Hover(Option<File>),
Preview(PathBuf, String, PreviewData),
Peek(usize, Option<PathBuf>),
Preview(PreviewLock),
// Input
Select(SelectOpt, oneshot::Sender<Result<usize>>),
@ -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)) => {{

View file

@ -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(())
}

View file

@ -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<String> {
pub async fn jq(path: &Path, skip: usize, limit: usize) -> Result<String, PeekError> {
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<String> {
.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)
}

View file

@ -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<LsarAttr>,
}
pub async fn lsar(path: &Path) -> Result<Vec<LsarFile>> {
#[allow(clippy::manual_map)]
pub async fn lsar(path: &Path, skip: usize, limit: usize) -> Result<Vec<LsarFile>, 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<Vec<LsarFile>> {
}
let output = String::from_utf8_lossy(&output.stdout);
let contents = serde_json::from_str::<Outer>(output.trim())?.contents;
let contents = serde_json::from_str::<Outer>(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::<LsarFile>(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::<LsarFile>(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)
}
}

View file

@ -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<Path>) -> Result<()> {
pub async fn pdftoppm(src: &Path, dest: impl AsRef<Path>, 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?)
}

View file

@ -108,6 +108,13 @@ impl Folder {
self.files.get_range(self.offset..end).unwrap()
}
#[inline]
pub fn window_for(&self, offset: usize) -> &Slice<PathBuf, File> {
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<usize>, state: Option<bool>) -> bool {
let len = self.files.len();
let mut apply = |idx: usize, state: Option<bool>| -> 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 }

View file

@ -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<File>) -> 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
}
}

View file

@ -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<AtomicUsize>,
handle: Option<JoinHandle<()>>,
}
#[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<PreviewData> {
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<PreviewData> {
Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?;
Ok(PreviewData::Image)
}
pub async fn video(path: &Path) -> Result<PreviewData> {
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<PreviewData> {
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<String> {
external::jq(path, MANAGER.layout.preview_height()).await
}
pub async fn archive(path: &Path) -> Result<String> {
Ok(
external::lsar(path)
.await?
.into_iter()
.take(MANAGER.layout.preview_height())
.map(|f| f.name)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub async fn highlight(path: &Path, incr: Arc<AtomicUsize>) -> Result<String> {
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<String> {
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(&regions, 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)
}
}

View file

@ -0,0 +1,5 @@
mod preview;
mod provider;
pub use preview::*;
use provider::*;

View file

@ -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<PreviewLock>,
skip: usize,
handle: Option<JoinHandle<()>>,
}
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<PreviewLock> { &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) }
}

View file

@ -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<PreviewData, PeekError> {
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<PreviewData, PeekError> {
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<PreviewData, PeekError> {
Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?;
Ok(PreviewData::Image)
}
pub(super) async fn video(path: &Path, skip: usize) -> Result<PreviewData, PeekError> {
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<PreviewData, PeekError> {
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<String, PeekError> {
external::jq(path, skip, MANAGER.layout.preview_height()).await
}
pub(super) async fn archive(path: &Path, skip: usize) -> Result<String, PeekError> {
Ok(
external::lsar(path, skip, MANAGER.layout.preview_height())
.await?
.into_iter()
.map(|f| f.name)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub(super) async fn highlight(path: &Path, skip: usize) -> Result<String, PeekError> {
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<String, PeekError> {
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(&regions, 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?
}
}

View file

@ -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<PathBuf>) {
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() }
}

View file

@ -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
}

View file

@ -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))?;
}
}

View file

@ -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
}
}

View file

@ -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" ] }

3
shared/src/errors/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod peek;
pub use peek::*;

34
shared/src/errors/peek.rs Normal file
View file

@ -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<String> 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<anyhow::Error> for PeekError {
fn from(error: anyhow::Error) -> Self { Self::from(error.to_string()) }
}
impl From<std::io::Error> for PeekError {
fn from(error: std::io::Error) -> Self { Self::from(error.to_string()) }
}
impl From<tokio::task::JoinError> for PeekError {
fn from(error: tokio::task::JoinError) -> Self { Self::from(error.to_string()) }
}

View file

@ -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::*;

View file

@ -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::<Vec<_>>();
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::<Vec<_>>();
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)
}
}