From e0d683b13a315253d23030122190c01af1f3ff71 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Mon, 20 Mar 2023 01:07:04 +0530 Subject: [PATCH] Release 0.21.0 (#602) * Add xplr.util.lscolor and xplr.util.paint (#569) * Add xplr.util.lscolor and xplr.util.style * Fix formatting * Fix clippy suggestions * Remove redundant closures * Optimize, support NO_COLOR, and rename style to paint * Use xplr.util.paint and xplr.util.color in init.lua Co-authored-by: Noah Mayr * Add utility function xplr.util.textwrap (#567) * Add utility function xplr.util.wrap * Cleanup and fix formatting * Update src/lua/util.rs Co-authored-by: Arijit Basu * Update wrap to return lines instead * Fix doc * Rename wrap -> text wrap Co-authored-by: Arijit Basu Co-authored-by: Arijit Basu * Add xplr.util.relative_to and xplr.util.path_shorthand (#568) * Add xplr.util.relative_to and xplr.util.path_shorthand * Remove duplicate slash at end * Use pwd from env and remove pathdiff package * Some fixes and improvements * Generate docs * Some more improvements * Improve selection rendering * Improve functions with test cases * Update docs * Minor doc fix * Rename path_shorthand -> shortened * Handle homedir edgecase Also fix init.lua * Minor fix * Use config argument for relative and shortened paths * Prefix relative paths with "." and fix edge cases where we're not showing the file name * Use and_then instead of map and flatten * WIP: Move selection rendering to lua * Make selection renderer function configurable on lua side * Some improvements * Some impovements * Minor doc fix * Remove symlink style --------- Co-authored-by: Arijit Basu * Add xplr.util.layout_replaced (#574) Closes: https://github.com/sayanarijit/xplr/issues/573 * Improve selection operations (#575) - `:sl` to list selection. - `:ss` to softlink. - `:sh` to hardlink. - Avoid conflict by adding suffix. - Unselect individual path only on operation success. Closes: - https://github.com/sayanarijit/xplr/issues/572 - https://github.com/sayanarijit/xplr/issues/571 - https://github.com/sayanarijit/xplr/issues/570 * Minor updates * Add more features (#581) * Add more features - Key binding ":se" to edit selection list in $EDITOR - New utility functions: - xplr.util.clone - xplr.util.exists - xplr.util.is_dir - xplr.util.is_file - xplr.util.is_symlink - xplr.util.is_absolute - xplr.util.path_split - xplr.util.node Closes: https://github.com/sayanarijit/xplr/issues/580 Closes: https://github.com/sayanarijit/xplr/issues/579 Closes: https://github.com/sayanarijit/xplr/issues/577 * Fix edit selection list * Fix clippy lints * Fix layout link in doc * xplr.util.shortened -> xplr.util.shorten * Fix more clippy lints * Fix xplr.util.shorten name change * More UI utilities and improvements (#582) * More UI utilities and improvements - Apply style only to the file column in the table. - Properly quote paths. - Expose the applicable style from config in the table renderer argument. - Add utility functions: - xplr.util.node_type - xplr.util.style_mix - xplr.util.shell_escape * Make escaping play nice with shorten * Fix tests * Fix doc * Some fixes * Fix selection editor * Fix clear selection for selection editor * Add selection navigation (#583) * Add selection navigation - FocusNextSelection (ctrl-n) - FocusPreviousSelection (ctrl-p) Also improve batch operations * Minor doc fixes * Minor doc fix * Remove tab -> ctrl-i binding * Improve batch operation interaction - More robust focus operation. - Focus on failed to delete paths. * Fix Rust compatibility * Fix panic on permission denial Also, improve the error messages. * More logging improvements * Fix layout_replace only working with table parameters (#586) * Improve builtin search mode (#585) * Improve builtin search mode * Remove commented out code * Make search ranking and algorithm more extensible * Flatten messages BREAKING: xplr.config.general.sort_and_filter_ui.search_identifier -> xplr.config.general.sort_and_filter_ui.search_identifiers Messages: - Search - SearchFromInput - SearchFuzzy - SearchFuzzyUnranked - SearchFuzzyUnrankedFromInput - SearchRegexUnrankedFromInput - SearchRegex - SearchRegexUnranked - SearchRegexUnrankedFromInput - SearchRegexUnrankedFromInput - CycleSearchAlgorithm - EnableRankedSearch - DisableRankedSearch - ToggleRankedSearch Static config: xplr.config.general.search.algorithm = "Fuzzy" * Handle search ranking in search algorithm * Make CycleSearchAlgorithm only cycle between algorithms, without changing ranking * Separate algorithm and ordering * Minor doc updates * Some cleanup * Final touch * Cycle -> Toggle --------- Co-authored-by: Arijit Basu * Fix layout replace for unit layouts (#588) * Allow custom title and ui config in custom layout. (#589) * Allow custom title and ui config in custom layout. Adds the following layouts: - Static - Dynamic Deprecates `CustomContent` (but won't be removed to maintain compatibility). Closes: https://github.com/sayanarijit/xplr/issues/563 * Delete init.lua * Update docs/en/src/layout.md * Update docs/en/src/layout.md * Rename - Paragraph => CustomParagraph - List => CustomList - Table => CustomTable Also update init.lua * Fix clippy errs * Fix doc links * Fix search order * Improve working with file permissions (#591) * Improve working with file permissions Implements: - xplr.util.permissions_rwx - xplr.util.permissions_octal * Edit permissions * Add permissions in Resolved Node (#592) * Add permissions in Relolved Node And handle application/x-executable mime type. * Fix bench * Improve permissions editor * More permissions editor improvements * Doc updates * Remove ResolvedNode.permissions (#593) Reason: Too much serialization making lua calls slow. * Add workaround for macos with legacy coreutils (#595) Refs: - https://github.com/sayanarijit/xplr/issues/594 - https://github.com/sayanarijit/xplr/issues/559 * Use H:M:S format to display logs (#596) * Keep the selection list and clear manually (#597) * Keep the selection list and clear manually Ref: https://github.com/sayanarijit/map.xplr/issues/4 * Fix linting err * Fix broken history (#599) * Fix broken hostory Fixes: https://github.com/sayanarijit/xplr/issues/598 * Minor cleanup * Slightly optimize selection retention (#600) * Update deps * chrono -> time * update: 0.20.2 -> 0.21.1 * Update post-install.md * Upgrade guide * Minor fix * Fix tests * Add missing doc * Fix clippy lints --------- Co-authored-by: Noah Mayr --- Cargo.lock | 824 ++++++++++++++++++++++------ Cargo.toml | 42 +- benches/criterion.rs | 10 +- docs/en/src/SUMMARY.md | 2 + docs/en/src/borders.md | 6 + docs/en/src/column-renderer.md | 9 + docs/en/src/default-key-bindings.md | 245 +++++---- docs/en/src/general-config.md | 38 +- docs/en/src/layout.md | 246 ++++----- docs/en/src/lua-function-calls.md | 25 +- docs/en/src/messages.md | 154 ++++++ docs/en/src/modes.md | 6 + docs/en/src/searching.md | 77 +++ docs/en/src/upgrade-guide.md | 78 +++ docs/en/src/xplr.util.md | 375 ++++++++++++- docs/script/generate.py | 3 + src/app.rs | 368 ++++++++++--- src/bin/xplr.rs | 12 +- src/cli.rs | 25 +- src/compat.rs | 224 ++++++++ src/config.rs | 74 ++- src/directory_buffer.rs | 14 +- src/explorer.rs | 41 +- src/init.lua | 566 ++++++++++++++----- src/input.rs | 2 +- src/lib.rs | 3 + src/lua/mod.rs | 14 +- src/lua/util.rs | 613 ++++++++++++++++++++- src/msg/in_/external.rs | 215 +++++++- src/node.rs | 75 ++- src/path.rs | 495 +++++++++++++++++ src/permissions.rs | 91 ++- src/runner.rs | 31 +- src/search.rs | 50 ++ src/ui.rs | 595 ++++++++++++-------- 35 files changed, 4617 insertions(+), 1031 deletions(-) create mode 100644 docs/en/src/searching.md create mode 100644 src/compat.rs create mode 100644 src/path.rs create mode 100644 src/search.rs diff --git a/Cargo.lock b/Cargo.lock index 8382f05..4f5f41d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -27,29 +38,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] -name = "ansi-to-tui" -version = "2.0.0" +name = "ansi-to-tui-forked" +version = "3.0.0-ratatui" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3460d7beaf8b192c09a55933da038ccd514f00efdb37d7d87f3ce078336b47e9" +checksum = "f0b908b67a7faf8682254111ae9aecee03d4a3b613de03088f9a77953f85bab3" dependencies = [ "nom", + "ratatui", "thiserror", - "tui", ] +[[package]] +name = "anstyle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" + [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "arrayvec" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "assert_cmd" -version = "2.0.8" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9834fcc22e0874394a010230586367d4a3e9f11b560f469262678547e1d2575e" +checksum = "ec0b2340f55d9661d76793b2bfc2eb0e62689bd79d067a95707ea762afd5e9dd" dependencies = [ - "bstr 1.1.0", + "anstyle", + "bstr 1.4.0", "doc-comment", "predicates", "predicates-core", @@ -74,6 +98,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bitflags" version = "1.3.2" @@ -91,9 +121,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", "once_cell", @@ -103,9 +133,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "cassowary" @@ -121,9 +151,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -133,16 +163,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", "num-integer", "num-traits", - "serde", - "time", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -180,9 +209,13 @@ version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ + "atty", "bitflags", "clap_lex", "indexmap", + "once_cell", + "strsim", + "termcolor", "textwrap", ] @@ -247,11 +280,25 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -259,9 +306,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -270,22 +317,32 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.8.0", "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if", ] @@ -306,6 +363,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.0" @@ -317,9 +390,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" +checksum = "a9c00419335c41018365ddf7e4d5f1c12ee3659ddcf3e01974650ba1de73d038" dependencies = [ "cc", "cxxbridge-flags", @@ -329,9 +402,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" +checksum = "fb8307ad413a98fff033c8545ecf133e3257747b3bae935e7602aab8aa92d4ca" dependencies = [ "cc", "codespan-reporting", @@ -339,24 +412,100 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 2.0.2", ] [[package]] name = "cxxbridge-flags" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" +checksum = "edc52e2eb08915cb12596d29d55f0b5384f00d697a646dbd269b6ecb0fbd9d31" [[package]] name = "cxxbridge-macro" -version = "1.0.86" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.2", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ + "fnv", + "ident_case", "proc-macro2", "quote", - "syn", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "defer-drop" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f613ec9fa66a6b28cdb1842b27f9adf24f39f9afc4dcdd9fdecee4aca7945c57" +dependencies = [ + "crossbeam-channel", + "once_cell", +] + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", ] [[package]] @@ -367,18 +516,39 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirs" -version = "4.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd" dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" -version = "0.3.7" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +dependencies = [ + "libc", + "redox_users", + "windows-sys", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", @@ -393,19 +563,38 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] [[package]] name = "erased-serde" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ca605381c017ec7a5fef5e548f1cfaa419ed0f6df6367339300db74c92aa7d" +checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569" dependencies = [ "serde", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -447,6 +636,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hermit-abi" @@ -475,6 +667,12 @@ dependencies = [ "libm", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -499,6 +697,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "1.9.2" @@ -521,15 +725,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -542,9 +746,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" [[package]] name = "libm" @@ -580,6 +784,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lscolors" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dedc85d67baf5327114fad78ab9418f8893b1121c17d5538dd11005ad1ddf2" +dependencies = [ + "nu-ansi-term 0.46.0", +] + [[package]] name = "lua-src" version = "544.0.1" @@ -606,9 +819,18 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.7.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] @@ -637,9 +859,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", @@ -649,9 +871,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee2ad7a9aa69056b148d9d590344bc155d3ce0d2200e3b2838f7034f6ba33c1" +checksum = "ea8ce6788556a67d90567809c7de94dfef2ff1f47ff897aeee935bcfbcdf5735" dependencies = [ "bstr 0.2.17", "cc", @@ -671,16 +893,60 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + [[package]] name = "nom" -version = "7.1.2" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] +[[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 = "nu-ansi-term" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df031e117bca634c262e9bd3173776844b6c17a90b3741c9163663b4385af76" +dependencies = [ + "windows-sys", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -710,11 +976,20 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "oorandom" @@ -724,9 +999,15 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "os_str_bytes" -version = "6.4.1" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" @@ -740,9 +1021,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", @@ -769,6 +1050,12 @@ dependencies = [ "once_cell", ] +[[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.26" @@ -805,10 +1092,11 @@ dependencies = [ [[package]] name = "predicates" -version = "2.1.5" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +checksum = "1ba7d6ead3e3966038f68caa9fc1f860185d95a793180bbcfe0d0da47b3961ed" dependencies = [ + "anstyle", "difflib", "itertools", "predicates-core", @@ -816,15 +1104,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", @@ -832,27 +1120,41 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d690717aac4aca6e901da642fafcceff63ded0ab4c65c18ceff39c9a27f21508" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.26.1", + "serde", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "rayon" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ "either", "rayon-core", @@ -860,9 +1162,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -920,10 +1222,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "ryu" +name = "rustversion" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" @@ -942,35 +1250,35 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.152" +version = "1.0.157" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "707de5fcf5df2b5788fca98dd7eab490bc2fd9b7ef1404defc462833b83f25ca" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.157" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "78997f4555c22a7971214540c4a661291970619afd56de19f77e0de86296e1e5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.2", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" dependencies = [ "itoa", "ryu", @@ -979,9 +1287,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.16" +version = "0.9.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b5b431e8907b50339b51223b97d102db8d987ced36f6e4d03621db9316c834" +checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" dependencies = [ "indexmap", "itoa", @@ -990,11 +1298,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", @@ -1013,77 +1327,156 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] +[[package]] +name = "skim" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d28de0a6cb2cdd83a076f1de9d965b973ae08b244df1aa70b432946dda0f32" +dependencies = [ + "atty", + "beef", + "bitflags", + "chrono", + "clap", + "crossbeam", + "defer-drop", + "derive_builder", + "env_logger", + "fuzzy-matcher", + "lazy_static", + "log", + "nix 0.25.1", + "rayon", + "regex", + "shlex", + "time 0.3.20", + "timer", + "tuikit", + "unicode-width", + "vte", +] + [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "snailquote" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec62a949bda7f15800481a711909f946e1204f2460f89210eaf7f57730f88f86" +dependencies = [ + "thiserror", + "unicode_categories", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d3276aee1fa0c33612917969b5172b5be2db051232a6e4826f1a1a9191b045" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "termtree" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "textwrap" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.2", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] @@ -1098,6 +1491,44 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "timer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" +dependencies = [ + "chrono", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1109,27 +1540,27 @@ dependencies = [ ] [[package]] -name = "tui" -version = "0.19.0" +name = "tui-input" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +checksum = "d6a207b3c48d2affc4a9645068b83512eebf3b586214af2a9784b6e7ec5aa41b" dependencies = [ - "bitflags", - "cassowary", - "crossterm", + "crossterm 0.25.0", "serde", - "unicode-segmentation", "unicode-width", ] [[package]] -name = "tui-input" -version = "0.6.1" +name = "tuikit" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a207b3c48d2affc4a9645068b83512eebf3b586214af2a9784b6e7ec5aa41b" +checksum = "5e19c6ab038babee3d50c8c12ff8b910bdb2196f62278776422f50390d8e53d8" dependencies = [ - "crossterm", - "serde", + "bitflags", + "lazy_static", + "log", + "nix 0.24.3", + "term", "unicode-width", ] @@ -1144,15 +1575,25 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" @@ -1160,11 +1601,23 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" +checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "version_check" @@ -1172,6 +1625,27 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae21c12ad2ec2d168c236f369c38ff332bc1134f7246350dca641437365045" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" @@ -1183,12 +1657,11 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] @@ -1206,9 +1679,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1216,24 +1689,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1241,28 +1714,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" dependencies = [ "js-sys", "wasm-bindgen", @@ -1270,9 +1743,9 @@ dependencies = [ [[package]] name = "which" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", @@ -1327,9 +1800,18 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1342,72 +1824,76 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "xplr" -version = "0.20.2" +version = "0.21.0" dependencies = [ - "ansi-to-tui", + "ansi-to-tui-forked", "anyhow", "assert_cmd", - "chrono", "criterion", - "crossterm", + "crossterm 0.26.1", "dirs", - "fuzzy-matcher", "gethostname", "humansize", "indexmap", "lazy_static", "libc", + "lscolors", "mime_guess", "mlua", "natord", + "nu-ansi-term 0.47.0", "path-absolutize", + "ratatui", "regex", "serde", "serde_json", "serde_yaml", - "tui", + "skim", + "snailquote", + "textwrap", + "time 0.3.20", "tui-input", "which", ] diff --git a/Cargo.toml b/Cargo.toml index 0434897..2ca9148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ path = './benches/criterion.rs' [package] name = 'xplr' -version = '0.20.2' +version = '0.21.0' authors = ['Arijit Basu '] edition = '2021' description = 'A hackable, minimal, fast TUI file explorer' @@ -22,20 +22,29 @@ categories = ['command-line-interface', 'command-line-utilities'] include = ['src/**/*', 'docs/en/src/**/*', 'LICENSE', 'README.md'] [dependencies] -libc = "0.2.139" +libc = "0.2.140" humansize = "2.1.3" natord = "1.0.9" -anyhow = "1.0.68" -serde_yaml = "0.9.16" -crossterm = "0.25.0" -dirs = "4.0.0" -ansi-to-tui = "2.0.0" +anyhow = "1.0.70" +serde_yaml = "0.9.19" +crossterm = "0.26.1" +dirs = "5.0.0" +ansi-to-tui-forked = "3.0.0-ratatui" regex = "1.7.1" gethostname = "0.4.1" -fuzzy-matcher = "0.3.7" -serde_json = "1.0.91" +serde_json = "1.0.94" path-absolutize = "3.0.14" -which = "4.3.0" +which = "4.4.0" +nu-ansi-term = "0.47.0" +textwrap = "0.16" +snailquote = "0.3.1" +skim = "0.10.4" +time = { version = "0.3.20", features = ["serde", "local-offset", "formatting", "macros"] } + +[dependencies.lscolors] +version = "0.13.0" +default-features = false +features = ["nu-ansi-term"] [dependencies.lazy_static] version = "1.4.0" @@ -46,24 +55,21 @@ version = "2.0.4" features = ["rev-mappings"] [dependencies.tui] -version = "0.19.0" +version = "0.20.0" default-features = false features = ['crossterm', 'serde'] +package = 'ratatui' [dependencies.serde] -version = "1.0.152" +version = "1.0.157" features = ['derive'] -[dependencies.chrono] -version = "0.4.23" -features = ['serde'] - [dependencies.indexmap] version = "1.9.2" features = ['serde'] [dependencies.mlua] -version = "0.8.7" +version = "0.8.8" features = ['luajit', 'vendored', 'serialize', 'send'] [dependencies.tui-input] @@ -72,7 +78,7 @@ features = ['serde'] [dev-dependencies] criterion = "0.4.0" -assert_cmd = "2.0.8" +assert_cmd = "2.0.10" [profile.release] lto = true diff --git a/benches/criterion.rs b/benches/criterion.rs index 98f7d9b..04d31a4 100644 --- a/benches/criterion.rs +++ b/benches/criterion.rs @@ -18,8 +18,9 @@ fn navigation_benchmark(c: &mut Criterion) { }); let lua = mlua::Lua::new(); - let mut app = app::App::create(PWD.into(), &lua, None, [].into()) - .expect("failed to create app"); + let mut app = + app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into()) + .expect("failed to create app"); app = app .clone() @@ -97,8 +98,9 @@ fn draw_benchmark(c: &mut Criterion) { }); let lua = mlua::Lua::new(); - let mut app = app::App::create(PWD.into(), &lua, None, [].into()) - .expect("failed to create app"); + let mut app = + app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into()) + .expect("failed to create app"); app = app .clone() diff --git a/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index 0677e98..b3a8abc 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -22,6 +22,7 @@ - [Input Operation][39] - [Borders][31] - [Style][11] + - [Searching][41] - [Sorting][12] - [Filtering][13] - [Column Renderer][26] @@ -79,3 +80,4 @@ [38]: messages.md [39]: input-operation.md [40]: xplr.util.md +[41]: searching.md diff --git a/docs/en/src/borders.md b/docs/en/src/borders.md index 713fac7..3ed204f 100644 --- a/docs/en/src/borders.md +++ b/docs/en/src/borders.md @@ -20,6 +20,10 @@ A border can be one of the following: - Double - Thick +### Border Style + +The [style][1] of the borders. + ## Example ```lua @@ -28,3 +32,5 @@ xplr.config.general.panel_ui.default.border_type = "Thick" xplr.config.general.panel_ui.default.border_style.fg = "Black" xplr.config.general.panel_ui.default.border_style.bg = "Gray" ``` + +[1]: style.md#style diff --git a/docs/en/src/column-renderer.md b/docs/en/src/column-renderer.md index c3d1509..40a7928 100644 --- a/docs/en/src/column-renderer.md +++ b/docs/en/src/column-renderer.md @@ -73,6 +73,7 @@ The special argument contains the following fields - [is_selected][25] - [is_focused][26] - [total][27] +- [style][38] - [meta][28] ### parent @@ -254,6 +255,12 @@ Type: integer The total number of the nodes. +### style + +Type: [Style][39] + +The applicable [style object][39] for the node. + ### meta Type: mapping of string and string @@ -333,3 +340,5 @@ It contains the following fields. [35]: #last_modified [36]: #uid [37]: #gid +[38]: #style +[39]: style.md#style diff --git a/docs/en/src/default-key-bindings.md b/docs/en/src/default-key-bindings.md index 871722b..54c282d 100644 --- a/docs/en/src/default-key-bindings.md +++ b/docs/en/src/default-key-bindings.md @@ -23,8 +23,10 @@ of [modes][4] and the key mappings for each mode. | G | | go to bottom | | V | ctrl-a | select/unselect all | | ctrl-d | | duplicate as | -| ctrl-i | tab | next visited path | +| ctrl-i | | next visited path | +| ctrl-n | | next selection | | ctrl-o | | last visited path | +| ctrl-p | | prev selection | | ctrl-r | | refresh screen | | ctrl-u | | clear selection | | ctrl-w | | switch layout | @@ -47,89 +49,16 @@ of [modes][4] and the key mappings for each mode. | ~ | | go home | | [0-9] | | input | -### filter - -| key | remaps | action | -| --------- | ------ | ---------------------------------- | -| R | | relative path does not match regex | -| backspace | | remove last filter | -| ctrl-r | | reset filters | -| ctrl-u | | clear filters | -| r | | relative path does match regex | - ### vroot | key | remaps | action | | ------ | ------ | ------------ | | . | | vroot $PWD | | / | | vroot / | -| ~ | | vroot $HOME | -| v | | toggle vroot | | ctrl-r | | reset vroot | | ctrl-u | | unset vroot | - -### create_file - -| key | remaps | action | -| ----- | ------ | ------------ | -| enter | | submit | -| tab | | try complete | - -### selection_ops - -| key | remaps | action | -| --- | ------ | --------------- | -| c | | copy here | -| m | | move here | -| u | | clear selection | - -### create - -| key | remaps | action | -| --- | ------ | ---------------- | -| d | | create directory | -| f | | create file | - -### quit - -| key | remaps | action | -| ----- | ------ | ----------------------- | -| enter | | just quit | -| f | | quit printing focus | -| p | | quit printing pwd | -| r | | quit printing result | -| s | | quit printing selection | - -### switch_layout - -| key | remaps | action | -| --- | ------ | -------------------- | -| 1 | | default | -| 2 | | no help menu | -| 3 | | no selection panel | -| 4 | | no help or selection | - -### delete - -| key | remaps | action | -| --- | ------ | ------------ | -| D | | force delete | -| d | | delete | - -### relative_path_does_not_match_regex - -| key | remaps | action | -| ----- | ------ | ------ | -| enter | | submit | - -### number - -| key | remaps | action | -| ----- | ------ | -------- | -| down | j | to down | -| enter | | to index | -| k | up | to up | -| [0-9] | | input | +| v | | toggle vroot | +| ~ | | vroot $HOME | ### relative_path_does_match_regex @@ -137,7 +66,7 @@ of [modes][4] and the key mappings for each mode. | ----- | ------ | ------ | | enter | | submit | -### create_directory +### go_to_path | key | remaps | action | | ----- | ------ | ------------ | @@ -151,19 +80,24 @@ of [modes][4] and the key mappings for each mode. | enter | | submit | | tab | | try complete | -### rename +### debug_error -| key | remaps | action | -| ----- | ------ | ------------ | -| enter | | submit | -| tab | | try complete | +| key | remaps | action | +| ----- | ------ | ------------------- | +| enter | | open logs in editor | +| q | | quit | -### go_to_path +### selection_ops -| key | remaps | action | -| ----- | ------ | ------------ | -| enter | | submit | -| tab | | try complete | +| key | remaps | action | +| --- | ------ | --------------- | +| c | | copy here | +| e | | edit selection | +| h | | hardlink here | +| l | | list selection | +| m | | move here | +| s | | softlink here | +| u | | clear selection | ### sort @@ -189,24 +123,86 @@ of [modes][4] and the key mappings for each mode. | r | | by relative path | | s | | by size | +### go_to + +| key | remaps | action | +| --- | ------ | -------------- | +| f | | follow symlink | +| g | | top | +| i | | initial $PWD | +| p | | path | +| x | | open in gui | + +### edit_permissions + +| key | remaps | action | +| ------ | ------ | ------ | +| G | | -group | +| M | | min | +| O | | -other | +| U | | -user | +| ctrl-r | | reset | +| enter | | submit | +| g | | +group | +| m | | max | +| o | | +other | +| u | | +user | + +### switch_layout + +| key | remaps | action | +| --- | ------ | -------------------- | +| 1 | | default | +| 2 | | no help menu | +| 3 | | no selection panel | +| 4 | | no help or selection | + +### create + +| key | remaps | action | +| --- | ------ | ---------------- | +| d | | create directory | +| f | | create file | + +### create_directory + +| key | remaps | action | +| ----- | ------ | ------------ | +| enter | | submit | +| tab | | try complete | + +### create_file + +| key | remaps | action | +| ----- | ------ | ------------ | +| enter | | submit | +| tab | | try complete | + ### search -| key | remaps | action | -| ------ | ------ | ---------------- | -| ctrl-n | down | down | -| ctrl-p | up | up | -| enter | | submit | -| esc | | cancel | -| left | | back | -| right | | enter | -| tab | | toggle selection | +| key | remaps | action | +| ------ | ------ | ----------------------- | +| ctrl-a | | toggle search algorithm | +| ctrl-f | | fuzzy search | +| ctrl-n | down | down | +| ctrl-p | up | up | +| ctrl-r | | regex search | +| ctrl-s | | sort (no search order) | +| ctrl-z | | toggle ordering | +| enter | | submit | +| esc | | cancel | +| left | | back | +| right | | enter | +| tab | | toggle selection | -### debug_error +### number -| key | remaps | action | -| ----- | ------ | ------------------- | -| enter | | open logs in editor | -| q | | quit | +| key | remaps | action | +| ----- | ------ | -------- | +| down | j | to down | +| enter | | to index | +| k | up | to up | +| [0-9] | | input | ### action @@ -217,22 +213,53 @@ of [modes][4] and the key mappings for each mode. | e | | open in editor | | l | | logs | | m | | toggle mouse | +| p | | edit permissions | | q | | quit options | | s | | selection operations | | v | | vroot | | [0-9] | | go to index | +### filter + +| key | remaps | action | +| --------- | ------ | ---------------------------------- | +| R | | relative path does not match regex | +| backspace | | remove last filter | +| ctrl-r | | reset filters | +| ctrl-u | | clear filters | +| r | | relative path does match regex | + +### rename + +| key | remaps | action | +| ----- | ------ | ------------ | +| enter | | submit | +| tab | | try complete | + +### relative_path_does_not_match_regex + +| key | remaps | action | +| ----- | ------ | ------ | +| enter | | submit | + +### quit + +| key | remaps | action | +| ----- | ------ | ----------------------- | +| enter | | just quit | +| f | | quit printing focus | +| p | | quit printing pwd | +| r | | quit printing result | +| s | | quit printing selection | + ### recover | key | remaps | action | | --- | ------ | ------ | -### go_to +### delete -| key | remaps | action | -| --- | ------ | -------------- | -| f | | follow symlink | -| g | | top | -| i | | initial $PWD | -| p | | path | -| x | | open in gui | +| key | remaps | action | +| --- | ------ | ------------ | +| D | | force delete | +| d | | delete | diff --git a/docs/en/src/general-config.md b/docs/en/src/general-config.md index 4515723..ecd73ec 100644 --- a/docs/en/src/general-config.md +++ b/docs/en/src/general-config.md @@ -181,6 +181,30 @@ Constraint for the column widths. Type: nullable list of [Constraint](https://xplr.dev/en/layouts#constraint) +#### xplr.config.general.selection.item.format + +Renderer for each item in the selection list. + +Type: nullable string + +#### xplr.config.general.selection.item.style + +Style for each item in the selection list. + +Type: [Style](https://xplr.dev/en/style) + +#### xplr.config.general.search.algorithm + +The default search algorithm + +Type: [Search Algorithm](https://xplr.dev/en/searching#algorithm) + +#### xplr.config.general.search.unordered + +The default search ordering + +Type: boolean + #### xplr.config.general.default_ui.prefix The content that is placed before the item name for each row by default. @@ -322,12 +346,24 @@ Type: nullable mapping of the following key-value pairs: - format: nullable string - style: [Style](https://xplr.dev/en/style) -#### xplr.config.general.sort_and_filter_ui.search_identifier +#### xplr.config.general.sort_and_filter_ui.search_identifiers The identifiers used to denote applied search input. Type: { format = nullable string, style = [Style](https://xplr.dev/en/style) } +#### xplr.config.general.sort_and_filter_ui.search_direction_identifiers.ordered.format + +The shape of ordered indicator for search ordering identifiers in Sort & filter panel. + +Type: nullable string + +#### xplr.config.general.sort_and_filter_ui.search_direction_identifiers.unordered.format + +The shape of unordered indicator for search ordering identifiers in Sort & filter panel. + +Type: nullable string + #### xplr.config.general.panel_ui.default.title.format The content for panel title by default. diff --git a/docs/en/src/layout.md b/docs/en/src/layout.md index e187e00..e2d09d1 100644 --- a/docs/en/src/layout.md +++ b/docs/en/src/layout.md @@ -32,56 +32,61 @@ A layout can be one of the following: - [Selection][11] - [HelpMenu][12] - [SortAndFilter][13] -- [CustomContent][25] +- [Static][25] +- [Dynamic][26] - [Horizontal][14] - [Vertical][16] +- CustomContent (deprecated, use `Static` or `Dynamic`) ### Nothing This layout contains a blank panel. -Example: "Nothing" +Type: "Nothing" ### Table -This layout contains the table displaying the files and directories in the -current directory. +This layout contains the table displaying the files and directories in the current +directory. ### InputAndLogs This layout contains the panel displaying the input prompt and logs. -Example: "InputAndLogs" +Type: "InputAndLogs" ### Selection This layout contains the panel displaying the selected paths. -Example: "Selection" +Type: "Selection" ### HelpMenu This layout contains the panel displaying the help menu for the current mode in real-time. -Example: "HelpMenu" +Type: "HelpMenu" ### SortAndFilter -This layout contains the panel displaying the pipeline of sorters and filters -applied of the list of paths being displayed. +This layout contains the panel displaying the pipeline of sorters and filters applied on +the list of paths being displayed. -Example: "SortAndFilter" +Type: "SortAndFilter" -### Custom Content +### Static -Custom content is a special layout to render something custom. -It contains the following information: +This is a custom layout to render static content. + +Type: { Static = [Custom Panel][27] } + +### Dynamic -- [title][33] -- [body][34] +This is a custom layout to render dynamic content using a function defined in +[xplr.fn][28] that takes [Content Renderer Argument][36] and returns [Custom Panel][27]. -Example: { CustomContent = { title = [title][33], body = [body][34] } +Type: { Dynamic = [Content Renderer][35] } ### Horizontal @@ -92,7 +97,7 @@ It contains the following information: - [config][15] - [splits][17] -Example: { Horizontal = { config = [config][15], splits = [splits][17] } +Type: { Horizontal = { config = [config][15], splits = [splits][17] } ### Vertical @@ -103,7 +108,7 @@ It contains the following information: - [config][15] - [splits][17] -Example: { Vertical = { config = [config][15], splits = [splits][17] } +Type: { Vertical = { config = [config][15], splits = [splits][17] } ## Layout Config @@ -166,187 +171,153 @@ Type: list of [Layout][3] The list of child layouts to fit into the parent layout. -## title - -Type: nullable string - -The title of the panel. - -## body +## Custom Panel -Type: [Content Body][26] +Custom panel can be one of the following: -The body of the panel. +- [CustomParagraph][29] +- [CustomList][30] +- [CustomTable][31] -## Content Body - -Content body can be one of the following: - -- [StaticParagraph][27] -- [DynamicParagraph][28] -- [StaticList][29] -- [DynamicList][30] -- [StaticTable][31] -- [DynamicTable][32] - -## Static Paragraph +### CustomParagraph A paragraph to render. It contains the following fields: -- **render** (string): The string to render. +- **ui** (nullable [Panel UI Config][32]): Optional UI config for the panel. +- **body** (string): The string to render. #### Example: Render a custom static paragraph ```lua xplr.config.layouts.builtin.default = { - CustomContent = { - title = "custom title", - body = { - StaticParagraph = { render = "custom body" }, + Static = { + CustomParagraph = { + ui = { title = { format = " custom title " } }, + body = "custom body", }, }, } ``` -## Dynamic Paragraph - -A [Lua function][35] to render a custom paragraph. -It contains the following fields: - -- **render** (string): The [lua function][35] that returns the paragraph to - render. - #### Example: Render a custom dynamic paragraph ```lua -xplr.config.layouts.builtin.default = { - CustomContent = { - title = "custom title", - body = { DynamicParagraph = { render = "custom.render_layout" } }, - }, -} +xplr.config.layouts.builtin.default = { Dynamic = "custom.render_layout" } xplr.fn.custom.render_layout = function(ctx) - return ctx.app.pwd + return { + CustomParagraph = { + ui = { title = { format = ctx.app.pwd } }, + body = xplr.util.to_yaml(ctx.app.focused_node), + }, + } end ``` -## Static List +### CustomList A list to render. It contains the following fields: -- **render** (list of string): The list to render. +- **ui** (nullable [Panel UI Config][32]): Optional UI config for the panel. +- **body** (list of string): The list of strings to display. #### Example: Render a custom static list ```lua xplr.config.layouts.builtin.default = { - CustomContent = { - title = "custom title", - body = { - StaticList = { render = { "1", "2", "3" } }, + Static = { + CustomList = { + ui = { title = { format = " custom title " } }, + body = { "1", "2", "3" }, }, }, } ``` -## Dynamic List - -A [Lua function][35] to render a custom list. -It contains the following fields: - -- **render** (string): The [lua function][35] that returns the list to render. - #### Example: Render a custom dynamic list ```lua -xplr.config.layouts.builtin.default = { - CustomContent = { - title = "custom title", - body = { DynamicList = { render = "custom.render_layout" } }, - }, -} +xplr.config.layouts.builtin.default = { Dynamic = "custom.render_layout" } xplr.fn.custom.render_layout = function(ctx) return { - ctx.app.pwd, - ctx.app.version, - tostring(ctx.app.pid), + CustomList = { + ui = { title = { format = ctx.app.pwd } }, + body = { + (ctx.app.focused_node or {}).relative_path or "", + ctx.app.version, + tostring(ctx.app.pid), + }, + }, } end ``` -## Static Table +## CustomTable -A table to render. It contains the following fields: +A custom table to render. It contains the following fields: +- **ui** (nullable [Panel UI Config][32]): Optional UI config for the panel. - **widths** (list of [Constraint][22]): Width of the columns. - **col_spacing** (nullable int): Spacing between columns. Defaults to 1. -- **render** (list of list of string): The rows and columns to render. +- **body** (list of list of string): The rows and columns to render. #### Example: Render a custom static table ```lua xplr.config.layouts.builtin.default = { - CustomContent = { - title = "custom title", - body = { - StaticTable = { - widths = { - { Percentage = 50 }, - { Percentage = 50 }, - }, - col_spacing = 1, - render = { - { "a", "b" }, - { "c", "d" }, - }, + Static = { + CustomTable = { + ui = { title = { format = " custom title " } }, + widths = { + { Percentage = 50 }, + { Percentage = 50 }, + }, + body = { + { "a", "b" }, + { "c", "d" }, }, }, }, } ``` -## Dynamic Table - -A [Lua function][35] to render a custom table. -It contains the following fields: - -- **widths** (list of [Constraint][22]): Width of the columns. -- **col_spacing** (nullable int): Spacing between columns. Defaults to 1. -- **render** (string): The [lua function][35] that returns the table to render. - #### Example: Render a custom dynamic table ```lua -xplr.config.layouts.builtin.default = { - CustomContent = { - title = "custom title", - body = { - DynamicTable = { - widths = { - { Percentage = 50 }, - { Percentage = 50 }, - }, - col_spacing = 1, - render = "custom.render_layout", - }, - }, - }, -} +xplr.config.layouts.builtin.default = {Dynamic = "custom.render_layout" } xplr.fn.custom.render_layout = function(ctx) return { - { "", "" }, - { "Layout height", tostring(ctx.layout_size.height) }, - { "Layout width", tostring(ctx.layout_size.width) }, - { "", "" }, - { "Screen height", tostring(ctx.screen_size.height) }, - { "Screen width", tostring(ctx.screen_size.width) }, + CustomTable = { + ui = { title = { format = ctx.app.pwd } }, + widths = { + { Percentage = 50 }, + { Percentage = 50 }, + }, + body = { + { "", "" }, + { "Layout height", tostring(ctx.layout_size.height) }, + { "Layout width", tostring(ctx.layout_size.width) }, + { "", "" }, + { "Screen height", tostring(ctx.screen_size.height) }, + { "Screen width", tostring(ctx.screen_size.width) }, + }, + }, } end ``` +## Panel UI Config + +It contains the following optional fields: + +- **title** ({ format = "string", style = [Style][33] }): the title of the panel. +- **style** ([Style][33]): The style of the panel body. +- **borders** (nullable list of [Border][34]): The shape of the borders. +- **border_type** ([Border Type][54]): The type of the borders. +- **border_style** ([Style][33]): The style of the borders. + ## Content Renderer It is a Lua function that receives [a special argument][36] as input and @@ -421,16 +392,16 @@ Hence, only the following fields are avilable. [22]: #constraint [23]: https://s6.gifyu.com/images/layout.png [24]: https://gifyu.com/image/1X38 -[25]: #custom-content -[26]: #content-body -[27]: #static-paragraph -[28]: #dynamic-paragraph -[29]: #static-list -[30]: #dynamic-list -[31]: #static-table -[32]: #dynamic-table -[33]: #title -[34]: #body +[25]: #static +[26]: #dynamic +[27]: #custom-panel +[28]: configuration.md#function +[29]: #customparagraph +[30]: #customlist +[31]: #customtable +[32]: #panel-ui-config +[33]: style.md#style +[34]: borders.md#border [35]: #content-renderer [36]: #content-renderer-argument [37]: #size @@ -450,3 +421,4 @@ Hence, only the following fields are avilable. [51]: layouts.md [52]: lua-function-calls.md#vroot [53]: lua-function-calls.md#initial_pwd +[54]: borders.md#border-type diff --git a/docs/en/src/lua-function-calls.md b/docs/en/src/lua-function-calls.md index 4278d1e..89bc1f8 100644 --- a/docs/en/src/lua-function-calls.md +++ b/docs/en/src/lua-function-calls.md @@ -368,26 +368,9 @@ Type: list of [Node Sorter Applicable][81] ### searcher -Type: nullable [Node Searcher][82] +The searcher to use (if any). -## Node Searcher - -Node Searcher contains the following fields: - -- [pattern][83] -- [recoverable_focus][84] - -### pattern - -The patters used to search. - -Type: string - -### recoverable_focus - -Where to focus when search is cancelled. - -Type: nullable string +Type: nullable [Node Searcher Applicable][82] ## Also Ssee: @@ -457,7 +440,5 @@ Type: nullable string [79]: #searcher [80]: filtering.md#node-filter-applicable [81]: sorting.md#node-sorter-applicable -[82]: #node-searcher -[83]: #pattern -[84]: #recoverable_focus +[82]: searching.md#node-searcher-applicable [85]: xplr.util.md diff --git a/docs/en/src/messages.md b/docs/en/src/messages.md index fb52de6..a89c5e1 100644 --- a/docs/en/src/messages.md +++ b/docs/en/src/messages.md @@ -97,6 +97,15 @@ Example: - Lua: `"FocusNext"` - YAML: `FocusNext` +#### FocusNextSelection + +Focus on the next selected node. + +Example: + +- Lua: `"FocusNextSelection"` +- YAML: `FocusNextSelection` + #### FocusNextByRelativeIndex Focus on the `n`th node relative to the current focus where `n` is a @@ -128,6 +137,15 @@ Example: - Lua: `"FocusPrevious"` - YAML: `FocusPrevious` +#### FocusPreviousSelection + +Focus on the previous selection item. + +Example: + +- Lua: `"FocusPreviousSelection"` +- YAML: `FocusPreviousSelection` + #### FocusPreviousByRelativeIndex Focus on the `-n`th node relative to the current focus where `n` is a @@ -1012,6 +1030,28 @@ Example: ### Search Operations +#### Search + +Search files using the current or default (fuzzy) search algorithm. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { Search = "string" } + +Example: + +- Lua: `{ Search = "pattern" }` +- YAML: `Search: pattern` + +#### SearchFromInput + +Calls `Search` with the input taken from the input buffer. + +Example: + +- Lua: `"SearchFromInput"` +- YAML: `SearchFromInput` + #### SearchFuzzy Search files using fuzzy match algorithm. @@ -1030,12 +1070,126 @@ Example: Calls `SearchFuzzy` with the input taken from the input buffer. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. Example: - Lua: `"SearchFuzzyFromInput"` - YAML: `SearchFuzzyFromInput` +#### SearchFuzzyUnordered + +Like `SearchFuzzy`, but doesn't not perform rank based sorting. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { SearchFuzzyUnordered = "string" } + +Example: + +- Lua: `{ SearchFuzzyUnordered = "pattern" }` +- YAML: `SearchFuzzyUnordered: pattern` + +#### SearchFuzzyUnorderedFromInput + +Calls `SearchFuzzyUnordered` with the input taken from the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Example: + +- Lua: `"SearchFuzzyUnorderedFromInput"` +- YAML: `SearchFuzzyUnorderedFromInput` + +#### SearchRegex + +Search files using regex match algorithm. +It keeps the filters, but overrides the sorters. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { SearchRegex = "string" } + +Example: + +- Lua: `{ SearchRegex = "pattern" }` +- YAML: `SearchRegex: pattern` + +#### SearchRegexFromInput + +Calls `SearchRegex` with the input taken from the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Example: + +- Lua: `"SearchRegexFromInput"` +- YAML: `SearchRegexFromInput` + +#### SearchRegexUnordered + +Like `SearchRegex`, but doesn't not perform rank based sorting. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Type: { SearchRegexUnordered = "string" } + +Example: + +- Lua: `{ SearchRegexUnordered = "pattern" }` +- YAML: `SearchRegexUnordered: pattern` + +#### SearchRegexUnorderedFromInput + +Calls `SearchRegexUnordered` with the input taken from the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. +It gets reset automatically when changing directory. + +Example: + +- Lua: `"SearchRegexUnorderedFromInput"` +- YAML: `SearchRegexUnorderedFromInput` + +#### ToggleSearchAlgorithm + +Toggles between different search algorithms, without changing the input +buffer +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + +Example: + +- Lua: `"ToggleSearchAlgorithm"` +- YAML: `ToggleSearchAlgorithm` + +#### EnableSearchOrder + +Enables ranked search without changing the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + +Example: + +- Lua: `"EnableOrderedSearch"` +- YAML: `EnableSearchOrder` + +#### DisableSearchOrder + +Disabled ranked search without changing the input buffer. +You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + +Example: + +- Lua: `"DisableSearchOrder"` +- YAML: `DisableSearchOrder` + +#### ToggleSearchOrder + +Toggles ranked search without changing the input buffer. + +Example: + +- Lua: `"ToggleSearchOrder"` +- YAML: `ToggleSearchOrder` + #### AcceptSearch Accepts the search by keeping the latest focus while in search mode. diff --git a/docs/en/src/modes.md b/docs/en/src/modes.md index fdb0ba5..38d999c 100644 --- a/docs/en/src/modes.md +++ b/docs/en/src/modes.md @@ -143,6 +143,12 @@ The builtin vroot mode. Type: [Mode](https://xplr.dev/en/mode) +#### xplr.config.modes.builtin.edit_permissions + +The builtin edit permissions mode. + +Type: [Mode](https://xplr.dev/en/mode) + #### xplr.config.modes.custom This is where you define custom modes. diff --git a/docs/en/src/searching.md b/docs/en/src/searching.md new file mode 100644 index 0000000..bfb8fd7 --- /dev/null +++ b/docs/en/src/searching.md @@ -0,0 +1,77 @@ +# Searching + +xplr supports searching paths using different algorithm. The search mechanism +generally appears between filters and sorters in the `Sort & filter` panel. + +Example: + +``` +fzy:foo↓ +``` + +This line means that the nodes visible on the table are being filtered using the +[fuzzy matching][1] algorithm on the input `foo`. The arrow means that ranking based +ordering is being applied, i.e. [sorters][2] are being ignored. + +## Node Searcher Applicable + +Node Searcher contains the following fields: + +- [pattern][3] +- [recoverable_focus][4] +- [algorithm][5] +- [unordered][7] + +### pattern + +The patters used to search. + +Type: string + +### recoverable_focus + +Where to focus when search is cancelled. + +Type: nullable string + +### algorithm + +Search algorithm to use. Defaults to the value set in +[xplr.config.general.search.algorithm][8]. + +It can be one of the following: + +- Fuzzy +- Regex + +### unordered + +Whether to skip ordering the search result by algorithm based ranking. Defaults +to the value set in [xplr.config.general.search.unordered][9]. + +Type: boolean + +## Example: + +```lua +local searcher = { + pattern = "pattern to search", + recoverable_focus = "/path/to/focus/on/cancel", + algorithm = "Fuzzy", + unordered = false, +} + +xplr.util.explore({ searcher = searcher }) +``` + +See [xplr.util.explore][6]. + +[1]: https://en.wikipedia.org/wiki/Approximate_string_matching +[2]: sorting.md +[3]: #pattern +[4]: #recoverable_focus +[5]: #algorithm +[6]: xplr.util.md#xplrutilexplore +[7]: #unordered +[8]: general-config.md#xplrconfiggeneralsearchalgorithm +[9]: general-config.md#xplrconfiggeneralsearchunordered diff --git a/docs/en/src/upgrade-guide.md b/docs/en/src/upgrade-guide.md index 32660a8..af9fcd3 100644 --- a/docs/en/src/upgrade-guide.md +++ b/docs/en/src/upgrade-guide.md @@ -45,6 +45,82 @@ compatibility. ### Instructions +#### [v0.20.2][48] -> [v0.21.0][49] + +- Some plugins might stop rendering colors. Wait for them to update. +- Rename `xplr.config.general.sort_and_filter_ui.search_identifier` to + `xplr.config.general.sort_and_filter_ui.search_identifiers`. +- Resolved Node API will not contain the `permissions` field anymore. + Use the utility function `xplr.util.node` to get its permissions. +- Layout `CustomContent` has been undocumented. It will stay for compatibility, + but you should prefer using the following new layouts, because they support + custom title: + - Static + - Dynamic +- Use the new messages for improved search operations: + - Search + - SearchFromInput + - SearchFuzzyUnordered + - SearchFuzzyUnorderedFromInput + - SearchRegex + - SearchRegexFromInput + - SearchRegexUnordered + - SearchRegexUnorderedFromInput + - ToggleSearchAlgorithm + - EnableSearchOrder + - DisableSearchOrder + - ToggleSearchOrder +- Use skim's [search syntax][50] to customize the search. +- Set your preferred search algorithm and ordering: + `xplr.config.general.search.algorithm = "Fuzzy" -- or "Regex"`. + `xplr.config.general.search.unordered = false -- or true` +- You need to clear the selection list manually after performing batch + operation like copy, softlink creation etc. +- Use the following new key bindings: + - `:sl` to list selection in a $PAGER. + - `:ss` to create softlink of the selected items. + - `:sh` to create hardlink of the selected items. + - `:se` to edit selection list in your $EDITOR. + - Better conflict handling: add suffix rather than overriding/skipping. +- Navigate between the selected paths using the following messages: + - FocusPreviousSelection (`ctrl-p`) + - FocusNextSelection (`ctrl-n`) +- Use `LS_COLORS` environment variable, along with the following utility +- functions for applying better styling/theaming. + - xplr.util.lscolor + - xplr.util.paint + - xplr.util.textwrap + - xplr.util.style_mix +- Use new the fields in Column Renderer Argument: + - style + - permissions +- Use the following config to specify how the paths in selection list should be + rendered: + - xplr.config.general.selection.item.format + - xplr.config.general.selection.item.style +- Use the following utility functions to work with teh file permissions: + - xplr.util.permissions_rwx + - xplr.util.permissions_octal +- Type `:p` to edit file permissions interactively. +- Also check out the following utility functions: + - xplr.util.layout_replace + - xplr.util.relative_to + - xplr.util.shorthand + - xplr.util.clone + - xplr.util.exists + - xplr.util.is_dir + - xplr.util.is_file + - xplr.util.is_symlink + - xplr.util.is_absolute + - xplr.util.path_split + - xplr.util.node + - xplr.util.node_type + - xplr.util.shell_escape +- Executables will me marked with the mime type: `application/x-executable`. +- macOS legacy coreutils will be generally supported, but please update it. + +Thanks to @noahmayr for contributing to a major part of this release. + #### [v0.19.4][47] -> [v0.20.2][48] - BREAKING: xplr shell (`:!`) will default to null (`\0`) delimited pipes, as @@ -440,3 +516,5 @@ Else do the following: [46]: https://github.com/sayanarijit/xplr/releases/tag/v0.18.0 [47]: https://github.com/sayanarijit/xplr/releases/tag/v0.19.4 [48]: https://github.com/sayanarijit/xplr/releases/tag/v0.20.2 +[49]: https://github.com/sayanarijit/xplr/releases/tag/v0.21.0 +[50]: https://github.com/lotabout/skim#search-syntax diff --git a/docs/en/src/xplr.util.md b/docs/en/src/xplr.util.md index 7a856f7..3ce26b6 100644 --- a/docs/en/src/xplr.util.md +++ b/docs/en/src/xplr.util.md @@ -11,6 +11,141 @@ xplr.util.version() -- { major = 0, minor = 0, patch = 0 } ``` +### xplr.util.clone + +Clone/deepcopy a Lua value. Doesn't work with functions. + +Type: function( value ) -> value + +Example: + +```lua +local val = { foo = "bar" } +local val_clone = xplr.util.clone(val) +val.foo = "baz" +print(val_clone.foo) +-- "bar" +``` + +### xplr.util.exists + +Check if the given path exists. + +Type: function( path:string ) -> boolean + +Example: + +```lua +xplr.util.exists("/foo/bar") +-- true +``` + +### xplr.util.is_dir + +Check if the given path is a directory. + +Type: function( path:string ) -> boolean + +Example: + +```lua +xplr.util.is_dir("/foo/bar") +-- true +``` + +### xplr.util.is_file + +Check if the given path is a file. + +Type: function( path:string ) -> boolean + +Example: + +```lua +xplr.util.is_file("/foo/bar") +-- true +``` + +### xplr.util.is_symlink + +Check if the given path is a symlink. + +Type: function( path:string ) -> boolean + +Example: + +```lua +xplr.util.is_file("/foo/bar") +-- true +``` + +### xplr.util.is_absolute + +Check if the given path is an absolute path. + +Type: function( path:string ) -> boolean + +Example: + +```lua +xplr.util.is_absolute("/foo/bar") +-- true +``` + +### xplr.util.path_split + +Split a path into its components. + +Type: function( path:string ) -> boolean + +Example: + +```lua +xplr.util.path_split("/foo/bar") +-- { "/", "foo", "bar" } + +xplr.util.path_split(".././foo") +-- { "..", "foo" } +``` + +### xplr.util.node + +Get [Node][5] information of a given path. +Doesn't check if the path exists. +Returns nil if the path is "/". +Errors out if absolute path can't be obtained. + +Type: function( path:string ) -> [Node][5]|nil + +Example: + +```lua +xplr.util.node("./bar") +-- { parent = "/pwd", relative_path = "bar", absolute_path = "/pwd/bar", ... } + +xplr.util.node("/") +-- nil +``` + +### xplr.util.node_type + +Get the configured [Node Type][6] of a given [Node][5]. + +Type: function( [Node][5], [xplr.config.node_types][7]|nil ) -> [Node Type][6] + +If the second argument is missing, global config `xplr.config.node_types` +will be used. + +Example: + +```lua +xplr.util.node_type(app.focused_node) +-- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... } + +xplr.util.node_type(xplr.util.node("/foo/bar"), xplr.config.node_types) +-- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... } +``` + ### xplr.util.dirname Get the directory name of a given path. @@ -51,36 +186,108 @@ xplr.util.absolute("foo/bar") -- "/tmp/foo/bar" ``` +### xplr.util.relative_to + +Get the relative path based on the given base path or current working dir. +Will error if it fails to determine a relative path. + +Type: function( path:string, options:table|nil ) -> path:string + +Options type: { base:string|nil, with_prefix_dots:bookean|nil, without_suffix_dots:boolean|nil } + +- If `base` path is given, the path will be relative to it. +- If `with_prefix_dots` is true, the path will always start with dots `..` / `.` +- If `without_suffix_dots` is true, the name will be visible instead of dots `..` / `.` + +Example: + +```lua +xplr.util.relative_to("/present/working/directory") +-- "." + +xplr.util.relative_to("/present/working/directory/foo") +-- "foo" + +xplr.util.relative_to("/present/working/directory/foo", { with_prefix_dots = true }) +-- "./foo" + +xplr.util.relative_to("/present/working/directory", { without_suffix_dots = true }) +-- "../directory" + +xplr.util.relative_to("/present/working") +-- ".." + +xplr.util.relative_to("/present/working", { without_suffix_dots = true }) +-- "../../working" + +xplr.util.relative_to("/present/working/directory", { base = "/present/foo/bar" }) +-- "../../working/directory" +``` + +### xplr.util.shorten + +Shorten the given absolute path using the following rules: + +- either relative to your home dir if it makes sense +- or relative to the current working directory +- or absolute path if it makes the most sense + +Type: Similar to `xplr.util.relative_to` + +Example: + +```lua +xplr.util.shorten("/home/username/.config") +-- "~/.config" + +xplr.util.shorten("/present/working/directory") +-- "." + +xplr.util.shorten("/present/working/directory/foo") +-- "foo" + +xplr.util.shorten("/present/working/directory/foo", { with_prefix_dots = true }) +-- "./foo" + +xplr.util.shorten("/present/working/directory", { without_suffix_dots = true }) +-- "../directory" + +xplr.util.shorten("/present/working/directory", { base = "/present/foo/bar" }) +-- "../../working/directory" + +xplr.util.shorten("/tmp") +-- "/tmp" +``` + ### xplr.util.explore Explore directories with the given explorer config. -Type: function( path:string, config:[Explorer Config][1]|nil ) --> { node:[Node][2]... } +Type: function( path:string, [ExplorerConfig][1]|nil ) -> { [Node][2], ... } Example: ```lua xplr.util.explore("/tmp") +-- { { absolute_path = "/tmp/a", ... }, ... } + xplr.util.explore("/tmp", app.explorer_config) -- { { absolute_path = "/tmp/a", ... }, ... } ``` -[1]: https://xplr.dev/en/lua-function-calls#explorer-config -[2]: https://xplr.dev/en/lua-function-calls#node - ### xplr.util.shell_execute Execute shell commands safely. -Type: function( program:string, args:{ arg:string... }|nil ) --> { stdout = string, stderr = string, returncode = number|nil } +Type: function( program:string, args:{ string, ... }|nil ) -> { stdout = string, stderr = string, returncode = number|nil } Example: ```lua xplr.util.shell_execute("pwd") +-- "/present/working/directory" + xplr.util.shell_execute("bash", {"-c", "xplr --help"}) -- { stdout = "xplr...", stderr = "", returncode = 0 } ``` @@ -98,11 +305,24 @@ xplr.util.shell_quote("a'b\"c") -- 'a'"'"'b"c' ``` +### xplr.util.shell_escape + +Escape commands and paths safely. + +Type: function( string ) -> string + +Example: + +```lua +xplr.util.shell_escape("a'b\"c") +-- "\"a'b\\\"c\"" +``` + ### xplr.util.from_json Load JSON string into Lua value. -Type: function( string ) -> value +Type: function( string ) -> any Example: @@ -121,11 +341,11 @@ Example: ```lua xplr.util.to_json({ foo = "bar" }) --- [[{ "foos": "bar" }]] +-- [[{ "foo": "bar" }]] xplr.util.to_json({ foo = "bar" }, { pretty = true }) -- [[{ --- "foos": "bar" +-- "foo": "bar" -- }]] ``` @@ -154,3 +374,138 @@ Example: xplr.util.to_yaml({ foo = "bar" }) -- "foo: bar" ``` + +### xplr.util.lscolor + +Get a [Style][3] object for the given path based on the LS_COLORS +environment variable. + +Type: function( path:string ) -> [Style][3]|nil + +Example: + +```lua +xplr.util.lscolor("Desktop") +-- { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} } +``` + +### xplr.util.paint + +Apply style (escape sequence) to string using a given [Style][3] object. + +Type: function( string, [Style][3]|nil ) -> string + +Example: + +```lua +xplr.util.paint("Desktop", { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} }) +-- "\u001b[31mDesktop\u001b[0m" +``` + +### xplr.util.style_mix + +Mix multiple [Style][3] objects into one. + +Type: function( { [Style][3], [Style][3], ... } ) -> [Style][3] + +Example: + +```lua +xplr.util.style_mix({{ fg = "Red" }, { bg = "Blue" }, { add_modifiers = {"Bold"} }}) +-- { fg = "Red", bg = "Blue", add_modifiers = { "Bold" }, sub_modifiers = {} } +``` + +### xplr.util.textwrap + +Wrap the given text to fit the specified width. +It will try to not split words when possible. + +Type: function( string, options:number|table ) -> { string, ...} + +Options type: { width = number, initial_indent = string|nil, subsequent_indent = string|nil, break_words = boolean|nil } + +Example: + +```lua +xplr.util.textwrap("this will be cut off", 11) +-- { "this will', 'be cut off" } + +xplr.util.textwrap( + "this will be cut off", + { width = 12, initial_indent = "", subsequent_indent = " ", break_words = false } +) +-- { "this will be", " cut off" } +``` + +### xplr.util.layout_replace + +Find the target layout in the given layout and replace it with the replacement layout, +returning a new layout. + +Type: function( layout:[Layout][4], target:[Layout][4], replacement:[Layout][4] ) -> layout:[Layout][4] + +Example: + +```lua +local layout = { + Horizontal = { + splits = { + "Table", -- Target + "HelpMenu", + }, + config = ..., + } +} + +xplr.util.layout_replace(layout, "Table", "Selection") +-- { +-- Horizontal = { +-- splits = { +-- "Selection", -- Replacement +-- "HelpMenu", +-- }, +-- config = ... +-- } +-- } +``` + +### xplr.util.permissions_rwx + +Convert [Permission][8] to rwxrwxrwx representation with special bits. + +Type: function( [Permission][8] ) -> string + +Example: + +```lua +xplr.util.permissions_rwx({ user_read = true }) +-- "r--------" + +xplr.util.permissions_rwx(app.focused_node.permission) +-- "rwxrwsrwT" +``` + +### xplr.util.permissions_octal + +Convert [Permission][8] to octal representation. + +Type: function( [Permission][8] ) -> { number, number, number, number } + +Example: + +```lua +xplr.util.permissions_octal({ user_read = true }) +-- { 0, 4, 0, 0 } + +xplr.util.permissions_octal(app.focused_node.permission) +-- { 0, 7, 5, 4 } +``` + +[1]: https://xplr.dev/en/lua-function-calls#explorer-config +[2]: https://xplr.dev/en/lua-function-calls#node +[3]: https://xplr.dev/en/style +[4]: https://xplr.dev/en/layout +[5]: https://xplr.dev/en/lua-function-calls#node +[6]: https://xplr.dev/en/node-type +[7]: https://xplr.dev/en/node_types +[8]: https://xplr.dev/en/column-renderer#permission diff --git a/docs/script/generate.py b/docs/script/generate.py index b826e30..7a142ab 100644 --- a/docs/script/generate.py +++ b/docs/script/generate.py @@ -260,6 +260,9 @@ def gen_xplr_util(): print("\n".join(function.doc)) print("\n".join(function.doc), file=f) + if reading: + print("\n".join(reading.doc), file=f) + def format_docs(): os.system("prettier --write docs/en/src") diff --git a/src/app.rs b/src/app.rs index a1ecf05..cdb357f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,7 @@ pub use crate::msg::in_::external::Command; pub use crate::msg::in_::external::ExplorerConfig; pub use crate::msg::in_::external::NodeFilter; pub use crate::msg::in_::external::NodeFilterApplicable; -use crate::msg::in_::external::NodeSearcher; +use crate::msg::in_::external::NodeSearcherApplicable; pub use crate::msg::in_::external::NodeSorter; pub use crate::msg::in_::external::NodeSorterApplicable; pub use crate::msg::in_::ExternalMsg; @@ -19,9 +19,9 @@ pub use crate::msg::out::MsgOut; pub use crate::node::Node; pub use crate::node::ResolvedNode; pub use crate::pipe::Pipe; +use crate::search::SearchAlgorithm; use crate::ui::Layout; use anyhow::{bail, Result}; -use chrono::{DateTime, Local}; use gethostname::gethostname; use indexmap::set::IndexSet; use path_absolutize::*; @@ -31,6 +31,7 @@ use std::collections::VecDeque; use std::env; use std::fs; use std::path::PathBuf; +use time::OffsetDateTime; use tui_input::{Input, InputRequest}; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -62,7 +63,7 @@ pub enum LogLevel { pub struct Log { pub level: LogLevel, pub message: String, - pub created_at: DateTime, + pub created_at: OffsetDateTime, } impl Log { @@ -70,7 +71,9 @@ impl Log { Self { level, message, - created_at: Local::now(), + created_at: OffsetDateTime::now_local() + .ok() + .unwrap_or_else(OffsetDateTime::now_utc), } } } @@ -83,7 +86,7 @@ impl std::fmt::Display for Log { LogLevel::Success => "SUCCESS", LogLevel::Error => "ERROR ", }; - write!(f, "[{}] {} {}", &self.created_at, level_str, &self.message) + write!(f, "[{0}] {level_str} {1}", &self.created_at, &self.message) } } @@ -100,23 +103,67 @@ pub struct History { } impl History { + fn loc_exists(&self) -> bool { + self.peek() + .map(|p| PathBuf::from(p).exists()) + .unwrap_or(false) + } + + fn cleanup(mut self) -> Self { + while self.loc > 0 + && self + .paths + .get(self.loc.saturating_sub(1)) + .and_then(|p1| self.peek().map(|p2| p1 == p2)) + .unwrap_or(false) + { + self.paths.remove(self.loc); + self.loc = self.loc.saturating_sub(1); + } + + while self.loc < self.paths.len().saturating_sub(1) + && self + .paths + .get(self.loc.saturating_add(1)) + .and_then(|p1| self.peek().map(|p2| p1 == p2)) + .unwrap_or(false) + { + self.paths.remove(self.loc.saturating_add(1)); + } + + self + } + fn push(mut self, path: String) -> Self { if self.peek() != Some(&path) { self.paths = self.paths.into_iter().take(self.loc + 1).collect(); self.paths.push(path); - self.loc = self.paths.len().max(1) - 1; + self.loc = self.paths.len().saturating_sub(1); } self } fn visit_last(mut self) -> Self { - self.loc = self.loc.max(1) - 1; - self + self.loc = self.loc.saturating_sub(1); + + while self.loc > 0 && !self.loc_exists() { + self.paths.remove(self.loc); + self.loc = self.loc.saturating_sub(1); + } + self.cleanup() } fn visit_next(mut self) -> Self { - self.loc = (self.loc + 1).min(self.paths.len().max(1) - 1); - self + self.loc = self + .loc + .saturating_add(1) + .min(self.paths.len().saturating_sub(1)); + + while self.loc < self.paths.len().saturating_sub(1) && !self.loc_exists() { + self.paths.remove(self.loc); + } + + self.cleanup() } fn peek(&self) -> Option<&String> { @@ -165,7 +212,7 @@ pub struct InputBuffer { pub prompt: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct App { pub bin: String, pub version: String, @@ -415,7 +462,7 @@ impl App { fn handle_external(self, msg: ExternalMsg, key: Option) -> Result { if self.config.general.read_only && !msg.is_read_only() { - self.log_error("Cannot execute code in read-only mode.".into()) + self.log_error("could not execute code in read-only mode.".into()) } else { use ExternalMsg::*; match msg { @@ -427,6 +474,7 @@ impl App { FocusFirst => self.focus_first(true), FocusLast => self.focus_last(), FocusPrevious => self.focus_previous(), + FocusPreviousSelection => self.focus_previous_selection(), FocusPreviousByRelativeIndex(i) => { self.focus_previous_by_relative_index(i) } @@ -435,6 +483,7 @@ impl App { self.focus_previous_by_relative_index_from_input() } FocusNext => self.focus_next(), + FocusNextSelection => self.focus_next_selection(), FocusNextByRelativeIndex(i) => self.focus_next_by_relative_index(i), FocusNextByRelativeIndexFromInput => { self.focus_next_by_relative_index_from_input() @@ -524,8 +573,32 @@ impl App { ReverseNodeSorters => self.reverse_node_sorters(), ResetNodeSorters => self.reset_node_sorters(), ClearNodeSorters => self.clear_node_sorters(), - SearchFuzzy(p) => self.search_fuzzy(p), - SearchFuzzyFromInput => self.search_fuzzy_from_input(), + Search(p) => self.search(p), + SearchFromInput => self.search_from_input(), + SearchFuzzy(p) => self.search_with(p, SearchAlgorithm::Fuzzy, false), + SearchFuzzyFromInput => { + self.search_from_input_with(SearchAlgorithm::Fuzzy, false) + } + SearchRegex(p) => self.search_with(p, SearchAlgorithm::Regex, false), + SearchRegexFromInput => { + self.search_from_input_with(SearchAlgorithm::Regex, false) + } + SearchFuzzyUnordered(p) => { + self.search_with(p, SearchAlgorithm::Fuzzy, true) + } + SearchFuzzyUnorderedFromInput => { + self.search_from_input_with(SearchAlgorithm::Fuzzy, true) + } + SearchRegexUnordered(p) => { + self.search_with(p, SearchAlgorithm::Regex, true) + } + SearchRegexUnorderedFromInput => { + self.search_from_input_with(SearchAlgorithm::Regex, true) + } + EnableSearchOrder => self.enable_search_order(), + DisableSearchOrder => self.disable_search_order(), + ToggleSearchOrder => self.toggle_search_order(), + ToggleSearchAlgorithm => self.toggle_search_algorithm(), AcceptSearch => self.accept_search(), CancelSearch => self.cancel_search(), EnableMouse => self.enable_mouse(), @@ -592,7 +665,7 @@ impl App { if self.config.general.enable_recover_mode { vec![ExternalMsg::SwitchModeBuiltin("recover".into())] } else { - vec![ExternalMsg::LogWarning("Key map not found.".into())] + vec![ExternalMsg::LogWarning("key map not found.".into())] } }); @@ -606,14 +679,20 @@ impl App { pub fn explore_pwd(mut self) -> Result { let focus = &self.last_focus.get(&self.pwd).cloned().unwrap_or(None); let pwd = self.pwd.clone(); - self = self.add_last_focus(pwd, focus.clone())?; - let dir = explorer::explore_sync( + self = self.add_last_focus(pwd.clone(), focus.clone())?; + + match explorer::explore_sync( self.explorer_config.clone(), self.pwd.clone().into(), focus.as_ref().map(PathBuf::from), self.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), - )?; - self.set_directory(dir) + ) { + Ok(dir) => self.set_directory(dir), + Err(e) => { + self.directory_buffer = None; + self.log_error(format!("could not explore {pwd:?}: {e}")) + } + } } fn explore_pwd_async(mut self) -> Result { @@ -663,7 +742,7 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.focus = dir.total.max(1) - 1; + dir.focus = dir.total.saturating_sub(1); if let Some(n) = dir.focused_node() { self.history = history.push(n.absolute_path.clone()); @@ -680,15 +759,55 @@ impl App { if bounded { dir.focus } else { - dir.total.max(1) - 1 + dir.total.saturating_sub(1) } } else { - dir.focus.max(1) - 1 + dir.focus.saturating_sub(1) }; }; Ok(self) } + fn focus_previous_selection(mut self) -> Result { + let total = self.selection.len(); + if total == 0 { + return Ok(self); + } + + let bounded = self.config.general.enforce_bounded_index_navigation; + + if let Some(n) = self + .directory_buffer + .as_ref() + .and_then(|d| d.focused_node()) + { + if let Some(idx) = self.selection.get_index_of(n) { + let idx = if idx == 0 { + if bounded { + idx + } else { + total.saturating_sub(1) + } + } else { + idx.saturating_sub(1) + }; + if let Some(p) = self + .selection + .get_index(idx) + .map(|n| n.absolute_path.clone()) + { + self = self.focus_path(&p, true)?; + } + } else if let Some(p) = + self.selection.last().map(|n| n.absolute_path.clone()) + { + self = self.focus_path(&p, true)?; + } + } + + Ok(self) + } + pub fn focus_previous_by_relative_index(mut self, index: usize) -> Result { let mut history = self.history.clone(); if let Some(dir) = self.directory_buffer_mut() { @@ -696,7 +815,7 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.focus = dir.focus.max(index) - index; + dir.focus = dir.focus.saturating_sub(index); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -734,6 +853,46 @@ impl App { Ok(self) } + fn focus_next_selection(mut self) -> Result { + let total = self.selection.len(); + if total == 0 { + return Ok(self); + } + + let bounded = self.config.general.enforce_bounded_index_navigation; + + if let Some(n) = self + .directory_buffer + .as_ref() + .and_then(|d| d.focused_node()) + { + if let Some(idx) = self.selection.get_index_of(n) { + let idx = if idx + 1 == total { + if bounded { + idx + } else { + 0 + } + } else { + idx + 1 + }; + if let Some(p) = self + .selection + .get_index(idx) + .map(|n| n.absolute_path.clone()) + { + self = self.focus_path(&p, true)?; + } + } else if let Some(p) = + self.selection.first().map(|n| n.absolute_path.clone()) + { + self = self.focus_path(&p, true)?; + } + } + + Ok(self) + } + pub fn focus_next_by_relative_index(mut self, index: usize) -> Result { let mut history = self.history.clone(); if let Some(dir) = self.directory_buffer_mut() { @@ -741,7 +900,11 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.focus = (dir.focus + index).min(dir.total.max(1) - 1); + dir.focus = dir + .focus + .saturating_add(index) + .min(dir.total.saturating_sub(1)); + if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -785,7 +948,7 @@ impl App { } } else { self.log_error(format!( - "not a valid directory: {}", + "not a valid directory: {:?}", vroot.to_string_lossy() )) } @@ -833,17 +996,22 @@ impl App { match env::set_current_dir(&dir) { Ok(()) => { - let pwd = self.pwd.clone(); + let lwd = self.pwd.clone(); let focus = self.focused_node().map(|n| n.relative_path.clone()); - self = self.add_last_focus(pwd, focus)?; + self = self.add_last_focus(lwd, focus)?; self.pwd = dir.to_string_lossy().to_string(); self.explorer_config.searcher = None; if save_history { - self.history = self.history.push(format!("{}/", self.pwd)); + let hist = if &self.pwd == "/" { + self.pwd.clone() + } else { + format!("{0}/", &self.pwd) + }; + self.history = self.history.push(hist); } self.explore_pwd() } - Err(e) => self.log_error(e.to_string()), + Err(e) => self.log_error(format!("could not enter {dir:?}: {e}")), } } @@ -981,7 +1149,7 @@ impl App { fn focus_by_index(mut self, index: usize) -> Result { let history = self.history.clone(); if let Some(dir) = self.directory_buffer_mut() { - dir.focus = index.min(dir.total.max(1) - 1); + dir.focus = index.min(dir.total.saturating_sub(1)); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -1026,7 +1194,7 @@ impl App { } Ok(self) } else { - self.log_error(format!("{} not found in $PWD", name)) + self.log_error(format!("{name:?} not found in $PWD")) } } else { Ok(self) @@ -1060,10 +1228,10 @@ impl App { self.change_directory(&parent.to_string_lossy(), false)? .focus_by_file_name(&filename.to_string_lossy(), save_history) } else { - self.log_error(format!("{} not found", path)) + self.log_error(format!("{path:?} not found")) } } else { - self.log_error(format!("Cannot focus on {}", path)) + self.log_error(format!("could not focus on {path:?}")) } } @@ -1111,7 +1279,7 @@ impl App { } else if self.config.modes.custom.contains_key(mode) { self.switch_mode_custom_keeping_input_buffer(mode) } else { - self.log_error(format!("Mode not found: {}", mode)) + self.log_error(format!("mode not found: {mode:?}")) } } @@ -1136,7 +1304,7 @@ impl App { Ok(self) } else { - self.log_error(format!("Builtin mode not found: {}", mode)) + self.log_error(format!("builtin mode not found: {mode:?}")) } } @@ -1161,7 +1329,7 @@ impl App { Ok(self) } else { - self.log_error(format!("Custom mode not found: {}", mode)) + self.log_error(format!("custom mode not found: {mode:?}")) } } @@ -1171,7 +1339,7 @@ impl App { } else if self.config.layouts.custom.contains_key(layout) { self.switch_layout_custom(layout) } else { - self.log_error(format!("Layout not found: {}", layout)) + self.log_error(format!("layout not found: {layout:?}")) } } @@ -1187,7 +1355,7 @@ impl App { Ok(self) } else { - self.log_error(format!("Builtin layout not found: {}", layout)) + self.log_error(format!("builtin layout not found: {layout:?}")) } } @@ -1203,7 +1371,7 @@ impl App { Ok(self) } else { - self.log_error(format!("Custom layout not found: {}", layout)) + self.log_error(format!("custom layout not found: {layout:?}")) } } @@ -1333,21 +1501,12 @@ impl App { pub fn select_all(mut self) -> Result { if let Some(d) = self.directory_buffer.as_ref() { - d.nodes.clone().into_iter().for_each(|n| { - self.selection.insert(n); - }); + self.selection = d.nodes.clone().into_iter().collect(); }; Ok(self) } - pub fn un_select(mut self) -> Result { - if let Some(n) = self.focused_node().map(|n| n.to_owned()) { - self.selection.retain(|s| s != &n); - } - Ok(self) - } - pub fn un_select_path(mut self, path: String) -> Result { let pathbuf = PathBuf::from(path).absolutize()?.to_path_buf(); self.selection @@ -1355,10 +1514,19 @@ impl App { Ok(self) } + pub fn un_select(mut self) -> Result { + if let Some(n) = self.focused_node().map(|n| n.to_owned()) { + self.selection + .retain(|s| s.absolute_path != n.absolute_path); + } + Ok(self) + } + pub fn un_select_all(mut self) -> Result { if let Some(d) = self.directory_buffer.as_ref() { d.nodes.clone().into_iter().for_each(|n| { - self.selection.retain(|s| s != &n); + self.selection + .retain(|s| s.absolute_path != n.absolute_path); }); }; @@ -1518,7 +1686,34 @@ impl App { Ok(self) } - pub fn search_fuzzy(mut self, pattern: String) -> Result { + pub fn search(self, pattern: String) -> Result { + let (algorithm, unordered) = self + .explorer_config + .searcher + .as_ref() + .map(|s| (s.algorithm, s.unordered)) + .unwrap_or(( + self.config.general.search.algorithm, + self.config.general.search.unordered, + )); + + self.search_with(pattern, algorithm, unordered) + } + + fn search_from_input(self) -> Result { + if let Some(pattern) = self.input.buffer.as_ref().map(Input::to_string) { + self.search(pattern) + } else { + Ok(self) + } + } + + pub fn search_with( + mut self, + pattern: String, + algorithm: SearchAlgorithm, + unordered: bool, + ) -> Result { let rf = self .explorer_config .searcher @@ -1526,18 +1721,54 @@ impl App { .map(|s| s.recoverable_focus.clone()) .unwrap_or_else(|| self.focused_node().map(|n| n.absolute_path.clone())); - self.explorer_config.searcher = Some(NodeSearcher::new(pattern, rf)); + self.explorer_config.searcher = Some(NodeSearcherApplicable::new( + pattern, rf, algorithm, unordered, + )); Ok(self) } - fn search_fuzzy_from_input(self) -> Result { + fn search_from_input_with( + self, + algorithm: SearchAlgorithm, + unordered: bool, + ) -> Result { if let Some(pattern) = self.input.buffer.as_ref().map(Input::to_string) { - self.search_fuzzy(pattern) + self.search_with(pattern, algorithm, unordered) } else { Ok(self) } } + fn enable_search_order(mut self) -> Result { + self.explorer_config.searcher = self + .explorer_config + .searcher + .map(|s| s.enable_search_order()); + Ok(self) + } + + fn disable_search_order(mut self) -> Result { + self.explorer_config.searcher = self + .explorer_config + .searcher + .map(|s| s.disable_search_order()); + Ok(self) + } + + fn toggle_search_order(mut self) -> Result { + self.explorer_config.searcher = self + .explorer_config + .searcher + .map(|s| s.toggle_search_order()); + Ok(self) + } + + fn toggle_search_algorithm(mut self) -> Result { + self.explorer_config.searcher = + self.explorer_config.searcher.map(|s| s.toggle_algorithm()); + Ok(self) + } + fn accept_search(mut self) -> Result { let focus = self .directory_buffer @@ -1664,13 +1895,15 @@ impl App { } pub fn mode_str(&self) -> String { - format!("{}\n", &self.mode.name) + format!("{0}\n", &self.mode.name) } fn refresh_selection(mut self) -> Result { - // Should be able to select broken symlink - self.selection - .retain(|n| PathBuf::from(&n.absolute_path).symlink_metadata().is_ok()); + self.selection.retain(|n| { + let p = PathBuf::from(&n.absolute_path); + // Should be able to retain broken symlink + p.exists() || p.symlink_metadata().is_ok() + }); Ok(self) } @@ -1688,7 +1921,7 @@ impl App { .map(|d| { d.nodes .iter() - .map(|n| format!("{}{}", n.absolute_path, delimiter)) + .map(|n| format!("{0}{delimiter}", n.absolute_path)) .collect::>() .join("") }) @@ -1696,13 +1929,13 @@ impl App { } pub fn pwd_str(&self, delimiter: char) -> String { - format!("{}{}", &self.pwd, delimiter) + format!("{0}{delimiter}", &self.pwd) } pub fn selection_str(&self, delimiter: char) -> String { self.selection .iter() - .map(|n| format!("{}{}", n.absolute_path, delimiter)) + .map(|n| format!("{0}{delimiter}", n.absolute_path)) .collect::>() .join("") } @@ -1710,7 +1943,7 @@ impl App { pub fn result_str(&self, delimiter: char) -> String { self.result() .into_iter() - .map(|n| format!("{}{}", n.absolute_path, delimiter)) + .map(|n| format!("{0}{delimiter}", n.absolute_path)) .collect::>() .join("") } @@ -1718,7 +1951,7 @@ impl App { pub fn logs_str(&self, delimiter: char) -> String { self.logs .iter() - .map(|l| format!("{}{}", l, delimiter)) + .map(|l| format!("{l}{delimiter}")) .collect::>() .join("") } @@ -1739,18 +1972,17 @@ impl App { .help_menu() .iter() .map(|l| match l { - HelpMenuLine::Paragraph(p) => format!("\t{}{}", p, delimiter), + HelpMenuLine::Paragraph(p) => format!("\t{p}{delimiter}"), HelpMenuLine::KeyMap(k, remaps, h) => { let remaps = remaps.join(", "); - format!(" {:15} | {:25} | {}{}", k, remaps, h , delimiter) + format!(" {k:15} | {remaps:25} | {h}{delimiter}") } }) .collect::>() .join(""); format!( - "### {}{d}{d} key | remaps | action\n --------------- | ------------------------- | ------{d}{}{d}", - name, help, d = delimiter + "### {name}{delimiter}{delimiter} key | remaps | action\n --------------- | ------------------------- | ------{delimiter}{help}{delimiter}" ) }) .collect::>() @@ -1761,7 +1993,7 @@ impl App { self.history .paths .iter() - .map(|p| format!("{}{}", &p, delimiter)) + .map(|p| format!("{p}{delimiter}")) .collect::>() .join("") } diff --git a/src/bin/xplr.rs b/src/bin/xplr.rs index 59946e3..027ef30 100644 --- a/src/bin/xplr.rs +++ b/src/bin/xplr.rs @@ -6,7 +6,7 @@ use xplr::runner; fn main() { let cli = Cli::parse(env::args()).unwrap_or_else(|e| { - eprintln!("error: {}", e); + eprintln!("error: {e}"); std::process::exit(1); }); @@ -54,28 +54,28 @@ fn main() { ); let help = help.trim(); - println!("{}", help); + println!("{help}"); } else if cli.version { println!("xplr {}", xplr::app::VERSION); } else if !cli.pipe_msg_in.is_empty() { if let Err(err) = cli::pipe_msg_in(cli.pipe_msg_in) { - eprintln!("error: {}", err); + eprintln!("error: {err}"); std::process::exit(1); } } else if !cli.print_msg_in.is_empty() { if let Err(err) = cli::print_msg_in(cli.print_msg_in) { - eprintln!("error: {}", err); + eprintln!("error: {err}"); std::process::exit(1); } } else { match runner::from_cli(cli).and_then(|a| a.run()) { Ok(Some(out)) => { - print!("{}", out); + print!("{out}"); } Ok(None) => {} Err(err) => { if !err.to_string().is_empty() { - eprintln!("error: {}", err); + eprintln!("error: {err}"); }; std::process::exit(1); diff --git a/src/cli.rs b/src/cli.rs index fd241ec..338b522 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -106,17 +106,17 @@ impl Cli { // Options "-c" | "--config" => { cli.config = Some( - args.next().map(|a| Cli::read_path(&a)).with_context( - || format!("usage: xplr {} PATH", arg), - )??, + args.next() + .map(|a| Cli::read_path(&a)) + .with_context(|| format!("usage: xplr {arg} PATH"))??, ); } "--vroot" => { cli.vroot = Some( - args.next().map(|a| Cli::read_path(&a)).with_context( - || format!("usage: xplr {} PATH", arg), - )??, + args.next() + .map(|a| Cli::read_path(&a)) + .with_context(|| format!("usage: xplr {arg} PATH"))??, ); } @@ -191,7 +191,7 @@ pub fn pipe_msg_in(args: Vec) -> Result<()> { .open(&path)? .write_all(msg.as_bytes())?; } else { - println!("{}", msg); + println!("{msg}"); }; Ok(()) @@ -199,7 +199,7 @@ pub fn pipe_msg_in(args: Vec) -> Result<()> { pub fn print_msg_in(args: Vec) -> Result<()> { let msg = fmt_msg_in(args)?; - print!("{}", msg); + print!("{msg}"); Ok(()) } @@ -220,24 +220,21 @@ fn fmt_msg_in(args: Vec) -> Result { } ('q', Some('%')) => { let arg = args.next().context(format!( - "argument missing for the placeholder at column {}", - col + "argument missing for the placeholder at column {col}" ))?; msg.push_str(&json::to_string(&arg)?); last_char = None; } ('s', Some('%')) => { let arg = args.next().context(format!( - "argument missing for the placeholder at column {}", - col + "argument missing for the placeholder at column {col}", ))?; msg.push_str(&arg); last_char = None; } (ch, Some('%')) => { bail!(format!( - "invalid placeholder '%{}' at column {}, use one of '%s' or '%q', or escape it using '%%'", - ch, col + "invalid placeholder '%{ch}' at column {col}, use one of '%s' or '%q', or escape it using '%%'", )); } (ch, _) => { diff --git a/src/compat.rs b/src/compat.rs new file mode 100644 index 0000000..7a05e3a --- /dev/null +++ b/src/compat.rs @@ -0,0 +1,224 @@ +// Things of the past, mostly bad decisions, which cannot erased, stays in this +// haunted module. + +use crate::app; +use crate::lua; +use crate::ui::block; +use crate::ui::string_to_text; +use crate::ui::Constraint; +use crate::ui::ContentRendererArg; +use mlua::Lua; +use serde::{Deserialize, Serialize}; +use tui::backend::Backend; +use tui::layout::Constraint as TuiConstraint; +use tui::layout::Rect as TuiRect; +use tui::widgets::Cell; +use tui::widgets::List; +use tui::widgets::ListItem; +use tui::widgets::Paragraph; +use tui::widgets::Row; +use tui::widgets::Table; +use tui::Frame; + +/// A cursed enum from crate::ui. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub enum ContentBody { + /// A paragraph to render + StaticParagraph { render: String }, + + /// A Lua function that returns a paragraph to render + DynamicParagraph { render: String }, + + /// List to render + StaticList { render: Vec }, + + /// A Lua function that returns lines to render + DynamicList { render: String }, + + /// A table to render + StaticTable { + widths: Vec, + col_spacing: Option, + render: Vec>, + }, + + /// A Lua function that returns a table to render + DynamicTable { + widths: Vec, + col_spacing: Option, + render: String, + }, +} + +/// A cursed struct from crate::ui. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct CustomContent { + pub title: Option, + pub body: ContentBody, +} + +/// A cursed function from crate::ui. +pub fn draw_custom_content( + f: &mut Frame, + screen_size: TuiRect, + layout_size: TuiRect, + app: &app::App, + content: CustomContent, + lua: &Lua, +) { + let config = app.config.general.panel_ui.default.clone(); + let title = content.title; + let body = content.body; + + match body { + ContentBody::StaticParagraph { render } => { + let render = string_to_text(render); + let content = Paragraph::new(render).block(block( + config, + title.map(|t| format!(" {t} ")).unwrap_or_default(), + )); + f.render_widget(content, layout_size); + } + + ContentBody::DynamicParagraph { render } => { + let ctx = ContentRendererArg { + app: app.to_lua_ctx_light(), + layout_size: layout_size.into(), + screen_size: screen_size.into(), + }; + + let render = lua::serialize(lua, &ctx) + .map(|arg| { + lua::call(lua, &render, arg).unwrap_or_else(|e| format!("{e:?}")) + }) + .unwrap_or_else(|e| e.to_string()); + + let render = string_to_text(render); + + let content = Paragraph::new(render).block(block( + config, + title.map(|t| format!(" {t} ")).unwrap_or_default(), + )); + f.render_widget(content, layout_size); + } + + ContentBody::StaticList { render } => { + let items = render + .into_iter() + .map(string_to_text) + .map(ListItem::new) + .collect::>(); + + let content = List::new(items).block(block( + config, + title.map(|t| format!(" {t} ")).unwrap_or_default(), + )); + f.render_widget(content, layout_size); + } + + ContentBody::DynamicList { render } => { + let ctx = ContentRendererArg { + app: app.to_lua_ctx_light(), + layout_size: layout_size.into(), + screen_size: screen_size.into(), + }; + + let items = lua::serialize(lua, &ctx) + .map(|arg| { + lua::call(lua, &render, arg) + .unwrap_or_else(|e| vec![format!("{e:?}")]) + }) + .unwrap_or_else(|e| vec![e.to_string()]) + .into_iter() + .map(string_to_text) + .map(ListItem::new) + .collect::>(); + + let content = List::new(items).block(block( + config, + title.map(|t| format!(" {t} ")).unwrap_or_default(), + )); + f.render_widget(content, layout_size); + } + + ContentBody::StaticTable { + widths, + col_spacing, + render, + } => { + let rows = render + .into_iter() + .map(|cols| { + Row::new( + cols.into_iter() + .map(string_to_text) + .map(Cell::from) + .collect::>(), + ) + }) + .collect::>(); + + let widths = widths + .into_iter() + .map(|w| w.to_tui(screen_size, layout_size)) + .collect::>(); + + let content = Table::new(rows) + .widths(&widths) + .column_spacing(col_spacing.unwrap_or(1)) + .block(block( + config, + title.map(|t| format!(" {t} ")).unwrap_or_default(), + )); + + f.render_widget(content, layout_size); + } + + ContentBody::DynamicTable { + widths, + col_spacing, + render, + } => { + let ctx = ContentRendererArg { + app: app.to_lua_ctx_light(), + layout_size: layout_size.into(), + screen_size: screen_size.into(), + }; + + let rows = lua::serialize(lua, &ctx) + .map(|arg| { + lua::call(lua, &render, arg) + .unwrap_or_else(|e| vec![vec![format!("{e:?}")]]) + }) + .unwrap_or_else(|e| vec![vec![e.to_string()]]) + .into_iter() + .map(|cols| { + Row::new( + cols.into_iter() + .map(string_to_text) + .map(Cell::from) + .collect::>(), + ) + }) + .collect::>(); + + let widths = widths + .into_iter() + .map(|w| w.to_tui(screen_size, layout_size)) + .collect::>(); + + let mut content = Table::new(rows).widths(&widths).block(block( + config, + title.map(|t| format!(" {t} ")).unwrap_or_default(), + )); + + if let Some(col_spacing) = col_spacing { + content = content.column_spacing(col_spacing); + }; + + f.render_widget(content, layout_size); + } + } +} diff --git a/src/config.rs b/src/config.rs index 5749e2b..b27d46c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,8 @@ use crate::app::HelpMenuLine; use crate::app::NodeFilter; use crate::app::NodeSorter; use crate::app::NodeSorterApplicable; +use crate::node::Node; +use crate::search::SearchAlgorithm; use crate::ui::Border; use crate::ui::BorderType; use crate::ui::Constraint; @@ -80,6 +82,40 @@ pub struct NodeTypesConfig { pub special: HashMap, } +impl NodeTypesConfig { + pub fn get(&self, node: &Node) -> NodeTypeConfig { + let mut node_type = if node.is_symlink { + self.symlink.to_owned() + } else if node.is_dir { + self.directory.to_owned() + } else { + self.file.to_owned() + }; + + let mut me = node.mime_essence.splitn(2, '/'); + let mimetype: String = me.next().map(|s| s.into()).unwrap_or_default(); + let mimesub: String = me.next().map(|s| s.into()).unwrap_or_default(); + + if let Some(conf) = self + .mime_essence + .get(&mimetype) + .and_then(|t| t.get(&mimesub).or_else(|| t.get("*"))) + { + node_type = node_type.extend(conf); + } + + if let Some(conf) = self.extension.get(&node.extension) { + node_type = node_type.extend(conf); + } + + if let Some(conf) = self.special.get(&node.relative_path) { + node_type = node_type.extend(conf); + } + + node_type + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct UiConfig { @@ -146,6 +182,23 @@ pub struct TableConfig { pub col_widths: Option>, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SelectionConfig { + #[serde(default)] + pub item: UiElement, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SearchConfig { + #[serde(default)] + pub algorithm: SearchAlgorithm, + + #[serde(default)] + pub unordered: bool, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct LogsConfig { @@ -172,6 +225,16 @@ pub struct SortDirectionIdentifiersUi { pub reverse: UiElement, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SearchDirectionIdentifiersUi { + #[serde(default)] + pub ordered: UiElement, + + #[serde(default)] + pub unordered: UiElement, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SortAndFilterUi { @@ -191,7 +254,10 @@ pub struct SortAndFilterUi { pub filter_identifiers: HashMap, #[serde(default)] - pub search_identifier: Option, + pub search_direction_identifiers: SearchDirectionIdentifiersUi, + + #[serde(default)] + pub search_identifiers: HashMap, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -249,6 +315,12 @@ pub struct GeneralConfig { #[serde(default)] pub table: TableConfig, + #[serde(default)] + pub selection: SelectionConfig, + + #[serde(default)] + pub search: SearchConfig, + #[serde(default)] pub default_ui: UiConfig, diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index d6b7a42..c6e755d 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -1,6 +1,6 @@ use crate::node::Node; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct DirectoryBuffer { @@ -9,8 +9,8 @@ pub struct DirectoryBuffer { pub total: usize, pub focus: usize, - #[serde(skip)] - pub explored_at: DateTime, + #[serde(skip, default = "now")] + pub explored_at: OffsetDateTime, } impl DirectoryBuffer { @@ -21,7 +21,7 @@ impl DirectoryBuffer { nodes, total, focus, - explored_at: Utc::now(), + explored_at: now(), } } @@ -29,3 +29,9 @@ impl DirectoryBuffer { self.nodes.get(self.focus) } } + +fn now() -> OffsetDateTime { + OffsetDateTime::now_local() + .ok() + .unwrap_or_else(OffsetDateTime::now_utc) +} diff --git a/src/explorer.rs b/src/explorer.rs index fb4a996..1f66d56 100644 --- a/src/explorer.rs +++ b/src/explorer.rs @@ -2,21 +2,14 @@ use crate::app::{ DirectoryBuffer, ExplorerConfig, ExternalMsg, InternalMsg, MsgIn, Node, Task, }; use anyhow::{Error, Result}; -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; -use lazy_static::lazy_static; use std::fs; use std::path::PathBuf; use std::sync::mpsc::Sender; use std::thread; -lazy_static! { - static ref FUZZY_MATCHER: SkimMatcherV2 = SkimMatcherV2::default(); -} - pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result> { let dirs = fs::read_dir(parent)?; - let mut nodes = dirs + let nodes = dirs .filter_map(|d| { d.ok().map(|e| { e.path() @@ -26,26 +19,24 @@ pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result> { }) }) .map(|name| Node::new(parent.to_string_lossy().to_string(), name)) - .filter(|n| config.filter(n)) - .collect::>(); - - nodes = if let Some(pattern) = config.searcher.as_ref().map(|s| &s.pattern) { - let mut nodes = nodes - .into_iter() - .filter_map(|n| { - FUZZY_MATCHER - .fuzzy_match(&n.relative_path, pattern) - .map(|score| (n, score)) - }) - .collect::>(); + .filter(|n| config.filter(n)); - nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1)); - nodes.into_iter().map(|(n, _)| n).collect::>() + let mut nodes = if let Some(searcher) = config.searcher.as_ref() { + searcher.search(nodes) } else { - nodes.sort_by(|a, b| config.sort(a, b)); - nodes + nodes.collect() }; + let is_ordered_search = config + .searcher + .as_ref() + .map(|s| !s.unordered) + .unwrap_or(false); + + if !is_ordered_search { + nodes.sort_by(|a, b| config.sort(a, b)); + } + Ok(nodes) } @@ -65,7 +56,7 @@ pub(crate) fn explore_sync( .enumerate() .find(|(_, n)| n.relative_path == focus_str) .map(|(i, _)| i) - .unwrap_or_else(|| fallback_focus.min(nodes.len().max(1) - 1)) + .unwrap_or_else(|| fallback_focus.min(nodes.len().saturating_sub(1))) } else { 0 }; diff --git a/src/init.lua b/src/init.lua index 4e51cb9..5846449 100644 --- a/src/init.lua +++ b/src/init.lua @@ -159,7 +159,7 @@ xplr.config.general.logs.error.style = { fg = "Red" } xplr.config.general.table.header.cols = { { format = " index", style = {} }, { format = "╭─── path", style = {} }, - { format = "permissions", style = {} }, + { format = "perm", style = {} }, { format = "size", style = {} }, { format = "modified", style = {} }, } @@ -246,6 +246,26 @@ xplr.config.general.table.col_widths = { { Percentage = 20 }, } +-- Renderer for each item in the selection list. +-- +-- Type: nullable string +xplr.config.general.selection.item.format = "builtin.fmt_general_selection_item" + +-- Style for each item in the selection list. +-- +-- Type: [Style](https://xplr.dev/en/style) +xplr.config.general.selection.item.style = {} + +-- The default search algorithm +-- +-- Type: [Search Algorithm](https://xplr.dev/en/searching#algorithm) +xplr.config.general.search.algorithm = "Fuzzy" + +-- The default search ordering +-- +-- Type: boolean +xplr.config.general.search.unordered = false + -- The content that is placed before the item name for each row by default. -- -- Type: nullable string @@ -414,7 +434,6 @@ xplr.config.general.sort_and_filter_ui.filter_identifiers = { RelativePathIsNot = { format = "rel!=", style = {} }, RelativePathDoesMatchRegex = { format = "rel=/", style = {} }, RelativePathDoesNotMatchRegex = { format = "rel!/", style = {} }, - IRelativePathDoesContain = { format = "[i]rel=~", style = {} }, IRelativePathDoesEndWith = { format = "[i]rel=$", style = {} }, IRelativePathDoesNotContain = { format = "[i]rel!~", style = {} }, @@ -425,7 +444,6 @@ xplr.config.general.sort_and_filter_ui.filter_identifiers = { IRelativePathIsNot = { format = "[i]rel!=", style = {} }, IRelativePathDoesMatchRegex = { format = "[i]rel=/", style = {} }, IRelativePathDoesNotMatchRegex = { format = "[i]rel!/", style = {} }, - AbsolutePathDoesContain = { format = "abs=~", style = {} }, AbsolutePathDoesEndWith = { format = "abs=$", style = {} }, AbsolutePathDoesNotContain = { format = "abs!~", style = {} }, @@ -436,7 +454,6 @@ xplr.config.general.sort_and_filter_ui.filter_identifiers = { AbsolutePathIsNot = { format = "abs!=", style = {} }, AbsolutePathDoesMatchRegex = { format = "abs=/", style = {} }, AbsolutePathDoesNotMatchRegex = { format = "abs!/", style = {} }, - IAbsolutePathDoesContain = { format = "[i]abs=~", style = {} }, IAbsolutePathDoesEndWith = { format = "[i]abs=$", style = {} }, IAbsolutePathDoesNotContain = { format = "[i]abs!~", style = {} }, @@ -452,11 +469,22 @@ xplr.config.general.sort_and_filter_ui.filter_identifiers = { -- The identifiers used to denote applied search input. -- -- Type: { format = nullable string, style = [Style](https://xplr.dev/en/style) } -xplr.config.general.sort_and_filter_ui.search_identifier = { - format = "search:", - style = {}, +xplr.config.general.sort_and_filter_ui.search_identifiers = { + Fuzzy = { format = "fzy:", style = {} }, + Regex = { format = "reg:", style = {} }, } +-- The shape of ordered indicator for search ordering identifiers in Sort & filter panel. +-- +-- Type: nullable string +xplr.config.general.sort_and_filter_ui.search_direction_identifiers.ordered.format = + "↓" + +-- The shape of unordered indicator for search ordering identifiers in Sort & filter panel. +-- +-- Type: nullable string +xplr.config.general.sort_and_filter_ui.search_direction_identifiers.unordered.format = "" + -- The content for panel title by default. -- -- Type: nullable string @@ -701,7 +729,7 @@ xplr.config.general.global_key_bindings = { -- -- Type: [Style](https://xplr.dev/en/style) xplr.config.node_types.directory.style = { - fg = "Cyan", + fg = "Blue", } -- Metadata for the directory nodes. @@ -1232,6 +1260,18 @@ xplr.config.modes.builtin.default = { "ScrollDownHalf", }, }, + ["ctrl-n"] = { + help = "next selection", + messages = { + "FocusNextSelection", + }, + }, + ["ctrl-p"] = { + help = "prev selection", + messages = { + "FocusPreviousSelection", + }, + }, }, on_number = { help = "input", @@ -1244,8 +1284,6 @@ xplr.config.modes.builtin.default = { }, } -xplr.config.modes.builtin.default.key_bindings.on_key["tab"] = - xplr.config.modes.builtin.default.key_bindings.on_key["ctrl-i"] xplr.config.modes.builtin.default.key_bindings.on_key["v"] = xplr.config.modes.builtin.default.key_bindings.on_key["space"] xplr.config.modes.builtin.default.key_bindings.on_key["V"] = @@ -1260,6 +1298,8 @@ xplr.config.modes.builtin.default.key_bindings.on_key["k"] = xplr.config.modes.builtin.default.key_bindings.on_key["up"] xplr.config.modes.builtin.default.key_bindings.on_key["l"] = xplr.config.modes.builtin.default.key_bindings.on_key["right"] +xplr.config.modes.builtin.default.key_bindings.on_key["tab"] = + xplr.config.modes.builtin.default.key_bindings.on_key["ctrl-i"] -- compatibility workaround -- The builtin debug error mode. -- @@ -1276,11 +1316,10 @@ xplr.config.modes.builtin.debug_error = { }, splits = { { - CustomContent = { - title = "debug error", - body = { - StaticParagraph = { - render = [[ + Static = { + CustomParagraph = { + ui = { title = { format = "debug error" } }, + body = [[ Some errors occurred during startup. If you think this is a bug, please report it at: @@ -1292,8 +1331,7 @@ xplr.config.modes.builtin.debug_error = { To disable this mode, set `xplr.config.general.disable_debug_error_mode` to `true` in your config file. - ]], - }, + ]], }, }, }, @@ -1332,11 +1370,10 @@ xplr.config.modes.builtin.debug_error = { xplr.config.modes.builtin.recover = { name = "recover", layout = { - CustomContent = { - title = " recover ", - body = { - StaticParagraph = { - render = [[ + Static = { + CustomParagraph = { + ui = { title = { format = "recover" } }, + body = [[ You pressed an invalid key and went into "recover" mode. This mode saves you from performing unwanted actions. @@ -1345,8 +1382,7 @@ xplr.config.modes.builtin.recover = { To disable this mode, set `xplr.config.general.enable_recover_mode` to `false` in your config file. - ]], - }, + ]], }, }, }, @@ -1369,7 +1405,7 @@ xplr.config.modes.builtin.go_to_path = { messages = { { BashExecSilently0 = [===[ - PTH=${XPLR_INPUT_BUFFER} + PTH="$XPLR_INPUT_BUFFER" PTH_ESC=$(printf %q "$PTH") if [ -d "$PTH" ]; then "$XPLR" -m 'ChangeDirectory: %q' "$PTH" @@ -1406,21 +1442,63 @@ xplr.config.modes.builtin.selection_ops = { layout = "HelpMenu", key_bindings = { on_key = { + ["e"] = { + help = "edit selection", + messages = { + { + BashExec0 = [===[ + TMPFILE="$(mktemp)" + (while IFS= read -r -d '' PTH; do + echo $(printf %q "${PTH:?}") >> "${TMPFILE:?}" + done < "${XPLR_PIPE_SELECTION_OUT:?}") + ${EDITOR:-vi} "${TMPFILE:?}" + [ ! -e "$TMPFILE" ] && exit + "$XPLR" -m ClearSelection + (while IFS= read -r PTH_ESC; do + "$XPLR" -m 'SelectPath: %q' "$(eval printf %s ${PTH_ESC:?})" + done < "${TMPFILE:?}") + rm -- "${TMPFILE:?}" + ]===], + }, + "PopMode", + }, + }, + ["l"] = { + help = "list selection", + messages = { + { + BashExec0 = [===[ + [ -z "$PAGER" ] && PAGER="less -+F" + + while IFS= read -r -d '' PTH; do + echo $(printf %q "$PTH") + done < "${XPLR_PIPE_SELECTION_OUT:?}" | ${PAGER:?} + ]===], + }, + "PopMode", + }, + }, ["c"] = { help = "copy here", messages = { { BashExec0 = [===[ + "$XPLR" -m ExplorePwd (while IFS= read -r -d '' PTH; do PTH_ESC=$(printf %q "$PTH") - if cp -vr -- "${PTH:?}" ./; then - "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC copied to ." + BASENAME=$(basename -- "$PTH") + BASENAME_ESC=$(printf %q "$BASENAME") + while [ -e "$BASENAME" ]; do + BASENAME="$BASENAME (copied)" + BASENAME_ESC=$(printf %q "$BASENAME") + done + if cp -vr -- "${PTH:?}" "./${BASENAME:?}"; then + "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC copied to ./$BASENAME_ESC" + "$XPLR" -m 'FocusPath: %q' "$BASENAME" else - "$XPLR" -m 'LogError: %q' "Failed to copy $PTH_ESC to ." + "$XPLR" -m 'LogError: %q' "could not copy $PTH_ESC to ./$BASENAME_ESC" fi done < "${XPLR_PIPE_SELECTION_OUT:?}") - "$XPLR" -m ExplorePwdAsync - "$XPLR" -m ClearSelection read -p "[enter to continue]" ]===], }, @@ -1432,15 +1510,76 @@ xplr.config.modes.builtin.selection_ops = { messages = { { BashExec0 = [===[ + "$XPLR" -m ExplorePwd + (while IFS= read -r -d '' PTH; do + PTH_ESC=$(printf %q "$PTH") + BASENAME=$(basename -- "$PTH") + BASENAME_ESC=$(printf %q "$BASENAME") + while [ -e "$BASENAME" ]; do + BASENAME="$BASENAME (moved)" + BASENAME_ESC=$(printf %q "$BASENAME") + done + if mv -v -- "${PTH:?}" "./${BASENAME:?}"; then + "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC moved to ./$BASENAME_ESC" + "$XPLR" -m 'FocusPath: %q' "$BASENAME" + else + "$XPLR" -m 'LogError: %q' "could not move $PTH_ESC to ./$BASENAME_ESC" + fi + done < "${XPLR_PIPE_SELECTION_OUT:?}") + read -p "[enter to continue]" + ]===], + }, + "PopMode", + }, + }, + ["s"] = { + help = "softlink here", + messages = { + { + BashExec0 = [===[ + "$XPLR" -m ExplorePwd (while IFS= read -r -d '' PTH; do PTH_ESC=$(printf %q "$PTH") - if mv -v -- "${PTH:?}" ./; then - "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC moved to ." + BASENAME=$(basename -- "$PTH") + BASENAME_ESC=$(printf %q "$BASENAME") + while [ -e "$BASENAME" ]; do + BASENAME="$BASENAME (softlinked)" + BASENAME_ESC=$(printf %q "$BASENAME") + done + if ln -sv -- "${PTH:?}" "./${BASENAME:?}"; then + "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC softlinked as ./$BASENAME_ESC" + "$XPLR" -m 'FocusPath: %q' "$BASENAME" else - "$XPLR" -m 'LogError: %q' "Failed to move $PTH_ESC to ." + "$XPLR" -m 'LogError: %q' "could not softlink $PTH_ESC as ./$BASENAME_ESC" + fi + done < "${XPLR_PIPE_SELECTION_OUT:?}") + read -p "[enter to continue]" + ]===], + }, + "PopMode", + }, + }, + ["h"] = { + help = "hardlink here", + messages = { + { + BashExec0 = [===[ + "$XPLR" -m ExplorePwd + (while IFS= read -r -d '' PTH; do + PTH_ESC=$(printf %q "$PTH") + BASENAME=$(basename -- "$PTH") + BASENAME_ESC=$(printf %q "$BASENAME") + while [ -e "$BASENAME" ]; do + BASENAME="$BASENAME (hardlinked)" + BASENAME_ESC=$(printf %q "$BASENAME") + done + if ln -v -- "${PTH:?}" "./${BASENAME:?}"; then + "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC hardlinked as ./$BASENAME_ESC" + "$XPLR" -m 'FocusPath: %q' "$BASENAME" + else + "$XPLR" -m 'LogError: %q' "could not hardlink $PTH_ESC as ./$BASENAME_ESC" fi done < "${XPLR_PIPE_SELECTION_OUT:?}") - "$XPLR" -m ExplorePwdAsync read -p "[enter to continue]" ]===], }, @@ -1551,8 +1690,8 @@ xplr.config.modes.builtin.create_file = { PTH="$XPLR_INPUT_BUFFER" PTH_ESC=$(printf %q "$PTH") if [ "$PTH" ]; then - mkdir -p -- "$(dirname $(realpath -m $PTH))" \ - && touch -- "$PTH" \ + mkdir -p -- "$(dirname $(realpath -m $PTH))" # This may fail. + touch -- "$PTH" \ && "$XPLR" -m 'SetInputBuffer: ""' \ && "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC created" \ && "$XPLR" -m 'ExplorePwd' \ @@ -1674,7 +1813,7 @@ xplr.config.modes.builtin.go_to = { elif command -v open; then OPENER=open else - "$XPLR" -m 'LogError: "$OPENER not found"' + "$XPLR" -m 'LogError: %q' "$OPENER not found" exit 1 fi fi @@ -1792,15 +1931,16 @@ xplr.config.modes.builtin.delete = { messages = { { BashExec0 = [===[ + "$XPLR" -m ExplorePwd (while IFS= read -r -d '' PTH; do PTH_ESC=$(printf %q "$PTH") if rm -rfv -- "${PTH:?}"; then "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC deleted" else - "$XPLR" -m 'LogError: %q' "Failed to delete $PTH_ESC" + "$XPLR" -m 'LogError: %q' "could not delete $PTH_ESC" + "$XPLR" -m 'FocusPath: %q' "$PTH" fi done < "${XPLR_PIPE_RESULT_OUT:?}") - "$XPLR" -m ExplorePwdAsync read -p "[enter to continue]" ]===], }, @@ -1812,23 +1952,25 @@ xplr.config.modes.builtin.delete = { messages = { { BashExec0 = [===[ + "$XPLR" -m ExplorePwd (while IFS= read -r -d '' PTH; do PTH_ESC=$(printf %q "$PTH") if [ -d "$PTH" ] && [ ! -L "$PTH" ]; then if rmdir -v -- "${PTH:?}"; then "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC deleted" else - "$XPLR" -m 'LogError: %q' "Failed to delete $PTH_ESC" + "$XPLR" -m 'LogError: %q' "could not delete $PTH_ESC" + "$XPLR" -m 'FocusPath: %q' "$PTH" fi else if rm -v -- "${PTH:?}"; then "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC deleted" else - "$XPLR" -m 'LogError: %q' "Failed to delete $PTH_ESC" + "$XPLR" -m 'LogError: %q' "could not delete $PTH_ESC" + "$XPLR" -m 'FocusPath: %q' "$PTH" fi fi done < "${XPLR_PIPE_RESULT_OUT:?}") - "$XPLR" -m ExplorePwdAsync read -p "[enter to continue]" ]===], }, @@ -1899,6 +2041,19 @@ xplr.config.modes.builtin.action = { "ToggleMouse", }, }, + ["p"] = { + help = "edit permissions", + messages = { + "PopMode", + { SwitchModeBuiltin = "edit_permissions" }, + { + BashExecSilently0 = [===[ + PERM=$(stat -c '%a' -- "${XPLR_FOCUS_PATH:?}") + "$XPLR" -m 'SetInputBuffer: %q' "${PERM:?}" + ]===], + }, + }, + }, ["v"] = { help = "vroot", messages = { @@ -1987,6 +2142,42 @@ xplr.config.modes.builtin.search = { "FocusNext", }, }, + ["ctrl-z"] = { + help = "toggle ordering", + messages = { + "ToggleSearchOrder", + "ExplorePwdAsync", + }, + }, + ["ctrl-a"] = { + help = "toggle search algorithm", + messages = { + "ToggleSearchAlgorithm", + "ExplorePwdAsync", + }, + }, + ["ctrl-r"] = { + help = "regex search", + messages = { + "SearchRegexFromInput", + "ExplorePwdAsync", + }, + }, + ["ctrl-f"] = { + help = "fuzzy search", + messages = { + "SearchFuzzyFromInput", + "ExplorePwdAsync", + }, + }, + ["ctrl-s"] = { + help = "sort (no search order)", + messages = { + "DisableSearchOrder", + "ExplorePwdAsync", + { SwitchModeBuiltinKeepingInputBuffer = "sort" }, + }, + }, ["right"] = { help = "enter", messages = { @@ -2026,7 +2217,7 @@ xplr.config.modes.builtin.search = { default = { messages = { "UpdateInputBufferFromKey", - "SearchFuzzyFromInput", + "SearchFromInput", "ExplorePwdAsync", }, }, @@ -2236,7 +2427,12 @@ xplr.config.modes.builtin.sort = { ["enter"] = { help = "submit", messages = { - "PopMode", + "PopModeKeepingInputBuffer", + }, + }, + ["esc"] = { + messages = { + "PopModeKeepingInputBuffer", }, }, ["m"] = { @@ -2407,6 +2603,161 @@ xplr.config.modes.builtin.vroot = { }, } +-- The builtin edit permissions mode. +-- +-- Type: [Mode](https://xplr.dev/en/mode) +xplr.config.modes.builtin.edit_permissions = { + name = "edit permissions", + key_bindings = { + on_key = { + ["u"] = { + help = "+user", + messages = { + { + BashExecSilently0 = [===[ + PERM="${XPLR_INPUT_BUFFER:-000}" + U="${PERM: -3:-2}" + G="${PERM: -2:-1}" + O="${PERM: -1}" + + U="$(( (${U:-0} + 1) % 8 ))" + "$XPLR" -m 'SetInputBuffer: %q' "${U:-0}${G:-0}${O:-0}" + ]===], + }, + }, + }, + ["U"] = { + help = "-user", + messages = { + { + BashExecSilently0 = [===[ + PERM="${XPLR_INPUT_BUFFER:-000}" + U="${PERM: -3:-2}" + G="${PERM: -2:-1}" + O="${PERM: -1}" + + U="$(( ${U:-0}-1 < 0 ? 7 : ${U:-0}-1 ))" + "$XPLR" -m 'SetInputBuffer: %q' "${U:-0}${G:-0}${O:-0}" + ]===], + }, + }, + }, + ["g"] = { + help = "+group", + messages = { + { + BashExecSilently0 = [===[ + PERM="${XPLR_INPUT_BUFFER:-000}" + U="${PERM: -3:-2}" + G="${PERM: -2:-1}" + O="${PERM: -1}" + + G="$(( (${G:-0} + 1) % 8 ))" + "$XPLR" -m 'SetInputBuffer: %q' "${U:-0}${G:-0}${O:-0}" + ]===], + }, + }, + }, + ["G"] = { + help = "-group", + messages = { + { + BashExecSilently0 = [===[ + PERM="${XPLR_INPUT_BUFFER:-000}" + U="${PERM: -3:-2}" + G="${PERM: -2:-1}" + O="${PERM: -1}" + + G="$(( ${G:-0}-1 < 0 ? 7 : ${G:-0}-1 ))" + "$XPLR" -m 'SetInputBuffer: %q' "${U:-0}${G:-0}${O:-0}" + ]===], + }, + }, + }, + ["o"] = { + help = "+other", + messages = { + { + BashExecSilently0 = [===[ + PERM="${XPLR_INPUT_BUFFER:-000}" + U="${PERM: -3:-2}" + G="${PERM: -2:-1}" + O="${PERM: -1}" + + O="$(( (${O:-0} + 1) % 8 ))" + "$XPLR" -m 'SetInputBuffer: %q' "${U:-0}${G:-0}${O:-0}" + ]===], + }, + }, + }, + ["O"] = { + help = "-other", + messages = { + { + BashExecSilently0 = [===[ + PERM="${XPLR_INPUT_BUFFER:-000}" + U="${PERM: -3:-2}" + G="${PERM: -2:-1}" + O="${PERM: -1}" + + O="$(( ${O:-0}-1 < 0 ? 7 : ${O:-0}-1 ))" + "$XPLR" -m 'SetInputBuffer: %q' "${U:-0}${G:-0}${O:-0}" + ]===], + }, + }, + }, + ["m"] = { + help = "max", + messages = { + { + BashExecSilently0 = [===[ + "$XPLR" -m 'SetInputBuffer: %q' "777" + ]===], + }, + }, + }, + ["M"] = { + help = "min", + messages = { + { + BashExecSilently0 = [===[ + "$XPLR" -m 'SetInputBuffer: %q' "000" + ]===], + }, + }, + }, + ["ctrl-r"] = { + help = "reset", + messages = { + { + BashExecSilently0 = [===[ + PERM=$(stat -c '%a' -- "${XPLR_FOCUS_PATH:?}") + "$XPLR" -m 'SetInputBuffer: %q' "${PERM:?}" + ]===], + }, + }, + }, + ["enter"] = { + help = "submit", + messages = { + { + BashExecSilently0 = [===[ + chmod "${XPLR_INPUT_BUFFER:?}" -- "${XPLR_FOCUS_PATH:?}" + ]===], + }, + "PopMode", + "ExplorePwdAsync", + }, + }, + }, + default = { + messages = { + "UpdateInputBufferFromKey", + }, + }, + }, +} + -- This is where you define custom modes. -- -- Type: mapping of the following key-value pairs: @@ -2505,6 +2856,19 @@ xplr.fn.builtin.try_complete_path = function(m) end end +xplr.fn.builtin.fmt_general_selection_item = function(n) + local nl = xplr.util.paint("\\n", { add_modifiers = { "Italic", "Dim" } }) + local sh_config = { with_prefix_dots = true, without_suffix_dots = true } + local shortened = xplr.util.shorten(n.absolute_path, sh_config) + if n.is_dir then + shortened = shortened .. "/" + end + local ls_style = xplr.util.lscolor(n.absolute_path) + local meta_style = xplr.util.node_type(n).style + local style = xplr.util.style_mix({ meta_style, ls_style }) + return xplr.util.paint(shortened:gsub("\n", nl), style) +end + -- Renders the first column in the table xplr.fn.builtin.fmt_general_table_row_cols_0 = function(m) local r = "" @@ -2521,11 +2885,10 @@ end -- Renders the second column in the table xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) + local nl = xplr.util.paint("\\n", { add_modifiers = { "Italic", "Dim" } }) local r = m.tree .. m.prefix - - local function path_escape(path) - return string.gsub(string.gsub(path, "\\", "\\\\"), "\n", "\\n") - end + local style = xplr.util.lscolor(m.absolute_path) + style = xplr.util.style_mix({ m.style, style }) if m.meta.icon == nil then r = r .. "" @@ -2533,11 +2896,11 @@ xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) r = r .. m.meta.icon .. " " end - r = r .. path_escape(m.relative_path) - + local rel = m.relative_path if m.is_dir then - r = r .. "/" + rel = rel .. "/" end + r = r .. xplr.util.paint(xplr.util.shell_escape(rel), style) r = r .. m.suffix .. " " @@ -2547,11 +2910,11 @@ xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) if m.is_broken then r = r .. "×" else - r = r .. path_escape(m.symlink.absolute_path) - + local symlink_path = xplr.util.shorten(m.symlink.absolute_path) if m.symlink.is_dir then - r = r .. "/" + symlink_path = symlink_path .. "/" end + r = r .. symlink_path:gsub("\n", nl) end end @@ -2560,84 +2923,23 @@ end -- Renders the third column in the table xplr.fn.builtin.fmt_general_table_row_cols_2 = function(m) - local no_color = os.getenv("NO_COLOR") - - local function green(x) - if no_color == nil then - return "\x1b[32m" .. x .. "\x1b[0m" - else - return x - end - end - - local function yellow(x) - if no_color == nil then - return "\x1b[33m" .. x .. "\x1b[0m" - else - return x - end - end - - local function red(x) - if no_color == nil then - return "\x1b[31m" .. x .. "\x1b[0m" - else - return x - end - end - - local function bit(x, color, cond) - if cond then - return color(x) - else - return color("-") - end - end - - local p = m.permissions - - local r = "" - - r = r .. bit("r", green, p.user_read) - r = r .. bit("w", yellow, p.user_write) - - if p.user_execute == false and p.setuid == false then - r = r .. bit("-", red, p.user_execute) - elseif p.user_execute == true and p.setuid == false then - r = r .. bit("x", red, p.user_execute) - elseif p.user_execute == false and p.setuid == true then - r = r .. bit("S", red, p.user_execute) - else - r = r .. bit("s", red, p.user_execute) - end - - r = r .. bit("r", green, p.group_read) - r = r .. bit("w", yellow, p.group_write) - - if p.group_execute == false and p.setuid == false then - r = r .. bit("-", red, p.group_execute) - elseif p.group_execute == true and p.setuid == false then - r = r .. bit("x", red, p.group_execute) - elseif p.group_execute == false and p.setuid == true then - r = r .. bit("S", red, p.group_execute) - else - r = r .. bit("s", red, p.group_execute) - end - - r = r .. bit("r", green, p.other_read) - r = r .. bit("w", yellow, p.other_write) - - if p.other_execute == false and p.setuid == false then - r = r .. bit("-", red, p.other_execute) - elseif p.other_execute == true and p.setuid == false then - r = r .. bit("x", red, p.other_execute) - elseif p.other_execute == false and p.setuid == true then - r = r .. bit("T", red, p.other_execute) - else - r = r .. bit("t", red, p.other_execute) - end - - return r + local r = xplr.util.paint("r", { fg = "Green" }) + local w = xplr.util.paint("w", { fg = "Yellow" }) + local x = xplr.util.paint("x", { fg = "Red" }) + local s = xplr.util.paint("s", { fg = "Red" }) + local S = xplr.util.paint("S", { fg = "Red" }) + local t = xplr.util.paint("t", { fg = "Red" }) + local T = xplr.util.paint("T", { fg = "Red" }) + + return xplr.util + .permissions_rwx(m.permissions) + :gsub("r", r) + :gsub("w", w) + :gsub("x", x) + :gsub("s", s) + :gsub("S", S) + :gsub("t", t) + :gsub("T", T) end -- Renders the fourth column in the table diff --git a/src/input.rs b/src/input.rs index a9fe216..6c7cdf6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -203,7 +203,7 @@ impl std::fmt::Display for Key { .unwrap_or_default() }); - write!(f, "{}", key_str) + write!(f, "{key_str}") } } diff --git a/src/lib.rs b/src/lib.rs index 00d91ad..eecb3c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod app; pub mod cli; +pub mod compat; pub mod config; pub mod directory_buffer; pub mod event_reader; @@ -12,10 +13,12 @@ pub mod input; pub mod lua; pub mod msg; pub mod node; +pub mod path; pub mod permissions; pub mod pipe; pub mod pwd_watcher; pub mod runner; +pub mod search; pub mod ui; pub mod yaml; diff --git a/src/lua/mod.rs b/src/lua/mod.rs index 3c6e05e..24014b8 100644 --- a/src/lua/mod.rs +++ b/src/lua/mod.rs @@ -143,7 +143,7 @@ pub fn call<'lua, R: Deserialize<'lua>>( func: &str, arg: mlua::Value<'lua>, ) -> Result { - let func = format!("xplr.fn.{}", func); + let func = format!("xplr.fn.{func}"); let func = resolve_fn(&lua.globals(), &func)?; let res: mlua::Value = func.call(arg)?; let res: R = lua.from_value(res)?; @@ -160,24 +160,24 @@ mod tests { assert!(check_version(VERSION, "foo path").is_ok()); // Current release if OK - assert!(check_version("0.20.2", "foo path").is_ok()); + assert!(check_version("0.21.0", "foo path").is_ok()); // Prev major release is ERR // - Not yet // Prev minor release is ERR (Change when we get to v1) - assert!(check_version("0.19.2", "foo path").is_err()); + assert!(check_version("0.20.0", "foo path").is_err()); // Prev bugfix release is OK - assert!(check_version("0.20.1", "foo path").is_ok()); + // assert!(check_version("0.21.-1", "foo path").is_ok()); // Next major release is ERR - assert!(check_version("1.20.2", "foo path").is_err()); + assert!(check_version("1.20.0", "foo path").is_err()); // Next minor release is ERR - assert!(check_version("0.21.2", "foo path").is_err()); + assert!(check_version("0.22.0", "foo path").is_err()); // Next bugfix release is ERR (Change when we get to v1) - assert!(check_version("0.20.3", "foo path").is_err()); + assert!(check_version("0.21.1", "foo path").is_err()); } } diff --git a/src/lua/util.rs b/src/lua/util.rs index 428b190..70d89dc 100644 --- a/src/lua/util.rs +++ b/src/lua/util.rs @@ -1,8 +1,19 @@ use crate::app::VERSION; +use crate::config::NodeTypesConfig; use crate::explorer; use crate::lua; use crate::msg::in_::external::ExplorerConfig; +use crate::node::Node; +use crate::path; +use crate::path::RelativityConfig; +use crate::permissions::Octal; +use crate::permissions::Permissions; +use crate::ui; +use crate::ui::Layout; +use crate::ui::Style; +use crate::ui::WrapOptions; use anyhow::Result; +use lscolors::LsColors; use mlua::Error as LuaError; use mlua::Lua; use mlua::LuaSerdeExt; @@ -13,27 +24,10 @@ use serde::de::Error; use serde::{Deserialize, Serialize}; use serde_json as json; use serde_yaml as yaml; +use std::borrow::Cow; use std::path::PathBuf; use std::process::Command; -pub(crate) fn create_table(lua: &Lua) -> Result { - let mut util = lua.create_table()?; - - util = version(util, lua)?; - util = dirname(util, lua)?; - util = basename(util, lua)?; - util = absolute(util, lua)?; - util = explore(util, lua)?; - util = shell_execute(util, lua)?; - util = shell_quote(util, lua)?; - util = from_json(util, lua)?; - util = to_json(util, lua)?; - util = from_yaml(util, lua)?; - util = to_yaml(util, lua)?; - - Ok(util) -} - /// Get the xplr version details. /// /// Type: function() -> { major: number, minor: number, patch: number } @@ -70,6 +64,209 @@ pub fn version<'a>(util: Table<'a>, lua: &Lua) -> Result> { Ok(util) } +/// Clone/deepcopy a Lua value. Doesn't work with functions. +/// +/// Type: function( value ) -> value +/// +/// Example: +/// +/// ```lua +/// local val = { foo = "bar" } +/// local val_clone = xplr.util.clone(val) +/// val.foo = "baz" +/// print(val_clone.foo) +/// -- "bar" +/// ``` +pub fn clone<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(move |lua, value: Value| { + lua::serialize(lua, &value).map_err(LuaError::custom) + })?; + util.set("clone", func)?; + Ok(util) +} + +/// Check if the given path exists. +/// +/// Type: function( path:string ) -> boolean +/// +/// Example: +/// +/// ```lua +/// xplr.util.exists("/foo/bar") +/// -- true +/// ``` +pub fn exists<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = + lua.create_function(move |_, path: String| Ok(PathBuf::from(path).exists()))?; + util.set("exists", func)?; + Ok(util) +} + +/// Check if the given path is a directory. +/// +/// Type: function( path:string ) -> boolean +/// +/// Example: +/// +/// ```lua +/// xplr.util.is_dir("/foo/bar") +/// -- true +/// ``` +pub fn is_dir<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = + lua.create_function(move |_, path: String| Ok(PathBuf::from(path).is_dir()))?; + util.set("is_dir", func)?; + Ok(util) +} + +/// Check if the given path is a file. +/// +/// Type: function( path:string ) -> boolean +/// +/// Example: +/// +/// ```lua +/// xplr.util.is_file("/foo/bar") +/// -- true +/// ``` +pub fn is_file<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = + lua.create_function(move |_, path: String| Ok(PathBuf::from(path).is_dir()))?; + util.set("is_file", func)?; + Ok(util) +} + +/// Check if the given path is a symlink. +/// +/// Type: function( path:string ) -> boolean +/// +/// Example: +/// +/// ```lua +/// xplr.util.is_file("/foo/bar") +/// -- true +/// ``` +pub fn is_symlink<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua + .create_function(move |_, path: String| Ok(PathBuf::from(path).is_symlink()))?; + util.set("is_symlink", func)?; + Ok(util) +} + +/// Check if the given path is an absolute path. +/// +/// Type: function( path:string ) -> boolean +/// +/// Example: +/// +/// ```lua +/// xplr.util.is_absolute("/foo/bar") +/// -- true +/// ``` +pub fn is_absolute<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua + .create_function(move |_, path: String| Ok(PathBuf::from(path).is_absolute()))?; + util.set("is_absolute", func)?; + Ok(util) +} + +/// Split a path into its components. +/// +/// Type: function( path:string ) -> boolean +/// +/// Example: +/// +/// ```lua +/// xplr.util.path_split("/foo/bar") +/// -- { "/", "foo", "bar" } +/// +/// xplr.util.path_split(".././foo") +/// -- { "..", "foo" } +/// ``` +pub fn path_split<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(move |_, path: String| { + let components: Vec = PathBuf::from(path) + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect(); + Ok(components) + })?; + util.set("path_split", func)?; + Ok(util) +} + +/// Get [Node][5] information of a given path. +/// Doesn't check if the path exists. +/// Returns nil if the path is "/". +/// Errors out if absolute path can't be obtained. +/// +/// Type: function( path:string ) -> [Node][5]|nil +/// +/// Example: +/// +/// ```lua +/// xplr.util.node("./bar") +/// -- { parent = "/pwd", relative_path = "bar", absolute_path = "/pwd/bar", ... } +/// +/// xplr.util.node("/") +/// -- nil +/// ``` +pub fn node<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(move |lua, path: String| { + let path = PathBuf::from(path); + let abs = path.absolutize()?; + match (abs.parent(), abs.file_name()) { + (Some(parent), Some(name)) => { + let node = Node::new( + parent.to_string_lossy().to_string(), + name.to_string_lossy().to_string(), + ); + Ok(lua::serialize(lua, &node).map_err(LuaError::custom)?) + } + (_, _) => Ok(Value::Nil), + } + })?; + util.set("node", func)?; + Ok(util) +} + +/// Get the configured [Node Type][6] of a given [Node][5]. +/// +/// Type: function( [Node][5], [xplr.config.node_types][7]|nil ) -> [Node Type][6] +/// +/// If the second argument is missing, global config `xplr.config.node_types` +/// will be used. +/// +/// Example: +/// +/// ```lua +/// xplr.util.node_type(app.focused_node) +/// -- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... } +/// +/// xplr.util.node_type(xplr.util.node("/foo/bar"), xplr.config.node_types) +/// -- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... } +/// ``` +pub fn node_type<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = + lua.create_function(move |lua, (node, config): (Table, Option
)| { + let node: Node = lua.from_value(Value::Table(node))?; + let config: Table = if let Some(config) = config { + config + } else { + lua.globals() + .get::<_, Table>("xplr")? + .get::<_, Table>("config")? + .get::<_, Table>("node_types")? + }; + let config: NodeTypesConfig = lua.from_value(Value::Table(config))?; + let node_type = config.get(&node); + let node_type = lua::serialize(lua, &node_type).map_err(LuaError::custom)?; + Ok(node_type) + })?; + util.set("node_type", func)?; + Ok(util) +} + /// Get the directory name of a given path. /// /// Type: function( path:string ) -> path:string|nil @@ -125,32 +322,119 @@ pub fn basename<'a>(util: Table<'a>, lua: &Lua) -> Result> { /// ``` pub fn absolute<'a>(util: Table<'a>, lua: &Lua) -> Result> { let func = lua.create_function(|_, path: String| { - let parent = PathBuf::from(path) + let abs = PathBuf::from(path) .absolutize()? .to_string_lossy() .to_string(); - Ok(parent) + Ok(abs) })?; util.set("absolute", func)?; Ok(util) } +/// Get the relative path based on the given base path or current working dir. +/// Will error if it fails to determine a relative path. +/// +/// Type: function( path:string, options:table|nil ) -> path:string +/// +/// Options type: { base:string|nil, with_prefix_dots:bookean|nil, without_suffix_dots:boolean|nil } +/// +/// - If `base` path is given, the path will be relative to it. +/// - If `with_prefix_dots` is true, the path will always start with dots `..` / `.` +/// - If `without_suffix_dots` is true, the name will be visible instead of dots `..` / `.` +/// +/// Example: +/// +/// ```lua +/// xplr.util.relative_to("/present/working/directory") +/// -- "." +/// +/// xplr.util.relative_to("/present/working/directory/foo") +/// -- "foo" +/// +/// xplr.util.relative_to("/present/working/directory/foo", { with_prefix_dots = true }) +/// -- "./foo" +/// +/// xplr.util.relative_to("/present/working/directory", { without_suffix_dots = true }) +/// -- "../directory" +/// +/// xplr.util.relative_to("/present/working") +/// -- ".." +/// +/// xplr.util.relative_to("/present/working", { without_suffix_dots = true }) +/// -- "../../working" +/// +/// xplr.util.relative_to("/present/working/directory", { base = "/present/foo/bar" }) +/// -- "../../working/directory" +/// ``` +pub fn relative_to<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(|lua, (path, config): (String, Option
)| { + let config: Option> = + lua.from_value(config.map(Value::Table).unwrap_or(Value::Nil))?; + path::relative_to(path, config.as_ref()) + .map(|p| p.to_string_lossy().to_string()) + .map_err(LuaError::custom) + })?; + util.set("relative_to", func)?; + Ok(util) +} + +/// Shorten the given absolute path using the following rules: +/// - either relative to your home dir if it makes sense +/// - or relative to the current working directory +/// - or absolute path if it makes the most sense +/// +/// Type: Similar to `xplr.util.relative_to` +/// +/// Example: +/// +/// ```lua +/// xplr.util.shorten("/home/username/.config") +/// -- "~/.config" +/// +/// xplr.util.shorten("/present/working/directory") +/// -- "." +/// +/// xplr.util.shorten("/present/working/directory/foo") +/// -- "foo" +/// +/// xplr.util.shorten("/present/working/directory/foo", { with_prefix_dots = true }) +/// -- "./foo" +/// +/// xplr.util.shorten("/present/working/directory", { without_suffix_dots = true }) +/// -- "../directory" +/// +/// xplr.util.shorten("/present/working/directory", { base = "/present/foo/bar" }) +/// -- "../../working/directory" +/// +/// xplr.util.shorten("/tmp") +/// -- "/tmp" +/// ``` +pub fn shorten<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = + lua.create_function(move |lua, (path, config): (String, Option
)| { + let config: Option> = + lua.from_value(config.map(Value::Table).unwrap_or(Value::Nil))?; + path::shorten(path, config.as_ref()).map_err(LuaError::custom) + })?; + util.set("shorten", func)?; + Ok(util) +} + /// Explore directories with the given explorer config. /// -/// Type: function( path:string, config:[Explorer Config][1]|nil ) -/// -> { node:[Node][2]... } +/// Type: function( path:string, [ExplorerConfig][1]|nil ) -> { [Node][2], ... } /// /// Example: /// /// ```lua /// /// xplr.util.explore("/tmp") +/// -- { { absolute_path = "/tmp/a", ... }, ... } +/// /// xplr.util.explore("/tmp", app.explorer_config) /// -- { { absolute_path = "/tmp/a", ... }, ... } /// ``` -/// -/// [1]: https://xplr.dev/en/lua-function-calls#explorer-config -/// [2]: https://xplr.dev/en/lua-function-calls#node pub fn explore<'a>(util: Table<'a>, lua: &Lua) -> Result> { let func = lua.create_function(|lua, (path, config): (String, Option
)| { let config: ExplorerConfig = if let Some(cfg) = config { @@ -170,13 +454,14 @@ pub fn explore<'a>(util: Table<'a>, lua: &Lua) -> Result> { /// Execute shell commands safely. /// -/// Type: function( program:string, args:{ arg:string... }|nil ) -/// -> { stdout = string, stderr = string, returncode = number|nil } +/// Type: function( program:string, args:{ string, ... }|nil ) -> { stdout = string, stderr = string, returncode = number|nil } /// /// Example: /// /// ```lua /// xplr.util.shell_execute("pwd") +/// -- "/present/working/directory" +/// /// xplr.util.shell_execute("bash", {"-c", "xplr --help"}) /// -- { stdout = "xplr...", stderr = "", returncode = 0 } /// ``` @@ -218,9 +503,28 @@ pub fn shell_quote<'a>(util: Table<'a>, lua: &Lua) -> Result> { Ok(util) } +/// Escape commands and paths safely. +/// +/// Type: function( string ) -> string +/// +/// Example: +/// +/// ```lua +/// xplr.util.shell_escape("a'b\"c") +/// -- "\"a'b\\\"c\"" +/// ``` +pub fn shell_escape<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(move |_, string: String| { + let val = path::escape(&string).to_string(); + Ok(val) + })?; + util.set("shell_escape", func)?; + Ok(util) +} + /// Load JSON string into Lua value. /// -/// Type: function( string ) -> value +/// Type: function( string ) -> any /// /// Example: /// @@ -245,11 +549,11 @@ pub fn from_json<'a>(util: Table<'a>, lua: &Lua) -> Result> { /// /// ```lua /// xplr.util.to_json({ foo = "bar" }) -/// -- [[{ "foos": "bar" }]] +/// -- [[{ "foo": "bar" }]] /// /// xplr.util.to_json({ foo = "bar" }, { pretty = true }) /// -- [[{ -/// -- "foos": "bar" +/// -- "foo": "bar" /// -- }]] /// ``` pub fn to_json<'a>(util: Table<'a>, lua: &Lua) -> Result> { @@ -317,3 +621,252 @@ pub fn to_yaml<'a>(util: Table<'a>, lua: &Lua) -> Result> { util.set("to_yaml", func)?; Ok(util) } + +/// Get a [Style][3] object for the given path based on the LS_COLORS +/// environment variable. +/// +/// Type: function( path:string ) -> [Style][3]|nil +/// +/// Example: +/// +/// ```lua +/// xplr.util.lscolor("Desktop") +/// -- { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} } +/// ``` +pub fn lscolor<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let lscolors = LsColors::from_env().unwrap_or_default(); + let func = lua.create_function(move |lua, path: String| { + let style = lscolors.style_for_path(path).map(Style::from); + lua::serialize(lua, &style).map_err(LuaError::custom) + })?; + util.set("lscolor", func)?; + Ok(util) +} + +/// Apply style (escape sequence) to string using a given [Style][3] object. +/// +/// Type: function( string, [Style][3]|nil ) -> string +/// +/// Example: +/// +/// ```lua +/// xplr.util.paint("Desktop", { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} }) +/// -- "\u001b[31mDesktop\u001b[0m" +/// ``` +pub fn paint<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = + lua.create_function(|lua, (string, style): (String, Option
)| { + if *ui::NO_COLOR { + return Ok(string); + } + + if let Some(style) = style { + let style: Style = lua.from_value(Value::Table(style))?; + let ansi_style: nu_ansi_term::Style = style.into(); + Ok::(ansi_style.paint(string).to_string()) + } else { + Ok(string) + } + })?; + util.set("paint", func)?; + Ok(util) +} + +/// Mix multiple [Style][3] objects into one. +/// +/// Type: function( { [Style][3], [Style][3], ... } ) -> [Style][3] +/// +/// Example: +/// +/// ```lua +/// xplr.util.style_mix({{ fg = "Red" }, { bg = "Blue" }, { add_modifiers = {"Bold"} }}) +/// -- { fg = "Red", bg = "Blue", add_modifiers = { "Bold" }, sub_modifiers = {} } +/// ``` +pub fn style_mix<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(|lua, styles: Vec
| { + let mut style = Style::default(); + for other in styles { + let other: Style = lua.from_value(Value::Table(other))?; + style = style.extend(&other); + } + + lua::serialize(lua, &style).map_err(LuaError::custom) + })?; + util.set("style_mix", func)?; + Ok(util) +} + +/// Wrap the given text to fit the specified width. +/// It will try to not split words when possible. +/// +/// Type: function( string, options:number|table ) -> { string, ...} +/// +/// Options type: { width = number, initial_indent = string|nil, subsequent_indent = string|nil, break_words = boolean|nil } +/// +/// Example: +/// +/// ```lua +/// xplr.util.textwrap("this will be cut off", 11) +/// -- { "this will', 'be cut off" } +/// +/// xplr.util.textwrap( +/// "this will be cut off", +/// { width = 12, initial_indent = "", subsequent_indent = " ", break_words = false } +/// ) +/// -- { "this will be", " cut off" } +/// ``` +pub fn textwrap<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(|lua, (text, options): (String, Value)| { + let lines = match lua.from_value::(options.clone()) { + Ok(width) => textwrap::wrap(&text, width), + Err(_) => { + let options = lua.from_value::(options)?; + textwrap::wrap(&text, options.get_options()) + } + }; + + Ok(lines.iter().map(Cow::to_string).collect::>()) + })?; + util.set("textwrap", func)?; + Ok(util) +} + +/// Find the target layout in the given layout and replace it with the replacement layout, +/// returning a new layout. +/// +/// Type: function( layout:[Layout][4], target:[Layout][4], replacement:[Layout][4] ) -> layout:[Layout][4] +/// +/// Example: +/// +/// ```lua +/// local layout = { +/// Horizontal = { +/// splits = { +/// "Table", -- Target +/// "HelpMenu", +/// }, +/// config = ..., +/// } +/// } +/// +/// xplr.util.layout_replace(layout, "Table", "Selection") +/// -- { +/// -- Horizontal = { +/// -- splits = { +/// -- "Selection", -- Replacement +/// -- "HelpMenu", +/// -- }, +/// -- config = ... +/// -- } +/// -- } +/// ``` +pub fn layout_replace<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function( + move |lua, (layout, target, replacement): (Value, Value, Value)| { + let layout: Layout = lua.from_value(layout)?; + let target: Layout = lua.from_value(target)?; + let replacement: Layout = lua.from_value(replacement)?; + + let res = layout.replace(&target, &replacement); + let res = lua::serialize(lua, &res).map_err(LuaError::custom)?; + + Ok(res) + }, + )?; + util.set("layout_replace", func)?; + Ok(util) +} + +/// Convert [Permission][8] to rwxrwxrwx representation with special bits. +/// +/// Type: function( [Permission][8] ) -> string +/// +/// Example: +/// +/// ```lua +/// xplr.util.permissions_rwx({ user_read = true }) +/// -- "r--------" +/// +/// xplr.util.permissions_rwx(app.focused_node.permission) +/// -- "rwxrwsrwT" +/// ``` +pub fn permissions_rwx<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(|lua, permission: Table| { + let permissions: Permissions = lua.from_value(Value::Table(permission))?; + let permissions = permissions.to_string(); + Ok(permissions) + })?; + util.set("permissions_rwx", func)?; + Ok(util) +} + +/// Convert [Permission][8] to octal representation. +/// +/// Type: function( [Permission][8] ) -> { number, number, number, number } +/// +/// Example: +/// +/// ```lua +/// xplr.util.permissions_octal({ user_read = true }) +/// -- { 0, 4, 0, 0 } +/// +/// xplr.util.permissions_octal(app.focused_node.permission) +/// -- { 0, 7, 5, 4 } +/// ``` +pub fn permissions_octal<'a>(util: Table<'a>, lua: &Lua) -> Result> { + let func = lua.create_function(|lua, permission: Table| { + let permissions: Permissions = lua.from_value(Value::Table(permission))?; + let permissions: Octal = permissions.into(); + let permissions = lua::serialize(lua, &permissions).map_err(LuaError::custom)?; + Ok(permissions) + })?; + util.set("permissions_octal", func)?; + Ok(util) +} + +/// +/// [1]: https://xplr.dev/en/lua-function-calls#explorer-config +/// [2]: https://xplr.dev/en/lua-function-calls#node +/// [3]: https://xplr.dev/en/style +/// [4]: https://xplr.dev/en/layout +/// [5]: https://xplr.dev/en/lua-function-calls#node +/// [6]: https://xplr.dev/en/node-type +/// [7]: https://xplr.dev/en/node_types +/// [8]: https://xplr.dev/en/column-renderer#permission + +pub(crate) fn create_table(lua: &Lua) -> Result
{ + let mut util = lua.create_table()?; + + util = version(util, lua)?; + util = clone(util, lua)?; + util = exists(util, lua)?; + util = is_dir(util, lua)?; + util = is_file(util, lua)?; + util = is_symlink(util, lua)?; + util = is_absolute(util, lua)?; + util = path_split(util, lua)?; + util = node(util, lua)?; + util = node_type(util, lua)?; + util = dirname(util, lua)?; + util = basename(util, lua)?; + util = absolute(util, lua)?; + util = relative_to(util, lua)?; + util = shorten(util, lua)?; + util = explore(util, lua)?; + util = shell_execute(util, lua)?; + util = shell_quote(util, lua)?; + util = shell_escape(util, lua)?; + util = from_json(util, lua)?; + util = to_json(util, lua)?; + util = from_yaml(util, lua)?; + util = to_yaml(util, lua)?; + util = lscolor(util, lua)?; + util = paint(util, lua)?; + util = style_mix(util, lua)?; + util = textwrap(util, lua)?; + util = layout_replace(util, lua)?; + util = permissions_rwx(util, lua)?; + util = permissions_octal(util, lua)?; + + Ok(util) +} diff --git a/src/msg/in_/external.rs b/src/msg/in_/external.rs index 6c91346..6de0307 100644 --- a/src/msg/in_/external.rs +++ b/src/msg/in_/external.rs @@ -1,8 +1,11 @@ -use crate::{app::Node, input::InputOperation}; +use crate::app::Node; +use crate::input::InputOperation; +use crate::search::PathItem; +use crate::search::SearchAlgorithm; use indexmap::IndexSet; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; +use std::{cmp::Ordering, sync::Arc}; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ExternalMsg { @@ -74,6 +77,14 @@ pub enum ExternalMsg { /// - YAML: `FocusNext` FocusNext, + /// Focus on the next selected node. + /// + /// Example: + /// + /// - Lua: `"FocusNextSelection"` + /// - YAML: `FocusNextSelection` + FocusNextSelection, + /// Focus on the `n`th node relative to the current focus where `n` is a /// given value. /// @@ -102,6 +113,14 @@ pub enum ExternalMsg { /// - YAML: `FocusPrevious` FocusPrevious, + /// Focus on the previous selection item. + /// + /// Example: + /// + /// - Lua: `"FocusPreviousSelection"` + /// - YAML: `FocusPreviousSelection` + FocusPreviousSelection, + /// Focus on the `-n`th node relative to the current focus where `n` is a /// given value. /// @@ -905,6 +924,26 @@ pub enum ExternalMsg { /// ### Search Operations -------------------------------------------------- + /// Search files using the current or default (fuzzy) search algorithm. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { Search = "string" } + /// + /// Example: + /// + /// - Lua: `{ Search = "pattern" }` + /// - YAML: `Search: pattern` + Search(String), + + /// Calls `Search` with the input taken from the input buffer. + /// + /// Example: + /// + /// - Lua: `"SearchFromInput"` + /// - YAML: `SearchFromInput` + SearchFromInput, + /// Search files using fuzzy match algorithm. /// It keeps the filters, but overrides the sorters. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. @@ -920,6 +959,7 @@ pub enum ExternalMsg { /// Calls `SearchFuzzy` with the input taken from the input buffer. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. /// /// Example: /// @@ -927,6 +967,109 @@ pub enum ExternalMsg { /// - YAML: `SearchFuzzyFromInput` SearchFuzzyFromInput, + /// Like `SearchFuzzy`, but doesn't not perform rank based sorting. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { SearchFuzzyUnordered = "string" } + /// + /// Example: + /// + /// - Lua: `{ SearchFuzzyUnordered = "pattern" }` + /// - YAML: `SearchFuzzyUnordered: pattern` + SearchFuzzyUnordered(String), + + /// Calls `SearchFuzzyUnordered` with the input taken from the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Example: + /// + /// - Lua: `"SearchFuzzyUnorderedFromInput"` + /// - YAML: `SearchFuzzyUnorderedFromInput` + SearchFuzzyUnorderedFromInput, + + /// Search files using regex match algorithm. + /// It keeps the filters, but overrides the sorters. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { SearchRegex = "string" } + /// + /// Example: + /// + /// - Lua: `{ SearchRegex = "pattern" }` + /// - YAML: `SearchRegex: pattern` + SearchRegex(String), + + /// Calls `SearchRegex` with the input taken from the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Example: + /// + /// - Lua: `"SearchRegexFromInput"` + /// - YAML: `SearchRegexFromInput` + SearchRegexFromInput, + + /// Like `SearchRegex`, but doesn't not perform rank based sorting. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Type: { SearchRegexUnordered = "string" } + /// + /// Example: + /// + /// - Lua: `{ SearchRegexUnordered = "pattern" }` + /// - YAML: `SearchRegexUnordered: pattern` + SearchRegexUnordered(String), + + /// Calls `SearchRegexUnordered` with the input taken from the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// It gets reset automatically when changing directory. + /// + /// Example: + /// + /// - Lua: `"SearchRegexUnorderedFromInput"` + /// - YAML: `SearchRegexUnorderedFromInput` + SearchRegexUnorderedFromInput, + + /// Toggles between different search algorithms, without changing the input + /// buffer + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// + /// Example: + /// + /// - Lua: `"ToggleSearchAlgorithm"` + /// - YAML: `ToggleSearchAlgorithm` + ToggleSearchAlgorithm, + + /// Enables ranked search without changing the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// + /// Example: + /// + /// - Lua: `"EnableOrderedSearch"` + /// - YAML: `EnableSearchOrder` + EnableSearchOrder, + + /// Disabled ranked search without changing the input buffer. + /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. + /// + /// Example: + /// + /// - Lua: `"DisableSearchOrder"` + /// - YAML: `DisableSearchOrder` + DisableSearchOrder, + + /// Toggles ranked search without changing the input buffer. + /// + /// Example: + /// + /// - Lua: `"ToggleSearchOrder"` + /// - YAML: `ToggleSearchOrder` + ToggleSearchOrder, + /// Accepts the search by keeping the latest focus while in search mode. /// Automatically calls `ExplorePwd`. /// @@ -1635,18 +1778,78 @@ impl NodeFilterApplicable { } #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub struct NodeSearcher { +pub struct NodeSearcherApplicable { pub pattern: String, #[serde(default)] pub recoverable_focus: Option, + + #[serde(default)] + pub algorithm: SearchAlgorithm, + + #[serde(default)] + pub unordered: bool, } -impl NodeSearcher { - pub fn new(pattern: String, recoverable_focus: Option) -> Self { +impl NodeSearcherApplicable { + pub fn new( + pattern: String, + recoverable_focus: Option, + algorithm: SearchAlgorithm, + unordered: bool, + ) -> Self { Self { pattern, recoverable_focus, + algorithm, + unordered, + } + } + + pub fn search(&self, nodes: I) -> Vec + where + I: IntoIterator, + { + let engine = self.algorithm.engine(&self.pattern); + let ranked_nodes = nodes.into_iter().filter_map(|n| { + let item = Arc::new(PathItem::from(n.relative_path.clone())); + engine.match_item(item).map(|res| (n, res.rank)) + }); + + if self.unordered { + ranked_nodes.map(|(n, _)| n).collect() + } else { + let mut ranked_nodes = ranked_nodes.collect::>(); + ranked_nodes.sort_by(|(_, s1), (_, s2)| s1.cmp(s2)); + ranked_nodes.into_iter().map(|(n, _)| n).collect() + } + } + + pub fn enable_search_order(self) -> Self { + Self { + unordered: false, + ..self + } + } + + pub fn disable_search_order(self) -> Self { + Self { + unordered: true, + ..self + } + } + + pub fn toggle_search_order(self) -> Self { + Self { + unordered: !self.unordered, + ..self + } + } + + pub fn toggle_algorithm(self) -> Self { + Self { + algorithm: self.algorithm.toggle(), + ..self } } } @@ -1660,7 +1863,7 @@ pub struct ExplorerConfig { pub sorters: IndexSet, #[serde(default)] - pub searcher: Option, + pub searcher: Option, } impl ExplorerConfig { diff --git a/src/node.rs b/src/node.rs index b6547f4..4467ff1 100644 --- a/src/node.rs +++ b/src/node.rs @@ -10,9 +10,16 @@ fn to_human_size(size: u64) -> String { format_size(size, DECIMAL) } -fn mime_essence(path: &Path, is_dir: bool) -> String { +fn mime_essence( + path: &Path, + is_dir: bool, + extension: &str, + is_executable: bool, +) -> String { if is_dir { String::from("inode/directory") + } else if extension.is_empty() && is_executable { + String::from("application/x-executable") } else { mime_guess::from_path(path) .first() @@ -44,29 +51,43 @@ impl ResolvedNode { .map(|e| e.to_string_lossy().to_string()) .unwrap_or_default(); - let (is_dir, is_file, is_readonly, size, created, last_modified, uid, gid) = - path.metadata() - .map(|m| { - ( - m.is_dir(), - m.is_file(), - m.permissions().readonly(), - m.len(), - m.created() - .ok() - .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) - .map(|d| d.as_nanos()), - m.modified() - .ok() - .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) - .map(|d| d.as_nanos()), - m.uid(), - m.gid(), - ) - }) - .unwrap_or((false, false, false, 0, None, None, 0, 0)); - - let mime_essence = mime_essence(&path, is_dir); + let ( + is_dir, + is_file, + is_readonly, + size, + permissions, + created, + last_modified, + uid, + gid, + ) = path + .metadata() + .map(|m| { + ( + m.is_dir(), + m.is_file(), + m.permissions().readonly(), + m.len(), + Permissions::from(&m), + m.created() + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_nanos()), + m.modified() + .ok() + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_nanos()), + m.uid(), + m.gid(), + ) + }) + .unwrap_or((false, false, false, 0, Default::default(), None, None, 0, 0)); + + let is_executable = permissions.user_execute + || permissions.group_execute + || permissions.other_execute; + let mime_essence = mime_essence(&path, is_dir, &extension, is_executable); let human_size = to_human_size(size); Self { @@ -177,7 +198,11 @@ impl Node { ) }); - let mime_essence = mime_essence(&path, is_dir); + let is_executable = permissions.user_execute + || permissions.group_execute + || permissions.other_execute; + + let mime_essence = mime_essence(&path, is_dir, &extension, is_executable); let human_size = to_human_size(size); Self { diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..2582d73 --- /dev/null +++ b/src/path.rs @@ -0,0 +1,495 @@ +use anyhow::{bail, Result}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +pub use snailquote::escape; +use std::path::{Component, Path, PathBuf}; + +lazy_static! { + pub static ref HOME: Option = dirs::home_dir(); +} + +// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs +pub fn diff(path: P, base: B) -> Result +where + P: AsRef, + B: AsRef, +{ + let path = path.as_ref(); + let base = base.as_ref(); + + if path.is_absolute() != base.is_absolute() { + if path.is_absolute() { + Ok(PathBuf::from(path)) + } else { + let path = path.to_string_lossy(); + bail!("{path}: is not absolute") + } + } else { + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + loop { + match (ita.next(), itb.next()) { + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(Component::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(b)) if b == Component::CurDir => comps.push(a), + (Some(_), Some(b)) if b == Component::ParentDir => { + let path = path.to_string_lossy(); + let base = base.to_string_lossy(); + bail!("{base} is not a parent of {path}") + } + (Some(a), Some(_)) => { + comps.push(Component::ParentDir); + for _ in itb { + comps.push(Component::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + Ok(comps.iter().map(|c| c.as_os_str()).collect()) + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct RelativityConfig> { + base: Option, + with_prefix_dots: Option, + without_suffix_dots: Option, +} + +impl> RelativityConfig { + pub fn with_base(mut self, base: B) -> Self { + self.base = Some(base); + self + } + + pub fn with_prefix_dots(mut self) -> Self { + self.with_prefix_dots = Some(true); + self + } + + pub fn without_suffix_dots(mut self) -> Self { + self.without_suffix_dots = Some(true); + self + } +} + +pub fn relative_to( + path: P, + config: Option<&RelativityConfig>, +) -> Result +where + P: AsRef, + B: AsRef, +{ + let path = path.as_ref(); + let base = match config.and_then(|c| c.base.as_ref()) { + Some(base) => PathBuf::from(base.as_ref()), + None => std::env::current_dir()?, + }; + + let diff = diff(path, base)?; + + let relative = if diff.to_str() == Some("") { + ".".into() + } else { + diff + }; + + let relative = if config.and_then(|c| c.with_prefix_dots).unwrap_or(false) + && !relative.starts_with(".") + && !relative.starts_with("..") + { + PathBuf::from(".").join(relative) + } else { + relative + }; + + let relative = if !config.and_then(|c| c.without_suffix_dots).unwrap_or(false) { + relative + } else if relative.ends_with(".") { + match (path.parent(), path.file_name()) { + (Some(_), Some(name)) => PathBuf::from("..").join(name), + (_, _) => relative, + } + } else if relative.ends_with("..") { + match (path.parent(), path.file_name()) { + (Some(parent), Some(name)) => { + if parent.parent().is_some() { + relative.join("..").join(name) + } else { + // always prefer absolute path if it's a child of the root directory + // to guarantee that the basename is included + path.into() + } + } + (_, _) => relative, + } + } else { + relative + }; + + Ok(relative) +} + +pub fn shorten(path: P, config: Option<&RelativityConfig>) -> Result +where + P: AsRef, + B: AsRef, +{ + let path = path.as_ref(); + let pathstring = path.to_string_lossy().to_string(); + let relative = relative_to(path, config)?; + + let relative = relative.to_string_lossy().to_string(); + + let fromhome = HOME + .as_ref() + .and_then(|h| { + path.strip_prefix(h).ok().map(|p| { + if p.to_str() == Some("") { + "~".into() + } else { + PathBuf::from("~").join(p).to_string_lossy().to_string() + } + }) + }) + .unwrap_or(pathstring); + + if relative.len() < fromhome.len() { + Ok(relative) + } else { + Ok(fromhome) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + type Config<'a> = Option<&'a RelativityConfig>; + + const NONE: Config = Config::None; + + fn default<'a>() -> RelativityConfig<&'a str> { + Default::default() + } + + #[test] + fn test_relative_to_pwd() { + let path = std::env::current_dir().unwrap(); + + let relative = relative_to(&path, NONE).unwrap(); + assert_eq!(relative, PathBuf::from(".")); + + let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap(); + assert_eq!(relative, PathBuf::from(".")); + + let relative = + relative_to(&path, Some(&default().without_suffix_dots())).unwrap(); + assert_eq!( + relative, + PathBuf::from("..").join(path.file_name().unwrap()) + ); + + let relative = relative_to( + &path, + Some(&default().with_prefix_dots().without_suffix_dots()), + ) + .unwrap(); + assert_eq!( + relative, + PathBuf::from("..").join(path.file_name().unwrap()) + ); + } + + #[test] + fn test_relative_to_parent() { + let path = std::env::current_dir().unwrap(); + let parent = path.parent().unwrap(); + + let relative = relative_to(parent, NONE).unwrap(); + assert_eq!(relative, PathBuf::from("..")); + + let relative = relative_to(parent, Some(&default().with_prefix_dots())).unwrap(); + assert_eq!(relative, PathBuf::from("..")); + + let relative = + relative_to(parent, Some(&default().without_suffix_dots())).unwrap(); + assert_eq!( + relative, + PathBuf::from("../..").join(parent.file_name().unwrap()) + ); + + let relative = relative_to( + parent, + Some(&default().with_prefix_dots().without_suffix_dots()), + ) + .unwrap(); + assert_eq!( + relative, + PathBuf::from("../..").join(parent.file_name().unwrap()) + ); + } + + #[test] + fn test_relative_to_file() { + let path = std::env::current_dir().unwrap().join("foo").join("bar"); + + let relative = relative_to(&path, NONE).unwrap(); + assert_eq!(relative, PathBuf::from("foo/bar")); + + let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap(); + assert_eq!(relative, PathBuf::from("./foo/bar")); + + let relative = relative_to( + &path, + Some(&default().with_prefix_dots().without_suffix_dots()), + ) + .unwrap(); + assert_eq!(relative, PathBuf::from("./foo/bar")); + } + + #[test] + fn test_relative_to_root() { + let relative = relative_to("/foo", Some(&default().with_base("/"))).unwrap(); + assert_eq!(relative, PathBuf::from("foo")); + + let relative = relative_to( + "/foo", + Some( + &default() + .with_base("/") + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(relative, PathBuf::from("./foo")); + + let relative = relative_to("/", Some(&default().with_base("/"))).unwrap(); + assert_eq!(relative, PathBuf::from(".")); + + let relative = relative_to( + "/", + Some( + &default() + .with_base("/") + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(relative, PathBuf::from(".")); + + let relative = relative_to("/", Some(&default().with_base("/foo"))).unwrap(); + assert_eq!(relative, PathBuf::from("..")); + + let relative = relative_to( + "/", + Some( + &default() + .with_base("/foo") + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(relative, PathBuf::from("..")); + } + + #[test] + fn test_relative_to_base() { + let path = "/some/directory"; + let base = "/another/foo/bar"; + + let relative = relative_to(path, Some(&default().with_base(base))).unwrap(); + assert_eq!(relative, PathBuf::from("../../../some/directory")); + + let relative = relative_to( + path, + Some( + &default() + .with_base(base) + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(relative, PathBuf::from("../../../some/directory")); + } + + #[test] + fn test_shorten_home() { + let path = HOME.as_ref().unwrap(); + + let res = shorten(path, NONE).unwrap(); + assert_eq!(res, "~"); + + let res = shorten( + path, + Some(&default().with_prefix_dots().without_suffix_dots()), + ) + .unwrap(); + assert_eq!(res, "~"); + + let res = shorten( + path, + Some(&default().with_prefix_dots().without_suffix_dots()), + ) + .unwrap(); + assert_eq!(res, "~"); + + let res = shorten(path.join("foo"), NONE).unwrap(); + assert_eq!(res, "~/foo"); + + let res = shorten( + path.join("foo"), + Some(&default().with_prefix_dots().without_suffix_dots()), + ) + .unwrap(); + assert_eq!(res, "~/foo"); + + let res = shorten(format!("{}foo", path.to_string_lossy()), NONE).unwrap(); + assert_ne!(res, "~/foo"); + assert_eq!(res, format!("{}foo", path.to_string_lossy())); + } + + #[test] + fn test_shorten_base() { + let path = "/present/working/directory"; + let base = "/present/foo/bar"; + + let res = shorten(path, Some(&default().with_base(base))).unwrap(); + assert_eq!(res, "../../working/directory"); + + let res = shorten( + path, + Some( + &default() + .with_base(base) + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(res, "../../working/directory"); + } + + #[test] + fn test_shorten_pwd() { + let path = "/present/working/directory"; + + let res = shorten(path, Some(&default().with_base(path))).unwrap(); + assert_eq!(res, "."); + + let res = shorten( + path, + Some( + &default() + .with_base(path) + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(res, "../directory"); + } + + #[test] + fn test_shorten_parent() { + let path = "/present/working"; + let base = "/present/working/directory"; + + let res = shorten(path, Some(&default().with_base(base))).unwrap(); + assert_eq!(res, ".."); + + let res = shorten( + path, + Some( + &default() + .with_base(base) + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(res, "../../working"); + } + + #[test] + fn test_shorten_root() { + let res = shorten("/", Some(&default().with_base("/"))).unwrap(); + assert_eq!(res, "/"); + + let res = shorten( + "/", + Some( + &default() + .with_base("/") + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(res, "/"); + + let res = shorten("/foo", Some(&default().with_base("/"))).unwrap(); + assert_eq!(res, "foo"); + + let res = shorten( + "/foo", + Some( + &default() + .with_base("/") + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(res, "/foo"); + + let res = shorten( + "/", + Some( + &default() + .with_base("/foo") + .with_prefix_dots() + .without_suffix_dots(), + ), + ) + .unwrap(); + assert_eq!(res, "/"); + } + + #[test] + fn test_path_escape() { + let text = "foo".to_string(); + assert_eq!(escape(&text), "foo"); + + let text = "foo bar".to_string(); + assert_eq!(escape(&text), "'foo bar'"); + + let text = "foo\nbar".to_string(); + assert_eq!(escape(&text), "\"foo\\nbar\""); + + let text = "foo$bar".to_string(); + assert_eq!(escape(&text), "'foo$bar'"); + + let text = "foo'$\n'bar".to_string(); + assert_eq!(escape(&text), "\"foo'\\$\\n'bar\""); + + let text = "a'b\"c".to_string(); + assert_eq!(escape(&text), "\"a'b\\\"c\""); + } +} diff --git a/src/permissions.rs b/src/permissions.rs index 7f41c6e..940b1c5 100644 --- a/src/permissions.rs +++ b/src/permissions.rs @@ -1,27 +1,52 @@ // Stolen from https://github.com/Peltoche/lsd/blob/master/src/meta/permissions.rs use serde::{Deserialize, Serialize}; -use std::fs::Metadata; +use std::{fmt::Display, fs::Metadata}; -#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Hash, Default)] +pub type RWX = (char, char, char, char, char, char, char, char, char); +pub type Octal = (u8, u8, u8, u8); + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Hash)] pub struct Permissions { + #[serde(default)] pub user_read: bool, + + #[serde(default)] pub user_write: bool, + + #[serde(default)] pub user_execute: bool, + #[serde(default)] pub group_read: bool, + + #[serde(default)] pub group_write: bool, + + #[serde(default)] pub group_execute: bool, + #[serde(default)] pub other_read: bool, + + #[serde(default)] pub other_write: bool, + + #[serde(default)] pub other_execute: bool, + #[serde(default)] pub sticky: bool, + + #[serde(default)] pub setgid: bool, + + #[serde(default)] pub setuid: bool, } +impl Permissions {} + impl<'a> From<&'a Metadata> for Permissions { #[cfg(unix)] fn from(meta: &Metadata) -> Self { @@ -55,6 +80,68 @@ impl<'a> From<&'a Metadata> for Permissions { } } +impl Display for Permissions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (ur, uw, ux, gr, gw, gx, or, ow, ox) = (*self).into(); + write!(f, "{ur}{uw}{ux}{gr}{gw}{gx}{or}{ow}{ox}") + } +} + +impl Into for Permissions { + fn into(self) -> RWX { + let bit = |bit: bool, chr: char| { + if bit { + chr + } else { + '-' + } + }; + + let ur = bit(self.user_read, 'r'); + let uw = bit(self.user_write, 'w'); + let ux = match (self.user_execute, self.setuid) { + (true, true) => 's', + (true, false) => 'x', + (false, true) => 'S', + (false, false) => '-', + }; + + let gr = bit(self.group_read, 'r'); + let gw = bit(self.group_write, 'w'); + let gx = match (self.group_execute, self.setgid) { + (true, true) => 's', + (true, false) => 'x', + (false, true) => 'S', + (false, false) => '-', + }; + + let or = bit(self.other_read, 'r'); + let ow = bit(self.other_write, 'w'); + let ox = match (self.other_execute, self.sticky) { + (true, true) => 't', + (true, false) => 'x', + (false, true) => 'T', + (false, false) => '-', + }; + + (ur, uw, ux, gr, gw, gx, or, ow, ox) + } +} + +impl Into for Permissions { + fn into(self) -> Octal { + let bits_to_octal = + |r: bool, w: bool, x: bool| (r as u8) * 4 + (w as u8) * 2 + (x as u8); + + ( + bits_to_octal(self.setuid, self.setgid, self.sticky), + bits_to_octal(self.user_read, self.user_write, self.user_execute), + bits_to_octal(self.group_read, self.group_write, self.group_execute), + bits_to_octal(self.other_read, self.other_write, self.other_execute), + ) + } +} + // More readable aliases for the permission bits exposed by libc. #[allow(trivial_numeric_casts)] #[cfg(unix)] diff --git a/src/runner.rs b/src/runner.rs index 940b1d1..8993b44 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -31,7 +31,7 @@ pub fn get_tty() -> Result { match fs::OpenOptions::new().read(true).write(true).open(tty) { Ok(f) => Ok(f), Err(e) => { - bail!(format!("Failed to open {}. {}", tty, e)) + bail!(format!("could not open {tty}. {e}")) } } } @@ -140,7 +140,7 @@ fn call( if s.success() { Ok(()) } else { - Err(format!("process exited with code {}", &s)) + Err(format!("process exited with code {s}")) } }) .unwrap_or_else(|e| Err(e.to_string())); @@ -184,7 +184,7 @@ fn call( fn start_fifo(path: &str, focus_path: &str) -> Result { match fs::OpenOptions::new().write(true).open(path) { Ok(mut file) => { - writeln!(file, "{}", focus_path)?; + writeln!(file, "{focus_path}")?; Ok(file) } Err(e) => Err(e.into()), @@ -297,12 +297,13 @@ impl Runner { execute!(stdout, term::EnterAlternateScreen)?; let mut fifo: Option = - if let Some(path) = app.config.general.start_fifo.as_ref() { + if let Some(path) = app.config.general.start_fifo.clone() { // TODO remove duplicate segment - match start_fifo(path, &app.focused_node_str()) { + match start_fifo(&path, &app.focused_node_str()) { Ok(file) => Some(file), Err(e) => { - app = app.log_error(e.to_string())?; + app = app + .log_error(format!("could not start fifo {path:?}: {e}"))?; None } } @@ -316,7 +317,7 @@ impl Runner { let mut mouse_enabled = app.config.general.enable_mouse; if mouse_enabled { if let Err(e) = execute!(stdout, event::EnableMouseCapture) { - app = app.log_error(e.to_string())?; + app = app.log_error(format!("could not enable mouse: {e}"))?; } } @@ -508,7 +509,9 @@ impl Runner { mouse_enabled = true; } Err(e) => { - app = app.log_error(e.to_string())?; + app = app.log_error(format!( + "could not enable mouse: {e}" + ))?; } } } @@ -536,7 +539,9 @@ impl Runner { mouse_enabled = false; } Err(e) => { - app = app.log_error(e.to_string())?; + app = app.log_error(format!( + "could not disable mouse: {e}" + ))?; } } } @@ -546,7 +551,9 @@ impl Runner { fifo = match start_fifo(&path, &app.focused_node_str()) { Ok(file) => Some(file), Err(e) => { - app = app.log_error(e.to_string())?; + app = app.log_error(format!( + "could not start fifo {path:?}: {e}" + ))?; None } } @@ -569,7 +576,9 @@ impl Runner { { Ok(file) => Some(file), Err(e) => { - app = app.log_error(e.to_string())?; + app = app.log_error(format!( + "could not toggle fifo {path:?}: {e}" + ))?; None } } diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..14b6adb --- /dev/null +++ b/src/search.rs @@ -0,0 +1,50 @@ +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory}; +use skim::{MatchEngine, MatchEngineFactory, SkimItem}; + +lazy_static! { + static ref FUZZY_FACTORY: ExactOrFuzzyEngineFactory = + ExactOrFuzzyEngineFactory::builder().build(); + static ref REGEX_FACTORY: RegexEngineFactory = RegexEngineFactory::builder().build(); +} + +pub struct PathItem { + path: String, +} + +impl From for PathItem { + fn from(value: String) -> Self { + Self { path: value } + } +} + +impl SkimItem for PathItem { + fn text(&self) -> std::borrow::Cow { + std::borrow::Cow::from(&self.path) + } +} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum SearchAlgorithm { + #[default] + Fuzzy, + Regex, +} + +impl SearchAlgorithm { + pub fn toggle(self) -> Self { + match self { + Self::Fuzzy => Self::Regex, + Self::Regex => Self::Fuzzy, + } + } + + pub fn engine(&self, pattern: &str) -> Box { + match self { + Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern), + Self::Regex => REGEX_FACTORY.create_engine(pattern), + } + } +} diff --git a/src/ui.rs b/src/ui.rs index 516c488..1df43b7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,18 +1,21 @@ -use crate::app; use crate::app::{HelpMenuLine, NodeFilterApplicable, NodeSorterApplicable}; use crate::app::{Node, ResolvedNode}; +use crate::compat::{draw_custom_content, CustomContent}; use crate::config::PanelUiConfig; use crate::lua; use crate::permissions::Permissions; -use ansi_to_tui::IntoText; +use crate::{app, path}; +use ansi_to_tui_forked::IntoText; use indexmap::IndexSet; use lazy_static::lazy_static; +use lscolors::{Color as LsColorsColor, Style as LsColorsStyle}; use mlua::Lua; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; use std::env; use std::ops::BitXor; +use time::macros::format_description; use tui::backend::Backend; use tui::layout::Rect as TuiRect; use tui::layout::{Constraint as TuiConstraint, Direction, Layout as TuiLayout}; @@ -37,14 +40,14 @@ fn read_only_indicator(app: &app::App) -> &str { } } -fn string_to_text<'a>(string: String) -> Text<'a> { +pub fn string_to_text<'a>(string: String) -> Text<'a> { if *NO_COLOR { Text::raw(string) } else { string .as_bytes() .into_text() - .unwrap_or_else(|e| Text::raw(format!("{:?}", e))) + .unwrap_or_else(|e| Text::raw(format!("{e:?}"))) } } @@ -76,31 +79,23 @@ impl LayoutOptions { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] -pub enum ContentBody { - /// A paragraph to render - StaticParagraph { render: String }, - - /// A Lua function that returns a paragraph to render - DynamicParagraph { render: String }, - - /// List to render - StaticList { render: Vec }, - - /// A Lua function that returns lines to render - DynamicList { render: String }, - - /// A table to render - StaticTable { - widths: Vec, - col_spacing: Option, - render: Vec>, +pub enum CustomPanel { + CustomParagraph { + #[serde(default)] + ui: PanelUiConfig, + body: String, }, - - /// A Lua function that returns a table to render - DynamicTable { + CustomList { + #[serde(default)] + ui: PanelUiConfig, + body: Vec, + }, + CustomTable { + #[serde(default)] + ui: PanelUiConfig, widths: Vec, col_spacing: Option, - render: String, + body: Vec>, }, } @@ -113,10 +108,8 @@ pub enum Layout { Selection, HelpMenu, SortAndFilter, - CustomContent { - title: Option, - body: ContentBody, - }, + Static(Box), + Dynamic(String), Horizontal { config: LayoutOptions, splits: Vec, @@ -125,6 +118,9 @@ pub enum Layout { config: LayoutOptions, splits: Vec, }, + + /// For compatibility only. A better choice is Static or Dymanic layout. + CustomContent(Box), } impl Default for Layout { @@ -167,6 +163,32 @@ impl Layout { (_, other) => other.to_owned(), } } + + pub fn replace(self, target: &Self, replacement: &Self) -> Self { + match self { + Self::Horizontal { splits, config } => Self::Horizontal { + splits: splits + .into_iter() + .map(|s| s.replace(target, replacement)) + .collect(), + config, + }, + Self::Vertical { splits, config } => Self::Vertical { + splits: splits + .into_iter() + .map(|s| s.replace(target, replacement)) + .collect(), + config, + }, + other => { + if other == *target { + replacement.to_owned() + } else { + other + } + } + } + } } #[derive( @@ -181,7 +203,7 @@ pub enum Border { } impl Border { - pub fn bits(self) -> u32 { + pub fn bits(self) -> u8 { match self { Self::Top => TuiBorders::TOP.bits(), Self::Right => TuiBorders::RIGHT.bits(), @@ -236,7 +258,7 @@ pub enum Modifier { } impl Modifier { - pub fn bits(self) -> u16 { + pub fn bits(self) -> u8 { match self { Self::Bold => TuiModifier::BOLD.bits(), Self::Dim => TuiModifier::DIM.bits(), @@ -251,6 +273,21 @@ impl Modifier { } } +fn extend_optional_modifiers( + a: Option>, + b: Option>, +) -> Option> { + match (a, b) { + (Some(mut a), Some(b)) => { + a.extend(b); + Some(a) + } + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + (None, None) => None, + } +} + #[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Style { @@ -264,15 +301,21 @@ impl Style { pub fn extend(mut self, other: &Self) -> Self { self.fg = other.fg.or(self.fg); self.bg = other.bg.or(self.bg); - self.add_modifiers = other.add_modifiers.to_owned().or(self.add_modifiers); - self.sub_modifiers = other.sub_modifiers.to_owned().or(self.sub_modifiers); + self.add_modifiers = extend_optional_modifiers( + self.add_modifiers, + other.add_modifiers.to_owned(), + ); + self.sub_modifiers = extend_optional_modifiers( + self.sub_modifiers, + other.sub_modifiers.to_owned(), + ); self } } impl Into for Style { fn into(self) -> TuiStyle { - fn xor(modifiers: Option>) -> u16 { + fn xor(modifiers: Option>) -> u8 { modifiers .unwrap_or_default() .into_iter() @@ -292,6 +335,115 @@ impl Into for Style { } } +impl From<&LsColorsStyle> for Style { + fn from(style: &LsColorsStyle) -> Self { + fn convert_color(color: &LsColorsColor) -> Color { + match color { + LsColorsColor::Black => Color::Black, + LsColorsColor::Red => Color::Red, + LsColorsColor::Green => Color::Green, + LsColorsColor::Yellow => Color::Yellow, + LsColorsColor::Blue => Color::Blue, + LsColorsColor::Magenta => Color::Magenta, + LsColorsColor::Cyan => Color::Cyan, + LsColorsColor::White => Color::Gray, + LsColorsColor::BrightBlack => Color::DarkGray, + LsColorsColor::BrightRed => Color::LightRed, + LsColorsColor::BrightGreen => Color::LightGreen, + LsColorsColor::BrightYellow => Color::LightYellow, + LsColorsColor::BrightBlue => Color::LightBlue, + LsColorsColor::BrightMagenta => Color::LightMagenta, + LsColorsColor::BrightCyan => Color::LightCyan, + LsColorsColor::BrightWhite => Color::White, + LsColorsColor::Fixed(index) => Color::Indexed(*index), + LsColorsColor::RGB(r, g, b) => Color::Rgb(*r, *g, *b), + } + } + Self { + fg: style.foreground.as_ref().map(convert_color), + bg: style.background.as_ref().map(convert_color), + add_modifiers: None, + sub_modifiers: None, + } + } +} + +impl Into for Style { + fn into(self) -> nu_ansi_term::Style { + fn convert_color(color: Color) -> Option { + match color { + Color::Black => Some(nu_ansi_term::Color::Black), + Color::Red => Some(nu_ansi_term::Color::Red), + Color::Green => Some(nu_ansi_term::Color::Green), + Color::Yellow => Some(nu_ansi_term::Color::Yellow), + Color::Blue => Some(nu_ansi_term::Color::Blue), + Color::Magenta => Some(nu_ansi_term::Color::Purple), + Color::Cyan => Some(nu_ansi_term::Color::Cyan), + Color::Gray => Some(nu_ansi_term::Color::LightGray), + Color::DarkGray => Some(nu_ansi_term::Color::DarkGray), + Color::LightRed => Some(nu_ansi_term::Color::LightRed), + Color::LightGreen => Some(nu_ansi_term::Color::LightGreen), + Color::LightYellow => Some(nu_ansi_term::Color::LightYellow), + Color::LightBlue => Some(nu_ansi_term::Color::LightBlue), + Color::LightMagenta => Some(nu_ansi_term::Color::LightMagenta), + Color::LightCyan => Some(nu_ansi_term::Color::LightCyan), + Color::White => Some(nu_ansi_term::Color::White), + Color::Rgb(r, g, b) => Some(nu_ansi_term::Color::Rgb(r, g, b)), + Color::Indexed(index) => Some(nu_ansi_term::Color::Fixed(index)), + _ => None, + } + } + fn match_modifiers(style: &Style, f: F) -> bool + where + F: Fn(&IndexSet) -> bool, + { + style.add_modifiers.as_ref().map_or(false, f) + } + + nu_ansi_term::Style { + foreground: self.fg.and_then(convert_color), + background: self.bg.and_then(convert_color), + is_bold: match_modifiers(&self, |m| m.contains(&Modifier::Bold)), + is_dimmed: match_modifiers(&self, |m| m.contains(&Modifier::Dim)), + is_italic: match_modifiers(&self, |m| m.contains(&Modifier::Italic)), + is_underline: match_modifiers(&self, |m| m.contains(&Modifier::Underlined)), + is_blink: match_modifiers(&self, |m| { + m.contains(&Modifier::SlowBlink) || m.contains(&Modifier::RapidBlink) + }), + is_reverse: match_modifiers(&self, |m| m.contains(&Modifier::Reversed)), + is_hidden: match_modifiers(&self, |m| m.contains(&Modifier::Hidden)), + is_strikethrough: match_modifiers(&self, |m| { + m.contains(&Modifier::CrossedOut) + }), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WrapOptions { + pub width: usize, + pub initial_indent: Option, + pub subsequent_indent: Option, + pub break_words: Option, +} + +impl WrapOptions { + pub fn get_options(&self) -> textwrap::Options<'_> { + let mut options = textwrap::Options::new(self.width); + if let Some(initial_indent) = &self.initial_indent { + options = options.initial_indent(initial_indent); + } + if let Some(subsequent_indent) = &self.subsequent_indent { + options = options.subsequent_indent(subsequent_indent); + } + if let Some(break_words) = self.break_words { + options = options.break_words(break_words); + } + options + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub enum Constraint { @@ -428,6 +580,7 @@ pub struct NodeUiMetadata { pub is_focused: bool, pub total: usize, pub meta: HashMap, + pub style: Style, } impl NodeUiMetadata { @@ -444,6 +597,7 @@ impl NodeUiMetadata { is_focused: bool, total: usize, meta: HashMap, + style: Style, ) -> Self { Self { parent: node.parent.to_owned(), @@ -476,11 +630,12 @@ impl NodeUiMetadata { is_focused, total, meta, + style, } } } -fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> { +pub fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> { Block::default() .borders(TuiBorders::from_bits_truncate( config @@ -550,40 +705,7 @@ fn draw_table( }) .unwrap_or_default(); - let mut me = node.mime_essence.splitn(2, '/'); - let mimetype: String = - me.next().map(|s| s.into()).unwrap_or_default(); - let mimesub: String = - me.next().map(|s| s.into()).unwrap_or_default(); - - let mut node_type = if node.is_symlink { - app_config.node_types.symlink.to_owned() - } else if node.is_dir { - app_config.node_types.directory.to_owned() - } else { - app_config.node_types.file.to_owned() - }; - - if let Some(conf) = app_config - .node_types - .mime_essence - .get(&mimetype) - .and_then(|t| t.get(&mimesub).or_else(|| t.get("*"))) - { - node_type = node_type.extend(conf); - } - - if let Some(conf) = - app_config.node_types.extension.get(&node.extension) - { - node_type = node_type.extend(conf); - } - - if let Some(conf) = - app_config.node_types.special.get(&node.relative_path) - { - node_type = node_type.extend(conf); - } + let node_type = app_config.node_types.get(node); let (relative_index, is_before_focus, is_after_focus) = match dir.focus.cmp(&index) { @@ -627,6 +749,7 @@ fn draw_table( is_focused, dir.total, node_type.meta, + style, ); let cols = lua::serialize::(lua, &meta) @@ -642,7 +765,7 @@ fn draw_table( .filter_map(|c| { c.format.as_ref().map(|f| { let out = lua::call(lua, f, v.clone()) - .unwrap_or_else(|e| e.to_string()); + .unwrap_or_else(|e| format!("{e:?}")); string_to_text(out) }) }) @@ -653,7 +776,7 @@ fn draw_table( .map(|x| Cell::from(x.to_owned())) .collect::>(); - Row::new(cols).style(style.into()) + Row::new(cols) }) .collect::>() }) @@ -674,9 +797,9 @@ fn draw_table( } else { &app.pwd } - .trim_matches('/') - .replace('\\', "\\\\") - .replace('\n', "\\n"); + .trim_matches('/'); + + let pwd = path::escape(pwd); let vroot_indicator = if app.vroot.is_some() { "vroot:" } else { "" }; @@ -722,7 +845,7 @@ fn draw_selection( _screen_size: TuiRect, layout_size: TuiRect, app: &app::App, - _: &Lua, + lua: &Lua, ) { let panel_config = &app.config.general.panel_ui; let config = panel_config @@ -738,7 +861,22 @@ fn draw_selection( .rev() .take((layout_size.height.max(2) - 2).into()) .rev() - .map(|n| n.absolute_path.replace('\\', "\\\\").replace('\n', "\\n")) + .map(|n| { + let out = app + .config + .general + .selection + .item + .format + .as_ref() + .map(|f| { + lua::serialize::(lua, n) + .and_then(|n| lua::call(lua, f, n)) + .unwrap_or_else(|e| format!("{e:?}")) + }) + .unwrap_or_else(|| n.absolute_path.clone()); + string_to_text(out) + }) .map(ListItem::new) .collect(); @@ -874,11 +1012,7 @@ fn draw_sort_n_filter( let ui = app.config.general.sort_and_filter_ui.to_owned(); let filter_by: &IndexSet = &app.explorer_config.filters; let sort_by: &IndexSet = &app.explorer_config.sorters; - let search = app - .explorer_config - .searcher - .as_ref() - .map(|s| s.pattern.clone()); + let search = app.explorer_config.searcher.as_ref(); let defaultui = &ui.default_identifier; let forwardui = defaultui @@ -888,6 +1022,15 @@ fn draw_sort_n_filter( .to_owned() .extend(&ui.sort_direction_identifiers.reverse); + let orderedui = defaultui + .to_owned() + .extend(&ui.search_direction_identifiers.ordered); + let unorderedui = defaultui + .to_owned() + .extend(&ui.search_direction_identifiers.unordered); + + let is_ordered_search = search.as_ref().map(|s| !s.unordered).unwrap_or(false); + let mut spans = filter_by .iter() .map(|f| { @@ -905,12 +1048,36 @@ fn draw_sort_n_filter( }) .unwrap_or((Span::raw("f"), Span::raw(""))) }) + .chain(search.iter().map(|s| { + ui.search_identifiers + .get(&s.algorithm) + .map(|u| { + let direction = if s.unordered { + &unorderedui + } else { + &orderedui + }; + let ui = defaultui.to_owned().extend(u); + let f = ui + .format + .as_ref() + .map(|f| format!("{f}{p}", p = &s.pattern)) + .unwrap_or_else(|| s.pattern.clone()); + ( + Span::styled(f, ui.style.into()), + Span::styled( + direction.format.to_owned().unwrap_or_default(), + direction.style.to_owned().into(), + ), + ) + }) + .unwrap_or((Span::raw("/"), Span::raw(&s.pattern))) + })) .chain( sort_by .iter() .map(|s| { let direction = if s.reverse { &reverseui } else { &forwardui }; - ui.sorter_identifiers .get(&s.sorter) .map(|u| { @@ -928,23 +1095,8 @@ fn draw_sort_n_filter( }) .unwrap_or((Span::raw("s"), Span::raw(""))) }) - .take(if search.is_some() { 0 } else { sort_by.len() }), + .take(if !is_ordered_search { sort_by.len() } else { 0 }), ) - .chain(search.iter().map(|s| { - ui.search_identifier - .as_ref() - .map(|u| { - let ui = defaultui.to_owned().extend(u); - ( - Span::styled( - ui.format.to_owned().unwrap_or_default(), - ui.style.to_owned().into(), - ), - Span::styled(s, ui.style.into()), - ) - }) - .unwrap_or((Span::raw("/"), Span::raw(s))) - })) .zip(std::iter::repeat(Span::styled( ui.separator.format.to_owned().unwrap_or_default(), ui.separator.style.to_owned().into(), @@ -988,7 +1140,8 @@ fn draw_logs( .rev() .take(layout_size.height as usize) .map(|log| { - let time = log.created_at.format("%r"); + let fd = format_description!("[hour]:[minute]:[second]"); + let time = log.created_at.format(fd).unwrap_or_else(|_| "when?".into()); let cfg = match log.level { app::LogLevel::Info => &logs_config.info, app::LogLevel::Warning => &logs_config.warning, @@ -997,7 +1150,7 @@ fn draw_logs( }; let prefix = - format!("{}|{}", time, cfg.format.to_owned().unwrap_or_default()); + format!("{time}|{0}", cfg.format.to_owned().unwrap_or_default()); let padding = " ".repeat(prefix.chars().count()); @@ -1007,9 +1160,9 @@ fn draw_logs( .enumerate() .map(|(i, line)| { if i == 0 { - format!("{}: {}", &prefix, line) + format!("{prefix}) {line}") } else { - format!("{} {}", &padding, line) + format!("{padding} {line}") } }) .take(layout_size.height as usize) @@ -1054,94 +1207,68 @@ pub fn draw_nothing( f.render_widget(nothing, layout_size); } -pub fn draw_custom_content( +pub fn draw_dynamic( f: &mut Frame, screen_size: TuiRect, layout_size: TuiRect, app: &app::App, - title: Option, - body: ContentBody, + func: &str, lua: &Lua, ) { - let config = app.config.general.panel_ui.default.clone(); - - match body { - ContentBody::StaticParagraph { render } => { - let render = string_to_text(render); - let content = Paragraph::new(render).block(block( - config, - title.map(|t| format!(" {} ", t)).unwrap_or_default(), - )); - f.render_widget(content, layout_size); - } - - ContentBody::DynamicParagraph { render } => { - let ctx = ContentRendererArg { - app: app.to_lua_ctx_light(), - layout_size: layout_size.into(), - screen_size: screen_size.into(), - }; - - let render = lua::serialize(lua, &ctx) - .map(|arg| { - lua::call(lua, &render, arg).unwrap_or_else(|e| format!("{:?}", e)) - }) - .unwrap_or_else(|e| e.to_string()); - - let render = string_to_text(render); + let ctx = ContentRendererArg { + app: app.to_lua_ctx_light(), + layout_size: layout_size.into(), + screen_size: screen_size.into(), + }; - let content = Paragraph::new(render).block(block( - config, - title.map(|t| format!(" {} ", t)).unwrap_or_default(), - )); - f.render_widget(content, layout_size); - } + let panel: CustomPanel = lua::serialize(lua, &ctx) + .and_then(|arg| lua::call(lua, func, arg)) + .unwrap_or_else(|e| CustomPanel::CustomParagraph { + ui: app.config.general.panel_ui.default.clone(), + body: format!("{e:?}"), + }); - ContentBody::StaticList { render } => { - let items = render - .into_iter() - .map(string_to_text) - .map(ListItem::new) - .collect::>(); + draw_static(f, screen_size, layout_size, app, panel, lua); +} - let content = List::new(items).block(block( - config, - title.map(|t| format!(" {} ", t)).unwrap_or_default(), - )); +pub fn draw_static( + f: &mut Frame, + screen_size: TuiRect, + layout_size: TuiRect, + app: &app::App, + panel: CustomPanel, + _lua: &Lua, +) { + let defaultui = app.config.general.panel_ui.default.clone(); + match panel { + CustomPanel::CustomParagraph { ui, body } => { + let config = defaultui.extend(&ui); + let body = string_to_text(body); + let content = Paragraph::new(body).block(block(config, "".into())); f.render_widget(content, layout_size); } - ContentBody::DynamicList { render } => { - let ctx = ContentRendererArg { - app: app.to_lua_ctx_light(), - layout_size: layout_size.into(), - screen_size: screen_size.into(), - }; + CustomPanel::CustomList { ui, body } => { + let config = defaultui.extend(&ui); - let items = lua::serialize(lua, &ctx) - .map(|arg| { - lua::call(lua, &render, arg) - .unwrap_or_else(|e| vec![format!("{:?}", e)]) - }) - .unwrap_or_else(|e| vec![e.to_string()]) + let items = body .into_iter() .map(string_to_text) .map(ListItem::new) .collect::>(); - let content = List::new(items).block(block( - config, - title.map(|t| format!(" {} ", t)).unwrap_or_default(), - )); + let content = List::new(items).block(block(config, "".into())); f.render_widget(content, layout_size); } - ContentBody::StaticTable { + CustomPanel::CustomTable { + ui, widths, col_spacing, - render, + body, } => { - let rows = render + let config = defaultui.extend(&ui); + let rows = body .into_iter() .map(|cols| { Row::new( @@ -1161,55 +1288,7 @@ pub fn draw_custom_content( let content = Table::new(rows) .widths(&widths) .column_spacing(col_spacing.unwrap_or(1)) - .block(block( - config, - title.map(|t| format!(" {} ", t)).unwrap_or_default(), - )); - - f.render_widget(content, layout_size); - } - - ContentBody::DynamicTable { - widths, - col_spacing, - render, - } => { - let ctx = ContentRendererArg { - app: app.to_lua_ctx_light(), - layout_size: layout_size.into(), - screen_size: screen_size.into(), - }; - - let rows = lua::serialize(lua, &ctx) - .map(|arg| { - lua::call(lua, &render, arg) - .unwrap_or_else(|e| vec![vec![format!("{:?}", e)]]) - }) - .unwrap_or_else(|e| vec![vec![e.to_string()]]) - .into_iter() - .map(|cols| { - Row::new( - cols.into_iter() - .map(string_to_text) - .map(Cell::from) - .collect::>(), - ) - }) - .collect::>(); - - let widths = widths - .into_iter() - .map(|w| w.to_tui(screen_size, layout_size)) - .collect::>(); - - let mut content = Table::new(rows).widths(&widths).block(block( - config, - title.map(|t| format!(" {} ", t)).unwrap_or_default(), - )); - - if let Some(col_spacing) = col_spacing { - content = content.column_spacing(col_spacing); - }; + .block(block(config, "".into())); f.render_widget(content, layout_size); } @@ -1237,9 +1316,9 @@ impl From for Rect { #[derive(Debug, Clone, Serialize)] pub struct ContentRendererArg { - app: app::LuaContextLight, - screen_size: Rect, - layout_size: Rect, + pub app: app::LuaContextLight, + pub screen_size: Rect, + pub layout_size: Rect, } pub fn draw_layout( @@ -1265,8 +1344,14 @@ pub fn draw_layout( draw_logs(f, screen_size, layout_size, app, lua); }; } - Layout::CustomContent { title, body } => { - draw_custom_content(f, screen_size, layout_size, app, title, body, lua) + Layout::Static(panel) => { + draw_static(f, screen_size, layout_size, app, *panel, lua) + } + Layout::Dynamic(ref func) => { + draw_dynamic(f, screen_size, layout_size, app, func, lua) + } + Layout::CustomContent(content) => { + draw_custom_content(f, screen_size, layout_size, app, *content, lua) } Layout::Horizontal { config, splits } => { let chunks = TuiLayout::default() @@ -1293,9 +1378,9 @@ pub fn draw_layout( splits .into_iter() - .zip(chunks.into_iter()) + .zip(chunks.iter()) .for_each(|(split, chunk)| { - draw_layout(split, f, screen_size, chunk, app, lua) + draw_layout(split, f, screen_size, *chunk, app, lua) }); } @@ -1324,9 +1409,9 @@ pub fn draw_layout( splits .into_iter() - .zip(chunks.into_iter()) + .zip(chunks.iter()) .for_each(|(split, chunk)| { - draw_layout(split, f, screen_size, chunk, app, lua) + draw_layout(split, f, screen_size, *chunk, app, lua) }); } } @@ -1384,7 +1469,7 @@ mod tests { ); assert_eq!( - b.to_owned().extend(&a), + b.extend(&a), Style { fg: Some(Color::Red), bg: Some(Color::Blue), @@ -1398,19 +1483,71 @@ mod tests { Style { fg: Some(Color::Cyan), bg: Some(Color::Magenta), - add_modifiers: modifier(Modifier::CrossedOut), + add_modifiers: Some( + vec![Modifier::Bold, Modifier::CrossedOut] + .into_iter() + .collect() + ), sub_modifiers: modifier(Modifier::Italic), } ); assert_eq!( - c.to_owned().extend(&a), + c.extend(&a), Style { fg: Some(Color::Red), bg: Some(Color::Magenta), - add_modifiers: modifier(Modifier::Bold), + add_modifiers: Some( + vec![Modifier::Bold, Modifier::CrossedOut] + .into_iter() + .collect() + ), sub_modifiers: modifier(Modifier::Italic), } ); } + + #[test] + fn test_layout_replace() { + let layout = Layout::Horizontal { + config: LayoutOptions { + margin: Some(2), + horizontal_margin: Some(3), + vertical_margin: Some(4), + constraints: Some(vec![ + Constraint::Percentage(80), + Constraint::Percentage(20), + ]), + }, + splits: vec![Layout::Table, Layout::HelpMenu], + }; + + let res = layout.clone().replace(&Layout::Table, &Layout::Selection); + + match (res, layout) { + ( + Layout::Horizontal { + config: res_config, + splits: res_splits, + }, + Layout::Horizontal { + config: layout_config, + splits: layout_splits, + }, + ) => { + assert_eq!(res_config, layout_config); + assert_eq!(res_splits.len(), layout_splits.len()); + assert_eq!(res_splits[0], Layout::Selection); + assert_ne!(res_splits[0], layout_splits[0]); + assert_eq!(res_splits[1], layout_splits[1]); + } + _ => panic!("Unexpected layout"), + } + + let res = Layout::Table.replace(&Layout::Table, &Layout::Selection); + assert_eq!(res, Layout::Selection); + + let res = Layout::Table.replace(&Layout::Nothing, &Layout::Selection); + assert_eq!(res, Layout::Table); + } }