From 52aa00ed23bf458e82f92bf92310ec860bd3de9c Mon Sep 17 00:00:00 2001 From: sxyazi Date: Sat, 8 Jul 2023 19:46:43 +0800 Subject: [PATCH] first commit --- .gitignore | 1 + Cargo.lock | 2396 +++++++++++++++++++++++++++++++++ Cargo.toml | 38 + LICENCE | 21 + README.md | 40 + src/config/keymap/exec.rs | 66 + src/config/keymap/key.rs | 112 ++ src/config/keymap/keymap.rs | 41 + src/config/keymap/mod.rs | 7 + src/config/manager/manager.rs | 41 + src/config/manager/mod.rs | 5 + src/config/manager/sorting.rs | 46 + src/config/mod.rs | 24 + src/config/open/mod.rs | 3 + src/config/open/open.rs | 65 + src/config/pattern.rs | 50 + src/config/preview/mod.rs | 3 + src/config/preview/preview.rs | 30 + src/config/theme/color.rs | 39 + src/config/theme/icon.rs | 45 + src/config/theme/mod.rs | 7 + src/config/theme/theme.rs | 56 + src/core/adapter/kitty.rs | 49 + src/core/adapter/mod.rs | 1 + src/core/event.rs | 95 ++ src/core/input/input.rs | 312 +++++ src/core/input/mod.rs | 3 + src/core/manager/folder.rs | 254 ++++ src/core/manager/manager.rs | 252 ++++ src/core/manager/mod.rs | 25 + src/core/manager/mode.rs | 41 + src/core/manager/preview.rs | 157 +++ src/core/manager/tab.rs | 144 ++ src/core/manager/tabs.rs | 98 ++ src/core/manager/watcher.rs | 81 ++ src/core/mod.rs | 11 + src/core/tasks/file.rs | 315 +++++ src/core/tasks/mod.rs | 14 + src/core/tasks/precache.rs | 94 ++ src/core/tasks/process.rs | 85 ++ src/core/tasks/scheduler.rs | 368 +++++ src/core/tasks/tasks.rs | 164 +++ src/main.rs | 15 + src/misc/chars.rs | 18 + src/misc/defer.rs | 13 + src/misc/fns.rs | 198 +++ src/misc/mod.rs | 7 + src/ui/app.rs | 115 ++ src/ui/context.rs | 21 + src/ui/dispatcher.rs | 186 +++ src/ui/header/layout.rs | 28 + src/ui/header/mod.rs | 5 + src/ui/header/tabs.rs | 33 + src/ui/input.rs | 40 + src/ui/logs.rs | 24 + src/ui/manager/folder.rs | 75 ++ src/ui/manager/layout.rs | 47 + src/ui/manager/mod.rs | 8 + src/ui/manager/preview.rs | 58 + src/ui/mod.rs | 20 + src/ui/root.rs | 35 + src/ui/signals.rs | 88 ++ src/ui/status/layout.rs | 51 + src/ui/status/mod.rs | 5 + src/ui/status/progress.rs | 27 + src/ui/tasks/clear.rs | 19 + src/ui/tasks/layout.rs | 72 + src/ui/tasks/mod.rs | 5 + src/ui/term.rs | 67 + 69 files changed, 6979 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENCE create mode 100644 README.md create mode 100644 src/config/keymap/exec.rs create mode 100644 src/config/keymap/key.rs create mode 100644 src/config/keymap/keymap.rs create mode 100644 src/config/keymap/mod.rs create mode 100644 src/config/manager/manager.rs create mode 100644 src/config/manager/mod.rs create mode 100644 src/config/manager/sorting.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/open/mod.rs create mode 100644 src/config/open/open.rs create mode 100644 src/config/pattern.rs create mode 100644 src/config/preview/mod.rs create mode 100644 src/config/preview/preview.rs create mode 100644 src/config/theme/color.rs create mode 100644 src/config/theme/icon.rs create mode 100644 src/config/theme/mod.rs create mode 100644 src/config/theme/theme.rs create mode 100644 src/core/adapter/kitty.rs create mode 100644 src/core/adapter/mod.rs create mode 100644 src/core/event.rs create mode 100644 src/core/input/input.rs create mode 100644 src/core/input/mod.rs create mode 100644 src/core/manager/folder.rs create mode 100644 src/core/manager/manager.rs create mode 100644 src/core/manager/mod.rs create mode 100644 src/core/manager/mode.rs create mode 100644 src/core/manager/preview.rs create mode 100644 src/core/manager/tab.rs create mode 100644 src/core/manager/tabs.rs create mode 100644 src/core/manager/watcher.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/tasks/file.rs create mode 100644 src/core/tasks/mod.rs create mode 100644 src/core/tasks/precache.rs create mode 100644 src/core/tasks/process.rs create mode 100644 src/core/tasks/scheduler.rs create mode 100644 src/core/tasks/tasks.rs create mode 100644 src/main.rs create mode 100644 src/misc/chars.rs create mode 100644 src/misc/defer.rs create mode 100644 src/misc/fns.rs create mode 100644 src/misc/mod.rs create mode 100644 src/ui/app.rs create mode 100644 src/ui/context.rs create mode 100644 src/ui/dispatcher.rs create mode 100644 src/ui/header/layout.rs create mode 100644 src/ui/header/mod.rs create mode 100644 src/ui/header/tabs.rs create mode 100644 src/ui/input.rs create mode 100644 src/ui/logs.rs create mode 100644 src/ui/manager/folder.rs create mode 100644 src/ui/manager/layout.rs create mode 100644 src/ui/manager/mod.rs create mode 100644 src/ui/manager/preview.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/root.rs create mode 100644 src/ui/signals.rs create mode 100644 src/ui/status/layout.rs create mode 100644 src/ui/status/mod.rs create mode 100644 src/ui/status/progress.rs create mode 100644 src/ui/tasks/clear.rs create mode 100644 src/ui/tasks/layout.rs create mode 100644 src/ui/tasks/mod.rs create mode 100644 src/ui/term.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..3fd86d8e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2396 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi-to-tui" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e348dcd256ba06d44d5deabc88a7c0e80ee7303158253ca069bcd9e9b7f57" +dependencies = [ + "nom", + "ratatui", + "thiserror", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-trait" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "winapi", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console-api" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "hdrhistogram" +version = "7.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" +dependencies = [ + "base64 0.13.1", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +dependencies = [ + "bitflags", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets 0.48.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plist" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" +dependencies = [ + "base64 0.21.2", + "indexmap 1.9.3", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ranger-rs" +version = "0.1.0" +dependencies = [ + "ansi-to-tui", + "anyhow", + "async-channel", + "base64 0.21.2", + "console-subscriber", + "crossterm", + "futures", + "glob", + "image", + "indexmap 2.0.0", + "libc", + "md5", + "notify", + "once_cell", + "parking_lot", + "ratatui", + "serde", + "signal-hook-tokio", + "syntect", + "tokio", + "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", + "trash", + "unicode-width", + "xdg", +] + +[[package]] +name = "ratatui" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce841e0486e7c2412c3740168ede33adeba8e154a15107b879d8162d77c7174e" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.2", + "regex-syntax 0.7.3", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.3", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustversion" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" + +[[package]] +name = "ryu" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.167" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "serde_json" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signal-hook-tokio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "syntect" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" +dependencies = [ + "bincode", + "bitflags", + "flate2", + "fnv", + "lazy_static", + "once_cell", + "onig", + "plist", + "regex-syntax 0.6.29", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64 0.21.2", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trash" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85931d0ad541fa457c3f94c7cc5f14920f17a385832a7ef7621762c23cbf9729" +dependencies = [ + "chrono", + "libc", + "log", + "objc", + "once_cell", + "scopeguard", + "url", + "windows 0.44.0", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.23", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.1", +] + +[[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", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +dependencies = [ + "memchr", +] + +[[package]] +name = "xdg" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee" +dependencies = [ + "home", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..bae0fa02 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "ranger-rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +ansi-to-tui = "3.0.0" +anyhow = "1.0.71" +async-channel = "1.8.0" +base64 = "0.21.2" +console-subscriber = "0.1.9" +crossterm = { version = "0.26.1", features = [ "event-stream" ] } +futures = "0.3.28" +glob = "0.3.1" +image = "0.24.6" +indexmap = "2.0.0" +libc = "0.2.146" +md5 = "0.7.0" +notify = { version = "6.0.1", default-features = false, features = [ "macos_fsevent" ] } +once_cell = "1.18.0" +parking_lot = "0.12.1" +ratatui = "0.21.0" +serde = { version = "1.0.164", features = [ "derive" ] } +signal-hook-tokio = { version = "0.3.1", features = [ "futures-v0_3" ] } +syntect = "5.0.0" +tokio = { version = "1.28.2", features = [ "parking_lot", "macros", "rt-multi-thread", "sync", "fs", "process", "io-std", "io-util", "time" ] } +toml = "0.7.4" +tracing = "0.1.37" +tracing-appender = "0.2.2" +tracing-subscriber = "0.3.17" +trash = "3.0.3" +unicode-width = "0.1.10" +xdg = "2.5.0" + +# [profile.release] +# strip = true +# lto = true +# panic = "abort" diff --git a/LICENCE b/LICENCE new file mode 100644 index 00000000..9a5124ee --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 - sxyazi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..e5b60bde --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +## Yazi - ⚡️ Blazing Fast Terminal File Manager + +Yazi ("duck" in Chinese) is a terminal file manager written in Rust, based on non-blocking async I/O. It aims to provide an efficient, user-friendly, and configurable file management experience. + +⚠️ Note: Yazi is currently in active development and may be unstable. The API is subject to change without prior notice. Please use it with caution in a non-production environment. + +## Installation + +Before getting started, ensure that the following dependencies are installed on your system: + +- jq (optional, for JSON preview) +- ffmpegthumbnailer (optional, for video thumbnails) +- fzf (optional, for fuzzy search) +- rg (optional, for fuzzy search) +- zoxide (optional, for directory jumping) + +Execute the following commands to clone the project and build Yazi: + +```bash +git clone https://github.com/sxyazi/yazi.git +cd yazi +cargo build --release +``` + +## Usage + +```bash +./target/release/yazi +``` + +## TODO + +- Integration with zoxide for fast directory navigation +- Integration with fzf, rg for fuzzy file searching +- Support for Überzug++ for image previews with X11/wayland environment +- Batch renaming support + +## License + +Yazi is MIT licensed. diff --git a/src/config/keymap/exec.rs b/src/config/keymap/exec.rs new file mode 100644 index 00000000..b2499e71 --- /dev/null +++ b/src/config/keymap/exec.rs @@ -0,0 +1,66 @@ +use std::{collections::BTreeMap, fmt}; + +use serde::{de::Visitor, Deserializer}; + +#[derive(Debug, Default)] +pub struct Exec { + pub cmd: String, + pub args: Vec, + pub named: BTreeMap, +} + +impl From<&str> for Exec { + fn from(value: &str) -> Self { + let mut exec = Self::default(); + for x in value.split_whitespace() { + if x.starts_with("--") { + let mut it = x[2..].splitn(2, '='); + let name = it.next().unwrap(); + let value = it.next().unwrap_or(""); + exec.named.insert(name.to_string(), value.to_string()); + } else if exec.cmd.is_empty() { + exec.cmd = x.to_string(); + } else { + exec.args.push(x.to_string()); + } + } + exec + } +} + +impl Exec { + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct ExecVisitor; + + impl<'de> Visitor<'de> for ExecVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a command string, e.g. tab_switch 0") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut execs = Vec::new(); + while let Some(value) = &seq.next_element::()? { + execs.push(Exec::from(value.as_str())); + } + Ok(execs) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(value.split(';').map(Exec::from).collect()) + } + } + + deserializer.deserialize_any(ExecVisitor) + } +} diff --git a/src/config/keymap/key.rs b/src/config/keymap/key.rs new file mode 100644 index 00000000..d6d39e13 --- /dev/null +++ b/src/config/keymap/key.rs @@ -0,0 +1,112 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use serde::{de::Visitor, Deserialize, Deserializer}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Key { + pub code: KeyCode, + pub shift: bool, + pub ctrl: bool, + pub alt: bool, +} + +impl Default for Key { + fn default() -> Self { Self { code: KeyCode::Null, shift: false, ctrl: false, alt: false } } +} + +impl From<&str> for Key { + fn from(value: &str) -> Self { + let mut key = Default::default(); + if value.is_empty() { + return key; + } + + if !value.starts_with("<") || !value.ends_with(">") { + let c = value.chars().next().unwrap(); + key.code = KeyCode::Char(c); + key.shift = c.is_ascii_uppercase(); + return key; + } + + let mut it = value[1..value.len() - 1].split_inclusive('-').peekable(); + while let Some(x) = it.next() { + match x { + "S-" => key.shift = true, + "C-" => key.ctrl = true, + "A-" => key.alt = true, + + "Space" => key.code = KeyCode::Char(' '), + "Backspace" => key.code = KeyCode::Backspace, + "Enter" => key.code = KeyCode::Enter, + "Left" => key.code = KeyCode::Left, + "Right" => key.code = KeyCode::Right, + "Up" => key.code = KeyCode::Up, + "Down" => key.code = KeyCode::Down, + "Home" => key.code = KeyCode::Home, + "End" => key.code = KeyCode::End, + "PageUp" => key.code = KeyCode::PageUp, + "PageDown" => key.code = KeyCode::PageDown, + "Tab" => key.code = KeyCode::Tab, + "Delete" => key.code = KeyCode::Delete, + "Insert" => key.code = KeyCode::Insert, + "F1" => key.code = KeyCode::F(1), + "F2" => key.code = KeyCode::F(2), + "F3" => key.code = KeyCode::F(3), + "F4" => key.code = KeyCode::F(4), + "F5" => key.code = KeyCode::F(5), + "F6" => key.code = KeyCode::F(6), + "F7" => key.code = KeyCode::F(7), + "F8" => key.code = KeyCode::F(8), + "F9" => key.code = KeyCode::F(9), + "F10" => key.code = KeyCode::F(10), + "F11" => key.code = KeyCode::F(11), + "F12" => key.code = KeyCode::F(12), + "Esc" => key.code = KeyCode::Esc, + + c if it.peek().is_none() => { + key.code = KeyCode::Char(c.chars().next().unwrap()); + } + _ => {} + } + } + key + } +} + +impl From for Key { + fn from(value: KeyEvent) -> Self { + let shift = if let KeyCode::Char(c) = value.code { c.is_ascii_uppercase() } else { false }; + + Self { + code: value.code, + shift: shift || value.modifiers.contains(KeyModifiers::SHIFT), + ctrl: value.modifiers.contains(KeyModifiers::CONTROL), + alt: value.modifiers.contains(KeyModifiers::ALT), + } + } +} + +impl<'de> Deserialize<'de> for Key { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct KeyVisitor; + + impl<'de> Visitor<'de> for KeyVisitor { + type Value = Key; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a key string, e.g. ") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(Key::from(value)) + } + } + + deserializer.deserialize_str(KeyVisitor) + } +} diff --git a/src/config/keymap/keymap.rs b/src/config/keymap/keymap.rs new file mode 100644 index 00000000..88c6a0dc --- /dev/null +++ b/src/config/keymap/keymap.rs @@ -0,0 +1,41 @@ +use std::fs; + +use serde::Deserialize; +use xdg::BaseDirectories; + +use super::{Exec, Key}; + +#[derive(Deserialize, Debug)] +pub struct Single { + pub on: Vec, + #[serde(deserialize_with = "Exec::deserialize")] + pub exec: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct Keymap { + pub manager: Vec, + pub tasks: Vec, + pub input: Vec, +} + +impl Keymap { + pub fn new() -> Self { + #[derive(Deserialize)] + struct Inner { + keymap: Vec, + } + + #[derive(Deserialize)] + struct All { + manager: Inner, + tasks: Inner, + input: Inner, + } + + let path = BaseDirectories::new().unwrap().get_config_file("yazi/keymap.toml"); + + let all: All = toml::from_str(&fs::read_to_string(path).unwrap()).unwrap(); + Self { manager: all.manager.keymap, tasks: all.tasks.keymap, input: all.input.keymap } + } +} diff --git a/src/config/keymap/mod.rs b/src/config/keymap/mod.rs new file mode 100644 index 00000000..12bd2809 --- /dev/null +++ b/src/config/keymap/mod.rs @@ -0,0 +1,7 @@ +mod exec; +mod key; +mod keymap; + +pub use exec::*; +pub use key::*; +pub use keymap::*; diff --git a/src/config/manager/manager.rs b/src/config/manager/manager.rs new file mode 100644 index 00000000..3a7faee3 --- /dev/null +++ b/src/config/manager/manager.rs @@ -0,0 +1,41 @@ +use std::{env, fs, path::PathBuf}; + +use serde::Deserialize; +use xdg::BaseDirectories; + +use super::SortBy; + +#[derive(Deserialize, Debug)] +pub struct Manager { + #[serde(skip)] + pub cwd: PathBuf, + #[serde(skip)] + pub cache: PathBuf, + + // Sorting + pub sort_by: SortBy, + pub sort_reverse: bool, + + // Display + pub show_hidden: bool, +} + +impl Manager { + pub fn new() -> Self { + #[derive(Deserialize)] + struct Outer { + manager: Manager, + } + + let path = BaseDirectories::new().unwrap().get_config_file("yazi/yazi.toml"); + let mut manager = toml::from_str::(&fs::read_to_string(path).unwrap()).unwrap().manager; + + manager.cwd = env::current_dir().unwrap_or("/".into()); + manager.cache = "/tmp/yazi".into(); + if !manager.cache.is_dir() { + fs::create_dir(&manager.cache).unwrap(); + } + + manager + } +} diff --git a/src/config/manager/mod.rs b/src/config/manager/mod.rs new file mode 100644 index 00000000..6bf4de2e --- /dev/null +++ b/src/config/manager/mod.rs @@ -0,0 +1,5 @@ +mod manager; +mod sorting; + +pub use manager::*; +pub use sorting::*; diff --git a/src/config/manager/sorting.rs b/src/config/manager/sorting.rs new file mode 100644 index 00000000..b86d077d --- /dev/null +++ b/src/config/manager/sorting.rs @@ -0,0 +1,46 @@ +use serde::{de::Visitor, Deserialize, Deserializer}; + +#[derive(Debug, Clone, Copy)] +pub enum SortBy { + Alphabetical, + Created, + Modified, + Size, +} + +impl From<&str> for SortBy { + fn from(value: &str) -> Self { + match value { + "created" => Self::Created, + "modified" => Self::Modified, + "size" => Self::Size, + _ => Self::Alphabetical, + } + } +} + +impl<'de> Deserialize<'de> for SortBy { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SortByVisitor; + + impl<'de> Visitor<'de> for SortByVisitor { + type Value = SortBy; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a sort_by string, e.g. modified") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(SortBy::from(value)) + } + } + + deserializer.deserialize_str(SortByVisitor) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 00000000..54890891 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,24 @@ +use once_cell::sync::Lazy; + +pub mod keymap; +pub mod manager; +pub mod open; +mod pattern; +pub mod preview; +pub mod theme; + +pub use pattern::*; + +pub static KEYMAP: Lazy = Lazy::new(|| keymap::Keymap::new()); +pub static MANAGER: Lazy = Lazy::new(|| manager::Manager::new()); +pub static OPEN: Lazy = Lazy::new(|| open::Open::new()); +pub static PREVIEW: Lazy = Lazy::new(|| preview::Preview::new()); +pub static THEME: Lazy = Lazy::new(|| theme::Theme::new()); + +pub fn init() { + Lazy::force(&KEYMAP); + Lazy::force(&MANAGER); + Lazy::force(&OPEN); + Lazy::force(&PREVIEW); + Lazy::force(&THEME); +} diff --git a/src/config/open/mod.rs b/src/config/open/mod.rs new file mode 100644 index 00000000..dd44d4a9 --- /dev/null +++ b/src/config/open/mod.rs @@ -0,0 +1,3 @@ +mod open; + +pub use open::*; diff --git a/src/config/open/open.rs b/src/config/open/open.rs new file mode 100644 index 00000000..ad8d8bf1 --- /dev/null +++ b/src/config/open/open.rs @@ -0,0 +1,65 @@ +use std::{collections::BTreeMap, fs, path::Path}; + +use serde::Deserialize; +use xdg::BaseDirectories; + +use crate::config::Pattern; + +#[derive(Clone, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Opener { + pub cmd: String, + pub args: Vec, + #[serde(default)] + pub block: bool, + #[serde(skip)] + pub spread: bool, +} + +#[derive(Deserialize, Debug)] +pub struct Open { + #[serde(skip)] + openers: BTreeMap>, + + rules: Vec, +} + +#[derive(Deserialize, Debug)] +struct OpenRule { + name: Option, + mime: Option, + #[serde(rename = "use")] + use_: String, +} + +impl Open { + pub fn new() -> Open { + #[derive(Deserialize)] + struct Outer { + opener: BTreeMap>, + open: Open, + } + + let path = BaseDirectories::new().unwrap().get_config_file("yazi/yazi.toml"); + let mut outer = toml::from_str::(&fs::read_to_string(path).unwrap()).unwrap(); + + for opener in outer.opener.values_mut() { + for one in opener.iter_mut() { + one.spread = one.args.iter().any(|a| a == "$*"); + } + } + + Self { openers: outer.opener, rules: outer.open.rules } + } + + pub fn opener(&self, path: &Path, mime: &str) -> Option<&Opener> { + self.rules.iter().find_map(|rule| { + if rule.name.as_ref().map_or(false, |e| e.match_path(path, Some(false))) + || rule.mime.as_ref().map_or(false, |m| m.matches(mime)) + { + self.openers.get(&rule.use_).and_then(|v| v.first()) + } else { + None + } + }) + } +} diff --git a/src/config/pattern.rs b/src/config/pattern.rs new file mode 100644 index 00000000..b2d53973 --- /dev/null +++ b/src/config/pattern.rs @@ -0,0 +1,50 @@ +use std::path::Path; + +use serde::{de::Visitor, Deserialize, Deserializer}; + +#[derive(Debug)] +pub struct Pattern { + inner: glob::Pattern, + is_folder: bool, +} + +impl Pattern { + pub fn matches(&self, str: &str) -> bool { self.inner.matches(str) } + + pub fn match_path(&self, path: &Path, is_folder: Option) -> bool { + is_folder.map_or(true, |f| f == self.is_folder) && self.inner.matches_path(path) + } +} + +impl From<&str> for Pattern { + fn from(value: &str) -> Self { + let is_folder = value.ends_with('/'); + Self { inner: glob::Pattern::new(value.trim_end_matches('/')).unwrap_or_default(), is_folder } + } +} + +impl<'de> Deserialize<'de> for Pattern { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct PatternVisitor; + + impl<'de> Visitor<'de> for PatternVisitor { + type Value = Pattern; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a glob pattern, e.g. *.rs") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(Pattern::from(value)) + } + } + + deserializer.deserialize_str(PatternVisitor) + } +} diff --git a/src/config/preview/mod.rs b/src/config/preview/mod.rs new file mode 100644 index 00000000..eacd60e4 --- /dev/null +++ b/src/config/preview/mod.rs @@ -0,0 +1,3 @@ +mod preview; + +pub use preview::*; diff --git a/src/config/preview/preview.rs b/src/config/preview/preview.rs new file mode 100644 index 00000000..0e24525a --- /dev/null +++ b/src/config/preview/preview.rs @@ -0,0 +1,30 @@ +use std::fs; + +use serde::Deserialize; +use xdg::BaseDirectories; + +#[derive(Deserialize, Debug)] +pub struct Manager { + pub sort_by: String, + pub sort_reverse: bool, +} + +#[derive(Deserialize, Debug)] +pub struct Preview { + pub tab_size: u32, + + pub max_width: u32, + pub max_height: u32, +} + +impl Preview { + pub fn new() -> Self { + #[derive(Deserialize)] + struct Outer { + preview: Preview, + } + + let path = BaseDirectories::new().unwrap().get_config_file("yazi/yazi.toml"); + toml::from_str::(&fs::read_to_string(path).unwrap()).unwrap().preview + } +} diff --git a/src/config/theme/color.rs b/src/config/theme/color.rs new file mode 100644 index 00000000..70f00081 --- /dev/null +++ b/src/config/theme/color.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use ratatui::style; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct Color { + pub fg: String, + pub bg: String, +} + +impl Color { + pub fn fg_rgb(&self) -> style::Color { + if self.fg.len() < 7 { + return style::Color::Reset; + } + let convert = || -> Result { + Ok(style::Color::Rgb( + u8::from_str_radix(&self.fg[1..3], 16)?, + u8::from_str_radix(&self.fg[3..5], 16)?, + u8::from_str_radix(&self.fg[5..7], 16)?, + )) + }; + convert().unwrap_or(style::Color::Reset) + } + + pub fn bg_rgb(&self) -> style::Color { + if self.bg.len() < 7 { + return style::Color::Reset; + } + let convert = || -> Result { + Ok(style::Color::Rgb( + u8::from_str_radix(&self.bg[1..3], 16)?, + u8::from_str_radix(&self.bg[3..5], 16)?, + u8::from_str_radix(&self.bg[5..7], 16)?, + )) + }; + convert().unwrap_or(style::Color::Reset) + } +} diff --git a/src/config/theme/icon.rs b/src/config/theme/icon.rs new file mode 100644 index 00000000..dbb73521 --- /dev/null +++ b/src/config/theme/icon.rs @@ -0,0 +1,45 @@ +use std::fmt; + +use serde::{de::Visitor, Deserializer}; + +use crate::config::Pattern; + +#[derive(Debug)] +pub struct Icon { + pub name: Pattern, + pub display: String, +} + +impl Icon { + pub fn new(name: String, display: String) -> Self { + Self { name: Pattern::from(name.as_ref()), display } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct IconVisitor; + + impl<'de> Visitor<'de> for IconVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a icon rule, e.g. \"*.md\" = \"\"") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut icons = Vec::new(); + while let Some((key, value)) = &map.next_entry::()? { + icons.push(Icon::new(key.clone(), value.clone())); + } + Ok(icons) + } + } + + deserializer.deserialize_map(IconVisitor) + } +} diff --git a/src/config/theme/mod.rs b/src/config/theme/mod.rs new file mode 100644 index 00000000..7157e8d2 --- /dev/null +++ b/src/config/theme/mod.rs @@ -0,0 +1,7 @@ +mod color; +mod icon; +mod theme; + +pub use color::*; +pub use icon::*; +pub use theme::*; diff --git a/src/config/theme/theme.rs b/src/config/theme/theme.rs new file mode 100644 index 00000000..228b2851 --- /dev/null +++ b/src/config/theme/theme.rs @@ -0,0 +1,56 @@ +use std::{fs, path::PathBuf}; + +use serde::Deserialize; +use xdg::BaseDirectories; + +use super::{Color, Icon}; +use crate::misc::absolute_path; + +#[derive(Deserialize)] +pub struct Mode { + pub normal: Color, + pub select: Color, + pub unselect: Color, +} + +#[derive(Deserialize)] +pub struct Tab { + pub active: Color, + pub inactive: Color, +} + +#[derive(Deserialize)] +pub struct Selection { + pub normal: Color, + pub hovered: Color, + pub selected: Color, +} + +#[derive(Deserialize)] +pub struct Filetype {} + +#[derive(Deserialize)] +pub struct Syntect { + pub theme: PathBuf, +} + +#[derive(Deserialize)] +pub struct Theme { + pub mode: Mode, + pub tab: Tab, + pub selection: Selection, + pub filetype: Filetype, + pub syntect: Syntect, + #[serde(deserialize_with = "Icon::deserialize")] + pub icons: Vec, +} + +impl Theme { + pub fn new() -> Self { + let path = BaseDirectories::new().unwrap().get_config_file("yazi/theme.toml"); + + let mut parsed: Self = toml::from_str(&fs::read_to_string(path).unwrap()).unwrap(); + parsed.syntect.theme = absolute_path(&parsed.syntect.theme); + parsed + } +} diff --git a/src/core/adapter/kitty.rs b/src/core/adapter/kitty.rs new file mode 100644 index 00000000..c314f767 --- /dev/null +++ b/src/core/adapter/kitty.rs @@ -0,0 +1,49 @@ +use std::io::Write; + +use anyhow::Result; +use base64::{engine::general_purpose, Engine}; +use image::DynamicImage; + +pub struct Kitty; + +impl Kitty { + pub fn image_show(img: DynamicImage) -> Result> { + fn output(raw: Vec, format: u8, size: (u32, u32)) -> Result> { + let b64 = general_purpose::STANDARD.encode(raw).chars().collect::>(); + + let mut it = b64.chunks(4096).peekable(); + let mut buf = Vec::with_capacity(b64.len() + it.len() * 50); + if let Some(first) = it.next() { + write!( + buf, + "\x1b_Ga=d\x1b\\\x1b_Ga=T,f={},s={},v={},m={};{}\x1b\\", + format, + size.0, + size.1, + it.peek().is_some() as u8, + first.iter().collect::(), + )?; + } + + while let Some(chunk) = it.next() { + write!( + buf, + "\x1b_Gm={};{}\x1b\\", + it.peek().is_some() as u8, + chunk.iter().collect::() + )?; + } + Ok(buf) + } + + let size = (img.width(), img.height()); + match img { + DynamicImage::ImageRgb8(v) => output(v.into_raw(), 24, size), + DynamicImage::ImageRgba8(v) => output(v.into_raw(), 32, size), + v => output(v.to_rgb8().into_raw(), 24, size), + } + } + + #[inline] + pub fn image_hide() -> &'static [u8; 8] { b"\x1b_Ga=d\x1b\\" } +} diff --git a/src/core/adapter/mod.rs b/src/core/adapter/mod.rs new file mode 100644 index 00000000..2823cb39 --- /dev/null +++ b/src/core/adapter/mod.rs @@ -0,0 +1 @@ +pub mod kitty; diff --git a/src/core/event.rs b/src/core/event.rs new file mode 100644 index 00000000..4c1f1886 --- /dev/null +++ b/src/core/event.rs @@ -0,0 +1,95 @@ +use std::path::PathBuf; + +use anyhow::Result; +use crossterm::event::KeyEvent; +use indexmap::IndexMap; +use tokio::sync::{mpsc::Sender, oneshot}; + +use super::{FolderItem, InputOpt, PreviewData}; + +static mut TX: Option> = None; + +pub enum Event { + Quit, + Stop(bool, oneshot::Sender<()>), + Key(KeyEvent), + Render(String), + Resize(u16, u16), + + Refresh, + Files(PathBuf, IndexMap), + Hover, + Mimetype(PathBuf, String), + Preview(PathBuf, PreviewData), + + Input(InputOpt, oneshot::Sender>), + + Open(Vec), + Progress(u8, u32), +} + +impl Event { + #[inline] + pub fn init(tx: Sender) { + unsafe { + TX.replace(tx); + } + } + + #[inline] + pub fn emit(self) { + let tx = unsafe { TX.as_ref().unwrap() }; + tokio::spawn(async { + tx.send(self).await.ok(); + }); + } + + pub async fn wait(self, rx: oneshot::Receiver) -> T { + let tx = unsafe { TX.as_ref().unwrap() }; + tx.send(self).await.ok(); + rx.await.unwrap() + } +} + +#[macro_export] +macro_rules! emit { + (Stop($state:expr)) => {{ + let (tx, rx) = tokio::sync::oneshot::channel(); + $crate::core::Event::Stop($state, tx).wait(rx) + }}; + (Key($key:expr)) => { + $crate::core::Event::Key($key).emit(); + }; + (Render) => { + $crate::core::Event::Render(format!("{}:{}", file!(), line!())).emit(); + }; + (Resize($cols:expr, $rows:expr)) => { + $crate::core::Event::Resize($cols, $rows).emit(); + }; + + (Files($path:expr, $items:expr)) => { + $crate::core::Event::Files($path, $items).emit(); + }; + (Mimetype($path:expr, $mime:expr)) => { + $crate::core::Event::Mimetype($path, $mime).emit(); + }; + (Preview($path:expr, $data:expr)) => { + $crate::core::Event::Preview($path, $data).emit(); + }; + + (Input($opt:expr)) => {{ + let (tx, rx) = tokio::sync::oneshot::channel(); + $crate::core::Event::Input($opt, tx).wait(rx) + }}; + + (Open($files:expr)) => { + $crate::core::Event::Open($files).emit(); + }; + (Progress($percent:expr, $tasks:expr)) => { + $crate::core::Event::Progress($percent, $tasks).emit(); + }; + + ($event:ident) => { + $crate::core::Event::$event.emit(); + }; +} diff --git a/src/core/input/input.rs b/src/core/input/input.rs new file mode 100644 index 00000000..405fd9b3 --- /dev/null +++ b/src/core/input/input.rs @@ -0,0 +1,312 @@ +use anyhow::{anyhow, Result}; +use ratatui::layout::Rect; +use tokio::sync::oneshot::Sender; +use unicode_width::UnicodeWidthStr; + +use crate::misc::{tty_size, CharKind}; + +pub struct Input { + title: String, + value: String, + position: (u16, u16), + + op: InputOp, + range: Option<(usize, usize)>, + + mode: InputMode, + offset: usize, + cursor: usize, + callback: Option>>, + + pub visible: bool, +} + +pub struct InputOpt { + pub title: String, + pub value: String, + pub position: (u16, u16), +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum InputMode { + Normal, + #[default] + Insert, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum InputOp { + #[default] + None, + Delete(bool), + Yank, +} + +impl Input { + pub fn show(&mut self, opt: InputOpt, tx: Sender>) { + self.title = opt.title; + self.value = opt.value; + self.position = opt.position; + + self.mode = InputMode::Insert; + self.cursor = self.count(); + self.offset = self.value.width().saturating_sub(50); + self.callback = Some(tx); + self.visible = true; + } + + pub fn close(&mut self, submit: bool) -> bool { + self.visible = false; + if let Some(cb) = self.callback.take() { + let _ = cb.send(if submit { Ok(self.value.clone()) } else { Err(anyhow!("canceled")) }); + } + true + } + + pub fn escape(&mut self) -> bool { + match self.mode { + InputMode::Normal => { + self.range = None; + } + InputMode::Insert => { + self.mode = InputMode::Normal; + self.move_(-1); + } + } + true + } + + pub fn insert(&mut self, append: bool) -> bool { + if self.mode != InputMode::Normal { + return false; + } + + self.mode = InputMode::Insert; + if append { + self.move_(1); + } + true + } + + pub fn visual(&mut self) -> bool { + if self.mode != InputMode::Normal { + return false; + } + + self.range = Some((self.cursor, self.cursor)); + true + } + + pub fn move_(&mut self, step: isize) -> bool { + let old = self.cursor; + let mut include = false; + + if step <= 0 { + self.cursor = self.cursor.saturating_sub(step.abs() as usize); + } else { + let count = self.count(); + self.cursor += step as usize; + + if self.cursor >= count { + include = true; + self.cursor = if self.mode == InputMode::Insert { count } else { count.saturating_sub(1) }; + } + } + + if self.cursor != old { + if self.cursor < self.offset { + self.offset = self.cursor; + } else if self.cursor > self.offset + 50 { + self.offset = self.cursor.saturating_sub(50); + } + } + + self.handle_op(include) || self.cursor != old + } + + pub fn backward(&mut self) -> bool { + if self.cursor == 0 { + return self.handle_op(false); + } + + let idx = self.idx(self.cursor).unwrap_or(self.value.len()); + let mut it = self.value[..idx].chars().rev().enumerate(); + let mut prev = CharKind::new(it.next().unwrap().1); + for (i, c) in it { + let c = CharKind::new(c); + if prev != CharKind::Space && prev != c { + return self.move_(-(i as isize)); + } + prev = c; + } + + if prev != CharKind::Space { + return self.move_(-(self.value.len() as isize)); + } + false + } + + pub fn forward(&mut self, end: bool) -> bool { + if self.value.is_empty() { + return self.handle_op(false); + } + + let mut it = self.value.chars().skip(self.cursor).enumerate(); + let mut prev = CharKind::new(it.next().unwrap().1); + for (i, c) in it { + let c = CharKind::new(c); + let b = if end { + prev != CharKind::Space && prev != c && i != 1 + } else { + c != CharKind::Space && c != prev + }; + if b { + return self.move_(if end { i - 1 } else { i } as isize); + } + prev = c; + } + + self.move_(self.value.len() as isize) + } + + pub fn type_(&mut self, c: char) -> bool { + if self.cursor < 1 { + self.value.insert(0, c); + } else if self.cursor == self.count() { + self.value.push(c); + } else { + self.value.insert(self.idx(self.cursor).unwrap(), c); + } + self.move_(1) + } + + pub fn backspace(&mut self) -> bool { + if self.cursor < 1 { + return false; + } else if self.cursor == self.count() { + self.value.pop(); + } else { + self.value.remove(self.idx(self.cursor - 1).unwrap()); + } + self.move_(-1) + } + + pub fn delete(&mut self, insert: bool) -> bool { + match self.op { + InputOp::None => { + self.op = InputOp::Delete(insert); + if self.range.is_some() { + return self.handle_op(true); + } + + self.range = Some((self.cursor, self.cursor)); + false + } + InputOp::Delete(_) => { + self.move_(-(self.value.len() as isize)); + self.value.clear(); + + self.op = InputOp::None; + self.range = None; + + self.mode = if insert { InputMode::Insert } else { InputMode::Normal }; + true + } + _ => false, + } + } + + fn handle_op(&mut self, include: bool) -> bool { + if let Some(ref mut range) = self.range { + *range = (range.0.min(self.cursor), range.0.max(self.cursor)); + } + + match self.op { + InputOp::None => return false, + InputOp::Delete(insert) => { + let range = self.range.take().unwrap(); + if !self.value.is_empty() { + let (start, end) = (self.idx(range.0), self.idx(range.1 + include as usize)); + self.value.drain(start.unwrap()..end.unwrap()); + } + self.mode = if insert { + self.cursor = range.0.min(self.count()); + InputMode::Insert + } else { + self.cursor = range.0.min(self.count().saturating_sub(1)); + InputMode::Normal + }; + } + InputOp::Yank => {} + } + + self.op = InputOp::None; + true + } +} + +impl Input { + #[inline] + pub fn title(&self) -> String { self.title.clone() } + + #[inline] + pub fn value(&self) -> String { self.value.clone() } + + #[inline] + pub fn area(&self) -> Rect { + Rect { x: self.position.0, y: self.position.1 + 2, width: 50, height: 3 } + } + + #[inline] + pub fn mode(&self) -> InputMode { self.mode } + + #[inline] + pub fn cursor(&self) -> (u16, u16) { + let width = self + .value + .chars() + .enumerate() + .take_while(|(i, _)| *i < self.cursor) + .map(|(_, c)| c) + .collect::() + .width() as u16; + + let area = self.area(); + (area.x + width + 1, area.y + 1) + } + + #[inline] + pub fn top_position() -> (u16, u16) { ((tty_size().ws_col / 2).saturating_sub(25), 2) } + + #[inline] + fn count(&self) -> usize { self.value.chars().count() } + + #[inline] + fn idx(&self, n: usize) -> Option { + self + .value + .char_indices() + .nth(n) + .map(|(i, _)| i) + .or_else(|| if n == self.count() { Some(self.value.len()) } else { None }) + } +} + +impl Default for Input { + fn default() -> Self { + Self { + title: "".to_string(), + value: "".to_string(), + position: Default::default(), + + op: Default::default(), + mode: Default::default(), + cursor: 0, + offset: 0, + range: None, + + visible: false, + callback: None, + } + } +} diff --git a/src/core/input/mod.rs b/src/core/input/mod.rs new file mode 100644 index 00000000..3c90976f --- /dev/null +++ b/src/core/input/mod.rs @@ -0,0 +1,3 @@ +mod input; + +pub use input::*; diff --git a/src/core/manager/folder.rs b/src/core/manager/folder.rs new file mode 100644 index 00000000..6adf10ae --- /dev/null +++ b/src/core/manager/folder.rs @@ -0,0 +1,254 @@ +use std::{fs::Metadata, path::{Path, PathBuf}, usize}; + +use indexmap::{map::Slice, IndexMap}; +use ratatui::layout::Rect; +use tokio::fs; + +use super::{ALL_RATIO, CURRENT_RATIO, DIR_PADDING, PARENT_RATIO}; +use crate::{config::{manager::SortBy, MANAGER}, emit, misc::tty_size}; + +#[derive(Default)] +pub struct Folder { + pub cwd: PathBuf, + items: IndexMap, + offset: usize, + cursor: usize, + + sort: FolderSort, + show_hidden: bool, +} + +#[derive(Clone)] +pub struct FolderItem { + pub name: String, + pub path: PathBuf, + pub meta: Metadata, + pub length: Option, + pub is_link: bool, + pub is_hidden: bool, + pub is_selected: bool, +} + +struct FolderSort { + pub by: SortBy, + pub reverse: bool, +} + +impl Default for FolderSort { + fn default() -> Self { Self { by: MANAGER.sort_by, reverse: MANAGER.sort_reverse } } +} + +impl Folder { + pub fn new(cwd: &Path) -> Self { + Self { cwd: cwd.to_path_buf(), show_hidden: MANAGER.show_hidden, ..Default::default() } + } + + #[inline] + pub fn limit() -> usize { tty_size().ws_row.saturating_sub(DIR_PADDING) as usize } + + pub async fn read(path: &Path) { + let mut iter = match fs::read_dir(path).await { + Ok(it) => it, + Err(_) => return, + }; + + let mut items = IndexMap::new(); + while let Ok(Some(item)) = iter.next_entry().await { + let mut meta = if let Ok(meta) = item.metadata().await { meta } else { continue }; + let is_link = meta.is_symlink(); + if is_link { + meta = fs::metadata(&path).await.unwrap_or(meta); + } + + let path = item.path(); + let name = item.file_name().to_string_lossy().to_string(); + + let length = if meta.is_dir() { None } else { Some(meta.len()) }; + let is_hidden = name.starts_with('.'); + + items.insert(path.clone(), FolderItem { + name, + path, + meta, + length, + is_link, + is_hidden, + is_selected: false, + }); + } + emit!(Files(path.to_path_buf(), items)); + } + + pub fn sort(&mut self) { + fn cmp(a: T, b: T, reverse: bool) -> std::cmp::Ordering { + if reverse { b.cmp(&a) } else { a.cmp(&b) } + } + + let reverse = self.sort.reverse; + match self.sort.by { + SortBy::Alphabetical => self.items.sort_by(|_, a, _, b| cmp(&a.name, &b.name, reverse)), + SortBy::Created => self.items.sort_by(|_, a, _, b| { + if let (Ok(a), Ok(b)) = (a.meta.created(), b.meta.created()) { + return cmp(a, b, reverse); + } + std::cmp::Ordering::Equal + }), + SortBy::Modified => self.items.sort_by(|_, a, _, b| { + if let (Ok(a), Ok(b)) = (a.meta.modified(), b.meta.modified()) { + return cmp(a, b, reverse); + } + std::cmp::Ordering::Equal + }), + SortBy::Size => { + self.items.sort_by(|_, a, _, b| cmp(a.length.unwrap_or(0), b.length.unwrap_or(0), reverse)) + } + } + } + + pub fn update(&mut self, mut items: IndexMap) -> bool { + if !self.show_hidden { + items.retain(|_, item| !item.is_hidden); + } + + for (path, item) in &mut items { + if let Some(old) = self.items.get(path) { + item.length = old.length; + item.is_selected = old.is_selected; + } + } + + let len = items.len(); + self.items = items; + self.cursor = self.cursor.min(len.saturating_sub(1)); + self.offset = self.offset.min(len); + self.sort(); + true + } + + pub fn next(&mut self, step: usize) -> bool { + let len = self.items.len(); + if len == 0 { + return false; + } + + let old = self.cursor; + self.cursor = (self.cursor + step).min(len - 1); + + let limit = Self::limit(); + if self.cursor >= (self.offset + limit).min(len).saturating_sub(5) { + self.offset = len.saturating_sub(limit).min(self.offset + self.cursor - old); + } + + old != self.cursor + } + + pub fn prev(&mut self, step: usize) -> bool { + let old = self.cursor; + self.cursor = self.cursor.saturating_sub(step); + + if self.cursor < self.offset + 5 { + self.offset = self.offset.saturating_sub(old - self.cursor); + } + + old != self.cursor + } + + pub fn hidden(&mut self, show: Option) -> bool { + if show.is_none() || self.show_hidden != show.unwrap() { + self.show_hidden = !self.show_hidden; + emit!(Refresh); + } + + false + } + + pub fn paginate(&self) -> &Slice { + let end = (self.offset + Self::limit()).min(self.items.len()); + self.items.get_range(self.offset..end).unwrap() + } + + pub fn select(&mut self, idx: Option, state: Option) -> bool { + let len = self.items.len(); + let mut apply = |idx: usize, state: Option| -> bool { + if state.is_none() { + self.items[idx].is_selected = !self.items[idx].is_selected; + return true; + } + + let state = state.unwrap(); + if state != self.items[idx].is_selected { + self.items[idx].is_selected = state; + return true; + } + + false + }; + + if let Some(idx) = idx { + if idx < len { + return apply(idx, state); + } + } else { + let mut applied = false; + for i in 0..len { + if apply(i, state) { + applied = true; + } + } + return applied; + } + + false + } + + pub fn selected(&self) -> Option> { + let v = self + .items + .iter() + .filter(|(_, item)| item.is_selected) + .map(|(path, _)| path.clone()) + .collect::>(); + + if v.is_empty() { None } else { Some(v) } + } + + pub fn hover(&mut self, path: &Path) -> bool { + if self.hovered().map(|h| h.path.as_path()) == Some(path) { + return false; + } + + let new = self.position(path).unwrap_or(self.cursor); + if new > self.cursor { self.next(new - self.cursor) } else { self.prev(self.cursor - new) } + } +} + +impl Folder { + #[inline] + pub fn hovered(&self) -> Option<&FolderItem> { + self.items.get_index(self.cursor).map(|(_, item)| item) + } + + #[inline] + pub fn cursor(&self) -> usize { self.cursor } + + #[inline] + pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } + + #[inline] + pub fn position(&self, path: &Path) -> Option { + self.items.iter().position(|(p, _)| p == path) + } + + #[inline] + pub fn rect_current(&self, path: &Path) -> Option { + let pos = self.position(path)? - self.offset; + let s = tty_size(); + + Some(Rect { + x: (s.ws_col as u32 * PARENT_RATIO / ALL_RATIO) as u16, + y: pos as u16, + width: (s.ws_col as u32 * CURRENT_RATIO / ALL_RATIO) as u16, + height: 1, + }) + } +} diff --git a/src/core/manager/manager.rs b/src/core/manager/manager.rs new file mode 100644 index 00000000..c682bb9d --- /dev/null +++ b/src/core/manager/manager.rs @@ -0,0 +1,252 @@ +use std::{collections::{BTreeSet, HashMap, HashSet}, path::PathBuf}; + +use indexmap::IndexMap; +use ratatui::layout::Rect; +use tokio::fs; +use tracing::trace; + +use super::{FolderItem, PreviewData, Tab, Tabs, Watcher}; +use crate::{core::{Folder, Input, InputOpt, Precache}, emit}; + +pub struct Manager { + tabs: Tabs, + yanked: (bool, HashSet), + + watcher: Watcher, + mimetype: HashMap, +} + +impl Manager { + pub fn new() -> Self { + Self { + tabs: Tabs::new(), + yanked: Default::default(), + + watcher: Watcher::init(), + mimetype: Default::default(), + } + } + + pub fn refresh(&mut self) { + self.watcher.trigger(&self.current().cwd); + if let Some(p) = self.parent() { + self.watcher.trigger(&p.cwd); + } + emit!(Hover); + + let mut to_watch = BTreeSet::new(); + for tab in self.tabs.iter() { + to_watch.insert(tab.current.cwd.clone()); + if let Some(ref p) = tab.parent { + to_watch.insert(p.cwd.clone()); + } + if let Some(ref h) = tab.current.hovered() { + to_watch.insert(h.path.clone()); + } + } + self.watcher.watch(to_watch); + } + + pub fn preview(&mut self) -> bool { + let hovered = if let Some(h) = self.hovered() { + h.clone() + } else { + return self.active_mut().preview.reset(); + }; + + if hovered.meta.is_dir() { + self.active_mut().preview.go(&hovered.path, "inode/directory"); + if self.active().history(&hovered.path).is_some() { + emit!(Preview(hovered.path, PreviewData::Folder)); + } + } else if let Some(mime) = self.mimetype.get(&hovered.path).cloned() { + self.active_mut().preview.go(&hovered.path, &mime); + } else { + tokio::spawn(async move { + if let Ok(mime) = Precache::mimetype(&vec![hovered.path.clone()]).await { + if let Some(Some(mime)) = mime.first() { + emit!(Mimetype(hovered.path, mime.clone())); + } + } + }); + } + false + } + + pub fn close(&mut self) -> bool { + if self.tabs.len() > 1 { + return self.tabs.close(self.tabs.idx()); + } + + emit!(Quit); + return false; + } + + pub fn yank(&mut self, cut: bool) -> bool { + self.yanked.0 = cut; + self.yanked.1.clear(); + self.yanked.1.extend(self.selected()); + false + } + + #[inline] + pub fn yanked(&self) -> &(bool, HashSet) { &self.yanked } + + pub fn create(&self) -> bool { + let pos = Input::top_position(); + let cwd = self.current().cwd.clone(); + + tokio::spawn(async move { + let result = emit!(Input(InputOpt { + title: "Create:".to_string(), + value: "".to_string(), + position: pos, + })) + .await; + + if let Ok(name) = result { + let path = cwd.join(&name); + if name.ends_with('/') { + fs::create_dir_all(path).await.ok(); + } else { + fs::create_dir_all(path.parent().unwrap()).await.ok(); + fs::File::create(path).await.ok(); + } + } + }); + + false + } + + pub fn rename(&self) -> bool { + let selected = self.selected(); + if selected.is_empty() { + return false; + } + + if selected.len() > 1 { + return self.bulk_rename(); + } + + let rect = self.current().rect_current(&selected[0]).unwrap(); + tokio::spawn(async move { + let result = emit!(Input(InputOpt { + title: "Rename:".to_string(), + value: selected[0].file_name().unwrap().to_string_lossy().to_string(), + position: (rect.x, rect.y), + })) + .await; + + if let Ok(new) = result { + let to = selected[0].parent().unwrap().join(new); + fs::rename(&selected[0], to).await.ok(); + } + }); + + false + } + + fn bulk_rename(&self) -> bool { false } + + pub fn selected(&self) -> Vec { + self + .current() + .selected() + .or_else(|| self.hovered().map(|h| vec![h.path.clone()])) + .unwrap_or_default() + } + + pub async fn mimetype(&mut self, files: &Vec) -> Vec> { + let todo = + files.iter().filter(|&p| !self.mimetype.contains_key(p)).cloned().collect::>(); + if let Ok(mime) = Precache::mimetype(&todo).await { + let mut it = todo.iter().zip(mime); + while let Some((p, Some(m))) = it.next() { + self.mimetype.insert(p.clone(), m); + } + } + + files.into_iter().map(|p| self.mimetype.get(p).cloned()).collect() + } + + pub fn update_files(&mut self, path: PathBuf, items: IndexMap) -> bool { + let cwd = self.current().cwd.clone(); + let hovered = self.hovered().map(|h| h.path.clone()); + + let mut b = if self.current().cwd == path { + self.current_mut().update(items) + } else if matches!(self.parent(), Some(p) if p.cwd == path) { + self.active_mut().parent.as_mut().unwrap().update(items) + } else { + self + .active_mut() + .history + .entry(path.clone()) + .or_insert_with(|| Folder::new(&path)) + .update(items); + + matches!(self.hovered(), Some(h) if h.path == path) + }; + + b |= self.active_mut().parent.as_mut().map_or(false, |p| p.hover(&cwd)); + b |= hovered.as_ref().map_or(false, |h| self.current_mut().hover(h)); + + if hovered != self.hovered().map(|h| h.path.clone()) { + emit!(Hover); + } + b + } + + pub fn update_mimetype(&mut self, path: PathBuf, mimetype: String) -> bool { + if matches!(self.mimetype.get(&path), Some(m) if m == &mimetype) { + return false; + } + + self.mimetype.insert(path, mimetype); + self.preview(); + true + } + + pub fn update_preview(&mut self, path: PathBuf, data: PreviewData) -> bool { + let hovered = if let Some(h) = self.current().hovered() { + h.path.clone() + } else { + return self.active_mut().preview.reset(); + }; + + if hovered != path { + return false; + } + + let preview = &mut self.active_mut().preview; + preview.path = path; + preview.data = data; + true + } +} + +impl Manager { + #[inline] + pub fn tabs(&self) -> &Tabs { &self.tabs } + + #[inline] + pub fn tabs_mut(&mut self) -> &mut Tabs { &mut self.tabs } + + #[inline] + pub fn active(&self) -> &Tab { self.tabs.active() } + + #[inline] + pub fn active_mut(&mut self) -> &mut Tab { self.tabs.active_mut() } + + #[inline] + pub fn current(&self) -> &Folder { &self.tabs.active().current } + + #[inline] + pub fn current_mut(&mut self) -> &mut Folder { &mut self.tabs.active_mut().current } + + #[inline] + pub fn parent(&self) -> &Option { &self.tabs.active().parent } + + #[inline] + pub fn hovered(&self) -> Option<&FolderItem> { self.tabs.active().current.hovered() } +} diff --git a/src/core/manager/mod.rs b/src/core/manager/mod.rs new file mode 100644 index 00000000..e0074f07 --- /dev/null +++ b/src/core/manager/mod.rs @@ -0,0 +1,25 @@ +mod folder; +mod manager; +mod mode; +mod preview; +mod tab; +mod tabs; +mod watcher; + +pub use folder::*; +pub use manager::*; +pub use mode::*; +pub use preview::*; +pub use tab::*; +pub use tabs::*; +pub use watcher::*; + +pub const PARENT_RATIO: u32 = 1; +pub const CURRENT_RATIO: u32 = 4; +pub const PREVIEW_RATIO: u32 = 3; +pub const ALL_RATIO: u32 = PARENT_RATIO + CURRENT_RATIO + PREVIEW_RATIO; + +pub const DIR_PADDING: u16 = 2; + +pub const PREVIEW_BORDER: u16 = 2; +pub const PREVIEW_PADDING: u16 = 2; diff --git a/src/core/manager/mode.rs b/src/core/manager/mode.rs new file mode 100644 index 00000000..556217be --- /dev/null +++ b/src/core/manager/mode.rs @@ -0,0 +1,41 @@ +use std::fmt::Display; + +use crate::config::{theme, THEME}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + #[default] + Normal, + Select(usize), + Unselect(usize), +} + +impl Mode { + #[inline] + pub fn color(&self) -> &theme::Color { + match *self { + Mode::Normal => &THEME.mode.normal, + Mode::Select(_) => &THEME.mode.select, + Mode::Unselect(_) => &THEME.mode.unselect, + } + } + + #[inline] + pub fn start(&self) -> Option { + match self { + Mode::Normal => None, + Mode::Select(n) => Some(*n), + Mode::Unselect(n) => Some(*n), + } + } +} + +impl Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Mode::Normal => write!(f, "NORMAL"), + Mode::Select(_) => write!(f, "SELECT"), + Mode::Unselect(_) => write!(f, "UN-SEL"), + } + } +} diff --git a/src/core/manager/preview.rs b/src/core/manager/preview.rs new file mode 100644 index 00000000..96b3747d --- /dev/null +++ b/src/core/manager/preview.rs @@ -0,0 +1,157 @@ +use std::{fs::File, io::BufReader, path::{Path, PathBuf}, sync::OnceLock}; + +use adapter::kitty::Kitty; +use anyhow::{anyhow, Context, Result}; +use image::imageops::FilterType; +use ratatui::layout::Rect; +use syntect::{easy::HighlightLines, highlighting::{Theme, ThemeSet}, parsing::SyntaxSet, util::as_24_bit_terminal_escaped}; +use tokio::{fs, task::JoinHandle}; + +use super::{Folder, ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO}; +use crate::{config::{PREVIEW, THEME}, core::{adapter, Precache}, emit, misc::{first_n_lines, tty_ratio, tty_size}}; + +static SYNTECT_SYNTAX: OnceLock = OnceLock::new(); +static SYNTECT_THEME: OnceLock = OnceLock::new(); + +#[derive(Debug)] +pub struct Preview { + pub path: PathBuf, + pub data: PreviewData, + handle: Option>, +} + +#[derive(Debug, Default)] +pub enum PreviewData { + #[default] + None, + Folder, + Text(String), + Image(Vec), +} + +impl Preview { + pub fn new() -> Self { + Self { path: Default::default(), data: Default::default(), handle: Default::default() } + } + + fn size() -> (u16, u16) { + let s = tty_size(); + let col = (s.ws_col as u32 * PREVIEW_RATIO / ALL_RATIO) as u16; + (col.saturating_sub(PREVIEW_BORDER), s.ws_row.saturating_sub(PREVIEW_PADDING)) + } + + pub fn go(&mut self, path: &Path, mime: &str) { + if let Some(handle) = self.handle.take() { + handle.abort(); + } + + let (path, mime) = (path.to_path_buf(), mime.to_string()); + self.handle = Some(tokio::spawn(async move { + let result = if mime == "inode/directory" { + Self::folder(&path).await + } else if mime == "application/json" { + Self::json(&path).await.map(PreviewData::Text) + } else if mime.starts_with("text/") || mime.ends_with("/xml") { + Self::highlight(&path).await.map(PreviewData::Text) + } else if mime.starts_with("image/") { + Self::image(&path).await.map(PreviewData::Image) + } else if mime.starts_with("video/") { + Self::video(&path).await.map(PreviewData::Image) + } else { + Err(anyhow!("Unsupported mimetype: {}", mime)) + }; + + emit!(Preview(path, result.unwrap_or_default())); + })); + } + + pub fn reset(&mut self) -> bool { + if self.path == PathBuf::default() { + return false; + } + + self.path = Default::default(); + self.data = Default::default(); + true + } + + pub async fn folder(path: &Path) -> Result { + Folder::read(&path).await; + Ok(PreviewData::Folder) + } + + pub async fn image(mut path: &Path) -> Result> { + let cache = Precache::cache(path); + if cache.exists() { + path = cache.as_path(); + } + + let (w, h) = { + let r = tty_ratio(); + let (w, h) = Self::size(); + let (w, h) = ((w as f64 * r.0) as u32, (h as f64 * r.1) as u32); + (w.min(PREVIEW.max_width), h.min(PREVIEW.max_height)) + }; + + let file = fs::read(path).await?; + tokio::task::spawn_blocking(move || -> Result> { + let img = image::load_from_memory(&file)?; + Kitty::image_show(if img.width() > w || img.height() > h { + img.resize(w, h, FilterType::Triangle) + } else { + img + }) + }) + .await? + } + + pub async fn video(path: &Path) -> Result> { + Precache::video(path).await?; + + let cache = Precache::cache(path); + Self::image(&cache).await + } + + pub async fn json(path: &Path) -> Result { + Ok( + Precache::json(path) + .await? + .lines() + .take(Self::size().1 as usize) + .collect::>() + .join("\n"), + ) + } + + pub async fn highlight(path: &Path) -> Result { + let syntax = SYNTECT_SYNTAX.get_or_init(|| SyntaxSet::load_defaults_newlines()); + + let theme = SYNTECT_THEME.get_or_init(|| { + let from_file = || -> Result { + let file = File::open(&THEME.syntect.theme)?; + Ok(ThemeSet::load_from_reader(&mut BufReader::new(file))?) + }; + from_file().unwrap_or_else(|_| ThemeSet::load_defaults().themes["base16-ocean.dark"].clone()) + }); + + let ext = path.extension().context("no extension found")?.to_string_lossy().to_string(); + + let lines = first_n_lines(path, Self::size().1 as usize).await?; + + tokio::task::spawn_blocking(move || -> Result { + let mut buf = "".to_string(); + if let Some(syn) = syntax.find_syntax_by_extension(&ext) { + let mut h = HighlightLines::new(syn, theme); + let tab = " ".repeat(PREVIEW.tab_size as usize); + for line in lines { + let line = line.replace('\t', &tab); + let ranges = h.highlight_line(&line, &syntax)?; + buf.push_str(&as_24_bit_terminal_escaped(&ranges, false)); + buf.push('\n'); + } + } + Ok(buf) + }) + .await? + } +} diff --git a/src/core/manager/tab.rs b/src/core/manager/tab.rs new file mode 100644 index 00000000..84f01210 --- /dev/null +++ b/src/core/manager/tab.rs @@ -0,0 +1,144 @@ +use std::{collections::BTreeMap, mem, path::{Path, PathBuf}}; + +use super::{Folder, Mode, Preview}; +use crate::emit; + +pub struct Tab { + pub(super) current: Folder, + pub(super) parent: Option, + + pub(super) mode: Mode, + + pub(super) history: BTreeMap, + pub(super) preview: Preview, +} + +impl Tab { + pub fn new(path: &Path) -> Self { + Self { + current: Folder::new(path), + parent: path.parent().map(|p| Folder::new(p)), + + mode: Default::default(), + + history: Default::default(), + preview: Preview::new(), + } + } + + pub fn escape(&mut self) -> bool { + if matches!(self.mode, Mode::Select(_) | Mode::Unselect(_)) { + self.mode = Mode::Normal; + return true; + } + + self.select_all(Some(false)) + } + + pub fn arrow(&mut self, step: isize) -> bool { + let before = self.current.cursor(); + let ok = if step > 0 { + self.current.next(step as usize) + } else { + self.current.prev(step.abs() as usize) + }; + if !ok { + return false; + } + + // Visual selection + if let Some(start) = self.mode.start() { + let after = self.current.cursor(); + if (after > before && before < start) || (after < before && before > start) { + for i in before.min(start)..=start.max(before) { + self.current.select(Some(i), Some(false)); + } + } + for i in start.min(after)..=after.max(start) { + self.current.select(Some(i), Some(true)); + } + } + + emit!(Hover); + true + } + + pub fn enter(&mut self) -> bool { + let hovered = if let Some(h) = self.current.hovered() { + h.clone() + } else { + return false; + }; + if !hovered.meta.is_dir() { + emit!(Open(self.current.selected().unwrap_or(vec![hovered.path]))); + return false; + } + + let current = self.history.remove(&hovered.path).unwrap_or_else(|| Folder::new(&hovered.path)); + let parent = mem::replace(&mut self.current, current); + + if self.parent.is_none() { + self.parent = Some(parent); + } else { + let cwd = self.parent.as_ref().unwrap().cwd.clone(); + let pparent = mem::replace(self.parent.as_mut().unwrap(), parent); + self.history.insert(cwd, pparent); + } + + emit!(Refresh); + true + } + + pub fn leave(&mut self) -> bool { + let parent = if let Some(p) = &self.parent { + p.cwd.clone() + } else { + return false; + }; + + let pparent = parent.parent().map(|p| self.history.remove(p).unwrap_or_else(|| Folder::new(p))); + + let cwd = self.current.cwd.clone(); + let parent = mem::replace(&mut self.parent, pparent).unwrap(); + let current = mem::replace(&mut self.current, parent); + self.history.insert(cwd, current); + + emit!(Refresh); + true + } + + pub fn back(&mut self) -> bool { todo!() } + + pub fn forward(&mut self) -> bool { todo!() } + + pub fn select(&mut self, state: Option) -> bool { + let idx = Some(self.current.cursor()); + self.current.select(idx, state) + } + + pub fn select_all(&mut self, state: Option) -> bool { self.current.select(None, state) } + + pub fn visual_mode(&mut self, unsel: bool) -> bool { + let idx = self.current.cursor(); + + if unsel { + self.mode = Mode::Unselect(idx); + self.current.select(Some(idx), Some(false)); + } else { + self.mode = Mode::Select(idx); + self.current.select(Some(idx), Some(true)); + }; + true + } +} + +impl Tab { + #[inline] + pub fn mode(&self) -> &Mode { &self.mode } + + #[inline] + pub fn history(&self, path: &Path) -> Option<&Folder> { self.history.get(path) } + + #[inline] + pub fn preview(&self) -> &Preview { &self.preview } +} diff --git a/src/core/manager/tabs.rs b/src/core/manager/tabs.rs new file mode 100644 index 00000000..aefc4bd6 --- /dev/null +++ b/src/core/manager/tabs.rs @@ -0,0 +1,98 @@ +use std::path::Path; + +use super::Tab; +use crate::{config::MANAGER, emit}; + +const MAX_TABS: usize = 9; + +pub struct Tabs { + idx: usize, + items: Vec, +} + +impl Tabs { + pub fn new() -> Self { + let tabs = Self { idx: 0, items: vec![Tab::new(&MANAGER.cwd)] }; + + emit!(Refresh); + tabs + } + + pub fn create(&mut self, path: &Path) -> bool { + if self.items.len() >= MAX_TABS { + return false; + } + + self.items.insert(self.idx + 1, Tab::new(path)); + self.set_idx(self.idx + 1); + true + } + + pub fn switch(&mut self, idx: isize, rel: bool) -> bool { + let idx = if rel { self.absolute(idx) } else { idx as usize }; + + if idx == self.idx || idx >= self.items.len() { + return false; + } + + self.set_idx(idx); + true + } + + pub fn swap(&mut self, rel: isize) -> bool { + let idx = self.absolute(rel); + if idx == self.idx { + return false; + } + + self.items.swap(self.idx, idx); + self.set_idx(idx); + true + } + + pub fn close(&mut self, idx: usize) -> bool { + let len = self.items.len(); + if len <= 1 || idx as usize >= len { + return false; + } + + self.items.remove(idx); + if idx == self.idx { + self.set_idx(self.absolute(1)); + } + + true + } + + #[inline] + fn absolute(&self, rel: isize) -> usize { + if rel > 0 { + (self.idx + rel as usize).min(self.items.len() - 1) + } else { + self.idx.saturating_sub(rel.abs() as usize) + } + } + + #[inline] + fn set_idx(&mut self, idx: usize) { + self.idx = idx; + emit!(Refresh); + } +} + +impl Tabs { + #[inline] + pub fn idx(&self) -> usize { self.idx } + + #[inline] + pub fn len(&self) -> usize { self.items.len() } + + #[inline] + pub fn iter(&self) -> impl Iterator { self.items.iter() } + + #[inline] + pub fn active(&self) -> &Tab { &self.items[self.idx] } + + #[inline] + pub(super) fn active_mut(&mut self) -> &mut Tab { &mut self.items[self.idx] } +} diff --git a/src/core/manager/watcher.rs b/src/core/manager/watcher.rs new file mode 100644 index 00000000..16249c97 --- /dev/null +++ b/src/core/manager/watcher.rs @@ -0,0 +1,81 @@ +use std::{collections::BTreeSet, path::{Path, PathBuf}}; + +use notify::{RecommendedWatcher, Watcher as _Watcher}; +use tokio::sync::mpsc::{self, Sender}; + +use super::Folder; +use crate::emit; + +pub struct Watcher { + tx: Sender, + + watcher: RecommendedWatcher, + watched: BTreeSet, +} + +impl Watcher { + pub fn init() -> Self { + let (watcher, tx) = Self::start(); + + Self { tx, watcher, watched: Default::default() } + } + + fn start() -> (RecommendedWatcher, Sender) { + let (tx, mut rx) = mpsc::channel(50); + + let watcher = RecommendedWatcher::new( + { + let tx = tx.clone(); + move |res: Result| { + if res.is_err() { + return; + } + + let event = res.unwrap(); + match event.kind { + notify::EventKind::Create(_) => {} + notify::EventKind::Modify(_) => {} + notify::EventKind::Remove(_) => {} + _ => return, + } + + let path = if event.paths.len() > 0 { + event.paths[0].parent().unwrap_or(&event.paths[0]) + } else { + return; + }; + + tx.blocking_send(path.to_path_buf()).ok(); + } + }, + notify::Config::default(), + ) + .unwrap(); + + tokio::spawn(async move { + while let Some(path) = rx.recv().await { + Folder::read(&path).await; + } + }); + + (watcher, tx) + } + + pub(super) fn watch(&mut self, to_watch: BTreeSet) { + for p in to_watch.difference(&self.watched) { + self.watcher.watch(&p, notify::RecursiveMode::NonRecursive).ok(); + } + for p in self.watched.difference(&to_watch) { + self.watcher.unwatch(p).ok(); + } + self.watched = to_watch; + } + + pub(super) fn trigger(&self, path: &Path) { + let tx = self.tx.clone(); + let path = path.to_path_buf(); + tokio::spawn(async move { + tx.send(path).await.ok(); + }); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 00000000..aeb27f44 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,11 @@ +mod adapter; +mod event; +mod input; +mod manager; +mod tasks; + +pub use adapter::*; +pub use event::*; +pub use input::*; +pub use manager::*; +pub use tasks::*; diff --git a/src/core/tasks/file.rs b/src/core/tasks/file.rs new file mode 100644 index 00000000..9f597494 --- /dev/null +++ b/src/core/tasks/file.rs @@ -0,0 +1,315 @@ +use std::{collections::VecDeque, fs::Metadata, path::{Path, PathBuf}}; + +use anyhow::Result; +use futures::{future::BoxFuture, FutureExt}; +use tokio::{fs, io::{self, ErrorKind::{AlreadyExists, NotFound}}, sync::mpsc}; +use tracing::{info, trace}; +use trash::{macos::{DeleteMethod, TrashContextExtMacos}, TrashContext}; + +use super::TaskOp; +use crate::misc::{calculate_size, copy_with_progress}; + +pub(super) struct File { + rx: async_channel::Receiver, + tx: async_channel::Sender, + + sch: mpsc::UnboundedSender, +} + +#[derive(Debug)] +pub(super) enum FileOp { + Paste(FileOpPaste), + Link(FileOpLink), + Delete(FileOpDelete), + Trash(FileOpTrash), +} + +#[derive(Clone, Debug)] +pub(super) struct FileOpPaste { + pub id: usize, + pub from: PathBuf, + pub to: PathBuf, + pub cut: bool, + pub follow: bool, + pub retry: u8, +} + +#[derive(Clone, Debug)] +pub(super) struct FileOpLink { + pub id: usize, + pub from: PathBuf, + pub to: PathBuf, + pub cut: bool, + pub length: u64, +} + +#[derive(Clone, Debug)] +pub(super) struct FileOpDelete { + pub id: usize, + pub target: PathBuf, + pub length: u64, +} + +#[derive(Clone, Debug)] +pub(super) struct FileOpTrash { + pub id: usize, + pub target: PathBuf, + pub length: u64, +} + +impl File { + pub(super) fn new(sch: mpsc::UnboundedSender) -> Self { + let (tx, rx) = async_channel::unbounded(); + Self { tx, rx, sch } + } + + #[inline] + pub(super) async fn recv(&self) -> Result<(usize, FileOp)> { + Ok(match self.rx.recv().await? { + FileOp::Paste(t) => (t.id, FileOp::Paste(t)), + FileOp::Link(t) => (t.id, FileOp::Link(t)), + FileOp::Delete(t) => (t.id, FileOp::Delete(t)), + FileOp::Trash(t) => (t.id, FileOp::Trash(t)), + }) + } + + pub(super) async fn work(&self, task: &mut FileOp) -> Result<()> { + match task { + FileOp::Paste(task) => { + match fs::remove_file(&task.to).await { + Err(e) if e.kind() != NotFound => Err(e)?, + _ => {} + } + + let mut it = copy_with_progress(&task.from, &task.to); + while let Some(res) = it.recv().await { + match res { + Ok(0) => { + if task.cut { + fs::remove_file(&task.from).await.ok(); + } + break; + } + Ok(n) => { + trace!("Paste task advanced {}: {:?}", n, task); + self.sch.send(TaskOp::Adv(task.id, 0, n))? + } + Err(e) if e.kind() == NotFound => { + trace!("Paste task partially done: {:?}", task); + break; + } + // Operation not permitted (os error 1) + // Attribute not found (os error 93) + Err(e) if task.retry < 3 && matches!(e.raw_os_error(), Some(1) | Some(93)) => { + trace!("Paste task retry: {:?}", task); + task.retry += 1; + return Ok(self.tx.send(FileOp::Paste(task.clone())).await?); + } + Err(e) => Err(e)?, + } + } + self.sch.send(TaskOp::Adv(task.id, 1, 0))?; + } + FileOp::Link(task) => { + let src = match fs::read_link(&task.from).await { + Ok(src) => src, + Err(e) if e.kind() == NotFound => { + trace!("Link task partially done: {:?}", task); + return Ok(self.sch.send(TaskOp::Adv(task.id, 1, task.length))?); + } + Err(e) => Err(e)?, + }; + + match fs::remove_file(&task.to).await { + Err(e) if e.kind() != NotFound => Err(e)?, + _ => fs::symlink(src, &task.to).await?, + } + + if task.cut { + fs::remove_file(&task.from).await.ok(); + } + self.sch.send(TaskOp::Adv(task.id, 1, task.length))?; + } + FileOp::Delete(task) => { + if let Err(e) = fs::remove_file(&task.target).await { + if e.kind() != NotFound && fs::symlink_metadata(&task.target).await.is_ok() { + info!("Delete task failed: {:?}, {}", task, e); + Err(e)? + } + } + self.sch.send(TaskOp::Adv(task.id, 1, task.length))? + } + FileOp::Trash(task) => { + #[cfg(target_os = "macos")] + { + let mut ctx = TrashContext::default(); + ctx.set_delete_method(DeleteMethod::NsFileManager); + ctx.delete(&task.target)?; + } + #[cfg(not(target_os = "macos"))] + { + trash::delete(&task.target)?; + } + self.sch.send(TaskOp::Adv(task.id, 1, task.length))?; + } + } + Ok(()) + } + + fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } + + pub(super) async fn paste(&self, mut task: FileOpPaste) -> Result<()> { + if task.cut { + match fs::rename(&task.from, &task.to).await { + Ok(_) => return self.done(task.id), + Err(e) if e.kind() == NotFound => return self.done(task.id), + _ => {} + } + } + + let meta = Self::metadata(&task.from, task.follow).await?; + if !meta.is_dir() { + let id = task.id; + self.sch.send(TaskOp::New(id, meta.len()))?; + + if meta.is_file() { + self.tx.send(FileOp::Paste(task)).await?; + } else if meta.is_symlink() { + self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?; + } + return self.done(id); + } + + let root = task.to.clone(); + let skip = task.from.components().count(); + let mut dirs = VecDeque::from([task.from]); + + while let Some(src) = dirs.pop_front() { + let dest = root.join(src.components().skip(skip).collect::()); + match fs::create_dir(&dest).await { + Err(e) if e.kind() != AlreadyExists => { + info!("Create dir failed: {:?}, {}", dest, e); + continue; + } + _ => {} + } + + let mut it = match fs::read_dir(&src).await { + Ok(it) => it, + Err(e) => { + info!("Read dir failed: {:?}, {}", src, e); + continue; + } + }; + + while let Ok(Some(entry)) = it.next_entry().await { + let src = entry.path(); + let meta = if let Ok(meta) = Self::metadata(&src, task.follow).await { + meta + } else { + continue; + }; + + if meta.is_dir() { + dirs.push_back(src); + continue; + } + + task.to = dest.join(src.file_name().unwrap()); + task.from = src; + self.sch.send(TaskOp::New(task.id, meta.len()))?; + + if meta.is_file() { + trace!("Paste: {:?} -> {:?}", task.from, task.to); + self.tx.send(FileOp::Paste(task.clone())).await?; + } else if meta.is_symlink() { + trace!("Link: {:?} -> {:?}", task.from, task.to); + self.tx.send(FileOp::Link(task.to_link(meta.len()))).await?; + } + } + } + self.done(task.id) + } + + pub(super) async fn delete(&self, mut task: FileOpDelete) -> Result<()> { + let meta = fs::symlink_metadata(&task.target).await?; + if !meta.is_dir() { + let id = task.id; + task.length = meta.len(); + self.sch.send(TaskOp::New(id, meta.len()))?; + self.tx.send(FileOp::Delete(task)).await?; + return self.done(id); + } + + let mut dirs = VecDeque::from([task.target]); + while let Some(target) = dirs.pop_front() { + let mut it = match fs::read_dir(target).await { + Ok(it) => it, + Err(_) => continue, + }; + + while let Ok(Some(entry)) = it.next_entry().await { + let meta = match entry.metadata().await { + Ok(m) => m, + Err(_) => continue, + }; + + if meta.is_dir() { + dirs.push_front(entry.path()); + continue; + } + + task.target = entry.path(); + task.length = meta.len(); + self.sch.send(TaskOp::New(task.id, meta.len()))?; + self.tx.send(FileOp::Delete(task.clone())).await?; + } + } + self.done(task.id) + } + + pub(super) async fn trash(&self, mut task: FileOpTrash) -> Result<()> { + let id = task.id; + task.length = calculate_size(&task.target).await; + + self.sch.send(TaskOp::New(id, task.length))?; + self.tx.send(FileOp::Trash(task)).await?; + self.done(id) + } + + async fn metadata(path: &Path, follow: bool) -> io::Result { + if !follow { + return fs::symlink_metadata(path).await; + } + + let meta = fs::metadata(path).await; + if meta.is_ok() { meta } else { fs::symlink_metadata(path).await } + } + + pub(super) fn remove_empty_dirs(dir: &Path) -> BoxFuture<()> { + trace!("Remove empty dirs: {:?}", dir); + async move { + let mut it = match fs::read_dir(dir).await { + Ok(it) => it, + Err(_) => return, + }; + + while let Ok(Some(entry)) = it.next_entry().await { + if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) { + let path = entry.path(); + Self::remove_empty_dirs(&path).await; + fs::remove_dir(path).await.ok(); + } + } + + fs::remove_dir(dir).await.ok(); + } + .boxed() + } +} + +impl FileOpPaste { + fn to_link(&self, length: u64) -> FileOpLink { + FileOpLink { id: self.id, from: self.from.clone(), to: self.to.clone(), cut: self.cut, length } + } +} diff --git a/src/core/tasks/mod.rs b/src/core/tasks/mod.rs new file mode 100644 index 00000000..7afb79f1 --- /dev/null +++ b/src/core/tasks/mod.rs @@ -0,0 +1,14 @@ +mod file; +mod precache; +mod process; +mod scheduler; +mod tasks; + +pub(crate) use file::*; +pub use precache::*; +pub(crate) use process::*; +pub use scheduler::*; +pub use tasks::*; + +pub const TASKS_PADDING: u16 = 2; +pub const TASKS_PERCENT: u16 = 80; diff --git a/src/core/tasks/precache.rs b/src/core/tasks/precache.rs new file mode 100644 index 00000000..8264a389 --- /dev/null +++ b/src/core/tasks/precache.rs @@ -0,0 +1,94 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Result}; +use image::imageops::FilterType; +use tokio::{fs, process::Command}; + +use crate::{config::PREVIEW, misc::valid_mimetype}; + +#[derive(Default)] +pub struct Precache; + +impl Precache { + pub fn cache(path: &Path) -> PathBuf { + PathBuf::from(format!("/tmp/yazi/{:x}", md5::compute(path.to_string_lossy().as_bytes()))) + } + + pub async fn mimetype(files: &Vec) -> Result>> { + if files.is_empty() { + return Ok(vec![]); + } + + let output = Command::new("file") + .args(["-bL", "--mime-type"]) + .args(files) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("failed to get mimetype: {}", String::from_utf8_lossy(&output.stderr)); + } + + Ok( + String::from_utf8_lossy(&output.stdout) + .trim() + .lines() + .map(|s| if valid_mimetype(s) { Some(s.to_string()) } else { None }) + .collect(), + ) + } + + pub async fn json(path: &Path) -> Result { + let output = Command::new("jq") + .args(["-C", "--indent", &PREVIEW.tab_size.to_string(), "."]) + .arg(path) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + bail!("failed to get json: {}", String::from_utf8_lossy(&output.stderr)); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } + + pub async fn image(path: &Path) -> Result<()> { + let cache = Self::cache(path); + if cache.exists() { + return Ok(()); + } + + let img = image::load_from_memory(&fs::read(path).await?)?; + let (w, h) = (PREVIEW.max_width, PREVIEW.max_height); + + let img = if img.width() > w || img.height() > h { + img.resize(w, h, FilterType::Triangle) + } else { + img + }; + Ok(img.save(cache)?) + } + + pub async fn video(path: &Path) -> Result<()> { + let cache = Self::cache(path); + if cache.exists() { + return Ok(()); + } + + let output = Command::new("ffmpegthumbnailer") + .arg("-i") + .arg(path) + .arg("-o") + .arg(cache) + .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)); + } + Ok(()) + } +} diff --git a/src/core/tasks/process.rs b/src/core/tasks/process.rs new file mode 100644 index 00000000..cf2ef74a --- /dev/null +++ b/src/core/tasks/process.rs @@ -0,0 +1,85 @@ +use anyhow::Result; +use tokio::{process::Command, select, sync::{mpsc, oneshot, Semaphore}}; +use tracing::trace; + +use super::TaskOp; +use crate::emit; + +pub(super) struct Process { + rx: async_channel::Receiver, + tx: async_channel::Sender, + + sch: mpsc::UnboundedSender, + + blocker: Semaphore, +} + +#[derive(Debug)] +pub(super) enum ProcessOp { + Open(ProcessOpOpen), +} + +#[derive(Debug)] +pub(super) struct ProcessOpOpen { + pub id: usize, + pub cmd: String, + pub args: Vec, + pub block: bool, + pub cancel: oneshot::Sender<()>, +} + +impl Process { + pub(super) fn new(sch: mpsc::UnboundedSender) -> Self { + let (tx, rx) = async_channel::unbounded(); + Self { tx, rx, sch, blocker: Semaphore::new(1) } + } + + #[inline] + pub(super) async fn recv(&self) -> Result<(usize, ProcessOp)> { + Ok(match self.rx.recv().await? { + ProcessOp::Open(t) => (t.id, ProcessOp::Open(t)), + }) + } + + pub(super) async fn work(&self, task: &mut ProcessOp) -> Result<()> { + match task { + ProcessOp::Open(task) => { + trace!("Open task: {:?}", task); + if !task.block { + select! { + _ = task.cancel.closed() => {}, + Ok(status) = Command::new(&task.cmd).args(&task.args).kill_on_drop(true).status() => { + trace!("{} exited with {:?}", task.cmd, status); + } + } + return Ok(self.sch.send(TaskOp::Adv(task.id, 1, 0))?); + } + + let _guard = self.blocker.acquire().await.unwrap(); + emit!(Stop(true)).await; + + match Command::new(&task.cmd).args(&task.args).kill_on_drop(true).spawn() { + Ok(mut child) => { + child.wait().await.ok(); + } + Err(e) => { + trace!("Failed to spawn {}: {}", task.cmd, e); + } + } + + emit!(Stop(false)).await; + self.sch.send(TaskOp::Adv(task.id, 1, 0))?; + } + } + Ok(()) + } + + fn done(&self, id: usize) -> Result<()> { Ok(self.sch.send(TaskOp::Done(id))?) } + + pub(super) async fn open(&self, task: ProcessOpOpen) -> Result<()> { + let id = task.id; + self.sch.send(TaskOp::New(id, 0))?; + self.tx.send(ProcessOp::Open(task)).await?; + self.done(id) + } +} diff --git a/src/core/tasks/scheduler.rs b/src/core/tasks/scheduler.rs new file mode 100644 index 00000000..73d13199 --- /dev/null +++ b/src/core/tasks/scheduler.rs @@ -0,0 +1,368 @@ +use std::{collections::BTreeMap, path::PathBuf, sync::Arc, time::Duration}; + +use async_channel::{Receiver, Sender}; +use futures::{future::BoxFuture, FutureExt}; +use parking_lot::RwLock; +use tokio::{fs, select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, time::sleep}; +use tracing::{info, trace}; + +use super::{File, FileOpDelete, FileOpPaste, FileOpTrash, Process, ProcessOpOpen, Task, TaskOp, TaskStage}; +use crate::{config::open::Opener, emit, misc::unique_path}; + +#[derive(Default)] +pub(super) struct Running { + incer: usize, + + hooks: BTreeMap BoxFuture<'static, ()>) + Send + Sync>>, + all: BTreeMap, +} + +impl Running { + fn add(&mut self, name: String) -> usize { + self.incer += 1; + self.all.insert(self.incer, Task::new(self.incer, name)); + self.incer + } + + #[inline] + fn get(&mut self, id: usize) -> Option<&mut Task> { self.all.get_mut(&id) } + + #[inline] + pub(super) fn len(&self) -> usize { self.all.len() } + + #[inline] + fn exists(&self, id: usize) -> bool { self.all.contains_key(&id) } + + #[inline] + pub(super) fn values(&self) -> impl Iterator { self.all.values() } + + #[inline] + fn is_empty(&self) -> bool { self.all.is_empty() } + + fn try_remove(&mut self, id: usize, stage: TaskStage) -> Option> { + if let Some(task) = self.get(id) { + if stage > task.stage { + task.stage = stage; + } + + match task.stage { + TaskStage::Pending => return None, + TaskStage::Dispatched => { + if task.processed < task.found { + return None; + } + if let Some(hook) = self.hooks.remove(&id) { + return Some(hook(false)); + } + } + TaskStage::Hooked => {} + } + + self.all.remove(&id); + } + None + } +} + +pub struct Scheduler { + file: Arc, + process: Arc, + + todo: Sender>, + pub(super) running: Arc>, +} + +impl Scheduler { + pub(super) fn start() -> Self { + let (todo_tx, todo_rx) = async_channel::unbounded(); + let (prog_tx, prog_rx) = mpsc::unbounded_channel(); + + let scheduler = Self { + file: Arc::new(File::new(prog_tx.clone())), + process: Arc::new(Process::new(prog_tx)), + + todo: todo_tx, + running: Default::default(), + }; + + for _ in 0..3 { + scheduler.schedule_micro(todo_rx.clone()); + } + for _ in 0..5 { + scheduler.schedule_macro(todo_rx.clone()); + } + scheduler.progress(prog_rx); + scheduler + } + + fn schedule_micro(&self, rx: Receiver>) { + tokio::spawn(async move { + loop { + if let Ok(fut) = rx.recv().await { + fut.await; + } + } + }); + } + + fn schedule_macro(&self, rx: Receiver>) { + let file = self.file.clone(); + let process = self.process.clone(); + let running = self.running.clone(); + + tokio::spawn(async move { + loop { + if let Ok(fut) = rx.try_recv() { + fut.await; + continue; + } + + select! { + Ok(fut) = rx.recv() => { + fut.await; + } + Ok((id, mut task)) = file.recv() => { + if !running.read().exists(id) { + trace!("Skipping task {:?} as it was removed", task); + continue; + } + if let Err(e) = file.work(&mut task).await { + info!("Failed to work on task {:?}: {}", task, e); + } else { + trace!("Finished task {:?}", task); + } + } + Ok((id, mut task)) = process.recv() => { + if !running.read().exists(id) { + trace!("Skipping task {:?} as it was removed", task); + continue; + } + if let Err(e) = process.work(&mut task).await { + info!("Failed to work on task {:?}: {}", task, e); + } else { + trace!("Finished task {:?}", task); + } + } + } + } + }); + } + + fn progress(&self, mut rx: UnboundedReceiver) { + let todo = self.todo.clone(); + let running = self.running.clone(); + + tokio::spawn(async move { + while let Some(task) = rx.recv().await { + match task { + TaskOp::New(id, size) => { + if let Some(task) = running.write().get(id) { + task.found += 1; + task.todo += size; + } + } + TaskOp::Adv(id, processed, size) => { + let mut running = running.write(); + if let Some(task) = running.get(id) { + task.processed += processed; + task.done += size; + } + if processed > 0 { + if let Some(fut) = running.try_remove(id, TaskStage::Pending) { + todo.send_blocking(fut).ok(); + } + } + } + TaskOp::Done(id) => { + if let Some(fut) = running.write().try_remove(id, TaskStage::Dispatched) { + todo.send_blocking(fut).ok(); + } + } + } + } + }); + + let running = self.running.clone(); + let mut last = 100; + tokio::spawn(async move { + loop { + sleep(Duration::from_secs(1)).await; + if running.read().is_empty() { + if last != 100 { + last = 100; + emit!(Progress(100, 0)); + } + continue; + } + + let mut tasks = 0u32; + let mut left = 0; + let mut progress = (0, 0); + for task in running.read().values() { + tasks += 1; + left += task.found.saturating_sub(task.processed); + progress = (progress.0 + task.done, progress.1 + task.todo); + } + + let mut new = match progress.1 { + 0 => 100u8, + _ => 100.min(progress.0 * 100 / progress.1) as u8, + }; + + if tasks != 0 { + new = new.min(99); + left = left.max(1); + } + + if new != last { + last = new; + emit!(Progress(new, left)); + } + } + }); + } + + pub(super) fn cancel(&self, id: usize) -> bool { + let mut running = self.running.write(); + let b = running.all.remove(&id).is_some(); + + if let Some(hook) = running.hooks.remove(&id) { + self.todo.send_blocking(hook(true)).ok(); + } + b + } + + pub(super) fn file_cut(&self, from: PathBuf, mut to: PathBuf, force: bool) { + let mut running = self.running.write(); + let id = running.add(format!("Cut {:?} to {:?}", from, to)); + + running.hooks.insert(id, { + let from = from.clone(); + let running = self.running.clone(); + + Box::new(move |canceled: bool| { + async move { + if !canceled { + File::remove_empty_dirs(&from).await; + } + running.write().try_remove(id, TaskStage::Hooked); + } + .boxed() + }) + }); + + let _ = self.todo.send_blocking({ + let file = self.file.clone(); + async move { + if !force { + to = unique_path(to).await; + } + file.paste(FileOpPaste { id, from, to, cut: true, follow: false, retry: 0 }).await.ok(); + } + .boxed() + }); + } + + pub(super) fn file_copy(&self, from: PathBuf, mut to: PathBuf, force: bool, follow: bool) { + let name = format!("Copy {:?} to {:?}", from, to); + let id = self.running.write().add(name); + + let _ = self.todo.send_blocking({ + let file = self.file.clone(); + async move { + if !force { + to = unique_path(to).await; + } + file.paste(FileOpPaste { id, from, to, cut: false, follow, retry: 0 }).await.ok(); + } + .boxed() + }); + } + + pub(super) fn file_delete(&self, target: PathBuf) { + let mut running = self.running.write(); + let id = running.add(format!("Delete {:?}", target)); + + running.hooks.insert(id, { + let target = target.clone(); + let running = self.running.clone(); + + Box::new(move |canceled: bool| { + async move { + if !canceled { + fs::remove_dir_all(target).await.ok(); + } + running.write().try_remove(id, TaskStage::Hooked); + } + .boxed() + }) + }); + + let _ = self.todo.send_blocking({ + let file = self.file.clone(); + async move { + file.delete(FileOpDelete { id, target, length: 0 }).await.ok(); + } + .boxed() + }); + } + + pub(super) fn file_trash(&self, target: PathBuf) { + let name = format!("Trash {:?}", target); + let id = self.running.write().add(name); + + let _ = self.todo.send_blocking({ + let file = self.file.clone(); + async move { + file.trash(FileOpTrash { id, target, length: 0 }).await.ok(); + } + .boxed() + }); + } + + pub(super) fn process_open(&self, opener: &Opener, args: &[String]) { + let args = opener + .args + .iter() + .map_while(|a| { + if !a.starts_with('$') { + return Some(vec![a.clone()]); + } + if a == "$*" { + return Some(args.to_vec()); + } + a[1..].parse().ok().and_then(|n: usize| args.get(n)).map(|a| vec![a.clone()]) + }) + .flatten() + .collect::>(); + + let mut running = self.running.write(); + let id = running.add(format!("Exec `{} {}`", opener.cmd, args.join(" "))); + + let (cancel_tx, mut cancel_rx) = oneshot::channel(); + running.hooks.insert(id, { + let running = self.running.clone(); + Box::new(move |canceled: bool| { + async move { + if canceled { + cancel_rx.close(); + } + running.write().try_remove(id, TaskStage::Hooked); + } + .boxed() + }) + }); + + let _ = self.todo.send_blocking({ + let process = self.process.clone(); + let opener = opener.clone(); + async move { + process + .open(ProcessOpOpen { id, cmd: opener.cmd, args, block: opener.block, cancel: cancel_tx }) + .await + .ok(); + } + .boxed() + }); + } +} diff --git a/src/core/tasks/tasks.rs b/src/core/tasks/tasks.rs new file mode 100644 index 00000000..e3c31c41 --- /dev/null +++ b/src/core/tasks/tasks.rs @@ -0,0 +1,164 @@ +use std::{collections::{BTreeMap, HashSet}, path::PathBuf}; + +use tracing::trace; + +use super::{Scheduler, TASKS_PADDING, TASKS_PERCENT}; +use crate::{config::OPEN, misc::tty_size}; + +#[derive(Clone, Debug)] +pub struct Task { + pub id: usize, + pub name: String, + pub stage: TaskStage, + + pub found: u32, + pub processed: u32, + + pub todo: u64, + pub done: u64, +} + +impl Task { + pub fn new(id: usize, name: String) -> Self { + Self { id, name, stage: Default::default(), found: 0, processed: 0, todo: 0, done: 0 } + } +} + +#[derive(Debug)] +pub enum TaskOp { + // task_id, size + New(usize, u64), + // task_id, processed, size + Adv(usize, u32, u64), + // task_id + Done(usize), +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub enum TaskStage { + #[default] + Pending, + Dispatched, + Hooked, +} + +pub struct Tasks { + scheduler: Scheduler, + + pub visible: bool, + pub cursor: usize, + pub progress: (u8, u32), +} + +impl Tasks { + pub fn start() -> Self { + Self { scheduler: Scheduler::start(), visible: false, cursor: 0, progress: (100, 0) } + } + + #[inline] + pub fn limit() -> usize { + (tty_size().ws_row * TASKS_PERCENT / 100).saturating_sub(TASKS_PADDING) as usize + } + + pub fn toggle(&mut self) -> bool { + self.visible = !self.visible; + true + } + + pub fn next(&mut self) -> bool { + let limit = Self::limit().min(self.scheduler.running.read().len()); + + let old = self.cursor; + self.cursor = limit.saturating_sub(1).min(self.cursor + 1); + + old != self.cursor + } + + pub fn prev(&mut self) -> bool { + let old = self.cursor; + self.cursor = self.cursor.saturating_sub(1); + old != self.cursor + } + + pub fn paginate(&self) -> Vec { + let running = self.scheduler.running.read(); + running.values().take(Self::limit()).cloned().collect::>() + } + + pub fn cancel(&self) -> bool { + let id = self.scheduler.running.read().values().skip(self.cursor).next().map(|t| t.id); + id.map(|id| self.scheduler.cancel(id)).unwrap_or(false) + } + + pub fn file_open(&self, targets: Vec<(PathBuf, String)>) -> bool { + let mut openers = BTreeMap::new(); + for target in targets { + if let Some(opener) = OPEN.opener(&target.0, &target.1) { + openers + .entry(opener.clone()) + .or_insert_with(|| vec![]) + .push(target.0.to_string_lossy().into_owned()); + } + } + for (opener, args) in openers { + if opener.spread { + self.scheduler.process_open(&opener, &args); + continue; + } + for target in args { + self.scheduler.process_open(&opener, &[target]); + } + } + false + } + + pub fn file_cut(&self, src: &HashSet, dest: PathBuf, force: bool) -> bool { + for p in src { + let to = dest.join(p.file_name().unwrap()); + if force && *p == to { + trace!("file_cut: same file, skipping {:?}", to); + } else { + self.scheduler.file_cut(p.clone(), to, force); + } + } + false + } + + pub fn file_copy( + &self, + src: &HashSet, + dest: PathBuf, + force: bool, + follow: bool, + ) -> bool { + for p in src { + let to = dest.join(p.file_name().unwrap()); + if force && *p == to { + trace!("file_copy: same file, skipping {:?}", to); + } else { + self.scheduler.file_copy(p.clone(), to, force, follow); + } + } + false + } + + pub fn file_remove(&self, targets: Vec, permanently: bool) -> bool { + for p in targets { + if permanently { + self.scheduler.file_delete(p); + } else { + self.scheduler.file_trash(p); + } + } + false + } + + pub fn update_progress(&mut self, percent: u8, left: u32) -> bool { + if self.progress.0 == percent { + return false; + } + + self.progress = (percent, left); + true + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..ed61b898 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,15 @@ +use ui::App; + +mod config; +mod core; +mod misc; +mod ui; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // console_subscriber::init(); + + config::init(); + + App::run().await +} diff --git a/src/misc/chars.rs b/src/misc/chars.rs new file mode 100644 index 00000000..4478d8ec --- /dev/null +++ b/src/misc/chars.rs @@ -0,0 +1,18 @@ +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum CharKind { + Space, + Punct, + Other, +} + +impl CharKind { + pub fn new(c: char) -> Self { + if c.is_whitespace() { + Self::Space + } else if c.is_ascii_punctuation() { + Self::Punct + } else { + Self::Other + } + } +} diff --git a/src/misc/defer.rs b/src/misc/defer.rs new file mode 100644 index 00000000..3c054bde --- /dev/null +++ b/src/misc/defer.rs @@ -0,0 +1,13 @@ +pub struct Defer(Option); + +impl Defer { + pub fn new(f: F) -> Self { Defer(Some(f)) } +} + +impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } +} diff --git a/src/misc/fns.rs b/src/misc/fns.rs new file mode 100644 index 00000000..875f4d33 --- /dev/null +++ b/src/misc/fns.rs @@ -0,0 +1,198 @@ +use std::{collections::VecDeque, env, path::{Path, PathBuf}}; + +use anyhow::Result; +use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ}; +use tokio::{fs::{self, File}, io::{self, AsyncBufReadExt, BufReader}, select, sync::{mpsc, oneshot}, time}; + +#[inline] +pub fn tty_size() -> winsize { + unsafe { + let s: winsize = std::mem::zeroed(); + ioctl(STDOUT_FILENO, TIOCGWINSZ, &s); + s + } +} + +#[inline] +pub fn tty_ratio() -> (f64, f64) { + let s = tty_size(); + (f64::from(s.ws_xpixel) / f64::from(s.ws_col), f64::from(s.ws_ypixel) / f64::from(s.ws_row)) +} + +pub fn absolute_path(p: &Path) -> PathBuf { + if p.starts_with("~") { + if let Ok(home) = env::var("HOME") { + let mut expanded = PathBuf::new(); + expanded.push(home); + expanded.push(p.strip_prefix("~").unwrap()); + return expanded; + } + } + p.to_path_buf() +} + +pub fn readable_path(p: &Path) -> String { + if let Ok(home) = env::var("HOME") { + if let Ok(p) = p.strip_prefix(home) { + return format!("~/{}", p.display()); + } + } + p.display().to_string() +} + +pub async fn unique_path(mut p: PathBuf) -> PathBuf { + let name = if let Some(name) = p.file_name() { + name.to_os_string() + } else { + return p; + }; + + let mut i = 0; + while fs::symlink_metadata(&p).await.is_ok() { + i += 1; + let mut name = name.clone(); + name.push(format!("_{}", i)); + p.set_file_name(name); + } + p +} + +#[inline] +pub fn optinal_bool(s: &str) -> Option { + if s == "true" { + Some(true) + } else if s == "false" { + Some(false) + } else { + None + } +} + +pub async fn first_n_lines(path: &Path, n: usize) -> Result> { + let mut lines = Vec::new(); + let mut it = BufReader::new(File::open(path).await?).lines(); + for _ in 0..n { + if let Some(line) = it.next_line().await? { + lines.push(line); + } else { + break; + } + } + Ok(lines) +} + +pub async fn calculate_size(path: &Path) -> u64 { + let mut total = 0; + let mut stack = VecDeque::from([path.to_path_buf()]); + while let Some(path) = stack.pop_front() { + let meta = if let Ok(meta) = fs::symlink_metadata(&path).await { + meta + } else { + continue; + }; + + if !meta.is_dir() { + total += meta.len(); + continue; + } + + let mut it = if let Ok(it) = fs::read_dir(path).await { + it + } else { + continue; + }; + + while let Ok(Some(entry)) = it.next_entry().await { + let meta = if let Ok(m) = entry.metadata().await { + m + } else { + continue; + }; + + if meta.is_dir() { + stack.push_back(entry.path()); + } else { + total += meta.len(); + } + } + } + total +} + +pub fn copy_with_progress(from: &Path, to: &Path) -> mpsc::Receiver> { + let (tx, rx) = mpsc::channel(1); + let (tick_tx, mut tick_rx) = oneshot::channel(); + + tokio::spawn({ + let (from, to) = (from.to_path_buf(), to.to_path_buf()); + + async move { + let _ = match fs::copy(from, to).await { + Ok(len) => tick_tx.send(Ok(len)), + Err(e) => tick_tx.send(Err(e)), + }; + } + }); + + tokio::spawn({ + let tx = tx.clone(); + let to = to.to_path_buf(); + + async move { + let mut last = 0; + let mut exit = None; + loop { + select! { + res = &mut tick_rx => exit = Some(res.unwrap()), + _ = tx.closed() => break, + _ = time::sleep(time::Duration::from_secs(1)) => (), + } + + match exit { + Some(Ok(len)) => { + if len > last { + tx.send(Ok(len - last)).await.ok(); + } + tx.send(Ok(0)).await.ok(); + break; + } + Some(Err(e)) => { + tx.send(Err(e)).await.ok(); + break; + } + None => {} + } + + let len = fs::symlink_metadata(&to).await.map(|m| m.len()).unwrap_or(0); + if len > last { + tx.send(Ok(len - last)).await.ok(); + last = len; + } + } + } + }); + + rx +} + +pub fn valid_mimetype(str: &str) -> bool { + let parts = str.split('/').collect::>(); + if parts.len() != 2 { + return false; + } + + let b = match parts[0] { + "application" => true, + "audio" => true, + "example" => true, + "font" => true, + "image" => true, + "message" => true, + "model" => true, + "multipart" => true, + "text" => true, + "video" => true, + _ => false, + }; + b && !parts[1].is_empty() +} diff --git a/src/misc/mod.rs b/src/misc/mod.rs new file mode 100644 index 00000000..4398f9fb --- /dev/null +++ b/src/misc/mod.rs @@ -0,0 +1,7 @@ +mod chars; +mod defer; +mod fns; + +pub use chars::*; +pub use defer::*; +pub use fns::*; diff --git a/src/ui/app.rs b/src/ui/app.rs new file mode 100644 index 00000000..f6711422 --- /dev/null +++ b/src/ui/app.rs @@ -0,0 +1,115 @@ +use anyhow::{Ok, Result}; +use crossterm::event::KeyEvent; +use tokio::sync::oneshot::{self}; + +use super::{root::Root, Ctx, Executor, Logs, Signals, Term}; +use crate::{config::keymap::Key, core::Event, emit}; + +pub struct App { + cx: Ctx, + term: Option, + signals: Signals, +} + +impl App { + pub async fn run() -> Result<()> { + let _log = Logs::init()?; + let term = Term::start()?; + + let signals = Signals::start()?; + let mut app = Self { cx: Ctx::new(), term: Some(term), signals }; + + while let Some(event) = app.signals.rx.recv().await { + match event { + Event::Quit => break, + Event::Stop(state, tx) => app.dispatch_stop(state, tx), + Event::Key(key) => app.dispatch_key(key), + Event::Render(_) => app.dispatch_render(), + Event::Resize(..) => app.dispatch_resize(), + event => app.dispatch_module(event).await, + } + } + Ok(()) + } + + fn dispatch_stop(&mut self, state: bool, tx: oneshot::Sender<()>) { + if state { + self.signals.stop_term(true); + self.term = None; + } else { + self.term = Some(Term::start().unwrap()); + self.signals.stop_term(false); + emit!(Render); + } + tx.send(()).ok(); + } + + fn dispatch_key(&mut self, key: KeyEvent) { + let key = Key::from(key); + if Executor::handle(&mut self.cx, key) { + emit!(Render); + } + } + + fn dispatch_render(&mut self) { + if let Some(term) = &mut self.term { + let _ = term.draw(|f| { + f.render_widget(Root::new(&mut self.cx), f.size()); + + if let Some((x, y)) = self.cx.cursor { + f.set_cursor(x, y); + } + }); + } + } + + fn dispatch_resize(&mut self) { + self.cx.manager.preview(); + emit!(Render); + } + + async fn dispatch_module(&mut self, event: Event) { + let manager = &mut self.cx.manager; + match event { + Event::Refresh => { + manager.refresh(); + } + Event::Files(path, files) => { + if manager.update_files(path, files) { + emit!(Render); + } + } + Event::Hover => { + if manager.preview() { + emit!(Render); + } + } + Event::Mimetype(file, mime) => { + if manager.update_mimetype(file, mime) { + emit!(Render); + } + } + Event::Preview(file, data) => { + manager.update_preview(file, data); + emit!(Render); + } + + Event::Input(opt, tx) => { + self.cx.input.show(opt, tx); + emit!(Render); + } + + Event::Open(files) => { + let mime = self.cx.manager.mimetype(&files).await; + let targets = files.into_iter().zip(mime).map_while(|(f, m)| m.map(|m| (f, m))).collect(); + self.cx.tasks.file_open(targets); + } + Event::Progress(percent, left) => { + self.cx.tasks.update_progress(percent, left); + emit!(Render); + } + + _ => unreachable!(), + } + } +} diff --git a/src/ui/context.rs b/src/ui/context.rs new file mode 100644 index 00000000..14fc55c9 --- /dev/null +++ b/src/ui/context.rs @@ -0,0 +1,21 @@ +use crate::core::{Input, Manager, Tasks}; + +pub struct Ctx { + pub cursor: Option<(u16, u16)>, + + pub manager: Manager, + pub input: Input, + pub tasks: Tasks, +} + +impl Ctx { + pub fn new() -> Self { + Self { + cursor: None, + + manager: Manager::new(), + input: Input::default(), + tasks: Tasks::start(), + } + } +} diff --git a/src/ui/dispatcher.rs b/src/ui/dispatcher.rs new file mode 100644 index 00000000..f91c83a8 --- /dev/null +++ b/src/ui/dispatcher.rs @@ -0,0 +1,186 @@ +use crossterm::event::KeyCode; + +use super::Ctx; +use crate::{config::{keymap::{Exec, Key, Single}, KEYMAP}, core::InputMode, emit, misc::optinal_bool}; + +pub struct Executor; + +impl Executor { + pub fn handle(cx: &mut Ctx, key: Key) -> bool { + let layer = if cx.input.visible { + 2 + } else if cx.tasks.visible { + 1 + } else { + 0 + }; + + let mut render = false; + let mut matched = false; + let keymap = [&KEYMAP.manager, &KEYMAP.tasks, &KEYMAP.input][layer]; + + for Single { on, exec } in keymap { + if on.len() < 1 || on[0] != key { + continue; + } + + matched = true; + for e in exec { + if layer == 0 { + render = Self::manager(cx, e) || render; + } else if layer == 1 { + render = Self::tasks(cx, e) || render; + } else if layer == 2 { + render = Self::input(cx, Some(e), key.code) || render; + } + } + } + + if layer == 2 && !matched { + render = Self::input(cx, None, key.code); + } + render + } + + fn manager(cx: &mut Ctx, exec: &Exec) -> bool { + match exec.cmd.as_str() { + "escape" => cx.manager.active_mut().escape(), + "quit" => { + emit!(Quit); + false + } + "close" => cx.manager.close(), + + // Navigation + "arrow" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + cx.manager.active_mut().arrow(step) + } + "leave" => cx.manager.active_mut().leave(), + "enter" => cx.manager.active_mut().enter(), + "back" => cx.manager.active_mut().back(), + "forward" => cx.manager.active_mut().forward(), + + // Selection + "select" => { + let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); + cx.manager.active_mut().select(optinal_bool(&state)) + } + "visual_mode" => cx.manager.active_mut().visual_mode(exec.named.contains_key("unselect")), + "select_all" => { + let state = exec.named.get("state").cloned().unwrap_or("none".to_string()); + cx.manager.active_mut().select_all(optinal_bool(&state)) + } + + // Operation + "yank" => cx.manager.yank(exec.named.contains_key("cut")), + "paste" => { + let dest = cx.manager.current().cwd.clone(); + let (cut, src) = cx.manager.yanked(); + + let force = exec.named.contains_key("force"); + if *cut { + cx.tasks.file_cut(src, dest, force) + } else { + cx.tasks.file_copy(src, dest, force, exec.named.contains_key("follow")) + } + } + "remove" => { + cx.tasks.file_remove(cx.manager.selected(), exec.named.contains_key("permanently")) + } + "create" => cx.manager.create(), + "rename" => cx.manager.rename(), + "hidden" => cx.manager.current_mut().hidden(match exec.args.get(0).map(|s| s.as_str()) { + Some("show") => Some(true), + Some("hide") => Some(false), + _ => None, + }), + + // Tabs + "tab_create" => { + let path = if exec.named.contains_key("current") { + cx.manager.current().cwd.clone() + } else { + exec.args.get(0).map(|p| p.into()).unwrap_or("/".into()) + }; + cx.manager.tabs_mut().create(&path) + } + "tab_close" => { + let idx = exec.args.get(0).and_then(|i| i.parse().ok()).unwrap_or(0); + cx.manager.tabs_mut().close(idx) + } + "tab_switch" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + let rel = exec.named.contains_key("relative"); + cx.manager.tabs_mut().switch(step, rel) + } + "tab_swap" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + cx.manager.tabs_mut().swap(step) + } + + // Tasks + "tasks_show" => cx.tasks.toggle(), + + _ => false, + } + } + + fn tasks(cx: &mut Ctx, exec: &Exec) -> bool { + match exec.cmd.as_str() { + "close" => cx.tasks.toggle(), + + "arrow" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + if step > 0 { cx.tasks.next() } else { cx.tasks.prev() } + } + + "cancel" => cx.tasks.cancel(), + _ => false, + } + } + + fn input(cx: &mut Ctx, exec: Option<&Exec>, code: KeyCode) -> bool { + let exec = if let Some(e) = exec { + e + } else { + if cx.input.mode() == InputMode::Insert { + if let KeyCode::Char(c) = code { + return cx.input.type_(c); + } + } + return false; + }; + + match cx.input.mode() { + InputMode::Normal => match exec.cmd.as_str() { + "close" => cx.input.close(exec.named.contains_key("submit")), + "escape" => cx.input.escape(), + + "insert" => cx.input.insert(exec.named.contains_key("append")), + "visual" => cx.input.visual(), + + "move" => { + let step = exec.args.get(0).and_then(|s| s.parse().ok()).unwrap_or(0); + cx.input.move_(step) + } + + "backward" => cx.input.backward(), + "forward" => cx.input.forward(exec.named.contains_key("end-of-word")), + "delete" => cx.input.delete(exec.named.contains_key("insert")), + _ => false, + }, + InputMode::Insert => match exec.cmd.as_str() { + "close" => cx.input.close(exec.named.contains_key("submit")), + "escape" => cx.input.escape(), + "backspace" => cx.input.backspace(), + _ => { + if let KeyCode::Char(c) = code { + return cx.input.type_(c); + } + false + } + }, + } + } +} diff --git a/src/ui/header/layout.rs b/src/ui/header/layout.rs new file mode 100644 index 00000000..dbd86136 --- /dev/null +++ b/src/ui/header/layout.rs @@ -0,0 +1,28 @@ +use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, style::{Color, Style}, widgets::{Paragraph, Widget}}; + +use super::tabs::Tabs; +use crate::{misc::readable_path, ui::Ctx}; + +pub struct Layout<'a> { + cx: &'a Ctx, +} + +impl<'a> Layout<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Layout<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(area); + + let manager = &self.cx.manager; + Paragraph::new(readable_path(&manager.current().cwd)) + .style(Style::default().fg(Color::Cyan)) + .render(chunks[0], buf); + + Tabs::new(self.cx).render(chunks[1], buf); + } +} diff --git a/src/ui/header/mod.rs b/src/ui/header/mod.rs new file mode 100644 index 00000000..c8ea98ef --- /dev/null +++ b/src/ui/header/mod.rs @@ -0,0 +1,5 @@ +mod layout; +mod tabs; + +pub use layout::*; +pub use tabs::*; diff --git a/src/ui/header/tabs.rs b/src/ui/header/tabs.rs new file mode 100644 index 00000000..ece1bb1e --- /dev/null +++ b/src/ui/header/tabs.rs @@ -0,0 +1,33 @@ +use ratatui::{buffer::Buffer, layout::{Alignment, Rect}, style::{Color, Style}, text::{Line, Span}, widgets::{Paragraph, Widget}}; + +use crate::ui::Ctx; + +pub struct Tabs<'a> { + cx: &'a Ctx, +} + +impl<'a> Tabs<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Tabs<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let tabs = self.cx.manager.tabs(); + + let spans = Line::from( + tabs + .iter() + .enumerate() + .map(|(i, _)| { + if i == tabs.idx() { + Span::styled(format!(" {} ", i + 1), Style::default().fg(Color::Black).bg(Color::Blue)) + } else { + Span::styled(format!(" {} ", i + 1), Style::default().fg(Color::Gray).bg(Color::Black)) + } + }) + .collect::>(), + ); + + Paragraph::new(spans).alignment(Alignment::Right).render(area, buf); + } +} diff --git a/src/ui/input.rs b/src/ui/input.rs new file mode 100644 index 00000000..fb6b3c2c --- /dev/null +++ b/src/ui/input.rs @@ -0,0 +1,40 @@ +use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Style}, text::Line, widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget}}; + +use super::{Ctx, Term}; +use crate::core::InputMode; + +pub struct Input<'a> { + cx: &'a Ctx, +} + +impl<'a> Input<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Input<'a> { + fn render(self, _: Rect, buf: &mut Buffer) { + let input = &self.cx.input; + let area = input.area(); + + Clear.render(area, buf); + Paragraph::new(input.value()) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Blue)) + .border_type(BorderType::Rounded) + .title({ + let mut line = Line::from(input.title()); + line.patch_style(Style::default().fg(Color::White)); + line + }), + ) + .style(Style::default().fg(Color::White)) + .render(area, buf); + + let _ = match input.mode() { + InputMode::Insert => Term::set_cursor_bar(), + _ => Term::set_cursor_block(), + }; + } +} diff --git a/src/ui/logs.rs b/src/ui/logs.rs new file mode 100644 index 00000000..50a55041 --- /dev/null +++ b/src/ui/logs.rs @@ -0,0 +1,24 @@ +use anyhow::{Context, Result}; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::{fmt, prelude::__tracing_subscriber_SubscriberExt, Registry}; + +pub struct Logs {} + +impl Logs { + pub fn init() -> Result { + let root = xdg::BaseDirectories::with_prefix("yazi") + .context("failed to get XDG base directories")? + .get_state_home(); + + let appender = tracing_appender::rolling::hourly(root, "yazi.log"); + let (handle, guard) = tracing_appender::non_blocking(appender); + + // let filter = EnvFilter::from_default_env(); + let subscriber = Registry::default().with(fmt::layer().compact().with_writer(handle)); + + tracing::subscriber::set_global_default(subscriber) + .context("setting default subscriber failed")?; + + Ok(guard) + } +} diff --git a/src/ui/manager/folder.rs b/src/ui/manager/folder.rs new file mode 100644 index 00000000..c4b1843e --- /dev/null +++ b/src/ui/manager/folder.rs @@ -0,0 +1,75 @@ +use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, widgets::{List, ListItem, Widget}}; + +use crate::{config::THEME, core}; + +pub struct Folder<'a> { + folder: &'a core::Folder, + is_preview: bool, + is_selection: bool, +} + +impl<'a> Folder<'a> { + pub fn new(folder: &'a core::Folder) -> Self { + Self { folder, is_preview: false, is_selection: false } + } + + #[inline] + pub fn with_preview(mut self, state: bool) -> Self { + self.is_preview = state; + self + } + + #[inline] + pub fn with_selection(mut self, state: bool) -> Self { + self.is_selection = state; + self + } +} + +impl<'a> Widget for Folder<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let items = self + .folder + .paginate() + .iter() + .enumerate() + .map(|(i, (_, v))| { + let icon = THEME + .icons + .iter() + .find(|x| x.name.match_path(&v.path, Some(v.meta.is_dir()))) + .map(|x| x.display.as_ref()) + .unwrap_or(""); + + let item = ListItem::new(if v.is_selected { + format!("> {} {}", icon, v.name) + } else { + format!("{} {}", icon, v.name) + }); + + let mut style = Style::default(); + if self.is_selection { + if i == self.folder.rel_cursor() { + style = style.fg(Color::Black).bg(Color::Red); + } else if v.is_selected { + style = style.fg(Color::Red); + } + } else if self.is_preview { + if i == self.folder.rel_cursor() { + style = style.add_modifier(Modifier::UNDERLINED) + } + } else { + if i == self.folder.rel_cursor() { + style = style.fg(Color::Black).bg(Color::Yellow); + } else if v.is_selected { + style = style.fg(Color::Red); + } + } + + item.style(style) + }) + .collect::>(); + + List::new(items).render(area, buf); + } +} diff --git a/src/ui/manager/layout.rs b/src/ui/manager/layout.rs new file mode 100644 index 00000000..b9f7199a --- /dev/null +++ b/src/ui/manager/layout.rs @@ -0,0 +1,47 @@ +use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Widget}}; + +use super::{Folder, Preview}; +use crate::{core::{Mode, ALL_RATIO, CURRENT_RATIO, PARENT_RATIO, PREVIEW_RATIO}, ui::Ctx}; + +pub struct Layout<'a> { + cx: &'a Ctx, +} + +impl<'a> Layout<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Layout<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let manager = &self.cx.manager; + + let chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Ratio(PARENT_RATIO, ALL_RATIO), + Constraint::Ratio(CURRENT_RATIO, ALL_RATIO), + Constraint::Ratio(PREVIEW_RATIO, ALL_RATIO), + ] + .as_ref(), + ) + .split(area); + + // Parent + let block = Block::default().borders(Borders::RIGHT); + if let Some(ref parent) = manager.parent() { + Folder::new(parent).render(block.inner(chunks[0]), buf); + } + block.render(chunks[0], buf); + + // Current + Folder::new(&manager.current()) + .with_selection(matches!(manager.active().mode(), Mode::Select(_))) + .render(chunks[1], buf); + + // Preview + let block = Block::default().borders(Borders::LEFT); + Preview::new(self.cx).render(block.inner(chunks[2]), buf); + block.render(chunks[2], buf); + } +} diff --git a/src/ui/manager/mod.rs b/src/ui/manager/mod.rs new file mode 100644 index 00000000..763a990b --- /dev/null +++ b/src/ui/manager/mod.rs @@ -0,0 +1,8 @@ +mod folder; +mod layout; +mod preview; + +pub use folder::*; +pub use layout::*; +pub use preview::*; + diff --git a/src/ui/manager/preview.rs b/src/ui/manager/preview.rs new file mode 100644 index 00000000..2e5e906b --- /dev/null +++ b/src/ui/manager/preview.rs @@ -0,0 +1,58 @@ +use std::io::{stdout, Write}; + +use ansi_to_tui::IntoText; +use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}}; + +use super::Folder; +use crate::{core::{kitty::Kitty, PreviewData}, ui::{Ctx, Term}}; + +pub struct Preview<'a> { + cx: &'a Ctx, +} + +impl<'a> Preview<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Preview<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + if self.cx.input.visible || self.cx.tasks.visible { + stdout().write(Kitty::image_hide()).ok(); + return; + } + + let manager = &self.cx.manager; + let hovered = if let Some(h) = manager.hovered() { + h.clone() + } else { + stdout().write(Kitty::image_hide()).ok(); + return; + }; + + let preview = manager.active().preview(); + if preview.path != hovered.path { + return; + } + + if !matches!(preview.data, PreviewData::Image(_)) { + stdout().write(Kitty::image_hide()).ok(); + } + + match &preview.data { + PreviewData::None => {} + PreviewData::Folder => { + if let Some(folder) = manager.active().history(&hovered.path) { + Folder::new(folder).with_preview(true).render(area, buf); + } + } + PreviewData::Text(s) => { + let p = Paragraph::new(s.as_bytes().into_text().unwrap()); + p.render(area, buf); + } + PreviewData::Image(b) => { + Term::move_to(area.x, area.y).ok(); + stdout().write(b).ok(); + } + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 00000000..9b00c0c1 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,20 @@ +mod app; +mod context; +mod dispatcher; +mod header; +mod input; +mod logs; +mod manager; +mod root; +mod signals; +mod status; +mod tasks; +mod term; + +pub use app::*; +pub use context::*; +pub use dispatcher::*; +pub use input::*; +pub use logs::*; +pub use signals::*; +pub use term::*; diff --git a/src/ui/root.rs b/src/ui/root.rs new file mode 100644 index 00000000..76821fbc --- /dev/null +++ b/src/ui/root.rs @@ -0,0 +1,35 @@ +use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; + +use super::{header, manager, status, tasks, Ctx, Input}; + +pub struct Root<'a> { + cx: &'a mut Ctx, +} + +impl<'a> Root<'a> { + pub fn new(cx: &'a mut Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Root<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(0), Constraint::Length(1)].as_ref()) + .split(area); + + header::Layout::new(self.cx).render(chunks[0], buf); + manager::Layout::new(self.cx).render(chunks[1], buf); + status::Layout::new(self.cx).render(chunks[2], buf); + + if self.cx.tasks.visible { + tasks::Layout::new(self.cx).render(area, buf); + } + + if self.cx.input.visible { + Input::new(self.cx).render(area, buf); + self.cx.cursor = Some(self.cx.input.cursor()); + } else { + self.cx.cursor = None; + } + } +} diff --git a/src/ui/signals.rs b/src/ui/signals.rs new file mode 100644 index 00000000..46670b64 --- /dev/null +++ b/src/ui/signals.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use crossterm::event::{Event as CrosstermEvent, EventStream}; +use futures::StreamExt; +use libc::{SIGHUP, SIGINT, SIGQUIT, SIGTERM}; +use tokio::{select, sync::{mpsc::{self, Receiver, Sender}, oneshot}, task::JoinHandle}; + +use crate::core::Event; + +pub struct Signals { + pub tx: Sender, + pub rx: Receiver, + + term_stop_tx: Option>, + term_stop_rx: Option>, +} + +impl Signals { + pub fn start() -> Result { + let (tx, rx) = mpsc::channel(500); + let (term_tx, term_rx) = oneshot::channel(); + + let mut signals = + Self { tx: tx.clone(), rx, term_stop_tx: Some(term_tx), term_stop_rx: Some(term_rx) }; + + signals.spawn_system_task()?; + signals.spawn_crossterm_task(); + + Event::init(tx); + Ok(signals) + } + + fn spawn_system_task(&self) -> Result> { + let tx = self.tx.clone(); + let mut signals = signal_hook_tokio::Signals::new([SIGHUP, SIGTERM, SIGINT, SIGQUIT])?; + + Ok(tokio::spawn(async move { + while let Some(signal) = signals.next().await { + match signal { + SIGHUP | SIGTERM | SIGINT | SIGQUIT => { + if tx.send(Event::Quit).await.is_err() { + break; + } + } + _ => {} + } + } + })) + } + + fn spawn_crossterm_task(&mut self) -> JoinHandle<()> { + let tx = self.tx.clone(); + let mut stop_rx = self.term_stop_rx.take().unwrap(); + + tokio::spawn(async move { + let mut reader = EventStream::new(); + + loop { + select! { + _ = &mut stop_rx => break, + Some(Ok(event)) = reader.next() => { + let event = match event { + CrosstermEvent::Key(key) => Event::Key(key), + CrosstermEvent::Resize(cols, rows) => Event::Resize(cols, rows), + _ => continue, + }; + if tx.send(event).await.is_err() { + break; + } + } + } + } + }) + } + + pub fn stop_term(&mut self, state: bool) { + if state == self.term_stop_tx.is_none() { + return; + } + + if let Some(tx) = self.term_stop_tx.take() { + tx.send(()).ok(); + } else { + let (tx, rx) = oneshot::channel(); + (self.term_stop_tx, self.term_stop_rx) = (Some(tx), Some(rx)); + self.spawn_crossterm_task(); + } + } +} diff --git a/src/ui/status/layout.rs b/src/ui/status/layout.rs new file mode 100644 index 00000000..3d1735fd --- /dev/null +++ b/src/ui/status/layout.rs @@ -0,0 +1,51 @@ +use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget}}; + +use super::Progress; +use crate::ui::Ctx; + +pub struct Layout<'a> { + cx: &'a Ctx, +} + +impl<'a> Layout<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Layout<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mode = self.cx.manager.active().mode(); + + let chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(8), + Constraint::Length(8), + Constraint::Length(1), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(area); + + Paragraph::new("").style(Style::default().fg(mode.color().bg_rgb())).render(chunks[0], buf); + + Paragraph::new(format!(" {} ", mode)) + .style( + Style::default() + .fg(mode.color().fg_rgb()) + .bg(mode.color().bg_rgb()) + .add_modifier(Modifier::BOLD), + ) + .render(chunks[1], buf); + + Paragraph::new(" master ") + .style(Style::default().fg(mode.color().bg_rgb()).bg(Color::Rgb(72, 77, 102))) + .render(chunks[2], buf); + + Paragraph::new("").style(Style::default().fg(Color::Rgb(72, 77, 102))).render(chunks[3], buf); + + Progress::new(self.cx).render(chunks[4], buf); + } +} diff --git a/src/ui/status/mod.rs b/src/ui/status/mod.rs new file mode 100644 index 00000000..7b68b8e5 --- /dev/null +++ b/src/ui/status/mod.rs @@ -0,0 +1,5 @@ +mod layout; +mod progress; + +pub use layout::*; +pub use progress::*; diff --git a/src/ui/status/progress.rs b/src/ui/status/progress.rs new file mode 100644 index 00000000..8573c7ba --- /dev/null +++ b/src/ui/status/progress.rs @@ -0,0 +1,27 @@ +use ratatui::{style::{Color, Style}, widgets::{Gauge, Widget}}; + +use crate::ui::Ctx; + +pub struct Progress<'a> { + cx: &'a Ctx, +} + +impl<'a> Progress<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } +} + +impl<'a> Widget for Progress<'a> { + fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) { + let progress = &self.cx.tasks.progress; + if progress.0 >= 100 { + return; + } + + Gauge::default() + .gauge_style(Style::default().fg(Color::Yellow)) + .percent(progress.0 as u16) + .label(format!("{}%, {} left", progress.0, progress.1)) + .use_unicode(true) + .render(area, buf); + } +} diff --git a/src/ui/tasks/clear.rs b/src/ui/tasks/clear.rs new file mode 100644 index 00000000..d9526402 --- /dev/null +++ b/src/ui/tasks/clear.rs @@ -0,0 +1,19 @@ +use ratatui::{buffer::Buffer, layout::Rect, widgets::{self, Widget}}; + +pub struct Clear; + +impl Widget for Clear { + fn render(self, mut area: Rect, buf: &mut Buffer) { + if area.x > 0 { + area.x -= 1; + area.width += 2; + } + + if area.y > 0 { + area.y -= 1; + area.height += 2; + } + + widgets::Clear.render(area, buf) + } +} diff --git a/src/ui/tasks/layout.rs b/src/ui/tasks/layout.rs new file mode 100644 index 00000000..55a18c43 --- /dev/null +++ b/src/ui/tasks/layout.rs @@ -0,0 +1,72 @@ +use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, Rect}, style::{Color, Modifier, Style}, widgets::{Block, BorderType, Borders, List, ListItem, Padding, Widget}}; + +use super::Clear; +use crate::{core::TASKS_PERCENT, ui::Ctx}; + +pub struct Layout<'a> { + cx: &'a Ctx, +} + +impl<'a> Layout<'a> { + pub fn new(cx: &'a Ctx) -> Self { Self { cx } } + + pub fn area(area: Rect) -> Rect { + let chunk = layout::Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + Constraint::Percentage(TASKS_PERCENT), + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + ] + .as_ref(), + ) + .split(area)[1]; + + let chunk = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + Constraint::Percentage(TASKS_PERCENT), + Constraint::Percentage((100 - TASKS_PERCENT) / 2), + ] + .as_ref(), + ) + .split(chunk)[1]; + + chunk + } +} + +impl<'a> Widget for Layout<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let area = Self::area(area); + + Clear.render(area, buf); + let block = Block::default() + .title("Tasks") + .title_alignment(Alignment::Center) + .padding(Padding::new(0, 0, 1, 1)) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(Color::Rgb(128, 174, 250))); + block.clone().render(area, buf); + + let tasks = &self.cx.tasks; + let items = tasks + .paginate() + .iter() + .enumerate() + .map(|(i, v)| { + let mut item = ListItem::new(v.name.clone()); + if i == tasks.cursor { + item = item.style(Style::default().add_modifier(Modifier::UNDERLINED)); + } + item + }) + .collect::>(); + + List::new(items).render(block.inner(area), buf); + } +} diff --git a/src/ui/tasks/mod.rs b/src/ui/tasks/mod.rs new file mode 100644 index 00000000..edda6bea --- /dev/null +++ b/src/ui/tasks/mod.rs @@ -0,0 +1,5 @@ +mod clear; +mod layout; + +pub use clear::*; +pub use layout::*; diff --git a/src/ui/term.rs b/src/ui/term.rs new file mode 100644 index 00000000..c140627b --- /dev/null +++ b/src/ui/term.rs @@ -0,0 +1,67 @@ +use std::{io::{stdout, Stdout}, ops::{Deref, DerefMut}}; + +use anyhow::Result; +use crossterm::{cursor::{MoveTo, SetCursorStyle}, event::{DisableBracketedPaste, DisableFocusChange, EnableBracketedPaste, EnableFocusChange, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, terminal::{disable_raw_mode, enable_raw_mode, supports_keyboard_enhancement, EnterAlternateScreen, LeaveAlternateScreen}}; +use ratatui::{backend::CrosstermBackend, Terminal}; + +pub struct Term { + inner: Terminal>, + csi_u: bool, +} + +impl Term { + pub fn start() -> Result { + let mut term = Self { inner: Terminal::new(CrosstermBackend::new(stdout()))?, csi_u: false }; + + enable_raw_mode()?; + execute!(stdout(), EnterAlternateScreen, EnableBracketedPaste, EnableFocusChange)?; + + term.csi_u = matches!(supports_keyboard_enhancement(), Ok(true)); + if term.csi_u { + queue!( + stdout(), + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS + ) + )?; + } + + term.hide_cursor()?; + term.clear()?; + Ok(term) + } + + pub fn move_to(x: u16, y: u16) -> Result<()> { Ok(execute!(stdout(), MoveTo(x, y))?) } + + pub fn set_cursor_block() -> Result<()> { Ok(execute!(stdout(), SetCursorStyle::BlinkingBlock)?) } + + pub fn set_cursor_bar() -> Result<()> { Ok(execute!(stdout(), SetCursorStyle::BlinkingBar)?) } +} + +impl Drop for Term { + fn drop(&mut self) { + let mut f = || -> Result<()> { + if self.csi_u { + execute!(stdout(), PopKeyboardEnhancementFlags)?; + } + + execute!(stdout(), DisableFocusChange, DisableBracketedPaste, LeaveAlternateScreen)?; + + self.show_cursor()?; + Ok(disable_raw_mode()?) + }; + + f().ok(); + } +} + +impl Deref for Term { + type Target = Terminal>; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl DerefMut for Term { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } +}