mirror of
https://github.com/sayanarijit/xplr.git
synced 2024-10-26 10:56:25 +03:00
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 <dev@noahmayr.com> * 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 <sayanarijit@users.noreply.github.com> * Update wrap to return lines instead * Fix doc * Rename wrap -> text wrap Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com> Co-authored-by: Arijit Basu <sayanarijit@gmail.com> * 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 <sayanarijit@gmail.com> * 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 <sayanarijit@gmail.com> * 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 <dev@noahmayr.com>
This commit is contained in:
parent
59279b816d
commit
e0d683b13a
852
Cargo.lock
generated
852
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
42
Cargo.toml
42
Cargo.toml
@ -8,7 +8,7 @@ path = './benches/criterion.rs'
|
||||
|
||||
[package]
|
||||
name = 'xplr'
|
||||
version = '0.20.2'
|
||||
version = '0.21.0'
|
||||
authors = ['Arijit Basu <hi@arijitbasu.in>']
|
||||
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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 |
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
- [title][33]
|
||||
- [body][34]
|
||||
Type: { Static = [Custom Panel][27] }
|
||||
|
||||
Example: { CustomContent = { title = [title][33], body = [body][34] }
|
||||
### Dynamic
|
||||
|
||||
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].
|
||||
|
||||
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
|
||||
## Custom Panel
|
||||
|
||||
Type: nullable string
|
||||
Custom panel can be one of the following:
|
||||
|
||||
The title of the panel.
|
||||
- [CustomParagraph][29]
|
||||
- [CustomList][30]
|
||||
- [CustomTable][31]
|
||||
|
||||
## body
|
||||
|
||||
Type: [Content Body][26]
|
||||
|
||||
The body of the panel.
|
||||
|
||||
## 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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
77
docs/en/src/searching.md
Normal file
77
docs/en/src/searching.md
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
368
src/app.rs
368
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<Local>,
|
||||
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<Key>) -> Result<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
@ -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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
pub fn search(self, pattern: String) -> Result<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
fn search_from_input_with(
|
||||
self,
|
||||
algorithm: SearchAlgorithm,
|
||||
unordered: bool,
|
||||
) -> Result<Self> {
|
||||
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> {
|
||||
self.explorer_config.searcher = self
|
||||
.explorer_config
|
||||
.searcher
|
||||
.map(|s| s.enable_search_order());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn disable_search_order(mut self) -> Result<Self> {
|
||||
self.explorer_config.searcher = self
|
||||
.explorer_config
|
||||
.searcher
|
||||
.map(|s| s.disable_search_order());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn toggle_search_order(mut self) -> Result<Self> {
|
||||
self.explorer_config.searcher = self
|
||||
.explorer_config
|
||||
.searcher
|
||||
.map(|s| s.toggle_search_order());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn toggle_search_algorithm(mut self) -> Result<Self> {
|
||||
self.explorer_config.searcher =
|
||||
self.explorer_config.searcher.map(|s| s.toggle_algorithm());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn accept_search(mut self) -> Result<Self> {
|
||||
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<Self> {
|
||||
// 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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
@ -1761,7 +1993,7 @@ impl App {
|
||||
self.history
|
||||
.paths
|
||||
.iter()
|
||||
.map(|p| format!("{}{}", &p, delimiter))
|
||||
.map(|p| format!("{p}{delimiter}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
||||
|
@ -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);
|
||||
|
25
src/cli.rs
25
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<String>) -> 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<String>) -> Result<()> {
|
||||
|
||||
pub fn print_msg_in(args: Vec<String>) -> Result<()> {
|
||||
let msg = fmt_msg_in(args)?;
|
||||
print!("{}", msg);
|
||||
print!("{msg}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -220,24 +220,21 @@ fn fmt_msg_in(args: Vec<String>) -> Result<String> {
|
||||
}
|
||||
('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, _) => {
|
||||
|
224
src/compat.rs
Normal file
224
src/compat.rs
Normal file
@ -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<String> },
|
||||
|
||||
/// A Lua function that returns lines to render
|
||||
DynamicList { render: String },
|
||||
|
||||
/// A table to render
|
||||
StaticTable {
|
||||
widths: Vec<Constraint>,
|
||||
col_spacing: Option<u16>,
|
||||
render: Vec<Vec<String>>,
|
||||
},
|
||||
|
||||
/// A Lua function that returns a table to render
|
||||
DynamicTable {
|
||||
widths: Vec<Constraint>,
|
||||
col_spacing: Option<u16>,
|
||||
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<String>,
|
||||
pub body: ContentBody,
|
||||
}
|
||||
|
||||
/// A cursed function from crate::ui.
|
||||
pub fn draw_custom_content<B: Backend>(
|
||||
f: &mut Frame<B>,
|
||||
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::<Vec<ListItem>>();
|
||||
|
||||
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::<Vec<ListItem>>();
|
||||
|
||||
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::<Vec<Cell>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Row>>();
|
||||
|
||||
let widths = widths
|
||||
.into_iter()
|
||||
.map(|w| w.to_tui(screen_size, layout_size))
|
||||
.collect::<Vec<TuiConstraint>>();
|
||||
|
||||
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::<Vec<Cell>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Row>>();
|
||||
|
||||
let widths = widths
|
||||
.into_iter()
|
||||
.map(|w| w.to_tui(screen_size, layout_size))
|
||||
.collect::<Vec<TuiConstraint>>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String, NodeTypeConfig>,
|
||||
}
|
||||
|
||||
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<Vec<Constraint>>,
|
||||
}
|
||||
|
||||
#[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<NodeFilter, UiElement>,
|
||||
|
||||
#[serde(default)]
|
||||
pub search_identifier: Option<UiElement>,
|
||||
pub search_direction_identifiers: SearchDirectionIdentifiersUi,
|
||||
|
||||
#[serde(default)]
|
||||
pub search_identifiers: HashMap<SearchAlgorithm, UiElement>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
|
@ -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<Utc>,
|
||||
#[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)
|
||||
}
|
||||
|
@ -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<Vec<Node>> {
|
||||
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<Vec<Node>> {
|
||||
})
|
||||
})
|
||||
.map(|name| Node::new(parent.to_string_lossy().to_string(), name))
|
||||
.filter(|n| config.filter(n))
|
||||
.collect::<Vec<Node>>();
|
||||
.filter(|n| config.filter(n));
|
||||
|
||||
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::<Vec<(_, _)>>();
|
||||
|
||||
nodes.sort_by(|(_, s1), (_, s2)| s2.cmp(s1));
|
||||
nodes.into_iter().map(|(n, _)| n).collect::<Vec<_>>()
|
||||
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
|
||||
};
|
||||
|
564
src/init.lua
564
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")
|
||||
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 (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' "Failed to move $PTH_ESC to ."
|
||||
"$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")
|
||||
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' "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 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" })
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -203,7 +203,7 @@ impl std::fmt::Display for Key {
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
write!(f, "{}", key_str)
|
||||
write!(f, "{key_str}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -143,7 +143,7 @@ pub fn call<'lua, R: Deserialize<'lua>>(
|
||||
func: &str,
|
||||
arg: mlua::Value<'lua>,
|
||||
) -> Result<R> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
613
src/lua/util.rs
613
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<Table> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
let func = lua.create_function(move |_, path: String| {
|
||||
let components: Vec<String> = 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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
let func =
|
||||
lua.create_function(move |lua, (node, config): (Table, Option<Table>)| {
|
||||
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<Table<'a>> {
|
||||
/// ```
|
||||
pub fn absolute<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
let func = lua.create_function(|lua, (path, config): (String, Option<Table>)| {
|
||||
let config: Option<RelativityConfig<String>> =
|
||||
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<Table<'a>> {
|
||||
let func =
|
||||
lua.create_function(move |lua, (path, config): (String, Option<Table>)| {
|
||||
let config: Option<RelativityConfig<String>> =
|
||||
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<Table<'a>> {
|
||||
let func = lua.create_function(|lua, (path, config): (String, Option<Table>)| {
|
||||
let config: ExplorerConfig = if let Some(cfg) = config {
|
||||
@ -170,13 +454,14 @@ pub fn explore<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
|
||||
|
||||
/// 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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
///
|
||||
/// ```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<Table<'a>> {
|
||||
@ -317,3 +621,252 @@ pub fn to_yaml<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
let func =
|
||||
lua.create_function(|lua, (string, style): (String, Option<Table>)| {
|
||||
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::<String, LuaError>(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<Table<'a>> {
|
||||
let func = lua.create_function(|lua, styles: Vec<Table>| {
|
||||
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<Table<'a>> {
|
||||
let func = lua.create_function(|lua, (text, options): (String, Value)| {
|
||||
let lines = match lua.from_value::<usize>(options.clone()) {
|
||||
Ok(width) => textwrap::wrap(&text, width),
|
||||
Err(_) => {
|
||||
let options = lua.from_value::<WrapOptions>(options)?;
|
||||
textwrap::wrap(&text, options.get_options())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(lines.iter().map(Cow::to_string).collect::<Vec<String>>())
|
||||
})?;
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table<'a>> {
|
||||
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<Table> {
|
||||
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)
|
||||
}
|
||||
|
@ -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<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub algorithm: SearchAlgorithm,
|
||||
|
||||
#[serde(default)]
|
||||
pub unordered: bool,
|
||||
}
|
||||
|
||||
impl NodeSearcher {
|
||||
pub fn new(pattern: String, recoverable_focus: Option<String>) -> Self {
|
||||
impl NodeSearcherApplicable {
|
||||
pub fn new(
|
||||
pattern: String,
|
||||
recoverable_focus: Option<String>,
|
||||
algorithm: SearchAlgorithm,
|
||||
unordered: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
pattern,
|
||||
recoverable_focus,
|
||||
algorithm,
|
||||
unordered,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search<I>(&self, nodes: I) -> Vec<Node>
|
||||
where
|
||||
I: IntoIterator<Item = Node>,
|
||||
{
|
||||
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::<Vec<_>>();
|
||||
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<NodeSorterApplicable>,
|
||||
|
||||
#[serde(default)]
|
||||
pub searcher: Option<NodeSearcher>,
|
||||
pub searcher: Option<NodeSearcherApplicable>,
|
||||
}
|
||||
|
||||
impl ExplorerConfig {
|
||||
|
73
src/node.rs
73
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 (
|
||||
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 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 {
|
||||
@ -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 {
|
||||
|
495
src/path.rs
Normal file
495
src/path.rs
Normal file
@ -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<PathBuf> = dirs::home_dir();
|
||||
}
|
||||
|
||||
// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
|
||||
pub fn diff<P, B>(path: P, base: B) -> Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
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<Component> = 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<B: AsRef<Path>> {
|
||||
base: Option<B>,
|
||||
with_prefix_dots: Option<bool>,
|
||||
without_suffix_dots: Option<bool>,
|
||||
}
|
||||
|
||||
impl<B: AsRef<Path>> RelativityConfig<B> {
|
||||
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<P, B>(
|
||||
path: P,
|
||||
config: Option<&RelativityConfig<B>>,
|
||||
) -> Result<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
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<P, B>(path: P, config: Option<&RelativityConfig<B>>) -> Result<String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
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<String>>;
|
||||
|
||||
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\"");
|
||||
}
|
||||
}
|
@ -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<RWX> 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<Octal> 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)]
|
||||
|
@ -31,7 +31,7 @@ pub fn get_tty() -> Result<fs::File> {
|
||||
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<fs::File> {
|
||||
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<fs::File> =
|
||||
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
|
||||
}
|
||||
}
|
||||
|
50
src/search.rs
Normal file
50
src/search.rs
Normal file
@ -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<String> for PathItem {
|
||||
fn from(value: String) -> Self {
|
||||
Self { path: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl SkimItem for PathItem {
|
||||
fn text(&self) -> std::borrow::Cow<str> {
|
||||
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<dyn MatchEngine> {
|
||||
match self {
|
||||
Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern),
|
||||
Self::Regex => REGEX_FACTORY.create_engine(pattern),
|
||||
}
|
||||
}
|
||||
}
|
599
src/ui.rs
599
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<String> },
|
||||
|
||||
/// A Lua function that returns lines to render
|
||||
DynamicList { render: String },
|
||||
|
||||
/// A table to render
|
||||
StaticTable {
|
||||
widths: Vec<Constraint>,
|
||||
col_spacing: Option<u16>,
|
||||
render: Vec<Vec<String>>,
|
||||
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<String>,
|
||||
},
|
||||
CustomTable {
|
||||
#[serde(default)]
|
||||
ui: PanelUiConfig,
|
||||
widths: Vec<Constraint>,
|
||||
col_spacing: Option<u16>,
|
||||
render: String,
|
||||
body: Vec<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -113,10 +108,8 @@ pub enum Layout {
|
||||
Selection,
|
||||
HelpMenu,
|
||||
SortAndFilter,
|
||||
CustomContent {
|
||||
title: Option<String>,
|
||||
body: ContentBody,
|
||||
},
|
||||
Static(Box<CustomPanel>),
|
||||
Dynamic(String),
|
||||
Horizontal {
|
||||
config: LayoutOptions,
|
||||
splits: Vec<Layout>,
|
||||
@ -125,6 +118,9 @@ pub enum Layout {
|
||||
config: LayoutOptions,
|
||||
splits: Vec<Layout>,
|
||||
},
|
||||
|
||||
/// For compatibility only. A better choice is Static or Dymanic layout.
|
||||
CustomContent(Box<CustomContent>),
|
||||
}
|
||||
|
||||
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<IndexSet<Modifier>>,
|
||||
b: Option<IndexSet<Modifier>>,
|
||||
) -> Option<IndexSet<Modifier>> {
|
||||
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<TuiStyle> for Style {
|
||||
fn into(self) -> TuiStyle {
|
||||
fn xor(modifiers: Option<IndexSet<Modifier>>) -> u16 {
|
||||
fn xor(modifiers: Option<IndexSet<Modifier>>) -> u8 {
|
||||
modifiers
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@ -292,6 +335,115 @@ impl Into<TuiStyle> 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<nu_ansi_term::Style> for Style {
|
||||
fn into(self) -> nu_ansi_term::Style {
|
||||
fn convert_color(color: Color) -> Option<nu_ansi_term::Color> {
|
||||
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<F>(style: &Style, f: F) -> bool
|
||||
where
|
||||
F: Fn(&IndexSet<Modifier>) -> 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<String>,
|
||||
pub subsequent_indent: Option<String>,
|
||||
pub break_words: Option<bool>,
|
||||
}
|
||||
|
||||
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<String, String>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl NodeUiMetadata {
|
||||
@ -444,6 +597,7 @@ impl NodeUiMetadata {
|
||||
is_focused: bool,
|
||||
total: usize,
|
||||
meta: HashMap<String, String>,
|
||||
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<B: Backend>(
|
||||
})
|
||||
.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<B: Backend>(
|
||||
is_focused,
|
||||
dir.total,
|
||||
node_type.meta,
|
||||
style,
|
||||
);
|
||||
|
||||
let cols = lua::serialize::<NodeUiMetadata>(lua, &meta)
|
||||
@ -642,7 +765,7 @@ fn draw_table<B: Backend>(
|
||||
.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<B: Backend>(
|
||||
.map(|x| Cell::from(x.to_owned()))
|
||||
.collect::<Vec<Cell>>();
|
||||
|
||||
Row::new(cols).style(style.into())
|
||||
Row::new(cols)
|
||||
})
|
||||
.collect::<Vec<Row>>()
|
||||
})
|
||||
@ -674,9 +797,9 @@ fn draw_table<B: Backend>(
|
||||
} 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<B: Backend>(
|
||||
_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<B: Backend>(
|
||||
.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::<Node>(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<B: Backend>(
|
||||
let ui = app.config.general.sort_and_filter_ui.to_owned();
|
||||
let filter_by: &IndexSet<NodeFilterApplicable> = &app.explorer_config.filters;
|
||||
let sort_by: &IndexSet<NodeSorterApplicable> = &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<B: Backend>(
|
||||
.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<B: Backend>(
|
||||
})
|
||||
.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<B: Backend>(
|
||||
})
|
||||
.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<B: Backend>(
|
||||
.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<B: Backend>(
|
||||
};
|
||||
|
||||
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<B: Backend>(
|
||||
.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<B: Backend>(
|
||||
f.render_widget(nothing, layout_size);
|
||||
}
|
||||
|
||||
pub fn draw_custom_content<B: Backend>(
|
||||
pub fn draw_dynamic<B: Backend>(
|
||||
f: &mut Frame<B>,
|
||||
screen_size: TuiRect,
|
||||
layout_size: TuiRect,
|
||||
app: &app::App,
|
||||
title: Option<String>,
|
||||
body: ContentBody,
|
||||
func: &str,
|
||||
lua: &Lua,
|
||||
) {
|
||||
let config = app.config.general.panel_ui.default.clone();
|
||||
let ctx = ContentRendererArg {
|
||||
app: app.to_lua_ctx_light(),
|
||||
layout_size: layout_size.into(),
|
||||
screen_size: screen_size.into(),
|
||||
};
|
||||
|
||||
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(),
|
||||
));
|
||||
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:?}"),
|
||||
});
|
||||
|
||||
draw_static(f, screen_size, layout_size, app, panel, lua);
|
||||
}
|
||||
|
||||
pub fn draw_static<B: Backend>(
|
||||
f: &mut Frame<B>,
|
||||
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::DynamicParagraph { 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 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
|
||||
let items = body
|
||||
.into_iter()
|
||||
.map(string_to_text)
|
||||
.map(ListItem::new)
|
||||
.collect::<Vec<ListItem>>();
|
||||
|
||||
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::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::<Vec<ListItem>>();
|
||||
|
||||
let content = List::new(items).block(block(
|
||||
config,
|
||||
title.map(|t| format!(" {} ", t)).unwrap_or_default(),
|
||||
));
|
||||
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<B: Backend>(
|
||||
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::<Vec<Cell>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Row>>();
|
||||
|
||||
let widths = widths
|
||||
.into_iter()
|
||||
.map(|w| w.to_tui(screen_size, layout_size))
|
||||
.collect::<Vec<TuiConstraint>>();
|
||||
|
||||
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<TuiRect> 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<B: Backend>(
|
||||
@ -1265,8 +1344,14 @@ pub fn draw_layout<B: Backend>(
|
||||
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<B: Backend>(
|
||||
|
||||
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<B: Backend>(
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user