Compare commits

...

114 Commits

Author SHA1 Message Date
Arijit Basu 68fb6fa1a2 Fix docs
Fixes: https://github.com/sayanarijit/xplr/issues/717
1 week ago
Arijit Basu a82ea6a3e5
Fix CI 1 week ago
Arijit Basu e13dd21728 Upgrade
Fixes: https://github.com/sayanarijit/xplr/issues/718
1 week ago
Arijit Basu 182a201b0d
Limit scroll padding 2 months ago
Arijit Basu e0b0466e42
v0.21.8 (#716)
- Added vim-like scrolling as the default scrolling method. Set
`xplr.config.general.paginated_scrolling = false` to disable ~ by
@ElSamhaa & @sayanarijit.
- Added `xplr.config.general.scroll_padding` config option to set the
padding in vim-like scrolling ~ by @ElSamhaa & @sayanarijit.
- Fixed some color rendering issues ~ by @har7an.
- Added feature flag so that xplr can be build with system Lua ~ by
@nekopsykose.
- Fixed `ScrollUpHalf` behavior.
- `xplr.util.lscolor()` won't return nil anymore.
- Arguments passed to the custom dynamic layout Lua function will
include `scrolltop` field.
- Fixed node_type resolution for directories with `.` in their name ~ by
@abhinavnatarajan.
- Dependency updates.
2 months ago
Arijit Basu 805e1594ed
Fix vim scrolling 2 months ago
Arijit Basu 41648ced34 Linting fixes 2 months ago
Arijit Basu 89d7bccce8 Update docs 2 months ago
Arijit Basu e15c1e8a8c
Lock ratatui 2 months ago
Arijit Basu 8afdf9e478
Fix node type resolution (#714)
Fixes #712 and #713.
2 months ago
Abhinav Natarajan a48dae008c Fix node type resolution
Fix node_type for directory with extension
2 months ago
Arijit Basu ad8afa9d38 Update deps 2 months ago
Arijit Basu c2a11059c8
Add yazi an alternative 2 months ago
Arijit Basu 6d7ccce282 Pass scrolltop in custom Lua function 2 months ago
Arijit Basu 90df0a2b5a vimlike_scrolling -> paginated_scrolling
Inspired by @ElSamhaa 's PR https://github.com/sayanarijit/xplr/pull/704
2 months ago
Arijit Basu ce52bcdf94 Revert vimlike scrolling
Use stateful ui widget.
2 months ago
Arijit Basu 6fb0781fe4 xplr.util.lscolor shouldn't return nil
Closes: https://github.com/sayanarijit/xplr/issues/705

Also update xplr version.
2 months ago
Arijit Basu c1bb251fef
Adds Vim-Like Scrolling to XPLR (#704)
- Added through a setting `vimlike_scrolling` which is turned off by
default
- A hard-coded _(for now)_ cushion of `5` lines that allows for
previewing the next lines while scrolling
- A separate struct `ScrollState` with getters and setters for the
`current_focus` field to disallow setting the field without updating the
`last_focus` field
3 months ago
Arijit Basu 976530ba70
Gen docs 3 months ago
Arijit Basu 96da7e1da8
Fix linting 3 months ago
Arijit Basu 96ffe8680b
Fix ScrollUpHalf 3 months ago
Ahmed ElSamhaa 1600ad9a9c Makes the preview cushion dynamic now, and sets an initial value 5 for it 3 months ago
Ahmed ElSamhaa 2a3d056bf1 Clarifies some comments 3 months ago
Ahmed ElSamhaa 91276f6871 Removes an unnecessary condition 3 months ago
Ahmed ElSamhaa 00bd54abe9 Removes unnecessary mut from the calc_skipped_rows fn 3 months ago
Ahmed ElSamhaa 95621af9eb Increases the preview_cushion to 5 like in vim 3 months ago
Ahmed ElSamhaa 5240b3904b Rolls back changes to the open terminal file 3 months ago
Ahmed ElSamhaa a6fb695ff9 Refactors the calc_skipped_rows function to make it even more readable 3 months ago
Ahmed ElSamhaa fd40de26e7 Adds tests for the ScrollState calc_skipped_rows fn 3 months ago
Ahmed ElSamhaa 87805509c5 Refactors the calc_skipped_rows function to make it more readable 3 months ago
Ahmed ElSamhaa 4aa367ca7c Makes the current_focus field private to limit usage to its setters and getters 3 months ago
Ahmed ElSamhaa 01606e0e60 Adds corresponding config setting for vimlike_scrolling 3 months ago
Ahmed ElSamhaa e834242f5d Adds vim-like scrolling 3 months ago
alice 7c6dffc2c6
cargo: allow building with system lua (#703)
useful for distros
3 months ago
har7an d5217f6677
cargo: Revert version update on `ansi-to-tui` (#702)
which causes custom styling to be lost on the currently selected line.
3 months ago
Arijit Basu 0285f0824c
Disable snap build 3 months ago
Arijit Basu a6b19425ae
Release v0.21.6 (#701)
- Snap build
- xplr.util.debug()
- `c` and `m` key bindings for quick copy and move.
- ScrollUpHalf fix
- Dependency updates
3 months ago
Arijit Basu 9db8b2cc19
Upgrade dependencies (#700) 3 months ago
mikoloism 68500f3a8e
[Feat] support `snapcraft` build package to releases (#697)
* build(snap): support snapcraft package

- add `snap/snapcraft.yaml` file

NOTE: under `devmode` until fit to release

* ci(gh-action): support `snapcraft` build and publish to `gh-release` page
4 months ago
Arijit Basu ded2e108bf
Add xplr.util.debug
Also update version
5 months ago
Arijit Basu 6e8f3da971
Quick copy and quick move (#692)
* Quick copy and quick move

- Press `c` to quickly copy the focused or selected path
- Press `m` to quickly move the focused or selected path
5 months ago
Arijit Basu d76a70fed4
Fix ScrollUpHalf 5 months ago
Arijit Basu 16673963aa
Minor fix 5 months ago
Arijit Basu b0ef9a5190
Remove unnecessary config example for now 5 months ago
Arijit Basu b70337708c
Minor fix 5 months ago
Arijit Basu 9127d15494
Use tree-view as example 5 months ago
Arijit Basu 66d9f7e586
Minor doc fix 5 months ago
Arijit Basu eab47a9044
Fix nixpkgs link 5 months ago
Arijit Basu a9e3752f56
Minor doc fix 5 months ago
Arijit Basu 470bea1265
NixOS install instructions 5 months ago
Arijit Basu cc578aaf0a
Add initial pwd to history 6 months ago
Arijit Basu 50e81853fe
Update version 6 months ago
Arijit Basu 414b45e4fd
Sync branch (#687)
* Update awesome-plugins.md

* Update awesome-plugins.md

* Visit deep level branches (#686)

* Visit deep level branches

- Press `)` to pass `NextVisitedDeepBranch`
- Press `(` to pass `LastVisitedDeepBranch`

* Last -> Previous

* Upgrade pkgs

* Clippy fixes

* Fix clippy err

---------

Co-authored-by: Dmitry Savosh <d.savosh@gmail.com>
6 months ago
Arijit Basu 75dabeb283
Add support for function keys upto F24 6 months ago
Arijit Basu 1629398adf
Sync branch (#677)
- Selection indicator in input and logs pane title for people who hide
the selection pane.
7 months ago
Arijit Basu dd8bb74dd4
Update Arch Linux package URL in install.md (#676)
The old URL returns 404 now.
7 months ago
Felix Yan 1dc5eae8fc
Update Arch Linux package URL in install.md
The old URL returns 404 now.
7 months ago
Arijit Basu 484b94a961
Add selection indicator in input and logs panel 8 months ago
Arijit Basu 50d9d1c54b
New plugin (#666) 9 months ago
Dugan Chen c7c3d2d7f6 Link to the one-table-column theme 9 months ago
Arijit Basu 1441275860
Avoid duplicate strip call (#664)
Fixes: https://github.com/sayanarijit/xplr/issues/662
11 months ago
Arijit Basu 8af1647c09
v0.21.3 (#661) 11 months ago
Arijit Basu 22b5fca8d9 Update version 11 months ago
Arijit Basu 4a3f18100d Display current mode help menu on top
Also, add global key binding f1.
Also, update deps.

Closes: https://github.com/sayanarijit/xplr/issues/655
11 months ago
Lewis Cook 6df168f8c1 init: Fix error upon deleting file on non-GNU systems 11 months ago
Arijit Basu eeb7b5d684
Update README.md 11 months ago
Arijit Basu 9a7ff5846d
Fix android build (#657)
* Fix android build

Ref: https://github.com/khvzak/mlua/issues/267#issuecomment-1644559018

* Update docs

* Fix typos by cheating a bit

* Instruction first
11 months ago
Arijit Basu 1b2226512f
Imrove builds (#650)
- Add more build targets
- Allow cross compile (if you have the resources, I don't)
- Fix failing nixos tests
11 months ago
Arijit Basu 56472998f5
Don't give up yet 11 months ago
Arijit Basu bf7ae3f748
Give up on the new platforms 11 months ago
Arijit Basu 94ba22bbcc
Upgrade 11 months ago
Arijit Basu 567a6201a8
Silently fail to 'enter' regular files (#654)
Silently fail to "Enter" regular files. Entering only makes sense for
directories.

This fixes
https://github.com/sayanarijit/xplr/issues/653#issue-1806818324

I don't know Rust at all, so I make no claims to the code quality. But I
have tested this change and it does work.
11 months ago
Arijit Basu 54d6d19003
Also enter symlink dir 11 months ago
Dugan Chen 4aeb3dd7c8 Use built-in node method 12 months ago
Dugan Chen 5626422ba4 Silently fail to 'enter' regular files 12 months ago
Arijit Basu 1941355128
Imrove builds
- Add more build targets
- Allow cross compile (if you have the resources, I don't)
- Fix failing nixos tests
12 months ago
Arijit Basu 2f78691333
Update/upgrade deps 12 months ago
Arijit Basu a2fbf759dd
Strip 12 months ago
Arijit Basu bc7f3cbbcf
Minor update 12 months ago
Arijit Basu ad50342260
Fix focus on back 12 months ago
Arijit Basu 313c61db96
Optimize get_current_dir
Closes: https://github.com/sayanarijit/xplr/issues/628
12 months ago
Arijit Basu 255517c2a9
Also respect general.table.headers.cols[*].style 12 months ago
Arijit Basu 9844ae1476
Respect low priority styles
- xplr.config.general.selection.item.style
- xplr.config.general.table.row.style
- xplr.config.general.table.row.cols[*].style
- xplr.config.general.table.header.cols[*].style

Ref: https://github.com/sayanarijit/xplr/issues/640
12 months ago
Arijit Basu d282032b3d
Fix symlink base again 12 months ago
Arijit Basu ba26752f6c
Use correct base for symlink for alternate layouts 12 months ago
Arijit Basu 0cc8723e8e
Document on_selection_change 12 months ago
Arijit Basu 2f3c2ea0e4
Fix lint 12 months ago
Arijit Basu 219ee68152
Update sum-type.md 12 months ago
Arijit Basu 859d888bde
Improve sum type docs 12 months ago
Arijit Basu f84d9d5c6a
More fixes 12 months ago
Arijit Basu 3fcfb1dbef
Fix sum type doc 12 months ago
Arijit Basu 4c51f0affe
Document sum types for hackers (#647)
* Document sum types for hackers

So you don't have to learn rust to configure xplr.

* Fix typos
12 months ago
Arijit Basu 9d1bd99fd4
Implement on_selection_change
Also optimize navigation with selection items.
Refresh selection only when it's required.

Closes: https://github.com/sayanarijit/xplr/issues/635
12 months ago
Arijit Basu 8209988ba6
Add tree-view.xplr 12 months ago
Arijit Basu 33c5aa9f14
Bring back enqueue 12 months ago
Arijit Basu cae50e4bcf
Remove unnecessary enqueue step 12 months ago
har7an 048b1c701a
docs: Add entry to awesome-plugins (#634)
mentioning https://gitlab.com/hartan/web-devicons.xplr
1 year ago
Arijit Basu 508f4b980b
Fix doc 1 year ago
Karim Lalani 28c9e0e3a0
chore: added codespell to github ci (#632)
chore: added codespell from 8cca2d3566 to github ci

chore: move spellcheck to out of testsuite

fix: add missing checkout step to spellcheck

exclude target

move works to ignore to .codespellignore

fix: typo

add lua specific
1 year ago
Noah Mayr 4ccd9796c4
Use xdg-rust crate instead of dirs crate (#631)
* Use xdg-rust crate instead of dirs crate

* Fix clippy warning
1 year ago
Solitude 36a7f1dc17
Honor XDG_CONFIG_HOME (#629) 1 year ago
Arijit Basu 2cc8e0c510
Cleanup unused features 1 year ago
Arijit Basu 27bc1217b3
Document jf syntax 1 year ago
Arijit Basu ab90381fda
Prompt when in doubt (#623)
* Update deps

* Prompt for user input when in doubt

- Ask before delete.
- For copy, move or symlink operations, ask what to do if a file with
  the same name exists.
- Update version.

Closes: https://github.com/sayanarijit/xplr/issues/615
1 year ago
Arijit Basu 2a775371f6
Use sayanarijit/jf for xplr -m 1 year ago
Arijit Basu 3bee8060c7
Lower LS_COLORS priority (#622)
* Lower LS_COLORS priority

Fixes: https://github.com/sayanarijit/xplr/issues/620

* Remove default style

* Fix doc markdown
1 year ago
Arijit Basu 97e30e2a6f
Allow nesting layouts inside a custom layout (#618)
This adds `CustomLayout` panel for nesting a `Layout` inside the `Static` and
`Dynamic` layouts.

This will help switching between different layouts dynamically, without
having to switch modes.
1 year ago
Arijit Basu 7c26c48e18
Trim binary size by reducing skim dependencies 1 year ago
Arijit Basu 17269ab17f
Update awesome-hacks.md 1 year ago
Arijit Basu 8aff0ba918
Clarify how to deal with init.lua 1 year ago
Arijit Basu 4228a71ed9
Mostly documentation fixes (#611)
Also, CLI help improvement.
1 year ago
Arijit Basu 252a1f5c37
Documentation fixes and cli help improvement 1 year ago
Henrique Goulart 4f0db1f3e3
docs: add missing comma to node_types lua conf
Add missing comma to node_types lua conf example in the docs.
1 year ago
Kian-Meng Ang 8cca2d3566 Fix typos
Found via `codespell -S target -L ratatui,crate,ser,enque,noice`
1 year ago

@ -1,4 +1,14 @@
# Why dynamic linking? # Why dynamic linking?
# See https://github.com/sayanarijit/xplr/issues/309 # See https://github.com/sayanarijit/xplr/issues/309
[target.x86_64-unknown-linux-gnu] [target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-args=-rdynamic"] rustflags = ["-C", "link-args=-rdynamic"]
[target.aarch64-unknown-linux-gnu]
rustflags = ["-C", "linker=aarch64-linux-gnu-gcc", "-C", "link-args=-rdynamic"]
[target.aarch64-linux-android]
rustflags = ["-C", "linker=aarch64-linux-android-clang", "-C", "link-args=-rdynamic", "-C", "default-linker-libraries"]
[target.arm-unknown-linux-gnueabihf]
rustflags = ["-C", "linker=arm-linux-gnueabihf-gcc", "-C", "link-args=-rdynamic"]

@ -0,0 +1,6 @@
ratatui
crate
ser
enque
noice
ans

@ -12,71 +12,84 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: build:
- macos-latest - macos
- ubuntu-latest - macos-aarch64
- ubuntu-20.04 - linux
- linux-musl
- linux-aarch64
- linux-arm
rust: [stable] rust: [stable]
include: include:
# See the list: https://github.com/cross-rs/cross # See the list: https://github.com/cross-rs/cross
- os: macos-latest - build: macos
artifact_prefix: macos os: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
binary_postfix: ""
- os: ubuntu-latest - build: macos-aarch64
artifact_prefix: linux os: macos-latest
target: aarch64-apple-darwin
- build: linux
os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
binary_postfix: ""
- os: ubuntu-20.04 - build: linux-musl
artifact_prefix: linux-musl os: ubuntu-latest
target: x86_64-unknown-linux-musl target: x86_64-unknown-linux-musl
binary_postfix: ""
# Will see later - build: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
# - os: ubuntu-latest - build: linux-arm
# artifact_prefix: x86_64-android os: ubuntu-latest
# target: x86_64-linux-android target: arm-unknown-linux-gnueabihf
# binary_postfix: ''
#
# - os: ubuntu-latest
# artifact_prefix: aarch64-android
# target: aarch64-linux-android
# binary_postfix: ''
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Installing Rust toolchain - name: Installing Rust toolchain
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: ${{ matrix.rust }} toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }} target: ${{ matrix.target }}
override: true
- name: Installing needed macOS dependencies - name: Installing needed macOS dependencies
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
run: brew install openssl@1.1 run: brew install openssl@1.1
- name: Installing needed Ubuntu dependencies - name: Installing needed Ubuntu dependencies
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-latest'
run: | run: |
sudo apt-get update --fix-missing sudo apt-get update --fix-missing
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev sudo apt-get install -y --no-install-recommends liblua5.1-0-dev libluajit-5.1-dev gcc pkg-config curl git make ca-certificates
sudo apt-get install -y snapd
# sudo snap install snapcraft --classic
# sudo snap install multipass --classic --beta
- if: matrix.build == 'linux-musl'
run: sudo apt-get install -y musl-tools
- name: Checking out sources - if: matrix.build == 'linux-aarch64'
uses: actions/checkout@v1 run: sudo apt-get install -y gcc-aarch64-linux-gnu
- if: matrix.build == 'linux-arm'
run: |
sudo apt-get install -y gcc-multilib
sudo apt-get install -y gcc-arm-linux-gnueabihf
sudo ln -s /usr/include/asm-generic/ /usr/include/asm
- name: Running cargo build - name: Running cargo build
uses: actions-rs/cargo@v1 run: cargo build --locked --release --target ${{ matrix.target }}
with:
use-cross: true # - name: Running snapcraft build
command: build # run: |
toolchain: ${{ matrix.rust }} # snapcraft
args: --locked --release --target ${{ matrix.target }} # printf ' [ INFO ] generated <snapcraft> files include:\n'
# command ls -Al | grep "\.snap" | awk '{ print $9 }'
# mv ./*.snap ./xplr.snap
- name: Install gpg secret key - name: Install gpg secret key
run: | run: |
@ -87,9 +100,8 @@ jobs:
shell: bash shell: bash
run: | run: |
cd target/${{ matrix.target }}/release cd target/${{ matrix.target }}/release
BINARY_NAME=xplr${{ matrix.binary_postfix }} BINARY_NAME=xplr
strip $BINARY_NAME RELEASE_NAME=$BINARY_NAME-${{ matrix.build }}
RELEASE_NAME=xplr-${{ matrix.artifact_prefix }}
tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME
shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.sha256 shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.sha256
cat <(echo "${{ secrets.GPG_PASS }}") | gpg --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor $RELEASE_NAME.tar.gz cat <(echo "${{ secrets.GPG_PASS }}") | gpg --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor $RELEASE_NAME.tar.gz
@ -98,17 +110,23 @@ jobs:
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: | files: |
target/${{ matrix.target }}/release/xplr-${{ matrix.artifact_prefix }}.tar.gz target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.tar.gz
target/${{ matrix.target }}/release/xplr-${{ matrix.artifact_prefix }}.sha256 target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.sha256
target/${{ matrix.target }}/release/xplr-${{ matrix.artifact_prefix }}.tar.gz.asc target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.tar.gz.asc
xplr.snap
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Cleaning snapcraft
# run: |
# command rm --verbose ./*.snap
# snapcraft clean
publish-gpg-signature: publish-gpg-signature:
name: Publishing GPG signature name: Publishing GPG signature
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install gpg secret key - name: Install gpg secret key
run: | run: |
cat <(echo -e "${{ secrets.GPG_SECRET }}") | gpg --batch --import cat <(echo -e "${{ secrets.GPG_SECRET }}") | gpg --batch --import
@ -133,20 +151,16 @@ jobs:
name: Publishing to Cargo name: Publishing to Cargo
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable toolchain: stable
override: true
- run: | - run: |
sudo apt-get update --fix-missing sudo apt-get update --fix-missing
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- uses: actions-rs/cargo@v1 - run: cargo publish --allow-dirty
with:
command: publish
args: --allow-dirty
env: env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_KEY }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_KEY }}

@ -11,120 +11,134 @@ jobs:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: stable toolchain: stable
profile: minimal - run: cargo check
override: true
- uses: actions-rs/cargo@v1 fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: rustfmt
- run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with: with:
command: check toolchain: stable
components: clippy
- run: cargo clippy -- -D warnings
spellcheck:
name: Spellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: codespell-project/actions-codespell@v1
with:
ignore_words_file: .codespellignore
test: test:
name: Test Suite name: Test Suite
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs:
- check
- fmt
- clippy
- spellcheck
strategy: strategy:
matrix: matrix:
os: build:
- macos-latest - macos
- ubuntu-latest - macos-aarch64
- ubuntu-20.04 - linux
- linux-musl
- linux-aarch64
- linux-arm
rust: [stable] rust: [stable]
include: include:
- os: macos-latest # See the list: https://github.com/cross-rs/cross
artifact_prefix: macos
- build: macos
os: macos-latest
target: x86_64-apple-darwin target: x86_64-apple-darwin
binary_postfix: ""
- os: ubuntu-latest - build: macos-aarch64
artifact_prefix: linux os: macos-latest
target: aarch64-apple-darwin
- build: linux
os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
binary_postfix: ""
- os: ubuntu-20.04 - build: linux-musl
artifact_prefix: linux-musl os: ubuntu-latest
target: x86_64-unknown-linux-musl target: x86_64-unknown-linux-musl
binary_postfix: ""
- build: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- build: linux-arm
os: ubuntu-latest
target: arm-unknown-linux-gnueabihf
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Installing Rust toolchain - name: Installing Rust toolchain
uses: actions-rs/toolchain@v1 uses: dtolnay/rust-toolchain@stable
with: with:
toolchain: ${{ matrix.rust }} toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }} target: ${{ matrix.target }}
override: true
- name: Installing needed macOS dependencies - name: Installing needed macOS dependencies
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
run: brew install openssl@1.1 run: brew install openssl@1.1
- name: Installing needed Ubuntu dependencies - name: Installing needed Ubuntu dependencies
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-latest'
run: | run: |
sudo apt-get update --fix-missing sudo apt-get update --fix-missing
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev sudo apt-get install -y --no-install-recommends liblua5.1-0-dev libluajit-5.1-dev gcc pkg-config curl git make ca-certificates
- name: Build - if: matrix.build == 'linux-musl'
uses: actions-rs/cargo@v1 run: sudo apt-get install -y musl-tools
with:
command: build
toolchain: ${{ matrix.rust }}
args: --target ${{ matrix.target }}
- name: Test - if: matrix.build == 'linux-aarch64'
uses: actions-rs/cargo@v1 run: sudo apt-get install -y gcc-aarch64-linux-gnu
with:
command: test - if: matrix.build == 'linux-arm'
toolchain: ${{ matrix.rust }} run: |
args: --target ${{ matrix.target }} sudo apt-get install -y gcc-multilib
sudo apt-get install -y gcc-arm-linux-gnueabihf
sudo ln -s /usr/include/asm-generic/ /usr/include/asm
- run: cargo build --target ${{ matrix.target }}
- if: matrix.build == 'macos' || matrix.build == 'linux'
run: cargo test --target ${{ matrix.target }}
# bench: # bench:
# name: Benchmarks # name: Benchmarks
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# steps: # steps:
# - uses: actions/checkout@v2 # - uses: actions/checkout@v3
# - uses: actions-rs/toolchain@v1 # - uses: dtolnay/rust-toolchain@stable
# with: # with:
# toolchain: stable # toolchain: stable
# profile: minimal
# override: true
# # These dependencies are required for `clipboard` # # These dependencies are required for `clipboard`
# - run: sudo apt-get install -y -qq libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev # - run: sudo apt-get install -y -qq libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
# - uses: actions-rs/cargo@v1 # - run: cargo bench
# with:
# command: bench
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
components: rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
components: clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings

6
.gitignore vendored

@ -17,3 +17,9 @@ book/
# direnv # direnv
.direnv/ .direnv/
# nix
result
# test files
/init.lua

@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status, identity and expression, level of experience, education, socioeconomic status,
nationality, personal appearance, race, religion, or sexual identity nationality, personal appearance, race, religion, or sexual identity
and orientation. and orientation.
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
* Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
* Focusing on what is best not just for us as individuals, but for the - Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban ### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community **Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals. individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within **Consequence**: A permanent ban from any sort of public interaction within

1184
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@ path = './benches/criterion.rs'
[package] [package]
name = 'xplr' name = 'xplr'
version = '0.21.1' version = '0.21.9'
authors = ['Arijit Basu <hi@arijitbasu.in>'] authors = ['Arijit Basu <hi@arijitbasu.in>']
edition = '2021' edition = '2021'
description = 'A hackable, minimal, fast TUI file explorer' description = 'A hackable, minimal, fast TUI file explorer'
@ -22,27 +22,29 @@ categories = ['command-line-interface', 'command-line-utilities']
include = ['src/**/*', 'docs/en/src/**/*', 'LICENSE', 'README.md'] include = ['src/**/*', 'docs/en/src/**/*', 'LICENSE', 'README.md']
[dependencies] [dependencies]
libc = "0.2.140" libc = "0.2.155"
humansize = "2.1.3" humansize = "2.1.3"
natord = "1.0.9" natord = "1.0.9"
anyhow = "1.0.70" anyhow = "1.0.86"
serde_yaml = "0.9.19" serde_yaml = "0.9.34"
crossterm = "0.26.1" crossterm = { version = "0.27.0", features = [], default-features = false }
dirs = "5.0.0" ansi-to-tui = "=3.1.0"
ansi-to-tui = "3.0.0" regex = "1.10.5"
regex = "1.7.3" gethostname = "0.4.3"
gethostname = "0.4.1" serde_json = "1.0.117"
serde_json = "1.0.94" path-absolutize = "3.1.1"
path-absolutize = "3.0.14" which = "6.0.1"
which = "4.4.0" nu-ansi-term = "0.50.0"
nu-ansi-term = "0.47.0"
textwrap = "0.16" textwrap = "0.16"
snailquote = "0.3.1" snailquote = "0.3.1"
skim = "0.10.4" skim = { version = "0.10.4", default-features = false }
time = { version = "0.3.20", features = ["serde", "local-offset", "formatting", "macros"] } time = { version = "0.3.36", features = ["serde", "local-offset", "formatting", "macros"] }
jf = "0.6.2"
xdg = "2.5.2"
home = "0.5.9"
[dependencies.lscolors] [dependencies.lscolors]
version = "0.13.0" version = "0.17.0"
default-features = false default-features = false
features = ["nu-ansi-term"] features = ["nu-ansi-term"]
@ -52,37 +54,41 @@ default-features = false
[dependencies.mime_guess] [dependencies.mime_guess]
version = "2.0.4" version = "2.0.4"
features = ["rev-mappings"] default-features = false
[dependencies.tui] [dependencies.tui]
version = "0.20.1" version = "=0.26.1" # https://github.com/ratatui-org/ratatui/issues/1032
default-features = false default-features = false
features = ['crossterm', 'serde'] features = ['crossterm', 'serde']
package = 'ratatui' package = 'ratatui'
[dependencies.serde] [dependencies.serde]
version = "1.0.158" version = "1.0.203"
features = ['derive'] features = []
default-features = false
[dependencies.indexmap] [dependencies.indexmap]
version = "1.9.3" version = "2.2.6"
features = ['serde'] features = ['serde']
[dependencies.mlua] [dependencies.mlua]
version = "0.8.8" version = "0.9.8"
features = ['luajit', 'vendored', 'serialize', 'send'] features = ['luajit', 'serialize', 'send']
[dependencies.tui-input] [dependencies.tui-input]
version = "0.7.0" version = "0.8.0"
features = ['serde'] features = ['serde']
[dev-dependencies] [dev-dependencies]
criterion = "0.4.0" criterion = "0.5.1"
assert_cmd = "2.0.10" assert_cmd = "2.0.14"
[profile.release] [profile.release]
lto = true lto = true
codegen-units = 1 codegen-units = 1
panic = 'abort' panic = 'abort'
strip = true
[features] [features]
default = ["vendored-lua"]
vendored-lua = ["mlua/vendored"]

@ -12,18 +12,6 @@ A hackable, minimal, fast TUI file explorer
<img src="https://img.shields.io/crates/v/xplr.svg" /> <img src="https://img.shields.io/crates/v/xplr.svg" />
</a> </a>
<a href="https://github.com/sayanarijit/xplr/commits">
<img src="https://img.shields.io/github/commit-activity/m/sayanarijit/xplr" />
</a>
<a href="https://matrix.to/#/#xplr-pub:matrix.org">
<img alt="Matrix" src="https://img.shields.io/matrix/xplr-pub:matrix.org?color=0DB787&label=matrix&logo=Matrix">
</a>
<a href="https://discord.gg/JmasSPCcz3">
<img src="https://img.shields.io/discord/834369918312382485?color=5865F2&label=discord&logo=Discord" />
</a>
</p> </p>
<p align="center"> <p align="center">
@ -38,7 +26,6 @@ https://user-images.githubusercontent.com/11632726/166747867-8a4573f2-cb2f-43a6-
[<a href="https://xplr.dev/en/awesome-hacks">Hacks</a>] [<a href="https://xplr.dev/en/awesome-hacks">Hacks</a>]
[<a href="https://xplr.dev/en/awesome-plugins">Plugins</a>] [<a href="https://xplr.dev/en/awesome-plugins">Plugins</a>]
[<a href="https://xplr.dev/en/awesome-integrations">Integrations</a>] [<a href="https://xplr.dev/en/awesome-integrations">Integrations</a>]
[<a href="https://xplr.dev/en/community">Community</a>]
</h3> </h3>
xplr is a terminal UI based file explorer that aims to increase our terminal xplr is a terminal UI based file explorer that aims to increase our terminal
@ -60,6 +47,8 @@ integration][15], enabling you to achieve insane terminal productivity.
- [[Article] What is a TUI file explorer & why would you need one? ~ xplr.stck.me](https://xplr.stck.me/post/25252/What-is-a-TUI-file-explorer-why-would-you-need-one) - [[Article] What is a TUI file explorer & why would you need one? ~ xplr.stck.me](https://xplr.stck.me/post/25252/What-is-a-TUI-file-explorer-why-would-you-need-one)
- [[Article] FOSSPicks - Linux Magazine](<https://www.linux-magazine.com/Issues/2022/258/FOSSPicks/(offset)/6>)
## Packaging ## Packaging
Package maintainers please refer to the [RELEASE.md](./RELEASE.md). Package maintainers please refer to the [RELEASE.md](./RELEASE.md).

@ -3,8 +3,8 @@
See [install.md](./docs/en/src/install.md#build-from-source) See [install.md](./docs/en/src/install.md#build-from-source)
Note: xplr ships with vendored luajit. If the platform can't compile this, Note: xplr ships with vendored luajit. If the platform can't compile this,
you need to grep out the feature "vendored" from the "mlua" dependency you need to compile using `--no-default-features` argument to avoid using
specified in [Cargo.toml](./Cargo.toml), and static link luajit yourself. vendored luajit, so that you can static link luajit yourself.
# Release # Release

@ -98,6 +98,7 @@ fn draw_benchmark(c: &mut Criterion) {
}); });
let lua = mlua::Lua::new(); let lua = mlua::Lua::new();
let mut ui = ui::UI::new(&lua);
let mut app = let mut app =
app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into()) app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into())
.expect("failed to create app"); .expect("failed to create app");
@ -121,7 +122,7 @@ fn draw_benchmark(c: &mut Criterion) {
c.bench_function("draw on terminal", |b| { c.bench_function("draw on terminal", |b| {
b.iter(|| { b.iter(|| {
terminal.draw(|f| ui::draw(f, &app, &lua)).unwrap(); terminal.draw(|f| ui.draw(f, &app)).unwrap();
}) })
}); });

@ -10,6 +10,7 @@
- [Layouts][9] - [Layouts][9]
- [Modes][7] - [Modes][7]
- [Concept][32] - [Concept][32]
- [Sum Type][42]
- [Key Bindings][27] - [Key Bindings][27]
- [Configure Key Bindings][28] - [Configure Key Bindings][28]
- [Default Key Bindings][14] - [Default Key Bindings][14]
@ -38,8 +39,6 @@
- [Awesome Integrations][20] - [Awesome Integrations][20]
- [Alternatives][22] - [Alternatives][22]
- [Upgrade Guide][23] - [Upgrade Guide][23]
- [Community][24]
- [Contribute][25]
[1]: introduction.md [1]: introduction.md
[2]: quickstart.md [2]: quickstart.md
@ -63,8 +62,6 @@
[20]: awesome-integrations.md [20]: awesome-integrations.md
[22]: alternatives.md [22]: alternatives.md
[23]: upgrade-guide.md [23]: upgrade-guide.md
[24]: community.md
[25]: contribute.md
[26]: column-renderer.md [26]: column-renderer.md
[27]: key-bindings.md [27]: key-bindings.md
[28]: configure-key-bindings.md [28]: configure-key-bindings.md
@ -81,3 +78,4 @@
[39]: input-operation.md [39]: input-operation.md
[40]: xplr.util.md [40]: xplr.util.md
[41]: searching.md [41]: searching.md
[42]: sum-type.md

@ -15,6 +15,7 @@ These are the alternative TUI/CLI file managers/explorers you might want to chec
- [clifm][11] - [clifm][11]
- [clifm][12] (non curses) - [clifm][12] (non curses)
- [felix][14] - [felix][14]
- [yazi][15]
[add more][13] [add more][13]
@ -30,5 +31,6 @@ These are the alternative TUI/CLI file managers/explorers you might want to chec
[10]: https://git.2f30.org/noice/ [10]: https://git.2f30.org/noice/
[11]: https://github.com/pasqu4le/clifm [11]: https://github.com/pasqu4le/clifm
[12]: https://github.com/leo-arch/clifm [12]: https://github.com/leo-arch/clifm
[13]: community.md [13]: https://github.com/sayanarijit/xplr/edit/dev/docs/en/src/alternatives.md
[14]: https://github.com/kyoheiu/felix [14]: https://github.com/kyoheiu/felix
[15]: https://github.com/sxyazi/yazi

@ -6,9 +6,9 @@ too small or too niche for a full fledge [plugin][2].
Do you have something cool to share? Do you have something cool to share?
[Edit this file][3] or [share them here][4] or [let us know][5]. [Edit this file][3] or [share them here][4].
You can try these hacks by writing them to a file, say `hack.lua` and passsing You can try these hacks by writing them to a file, say `hack.lua` and passing
it to xplr with `--extra-config` or `-C`. it to xplr with `--extra-config` or `-C`.
```bash ```bash
@ -423,7 +423,7 @@ Preview text files in a native xplr pane (should be fast enough).
```lua ```lua
local function stat(node) local function stat(node)
return node.mime_essence return xplr.util.to_yaml(xplr.util.node(node.absolute_path))
end end
local function read(path, height) local function read(path, height)
@ -452,8 +452,29 @@ local function read(path, height)
return res return res
end end
xplr.fn.custom.preview_pane = {}
xplr.fn.custom.preview_pane.render = function(ctx)
local title = nil
local body = ""
local n = ctx.app.focused_node
if n and n.canonical then
n = n.canonical
end
xplr.config.layouts.builtin.default = { if n then
title = { format = n.absolute_path, style = xplr.util.lscolor(n.absolute_path) }
if n.is_file then
body = read(n.absolute_path, ctx.layout_size.height) or stat(n)
else
body = stat(n)
end
end
return { CustomParagraph = { ui = { title = title }, body = body } }
end
local preview_pane = { Dynamic = "custom.preview_pane.render" }
local split_preview = {
Horizontal = { Horizontal = {
config = { config = {
constraints = { constraints = {
@ -463,34 +484,13 @@ xplr.config.layouts.builtin.default = {
}, },
splits = { splits = {
"Table", "Table",
{ preview_pane,
CustomContent = {
title = "preview",
body = { DynamicParagraph = { render = "custom.preview_pane.render" } },
},
},
}, },
}, },
} }
xplr.fn.custom.preview_pane = {} xplr.config.layouts.builtin.default =
xplr.fn.custom.preview_pane.render = function(ctx) xplr.util.layout_replace(xplr.config.layouts.builtin.default, "Table", split_preview)
local n = ctx.app.focused_node
if n and n.canonical then
n = n.canonical
end
if n then
if n.is_file then
return read(n.absolute_path, ctx.layout_size.height)
else
return stat(n)
end
else
return ""
end
end
``` ```
</details> </details>
@ -510,7 +510,7 @@ Navigate using the [tere][19] file explorer (defaults to type-to-nav system).
xplr.config.modes.builtin.default.key_bindings.on_key.T = { xplr.config.modes.builtin.default.key_bindings.on_key.T = {
help = "tere nav", help = "tere nav",
messages = { messages = {
{ BashExec0 = [[xplr -m 'ChangeDirectory: %q' "$(tere)"]] }, { BashExec0 = [["$XPLR" -m 'ChangeDirectory: %q' "$(tere)"]] },
}, },
} }
``` ```
@ -526,7 +526,6 @@ xplr.config.modes.builtin.default.key_bindings.on_key.T = {
[2]: plugin.md [2]: plugin.md
[3]: https://github.com/sayanarijit/xplr/edit/main/docs/en/src/awesome-hacks.md [3]: https://github.com/sayanarijit/xplr/edit/main/docs/en/src/awesome-hacks.md
[4]: https://github.com/sayanarijit/xplr/discussions/categories/show-and-tell [4]: https://github.com/sayanarijit/xplr/discussions/categories/show-and-tell
[5]: community.md
[6]: https://gifyu.com/image/rGSR [6]: https://gifyu.com/image/rGSR
[7]: https://s4.gifyu.com/images/xplr-bookmark.gif [7]: https://s4.gifyu.com/images/xplr-bookmark.gif
[8]: https://github.com/sayanarijit [8]: https://github.com/sayanarijit

@ -11,8 +11,9 @@ of the following plugins work for you, it's very easy to
- [**sayanarijit/dual-pane.xplr**][43] Implements support for dual-pane navigation into xplr. - [**sayanarijit/dual-pane.xplr**][43] Implements support for dual-pane navigation into xplr.
- [**sayanarijit/map.xplr**][38] Visually inspect and interactively execute batch commands using xplr. - [**sayanarijit/map.xplr**][38] Visually inspect and interactively execute batch commands using xplr.
- [**sayanarijit/offline-docs.xplr**][51] Fetch the appropriate version of xplr docs and browse offline. - [**sayanarijit/offline-docs.xplr**][51] Fetch the appropriate version of xplr docs and browse offline.
- [**sayanarijit/regex-search.xplr**][55] Bring back the regex based seach in xplr. - [**sayanarijit/regex-search.xplr**][55] Bring back the regex based search in xplr.
- [**sayanarijit/registers.xplr**][49] Use multiple registers to store the selected paths. - [**sayanarijit/registers.xplr**][49] Use multiple registers to store the selected paths.
- [**sayanarijit/tree-view.xplr**][61] Hackable tree view for xplr
- [**sayanarijit/tri-pane.xplr**][56] xplr plugin that implements ranger-like three pane layout. - [**sayanarijit/tri-pane.xplr**][56] xplr plugin that implements ranger-like three pane layout.
- [**sayanarijit/type-to-nav.xplr**][28] Inspired by [nnn's type-to-nav mode][29] for xplr, - [**sayanarijit/type-to-nav.xplr**][28] Inspired by [nnn's type-to-nav mode][29] for xplr,
with some tweaks. with some tweaks.
@ -48,9 +49,14 @@ of the following plugins work for you, it's very easy to
- [**sayanarijit/material-landscape.xplr**][19] Material Landscape - [**sayanarijit/material-landscape.xplr**][19] Material Landscape
- [**sayanarijit/material-landscape2.xplr**][20] Material Landscape 2 - [**sayanarijit/material-landscape2.xplr**][20] Material Landscape 2
- [**sayanarijit/zentable.xplr**][31] A clean, distraction free xplr table UI - [**sayanarijit/zentable.xplr**][31] A clean, distraction free xplr table UI
- [**dy-sh/dysh-style.xplr**][63] Complements xplr theme with icons and highlighting.
- [**prncss-xyz/icons.xplr**][30] An icon theme for xplr. - [**prncss-xyz/icons.xplr**][30] An icon theme for xplr.
- [**dtomvan/extra-icons.xplr**][50] Adds more icons to icons.xplr, compatible - [**dtomvan/extra-icons.xplr**][50] Adds more icons to icons.xplr, compatible
with zentable.xplr. with zentable.xplr.
- [**hartan/web-devicons.xplr**][59] Adds [nvim-web-devicons][60] to xplr with
optional coloring
- [**duganchen/one-table-column.xplr**][62] Moves file stats to a status bar.
- [**dy-sh/get-rid-of-index.xplr**][64] Removes the index column.
## Also See: ## Also See:
@ -114,3 +120,9 @@ of the following plugins work for you, it's very easy to
[56]: https://github.com/sayanarijit/tri-pane.xplr [56]: https://github.com/sayanarijit/tri-pane.xplr
[57]: https://github.com/emsquid/style.xplr [57]: https://github.com/emsquid/style.xplr
[58]: style.md [58]: style.md
[59]: https://gitlab.com/hartan/web-devicons.xplr
[60]: https://github.com/nvim-tree/nvim-web-devicons
[61]: https://github.com/sayanarijit/tree-view.xplr
[62]: https://github.com/duganchen/one-table-column.xplr
[63]: https://github.com/dy-sh/dysh-style.xplr
[64]: https://github.com/dy-sh/get-rid-of-index.xplr

@ -4,21 +4,21 @@ xplr allows customizing the shape and style of the borders.
### Border ### Border
A border can be one of the following: A border is a [sum type][2] that can be one of the following:
- Top - "Top"
- Right - "Right"
- Bottom - "Bottom"
- Left - "Left"
### Border Type ### Border Type
A border can be one of the following: A border type is a [sum type][2] that can be one of the following:
- Plain - "Plain"
- Rounded - "Rounded"
- Double - "Double"
- Thick - "Thick"
### Border Style ### Border Style
@ -34,3 +34,4 @@ xplr.config.general.panel_ui.default.border_style.bg = "Gray"
``` ```
[1]: style.md#style [1]: style.md#style
[2]: sum-type.md

@ -1,12 +0,0 @@
# Community
Building an active community of awesome people and learning stuff together is
one of my reasons to publish this tool and maintain it. Hence, please feel free
to reach out via your preferred way.
- Real-time chat lovers can join our [**matrix room**][3] or [**discord channel**][1].
- Forum discussion veterans can [**start a new GitHub discussion**][2].
[1]: https://discord.gg/JmasSPCcz3
[2]: https://github.com/sayanarijit/xplr/discussions
[3]: https://matrix.to/#/#xplr-pub:matrix.org

@ -131,5 +131,26 @@ return {
{ LogSuccess = "Switched layout" }, { LogSuccess = "Switched layout" },
{ CallLuaSilently = "custom.some_plugin_with_hooks.on_layout_switch" }, { CallLuaSilently = "custom.some_plugin_with_hooks.on_layout_switch" },
} }
-- Add messages to send when the selection changes
--
-- Type: list of [Message](https://xplr.dev/en/message#message)s
on_selection_change = {
{ LogSuccess = "Selection changed" },
{ CallLuaSilently = "custom.some_plugin_with_hooks.on_selection_change" },
}
} }
``` ```
---
> Note:
>
> It's not recommended to copy the entire configuration, unless you want to
> freeze it and miss out on useful updates to the defaults.
>
> Instead, you can use this as a reference to overwrite only the parts you
> want to update.
>
> If you still want to copy the entire configuration, make sure to put your
> customization before the return statement.

@ -86,7 +86,7 @@ Default action to perform in case if a keyboard input not mapped via any of the
## Key ## Key
A key can be one of the following: A key is a [sum type][36] can be one of the following:
- 0, 1, ... 9 - 0, 1, ... 9
- a, b, ... z - a, b, ... z
@ -230,3 +230,4 @@ Visit [Awesome Plugins][27] for more [integration][28] options.
[33]: #on_character [33]: #on_character
[34]: #on_navigation [34]: #on_navigation
[35]: #on_function [35]: #on_function
[36]: sum-type.md

@ -1,32 +0,0 @@
If you like xplr, and want to contribute, that would be really awesome.
You can contribute to this project in the following ways
- Contribute your time and expertise (read [CONTRIBUTING.md][1] for instructions).
- **Developers:** You can help me improve my code, fix things, implement features etc.
- **Repository maintainers:** You can save the users from the pain of managing xplr in their system manually.
- **Code Reviewers:** Teach me your ways of code.
- **Designers:** You can make the logo even more awesome, donate stickers and blog post worthy pictures.
- **Bloggers, YouTubers & broadcasters:** You can help spread the word.
- Contribute by donating or sponsoring me via any of the following ways.
- [GitHub Sponsors][5]
- [Open Collective][2]
- [ko-fi][3]
- [liberapay][6]
- [PayPal][7]
For further queries or concern related to `xplr`, [just ask us][4].
### Backers
<a href="https://opencollective.com/xplr#backer"><img src="https://opencollective.com/xplr/tiers/backer.svg?width=890" /></a>
[1]: https://github.com/sayanarijit/xplr/blob/main/CONTRIBUTING.md
[2]: https://opencollective.com/xplr
[3]: https://ko-fi.com/sayanarijit
[4]: community.md
[5]: https://github.com/sponsors/sayanarijit?o=esb
[6]: https://liberapay.com/sayanarijit
[7]: https://paypal.me/sayanarijit

@ -16,14 +16,17 @@ of [modes][4] and the key mappings for each mode.
| key | remaps | action | | key | remaps | action |
| --------- | ------ | ------------------- | | --------- | ------ | ------------------- |
| ( | | prev deep branch |
| ) | | next deep branch |
| . | | show hidden | | . | | show hidden |
| / | ctrl-f | search | | / | ctrl-f | search |
| : | | action | | : | | action |
| ? | | global help menu | | ? | f1 | global help menu |
| G | | go to bottom | | G | | go to bottom |
| V | ctrl-a | select/unselect all | | V | ctrl-a | select/unselect all |
| c | | copy to |
| ctrl-d | | duplicate as | | ctrl-d | | duplicate as |
| ctrl-i | | next visited path | | ctrl-i | tab | next visited path |
| ctrl-n | | next selection | | ctrl-n | | next selection |
| ctrl-o | | last visited path | | ctrl-o | | last visited path |
| ctrl-p | | prev selection | | ctrl-p | | prev selection |
@ -38,6 +41,7 @@ of [modes][4] and the key mappings for each mode.
| h | left | back | | h | left | back |
| k | up | up | | k | up | up |
| l | right | enter | | l | right | enter |
| m | | move to |
| page-down | | scroll down | | page-down | | scroll down |
| page-up | | scroll up | | page-up | | scroll up |
| q | | quit | | q | | quit |
@ -49,113 +53,157 @@ of [modes][4] and the key mappings for each mode.
| ~ | | go home | | ~ | | go home |
| [0-9] | | input | | [0-9] | | input |
### vroot ### go_to_path
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
### rename
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
### recover
| key | remaps | action |
| --- | ------ | ---------------- |
| f1 | | global help menu |
### go_to
| key | remaps | action | | key | remaps | action |
| ------ | ------ | ------------ | | --- | ------ | ---------------- |
| . | | vroot $PWD | | f | | follow symlink |
| / | | vroot / | | f1 | | global help menu |
| ctrl-r | | reset vroot | | g | | top |
| ctrl-u | | unset vroot | | i | | initial $PWD |
| v | | toggle vroot | | p | | path |
| ~ | | vroot $HOME | | x | | open in gui |
### relative_path_does_match_regex ### relative_path_does_match_regex
| key | remaps | action | | key | remaps | action |
| ----- | ------ | ------ | | ----- | ------ | ---------------- |
| enter | | submit | | enter | | submit |
| f1 | | global help menu |
### go_to_path ### action
| key | remaps | action | | key | remaps | action |
| ----- | ------ | ------------ | | ----- | ------ | -------------------- |
| enter | | submit | | ! | | shell |
| tab | | try complete | | c | | create |
| e | | open in editor |
| f1 | | global help menu |
| l | | logs |
| m | | toggle mouse |
| p | | edit permissions |
| q | | quit options |
| s | | selection operations |
| v | | vroot |
| [0-9] | | go to index |
### duplicate_as ### default
| key | remaps | action | | key | remaps | action |
| ----- | ------ | ------------ | | --------- | ------ | ------------------- |
| enter | | submit | | ( | | prev deep branch |
| tab | | try complete | | ) | | next deep branch |
| . | | show hidden |
| / | ctrl-f | search |
| : | | action |
| ? | f1 | global help menu |
| G | | go to bottom |
| V | ctrl-a | select/unselect all |
| c | | copy to |
| ctrl-d | | duplicate as |
| ctrl-i | tab | 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 |
| d | | delete |
| down | j | down |
| enter | | quit with result |
| f | | filter |
| g | | go to |
| h | left | back |
| k | up | up |
| l | right | enter |
| m | | move to |
| page-down | | scroll down |
| page-up | | scroll up |
| q | | quit |
| r | | rename |
| s | | sort |
| space | v | toggle selection |
| { | | scroll up half |
| } | | scroll down half |
| ~ | | go home |
| [0-9] | | input |
### debug_error ### debug_error
| key | remaps | action | | key | remaps | action |
| ----- | ------ | ------------------- | | ----- | ------ | ------------------- |
| enter | | open logs in editor | | enter | | open logs in editor |
| f1 | | global help menu |
| q | | quit | | q | | quit |
### selection_ops ### create_directory
| key | remaps | action | | key | remaps | action |
| --- | ------ | --------------- | | ----- | ------ | ---------------- |
| c | | copy here | | enter | | submit |
| e | | edit selection | | f1 | | global help menu |
| h | | hardlink here | | tab | | try complete |
| l | | list selection |
| m | | move here |
| s | | softlink here |
| u | | clear selection |
### sort ### selection_ops
| key | remaps | action | | key | remaps | action |
| --------- | ------ | --------------------------------- | | --- | ------ | ---------------- |
| ! | | reverse sorters | | c | | copy here |
| C | | by created reverse | | e | | edit selection |
| E | | by canonical extension reverse | | f1 | | global help menu |
| L | | by last modified reverse | | h | | hardlink here |
| M | | by canonical mime essence reverse | | l | | list selection |
| N | | by node type reverse | | m | | move here |
| R | | by relative path reverse | | s | | softlink here |
| S | | by size reverse | | u | | clear selection |
| backspace | | remove last sorter |
| c | | by created |
| ctrl-r | | reset sorters |
| ctrl-u | | clear sorters |
| e | | by canonical extension |
| enter | | submit |
| l | | by last modified |
| m | | by canonical mime essence |
| n | | by node type |
| r | | by relative path |
| s | | by size |
### go_to ### relative_path_does_not_match_regex
| key | remaps | action | | key | remaps | action |
| --- | ------ | -------------- | | ----- | ------ | ---------------- |
| f | | follow symlink | | enter | | submit |
| g | | top | | f1 | | global help menu |
| i | | initial $PWD |
| p | | path |
| x | | open in gui |
### edit_permissions ### create_file
| key | remaps | action | | key | remaps | action |
| ------ | ------ | ------ | | ----- | ------ | ---------------- |
| G | | -group | | enter | | submit |
| M | | min | | f1 | | global help menu |
| O | | -other | | tab | | try complete |
| U | | -user |
| ctrl-r | | reset |
| enter | | submit |
| g | | +group |
| m | | max |
| o | | +other |
| u | | +user |
### switch_layout ### quit
| key | remaps | action | | key | remaps | action |
| --- | ------ | -------------------- | | ----- | ------ | ----------------------- |
| 1 | | default | | enter | | just quit |
| 2 | | no help menu | | f | | quit printing focus |
| 3 | | no selection panel | | f1 | | global help menu |
| 4 | | no help or selection | | p | | quit printing pwd |
| r | | quit printing result |
| s | | quit printing selection |
### create ### create
@ -163,20 +211,19 @@ of [modes][4] and the key mappings for each mode.
| --- | ------ | ---------------- | | --- | ------ | ---------------- |
| d | | create directory | | d | | create directory |
| f | | create file | | f | | create file |
| f1 | | global help menu |
### create_directory ### vroot
| key | remaps | action |
| ----- | ------ | ------------ |
| enter | | submit |
| tab | | try complete |
### create_file
| key | remaps | action | | key | remaps | action |
| ----- | ------ | ------------ | | ------ | ------ | ---------------- |
| enter | | submit | | . | | vroot $PWD |
| tab | | try complete | | / | | vroot / |
| ctrl-r | | reset vroot |
| ctrl-u | | unset vroot |
| f1 | | global help menu |
| v | | toggle vroot |
| ~ | | vroot $HOME |
### search ### search
@ -191,33 +238,95 @@ of [modes][4] and the key mappings for each mode.
| ctrl-z | | toggle ordering | | ctrl-z | | toggle ordering |
| enter | | submit | | enter | | submit |
| esc | | cancel | | esc | | cancel |
| f1 | | global help menu |
| left | | back | | left | | back |
| right | | enter | | right | | enter |
| tab | | toggle selection | | tab | | toggle selection |
### switch_layout
| key | remaps | action |
| --- | ------ | -------------------- |
| 1 | | default |
| 2 | | no help menu |
| 3 | | no selection panel |
| 4 | | no help or selection |
| f1 | | global help menu |
### sort
| key | remaps | action |
| --------- | ------ | --------------------------------- |
| ! | | reverse sorters |
| C | | by created reverse |
| E | | by canonical extension reverse |
| L | | by last modified reverse |
| M | | by canonical mime essence reverse |
| N | | by node type reverse |
| R | | by relative path reverse |
| S | | by size reverse |
| backspace | | remove last sorter |
| c | | by created |
| ctrl-r | | reset sorters |
| ctrl-u | | clear sorters |
| e | | by canonical extension |
| enter | | submit |
| f1 | | global help menu |
| l | | by last modified |
| m | | by canonical mime essence |
| n | | by node type |
| r | | by relative path |
| s | | by size |
### number ### number
| key | remaps | action | | key | remaps | action |
| ----- | ------ | -------- | | ----- | ------ | ---------------- |
| down | j | to down | | down | j | to down |
| enter | | to index | | enter | | to index |
| k | up | to up | | f1 | | global help menu |
| [0-9] | | input | | k | up | to up |
| [0-9] | | input |
### action ### copy_to
| key | remaps | action | | key | remaps | action |
| ----- | ------ | -------------------- | | ----- | ------ | ---------------- |
| ! | | shell | | enter | | submit |
| c | | create | | f1 | | global help menu |
| e | | open in editor | | tab | | try complete |
| l | | logs |
| m | | toggle mouse | ### edit_permissions
| p | | edit permissions |
| q | | quit options | | key | remaps | action |
| s | | selection operations | | ------ | ------ | ---------------- |
| v | | vroot | | G | | -group |
| [0-9] | | go to index | | M | | min |
| O | | -other |
| U | | -user |
| ctrl-r | | reset |
| enter | | submit |
| f1 | | global help menu |
| g | | +group |
| m | | max |
| o | | +other |
| u | | +user |
### delete
| key | remaps | action |
| --- | ------ | ---------------- |
| D | | force delete |
| d | | delete |
| f1 | | global help menu |
### move_to
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
### filter ### filter
@ -227,39 +336,13 @@ of [modes][4] and the key mappings for each mode.
| backspace | | remove last filter | | backspace | | remove last filter |
| ctrl-r | | reset filters | | ctrl-r | | reset filters |
| ctrl-u | | clear filters | | ctrl-u | | clear filters |
| f1 | | global help menu |
| r | | relative path does match regex | | r | | relative path does match regex |
### rename ### duplicate_as
| 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 |
| --- | ------ | ------ |
### delete
| key | remaps | action | | key | remaps | action |
| --- | ------ | ------------ | | ----- | ------ | ---------------- |
| D | | force delete | | enter | | submit |
| d | | delete | | f1 | | global help menu |
| tab | | try complete |

@ -174,6 +174,8 @@ message using newline or null character (`\n` or `\0`).
> directly to this file, as safely escaping YAML strings is a lot of work. Use > directly to this file, as safely escaping YAML strings is a lot of work. Use
> `xplr -m` / `xplr --pipe-msg-in` to pass messages to xplr in a safer way. > `xplr -m` / `xplr --pipe-msg-in` to pass messages to xplr in a safer way.
> >
> It uses [jf][41] syntax to safely convert an YAML template into a valid message.
>
> Example: `"$XPLR" -m 'ChangeDirectory: %q' "${HOME:?}"` > Example: `"$XPLR" -m 'ChangeDirectory: %q' "${HOME:?}"`
#### XPLR_PIPE_SELECTION_OUT #### XPLR_PIPE_SELECTION_OUT
@ -225,3 +227,4 @@ List of paths, filtered and sorted as displayed in the [files table][28].
[38]: #xplr [38]: #xplr
[39]: #xplr_vroot [39]: #xplr_vroot
[40]: #xplr_initial_pwd [40]: #xplr_initial_pwd
[41]: https://github.com/sayanarijit/jf

@ -25,7 +25,7 @@ It contains the following information:
### filter ### filter
A filter can be one of the following: A filter is a [sum type][5] that can be one of the following:
- "RelativePathIs" - "RelativePathIs"
- "RelativePathIsNot" - "RelativePathIsNot"
@ -90,3 +90,4 @@ Here, `ToggleNodeFilter` is a [message][4] that adds or removes
[2]: #filter [2]: #filter
[3]: #input [3]: #input
[4]: message.md [4]: message.md
[5]: sum-type.md

@ -42,6 +42,19 @@ Set it to `true` if you want to hide all remaps in the help menu.
Type: boolean Type: boolean
#### xplr.config.general.paginated_scrolling
Set it to `true` if you want paginated scrolling.
Type: boolean
#### xplr.config.general.scroll_padding
Set the padding value to the scroll area.
Only applicable when `xplr.config.general.paginated_scrolling = false`.
Type: boolean
#### xplr.config.general.enforce_bounded_index_navigation #### xplr.config.general.enforce_bounded_index_navigation
Set it to `true` if you want the cursor to stay in the same position when Set it to `true` if you want the cursor to stay in the same position when
@ -72,7 +85,7 @@ Type: nullable string
#### xplr.config.general.logs.info.style #### xplr.config.general.logs.info.style
The style for the informations logs. The style for the information logs.
Type: [Style](https://xplr.dev/en/style) Type: [Style](https://xplr.dev/en/style)
@ -587,12 +600,16 @@ Type: nullable list of [Node Sorter](https://xplr.dev/en/sorting#node-sorter-app
#### xplr.config.general.initial_mode #### xplr.config.general.initial_mode
The name of one of the modes to use when xplr loads. The name of one of the modes to use when xplr loads.
This isn't the default mode. To modify the default mode, overwrite
[xplr.config.modes.builtin.default](https://xplr.dev/en/modes#xplrconfigmodesbuiltindefault).
Type: nullable string Type: nullable string
#### xplr.config.general.initial_layout #### xplr.config.general.initial_layout
The name of one of the layouts to use when xplr loads. The name of one of the layouts to use when xplr loads.
This isn't the default layout. To modify the default layout, overwrite
[xplr.config.layouts.builtin.default](https://xplr.dev/en/layouts#xplrconfiglayoutsbuiltindefault).
Type: nullable string Type: nullable string

@ -1,6 +1,6 @@
# Input Operation # Input Operation
Cursor based input operation can be one of the following: Cursor based input operation is a [sum type][3] can be one of the following:
- { SetCursor = int } - { SetCursor = int }
- { InsertCharacter = str } - { InsertCharacter = str }
@ -24,3 +24,4 @@ Cursor based input operation can be one of the following:
[1]: message.md [1]: message.md
[2]: messages.md [2]: messages.md
[3]: sum-type.md

@ -41,6 +41,48 @@ repositories:
nix-env -f https://github.com/NixOS/nixpkgs/tarball/master -iA xplr nix-env -f https://github.com/NixOS/nixpkgs/tarball/master -iA xplr
``` ```
Or
```nix
# configuration.nix or darwin-configuration.nix
environment.systemPackages = with nixpkgs; [
xplr
# ...
];
```
#### [Home Manager][30]
```nix
# home.nix
home.packages = with nixpkgs; [
xplr
# ...
];
```
Or
```nix
# home.nix
programs.xplr = {
enable = true;
# Optional params:
plugins = {
tree-view = fetchFromGitHub {
owner = "sayanarijit";
repo = "tree-view.xplr";
};
local-plugin = "/home/user/.config/xplr/plugins/local-plugin";
};
extraConfig = ''
require("tree-view").setup()
require("local-plugin").setup()
'';
};
```
### Arch Linux ### Arch Linux
(same for Manjaro Linux) (same for Manjaro Linux)
@ -196,57 +238,16 @@ cargo build --locked --release --bin xplr
sudo cp target/release/xplr /usr/local/bin/ sudo cp target/release/xplr /usr/local/bin/
``` ```
## Android
### [Termux][23]
[![xplr-termuxfd3c398d3cf4bcbc.md.jpg][24]][25]
> Please note that xplr isn't heavily tested on Termux, hence things might
> need a little tweaking and fixing for a smooth usage experience.
- Install build dependencies
```bash
pkg install rustc cargo make
```
- Install `xplr`
```bash
cargo install --locked --force xplr
```
- Setup storage
```bash
termux-setup-storage
```
- Setup config and runtime dir
```bash
export XDG_CONFIG_HOME="$PWD/storage/.config"
export XDG_RUNTIME_DIR="$PWD/storage/run"
mkdir -p "$XDG_CONFIG_HOME" "$XDG_RUNTIME_DIR"
```
- Run
```bash
~/.cargo/bin/xplr
```
[1]: #direct-download [1]: #direct-download
[2]: #from-cratesio [2]: #from-cratesio
[3]: #build-from-source [3]: #build-from-source
[4]: https://github.com/sayanarijit/xplr/watchers [4]: https://github.com/sayanarijit/xplr/watchers
[5]: https://repology.org/badge/vertical-allrepos/xplr.svg [5]: https://repology.org/badge/vertical-allrepos/xplr.svg
[6]: https://repology.org/project/xplr/versions [6]: https://repology.org/project/xplr/versions
[7]: https://archlinux.org/packages/community/x86_64/xplr [7]: https://archlinux.org/packages/extra/x86_64/xplr
[8]: https://aur.archlinux.org/packages/?O=0&SeB=n&K=xplr&outdated=&SB=n&SO=a&PP=50&do_Search=Go [8]: https://aur.archlinux.org/packages/?O=0&SeB=n&K=xplr&outdated=&SB=n&SO=a&PP=50&do_Search=Go
[9]: https://github.com/shubham-cpp/void-pkg-templates [9]: https://github.com/shubham-cpp/void-pkg-templates
[10]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/xplr [10]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/xp/xplr/package.nix
[11]: https://ports.macports.org/port/xplr [11]: https://ports.macports.org/port/xplr
[12]: https://formulae.brew.sh/formula/xplr [12]: https://formulae.brew.sh/formula/xplr
[13]: https://cgit.freebsd.org/ports/plain/misc/xplr/ [13]: https://cgit.freebsd.org/ports/plain/misc/xplr/
@ -259,10 +260,10 @@ sudo cp target/release/xplr /usr/local/bin/
[20]: https://gcc.gnu.org/ [20]: https://gcc.gnu.org/
[21]: https://www.gnu.org/software/make/ [21]: https://www.gnu.org/software/make/
[22]: https://git-scm.com/ [22]: https://git-scm.com/
[23]: https://termux.com/ [23]: https://github.com/sayanarijit/xplr/assets/11632726/3b61e8c8-76f0-48e8-8734-50e9e7e495b7
[24]: https://s3.gifyu.com/images/xplr-termuxfd3c398d3cf4bcbc.md.jpg
[25]: https://gifyu.com/image/tF2D [25]: https://gifyu.com/image/tF2D
[26]: https://github.com/sayanarijit/xplr/releases/latest/download/xplr-linux-musl.tar.gz [26]: https://github.com/sayanarijit/xplr/releases/latest/download/xplr-linux-musl.tar.gz
[27]: https://pkgs.alpinelinux.org/packages?name=xplr [27]: https://pkgs.alpinelinux.org/packages?name=xplr
[28]: https://gpo.zugaina.org/Overlays/guru/app-misc/xplr [28]: https://gpo.zugaina.org/Overlays/guru/app-misc/xplr
[29]: https://formulae.brew.sh/formula/coreutils [29]: https://formulae.brew.sh/formula/coreutils
[30]: https://github.com/nix-community/home-manager/blob/master/modules/programs/xplr.nix

@ -84,8 +84,6 @@ Some of the coolest features xplr provide beside the basic stuff:
(`:` `q` `s`). (`:` `q` `s`).
- Quit with failure (`ctrl-c`). - Quit with failure (`ctrl-c`).
**Q.** What features should be added here? [let us know][20].
[1]: layouts.md [1]: layouts.md
[2]: configure-key-bindings.md [2]: configure-key-bindings.md
[3]: awesome-plugins.md [3]: awesome-plugins.md
@ -105,6 +103,5 @@ Some of the coolest features xplr provide beside the basic stuff:
[17]: node_types.md [17]: node_types.md
[18]: https://github.com/sayanarijit/xplr/blob/main/src/init.lua [18]: https://github.com/sayanarijit/xplr/blob/main/src/init.lua
[19]: messages.md#startfifo [19]: messages.md#startfifo
[20]: community.md
[21]: messages.md#virtual-root [21]: messages.md#virtual-root
[22]: configuration.md#hooks [22]: configuration.md#hooks

@ -2,15 +2,13 @@
#### Example: Defining Custom Layout #### Example: Defining Custom Layout
[![layout.png][23]][24]
```lua ```lua
xplr.config.layouts.builtin.default = { xplr.config.layouts.builtin.default = {
Horizontal = { Horizontal = {
config = { config = {
margin = 1, margin = 1,
horizontal_margin = 2, horizontal_margin = 1,
vertical_margin = 3, vertical_margin = 1,
constraints = { constraints = {
{ Percentage = 50 }, { Percentage = 50 },
{ Percentage = 50 }, { Percentage = 50 },
@ -24,7 +22,22 @@ xplr.config.layouts.builtin.default = {
} }
``` ```
A layout can be one of the following: Result:
```
╭ /home ─────────────╮╭ Help [default] ────╮
│ ╭─── path ││. show hidden │
│ ├▸[ð Desktop/] ││/ search │
│ ├ ð Documents/ ││: action │
│ ├ ð Downloads/ ││? global help │
│ ├ ð GitHub/ ││G go to bottom │
│ ├ ð Music/ ││V select/unselect│
│ ├ ð Pictures/ ││ctrl duplicate as │
│ ├ ð Public/ ││ctrl next visit │
╰────────────────────╯╰────────────────────╯
```
A layout is a [sum type][56] can be one of the following:
- [Nothing][8] - [Nothing][8]
- [Table][9] - [Table][9]
@ -86,7 +99,7 @@ Type: { Static = [Custom Panel][27] }
This is a custom layout to render dynamic content using a function defined in 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]. [xplr.fn][28] that takes [Content Renderer Argument][36] and returns [Custom Panel][27].
Type: { Dynamic = [Content Renderer][35] } Type: { Dynamic = "[Content Renderer][35]" }
### Horizontal ### Horizontal
@ -97,7 +110,7 @@ It contains the following information:
- [config][15] - [config][15]
- [splits][17] - [splits][17]
Type: { Horizontal = { config = [config][15], splits = [splits][17] } Type: { Vertical = { config = [Layout Config][15], splits = { [Layout][17], ... } }
### Vertical ### Vertical
@ -108,7 +121,7 @@ It contains the following information:
- [config][15] - [config][15]
- [splits][17] - [splits][17]
Type: { Vertical = { config = [config][15], splits = [splits][17] } Type: { Vertical = { config = [Layout Config][15], splits = { [Layout][17], ... } }
## Layout Config ## Layout Config
@ -145,7 +158,7 @@ The constraints applied on the layout.
## Constraint ## Constraint
A constraint can be one of the following: A constraint is a [sum type][56] can be one of the following:
- { Percentage = int } - { Percentage = int }
- { Ratio = { int, int } } - { Ratio = { int, int } }
@ -173,11 +186,12 @@ The list of child layouts to fit into the parent layout.
## Custom Panel ## Custom Panel
Custom panel can be one of the following: Custom panel is a [sum type][56] can be one of the following:
- [CustomParagraph][29] - [CustomParagraph][29]
- [CustomList][30] - [CustomList][30]
- [CustomTable][31] - [CustomTable][31]
- [CustomLayout][55]
### CustomParagraph ### CustomParagraph
@ -199,6 +213,16 @@ xplr.config.layouts.builtin.default = {
} }
``` ```
Result:
```
╭ custom title ────────╮
│custom body │
│ │
│ │
╰──────────────────────╯
```
#### Example: Render a custom dynamic paragraph #### Example: Render a custom dynamic paragraph
```lua ```lua
@ -214,6 +238,23 @@ xplr.fn.custom.render_layout = function(ctx)
end end
``` ```
Result:
```
╭/home/sayanarijit───────────────────────────╮
│mime_essence: inode/directory │
│relative_path: Desktop │
│is_symlink: false │
│is_readonly: false │
│parent: /home/sayanarijit │
│absolute_path: /home/sayanarijit/Desktop │
│is_broken: false │
│created: 1668087850396758714 │
│size: 4096 │
│gid: 100 │
╰────────────────────────────────────────────╯
```
### CustomList ### CustomList
A list to render. It contains the following fields: A list to render. It contains the following fields:
@ -234,6 +275,17 @@ xplr.config.layouts.builtin.default = {
} }
``` ```
Result:
```
╭ custom title ─────────────╮
│1 │
│2 │
│3 │
│ │
╰───────────────────────────╯
```
#### Example: Render a custom dynamic list #### Example: Render a custom dynamic list
```lua ```lua
@ -253,6 +305,18 @@ xplr.fn.custom.render_layout = function(ctx)
end end
``` ```
Result:
```
╭/home/sayanarijit──────────╮
│Desktop │
│0.21.2 │
│17336 │
│ │
│ │
╰───────────────────────────╯
```
## CustomTable ## CustomTable
A custom table to render. It contains the following fields: A custom table to render. It contains the following fields:
@ -282,6 +346,18 @@ xplr.config.layouts.builtin.default = {
} }
``` ```
Result:
```
╭ custom title ────────────────────╮
│a b │
│c d │
│ │
│ │
│ │
╰──────────────────────────────────╯
```
#### Example: Render a custom dynamic table #### Example: Render a custom dynamic table
```lua ```lua
@ -308,6 +384,95 @@ xplr.fn.custom.render_layout = function(ctx)
end end
``` ```
Result:
```
╭/home/sayanarijit───────────────────────────╮
│ │
│Layout height 12 │
│Layout width 46 │
│ │
│Screen height 12 │
│Screen width 46 │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────╯
```
### CustomLayout
A whole custom layout to render. It doesn't make sense to use it as a
[Static][25] layout, but can be very useful to render as a [Dynamic][26] layout
for use cases where the structure of the layout needs to change without having
to switch modes.
> WARNING: Rendering the same dynamic custom layout recursively will result in
> a ugly crash.
#### Example: Render a custom dynamic layout
```lua
xplr.config.layouts.builtin.default = { Dynamic = "custom.render_layout" }
xplr.fn.custom.render_layout = function(ctx)
local inner = {
config = {
constraints = {
{ Percentage = 50 },
{ Percentage = 50 },
},
},
splits = {
{ Static = { CustomParagraph = { body = "Try your luck..." } } },
{ Static = { CustomParagraph = { body = "Press ctrl-r" } } },
},
}
local layout_type = "Vertical"
if math.random(1, 2) == 1 then
layout_type = "Horizontal"
end
return { CustomLayout = { [layout_type] = inner } }
end
```
Result:
```
╭─────────────────────╮╭─────────────────────╮
│Try your luck... ││Press ctrl-r │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
╰─────────────────────╯╰─────────────────────╯
```
Or
```
╭────────────────────────────────────────────╮
│Try your luck... │
│ │
│ │
│ │
╰────────────────────────────────────────────╯
╭────────────────────────────────────────────╮
│Press ctrl-r │
│ │
│ │
│ │
╰────────────────────────────────────────────╯
```
## Panel UI Config ## Panel UI Config
It contains the following optional fields: It contains the following optional fields:
@ -330,6 +495,7 @@ It contains the following information:
- [layout_size][37] - [layout_size][37]
- [screen_size][37] - [screen_size][37]
- [scrolltop][57]
- [app][38] - [app][38]
### Size ### Size
@ -343,13 +509,19 @@ It contains the following information:
Every field is of integer type. Every field is of integer type.
### scrolltop
Type: integer
The start index of the visible nodes in the table.
### app ### app
This is a lightweight version of the [Lua Context][39]. In this context, the This is a lightweight version of the [Lua Context][39]. In this context, the
heavyweight fields like [directory_buffer][50] are omitted for performance heavyweight fields like [directory_buffer][50] are omitted for performance
reasons. reasons.
Hence, only the following fields are avilable. Hence, only the following fields are available.
- [version][40] - [version][40]
- [pwd][41] - [pwd][41]
@ -390,8 +562,6 @@ Hence, only the following fields are avilable.
[20]: #vertical_margin [20]: #vertical_margin
[21]: #constraints [21]: #constraints
[22]: #constraint [22]: #constraint
[23]: https://s6.gifyu.com/images/layout.png
[24]: https://gifyu.com/image/1X38
[25]: #static [25]: #static
[26]: #dynamic [26]: #dynamic
[27]: #custom-panel [27]: #custom-panel
@ -422,3 +592,6 @@ Hence, only the following fields are avilable.
[52]: lua-function-calls.md#vroot [52]: lua-function-calls.md#vroot
[53]: lua-function-calls.md#initial_pwd [53]: lua-function-calls.md#initial_pwd
[54]: borders.md#border-type [54]: borders.md#border-type
[55]: #customlayout
[56]: sum-type.md
[57]: #scrolltop

@ -12,15 +12,13 @@ You can add new panels in `xplr.config.layouts.custom`.
##### Example: Defining Custom Layout ##### Example: Defining Custom Layout
![demo](https://s6.gifyu.com/images/layout.png)
```lua ```lua
xplr.config.layouts.builtin.default = { xplr.config.layouts.builtin.default = {
Horizontal = { Horizontal = {
config = { config = {
margin = 1, margin = 1,
horizontal_margin = 2, horizontal_margin = 1,
vertical_margin = 3, vertical_margin = 1,
constraints = { constraints = {
{ Percentage = 50 }, { Percentage = 50 },
{ Percentage = 50 }, { Percentage = 50 },
@ -34,6 +32,21 @@ xplr.config.layouts.builtin.default = {
} }
``` ```
Result:
```
╭ /home ─────────────╮╭ Help [default] ────╮
│ ╭─── path ││. show hidden │
│ ├▸[ð Desktop/] ││/ search │
│ ├ ð Documents/ ││: action │
│ ├ ð Downloads/ ││? global help │
│ ├ ð GitHub/ ││G go to bottom │
│ ├ ð Music/ ││V select/unselect│
│ ├ ð Pictures/ ││ctrl duplicate as │
│ ├ ð Public/ ││ctrl next visit │
╰────────────────────╯╰────────────────────╯
```
#### xplr.config.layouts.builtin.default #### xplr.config.layouts.builtin.default
The default layout The default layout

@ -372,7 +372,7 @@ The searcher to use (if any).
Type: nullable [Node Searcher Applicable][82] Type: nullable [Node Searcher Applicable][82]
## Also Ssee: ## Also See:
- [xplr.util][85] - [xplr.util][85]

@ -1,7 +1,9 @@
# Message # Message
You can think of xplr as a server. Just like web servers listen to HTTP You can think of xplr as a server. Just like web servers listen to HTTP
requests, xplr listens to [messages][1]. requests, xplr listens to messages.
A message is a [sum type][9] that can have [these possible values][1].
You can send these messages to an xplr session in the following ways: You can send these messages to an xplr session in the following ways:
@ -13,22 +15,35 @@ You can send these messages to an xplr session in the following ways:
### Format ### Format
To send messages using the [key bindings][2] or To send messages using the [key bindings][2] or [Lua function calls][3],
[Lua function calls][3], messages are represented in messages are represented in [Lua][5] syntax.
[Lua][5] syntax. For example:
For example:
- `"Quit"` - `"Quit"`
- `{ FocusPath = "/path/to/file" }` - `{ FocusPath = "/path/to/file" }`
- `{ Call = { command = "bash", args = { "-c", "read -p test" } } }` - `{ Call = { command = "bash", args = { "-c", "read -p test" } } }`
However, to send messages using the [input pipe][4], they need to be However, to send messages using the [input pipe][4], they need to be
represented using represented using [YAML][6] (or [JSON][7]) syntax.
[YAML][6] (or [JSON][7]) syntax. For example:
For example:
- `Quit` - `Quit`
- `FocusPath: "/path/to/file"` - `FocusPath: "/path/to/file"`
- `Call: { command: bash, args: ["-c", "read -p test"] }` - `Call: { command: bash, args: ["-c", "read -p test"] }`
Use `"$XPLR" -m TEMPLATE [VALUE]...` command-line option to safely format
`TEMPLATE` into a valid message. If uses [jf][8] to parse and render the
template. And `$XPLR` (rather than `xplr`) makes sure that the correct version
of the binary is being used.
For example:
- `"$XPLR" -m Quit`
- `"$XPLR" -m 'FocusPath: %q' "/path/to/file"`
- `"$XPLR" -m 'Call: { command: %q, args: [%*q] }' bash -c "read -p test"`
## Also See: ## Also See:
- [Full List of Messages][1] - [Full List of Messages][1]
@ -40,3 +55,5 @@ represented using
[5]: https://www.lua.org/ [5]: https://www.lua.org/
[6]: http://yaml.org/ [6]: http://yaml.org/
[7]: https://www.json.org [7]: https://www.json.org
[8]: https://github.com/sayanarijit/jf
[9]: sum-type.md

@ -320,6 +320,24 @@ Example:
- Lua: `"NextVisitedPath"` - Lua: `"NextVisitedPath"`
- YAML: `NextVisitedPath` - YAML: `NextVisitedPath`
#### PreviousVisitedDeepBranch
Go to the previous deep level branch.
Example:
- Lua: `"PreviousVisitedDeepBranch"`
- YAML: `PreviousVisitedDeepBranch`
#### NextVisitedDeepBranch
Go to the next deep level branch.
Example:
- Lua: `"NextVisitedDeepBranch"`
- YAML: `NextVisitedDeepBranch`
#### FollowSymlink #### FollowSymlink
Follow the symlink under focus to its actual location. Follow the symlink under focus to its actual location.
@ -389,7 +407,7 @@ Example:
Update the input buffer using cursor based operations. Update the input buffer using cursor based operations.
Type: { UpdateInputBuffer = [Input Opertaion](https://xplr.dev/en/input-operation) } Type: { UpdateInputBuffer = [Input Operation](https://xplr.dev/en/input-operation) }
Example: Example:
@ -418,7 +436,7 @@ Example:
#### BufferInputFromKey #### BufferInputFromKey
Append/buffer the characted read from a keyboard input into the Append/buffer the character read from a keyboard input into the
input buffer. input buffer.
Example: Example:
@ -843,7 +861,7 @@ Example:
Add a [filter](https://xplr.dev/en/filtering#filter) to exclude nodes Add a [filter](https://xplr.dev/en/filtering#filter) to exclude nodes
while exploring directories. while exploring directories.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Filters get automatically cleared when changing directories. Filters get automatically cleared when changing directories.
Type: { AddNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering#filter), input = "string" } Type: { AddNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering#filter), input = "string" }
@ -856,7 +874,7 @@ Example:
#### RemoveNodeFilter #### RemoveNodeFilter
Remove an existing [filter](https://xplr.dev/en/filtering#filter). Remove an existing [filter](https://xplr.dev/en/filtering#filter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { RemoveNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" } Type: { RemoveNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
@ -869,7 +887,7 @@ Example:
Remove a [filter](https://xplr.dev/en/filtering#filter) if it exists, Remove a [filter](https://xplr.dev/en/filtering#filter) if it exists,
else, add a it. else, add a it.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { ToggleNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" } Type: { ToggleNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
@ -882,7 +900,7 @@ Example:
Add a node [filter](https://xplr.dev/en/filtering#filter) reading the Add a node [filter](https://xplr.dev/en/filtering#filter) reading the
input from the buffer. input from the buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { AddNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) } Type: { AddNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
@ -895,7 +913,7 @@ Example:
Remove a node [filter](https://xplr.dev/en/filtering#filter) reading Remove a node [filter](https://xplr.dev/en/filtering#filter) reading
the input from the buffer. the input from the buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { RemoveNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) } Type: { RemoveNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
@ -907,7 +925,7 @@ Example:
#### RemoveLastNodeFilter #### RemoveLastNodeFilter
Remove the last node [filter](https://xplr.dev/en/filtering). Remove the last node [filter](https://xplr.dev/en/filtering).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -918,7 +936,7 @@ Example:
Reset the node [filters](https://xplr.dev/en/filtering) back to the Reset the node [filters](https://xplr.dev/en/filtering) back to the
default configuration. default configuration.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -928,7 +946,7 @@ Example:
#### ClearNodeFilters #### ClearNodeFilters
Clear all the node [filters](https://xplr.dev/en/filtering). Clear all the node [filters](https://xplr.dev/en/filtering).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -941,7 +959,7 @@ Example:
Add a [sorter](https://xplr.dev/en/sorting#sorter) to sort nodes while Add a [sorter](https://xplr.dev/en/sorting#sorter) to sort nodes while
exploring directories. exploring directories.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { AddNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } } Type: { AddNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
@ -953,7 +971,7 @@ Example:
#### RemoveNodeSorter #### RemoveNodeSorter
Remove an existing [sorter](https://xplr.dev/en/sorting#sorter). Remove an existing [sorter](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { RemoveNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) } Type: { RemoveNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
@ -965,7 +983,7 @@ Example:
#### ReverseNodeSorter #### ReverseNodeSorter
Reverse a node [sorter](https://xplr.dev/en/sorting#sorter). Reverse a node [sorter](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { ReverseNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) } Type: { ReverseNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
@ -978,7 +996,7 @@ Example:
Remove a [sorter](https://xplr.dev/en/sorting#sorter) if it exists, Remove a [sorter](https://xplr.dev/en/sorting#sorter) if it exists,
else, add a it. else, add a it.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { ToggleNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } } Type: { ToggleNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
@ -990,7 +1008,7 @@ Example:
#### ReverseNodeSorters #### ReverseNodeSorters
Reverse the node [sorters](https://xplr.dev/en/sorting#sorter). Reverse the node [sorters](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1000,7 +1018,7 @@ Example:
#### RemoveLastNodeSorter #### RemoveLastNodeSorter
Remove the last node [sorter](https://xplr.dev/en/sorting#sorter). Remove the last node [sorter](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1011,7 +1029,7 @@ Example:
Reset the node [sorters](https://xplr.dev/en/sorting#sorter) back to Reset the node [sorters](https://xplr.dev/en/sorting#sorter) back to
the default configuration. the default configuration.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1021,7 +1039,7 @@ Example:
#### ClearNodeSorters #### ClearNodeSorters
Clear all the node [sorters](https://xplr.dev/en/sorting#sorter). Clear all the node [sorters](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1033,7 +1051,7 @@ Example:
#### Search #### Search
Search files using the current or default (fuzzy) search algorithm. Search files using the current or default (fuzzy) search algorithm.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Type: { Search = "string" } Type: { Search = "string" }
@ -1056,7 +1074,7 @@ Example:
Search files using fuzzy match algorithm. Search files using fuzzy match algorithm.
It keeps the filters, but overrides the sorters. It keeps the filters, but overrides the sorters.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Type: { SearchFuzzy = "string" } Type: { SearchFuzzy = "string" }
@ -1069,7 +1087,7 @@ Example:
#### SearchFuzzyFromInput #### SearchFuzzyFromInput
Calls `SearchFuzzy` with the input taken from the input buffer. Calls `SearchFuzzy` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Example: Example:
@ -1080,7 +1098,7 @@ Example:
#### SearchFuzzyUnordered #### SearchFuzzyUnordered
Like `SearchFuzzy`, but doesn't not perform rank based sorting. Like `SearchFuzzy`, but doesn't not perform rank based sorting.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Type: { SearchFuzzyUnordered = "string" } Type: { SearchFuzzyUnordered = "string" }
@ -1093,7 +1111,7 @@ Example:
#### SearchFuzzyUnorderedFromInput #### SearchFuzzyUnorderedFromInput
Calls `SearchFuzzyUnordered` with the input taken from the input buffer. Calls `SearchFuzzyUnordered` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Example: Example:
@ -1105,7 +1123,7 @@ Example:
Search files using regex match algorithm. Search files using regex match algorithm.
It keeps the filters, but overrides the sorters. It keeps the filters, but overrides the sorters.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Type: { SearchRegex = "string" } Type: { SearchRegex = "string" }
@ -1118,7 +1136,7 @@ Example:
#### SearchRegexFromInput #### SearchRegexFromInput
Calls `SearchRegex` with the input taken from the input buffer. Calls `SearchRegex` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Example: Example:
@ -1129,7 +1147,7 @@ Example:
#### SearchRegexUnordered #### SearchRegexUnordered
Like `SearchRegex`, but doesn't not perform rank based sorting. Like `SearchRegex`, but doesn't not perform rank based sorting.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Type: { SearchRegexUnordered = "string" } Type: { SearchRegexUnordered = "string" }
@ -1142,7 +1160,7 @@ Example:
#### SearchRegexUnorderedFromInput #### SearchRegexUnorderedFromInput
Calls `SearchRegexUnordered` with the input taken from the input buffer. Calls `SearchRegexUnordered` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory. It gets reset automatically when changing directory.
Example: Example:
@ -1154,7 +1172,7 @@ Example:
Toggles between different search algorithms, without changing the input Toggles between different search algorithms, without changing the input
buffer buffer
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1164,7 +1182,7 @@ Example:
#### EnableSearchOrder #### EnableSearchOrder
Enables ranked search without changing the input buffer. Enables ranked search without changing the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1174,7 +1192,7 @@ Example:
#### DisableSearchOrder #### DisableSearchOrder
Disabled ranked search without changing the input buffer. Disabled ranked search without changing the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example: Example:
@ -1264,7 +1282,7 @@ Example:
#### ToggleFifo #### ToggleFifo
Toggle betwen {Start|Stop}Fifo Toggle between {Start|Stop}Fifo
Type: { ToggleFifo = "string" } Type: { ToggleFifo = "string" }

@ -35,6 +35,18 @@ The builtin go to path mode.
Type: [Mode](https://xplr.dev/en/mode) Type: [Mode](https://xplr.dev/en/mode)
#### xplr.config.modes.builtin.move_to
The builtin move_to mode.
Type: [Mode](https://xplr.dev/en/mode)
#### xplr.config.modes.builtin.copy_to
The builtin copy_to mode.
Type: [Mode](https://xplr.dev/en/mode)
#### xplr.config.modes.builtin.selection_ops #### xplr.config.modes.builtin.selection_ops
The builtin selection ops mode. The builtin selection ops mode.

@ -88,7 +88,7 @@ Example:
xplr.config.node_types.mime_essence = { xplr.config.node_types.mime_essence = {
application = { application = {
-- application/* -- application/*
["*"] = { meta = { icon = "a" } } ["*"] = { meta = { icon = "a" } },
-- application/pdf -- application/pdf
pdf = { meta = { icon = "" }, style = { fg = "Blue" } }, pdf = { meta = { icon = "" }, style = { fg = "Blue" } },

@ -24,7 +24,7 @@ Node Searcher contains the following fields:
### pattern ### pattern
The patters used to search. The patterns used to search.
Type: string Type: string

@ -27,7 +27,7 @@ It contains the following information:
### sorter ### sorter
A sorter can be one of the following: A sorter is a [sum type][4] that can be one of the following:
- "ByRelativePath" - "ByRelativePath"
- "ByIRelativePath" - "ByIRelativePath"
@ -82,3 +82,4 @@ This snippet defines the initial sorting logic to be applied when xplr loads.
[1]: #node-sorter-applicable [1]: #node-sorter-applicable
[2]: #sorter [2]: #sorter
[3]: #reverse [3]: #reverse
[4]: sum-type.md

@ -33,7 +33,7 @@ Modifiers to remove.
## Color ## Color
Color can be one of the following: Color is a [sum type][7] that can be one of the following:
- "Reset" - "Reset"
- "Black" - "Black"
@ -57,7 +57,7 @@ Color can be one of the following:
## Modifier ## Modifier
Modifier can be one of the following: Modifier is a [sum type][7] that can be one of the following:
- "Bold" - "Bold"
- "Dim" - "Dim"
@ -84,3 +84,4 @@ xplr.config.general.prompt.style.sub_modifiers = { "Hidden" }
[4]: #sub_modifiers [4]: #sub_modifiers
[5]: #color [5]: #color
[6]: #modifier [6]: #modifier
[7]: sum-type.md

@ -0,0 +1,96 @@
# Sum Type
> This section isn't specific to xplr. However, since xplr configuration makes
> heavy use of this particular data type, even though it isn't available in
> most of the mainstream programming languages (yet), making it a wild or
> unfamiliar concept for many, it's worth doing a quick introduction here.
>
> If you're already familiar with [Sum Type / Tagged Union][1] (e.g. Rust's
> enum), you can skip ahead.
While reading this doc, you'll come across some data types like [Layout][2],
[Color][4], [Message][3] etc. that says something like "x is a sum type that
can be any of the following", and then you'll see a list of strings and/or lua
tables just below.
Yes, they are actually sum types, i.e. they can be any of the given set of
tagged variants listed there.
Notice the word "be". Unlike classes or structs (aka product types), they can't
"have" values, they can only "be" the value, or rather, be one of the possible
set of values.
Also notice the word "tagged". Unlike the single variant `null`, or the dual
variant `boolean` types, the variants of sum types are tagged (i.e. named), and
may further have, or be, value or values of any data type.
A simple example of a sum type is an enum. Many programming languages have
them, but only a few modern programming languages allow nesting other types
into a sum type.
```rust
enum Color {
Red,
Green,
}
```
Here, `Color` can be one of two possible set of values: `Red` and `Green`, just
like `boolean`, but unlike `boolean`, being tagged allows `Color` to have more
than two variants if required, by changing the definition.
e.g.
```rust
enum Color {
Red,
Green,
Blue,
}
```
We'd document it here as:
> Result is a sum type that can be one of the following:
>
> - "Red"
> - "Green"
> - "Blue"
But some languages (like Rust, Haskell, Elm etc.) go even further, allowing us
to associate each branch of the enum with further nested types like:
```rust
enum Layout {
Table,
HelpMenu,
Horizontal {
config: LayoutConfig, // A product type (similar to class/struct)
splits: Vec<Layout> // A list of "Layout"s (i.e. list of sum types)
},
}
```
Here, as we can see, unlike the first example, some of `Layout`'s possible
variants can have further nested types associated with them. Note that
`Horizontal` here can have a sum type (e.g. enum), or a product type (e.g.
class/struct), or both (any number of them actually) nested in it. But the
nested values will only exist when `Layout` is `Horizontal`.
We'd document it here as:
> Layout is a sum type that can be one of the following:
>
> - "Table"
> - "HelpMenu"
> - { Horizontal = { config = Layout Config, splits = { Layout, ... } }
And then we'd go on documenting whatever `Layout Config` is.
So, there you go. This is exactly what sum types are - glorified enums that can
have nested types in each branch.
[1]: https://en.wikipedia.org/wiki/Tagged_union
[2]: layout.md
[3]: message.md
[4]: style.md#color

@ -45,7 +45,7 @@ compatibility.
### Instructions ### Instructions
#### [v0.20.2][48] -> [v0.21.1][49] #### [v0.20.2][48] -> [v0.21.9][49]
- Some plugins might stop rendering colors. Wait for them to update. - Some plugins might stop rendering colors. Wait for them to update.
- Rename `xplr.config.general.sort_and_filter_ui.search_identifier` to - Rename `xplr.config.general.sort_and_filter_ui.search_identifier` to
@ -81,7 +81,7 @@ compatibility.
- `:ss` to create softlink of the selected items. - `:ss` to create softlink of the selected items.
- `:sh` to create hardlink of the selected items. - `:sh` to create hardlink of the selected items.
- `:se` to edit selection list in your $EDITOR. - `:se` to edit selection list in your $EDITOR.
- Better conflict handling: add suffix rather than overriding/skipping. - Better conflict handling: prompt for action.
- Navigate between the selected paths using the following messages: - Navigate between the selected paths using the following messages:
- FocusPreviousSelection (`ctrl-p`) - FocusPreviousSelection (`ctrl-p`)
- FocusNextSelection (`ctrl-n`) - FocusNextSelection (`ctrl-n`)
@ -98,7 +98,7 @@ compatibility.
rendered: rendered:
- xplr.config.general.selection.item.format - xplr.config.general.selection.item.format
- xplr.config.general.selection.item.style - xplr.config.general.selection.item.style
- Use the following utility functions to work with teh file permissions: - Use the following utility functions to work with the file permissions:
- xplr.util.permissions_rwx - xplr.util.permissions_rwx
- xplr.util.permissions_octal - xplr.util.permissions_octal
- Type `:p` to edit file permissions interactively. - Type `:p` to edit file permissions interactively.
@ -118,6 +118,22 @@ compatibility.
- xplr.util.shell_escape - xplr.util.shell_escape
- Executables will me marked with the mime type: `application/x-executable`. - Executables will me marked with the mime type: `application/x-executable`.
- macOS legacy coreutils will be generally supported, but please update it. - macOS legacy coreutils will be generally supported, but please update it.
- Since v0.21.2 you can use the on_selection_change hook.
- Since v0.21.4 you can use function keys upto F24 and the following new
messages:
- NextVisitedDeepBranch (bound to `)` key)
- PreviousVisitedDeepBranch (bound to `(` key)
- Since v0.21.6:
- You can use `c` and `m` keys in default mode to quickly copy
and move focused or selected files, without having to change directory.
- Use `xplr.util.debug()` to debug lua values.
- Since v0.21.8:
- Scroll behavior will default to vim-like continuous scrolling. You can set
`xplr.config.general.paginated_scrolling = true` to revert back to the
paginated scrolling.
- Set `xplr.config.general.scroll_padding` to customize the scroll padding.
- The calculated `scrolltop` value will be passed as part of the
`Content Rendeder Argument` in `Dynamic` layout renderer functions.
Thanks to @noahmayr for contributing to a major part of this release. Thanks to @noahmayr for contributing to a major part of this release.
@ -205,8 +221,6 @@ Thanks to @noahmayr for contributing to a major part of this release.
- ScrollUpHalf ---- { - ScrollUpHalf ---- {
- ScrollDownHalf -- } - ScrollDownHalf -- }
<sub>Like this project so far? **[Please consider contributing][5]**.</sub>
#### [v0.17.6][45] -> [v0.18.0][46] #### [v0.17.6][45] -> [v0.18.0][46]
- Key binding `f` `r` and `f` `R` will now filter using regex. - Key binding `f` `r` and `f` `R` will now filter using regex.
@ -305,7 +319,7 @@ Thanks to @noahmayr for contributing to a major part of this release.
- You can disable the recover mode using `config.general.disable_recover_mode = true`. - You can disable the recover mode using `config.general.disable_recover_mode = true`.
- Try running `xplr --help`. Yes, CLI has been implemented. - Try running `xplr --help`. Yes, CLI has been implemented.
- Since version `v0.14.3`, `StartFifo` and `ToggleFifo` will write to the FIFO - Since version `v0.14.3`, `StartFifo` and `ToggleFifo` will write to the FIFO
path when called. So, there's no need to pipe the focus path explicitely. path when called. So, there's no need to pipe the focus path explicitly.
- Since version `v0.14.3`, general config `xplr.config.start_fifo` is available - Since version `v0.14.3`, general config `xplr.config.start_fifo` is available
which can be set to a file path to start a fifo when xplr starts. which can be set to a file path to start a fifo when xplr starts.
- Since version `v0.14.4`, `$XPLR_SESSION_PATH` can be used to dump session - Since version `v0.14.4`, `$XPLR_SESSION_PATH` can be used to dump session
@ -316,7 +330,7 @@ Thanks to @noahmayr for contributing to a major part of this release.
#### [v0.12.1][6] -> [v0.13.7][2] #### [v0.12.1][6] -> [v0.13.7][2]
- Lua functions called using [`CallLua`][7] and [`CallLuaSilently`][8] messages will receive [`CallLuaArg`][9] object as the function argument (instead of the [`App`][10] object). - Lua functions called using [`CallLua`][7] and [`CallLuaSilently`][8] messages will receive [`CallLuaArg`][9] object as the function argument (instead of the [`App`][10] object).
- Each `node_types` config will inherit defaults from matching less specifig `node_types` config and overwrite them. - Each `node_types` config will inherit defaults from matching less specific `node_types` config and overwrite them.
- Since version `v0.13.2`, you don't need to use/send `Refresh` anymore. It will be auto-handled by xplr. - Since version `v0.13.2`, you don't need to use/send `Refresh` anymore. It will be auto-handled by xplr.
#### [v0.11.1][11] -> [v0.12.1][6] #### [v0.11.1][11] -> [v0.12.1][6]
@ -472,7 +486,6 @@ Else do the following:
[2]: https://github.com/sayanarijit/xplr/releases/tag/v0.13.7 [2]: https://github.com/sayanarijit/xplr/releases/tag/v0.13.7
[3]: https://github.com/sayanarijit/xplr/releases/tag/v0.14.7 [3]: https://github.com/sayanarijit/xplr/releases/tag/v0.14.7
[4]: https://github.com/sayanarijit/xplr/pull/229#issue-662426960 [4]: https://github.com/sayanarijit/xplr/pull/229#issue-662426960
[5]: contribute.md
[6]: https://github.com/sayanarijit/xplr/releases/tag/v0.12.1 [6]: https://github.com/sayanarijit/xplr/releases/tag/v0.12.1
[7]: https://docs.rs/xplr/latest/xplr/app/enum.ExternalMsg.html#variant.CallLua [7]: https://docs.rs/xplr/latest/xplr/app/enum.ExternalMsg.html#variant.CallLua
[8]: https://docs.rs/xplr/latest/xplr/app/enum.ExternalMsg.html#variant.CallLuaSilently [8]: https://docs.rs/xplr/latest/xplr/app/enum.ExternalMsg.html#variant.CallLuaSilently
@ -516,5 +529,5 @@ Else do the following:
[46]: https://github.com/sayanarijit/xplr/releases/tag/v0.18.0 [46]: https://github.com/sayanarijit/xplr/releases/tag/v0.18.0
[47]: https://github.com/sayanarijit/xplr/releases/tag/v0.19.4 [47]: https://github.com/sayanarijit/xplr/releases/tag/v0.19.4
[48]: https://github.com/sayanarijit/xplr/releases/tag/v0.20.2 [48]: https://github.com/sayanarijit/xplr/releases/tag/v0.20.2
[49]: https://github.com/sayanarijit/xplr/releases/tag/v0.21.1 [49]: https://github.com/sayanarijit/xplr/releases/tag/v0.21.9
[50]: https://github.com/lotabout/skim#search-syntax [50]: https://github.com/lotabout/skim#search-syntax

@ -69,9 +69,7 @@ Visit [Awesome Plugins][5] for xplr plugin examples.
## Also See ## Also See
- [Tip: A list of hacks yet to make it as Lua plugins][15] - [Tip: A list of hacks yet to make it as Lua plugins][15]
- [Tip: Some UI and themeing tips][12] - [Tip: Some UI and theming tips][12]
- [Tip: A list of handy utility functions][13]
- [Tip: Share tips and tricks working with Lua][14]
- [Tutorial: Adding a New Mode][6] - [Tutorial: Adding a New Mode][6]
- [Example: Using Environment Variables and Pipes][7] - [Example: Using Environment Variables and Pipes][7]
- [Example: Using Lua Function Calls][8] - [Example: Using Lua Function Calls][8]
@ -92,7 +90,5 @@ Visit [Awesome Plugins][5] for xplr plugin examples.
[10]: column-renderer.md#example-customizing-table-renderer [10]: column-renderer.md#example-customizing-table-renderer
[11]: layout.md#example-render-a-custom-dynamic-table [11]: layout.md#example-render-a-custom-dynamic-table
[12]: https://github.com/sayanarijit/xplr/discussions/274 [12]: https://github.com/sayanarijit/xplr/discussions/274
[13]: https://github.com/sayanarijit/xplr/discussions/273 [15]: awesome-hacks.md
[14]: https://github.com/sayanarijit/xplr/discussions/250
[15]: https://github.com/sayanarijit/xplr/wiki/Hacks
[16]: https://github.com/sayanarijit/xplr/discussions/529#discussioncomment-4073734 [16]: https://github.com/sayanarijit/xplr/discussions/529#discussioncomment-4073734

@ -11,6 +11,23 @@ xplr.util.version()
-- { major = 0, minor = 0, patch = 0 } -- { major = 0, minor = 0, patch = 0 }
``` ```
### xplr.util.debug
Print the given value to the console, and return it as a string.
Useful for debugging.
Type: function( value ) -> string
Example:
```lua
xplr.util.debug({ foo = "bar", bar = function() end })
-- {
-- ["bar"] = function: 0x55e5cebdeae0,
-- ["foo"] = "bar",
-- }
```
### xplr.util.clone ### xplr.util.clone
Clone/deepcopy a Lua value. Doesn't work with functions. Clone/deepcopy a Lua value. Doesn't work with functions.
@ -286,7 +303,7 @@ Example:
```lua ```lua
xplr.util.shell_execute("pwd") xplr.util.shell_execute("pwd")
-- "/present/working/directory" -- { stdout = "/present/working/directory", stderr = "", returncode = 0 }
xplr.util.shell_execute("bash", {"-c", "xplr --help"}) xplr.util.shell_execute("bash", {"-c", "xplr --help"})
-- { stdout = "xplr...", stderr = "", returncode = 0 } -- { stdout = "xplr...", stderr = "", returncode = 0 }
@ -380,7 +397,7 @@ xplr.util.to_yaml({ foo = "bar" })
Get a [Style][3] object for the given path based on the LS_COLORS Get a [Style][3] object for the given path based on the LS_COLORS
environment variable. environment variable.
Type: function( path:string ) -> [Style][3]|nil Type: function( path:string ) -> [Style][3]
Example: Example:

@ -1,11 +1,11 @@
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}"> <html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
<head> <head>
<!-- Book generated using mdBook --> <!-- Book generated using mdBook -->
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ title }}</title> <title>{{ title }}</title>
{{#if is_print }} {{#if is_print }}
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex">
{{/if}} {{/if}}
{{#if base_url}} {{#if base_url}}
<base href="{{ base_url }}"> <base href="{{ base_url }}">
@ -15,10 +15,9 @@
<!-- Custom HTML head --> <!-- Custom HTML head -->
{{> head}} {{> head}}
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}"> <meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff">
{{#if favicon_svg}} {{#if favicon_svg}}
<link rel="icon" href="{{ path_to_root }}favicon.svg"> <link rel="icon" href="{{ path_to_root }}favicon.svg">
@ -51,21 +50,22 @@
{{#if mathjax_support}} {{#if mathjax_support}}
<!-- MathJax --> <!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}} {{/if}}
<!-- EthicalAds --> <!-- EthicalAds -->
<script async src="https://media.ethicalads.io/media/client/ethicalads.min.js"></script> <script async src="https://media.ethicalads.io/media/client/ethicalads.min.js"></script>
</head> </head>
<body> <body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript --> <!-- Provide site root to javascript -->
<script type="text/javascript"> <script>
var path_to_root = "{{ path_to_root }}"; var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}"; var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script> </script>
<!-- Work around some values being stored in localStorage wrapped in quotes --> <!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript"> <script>
try { try {
var theme = localStorage.getItem('mdbook-theme'); var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar'); var sidebar = localStorage.getItem('mdbook-sidebar');
@ -81,32 +81,38 @@
</script> </script>
<!-- Set the theme before any content is loaded, prevents flash --> <!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript"> <script>
var theme; var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { } try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; } if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html'); var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('{{ default_theme }}') html.classList.remove('{{ default_theme }}')
html.classList.add(theme); html.classList.add(theme);
html.classList.add('js'); var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script> </script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed --> <!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript"> <script>
var html = document.querySelector('html'); var body = document.querySelector('body');
var sidebar = 'hidden'; var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) { if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { } try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible'; sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
} }
html.classList.remove('sidebar-visible'); sidebar_toggle.checked = sidebar === 'visible';
html.classList.add("sidebar-" + sidebar); body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script> </script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents"> <nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox"> <div class="sidebar-scrollbox">
{{#toc}}{{/toc}} {{#toc}}{{/toc}}
<!-- EthicalAds --> <!-- EthicalAds -->
@ -116,30 +122,53 @@
data-ea-publisher="xplrdev" data-ea-publisher="xplrdev"
data-ea-type="image" data-ea-type="image"
></div> ></div>
</div> </div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div> <div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav> </nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper"> <div id="page-wrapper" class="page-wrapper">
<div class="page"> <div class="page">
{{> header}} {{> header}}
<div id="menu-bar-hover-placeholder"></div> <div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered"> <div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons"> <div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar"> <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
</button> </label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list"> <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i> <i class="fa fa-paint-brush"></i>
</button> </button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu"> <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li> <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul> </ul>
{{#if search_enabled}} {{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar"> <button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
@ -184,7 +213,7 @@
{{/if}} {{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM --> <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript"> <script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible'); document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible'); document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) { Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@ -206,7 +235,7 @@
{{/previous}} {{/previous}}
{{#next}} {{#next}}
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right"> <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
{{/next}} {{/next}}
@ -224,7 +253,7 @@
{{/previous}} {{/previous}}
{{#next}} {{#next}}
<a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right"> <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
{{/next}} {{/next}}
@ -232,10 +261,12 @@
</div> </div>
{{#if livereload}} {{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) --> <!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript"> <script>
var socket = new WebSocket("{{{livereload}}}"); const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) { socket.onmessage = function (event) {
if (event.data === "reload") { if (event.data === "reload") {
socket.close(); socket.close();
@ -251,7 +282,7 @@
{{#if google_analytics}} {{#if google_analytics}}
<!-- Google Analytics Tag --> <!-- Google Analytics Tag -->
<script type="text/javascript"> <script>
var localAddrs = ["localhost", "127.0.0.1", ""]; var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is // make sure we don't activate google analytics if the developer is
@ -269,43 +300,43 @@
{{/if}} {{/if}}
{{#if playground_line_numbers}} {{#if playground_line_numbers}}
<script type="text/javascript"> <script>
window.playground_line_numbers = true; window.playground_line_numbers = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_copyable}} {{#if playground_copyable}}
<script type="text/javascript"> <script>
window.playground_copyable = true; window.playground_copyable = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_js}} {{#if playground_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}ace.js"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}editor.js"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mode-rust.js"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}theme-dawn.js"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
{{/if}} {{/if}}
{{#if search_js}} {{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}elasticlunr.min.js"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mark.min.js"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}searcher.js"></script>
{{/if}} {{/if}}
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}clipboard.min.js"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}highlight.js"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}book.js"></script>
<!-- Custom JS scripts --> <!-- Custom JS scripts -->
{{#each additional_js}} {{#each additional_js}}
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script> <script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}} {{/each}}
{{#if is_print}} {{#if is_print}}
{{#if mathjax_support}} {{#if mathjax_support}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() { MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
@ -313,7 +344,7 @@
}); });
</script> </script>
{{else}} {{else}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
}); });
@ -321,5 +352,6 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
</div>
</body> </body>
</html> </html>

@ -151,7 +151,7 @@
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link page-scroll" class="nav-link page-scroll"
href="https://xplr.stck.me" href="https://blog.xplr.dev"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >

@ -0,0 +1,11 @@
v="0.4.40"
curl -L https://github.com/rust-lang/mdBook/releases/download/v$v/mdbook-v$v-x86_64-unknown-linux-gnu.tar.gz -o mdbook.tgz \
&& tar xzvf mdbook.tgz \
&& ./mdbook build docs/en \
&& mkdir dist \
&& mv -v docs/en/book/html dist/en \
&& mv -v assets dist \
&& mv -v docs/landing/index.html docs/landing/css docs/landing/js dist \
&& rm -v mdbook \
&& rm -v mdbook.tgz

@ -1,97 +1,12 @@
{ {
"nodes": { "nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"lowdown-src": {
"flake": false,
"locked": {
"lastModified": 1633514407,
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
"owner": "kristapsdz",
"repo": "lowdown",
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
"type": "github"
},
"original": {
"owner": "kristapsdz",
"repo": "lowdown",
"type": "github"
}
},
"nix": {
"inputs": {
"lowdown-src": "lowdown-src",
"nixpkgs": "nixpkgs",
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1667849540,
"narHash": "sha256-kqo8PTE3i21Z0pnxJwcwCQRZKKC15Y8xFTEN6qko3M8=",
"owner": "domenkozar",
"repo": "nix",
"rev": "3dcbdd412592413f34552a50403daf58656d43e6",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "relaxed-flakes",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1657693803, "lastModified": 1704262187,
"narHash": "sha256-G++2CJ9u0E7NNTAi9n5G8TdDmGJXcIjkJ3NF8cetQB8=", "narHash": "sha256-N4j9qghlp/Eb3Z11WF7Cb9U91AXwpascUbLH7YKMcLc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "365e1b3a859281cf11b94f87231adeabbdd878a2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.05-small",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1668199800,
"narHash": "sha256-qnEeVo88H8an0Wds9kBSxWlGCa0vY5EgEswnQUZnXmc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "98bb201bbfba99003e6cd5f85c13c46050fb8ee8", "rev": "65f0d241783c94a08e4c9a3870736fc8854dd520",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -102,9 +17,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "nixpkgs": "nixpkgs"
"nix": "nix",
"nixpkgs": "nixpkgs_2"
} }
} }
}, },

@ -3,30 +3,26 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs"; nixpkgs.url = "github:nixos/nixpkgs";
nix.url = "github:domenkozar/nix/relaxed-flakes";
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
}; };
outputs = { self, nixpkgs, nix, ... }: outputs = inputs@{ self, nixpkgs, ... }:
let let
systems = [ lib = nixpkgs.lib;
"x86_64-linux"
"i686-linux" darwin = [ "x86_64-darwin" "aarch64-darwin" ];
"x86_64-darwin" linux = [ "x86_64-linux" "x86_64-linux-musl" "aarch64-linux" "aarch64-linux-android" "i86_64-linux" ];
"aarch64-linux" allSystems = darwin ++ linux;
"aarch64-darwin"
]; forEachSystem = systems: f: lib.genAttrs systems (system: f system);
forAllSystems = f: builtins.listToAttrs (map (name: { inherit name; value = f name; }) systems); forAllSystems = forEachSystem allSystems;
in in
{ {
packages = forAllSystems (system: packages = forAllSystems (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
in in
{ rec {
# e.g. nix build .#xplr
xplr = pkgs.rustPlatform.buildRustPackage rec { xplr = pkgs.rustPlatform.buildRustPackage rec {
name = "xplr"; name = "xplr";
src = ./.; src = ./.;
@ -34,6 +30,14 @@
lockFile = ./Cargo.lock; lockFile = ./Cargo.lock;
}; };
}; };
# e.g. nix build .#cross.x86_64-linux-musl.xplr --impure
cross = forEachSystem (lib.filter (sys: sys != system) allSystems) (targetSystem:
let
crossPkgs = import nixpkgs { localSystem = system; crossSystem = targetSystem; };
in
{ inherit (crossPkgs) xplr; }
);
} }
); );
defaultPackage = forAllSystems (system: self.packages.${system}.xplr); defaultPackage = forAllSystems (system: self.packages.${system}.xplr);
@ -54,6 +58,9 @@
default = pkgs.mkShell { default = pkgs.mkShell {
RUST_BACKTRACE = 1; RUST_BACKTRACE = 1;
# For cross compilation
NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM = 1;
buildInputs = devRequirements; buildInputs = devRequirements;
packages = devRequirements; packages = devRequirements;
}; };

@ -0,0 +1,26 @@
name: xplr
version: git
summary: A hackable, minimal, fast TUI file explorer
description: |
xplr is a terminal UI based file explorer
that aims to increase our terminal productivity by being a flexible,
interactive orchestrator for the ever growing awesome command-line
utilities that work with the file-system.
source-code: https://github.com/sayanarijit/xplr
issues: https://github.com/sayanarijit/xplr/issues
website: https://xplr.dev/
base: core20
grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots
parts:
xplr:
plugin: rust
source: .
apps:
xplr:
command: bin/xplr

@ -2,6 +2,7 @@ use crate::config::Config;
use crate::config::Hooks; use crate::config::Hooks;
use crate::config::Mode; use crate::config::Mode;
pub use crate::directory_buffer::DirectoryBuffer; pub use crate::directory_buffer::DirectoryBuffer;
use crate::dirs;
use crate::explorer; use crate::explorer;
use crate::input::{InputOperation, Key}; use crate::input::{InputOperation, Key};
use crate::lua; use crate::lua;
@ -134,6 +135,10 @@ impl History {
self self
} }
fn peek(&self) -> Option<&String> {
self.paths.get(self.loc)
}
fn push(mut self, path: String) -> Self { fn push(mut self, path: String) -> Self {
if self.peek() != Some(&path) { if self.peek() != Some(&path) {
self.paths = self.paths.into_iter().take(self.loc + 1).collect(); self.paths = self.paths.into_iter().take(self.loc + 1).collect();
@ -166,8 +171,47 @@ impl History {
self.cleanup() self.cleanup()
} }
fn peek(&self) -> Option<&String> { fn _is_deepest_dir(&self, path: &str) -> bool {
self.paths.get(self.loc) return !self
.paths
.iter()
.any(|p| p.ends_with('/') && p.starts_with(path) && path != p);
}
fn _uniq_deep_dirs(&self) -> IndexSet<String> {
self.paths
.clone()
.into_iter()
.filter(|p| p.ends_with('/') && self._is_deepest_dir(p))
.collect::<IndexSet<String>>()
}
fn visit_next_deep_branch(self, pwd: &str) -> Self {
let uniq_deep_dirs = self._uniq_deep_dirs();
if let Some(path) = uniq_deep_dirs
.iter()
.skip_while(|p| p.trim_end_matches('/') != pwd)
.nth(1)
{
self.push(path.to_string())
} else {
self
}
}
fn visit_previous_deep_branch(self, pwd: &str) -> Self {
let uniq_deep_dirs = self._uniq_deep_dirs();
if let Some(path) = uniq_deep_dirs
.iter()
.rev()
.skip_while(|p| p.trim_end_matches('/') != pwd)
.nth(1)
{
self.push(path.to_string())
} else {
self
}
} }
} }
@ -254,8 +298,8 @@ impl App {
let config_file = if let Some(path) = config_file { let config_file = if let Some(path) = config_file {
Some(path) Some(path)
} else if let Some(dir) = dirs::home_dir() { } else if let Some(dir) = dirs::config_dir() {
let path = dir.join(".config/xplr/init.lua"); let path = dir.join("xplr/init.lua");
if path.exists() { if path.exists() {
Some(path) Some(path)
} else { } else {
@ -270,9 +314,7 @@ impl App {
} }
}; };
let config_files = config_file let config_files = config_file.into_iter().chain(extra_config_files);
.into_iter()
.chain(extra_config_files.into_iter());
let mut load_errs = vec![]; let mut load_errs = vec![];
for config_file in config_files { for config_file in config_files {
@ -293,12 +335,12 @@ impl App {
&config &config
.general .general
.initial_mode .initial_mode
.to_owned() .clone()
.unwrap_or_else(|| "default".into()), .unwrap_or_else(|| "default".into()),
) { ) {
Some(m) => m.clone().sanitized( Some(m) => m.clone().sanitized(
config.general.read_only, config.general.read_only,
config.general.global_key_bindings.to_owned(), config.general.global_key_bindings.clone(),
), ),
None => { None => {
bail!("'default' mode is missing") bail!("'default' mode is missing")
@ -309,7 +351,7 @@ impl App {
&config &config
.general .general
.initial_layout .initial_layout
.to_owned() .clone()
.unwrap_or_else(|| "default".into()), .unwrap_or_else(|| "default".into()),
) { ) {
Some(l) => l.clone(), Some(l) => l.clone(),
@ -320,7 +362,6 @@ impl App {
let pid = std::process::id(); let pid = std::process::id();
let mut session_path = dirs::runtime_dir() let mut session_path = dirs::runtime_dir()
.unwrap_or_else(env::temp_dir)
.join("xplr") .join("xplr")
.join("session") .join("session")
.join(pid.to_string()) .join(pid.to_string())
@ -346,7 +387,7 @@ impl App {
} }
if let Some(sorters) = &config.general.initial_sorting { if let Some(sorters) = &config.general.initial_sorting {
explorer_config.sorters = sorters.clone(); explorer_config.sorters.clone_from(sorters);
}; };
let hostname = gethostname().to_string_lossy().to_string(); let hostname = gethostname().to_string_lossy().to_string();
@ -373,6 +414,12 @@ impl App {
prompt: config.general.prompt.format.clone().unwrap_or_default(), prompt: config.general.prompt.format.clone().unwrap_or_default(),
}; };
let hist = if &pwd == "/" {
pwd.clone()
} else {
format!("{0}/", &pwd)
};
let mut app = Self { let mut app = Self {
bin, bin,
version: VERSION.to_string(), version: VERSION.to_string(),
@ -394,7 +441,7 @@ impl App {
explorer_config, explorer_config,
logs: Default::default(), logs: Default::default(),
logs_hidden: Default::default(), logs_hidden: Default::default(),
history: Default::default(), history: History::default().push(hist),
last_modes: Default::default(), last_modes: Default::default(),
hostname, hostname,
hooks, hooks,
@ -424,11 +471,6 @@ impl App {
.unwrap_or_default() .unwrap_or_default()
} }
fn enqueue(mut self, task: Task) -> Self {
self.msg_out.push_back(MsgOut::Enque(task));
self
}
pub fn handle_batch_external_msgs(mut self, msgs: Vec<ExternalMsg>) -> Result<Self> { pub fn handle_batch_external_msgs(mut self, msgs: Vec<ExternalMsg>) -> Result<Self> {
for task in msgs for task in msgs
.into_iter() .into_iter()
@ -457,15 +499,23 @@ impl App {
self.add_last_focus(parent, focus_path) self.add_last_focus(parent, focus_path)
} }
InternalMsg::HandleKey(key) => self.handle_key(key), InternalMsg::HandleKey(key) => self.handle_key(key),
InternalMsg::RefreshSelection => self.refresh_selection(),
} }
} }
fn handle_external(self, msg: ExternalMsg, key: Option<Key>) -> Result<Self> { fn handle_external(mut self, msg: ExternalMsg, key: Option<Key>) -> Result<Self> {
if self.config.general.read_only && !msg.is_read_only() { let is_msg_read_only = msg.is_read_only();
if self.config.general.read_only && !is_msg_read_only {
self.log_error("could not execute code in read-only mode.".into()) self.log_error("could not execute code in read-only mode.".into())
} else { } else {
use ExternalMsg::*; use ExternalMsg::*;
match msg {
if !is_msg_read_only {
// We don't want to operate on imaginary paths.
self = self.refresh_selection()?;
}
self = match msg {
ExplorePwd => self.explore_pwd(), ExplorePwd => self.explore_pwd(),
ExploreParentsAsync => self.explore_parents_async(), ExploreParentsAsync => self.explore_parents_async(),
ExplorePwdAsync => self.explore_pwd_async(), ExplorePwdAsync => self.explore_pwd_async(),
@ -502,6 +552,8 @@ impl App {
Back => self.back(), Back => self.back(),
LastVisitedPath => self.last_visited_path(), LastVisitedPath => self.last_visited_path(),
NextVisitedPath => self.next_visited_path(), NextVisitedPath => self.next_visited_path(),
PreviousVisitedDeepBranch => self.previous_visited_deep_branch(),
NextVisitedDeepBranch => self.next_visited_deep_branch(),
FollowSymlink => self.follow_symlink(), FollowSymlink => self.follow_symlink(),
SetVroot(p) => self.set_vroot(&p), SetVroot(p) => self.set_vroot(&p),
UnsetVroot => self.unset_vroot(), UnsetVroot => self.unset_vroot(),
@ -619,9 +671,20 @@ impl App {
PrintAppStateAndQuit => self.print_app_state_and_quit(), PrintAppStateAndQuit => self.print_app_state_and_quit(),
Debug(path) => self.debug(path), Debug(path) => self.debug(path),
Terminate => bail!(""), Terminate => bail!(""),
}?;
if !is_msg_read_only {
// We don't want to keep imaginary paths in the selection.
// But the write action is probably still in queue.
// So we need to refresh selection after the write action.
let msg = InternalMsg::RefreshSelection;
let msg = MsgIn::Internal(msg);
let task = Task::new(msg, None);
self.msg_out.push_back(MsgOut::Enqueue(task));
} }
}?
.refresh_selection() Ok(self)
}
} }
fn handle_key(mut self, key: Key) -> Result<Self> { fn handle_key(mut self, key: Key) -> Result<Self> {
@ -670,7 +733,11 @@ impl App {
}); });
for msg in msgs { for msg in msgs {
self = self.enqueue(Task::new(MsgIn::External(msg), Some(key))); // Rename breaks without enqueue
let external = MsgIn::External(msg);
let task = Task::new(external, Some(key));
let msg_out = MsgOut::Enqueue(task);
self.msg_out.push_back(msg_out);
} }
Ok(self) Ok(self)
@ -753,7 +820,6 @@ impl App {
fn focus_previous(mut self) -> Result<Self> { fn focus_previous(mut self) -> Result<Self> {
let bounded = self.config.general.enforce_bounded_index_navigation; let bounded = self.config.general.enforce_bounded_index_navigation;
if let Some(dir) = self.directory_buffer_mut() { if let Some(dir) = self.directory_buffer_mut() {
dir.focus = if dir.focus == 0 { dir.focus = if dir.focus == 0 {
if bounded { if bounded {
@ -838,7 +904,6 @@ impl App {
fn focus_next(mut self) -> Result<Self> { fn focus_next(mut self) -> Result<Self> {
let bounded = self.config.general.enforce_bounded_index_navigation; let bounded = self.config.general.enforce_bounded_index_navigation;
if let Some(dir) = self.directory_buffer_mut() { if let Some(dir) = self.directory_buffer_mut() {
dir.focus = if (dir.focus + 1) == dir.total { dir.focus = if (dir.focus + 1) == dir.total {
if bounded { if bounded {
@ -850,6 +915,7 @@ impl App {
dir.focus + 1 dir.focus + 1
} }
}; };
Ok(self) Ok(self)
} }
@ -928,7 +994,7 @@ impl App {
fn follow_symlink(self) -> Result<Self> { fn follow_symlink(self) -> Result<Self> {
if let Some(pth) = self if let Some(pth) = self
.focused_node() .focused_node()
.and_then(|n| n.symlink.to_owned().map(|s| s.absolute_path)) .and_then(|n| n.symlink.clone().map(|s| s.absolute_path))
{ {
self.focus_path(&pth, true) self.focus_path(&pth, true)
} else { } else {
@ -1016,19 +1082,23 @@ impl App {
} }
fn enter(self) -> Result<Self> { fn enter(self) -> Result<Self> {
if let Some(path) = self.focused_node().map(|n| n.absolute_path.clone()) { if let Some(node) = self.focused_node() {
self.change_directory(&path, true) if node.is_dir || node.symlink.as_ref().map(|s| s.is_dir).unwrap_or(false) {
let path = node.absolute_path.clone();
self.change_directory(&path, true)
} else {
Ok(self)
}
} else { } else {
Ok(self) Ok(self)
} }
} }
fn back(self) -> Result<Self> { fn back(self) -> Result<Self> {
if let Some(p) = PathBuf::from(self.pwd.clone()) let pwd = self.pwd.clone();
.parent() if let Some(p) = PathBuf::from(&pwd).parent().and_then(|p| p.to_str()) {
.and_then(|p| p.to_str()) self.change_directory(p, false)
{ .and_then(|a| a.focus_path(&pwd, true))
self.change_directory(p, true)
} else { } else {
Ok(self) Ok(self)
} }
@ -1060,6 +1130,24 @@ impl App {
} }
} }
fn previous_visited_deep_branch(mut self) -> Result<Self> {
self.history = self.history.visit_previous_deep_branch(&self.pwd);
if let Some(path) = self.history.peek().cloned() {
self.change_directory(path.trim_end_matches('/'), false)
} else {
Ok(self)
}
}
fn next_visited_deep_branch(mut self) -> Result<Self> {
self.history = self.history.visit_next_deep_branch(&self.pwd);
if let Some(path) = self.history.peek().cloned() {
self.change_directory(path.trim_end_matches('/'), false)
} else {
Ok(self)
}
}
fn set_input_prompt(mut self, p: String) -> Result<Self> { fn set_input_prompt(mut self, p: String) -> Result<Self> {
self.input.prompt = p; self.input.prompt = p;
Ok(self) Ok(self)
@ -1212,7 +1300,7 @@ impl App {
} }
pub fn scroll_up_half(mut self) -> Result<Self> { pub fn scroll_up_half(mut self) -> Result<Self> {
self.msg_out.push_back(MsgOut::ScrollUp); self.msg_out.push_back(MsgOut::ScrollUpHalf);
Ok(self) Ok(self)
} }
@ -1293,7 +1381,7 @@ impl App {
self = self.push_mode(); self = self.push_mode();
self.mode = mode.sanitized( self.mode = mode.sanitized(
self.config.general.read_only, self.config.general.read_only,
self.config.general.global_key_bindings.to_owned(), self.config.general.global_key_bindings.clone(),
); );
// Hooks // Hooks
@ -1318,7 +1406,7 @@ impl App {
self = self.push_mode(); self = self.push_mode();
self.mode = mode.sanitized( self.mode = mode.sanitized(
self.config.general.read_only, self.config.general.read_only,
self.config.general.global_key_bindings.to_owned(), self.config.general.global_key_bindings.clone(),
); );
// Hooks // Hooks
@ -1345,7 +1433,7 @@ impl App {
fn switch_layout_builtin(mut self, layout: &str) -> Result<Self> { fn switch_layout_builtin(mut self, layout: &str) -> Result<Self> {
if let Some(l) = self.config.layouts.builtin.get(layout) { if let Some(l) = self.config.layouts.builtin.get(layout) {
self.layout = l.to_owned(); self.layout = l.clone();
// Hooks // Hooks
if !self.hooks.on_layout_switch.is_empty() { if !self.hooks.on_layout_switch.is_empty() {
@ -1361,7 +1449,7 @@ impl App {
fn switch_layout_custom(mut self, layout: &str) -> Result<Self> { fn switch_layout_custom(mut self, layout: &str) -> Result<Self> {
if let Some(l) = self.config.layouts.get_custom(layout) { if let Some(l) = self.config.layouts.get_custom(layout) {
self.layout = l.to_owned(); self.layout = l.clone();
// Hooks // Hooks
if !self.hooks.on_layout_switch.is_empty() { if !self.hooks.on_layout_switch.is_empty() {
@ -1468,6 +1556,8 @@ impl App {
if dir.parent == self.pwd { if dir.parent == self.pwd {
self.directory_buffer = Some(dir); self.directory_buffer = Some(dir);
// Might as well refresh the selection
self = self.refresh_selection()?;
}; };
Ok(self) Ok(self)
@ -1483,9 +1573,15 @@ impl App {
} }
pub fn select(mut self) -> Result<Self> { pub fn select(mut self) -> Result<Self> {
if let Some(n) = self.focused_node().map(|n| n.to_owned()) { let count = self.selection.len();
if let Some(n) = self.focused_node().cloned() {
self.selection.insert(n); self.selection.insert(n);
} }
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
@ -1493,36 +1589,59 @@ impl App {
let path = PathBuf::from(path).absolutize()?.to_path_buf(); let path = PathBuf::from(path).absolutize()?.to_path_buf();
let parent = path.parent().map(|p| p.to_string_lossy().to_string()); let parent = path.parent().map(|p| p.to_string_lossy().to_string());
let filename = path.file_name().map(|p| p.to_string_lossy().to_string()); let filename = path.file_name().map(|p| p.to_string_lossy().to_string());
let count = self.selection.len();
if let (Some(p), Some(n)) = (parent, filename) { if let (Some(p), Some(n)) = (parent, filename) {
self.selection.insert(Node::new(p, n)); self.selection.insert(Node::new(p, n));
} }
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
pub fn select_all(mut self) -> Result<Self> { pub fn select_all(mut self) -> Result<Self> {
let count = self.selection.len();
if let Some(d) = self.directory_buffer.as_ref() { if let Some(d) = self.directory_buffer.as_ref() {
self.selection.extend(d.nodes.clone()); self.selection.extend(d.nodes.clone());
}; };
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
pub fn un_select_path(mut self, path: String) -> Result<Self> { pub fn un_select_path(mut self, path: String) -> Result<Self> {
let pathbuf = PathBuf::from(path).absolutize()?.to_path_buf(); let pathbuf = PathBuf::from(path).absolutize()?.to_path_buf();
let count = self.selection.len();
self.selection self.selection
.retain(|n| PathBuf::from(&n.absolute_path) != pathbuf); .retain(|n| PathBuf::from(&n.absolute_path) != pathbuf);
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
pub fn un_select(mut self) -> Result<Self> { pub fn un_select(mut self) -> Result<Self> {
if let Some(n) = self.focused_node().map(|n| n.to_owned()) { let count = self.selection.len();
if let Some(n) = self.focused_node().cloned() {
self.selection self.selection
.retain(|s| s.absolute_path != n.absolute_path); .retain(|s| s.absolute_path != n.absolute_path);
} }
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
pub fn un_select_all(mut self) -> Result<Self> { pub fn un_select_all(mut self) -> Result<Self> {
let count = self.selection.len();
if let Some(d) = self.directory_buffer.as_ref() { if let Some(d) = self.directory_buffer.as_ref() {
d.nodes.clone().into_iter().for_each(|n| { d.nodes.clone().into_iter().for_each(|n| {
self.selection self.selection
@ -1530,6 +1649,10 @@ impl App {
}); });
}; };
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
@ -1567,7 +1690,11 @@ impl App {
} }
fn clear_selection(mut self) -> Result<Self> { fn clear_selection(mut self) -> Result<Self> {
let count = self.selection.len();
self.selection.clear(); self.selection.clear();
if self.selection.len() != count {
self = self.on_selection_change()?;
}
Ok(self) Ok(self)
} }
@ -1676,7 +1803,7 @@ impl App {
.config .config
.general .general
.initial_sorting .initial_sorting
.to_owned() .clone()
.unwrap_or_default(); .unwrap_or_default();
Ok(self) Ok(self)
} }
@ -1898,12 +2025,28 @@ impl App {
format!("{0}\n", &self.mode.name) format!("{0}\n", &self.mode.name)
} }
// This is a performance heavy function. Use it only when necessary.
fn refresh_selection(mut self) -> Result<Self> { fn refresh_selection(mut self) -> Result<Self> {
let count = self.selection.len();
self.selection.retain(|n| { self.selection.retain(|n| {
let p = PathBuf::from(&n.absolute_path); let p = PathBuf::from(&n.absolute_path);
// Should be able to retain broken symlink // Should be able to retain broken symlink
p.exists() || p.symlink_metadata().is_ok() p.exists() || p.symlink_metadata().is_ok()
}); });
if count != self.selection.len() {
self = self.on_selection_change()?;
}
Ok(self)
}
fn on_selection_change(mut self) -> Result<Self> {
if !self.hooks.on_selection_change.is_empty() {
let msgs = self.hooks.on_selection_change.clone();
self = self.handle_batch_external_msgs(msgs)?
}
Ok(self) Ok(self)
} }
@ -1962,8 +2105,10 @@ impl App {
let read_only = self.config.general.read_only; let read_only = self.config.general.read_only;
let global_kb = &self.config.general.global_key_bindings; let global_kb = &self.config.general.global_key_bindings;
builtin.into_iter() let modes = builtin.into_iter().chain(custom);
.chain(custom.into_iter())
std::iter::once((self.mode.name.clone(), self.mode.clone()))
.chain(modes)
.map(|(name, mode)| { .map(|(name, mode)| {
(name, mode.sanitized(read_only, global_kb.clone())) (name, mode.sanitized(read_only, global_kb.clone()))
}) })

@ -15,32 +15,32 @@ fn main() {
xplr [FLAG]... [OPTION]... [PATH] [SELECTION]..."###; xplr [FLAG]... [OPTION]... [PATH] [SELECTION]..."###;
let flags = r###" let flags = r###"
- Reads new-line (\n) separated paths from stdin - Reads new-line (\n) separated paths from stdin
-- Denotes the end of command-line flags and options -- Denotes the end of command-line flags and options
--force-focus Focuses on the given <PATH>, even if it is a directory --force-focus Focuses on the given <PATH>, even if it is a directory
-h, --help Prints help information -h, --help Prints help information
-m, --pipe-msg-in Helps safely passing messages to the active xplr -m, --pipe-msg-in Helps safely passing messages to the active xplr
session, use %%, %s and %q as the placeholders session, use %%, %s and %q as the placeholders
-M, --print-msg-in Like --pipe-msg-in, but prints the message instead of -M, --print-msg-in Like --pipe-msg-in, but prints the message instead of
passing to the active xplr session passing to the active xplr session
--print-pwd-as-result Prints the present working directory when quitting --print-pwd-as-result Prints the present working directory when quitting
with `PrintResultAndQuit` with `PrintResultAndQuit`
--read-only Enables read-only mode (config.general.read_only) --read-only Enables read-only mode (config.general.read_only)
--read0 Reads paths separated using the null character (\0) --read0 Reads paths separated using the null character (\0)
--write0 Prints paths separated using the null character (\0) --write0 Prints paths separated using the null character (\0)
-0 --null Combines --read0 and --write0 -0 --null Combines --read0 and --write0
-V, --version Prints version information"###; -V, --version Prints version information"###;
let options = r###" let options = r###"
-c, --config <PATH> Specifies a custom config file (default is -c, --config <PATH> Specifies a custom config file (default is
"$HOME/.config/xplr/init.lua") "$HOME/.config/xplr/init.lua")
-C, --extra-config <PATH>... Specifies extra config files to load -C, --extra-config <PATH>... Specifies extra config files to load
--on-load <MESSAGE>... Sends messages when xplr loads --on-load <MESSAGE>... Sends messages when xplr loads
--vroot <PATH> Treats the specified path as the virtual root"###; --vroot <PATH> Treats the specified path as the virtual root"###;
let args = r###" let args = r###"
<PATH> Path to focus on, or enter if directory, (default is `.`) <PATH> Path to focus on, or enter if directory, (default is `.`)
<SELECTION>... Paths to select, requires <PATH> to be set explicitly"###; <SELECTION>... Paths to select, requires <PATH> to be set explicitly"###;
let help = format!( let help = format!(
"xplr {}\n{}\n{}\n\nUSAGE:{}\n\nFLAGS:{}\n\nOPTIONS:{}\n\nARGS:{}", "xplr {}\n{}\n{}\n\nUSAGE:{}\n\nFLAGS:{}\n\nOPTIONS:{}\n\nARGS:{}",

@ -185,7 +185,7 @@ pub fn pipe_msg_in(args: Vec<String>) -> Result<()> {
.cloned() .cloned()
.context("failed to detect delimmiter")?; .context("failed to detect delimmiter")?;
msg.push(delimiter.try_into()?); msg.push(delimiter.into());
File::options() File::options()
.append(true) .append(true)
.open(&path)? .open(&path)?
@ -204,62 +204,16 @@ pub fn print_msg_in(args: Vec<String>) -> Result<()> {
} }
fn fmt_msg_in(args: Vec<String>) -> Result<String> { fn fmt_msg_in(args: Vec<String>) -> Result<String> {
let mut args = args.into_iter(); let msg = match jf::format(args.into_iter().map(Into::into)) {
let format = args.next().context("FORMAT is missing")?; Ok(msg) => msg,
let mut msg = "".to_string(); Err(jf::Error::Jf(e)) => bail!("xplr -m: {e}"),
let mut last_char = None; Err(jf::Error::Json(e)) => bail!("xplr -m: json: {e}"),
Err(jf::Error::Yaml(e)) => bail!("xplr -m: yaml: {e}"),
for (col, ch) in format.chars().enumerate() { Err(jf::Error::Io(e)) => bail!("xplr -m: io: {e}"),
match (ch, last_char) {
('%', Some('%')) => {
msg.push(ch);
last_char = None;
}
('%', _) => {
last_char = Some(ch);
}
('q', Some('%')) => {
let arg = args.next().context(format!(
"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}",
))?;
msg.push_str(&arg);
last_char = None;
}
(ch, Some('%')) => {
bail!(format!(
"invalid placeholder '%{ch}' at column {col}, use one of '%s' or '%q', or escape it using '%%'",
));
}
(ch, _) => {
msg.push(ch);
last_char = Some(ch);
}
}
}
if last_char == Some('%') {
bail!("message ended with incomplete placeholder");
}
if args.count() != 0 {
bail!("too many arguments, not enough placeholders")
}
// Since we'll mostly by passing json using `-m`, and json is faster than yaml
// let's try to validate using json first.
let msg = if let Ok(val) = json::from_str::<ExternalMsg>(&msg) {
json::to_string(&val)?
} else {
let val: ExternalMsg = yaml::from_str(&msg)?;
json::to_string(&val)?
}; };
// validate
let _: ExternalMsg = json::from_str(&msg)?;
Ok(msg) Ok(msg)
} }

@ -7,9 +7,8 @@ use crate::ui::block;
use crate::ui::string_to_text; use crate::ui::string_to_text;
use crate::ui::Constraint; use crate::ui::Constraint;
use crate::ui::ContentRendererArg; use crate::ui::ContentRendererArg;
use mlua::Lua; use crate::ui::UI;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tui::backend::Backend;
use tui::layout::Constraint as TuiConstraint; use tui::layout::Constraint as TuiConstraint;
use tui::layout::Rect as TuiRect; use tui::layout::Rect as TuiRect;
use tui::widgets::Cell; use tui::widgets::Cell;
@ -60,13 +59,12 @@ pub struct CustomContent {
} }
/// A cursed function from crate::ui. /// A cursed function from crate::ui.
pub fn draw_custom_content<B: Backend>( pub fn draw_custom_content(
f: &mut Frame<B>, ui: &mut UI,
screen_size: TuiRect, f: &mut Frame,
layout_size: TuiRect, layout_size: TuiRect,
app: &app::App, app: &app::App,
content: CustomContent, content: CustomContent,
lua: &Lua,
) { ) {
let config = app.config.general.panel_ui.default.clone(); let config = app.config.general.panel_ui.default.clone();
let title = content.title; let title = content.title;
@ -86,12 +84,13 @@ pub fn draw_custom_content<B: Backend>(
let ctx = ContentRendererArg { let ctx = ContentRendererArg {
app: app.to_lua_ctx_light(), app: app.to_lua_ctx_light(),
layout_size: layout_size.into(), layout_size: layout_size.into(),
screen_size: screen_size.into(), screen_size: ui.screen_size.into(),
scrolltop: ui.scrolltop as u16,
}; };
let render = lua::serialize(lua, &ctx) let render = lua::serialize(ui.lua, &ctx)
.map(|arg| { .map(|arg| {
lua::call(lua, &render, arg).unwrap_or_else(|e| format!("{e:?}")) lua::call(ui.lua, &render, arg).unwrap_or_else(|e| format!("{e:?}"))
}) })
.unwrap_or_else(|e| e.to_string()); .unwrap_or_else(|e| e.to_string());
@ -122,12 +121,13 @@ pub fn draw_custom_content<B: Backend>(
let ctx = ContentRendererArg { let ctx = ContentRendererArg {
app: app.to_lua_ctx_light(), app: app.to_lua_ctx_light(),
layout_size: layout_size.into(), layout_size: layout_size.into(),
screen_size: screen_size.into(), screen_size: ui.screen_size.into(),
scrolltop: ui.scrolltop as u16,
}; };
let items = lua::serialize(lua, &ctx) let items = lua::serialize(ui.lua, &ctx)
.map(|arg| { .map(|arg| {
lua::call(lua, &render, arg) lua::call(ui.lua, &render, arg)
.unwrap_or_else(|e| vec![format!("{e:?}")]) .unwrap_or_else(|e| vec![format!("{e:?}")])
}) })
.unwrap_or_else(|e| vec![e.to_string()]) .unwrap_or_else(|e| vec![e.to_string()])
@ -162,11 +162,10 @@ pub fn draw_custom_content<B: Backend>(
let widths = widths let widths = widths
.into_iter() .into_iter()
.map(|w| w.to_tui(screen_size, layout_size)) .map(|w| w.to_tui(ui.screen_size, layout_size))
.collect::<Vec<TuiConstraint>>(); .collect::<Vec<TuiConstraint>>();
let content = Table::new(rows) let content = Table::new(rows, widths)
.widths(&widths)
.column_spacing(col_spacing.unwrap_or(1)) .column_spacing(col_spacing.unwrap_or(1))
.block(block( .block(block(
config, config,
@ -184,12 +183,13 @@ pub fn draw_custom_content<B: Backend>(
let ctx = ContentRendererArg { let ctx = ContentRendererArg {
app: app.to_lua_ctx_light(), app: app.to_lua_ctx_light(),
layout_size: layout_size.into(), layout_size: layout_size.into(),
screen_size: screen_size.into(), screen_size: ui.screen_size.into(),
scrolltop: ui.scrolltop as u16,
}; };
let rows = lua::serialize(lua, &ctx) let rows = lua::serialize(ui.lua, &ctx)
.map(|arg| { .map(|arg| {
lua::call(lua, &render, arg) lua::call(ui.lua, &render, arg)
.unwrap_or_else(|e| vec![vec![format!("{e:?}")]]) .unwrap_or_else(|e| vec![vec![format!("{e:?}")]])
}) })
.unwrap_or_else(|e| vec![vec![e.to_string()]]) .unwrap_or_else(|e| vec![vec![e.to_string()]])
@ -206,10 +206,10 @@ pub fn draw_custom_content<B: Backend>(
let widths = widths let widths = widths
.into_iter() .into_iter()
.map(|w| w.to_tui(screen_size, layout_size)) .map(|w| w.to_tui(ui.screen_size, layout_size))
.collect::<Vec<TuiConstraint>>(); .collect::<Vec<TuiConstraint>>();
let mut content = Table::new(rows).widths(&widths).block(block( let mut content = Table::new(rows, &widths).block(block(
config, config,
title.map(|t| format!(" {t} ")).unwrap_or_default(), title.map(|t| format!(" {t} ")).unwrap_or_default(),
)); ));

@ -55,7 +55,7 @@ pub struct NodeTypeConfig {
impl NodeTypeConfig { impl NodeTypeConfig {
pub fn extend(mut self, other: &Self) -> Self { pub fn extend(mut self, other: &Self) -> Self {
self.style = self.style.extend(&other.style); self.style = self.style.extend(&other.style);
self.meta.extend(other.meta.to_owned()); self.meta.extend(other.meta.clone());
self self
} }
} }
@ -85,11 +85,11 @@ pub struct NodeTypesConfig {
impl NodeTypesConfig { impl NodeTypesConfig {
pub fn get(&self, node: &Node) -> NodeTypeConfig { pub fn get(&self, node: &Node) -> NodeTypeConfig {
let mut node_type = if node.is_symlink { let mut node_type = if node.is_symlink {
self.symlink.to_owned() self.symlink.clone()
} else if node.is_dir { } else if node.is_dir {
self.directory.to_owned() self.directory.clone()
} else { } else {
self.file.to_owned() self.file.clone()
}; };
let mut me = node.mime_essence.splitn(2, '/'); let mut me = node.mime_essence.splitn(2, '/');
@ -104,7 +104,7 @@ impl NodeTypesConfig {
node_type = node_type.extend(conf); node_type = node_type.extend(conf);
} }
if let Some(conf) = self.extension.get(&node.extension) { if let (Some(conf), false) = (self.extension.get(&node.extension), node.is_dir) {
node_type = node_type.extend(conf); node_type = node_type.extend(conf);
} }
@ -141,7 +141,7 @@ pub struct UiElement {
impl UiElement { impl UiElement {
pub fn extend(mut self, other: &Self) -> Self { pub fn extend(mut self, other: &Self) -> Self {
self.format = other.format.to_owned().or(self.format); self.format = other.format.clone().or(self.format);
self.style = self.style.extend(&other.style); self.style = self.style.extend(&other.style);
self self
} }
@ -353,6 +353,12 @@ pub struct GeneralConfig {
#[serde(default)] #[serde(default)]
pub global_key_bindings: KeyBindings, pub global_key_bindings: KeyBindings,
#[serde(default)]
pub paginated_scrolling: bool,
#[serde(default)]
pub scroll_padding: usize,
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
@ -638,8 +644,8 @@ impl PanelUiConfig {
pub fn extend(mut self, other: &Self) -> Self { pub fn extend(mut self, other: &Self) -> Self {
self.title = self.title.extend(&other.title); self.title = self.title.extend(&other.title);
self.style = self.style.extend(&other.style); self.style = self.style.extend(&other.style);
self.borders = other.borders.to_owned().or(self.borders); self.borders = other.borders.clone().or(self.borders);
self.border_type = other.border_type.to_owned().or(self.border_type); self.border_type = other.border_type.or(self.border_type);
self.border_style = self.border_style.extend(&other.border_style); self.border_style = self.border_style.extend(&other.border_style);
self self
} }
@ -702,6 +708,9 @@ pub struct Hooks {
#[serde(default)] #[serde(default)]
pub on_layout_switch: Vec<ExternalMsg>, pub on_layout_switch: Vec<ExternalMsg>,
#[serde(default)]
pub on_selection_change: Vec<ExternalMsg>,
// TODO After cleanup or Runner::run // TODO After cleanup or Runner::run
// #[serde(default)] // #[serde(default)]
// pub before_quit: Vec<ExternalMsg>, // pub before_quit: Vec<ExternalMsg>,
@ -714,6 +723,7 @@ impl Hooks {
self.on_focus_change.extend(other.on_focus_change); self.on_focus_change.extend(other.on_focus_change);
self.on_mode_switch.extend(other.on_mode_switch); self.on_mode_switch.extend(other.on_mode_switch);
self.on_layout_switch.extend(other.on_layout_switch); self.on_layout_switch.extend(other.on_layout_switch);
self.on_selection_change.extend(other.on_selection_change);
self self
} }
} }

@ -0,0 +1,26 @@
use std::{env, path::PathBuf};
use lazy_static::lazy_static;
use xdg::BaseDirectories;
lazy_static! {
pub static ref BASE_DIRS: Option<BaseDirectories> = BaseDirectories::new().ok();
}
pub fn home_dir() -> Option<PathBuf> {
home::home_dir()
}
pub fn config_dir() -> Option<PathBuf> {
BASE_DIRS.as_ref().map(|base| base.get_config_home())
}
pub fn runtime_dir() -> PathBuf {
let Some(dir) = BASE_DIRS
.as_ref()
.and_then(|base| base.get_runtime_directory().ok())
else {
return env::temp_dir();
};
dir.clone()
}

@ -91,6 +91,17 @@ xplr.config.general.enable_recover_mode = false
-- Type: boolean -- Type: boolean
xplr.config.general.hide_remaps_in_help_menu = false xplr.config.general.hide_remaps_in_help_menu = false
-- Set it to `true` if you want paginated scrolling.
--
-- Type: boolean
xplr.config.general.paginated_scrolling = false
-- Set the padding value to the scroll area.
-- Only applicable when `xplr.config.general.paginated_scrolling = false`.
--
-- Type: boolean
xplr.config.general.scroll_padding = 5
-- Set it to `true` if you want the cursor to stay in the same position when -- Set it to `true` if you want the cursor to stay in the same position when
-- the focus is on the first path and you navigate to the previous path -- the focus is on the first path and you navigate to the previous path
-- (by pressing `up`/`k`), or when the focus is on the last path and you -- (by pressing `up`/`k`), or when the focus is on the last path and you
@ -115,7 +126,7 @@ xplr.config.general.prompt.style = {}
-- Type: nullable string -- Type: nullable string
xplr.config.general.logs.info.format = "INFO" xplr.config.general.logs.info.format = "INFO"
-- The style for the informations logs. -- The style for the information logs.
-- --
-- Type: [Style](https://xplr.dev/en/style) -- Type: [Style](https://xplr.dev/en/style)
xplr.config.general.logs.info.style = { fg = "LightBlue" } xplr.config.general.logs.info.style = { fg = "LightBlue" }
@ -680,11 +691,15 @@ xplr.config.general.initial_sorting = {
} }
-- The name of one of the modes to use when xplr loads. -- The name of one of the modes to use when xplr loads.
-- This isn't the default mode. To modify the default mode, overwrite
-- [xplr.config.modes.builtin.default](https://xplr.dev/en/modes#xplrconfigmodesbuiltindefault).
-- --
-- Type: nullable string -- Type: nullable string
xplr.config.general.initial_mode = "default" xplr.config.general.initial_mode = "default"
-- The name of one of the layouts to use when xplr loads. -- The name of one of the layouts to use when xplr loads.
-- This isn't the default layout. To modify the default layout, overwrite
-- [xplr.config.layouts.builtin.default](https://xplr.dev/en/layouts#xplrconfiglayoutsbuiltindefault).
-- --
-- Type: nullable string -- Type: nullable string
xplr.config.general.initial_layout = "default" xplr.config.general.initial_layout = "default"
@ -701,6 +716,17 @@ xplr.config.general.start_fifo = nil
-- Type: [Key Bindings](https://xplr.dev/en/configure-key-bindings#key-bindings) -- Type: [Key Bindings](https://xplr.dev/en/configure-key-bindings#key-bindings)
xplr.config.general.global_key_bindings = { xplr.config.general.global_key_bindings = {
on_key = { on_key = {
["f1"] = {
help = "global help menu",
messages = {
{
BashExec = [===[
[ -z "$PAGER" ] && PAGER="less -+F"
cat -- "${XPLR_PIPE_GLOBAL_HELP_MENU_OUT}" | ${PAGER:?}
]===],
},
},
},
["esc"] = { ["esc"] = {
messages = { messages = {
"PopMode", "PopMode",
@ -728,9 +754,7 @@ xplr.config.general.global_key_bindings = {
-- The style for the directory nodes -- The style for the directory nodes
-- --
-- Type: [Style](https://xplr.dev/en/style) -- Type: [Style](https://xplr.dev/en/style)
xplr.config.node_types.directory.style = { xplr.config.node_types.directory.style = {}
fg = "Blue",
}
-- Metadata for the directory nodes. -- Metadata for the directory nodes.
-- You can set as many metadata as you want. -- You can set as many metadata as you want.
@ -766,10 +790,7 @@ xplr.config.node_types.file.meta.icon = "ƒ"
-- The style for the symlink nodes. -- The style for the symlink nodes.
-- --
-- Type: [Style](https://xplr.dev/en/style) -- Type: [Style](https://xplr.dev/en/style)
xplr.config.node_types.symlink.style = { xplr.config.node_types.symlink.style = {}
fg = "Magenta",
add_modifiers = { "Italic" },
}
-- Metadata for the symlink nodes. -- Metadata for the symlink nodes.
-- You can set as many metadata as you want. -- You can set as many metadata as you want.
@ -801,7 +822,7 @@ xplr.config.node_types.symlink.meta.icon = "§"
-- xplr.config.node_types.mime_essence = { -- xplr.config.node_types.mime_essence = {
-- application = { -- application = {
-- -- application/* -- -- application/*
-- ["*"] = { meta = { icon = "a" } } -- ["*"] = { meta = { icon = "a" } },
-- --
-- -- application/pdf -- -- application/pdf
-- pdf = { meta = { icon = "" }, style = { fg = "Blue" } }, -- pdf = { meta = { icon = "" }, style = { fg = "Blue" } },
@ -857,15 +878,13 @@ xplr.config.node_types.special = {}
-- --
-- ##### Example: Defining Custom Layout -- ##### Example: Defining Custom Layout
-- --
-- ![demo](https://s6.gifyu.com/images/layout.png)
--
-- ```lua -- ```lua
-- xplr.config.layouts.builtin.default = { -- xplr.config.layouts.builtin.default = {
-- Horizontal = { -- Horizontal = {
-- config = { -- config = {
-- margin = 1, -- margin = 1,
-- horizontal_margin = 2, -- horizontal_margin = 1,
-- vertical_margin = 3, -- vertical_margin = 1,
-- constraints = { -- constraints = {
-- { Percentage = 50 }, -- { Percentage = 50 },
-- { Percentage = 50 }, -- { Percentage = 50 },
@ -878,6 +897,21 @@ xplr.config.node_types.special = {}
-- } -- }
-- } -- }
-- ``` -- ```
--
-- Result:
--
-- ```
-- ╭ /home ─────────────╮╭ Help [default] ────╮
-- │ ╭─── path ││. show hidden │
-- │ ├▸[ð Desktop/] ││/ search │
-- │ ├ ð Documents/ ││: action │
-- │ ├ ð Downloads/ ││? global help │
-- │ ├ ð GitHub/ ││G go to bottom │
-- │ ├ ð Music/ ││V select/unselect│
-- │ ├ ð Pictures/ ││ctrl duplicate as │
-- │ ├ ð Public/ ││ctrl next visit │
-- ╰────────────────────╯╰────────────────────╯
-- ```
-- The default layout -- The default layout
-- --
@ -1067,17 +1101,6 @@ xplr.config.modes.builtin.default = {
{ SwitchModeBuiltin = "action" }, { SwitchModeBuiltin = "action" },
}, },
}, },
["?"] = {
help = "global help menu",
messages = {
{
BashExec = [===[
[ -z "$PAGER" ] && PAGER="less -+F"
cat -- "${XPLR_PIPE_GLOBAL_HELP_MENU_OUT}" | ${PAGER:?}
]===],
},
},
},
["G"] = { ["G"] = {
help = "go to bottom", help = "go to bottom",
messages = { messages = {
@ -1111,6 +1134,18 @@ xplr.config.modes.builtin.default = {
"LastVisitedPath", "LastVisitedPath",
}, },
}, },
[")"] = {
help = "next deep branch",
messages = {
"NextVisitedDeepBranch",
},
},
["("] = {
help = "prev deep branch",
messages = {
"PreviousVisitedDeepBranch",
},
},
["ctrl-r"] = { ["ctrl-r"] = {
help = "refresh screen", help = "refresh screen",
messages = { messages = {
@ -1272,6 +1307,22 @@ xplr.config.modes.builtin.default = {
"FocusPreviousSelection", "FocusPreviousSelection",
}, },
}, },
["m"] = {
help = "move to",
messages = {
"PopMode",
{ SwitchModeBuiltin = "move_to" },
{ SetInputBuffer = "" },
},
},
["c"] = {
help = "copy to",
messages = {
"PopMode",
{ SwitchModeBuiltin = "copy_to" },
{ SetInputBuffer = "" },
},
},
}, },
on_number = { on_number = {
help = "input", help = "input",
@ -1300,6 +1351,8 @@ 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["right"]
xplr.config.modes.builtin.default.key_bindings.on_key["tab"] = xplr.config.modes.builtin.default.key_bindings.on_key["tab"] =
xplr.config.modes.builtin.default.key_bindings.on_key["ctrl-i"] -- compatibility workaround xplr.config.modes.builtin.default.key_bindings.on_key["ctrl-i"] -- compatibility workaround
xplr.config.modes.builtin.default.key_bindings.on_key["?"] =
xplr.config.general.global_key_bindings.on_key["f1"]
-- The builtin debug error mode. -- The builtin debug error mode.
-- --
@ -1434,6 +1487,148 @@ xplr.config.modes.builtin.go_to_path = {
}, },
} }
-- The builtin move_to mode.
--
-- Type: [Mode](https://xplr.dev/en/mode)
xplr.config.modes.builtin.move_to = {
name = "move_to",
prompt = "ð ",
key_bindings = {
on_key = {
["enter"] = {
help = "submit",
messages = {
{
BashExec0 = [===[
DEST="$XPLR_INPUT_BUFFER"
[ -z "$DEST" ] && exit
if [ ! -d "$DEST" ] && ! mkdir -p -- "$DEST"; then
"$XPLR" -m 'LogError: %q' "could not create $DEST"
exit
fi
"$XPLR" -m "ChangeDirectory: %q" "$DEST"
! cd -- "$DEST" && exit
DEST="$(pwd)" && echo "PWD=$DEST"
while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH")
BASENAME=$(basename -- "$PTH")
BASENAME_ESC=$(printf %q "$BASENAME")
if [ -e "$BASENAME" ]; then
echo
echo "$BASENAME_ESC exists, do you want to overwrite it?"
read -p "[y]es, [n]o, [S]kip: " ANS < /dev/tty
case "$ANS" in
[yY]*)
;;
[nN]*)
read -p "Enter new name: " BASENAME < /dev/tty
BASENAME_ESC=$(printf %q "$BASENAME")
;;
*)
continue
;;
esac
fi
if mv -v -- "${PTH:?}" "./${BASENAME:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC moved to $BASENAME_ESC"
"$XPLR" -m 'FocusPath: %q' "$BASENAME"
else
"$XPLR" -m 'LogError: %q' "could not move $PTH_ESC to $BASENAME_ESC"
fi
done < "${XPLR_PIPE_RESULT_OUT:?}"
echo
read -p "[press enter to continue]"
]===],
},
"PopMode",
},
},
["tab"] = {
help = "try complete",
messages = {
{ CallLuaSilently = "builtin.try_complete_path" },
},
},
},
default = {
messages = {
"UpdateInputBufferFromKey",
},
},
},
}
-- The builtin copy_to mode.
--
-- Type: [Mode](https://xplr.dev/en/mode)
xplr.config.modes.builtin.copy_to = {
name = "copy_to",
prompt = "ð ",
key_bindings = {
on_key = {
["enter"] = {
help = "submit",
messages = {
{
BashExec0 = [===[
DEST="$XPLR_INPUT_BUFFER"
[ -z "$DEST" ] && exit
if [ ! -d "$DEST" ] && ! mkdir -p -- "$DEST"; then
"$XPLR" -m 'LogError: %q' "could not create $DEST"
exit
fi
"$XPLR" -m "ChangeDirectory: %q" "$DEST"
! cd -- "$DEST" && exit
DEST="$(pwd)" && echo "PWD=$DEST"
while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH")
BASENAME=$(basename -- "$PTH")
BASENAME_ESC=$(printf %q "$BASENAME")
if [ -e "$BASENAME" ]; then
echo
echo "$BASENAME_ESC exists, do you want to overwrite it?"
read -p "[y]es, [n]o, [S]kip: " ANS < /dev/tty
case "$ANS" in
[yY]*)
;;
[nN]*)
read -p "Enter new name: " BASENAME < /dev/tty
BASENAME_ESC=$(printf %q "$BASENAME")
;;
*)
continue
;;
esac
fi
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' "could not copy $PTH_ESC to $BASENAME_ESC"
fi
done < "${XPLR_PIPE_RESULT_OUT:?}"
echo
read -p "[press enter to continue]"
]===],
},
"PopMode",
},
},
["tab"] = {
help = "try complete",
messages = {
{ CallLuaSilently = "builtin.try_complete_path" },
},
},
},
default = {
messages = {
"UpdateInputBufferFromKey",
},
},
},
}
-- The builtin selection ops mode. -- The builtin selection ops mode.
-- --
-- Type: [Mode](https://xplr.dev/en/mode) -- Type: [Mode](https://xplr.dev/en/mode)
@ -1448,15 +1643,15 @@ xplr.config.modes.builtin.selection_ops = {
{ {
BashExec0 = [===[ BashExec0 = [===[
TMPFILE="$(mktemp)" TMPFILE="$(mktemp)"
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
echo $(printf %q "${PTH:?}") >> "${TMPFILE:?}" echo $(printf %q "${PTH:?}") >> "${TMPFILE:?}"
done < "${XPLR_PIPE_SELECTION_OUT:?}") done < "${XPLR_PIPE_SELECTION_OUT:?}"
${EDITOR:-vi} "${TMPFILE:?}" ${EDITOR:-vi} "${TMPFILE:?}"
[ ! -e "$TMPFILE" ] && exit [ ! -e "$TMPFILE" ] && exit
"$XPLR" -m ClearSelection "$XPLR" -m ClearSelection
(while IFS= read -r PTH_ESC; do while IFS= read -r PTH_ESC; do
"$XPLR" -m 'SelectPath: %q' "$(eval printf %s ${PTH_ESC:?})" "$XPLR" -m 'SelectPath: %q' "$(eval printf %s ${PTH_ESC:?})"
done < "${TMPFILE:?}") done < "${TMPFILE:?}"
rm -- "${TMPFILE:?}" rm -- "${TMPFILE:?}"
]===], ]===],
}, },
@ -1484,22 +1679,35 @@ xplr.config.modes.builtin.selection_ops = {
{ {
BashExec0 = [===[ BashExec0 = [===[
"$XPLR" -m ExplorePwd "$XPLR" -m ExplorePwd
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH") PTH_ESC=$(printf %q "$PTH")
BASENAME=$(basename -- "$PTH") BASENAME=$(basename -- "$PTH")
BASENAME_ESC=$(printf %q "$BASENAME") BASENAME_ESC=$(printf %q "$BASENAME")
while [ -e "$BASENAME" ]; do if [ -e "$BASENAME" ]; then
BASENAME="$BASENAME (copied)" echo
BASENAME_ESC=$(printf %q "$BASENAME") echo "$BASENAME_ESC exists, do you want to overwrite it?"
done read -p "[y]es, [n]o, [S]kip: " ANS < /dev/tty
case "$ANS" in
[yY]*)
;;
[nN]*)
read -p "Enter new name: " BASENAME < /dev/tty
BASENAME_ESC=$(printf %q "$BASENAME")
;;
*)
continue
;;
esac
fi
if cp -vr -- "${PTH:?}" "./${BASENAME:?}"; then if cp -vr -- "${PTH:?}" "./${BASENAME:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC copied to ./$BASENAME_ESC" "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC copied to $BASENAME_ESC"
"$XPLR" -m 'FocusPath: %q' "$BASENAME" "$XPLR" -m 'FocusPath: %q' "$BASENAME"
else else
"$XPLR" -m 'LogError: %q' "could not copy $PTH_ESC to ./$BASENAME_ESC" "$XPLR" -m 'LogError: %q' "could not copy $PTH_ESC to $BASENAME_ESC"
fi fi
done < "${XPLR_PIPE_SELECTION_OUT:?}") done < "${XPLR_PIPE_SELECTION_OUT:?}"
read -p "[enter to continue]" echo
read -p "[press enter to continue]"
]===], ]===],
}, },
"PopMode", "PopMode",
@ -1511,22 +1719,35 @@ xplr.config.modes.builtin.selection_ops = {
{ {
BashExec0 = [===[ BashExec0 = [===[
"$XPLR" -m ExplorePwd "$XPLR" -m ExplorePwd
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH") PTH_ESC=$(printf %q "$PTH")
BASENAME=$(basename -- "$PTH") BASENAME=$(basename -- "$PTH")
BASENAME_ESC=$(printf %q "$BASENAME") BASENAME_ESC=$(printf %q "$BASENAME")
while [ -e "$BASENAME" ]; do if [ -e "$BASENAME" ]; then
BASENAME="$BASENAME (moved)" echo
BASENAME_ESC=$(printf %q "$BASENAME") echo "$BASENAME_ESC exists, do you want to overwrite it?"
done read -p "[y]es, [n]o, [S]kip: " ANS < /dev/tty
case "$ANS" in
[yY]*)
;;
[nN]*)
read -p "Enter new name: " BASENAME < /dev/tty
BASENAME_ESC=$(printf %q "$BASENAME")
;;
*)
continue
;;
esac
fi
if mv -v -- "${PTH:?}" "./${BASENAME:?}"; then if mv -v -- "${PTH:?}" "./${BASENAME:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC moved to ./$BASENAME_ESC" "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC moved to $BASENAME_ESC"
"$XPLR" -m 'FocusPath: %q' "$BASENAME" "$XPLR" -m 'FocusPath: %q' "$BASENAME"
else else
"$XPLR" -m 'LogError: %q' "could not move $PTH_ESC to ./$BASENAME_ESC" "$XPLR" -m 'LogError: %q' "could not move $PTH_ESC to $BASENAME_ESC"
fi fi
done < "${XPLR_PIPE_SELECTION_OUT:?}") done < "${XPLR_PIPE_SELECTION_OUT:?}"
read -p "[enter to continue]" echo
read -p "[press enter to continue]"
]===], ]===],
}, },
"PopMode", "PopMode",
@ -1538,22 +1759,35 @@ xplr.config.modes.builtin.selection_ops = {
{ {
BashExec0 = [===[ BashExec0 = [===[
"$XPLR" -m ExplorePwd "$XPLR" -m ExplorePwd
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH") PTH_ESC=$(printf %q "$PTH")
BASENAME=$(basename -- "$PTH") BASENAME=$(basename -- "$PTH")
BASENAME_ESC=$(printf %q "$BASENAME") BASENAME_ESC=$(printf %q "$BASENAME")
while [ -e "$BASENAME" ]; do if [ -e "$BASENAME" ]; then
BASENAME="$BASENAME (softlinked)" echo
BASENAME_ESC=$(printf %q "$BASENAME") echo "$BASENAME_ESC exists, do you want to overwrite it?"
done read -p "[y]es, [n]o, [S]kip: " ANS < /dev/tty
case "$ANS" in
[yY]*)
;;
[nN]*)
read -p "Enter new name: " BASENAME < /dev/tty
BASENAME_ESC=$(printf %q "$BASENAME")
;;
*)
continue
;;
esac
fi
if ln -sv -- "${PTH:?}" "./${BASENAME:?}"; then if ln -sv -- "${PTH:?}" "./${BASENAME:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC softlinked as ./$BASENAME_ESC" "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC softlinked as $BASENAME_ESC"
"$XPLR" -m 'FocusPath: %q' "$BASENAME" "$XPLR" -m 'FocusPath: %q' "$BASENAME"
else else
"$XPLR" -m 'LogError: %q' "could not softlink $PTH_ESC as ./$BASENAME_ESC" "$XPLR" -m 'LogError: %q' "could not softlink $PTH_ESC as $BASENAME_ESC"
fi fi
done < "${XPLR_PIPE_SELECTION_OUT:?}") done < "${XPLR_PIPE_SELECTION_OUT:?}"
read -p "[enter to continue]" echo
read -p "[press enter to continue]"
]===], ]===],
}, },
"PopMode", "PopMode",
@ -1565,22 +1799,35 @@ xplr.config.modes.builtin.selection_ops = {
{ {
BashExec0 = [===[ BashExec0 = [===[
"$XPLR" -m ExplorePwd "$XPLR" -m ExplorePwd
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH") PTH_ESC=$(printf %q "$PTH")
BASENAME=$(basename -- "$PTH") BASENAME=$(basename -- "$PTH")
BASENAME_ESC=$(printf %q "$BASENAME") BASENAME_ESC=$(printf %q "$BASENAME")
while [ -e "$BASENAME" ]; do if [ -e "$BASENAME" ]; then
BASENAME="$BASENAME (hardlinked)" echo
BASENAME_ESC=$(printf %q "$BASENAME") echo "$BASENAME_ESC exists, do you want to overwrite it?"
done read -p "[y]es, [n]o, [S]kip: " ANS < /dev/tty
case "$ANS" in
[yY]*)
;;
[nN]*)
read -p "Enter new name: " BASENAME < /dev/tty
BASENAME_ESC=$(printf %q "$BASENAME")
;;
*)
continue
;;
esac
fi
if ln -v -- "${PTH:?}" "./${BASENAME:?}"; then if ln -v -- "${PTH:?}" "./${BASENAME:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC hardlinked as ./$BASENAME_ESC" "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC hardlinked as $BASENAME_ESC"
"$XPLR" -m 'FocusPath: %q' "$BASENAME" "$XPLR" -m 'FocusPath: %q' "$BASENAME"
else else
"$XPLR" -m 'LogError: %q' "could not hardlink $PTH_ESC as ./$BASENAME_ESC" "$XPLR" -m 'LogError: %q' "could not hardlink $PTH_ESC as $BASENAME_ESC"
fi fi
done < "${XPLR_PIPE_SELECTION_OUT:?}") done < "${XPLR_PIPE_SELECTION_OUT:?}"
read -p "[enter to continue]" echo
read -p "[press enter to continue]"
]===], ]===],
}, },
"PopMode", "PopMode",
@ -1817,9 +2064,9 @@ xplr.config.modes.builtin.go_to = {
exit 1 exit 1
fi fi
fi fi
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
$OPENER "${PTH:?}" > /dev/null 2>&1 $OPENER "${PTH:?}" > /dev/null 2>&1
done < "${XPLR_PIPE_RESULT_OUT:?}") done < "${XPLR_PIPE_RESULT_OUT:?}"
]===], ]===],
}, },
"ClearScreen", "ClearScreen",
@ -1931,8 +2178,16 @@ xplr.config.modes.builtin.delete = {
messages = { messages = {
{ {
BashExec0 = [===[ BashExec0 = [===[
while IFS= read -r -d '' PTH; do
printf '%q\n' "$PTH"
done < "${XPLR_PIPE_RESULT_OUT:?}"
echo
read -p "Permanently delete these files? [Y/n]: " ANS
[ "${ANS:-Y}" = "Y" ] || [ "$ANS" = "y" ] || exit 0
echo
"$XPLR" -m ExplorePwd "$XPLR" -m ExplorePwd
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH") PTH_ESC=$(printf %q "$PTH")
if rm -rfv -- "${PTH:?}"; then if rm -rfv -- "${PTH:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC deleted" "$XPLR" -m 'LogSuccess: %q' "$PTH_ESC deleted"
@ -1940,8 +2195,9 @@ xplr.config.modes.builtin.delete = {
"$XPLR" -m 'LogError: %q' "could not delete $PTH_ESC" "$XPLR" -m 'LogError: %q' "could not delete $PTH_ESC"
"$XPLR" -m 'FocusPath: %q' "$PTH" "$XPLR" -m 'FocusPath: %q' "$PTH"
fi fi
done < "${XPLR_PIPE_RESULT_OUT:?}") done < "${XPLR_PIPE_RESULT_OUT:?}"
read -p "[enter to continue]" echo
read -p "[press enter to continue]"
]===], ]===],
}, },
"PopMode", "PopMode",
@ -1952,8 +2208,16 @@ xplr.config.modes.builtin.delete = {
messages = { messages = {
{ {
BashExec0 = [===[ BashExec0 = [===[
while IFS= read -r -d '' PTH; do
printf '%q\n' "$PTH"
done < "${XPLR_PIPE_RESULT_OUT:?}"
echo
read -p "Permanently delete these files? [Y/n]: " ANS
[ "${ANS:-Y}" = "Y" ] || [ "$ANS" = "y" ] || exit 0
echo
"$XPLR" -m ExplorePwd "$XPLR" -m ExplorePwd
(while IFS= read -r -d '' PTH; do while IFS= read -r -d '' PTH; do
PTH_ESC=$(printf %q "$PTH") PTH_ESC=$(printf %q "$PTH")
if [ -d "$PTH" ] && [ ! -L "$PTH" ]; then if [ -d "$PTH" ] && [ ! -L "$PTH" ]; then
if rmdir -v -- "${PTH:?}"; then if rmdir -v -- "${PTH:?}"; then
@ -1970,8 +2234,9 @@ xplr.config.modes.builtin.delete = {
"$XPLR" -m 'FocusPath: %q' "$PTH" "$XPLR" -m 'FocusPath: %q' "$PTH"
fi fi
fi fi
done < "${XPLR_PIPE_RESULT_OUT:?}") done < "${XPLR_PIPE_RESULT_OUT:?}"
read -p "[enter to continue]" echo
read -p "[press enter to continue]"
]===], ]===],
}, },
"PopMode", "PopMode",
@ -2465,7 +2730,6 @@ xplr.config.modes.builtin.sort = {
"ExplorePwdAsync", "ExplorePwdAsync",
}, },
}, },
["c"] = { ["c"] = {
help = "by created", help = "by created",
messages = { messages = {
@ -2473,7 +2737,6 @@ xplr.config.modes.builtin.sort = {
"ExplorePwdAsync", "ExplorePwdAsync",
}, },
}, },
["C"] = { ["C"] = {
help = "by created reverse", help = "by created reverse",
messages = { messages = {
@ -2481,7 +2744,6 @@ xplr.config.modes.builtin.sort = {
"ExplorePwdAsync", "ExplorePwdAsync",
}, },
}, },
["l"] = { ["l"] = {
help = "by last modified", help = "by last modified",
messages = { messages = {
@ -2489,7 +2751,6 @@ xplr.config.modes.builtin.sort = {
"ExplorePwdAsync", "ExplorePwdAsync",
}, },
}, },
["L"] = { ["L"] = {
help = "by last modified reverse", help = "by last modified reverse",
messages = { messages = {
@ -2863,9 +3124,9 @@ xplr.fn.builtin.fmt_general_selection_item = function(n)
if n.is_dir then if n.is_dir then
shortened = shortened .. "/" shortened = shortened .. "/"
end end
local ls_style = xplr.util.lscolor(n.absolute_path)
local meta_style = xplr.util.node_type(n).style local meta_style = xplr.util.node_type(n).style
local style = xplr.util.style_mix({ meta_style, ls_style }) local ls_style = xplr.util.lscolor(n.absolute_path)
local style = xplr.util.style_mix({ ls_style, meta_style })
return xplr.util.paint(shortened:gsub("\n", nl), style) return xplr.util.paint(shortened:gsub("\n", nl), style)
end end
@ -2887,8 +3148,8 @@ end
xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m)
local nl = xplr.util.paint("\\n", { add_modifiers = { "Italic", "Dim" } }) local nl = xplr.util.paint("\\n", { add_modifiers = { "Italic", "Dim" } })
local r = m.tree .. m.prefix local r = m.tree .. m.prefix
local style = xplr.util.lscolor(m.absolute_path) local ls_style = xplr.util.lscolor(m.absolute_path)
style = xplr.util.style_mix({ m.style, style }) local style = xplr.util.style_mix({ ls_style, m.style })
if m.meta.icon == nil then if m.meta.icon == nil then
r = r .. "" r = r .. ""
@ -2910,7 +3171,8 @@ xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m)
if m.is_broken then if m.is_broken then
r = r .. "×" r = r .. "×"
else else
local symlink_path = xplr.util.shorten(m.symlink.absolute_path) local symlink_path =
xplr.util.shorten(m.symlink.absolute_path, { base = m.parent })
if m.symlink.is_dir then if m.symlink.is_dir then
symlink_path = symlink_path .. "/" symlink_path = symlink_path .. "/"
end end
@ -3019,6 +3281,14 @@ xplr.fn.custom = {}
-- { LogSuccess = "Switched layout" }, -- { LogSuccess = "Switched layout" },
-- { CallLuaSilently = "custom.some_plugin_with_hooks.on_layout_switch" }, -- { CallLuaSilently = "custom.some_plugin_with_hooks.on_layout_switch" },
-- } -- }
--
-- -- Add messages to send when the selection changes
-- --
-- -- Type: list of [Message](https://xplr.dev/en/message#message)s
-- on_selection_change = {
-- { LogSuccess = "Selection changed" },
-- { CallLuaSilently = "custom.some_plugin_with_hooks.on_selection_change" },
-- }
-- } -- }
-- ``` -- ```
@ -3029,3 +3299,15 @@ return {
on_mode_switch = {}, on_mode_switch = {},
on_layout_switch = {}, on_layout_switch = {},
} }
-- ----------------------------------------------------------------------------
-- > Note:
-- >
-- > It's not recommended to copy the entire configuration, unless you want to
-- > freeze it and miss out on useful updates to the defaults.
-- >
-- > Instead, you can use this as a reference to overwrite only the parts you
-- > want to update.
-- >
-- > If you still want to copy the entire configuration, make sure to put your
-- > customization before the return statement.

@ -18,6 +18,18 @@ pub enum Key {
F10, F10,
F11, F11,
F12, F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Num0, Num0,
Num1, Num1,
@ -339,7 +351,19 @@ impl Key {
KeyCode::F(9) => Key::F9, KeyCode::F(9) => Key::F9,
KeyCode::F(10) => Key::F10, KeyCode::F(10) => Key::F10,
KeyCode::F(11) => Key::F11, KeyCode::F(11) => Key::F11,
KeyCode::F(12) => Key::F12, KeyCode::F(13) => Key::F13,
KeyCode::F(12) => Key::F13,
KeyCode::F(14) => Key::F14,
KeyCode::F(15) => Key::F15,
KeyCode::F(16) => Key::F16,
KeyCode::F(17) => Key::F17,
KeyCode::F(18) => Key::F18,
KeyCode::F(19) => Key::F19,
KeyCode::F(20) => Key::F20,
KeyCode::F(21) => Key::F21,
KeyCode::F(22) => Key::F22,
KeyCode::F(23) => Key::F23,
KeyCode::F(24) => Key::F24,
KeyCode::Backspace => Key::Backspace, KeyCode::Backspace => Key::Backspace,
KeyCode::Left => Key::Left, KeyCode::Left => Key::Left,
@ -623,7 +647,7 @@ impl Key {
Self::ShiftZ => Some('Z'), Self::ShiftZ => Some('Z'),
Self::Space => Some(' '), Self::Space => Some(' '),
Self::Special(c) => Some(c.to_owned()), Self::Special(c) => Some(*c),
_ => None, _ => None,
} }
@ -707,12 +731,6 @@ impl From<char> for Key {
} }
} }
impl From<String> for Key {
fn from(string: String) -> Self {
string.into()
}
}
impl From<&str> for Key { impl From<&str> for Key {
fn from(string: &str) -> Self { fn from(string: &str) -> Self {
if string.len() == 1 { if string.len() == 1 {

@ -7,6 +7,7 @@ pub mod cli;
pub mod compat; pub mod compat;
pub mod config; pub mod config;
pub mod directory_buffer; pub mod directory_buffer;
pub mod dirs;
pub mod event_reader; pub mod event_reader;
pub mod explorer; pub mod explorer;
pub mod input; pub mod input;

@ -7,7 +7,7 @@ use anyhow::Result;
use mlua::Lua; use mlua::Lua;
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use mlua::SerializeOptions; use mlua::SerializeOptions;
use serde::Deserialize; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use std::fs; use std::fs;
@ -81,7 +81,7 @@ pub fn init(lua: &Lua) -> Result<(Config, Option<Hooks>)> {
let hooks: Option<Hooks> = lua let hooks: Option<Hooks> = lua
.load(DEFAULT_LUA_SCRIPT) .load(DEFAULT_LUA_SCRIPT)
.set_name("xplr init")? .set_name("xplr init")
.call(()) .call(())
.and_then(|v| lua.from_value(v))?; .and_then(|v| lua.from_value(v))?;
@ -98,7 +98,7 @@ pub fn extend(lua: &Lua, path: &str) -> Result<(Config, Option<Hooks>)> {
let hooks: Option<Hooks> = lua let hooks: Option<Hooks> = lua
.load(&script) .load(&script)
.set_name(path)? .set_name(path)
.call(()) .call(())
.and_then(|v| lua.from_value(v))?; .and_then(|v| lua.from_value(v))?;
@ -138,7 +138,7 @@ pub fn resolve_fn<'lua>(
resolve_fn_recursive(globals, path.split('.')) resolve_fn_recursive(globals, path.split('.'))
} }
pub fn call<'lua, R: Deserialize<'lua>>( pub fn call<'lua, R: DeserializeOwned>(
lua: &'lua Lua, lua: &'lua Lua,
func: &str, func: &str,
arg: mlua::Value<'lua>, arg: mlua::Value<'lua>,
@ -160,24 +160,24 @@ mod tests {
assert!(check_version(VERSION, "foo path").is_ok()); assert!(check_version(VERSION, "foo path").is_ok());
// Current release if OK // Current release if OK
assert!(check_version("0.21.1", "foo path").is_ok()); assert!(check_version("0.21.9", "foo path").is_ok());
// Prev major release is ERR // Prev major release is ERR
// - Not yet // - Not yet
// Prev minor release is ERR (Change when we get to v1) // Prev minor release is ERR (Change when we get to v1)
assert!(check_version("0.20.1", "foo path").is_err()); assert!(check_version("0.20.9", "foo path").is_err());
// Prev bugfix release is OK // Prev bugfix release is OK
assert!(check_version("0.21.0", "foo path").is_ok()); assert!(check_version("0.21.8", "foo path").is_ok());
// Next major release is ERR // Next major release is ERR
assert!(check_version("1.20.1", "foo path").is_err()); assert!(check_version("1.20.9", "foo path").is_err());
// Next minor release is ERR // Next minor release is ERR
assert!(check_version("0.22.1", "foo path").is_err()); assert!(check_version("0.22.9", "foo path").is_err());
// Next bugfix release is ERR (Change when we get to v1) // Next bugfix release is ERR (Change when we get to v1)
assert!(check_version("0.21.2", "foo path").is_err()); assert!(check_version("0.21.10", "foo path").is_err());
} }
} }

@ -13,6 +13,7 @@ use crate::ui::Layout;
use crate::ui::Style; use crate::ui::Style;
use crate::ui::WrapOptions; use crate::ui::WrapOptions;
use anyhow::Result; use anyhow::Result;
use lazy_static::lazy_static;
use lscolors::LsColors; use lscolors::LsColors;
use mlua::Error as LuaError; use mlua::Error as LuaError;
use mlua::Lua; use mlua::Lua;
@ -28,6 +29,10 @@ use std::borrow::Cow;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
lazy_static! {
static ref LS_COLORS: LsColors = LsColors::from_env().unwrap_or_default();
}
/// Get the xplr version details. /// Get the xplr version details.
/// ///
/// Type: function() -> { major: number, minor: number, patch: number } /// Type: function() -> { major: number, minor: number, patch: number }
@ -64,6 +69,30 @@ pub fn version<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
Ok(util) Ok(util)
} }
/// Print the given value to the console, and return it as a string.
/// Useful for debugging.
///
/// Type: function( value ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.debug({ foo = "bar", bar = function() end })
/// -- {
/// -- ["bar"] = function: 0x55e5cebdeae0,
/// -- ["foo"] = "bar",
/// -- }
/// ```
pub fn debug<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|_, value: Value| {
let log = format!("{:#?}", value);
println!("{}", log);
Ok(log)
})?;
util.set("debug", func)?;
Ok(util)
}
/// Clone/deepcopy a Lua value. Doesn't work with functions. /// Clone/deepcopy a Lua value. Doesn't work with functions.
/// ///
/// Type: function( value ) -> value /// Type: function( value ) -> value
@ -131,7 +160,7 @@ pub fn is_dir<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
/// ``` /// ```
pub fn is_file<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> { pub fn is_file<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = let func =
lua.create_function(move |_, path: String| Ok(PathBuf::from(path).is_dir()))?; lua.create_function(move |_, path: String| Ok(PathBuf::from(path).is_file()))?;
util.set("is_file", func)?; util.set("is_file", func)?;
Ok(util) Ok(util)
} }
@ -460,7 +489,7 @@ pub fn explore<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
/// ///
/// ```lua /// ```lua
/// xplr.util.shell_execute("pwd") /// xplr.util.shell_execute("pwd")
/// -- "/present/working/directory" /// -- { stdout = "/present/working/directory", stderr = "", returncode = 0 }
/// ///
/// xplr.util.shell_execute("bash", {"-c", "xplr --help"}) /// xplr.util.shell_execute("bash", {"-c", "xplr --help"})
/// -- { stdout = "xplr...", stderr = "", returncode = 0 } /// -- { stdout = "xplr...", stderr = "", returncode = 0 }
@ -625,7 +654,7 @@ pub fn to_yaml<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
/// Get a [Style][3] object for the given path based on the LS_COLORS /// Get a [Style][3] object for the given path based on the LS_COLORS
/// environment variable. /// environment variable.
/// ///
/// Type: function( path:string ) -> [Style][3]|nil /// Type: function( path:string ) -> [Style][3]
/// ///
/// Example: /// Example:
/// ///
@ -634,9 +663,11 @@ pub fn to_yaml<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
/// -- { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} } /// -- { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} }
/// ``` /// ```
pub fn lscolor<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> { 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 func = lua.create_function(move |lua, path: String| {
let style = lscolors.style_for_path(path).map(Style::from); let style = LS_COLORS
.style_for_path(path)
.map(Style::from)
.unwrap_or_default();
lua::serialize(lua, &style).map_err(LuaError::custom) lua::serialize(lua, &style).map_err(LuaError::custom)
})?; })?;
util.set("lscolor", func)?; util.set("lscolor", func)?;
@ -838,6 +869,7 @@ pub(crate) fn create_table(lua: &Lua) -> Result<Table> {
let mut util = lua.create_table()?; let mut util = lua.create_table()?;
util = version(util, lua)?; util = version(util, lua)?;
util = debug(util, lua)?;
util = clone(util, lua)?; util = clone(util, lua)?;
util = exists(util, lua)?; util = exists(util, lua)?;
util = is_dir(util, lua)?; util = is_dir(util, lua)?;

@ -277,6 +277,22 @@ pub enum ExternalMsg {
/// - YAML: `NextVisitedPath` /// - YAML: `NextVisitedPath`
NextVisitedPath, NextVisitedPath,
/// Go to the previous deep level branch.
///
/// Example:
///
/// - Lua: `"PreviousVisitedDeepBranch"`
/// - YAML: `PreviousVisitedDeepBranch`
PreviousVisitedDeepBranch,
/// Go to the next deep level branch.
///
/// Example:
///
/// - Lua: `"NextVisitedDeepBranch"`
/// - YAML: `NextVisitedDeepBranch`
NextVisitedDeepBranch,
/// Follow the symlink under focus to its actual location. /// Follow the symlink under focus to its actual location.
/// ///
/// Example: /// Example:
@ -338,7 +354,7 @@ pub enum ExternalMsg {
/// Update the input buffer using cursor based operations. /// Update the input buffer using cursor based operations.
/// ///
/// Type: { UpdateInputBuffer = [Input Opertaion](https://xplr.dev/en/input-operation) } /// Type: { UpdateInputBuffer = [Input Operation](https://xplr.dev/en/input-operation) }
/// ///
/// Example: /// Example:
/// ///
@ -364,7 +380,7 @@ pub enum ExternalMsg {
/// - YAML: `BufferInput: foo` /// - YAML: `BufferInput: foo`
BufferInput(String), BufferInput(String),
/// Append/buffer the characted read from a keyboard input into the /// Append/buffer the character read from a keyboard input into the
/// input buffer. /// input buffer.
/// ///
/// Example: /// Example:
@ -751,7 +767,7 @@ pub enum ExternalMsg {
/// Add a [filter](https://xplr.dev/en/filtering#filter) to exclude nodes /// Add a [filter](https://xplr.dev/en/filtering#filter) to exclude nodes
/// while exploring directories. /// while exploring directories.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// Filters get automatically cleared when changing directories. /// Filters get automatically cleared when changing directories.
/// ///
/// Type: { AddNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering#filter), input = "string" } /// Type: { AddNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering#filter), input = "string" }
@ -763,7 +779,7 @@ pub enum ExternalMsg {
AddNodeFilter(NodeFilterApplicable), AddNodeFilter(NodeFilterApplicable),
/// Remove an existing [filter](https://xplr.dev/en/filtering#filter). /// Remove an existing [filter](https://xplr.dev/en/filtering#filter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { RemoveNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" } /// Type: { RemoveNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
/// ///
@ -775,7 +791,7 @@ pub enum ExternalMsg {
/// Remove a [filter](https://xplr.dev/en/filtering#filter) if it exists, /// Remove a [filter](https://xplr.dev/en/filtering#filter) if it exists,
/// else, add a it. /// else, add a it.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { ToggleNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" } /// Type: { ToggleNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
/// ///
@ -787,7 +803,7 @@ pub enum ExternalMsg {
/// Add a node [filter](https://xplr.dev/en/filtering#filter) reading the /// Add a node [filter](https://xplr.dev/en/filtering#filter) reading the
/// input from the buffer. /// input from the buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { AddNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) } /// Type: { AddNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
/// ///
@ -799,7 +815,7 @@ pub enum ExternalMsg {
/// Remove a node [filter](https://xplr.dev/en/filtering#filter) reading /// Remove a node [filter](https://xplr.dev/en/filtering#filter) reading
/// the input from the buffer. /// the input from the buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { RemoveNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) } /// Type: { RemoveNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
/// ///
@ -810,7 +826,7 @@ pub enum ExternalMsg {
RemoveNodeFilterFromInput(NodeFilter), RemoveNodeFilterFromInput(NodeFilter),
/// Remove the last node [filter](https://xplr.dev/en/filtering). /// Remove the last node [filter](https://xplr.dev/en/filtering).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -820,7 +836,7 @@ pub enum ExternalMsg {
/// Reset the node [filters](https://xplr.dev/en/filtering) back to the /// Reset the node [filters](https://xplr.dev/en/filtering) back to the
/// default configuration. /// default configuration.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -829,7 +845,7 @@ pub enum ExternalMsg {
ResetNodeFilters, ResetNodeFilters,
/// Clear all the node [filters](https://xplr.dev/en/filtering). /// Clear all the node [filters](https://xplr.dev/en/filtering).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -841,7 +857,7 @@ pub enum ExternalMsg {
/// Add a [sorter](https://xplr.dev/en/sorting#sorter) to sort nodes while /// Add a [sorter](https://xplr.dev/en/sorting#sorter) to sort nodes while
/// exploring directories. /// exploring directories.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { AddNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } } /// Type: { AddNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
/// ///
@ -852,7 +868,7 @@ pub enum ExternalMsg {
AddNodeSorter(NodeSorterApplicable), AddNodeSorter(NodeSorterApplicable),
/// Remove an existing [sorter](https://xplr.dev/en/sorting#sorter). /// Remove an existing [sorter](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { RemoveNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) } /// Type: { RemoveNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
/// ///
@ -863,7 +879,7 @@ pub enum ExternalMsg {
RemoveNodeSorter(NodeSorter), RemoveNodeSorter(NodeSorter),
/// Reverse a node [sorter](https://xplr.dev/en/sorting#sorter). /// Reverse a node [sorter](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { ReverseNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) } /// Type: { ReverseNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
/// ///
@ -875,7 +891,7 @@ pub enum ExternalMsg {
/// Remove a [sorter](https://xplr.dev/en/sorting#sorter) if it exists, /// Remove a [sorter](https://xplr.dev/en/sorting#sorter) if it exists,
/// else, add a it. /// else, add a it.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Type: { ToggleNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } } /// Type: { ToggleNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
/// ///
@ -886,7 +902,7 @@ pub enum ExternalMsg {
ToggleNodeSorter(NodeSorterApplicable), ToggleNodeSorter(NodeSorterApplicable),
/// Reverse the node [sorters](https://xplr.dev/en/sorting#sorter). /// Reverse the node [sorters](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -895,7 +911,7 @@ pub enum ExternalMsg {
ReverseNodeSorters, ReverseNodeSorters,
/// Remove the last node [sorter](https://xplr.dev/en/sorting#sorter). /// Remove the last node [sorter](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -905,7 +921,7 @@ pub enum ExternalMsg {
/// Reset the node [sorters](https://xplr.dev/en/sorting#sorter) back to /// Reset the node [sorters](https://xplr.dev/en/sorting#sorter) back to
/// the default configuration. /// the default configuration.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -914,7 +930,7 @@ pub enum ExternalMsg {
ResetNodeSorters, ResetNodeSorters,
/// Clear all the node [sorters](https://xplr.dev/en/sorting#sorter). /// Clear all the node [sorters](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -925,7 +941,7 @@ pub enum ExternalMsg {
/// ### Search Operations -------------------------------------------------- /// ### Search Operations --------------------------------------------------
/// Search files using the current or default (fuzzy) search algorithm. /// Search files using the current or default (fuzzy) search algorithm.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Type: { Search = "string" } /// Type: { Search = "string" }
@ -946,7 +962,7 @@ pub enum ExternalMsg {
/// Search files using fuzzy match algorithm. /// Search files using fuzzy match algorithm.
/// It keeps the filters, but overrides the sorters. /// It keeps the filters, but overrides the sorters.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Type: { SearchFuzzy = "string" } /// Type: { SearchFuzzy = "string" }
@ -958,7 +974,7 @@ pub enum ExternalMsg {
SearchFuzzy(String), SearchFuzzy(String),
/// Calls `SearchFuzzy` with the input taken from the input buffer. /// Calls `SearchFuzzy` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Example: /// Example:
@ -968,7 +984,7 @@ pub enum ExternalMsg {
SearchFuzzyFromInput, SearchFuzzyFromInput,
/// Like `SearchFuzzy`, but doesn't not perform rank based sorting. /// Like `SearchFuzzy`, but doesn't not perform rank based sorting.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Type: { SearchFuzzyUnordered = "string" } /// Type: { SearchFuzzyUnordered = "string" }
@ -980,7 +996,7 @@ pub enum ExternalMsg {
SearchFuzzyUnordered(String), SearchFuzzyUnordered(String),
/// Calls `SearchFuzzyUnordered` with the input taken from the input buffer. /// Calls `SearchFuzzyUnordered` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Example: /// Example:
@ -991,7 +1007,7 @@ pub enum ExternalMsg {
/// Search files using regex match algorithm. /// Search files using regex match algorithm.
/// It keeps the filters, but overrides the sorters. /// It keeps the filters, but overrides the sorters.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Type: { SearchRegex = "string" } /// Type: { SearchRegex = "string" }
@ -1003,7 +1019,7 @@ pub enum ExternalMsg {
SearchRegex(String), SearchRegex(String),
/// Calls `SearchRegex` with the input taken from the input buffer. /// Calls `SearchRegex` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Example: /// Example:
@ -1013,7 +1029,7 @@ pub enum ExternalMsg {
SearchRegexFromInput, SearchRegexFromInput,
/// Like `SearchRegex`, but doesn't not perform rank based sorting. /// Like `SearchRegex`, but doesn't not perform rank based sorting.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Type: { SearchRegexUnordered = "string" } /// Type: { SearchRegexUnordered = "string" }
@ -1025,7 +1041,7 @@ pub enum ExternalMsg {
SearchRegexUnordered(String), SearchRegexUnordered(String),
/// Calls `SearchRegexUnordered` with the input taken from the input buffer. /// Calls `SearchRegexUnordered` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory. /// It gets reset automatically when changing directory.
/// ///
/// Example: /// Example:
@ -1036,7 +1052,7 @@ pub enum ExternalMsg {
/// Toggles between different search algorithms, without changing the input /// Toggles between different search algorithms, without changing the input
/// buffer /// buffer
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -1045,7 +1061,7 @@ pub enum ExternalMsg {
ToggleSearchAlgorithm, ToggleSearchAlgorithm,
/// Enables ranked search without changing the input buffer. /// Enables ranked search without changing the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -1054,7 +1070,7 @@ pub enum ExternalMsg {
EnableSearchOrder, EnableSearchOrder,
/// Disabled ranked search without changing the input buffer. /// Disabled ranked search without changing the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitely. /// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// ///
/// Example: /// Example:
/// ///
@ -1135,7 +1151,7 @@ pub enum ExternalMsg {
/// - YAML: `StopFifo` /// - YAML: `StopFifo`
StopFifo, StopFifo,
/// Toggle betwen {Start|Stop}Fifo /// Toggle between {Start|Stop}Fifo
/// ///
/// Type: { ToggleFifo = "string" } /// Type: { ToggleFifo = "string" }
/// ///

@ -7,4 +7,5 @@ pub enum InternalMsg {
AddLastFocus(String, Option<String>), AddLastFocus(String, Option<String>),
SetDirectory(DirectoryBuffer), SetDirectory(DirectoryBuffer),
HandleKey(Key), HandleKey(Key),
RefreshSelection,
} }

@ -17,7 +17,6 @@ pub enum MsgOut {
CallLuaSilently(String), CallLuaSilently(String),
LuaEval(String), LuaEval(String),
LuaEvalSilently(String), LuaEvalSilently(String),
Enque(Task),
EnableMouse, EnableMouse,
DisableMouse, DisableMouse,
ToggleMouse, ToggleMouse,
@ -34,4 +33,5 @@ pub enum MsgOut {
PrintSelectionAndQuit, PrintSelectionAndQuit,
PrintResultAndQuit, PrintResultAndQuit,
PrintAppStateAndQuit, PrintAppStateAndQuit,
Enqueue(Task),
} }

@ -1,3 +1,4 @@
use crate::dirs;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -38,8 +39,8 @@ where
} }
(None, _) => comps.push(Component::ParentDir), (None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (), (Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a), (Some(a), Some(Component::CurDir)) => comps.push(a),
(Some(_), Some(b)) if b == Component::ParentDir => { (Some(_), Some(Component::ParentDir)) => {
let path = path.to_string_lossy(); let path = path.to_string_lossy();
let base = base.to_string_lossy(); let base = base.to_string_lossy();
bail!("{base} is not a parent of {path}") bail!("{base} is not a parent of {path}")
@ -215,17 +216,20 @@ mod tests {
#[test] #[test]
fn test_relative_to_parent() { fn test_relative_to_parent() {
let path = std::env::current_dir().unwrap(); let path = std::env::current_dir().unwrap().join("docs");
let parent = path.parent().unwrap(); let parent = path.parent().unwrap();
let relative = relative_to(parent, NONE).unwrap(); let base = default().with_base(path.to_str().unwrap());
let relative = relative_to(parent, Some(&base)).unwrap();
assert_eq!(relative, PathBuf::from("..")); assert_eq!(relative, PathBuf::from(".."));
let relative = relative_to(parent, Some(&default().with_prefix_dots())).unwrap(); let relative =
relative_to(parent, Some(&base.clone().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("..")); assert_eq!(relative, PathBuf::from(".."));
let relative = let relative =
relative_to(parent, Some(&default().without_suffix_dots())).unwrap(); relative_to(parent, Some(&base.clone().without_suffix_dots())).unwrap();
assert_eq!( assert_eq!(
relative, relative,
PathBuf::from("../..").join(parent.file_name().unwrap()) PathBuf::from("../..").join(parent.file_name().unwrap())
@ -233,7 +237,7 @@ mod tests {
let relative = relative_to( let relative = relative_to(
parent, parent,
Some(&default().with_prefix_dots().without_suffix_dots()), Some(&base.clone().with_prefix_dots().without_suffix_dots()),
) )
.unwrap(); .unwrap();
assert_eq!( assert_eq!(

@ -8,7 +8,8 @@ use crate::explorer;
use crate::lua; use crate::lua;
use crate::pipe; use crate::pipe;
use crate::pwd_watcher; use crate::pwd_watcher;
use crate::ui; use crate::ui::NO_COLOR;
use crate::ui::UI;
use crate::yaml; use crate::yaml;
use anyhow::{bail, Error, Result}; use anyhow::{bail, Error, Result};
use crossterm::event; use crossterm::event;
@ -40,15 +41,14 @@ pub fn get_tty() -> Result<fs::File> {
// returns physical path. As a workaround, this function tries to use `PWD` // returns physical path. As a workaround, this function tries to use `PWD`
// environment variable that is configured by shell. // environment variable that is configured by shell.
fn get_current_dir() -> Result<PathBuf, std::io::Error> { fn get_current_dir() -> Result<PathBuf, std::io::Error> {
let cur = std::env::current_dir();
if let Ok(pwd) = std::env::var("PWD") { if let Ok(pwd) = std::env::var("PWD") {
if pwd.is_empty() { if pwd.is_empty() {
cur std::env::current_dir()
} else { } else {
Ok(PathBuf::from(pwd)) Ok(PathBuf::from(pwd))
} }
} else { } else {
cur std::env::current_dir()
} }
} }
@ -286,7 +286,7 @@ impl Runner {
tx_pwd_watcher.send(app.pwd.clone())?; tx_pwd_watcher.send(app.pwd.clone())?;
let mut result = Ok(None); let mut result = Ok(None);
let session_path = app.session_path.to_owned(); let session_path = app.session_path.clone();
term::enable_raw_mode()?; term::enable_raw_mode()?;
@ -342,6 +342,9 @@ impl Runner {
None, None,
))?; ))?;
// UI
let mut ui = UI::new(&lua);
'outer: for task in rx_msg_in { 'outer: for task in rx_msg_in {
match app.handle_task(task) { match app.handle_task(task) {
Ok(a) => { Ok(a) => {
@ -349,9 +352,7 @@ impl Runner {
while let Some(msg) = app.msg_out.pop_front() { while let Some(msg) = app.msg_out.pop_front() {
use app::MsgOut::*; use app::MsgOut::*;
match msg { match msg {
// NOTE: Do not schedule critical tasks via tx_msg_in in this loop. Enqueue(task) => {
// Try handling them immediately.
Enque(task) => {
tx_msg_in.send(task)?; tx_msg_in.send(task)?;
} }
@ -414,7 +415,7 @@ impl Runner {
} }
ScrollUpHalf => { ScrollUpHalf => {
app = app.focus_next_by_relative_index( app = app.focus_previous_by_relative_index(
terminal.size()?.height as usize / 2, terminal.size()?.height as usize / 2,
)?; )?;
} }
@ -473,13 +474,13 @@ impl Runner {
} }
if app.pwd != last_pwd { if app.pwd != last_pwd {
last_pwd = app.pwd.clone(); last_pwd.clone_from(&app.pwd);
// $PWD watcher // $PWD watcher
tx_pwd_watcher.send(app.pwd.clone())?; tx_pwd_watcher.send(app.pwd.clone())?;
// OSC 7: Change CWD // OSC 7: Change CWD
if !(*ui::NO_COLOR) { if !(*NO_COLOR) {
write!( write!(
terminal.backend_mut(), terminal.backend_mut(),
"\x1b]7;file://{}{}\x1b\\", "\x1b]7;file://{}{}\x1b\\",
@ -496,7 +497,7 @@ impl Runner {
} }
// UI // UI
terminal.draw(|f| ui::draw(f, &app, &lua))?; terminal.draw(|f| ui.draw(f, &app))?;
} }
EnableMouse => { EnableMouse => {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save