Compare commits

..

No commits in common. 'main' and 'v0.19.1' have entirely different histories.

@ -1,14 +1,4 @@
# Why dynamic linking?
# See https://github.com/sayanarijit/xplr/issues/309
[target.x86_64-unknown-linux-gnu]
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"]

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

@ -1 +0,0 @@
use flake

@ -3,7 +3,7 @@ name: Continuous Deployment
on:
push:
tags:
- "v*.*.*"
- 'v*.*.*'
workflow_dispatch:
jobs:
@ -12,84 +12,72 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
build:
- macos
- macos-aarch64
- linux
- linux-musl
- linux-aarch64
- linux-arm
os:
- macos-latest
- ubuntu-latest
- ubuntu-20.04
rust: [stable]
include:
# See the list: https://github.com/cross-rs/cross
- build: macos
os: macos-latest
- os: macos-latest
artifact_prefix: macos
target: x86_64-apple-darwin
binary_postfix: ''
- build: macos-aarch64
os: macos-latest
target: aarch64-apple-darwin
- build: linux
os: ubuntu-latest
- os: ubuntu-latest
artifact_prefix: linux
target: x86_64-unknown-linux-gnu
binary_postfix: ''
- build: linux-musl
os: ubuntu-latest
- os: ubuntu-20.04
artifact_prefix: linux-musl
target: x86_64-unknown-linux-musl
binary_postfix: ''
- build: linux-aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
# Will see later
- build: linux-arm
os: ubuntu-latest
target: arm-unknown-linux-gnueabihf
# - os: ubuntu-latest
# artifact_prefix: x86_64-android
# target: x86_64-linux-android
# binary_postfix: ''
#
# - os: ubuntu-latest
# artifact_prefix: aarch64-android
# target: aarch64-linux-android
# binary_postfix: ''
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Installing Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- name: Installing needed macOS dependencies
if: matrix.os == 'macos-latest'
run: brew install openssl@1.1
- name: Installing needed Ubuntu dependencies
if: matrix.os == 'ubuntu-latest'
if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-20.04'
run: |
sudo apt-get update --fix-missing
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
- if: matrix.build == 'linux-aarch64'
run: sudo apt-get install -y gcc-aarch64-linux-gnu
sudo apt-get update
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- 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: Checking out sources
uses: actions/checkout@v1
- name: Running cargo build
run: cargo build --locked --release --target ${{ matrix.target }}
# - name: Running snapcraft build
# run: |
# snapcraft
# printf ' [ INFO ] generated <snapcraft> files include:\n'
# command ls -Al | grep "\.snap" | awk '{ print $9 }'
# mv ./*.snap ./xplr.snap
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
toolchain: ${{ matrix.rust }}
args: --locked --release --target ${{ matrix.target }}
- name: Install gpg secret key
run: |
@ -100,8 +88,9 @@ jobs:
shell: bash
run: |
cd target/${{ matrix.target }}/release
BINARY_NAME=xplr
RELEASE_NAME=$BINARY_NAME-${{ matrix.build }}
BINARY_NAME=xplr${{ matrix.binary_postfix }}
strip $BINARY_NAME
RELEASE_NAME=xplr-${{ matrix.artifact_prefix }}
tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME
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
@ -110,23 +99,17 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: |
target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.tar.gz
target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.sha256
target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.tar.gz.asc
xplr.snap
target/${{ matrix.target }}/release/xplr-${{ matrix.artifact_prefix }}.tar.gz
target/${{ matrix.target }}/release/xplr-${{ matrix.artifact_prefix }}.sha256
target/${{ matrix.target }}/release/xplr-${{ matrix.artifact_prefix }}.tar.gz.asc
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Cleaning snapcraft
# run: |
# command rm --verbose ./*.snap
# snapcraft clean
publish-gpg-signature:
name: Publishing GPG signature
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Install gpg secret key
run: |
cat <(echo -e "${{ secrets.GPG_SECRET }}") | gpg --batch --import
@ -135,7 +118,7 @@ jobs:
- name: Signing archive with GPG
run: |
VERSION=${GITHUB_REF##*v}
git -c tar.tar.gz.command='gzip -cn' archive -o xplr-${VERSION:?}.tar.gz --format tar.gz --prefix "xplr-${VERSION:?}/" "v${VERSION}"
git archive -o xplr-${VERSION:?}.tar.gz --format tar.gz --prefix "xplr-${VERSION:?}/" "v${VERSION}"
cat <(echo "${{ secrets.GPG_PASS }}") | gpg --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor "xplr-${VERSION:?}.tar.gz"
mv "xplr-${VERSION:?}.tar.gz.asc" "source.tar.gz.asc"
@ -151,16 +134,20 @@ jobs:
name: Publishing to Cargo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- run: |
sudo apt-get update --fix-missing
sudo apt-get update
sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- run: cargo publish --allow-dirty
- uses: actions-rs/cargo@v1
with:
command: publish
args: --allow-dirty
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_KEY }}

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

@ -1,23 +0,0 @@
name: "Push Binary Cache for Nix"
on:
pull_request:
push:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: nixbuild/nix-quick-install-action@v19
with:
nix_conf: experimental-features = nix-command flakes
- uses: cachix/cachix-action@v11
with:
name: xplr
authtoken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix profile install .
- name: Run tests
run: |
xplr --version

9
.gitignore vendored

@ -14,12 +14,3 @@ book/
.idea/
.venv/
# direnv
.direnv/
# nix
result
# test files
/init.lua

@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socioeconomic status,
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
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
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
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
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
## Enforcement Responsibilities
@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**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.
**Consequence**: A permanent ban from any sort of public interaction within

1453
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@ path = './benches/criterion.rs'
[package]
name = 'xplr'
version = '0.21.9'
version = '0.19.1'
authors = ['Arijit Basu <hi@arijitbasu.in>']
edition = '2021'
description = 'A hackable, minimal, fast TUI file explorer'
@ -22,31 +22,19 @@ categories = ['command-line-interface', 'command-line-utilities']
include = ['src/**/*', 'docs/en/src/**/*', 'LICENSE', 'README.md']
[dependencies]
libc = "0.2.155"
humansize = "2.1.3"
libc = "0.2.132"
humansize = "1.1.1"
natord = "1.0.9"
anyhow = "1.0.86"
serde_yaml = "0.9.34"
crossterm = { version = "0.27.0", features = [], default-features = false }
ansi-to-tui = "=3.1.0"
regex = "1.10.5"
gethostname = "0.4.3"
serde_json = "1.0.117"
path-absolutize = "3.1.1"
which = "6.0.1"
nu-ansi-term = "0.50.0"
textwrap = "0.16"
snailquote = "0.3.1"
skim = { version = "0.10.4", default-features = false }
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]
version = "0.17.0"
default-features = false
features = ["nu-ansi-term"]
anyhow = "1.0.64"
# Let's keep this locked. See https://docs.rs/serde_yaml/0.9.11/serde_yaml/
serde_yaml = "0.8.26"
crossterm = "0.25.0"
dirs = "4.0.0"
ansi-to-tui = "1.0.1"
regex = "1.6.0"
gethostname = "0.2.3"
[dependencies.lazy_static]
version = "1.4.0"
@ -54,41 +42,40 @@ default-features = false
[dependencies.mime_guess]
version = "2.0.4"
default-features = false
features = ["rev-mappings"]
[dependencies.tui]
version = "=0.26.1" # https://github.com/ratatui-org/ratatui/issues/1032
version = "0.18.0"
default-features = false
features = ['crossterm', 'serde']
package = 'ratatui'
[dependencies.serde]
version = "1.0.203"
features = []
default-features = false
version = "1.0.144"
features = ['derive']
[dependencies.chrono]
version = "0.4.22"
features = ['serde']
[dependencies.indexmap]
version = "2.2.6"
version = "1.9.1"
features = ['serde']
[dependencies.mlua]
version = "0.9.8"
features = ['luajit', 'serialize', 'send']
version = "0.8.3"
features = ['luajit', 'vendored', 'serialize', 'send']
[dependencies.tui-input]
version = "0.8.0"
version = "0.5.1"
features = ['serde']
[dev-dependencies]
criterion = "0.5.1"
assert_cmd = "2.0.14"
criterion = "0.3.6"
assert_cmd = "2.0.4"
[profile.release]
lto = true
codegen-units = 1
panic = 'abort'
strip = true
[features]
default = ["vendored-lua"]
vendored-lua = ["mlua/vendored"]

@ -12,6 +12,18 @@ A hackable, minimal, fast TUI file explorer
<img src="https://img.shields.io/crates/v/xplr.svg" />
</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 align="center">
@ -26,6 +38,7 @@ 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-plugins">Plugins</a>]
[<a href="https://xplr.dev/en/awesome-integrations">Integrations</a>]
[<a href="https://xplr.dev/en/community">Community</a>]
</h3>
xplr is a terminal UI based file explorer that aims to increase our terminal
@ -45,14 +58,8 @@ integration][15], enabling you to achieve insane terminal productivity.
- [[VIDEO] XPLR: Insanely Hackable Lua File Manager ~ Brodie Robertson](https://youtu.be/MaVRtYh1IRU)
- [[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
Package maintainers please refer to the [RELEASE.md](./RELEASE.md).
<a href="https://repology.org/project/xplr/versions"><img src="https://repology.org/badge/vertical-allrepos/xplr.svg" /></a>
## Backers

@ -1,17 +1,7 @@
# Build
See [install.md](./docs/en/src/install.md#build-from-source)
Note: xplr ships with vendored luajit. If the platform can't compile this,
you need to compile using `--no-default-features` argument to avoid using
vendored luajit, so that you can static link luajit yourself.
# Release
The final binary `target/release/xplr` can be shipped with the following assets
- [License](./LICENSE)
- [Desktop Entry](./assets/desktop/xplr.desktop)
- [Desktop Icons](./assets/icon/)
- [Offline Docs](./docs/en/src)
- [Lua Configuration Example](./src/init.lua)
Process
-------
- [ ] Update `Cargo.toml` version.
- [ ] Update `config.yml` version.
- [ ] Update `Config::is_compatible()` and `Config::upgrade_notification()`.
- [ ] Publish GitHub tag.
- [ ] Update Wiki and docs.

@ -1,7 +1,7 @@
[Desktop Entry]
Type=Application
Name=xplr
Comment=Terminal file explorer
Comment=Terminal file manager
Exec=xplr
Terminal=true
Icon=xplr

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

@ -1,10 +0,0 @@
(import
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{ src = ./.; }
).defaultNix

@ -10,7 +10,6 @@
- [Layouts][9]
- [Modes][7]
- [Concept][32]
- [Sum Type][42]
- [Key Bindings][27]
- [Configure Key Bindings][28]
- [Default Key Bindings][14]
@ -23,12 +22,10 @@
- [Input Operation][39]
- [Borders][31]
- [Style][11]
- [Searching][41]
- [Sorting][12]
- [Filtering][13]
- [Column Renderer][26]
- [Lua Function Calls][36]
- [xplr.util][40]
- [Environment Variables and Pipes][37]
- [Awesome Hacks][30]
- [Plugin][15]
@ -37,8 +34,11 @@
- [Awesome Plugins][18]
- [Integration][19]
- [Awesome Integrations][20]
- [TODO][21]
- [Alternatives][22]
- [Upgrade Guide][23]
- [Community][24]
- [Contribute][25]
[1]: introduction.md
[2]: quickstart.md
@ -60,8 +60,11 @@
[18]: awesome-plugins.md
[19]: integration.md
[20]: awesome-integrations.md
[21]: todo.md
[22]: alternatives.md
[23]: upgrade-guide.md
[24]: community.md
[25]: contribute.md
[26]: column-renderer.md
[27]: key-bindings.md
[28]: configure-key-bindings.md
@ -76,6 +79,3 @@
[37]: environment-variables-and-pipes.md
[38]: messages.md
[39]: input-operation.md
[40]: xplr.util.md
[41]: searching.md
[42]: sum-type.md

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

@ -6,14 +6,7 @@ too small or too niche for a full fledge [plugin][2].
Do you have something cool to share?
[Edit this file][3] or [share them here][4].
You can try these hacks by writing them to a file, say `hack.lua` and passing
it to xplr with `--extra-config` or `-C`.
```bash
xplr -C hack.lua
```
[Edit this file][3] or [share them here][4] or [let us know][5].
### cd on quit
@ -25,9 +18,6 @@ Change directory using xplr.
- Author: [@sayanarijit][8]
- Tested on: Linux
NOTE: This is a shell hack, rather than Lua config hack. Add this in
`.bashrc` or `.profile` file in your home directory.
With this alias set, you can navigate directories using xplr by entering
xcd command, and when you quit by pressing enter, you will enter the
directory.
@ -90,13 +80,12 @@ xplr.config.modes.builtin.default.key_bindings.on_key.m = {
help = "bookmark",
messages = {
{
BashExecSilently0 = [===[
BashExecSilently = [===[
PTH="${XPLR_FOCUS_PATH:?}"
PTH_ESC=$(printf %q "$PTH")
if echo "${PTH:?}" >> "${XPLR_SESSION_PATH:?}/bookmarks"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC added to bookmarks"
echo "LogSuccess: ${PTH:?} added to bookmarks" >> "${XPLR_PIPE_MSG_IN:?}"
else
"$XPLR" -m 'LogError: %q' "Failed to bookmark $PTH_ESC"
echo "LogError: Failed to bookmark ${PTH:?}" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===],
},
@ -107,11 +96,10 @@ xplr.config.modes.builtin.default.key_bindings.on_key["`"] = {
help = "go to bookmark",
messages = {
{
BashExec0 = [===[
BashExec = [===[
PTH=$(cat "${XPLR_SESSION_PATH:?}/bookmarks" | fzf --no-sort)
PTH_ESC=$(printf %q "$PTH")
if [ "$PTH" ]; then
"$XPLR" -m 'FocusPath: %q' "$PTH"
echo FocusPath: "'"${PTH:?}"'" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===],
},
@ -135,14 +123,7 @@ bookmark can be added, deleted, or jumped to.
- Tested on: MacOS
```lua
-- With `export XPLR_BOOKMARK_FILE="$HOME/bookmarks"`
-- Bookmark: mode binding
xplr.config.modes.builtin.default.key_bindings.on_key["b"] = {
help = "bookmark mode",
messages = {
{ SwitchModeCustom = "bookmark" },
},
}
xplr.config.modes.custom.bookmark = {
name = "bookmark",
key_bindings = {
@ -150,49 +131,43 @@ xplr.config.modes.custom.bookmark = {
m = {
help = "bookmark dir",
messages = {
{
BashExecSilently0 = [[
{ BashExecSilently = [[
PTH="${XPLR_FOCUS_PATH:?}"
if [ -d "${PTH}" ]; then
PTH="${PTH}"
elif [ -f "${PTH}" ]; then
PTH=$(dirname "${PTH}")
PTH="$(dirname "${PTH}")"
fi
PTH_ESC=$(printf %q "$PTH")
if echo "${PTH:?}" >> "${XPLR_BOOKMARK_FILE:?}"; then
"$XPLR" -m 'LogSuccess: %q' "$PTH_ESC added to bookmarks"
echo "LogSuccess: ${PTH:?} added to bookmarks" >> "${XPLR_PIPE_MSG_IN:?}"
else
"$XPLR" -m 'LogError: %q' "Failed to bookmark $PTH_ESC"
echo "LogError: Failed to bookmark ${PTH:?}" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]],
]]
},
"PopMode",
},
},
g = {
help = "go to bookmark",
messages = {
{
BashExec0 = [===[
BashExec = [===[
PTH=$(cat "${XPLR_BOOKMARK_FILE:?}" | fzf --no-sort)
if [ "$PTH" ]; then
"$XPLR" -m 'FocusPath: %q' "$PTH"
echo FocusPath: "'"${PTH:?}"'" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===],
]===]
},
"PopMode",
},
},
d = {
help = "delete bookmark",
messages = {
{
BashExec0 = [[
{ BashExec = [[
PTH=$(cat "${XPLR_BOOKMARK_FILE:?}" | fzf --no-sort)
sd "$PTH\n" "" "${XPLR_BOOKMARK_FILE:?}"
]],
]]
},
"PopMode",
},
},
esc = {
@ -204,7 +179,6 @@ xplr.config.modes.custom.bookmark = {
},
},
}
```
</details>
@ -225,7 +199,7 @@ xplr.config.modes.builtin.go_to.key_bindings.on_key.b = {
help = "bookmark jump",
messages = {
"PopMode",
{ BashExec0 = [===[
{ BashExec = [===[
field='\(\S\+\s*\)'
esc=$(printf '\033')
N="${esc}[0m"
@ -242,7 +216,7 @@ xplr.config.modes.builtin.go_to.key_bindings.on_key.b = {
--preview-window="right:50%" \
| sed 's#.*-> ##')
if [ "$PTH" ]; then
"$XPLR" -m 'ChangeDirectory: %q' "$PTH"
echo ChangeDirectory: "'"${PTH:?}"'" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===]
},
@ -269,10 +243,10 @@ xplr.config.modes.builtin.go_to.key_bindings.on_key.h = {
messages = {
"PopMode",
{
BashExec0 = [===[
PTH=$(cat "${XPLR_PIPE_HISTORY_OUT:?}" | sort -z -u | fzf --read0)
BashExec = [===[
PTH=$(cat "${XPLR_PIPE_HISTORY_OUT:?}" | sort -u | fzf --no-sort)
if [ "$PTH" ]; then
"$XPLR" -m 'ChangeDirectory: %q' "$PTH"
echo ChangeDirectory: "'"${PTH:?}"'" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===],
},
@ -305,7 +279,7 @@ xplr.config.modes.builtin.default.key_bindings.on_key.R = {
NODES=${SELECTION:-$(cat "${XPLR_PIPE_DIRECTORY_NODES_OUT:?}")}
if [ "$NODES" ]; then
echo -e "$NODES" | renamer
"$XPLR" -m ExplorePwdAsync
echo ExplorePwdAsync >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===],
},
@ -331,7 +305,7 @@ xplr.config.modes.builtin.default.key_bindings.on_key.S = {
help = "serve $PWD",
messages = {
{
BashExec0 = [===[
BashExec = [===[
IP=$(ip addr | grep -w inet | cut -d/ -f1 | grep -Eo '[0-9]{1,3}\.[0-9]{ 1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | fzf --prompt 'Select IP > ')
echo "IP: ${IP:?}"
read -p "Port (default 5000): " PORT
@ -363,16 +337,16 @@ xplr.config.modes.builtin.default.key_bindings.on_key.P = {
help = "preview",
messages = {
{
BashExecSilently0 = [===[
BashExecSilently = [===[
FIFO_PATH="/tmp/xplr.fifo"
if [ -e "$FIFO_PATH" ]; then
"$XPLR" -m StopFifo
echo StopFifo >> "$XPLR_PIPE_MSG_IN"
rm -f -- "$FIFO_PATH"
else
mkfifo "$FIFO_PATH"
"$HOME/.local/bin/imv-open.sh" "$FIFO_PATH" "$XPLR_FOCUS_PATH" &
"$XPLR" -m 'StartFifo: %q' "$FIFO_PATH"
echo "StartFifo: '$FIFO_PATH'" >> "$XPLR_PIPE_MSG_IN"
fi
]===],
},
@ -423,58 +397,31 @@ Preview text files in a native xplr pane (should be fast enough).
```lua
local function stat(node)
return xplr.util.to_yaml(xplr.util.node(node.absolute_path))
return node.mime_essence
end
local function read(path, height)
local function read(path, lines)
local out = ""
local p = io.open(path)
if p == nil then
return nil
return stat(path)
end
local i = 0
local res = ""
for line in p:lines() do
if line:match("[^ -~\n\t]") then
p:close()
return
end
res = res .. line .. "\n"
if i == height then
out = out .. line .. "\n"
if i == lines then
break
end
i = i + 1
end
p:close()
return res
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
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 } }
return out
end
local preview_pane = { Dynamic = "custom.preview_pane.render" }
local split_preview = {
xplr.config.layouts.builtin.default = {
Horizontal = {
config = {
constraints = {
@ -484,13 +431,34 @@ local split_preview = {
},
splits = {
"Table",
preview_pane,
{
CustomContent = {
title = "preview",
body = { DynamicParagraph = { render = "custom.preview_pane.render" } },
},
},
},
},
}
xplr.config.layouts.builtin.default =
xplr.util.layout_replace(xplr.config.layouts.builtin.default, "Table", split_preview)
xplr.fn.custom.preview_pane = {}
xplr.fn.custom.preview_pane.render = function(ctx)
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>
@ -510,7 +478,7 @@ Navigate using the [tere][19] file explorer (defaults to type-to-nav system).
xplr.config.modes.builtin.default.key_bindings.on_key.T = {
help = "tere nav",
messages = {
{ BashExec0 = [["$XPLR" -m 'ChangeDirectory: %q' "$(tere)"]] },
{ BashExec = [[echo ChangeDirectory: "'"$(tere)"'" >> "$XPLR_PIPE_MSG_IN"]] },
},
}
```
@ -526,6 +494,7 @@ xplr.config.modes.builtin.default.key_bindings.on_key.T = {
[2]: plugin.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
[5]: community.md
[6]: https://gifyu.com/image/rGSR
[7]: https://s4.gifyu.com/images/xplr-bookmark.gif
[8]: https://github.com/sayanarijit

@ -11,16 +11,12 @@ 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/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/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/tree-view.xplr**][61] Hackable tree view for xplr
- [**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,
with some tweaks.
- [**dtomvan/term.xplr**][39] Terminal integration for xplr.
- [**igorepst/term.xplr**][39] Terminal integration for xplr
- [**sayanarijit/wl-clipboard.xplr**][52] Copy and paste with system clipboard using wl-clipboard
- [**dtomvan/xpm.xplr**][47] The XPLR Plugin Manager.
- [**emsquid/style.xplr**][57] Helper plugin that allows you to integrate xplr's [Style][58] anywhere.
- [**dtomvan/xpm.xplr**][47] The XPLR Plugin Manager
### Integration
@ -28,9 +24,9 @@ of the following plugins work for you, it's very easy to
- [**sayanarijit/dragon.xplr**][4] Drag and drop files using [dragon][5].
- [**sayanarijit/dua-cli.xplr**][6] Get the disk usage using [dua-cli][7] with selection
support.
- [**sayanarijit/fzf.xplr**][8] Fuzzy search using [fzf][9] to focus on a file or enter.
- [**sayanarijit/fzf.xplr**][8] Fuzzy search using [fzf][9] to focus on a file or enter
- [**sayanarijit/find.xplr**][44] An interactive finder plugin to complement [map.xplr][38].
- [**Junker/nuke.xplr**][53] Open files in apps by file type or mime.
- [**Junker/nuke.xplr**][53] Open files in apps by file type or mime
- [**sayanarijit/nvim-ctrl.xplr**][35] Send files to running Neovim sessions using
[nvim-ctrl][36].
- [**dtomvan/ouch.xplr**][40] This plugin uses [ouch][41] to compress and decompress files.
@ -41,6 +37,8 @@ of the following plugins work for you, it's very easy to
- [**sayanarijit/qrcp.xplr**][26] Send and receive files via QR code using [qrcp][27].
- [**sayanarijit/scp.xplr**][54] Integrate xplr with scp.
- [**sayanarijit/trash-cli.xplr**][13] Trash files and directories using [trash-cli][14].
- [**sayanarijit/xargs.xplr**][22] Batch execute commands on the focused or selected files
using `xargs`.
- [**sayanarijit/xclip.xplr**][15] Copy and paste with system clipboard using [xclip][16].
- [**sayanarijit/zoxide.xplr**][17] Change directory using the [zoxide][18] database.
@ -49,14 +47,9 @@ of the following plugins work for you, it's very easy to
- [**sayanarijit/material-landscape.xplr**][19] Material Landscape
- [**sayanarijit/material-landscape2.xplr**][20] Material Landscape 2
- [**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.
- [**dtomvan/extra-icons.xplr**][50] Adds more icons to icons.xplr, compatible
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:
@ -100,7 +93,7 @@ of the following plugins work for you, it's very easy to
[36]: https://github.com/chmln/nvim-ctrl
[37]: https://github.com/sayanarijit/command-mode.xplr
[38]: https://github.com/sayanarijit/map.xplr
[39]: https://github.com/dtomvan/term.xplr
[39]: https://github.com/igorepst/term.xplr
[40]: https://github.com/dtomvan/ouch.xplr
[41]: https://github.com/ouch-org/ouch
[42]: https://github.com/igorepst/context-switch.xplr
@ -116,13 +109,3 @@ of the following plugins work for you, it's very easy to
[52]: https://github.com/sayanarijit/wl-clipboard.xplr
[53]: https://github.com/Junker/nuke.xplr
[54]: https://github.com/sayanarijit/scp.xplr
[55]: https://github.com/sayanarijit/regex-search.xplr
[56]: https://github.com/sayanarijit/tri-pane.xplr
[57]: https://github.com/emsquid/style.xplr
[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,25 +4,21 @@ xplr allows customizing the shape and style of the borders.
### Border
A border is a [sum type][2] that can be one of the following:
A border can be one of the following:
- "Top"
- "Right"
- "Bottom"
- "Left"
- Top
- Right
- Bottom
- Left
### Border Type
A border type is a [sum type][2] that can be one of the following:
A border can be one of the following:
- "Plain"
- "Rounded"
- "Double"
- "Thick"
### Border Style
The [style][1] of the borders.
- Plain
- Rounded
- Double
- Thick
## Example
@ -32,6 +28,3 @@ xplr.config.general.panel_ui.default.border_type = "Thick"
xplr.config.general.panel_ui.default.border_style.fg = "Black"
xplr.config.general.panel_ui.default.border_style.bg = "Gray"
```
[1]: style.md#style
[2]: sum-type.md

@ -4,29 +4,6 @@ A column renderer is a Lua function that receives a [special argument][1] and
returns a string that will be displayed in each specific field of the
[files table][2].
## Example: Customizing Table Renderer
```lua
xplr.fn.custom.fmt_simple_column = function(m)
return m.prefix .. m.relative_path .. m.suffix
end
xplr.config.general.table.header.cols = {
{ format = " path" }
}
xplr.config.general.table.row.cols = {
{ format = "custom.fmt_simple_column" }
}
xplr.config.general.table.col_widths = {
{ Percentage = 100 }
}
-- With this config, you should only see a single column displaying the
-- relative paths.
```
xplr by default provides the following column renderers:
- `xplr.fn.builtin.fmt_general_table_row_cols_0`
@ -73,7 +50,6 @@ The special argument contains the following fields
- [is_selected][25]
- [is_focused][26]
- [total][27]
- [style][38]
- [meta][28]
### parent
@ -255,12 +231,6 @@ Type: integer
The total number of the nodes.
### style
Type: [Style][39]
The applicable [style object][39] for the node.
### meta
Type: mapping of string and string
@ -303,6 +273,29 @@ It contains the following fields.
- [uid][36]
- [gid][37]
## Example: Customizing Table Renderer
```lua
xplr.fn.custom.fmt_simple_column = function(m)
return m.prefix .. m.relative_path .. m.suffix
end
xplr.config.general.table.header.cols = {
{ format = " path" }
}
xplr.config.general.table.row.cols = {
{ format = "custom.fmt_simple_column" }
}
xplr.config.general.table.col_widths = {
{ Percentage = 100 }
}
-- With this config, you should only see a single column displaying the
-- relative paths.
```
[1]: #table-renderer-argument
[2]: layout.md#table
[3]: #parent
@ -340,5 +333,3 @@ It contains the following fields.
[35]: #last_modified
[36]: #uid
[37]: #gid
[38]: #style
[39]: style.md#style

@ -0,0 +1,12 @@
# 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

@ -76,81 +76,3 @@ in `xplr.fn.custom`.
You can also use nested tables such as
`xplr.fn.custom.my_plugin.my_function` to define custom functions.
## Hooks
This section of the configuration cannot be overwritten by another config
file or plugin, since this is an optional lua return statement specific to
each config file. It can be used to define things that should be explicit
for reasons like performance concerns, such as hooks.
Plugins should expose the hooks, and require users to subscribe to them
explicitly.
Example:
```lua
return {
-- Add messages to send when the xplr loads.
-- This is similar to the `--on-load` command-line option.
--
-- Type: list of [Message](https://xplr.dev/en/message#message)s
on_load = {
{ LogSuccess = "Configuration successfully loaded!" },
{ CallLuaSilently = "custom.some_plugin_with_hooks.on_load" },
},
-- Add messages to send when the directory changes.
--
-- Type: list of [Message](https://xplr.dev/en/message#message)s
on_directory_change = {
{ LogSuccess = "Changed directory" },
{ CallLuaSilently = "custom.some_plugin_with_hooks.on_directory_change" },
},
-- Add messages to send when the focus changes.
--
-- Type: list of [Message](https://xplr.dev/en/message#message)s
on_focus_change = {
{ LogSuccess = "Changed focus" },
{ CallLuaSilently = "custom.some_plugin_with_hooks.on_focus_change" },
}
-- Add messages to send when the mode is switched.
--
-- Type: list of [Message](https://xplr.dev/en/message#message)s
on_mode_switch = {
{ LogSuccess = "Switched mode" },
{ CallLuaSilently = "custom.some_plugin_with_hooks.on_mode_switch" },
}
-- Add messages to send when the layout is switched
--
-- Type: list of [Message](https://xplr.dev/en/message#message)s
on_layout_switch = {
{ LogSuccess = "Switched layout" },
{ 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
A key is a [sum type][36] can be one of the following:
A key can be one of the following:
- 0, 1, ... 9
- a, b, ... z
@ -155,12 +155,12 @@ xplr.config.modes.custom.fzxplr = {
messages = {
{
BashExec = [===[
PTH=$(cat "${XPLR_PIPE_DIRECTORY_NODES_OUT:?}" | awk -F/ '{print $NF}' | fzf)
if [ -d "$PTH" ]; then
"$XPLR" -m 'ChangeDirectory: %q' "$PTH"
else
"$XPLR" -m 'FocusPath: %q' "$PTH"
fi
PTH=$(cat "${XPLR_PIPE_DIRECTORY_NODES_OUT:?}" | awk -F/ '{print $NF}' | fzf)
if [ -d "$PTH" ]; then
echo ChangeDirectory: "'"${PWD:?}/${PTH:?}"'" >> "${XPLR_PIPE_MSG_IN:?}"
else
echo FocusPath: "'"${PWD:?}/${PTH:?}"'" >> "${XPLR_PIPE_MSG_IN:?}"
fi
]===]
},
"PopMode",
@ -180,10 +180,12 @@ As you can see, the key `F` in mode `fzxplr` (the name can be anything)
executes a script in `bash`.
`BashExec`, `PopMode`, `SwitchModeBuiltin`, `ChangeDirectory` and `FocusPath`
are [messages][18], `$XPLR`, `$XPLR_PIPE_DIRECTORY_NODES_OUT` are
[environment variables][22] exported by `xplr` before executing the command.
They contain the path to the [input][23] and [output][24] pipes that allows
external tools to interact with `xplr`.
are [messages][18], `$XPLR_PIPE_MSG_IN`,
`$XPLR_PIPE_DIRECTORY_NODES_OUT` are
[environment variables][22] exported by `xplr`
before executing the command. They contain the path to the
[input][23] and [output][24] pipes that
allows external tools to interact with `xplr`.
Now that we have our new mode ready, let's add an entry point to this mode via
the `default` mode.
@ -230,4 +232,3 @@ Visit [Awesome Plugins][27] for more [integration][28] options.
[33]: #on_character
[34]: #on_navigation
[35]: #on_function
[36]: sum-type.md

@ -0,0 +1,32 @@
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

@ -7,251 +7,127 @@ requirements.
When you press `?` in [default mode][3], you can see the complete list
of [modes][4] and the key mappings for each mode.
[1]: https://www.vim.org/
[2]: https://github.com/jarun/nnn/
[3]: #default
[4]: modes.md
### default
| key | remaps | action |
| --------- | ------ | ------------------- |
| ( | | prev deep branch |
| ) | | 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 |
### 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 |
| --- | ------ | ---------------- |
| f | | follow symlink |
| f1 | | global help menu |
| g | | top |
| i | | initial $PWD |
| p | | path |
| x | | open in gui |
### relative_path_does_match_regex
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
### action
### create_directory
| key | remaps | action |
| ----- | ------ | -------------------- |
| ! | | shell |
| 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 |
| key | remaps | action |
| ------ | ------ | ------------ |
| ctrl-c | | terminate |
| enter | | submit |
| esc | | cancel |
| tab | | try complete |
### default
| key | remaps | action |
| --------- | ------ | ------------------- |
| ( | | prev deep branch |
| ) | | 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
| key | remaps | action |
| ----- | ------ | ------------------- |
| enter | | open logs in editor |
| f1 | | global help menu |
| q | | quit |
| key | remaps | action |
| ------ | ------ | ------------------- |
| . | | show hidden |
| / | ctrl-f | search |
| : | | action |
| ? | | global help menu |
| G | | go to bottom |
| V | ctrl-a | select/unselect all |
| ctrl-c | | terminate |
| ctrl-d | | duplicate as |
| ctrl-i | tab | next visited path |
| ctrl-o | | last visited path |
| 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 |
| q | | quit |
| r | | rename |
| s | | sort |
| space | v | toggle selection |
| ~ | | go home |
| [0-9] | | input |
### create_directory
### duplicate_as
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
| key | remaps | action |
| ------ | ------ | --------- |
| ctrl-c | | terminate |
| enter | | duplicate |
| esc | | cancel |
### selection_ops
### relative_path_does_not_match_regex
| key | remaps | action |
| --- | ------ | ---------------- |
| c | | copy here |
| e | | edit selection |
| f1 | | global help menu |
| h | | hardlink here |
| l | | list selection |
| m | | move here |
| s | | softlink here |
| u | | clear selection |
| key | remaps | action |
| ------ | ------ | --------- |
| ctrl-c | | terminate |
| enter | | submit |
| esc | | cancel |
### relative_path_does_not_match_regex
### number
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| key | remaps | action |
| ------ | ------ | --------- |
| ctrl-c | | terminate |
| down | j | to down |
| enter | | to index |
| esc | | cancel |
| k | up | to up |
| [0-9] | | input |
### create_file
### search
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
| key | remaps | action |
| ------ | ------ | ---------------- |
| ctrl-c | | terminate |
| ctrl-n | down | down |
| ctrl-p | up | up |
| enter | esc | focus |
| left | | back |
| right | | enter |
| tab | | toggle selection |
### quit
| key | remaps | action |
| ----- | ------ | ----------------------- |
| enter | | just quit |
| f | | quit printing focus |
| f1 | | global help menu |
| p | | quit printing pwd |
| r | | quit printing result |
| s | | quit printing selection |
| key | remaps | action |
| ------ | ------ | ----------------------- |
| ctrl-c | | terminate |
| enter | | just quit |
| esc | | cancel |
| f | | quit printing focus |
| p | | quit printing pwd |
| r | | quit printing result |
| s | | quit printing selection |
### create
| key | remaps | action |
| --- | ------ | ---------------- |
| d | | create directory |
| f | | create file |
| f1 | | global help menu |
### vroot
| key | remaps | action |
| ------ | ------ | ---------------- |
| . | | vroot $PWD |
| / | | vroot / |
| ctrl-r | | reset vroot |
| ctrl-u | | unset vroot |
| f1 | | global help menu |
| v | | toggle vroot |
| ~ | | vroot $HOME |
| ctrl-c | | terminate |
| d | | create directory |
| esc | | cancel |
| f | | create file |
### search
### filter
| key | remaps | action |
| ------ | ------ | ----------------------- |
| ctrl-a | | toggle search algorithm |
| ctrl-f | | fuzzy search |
| ctrl-n | down | down |
| ctrl-p | up | up |
| ctrl-r | | regex search |
| ctrl-s | | sort (no search order) |
| ctrl-z | | toggle ordering |
| enter | | submit |
| esc | | cancel |
| f1 | | global help menu |
| left | | back |
| right | | enter |
| tab | | toggle selection |
| key | remaps | action |
| --------- | ------ | ---------------------------------- |
| R | | relative path does not match regex |
| backspace | | remove last filter |
| ctrl-c | | terminate |
| ctrl-r | | reset filters |
| ctrl-u | | clear filters |
| enter | esc | submit |
| r | | relative path does match regex |
### switch_layout
### delete
| key | remaps | action |
| --- | ------ | -------------------- |
| 1 | | default |
| 2 | | no help menu |
| 3 | | no selection panel |
| 4 | | no help or selection |
| f1 | | global help menu |
| key | remaps | action |
| ------ | ------ | ------------ |
| D | | force delete |
| ctrl-c | | terminate |
| d | | delete |
| esc | | cancel |
### sort
@ -267,82 +143,116 @@ of [modes][4] and the key mappings for each mode.
| S | | by size reverse |
| backspace | | remove last sorter |
| c | | by created |
| ctrl-c | | terminate |
| ctrl-r | | reset sorters |
| ctrl-u | | clear sorters |
| e | | by canonical extension |
| enter | | submit |
| f1 | | global help menu |
| enter | esc | submit |
| l | | by last modified |
| m | | by canonical mime essence |
| n | | by node type |
| r | | by relative path |
| s | | by size |
### number
### go_to
| key | remaps | action |
| ----- | ------ | ---------------- |
| down | j | to down |
| enter | | to index |
| f1 | | global help menu |
| k | up | to up |
| [0-9] | | input |
| key | remaps | action |
| ------ | ------ | -------------- |
| ctrl-c | | terminate |
| esc | | cancel |
| f | | follow symlink |
| g | | top |
| p | | path |
| x | | open in gui |
### copy_to
### create_file
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
| key | remaps | action |
| ------ | ------ | ------------ |
| ctrl-c | | terminate |
| enter | | submit |
| esc | | cancel |
| tab | | try complete |
### edit_permissions
### recover
| key | remaps | action |
| ------ | ------ | ---------------- |
| G | | -group |
| M | | min |
| O | | -other |
| U | | -user |
| ctrl-r | | reset |
| enter | | submit |
| f1 | | global help menu |
| g | | +group |
| m | | max |
| o | | +other |
| u | | +user |
| key | remaps | action |
| ------ | ------ | --------- |
| ctrl-c | | terminate |
| esc | | escape |
### delete
### action
| key | remaps | action |
| --- | ------ | ---------------- |
| D | | force delete |
| d | | delete |
| f1 | | global help menu |
| key | remaps | action |
| ------ | ------ | -------------------- |
| ! | | shell |
| c | | create |
| ctrl-c | | terminate |
| e | | open in editor |
| esc | | cancel |
| l | | logs |
| m | | toggle mouse |
| q | | quit options |
| s | | selection operations |
| [0-9] | | go to index |
### move_to
### switch_layout
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
| key | remaps | action |
| ------ | ------ | -------------------- |
| 1 | | default |
| 2 | | no help menu |
| 3 | | no selection panel |
| 4 | | no help or selection |
| ctrl-c | | terminate |
| esc | | cancel |
### filter
### relative_path_does_match_regex
| key | remaps | action |
| --------- | ------ | ---------------------------------- |
| R | | relative path does not match regex |
| backspace | | remove last filter |
| ctrl-r | | reset filters |
| ctrl-u | | clear filters |
| f1 | | global help menu |
| r | | relative path does match regex |
| key | remaps | action |
| ------ | ------ | --------- |
| ctrl-c | | terminate |
| enter | | submit |
| esc | | cancel |
### duplicate_as
### debug_error
| key | remaps | action |
| ------ | ------ | ------------------- |
| ctrl-c | | terminate |
| enter | | open logs in editor |
| esc | | escape |
| q | | quit |
### go_to_path
| key | remaps | action |
| ----- | ------ | ---------------- |
| enter | | submit |
| f1 | | global help menu |
| tab | | try complete |
| key | remaps | action |
| ------ | ------ | ------------ |
| ctrl-c | | terminate |
| enter | | submit |
| esc | | cancel |
| tab | | try complete |
### selection_ops
| key | remaps | action |
| ------ | ------ | ----------- |
| c | | copy here |
| ctrl-c | | terminate |
| esc | | cancel |
| m | | move here |
| x | | open in gui |
### rename
| key | remaps | action |
| ------ | ------ | ------------ |
| ctrl-c | | terminate |
| enter | | rename |
| esc | | cancel |
| tab | | try complete |
[1]: https://www.vim.org/
[2]: https://github.com/jarun/nnn/
[3]: #default
[4]: modes.md

@ -1,66 +1,27 @@
# Environment Variables and Pipes
Alternative to `CallLua`, `CallLuaSilently` messages that call Lua functions,
there are `Call0`, `CallSilently0`, `BashExec0`, `BashExecSilently0` messages
there are `Call`, `CallSilently`, `BashExec`, `BashExecSilently` messages
that call shell commands.
### Example: Simple file opener using xdg-open and $XPLR_FOCUS_PATH
```lua
xplr.config.modes.builtin.default.key_bindings.on_key["X"] = {
help = "open",
messages = {
{
BashExecSilently0 = [===[
xdg-open "${XPLR_FOCUS_PATH:?}"
]===],
},
},
}
```
However, unlike the Lua functions, these shell commands have to read the useful
information and send messages via environment variables and temporary files
called "pipe"s. These environment variables and files are only available when
a command is being executed.
### Example: Using Environment Variables and Pipes
```lua
xplr.config.modes.builtin.default.key_bindings.on_key["space"] = {
help = "ask name and greet",
messages = {
{
BashExec0 = [===[
echo "What's your name?"
read name
greeting="Hello $name!"
message="$greeting You are inside $PWD"
"$XPLR" -m 'LogSuccess: %q' "$message"
]===]
}
}
}
-- Now, when you press "space" in default mode, you will be prompted for your
-- name. Enter your name to receive a nice greeting and to know your location.
```
Visit the [**fzf integration tutorial**][19] for another example.
Visit the [**fzf integration tutorial**][19]
for example.
To see the environment variables and pipes, invoke the shell by typing `:!` in default
mode and run the following command:
```
env | grep ^XPLR
env | grep ^XPLR_
```
You will see something like:
```
XPLR=xplr
XPLR_FOCUS_INDEX=0
XPLR_MODE=action to
XPLR_PIPE_SELECTION_OUT=/run/user/1000/xplr/session/122278/pipe/selection_out
@ -82,44 +43,31 @@ called ["pipe"s][18].
The other variables are single-line variables containing simple information:
- [XPLR][38]
- [XPLR_APP_VERSION][30]
- [XPLR_FOCUS_INDEX][31]
- [XPLR_FOCUS_PATH][32]
- [XPLR_INPUT_BUFFER][33]
- [XPLR_INITIAL_PWD][40]
- [XPLR_MODE][34]
- [XPLR_PID][35]
- [XPLR_SESSION_PATH][36]
- [XPLR_VROOT][39]
### Environment variables
#### XPLR
The binary path of xplr command.
#### XPLR_APP_VERSION
Self-explanatory.
#### XPLR_FOCUS_INDEX
Contains the index of the currently focused item, as seen in
[column-renderer/index][10].
Contains the index of the currently focused item, as seen in [column-renderer/index][10].
#### XPLR_FOCUS_PATH
Contains the full path of the currently focused node.
#### XPLR_INITIAL_PWD
The $PWD then xplr started.
#### XPLR_INPUT_BUFFER
The line currently in displaying in the xplr input buffer. For e.g. the search
input while searching. See [Reading Input][37].
The line currently in displaying in the xplr input buffer. For e.g. the search input while searching. See [Reading Input][37].
#### XPLR_MODE
@ -131,12 +79,7 @@ Contains the process ID of the current xplr process.
#### XPLR_SESSION_PATH
Contains the current session path, like /tmp/runtime-"$USER"/xplr/session/"$XPLR_PID"/,
you can find temporary files here, such as pipes.
#### XPLR_VROOT
Contains the path of current virtual root, is set.
Contains the current session path, like /tmp/runtime-"$USER"/xplr/session/"$XPLR_PID"/, you can find temporary files here, such as pipes.
### Pipes
@ -150,10 +93,7 @@ Currently there is only one input pipe.
`XPLR_PIPE_*_OUT` are the output pipes that contain data which cannot be
exposed directly via environment variables, like multi-line strings.
These pipes can be accessed as plain text files located in $XPLR_SESSION_PATH.
Depending on the message (e.g. `Call` or `Call0`), each line will be separated
by newline or null character (`\n` or `\0`).
These pipes can be accessed as plaintext files located in $XPLR_SESSION_PATH.
- [XPLR_PIPE_SELECTION_OUT][21]
- [XPLR_PIPE_GLOBAL_HELP_MENU_OUT][22]
@ -164,23 +104,13 @@ by newline or null character (`\n` or `\0`).
#### XPLR_PIPE_MSG_IN
Append new messages to this pipe in [YAML][27] (or [JSON][7]) syntax. These
messages will be read and handled by xplr after the command execution.
Depending on the message (e.g. `Call` or `Call0`) you need to separate each
message using newline or null character (`\n` or `\0`).
> **_NOTE:_** Since version `v0.20.0`, it's recommended to avoid writing
> 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.
>
> It uses [jf][41] syntax to safely convert an YAML template into a valid message.
>
> Example: `"$XPLR" -m 'ChangeDirectory: %q' "${HOME:?}"`
Append new-line delimited messages to this pipe in [YAML][27]
(or [JSON][7]) syntax. These messages will be read and
handled by xplr after the command execution.
#### XPLR_PIPE_SELECTION_OUT
List of selected paths.
New-line delimited list of selected paths.
#### XPLR_PIPE_GLOBAL_HELP_MENU_OUT
@ -188,19 +118,59 @@ The full help menu.
#### XPLR_PIPE_LOGS_OUT
List of logs.
New-line delimited list of logs.
#### XPLR_PIPE_RESULT_OUT
Result (selected paths if any, else the focused path)
New-line delimited result (selected paths if any, else the focused path)
#### XPLR_PIPE_HISTORY_OUT
List of last visited paths (similar to jump list in vim).
New-line delimited list of last visited paths (similar to jump list in vim).
#### XPLR_PIPE_DIRECTORY_NODES_OUT
List of paths, filtered and sorted as displayed in the [files table][28].
New-line delimited list of paths, filtered and sorted as displayed in the
[files table][28].
### Example: Using Environment Variables and Pipes
```lua
xplr.config.modes.builtin.default.key_bindings.on_key.space = {
help = "ask name and greet",
messages = {
{
BashExec = [===[
echo "What's your name?"
read name
greeting="Hello $name!"
message="$greeting You are inside $PWD"
echo LogSuccess: '"'$message'"' >> "${XPLR_PIPE_MSG_IN:?}"
]===]
}
}
}
-- Now, when you press "space" in default mode, you will be prompted for your
-- name. Enter your name to receive a nice greeting and to know your location.
```
### Another example: Simple file opener using xdg-open and $XPLR_FOCUS_PATH
```lua
xplr.config.modes.builtin.default.key_bindings.on_key.X = {
help = "open",
messages = {
{
BashExecSilently = [===[
xdg-open "${XPLR_FOCUS_PATH:?}"
]===],
},
},
}
```
[7]: https://www.json.org
[10]: column-renderer.md#index
@ -224,7 +194,3 @@ List of paths, filtered and sorted as displayed in the [files table][28].
[35]: #xplr_pid
[36]: #xplr_session_path
[37]: messages.md#reading-input
[38]: #xplr
[39]: #xplr_vroot
[40]: #xplr_initial_pwd
[41]: https://github.com/sayanarijit/jf

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

@ -42,19 +42,6 @@ Set it to `true` if you want to hide all remaps in the help menu.
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
Set it to `true` if you want the cursor to stay in the same position when
@ -85,7 +72,7 @@ Type: nullable string
#### xplr.config.general.logs.info.style
The style for the information logs.
The style for the informations logs.
Type: [Style](https://xplr.dev/en/style)
@ -194,30 +181,6 @@ Constraint for the column widths.
Type: nullable list of [Constraint](https://xplr.dev/en/layouts#constraint)
#### xplr.config.general.selection.item.format
Renderer for each item in the selection list.
Type: nullable string
#### xplr.config.general.selection.item.style
Style for each item in the selection list.
Type: [Style](https://xplr.dev/en/style)
#### xplr.config.general.search.algorithm
The default search algorithm
Type: [Search Algorithm](https://xplr.dev/en/searching#algorithm)
#### xplr.config.general.search.unordered
The default search ordering
Type: boolean
#### xplr.config.general.default_ui.prefix
The content that is placed before the item name for each row by default.
@ -359,24 +322,6 @@ Type: nullable mapping of the following key-value pairs:
- format: nullable string
- style: [Style](https://xplr.dev/en/style)
#### xplr.config.general.sort_and_filter_ui.search_identifiers
The identifiers used to denote applied search input.
Type: { format = nullable string, style = [Style](https://xplr.dev/en/style) }
#### xplr.config.general.sort_and_filter_ui.search_direction_identifiers.ordered.format
The shape of ordered indicator for search ordering identifiers in Sort & filter panel.
Type: nullable string
#### xplr.config.general.sort_and_filter_ui.search_direction_identifiers.unordered.format
The shape of unordered indicator for search ordering identifiers in Sort & filter panel.
Type: nullable string
#### xplr.config.general.panel_ui.default.title.format
The content for panel title by default.
@ -600,16 +545,12 @@ Type: nullable list of [Node Sorter](https://xplr.dev/en/sorting#node-sorter-app
#### xplr.config.general.initial_mode
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
#### xplr.config.general.initial_layout
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

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

@ -24,7 +24,8 @@ version of xplr, but they have one common drawback - the user will need to keep
an eye on the releases, and manually upgrade xplr when a new version is
available.
One way to keep an eye on the releases is to [watch the repository][4].
One way to keep an eye on the releases is to
[watch the repository][4].
## Community Maintained Repositories
@ -41,48 +42,6 @@ repositories:
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
(same for Manjaro Linux)
@ -122,8 +81,6 @@ apk add xplr bash less
### macOS
Make sure you have the latest version of [GNU core utilities][29] installed.
#### [MacPorts][11]
```
@ -238,16 +195,57 @@ cargo build --locked --release --bin xplr
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
[2]: #from-cratesio
[2]: #from-a-hrefhttpscratesiocratesxplrcratesioa
[3]: #build-from-source
[4]: https://github.com/sayanarijit/xplr/watchers
[5]: https://repology.org/badge/vertical-allrepos/xplr.svg
[6]: https://repology.org/project/xplr/versions
[7]: https://archlinux.org/packages/extra/x86_64/xplr
[7]: https://archlinux.org/packages/community/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
[9]: https://github.com/shubham-cpp/void-pkg-templates
[10]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/xp/xplr/package.nix
[10]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/xplr
[11]: https://ports.macports.org/port/xplr
[12]: https://formulae.brew.sh/formula/xplr
[13]: https://cgit.freebsd.org/ports/plain/misc/xplr/
@ -260,10 +258,9 @@ sudo cp target/release/xplr /usr/local/bin/
[20]: https://gcc.gnu.org/
[21]: https://www.gnu.org/software/make/
[22]: https://git-scm.com/
[23]: https://github.com/sayanarijit/xplr/assets/11632726/3b61e8c8-76f0-48e8-8734-50e9e7e495b7
[23]: https://termux.com/
[24]: https://s3.gifyu.com/images/xplr-termuxfd3c398d3cf4bcbc.md.jpg
[25]: https://gifyu.com/image/tF2D
[26]: https://github.com/sayanarijit/xplr/releases/latest/download/xplr-linux-musl.tar.gz
[27]: https://pkgs.alpinelinux.org/packages?name=xplr
[28]: https://gpo.zugaina.org/Overlays/guru/app-misc/xplr
[29]: https://formulae.brew.sh/formula/coreutils
[30]: https://github.com/nix-community/home-manager/blob/master/modules/programs/xplr.nix

@ -53,17 +53,14 @@ to lose some kb if it makes sense.
Some of the coolest features xplr provide beside the basic stuff:
- [Embedded LuaJIT][5] for portability and extensibility.
- [A simple modal system based on message passing][10] to control xplr session
using:
- [A simple system based on message passing][10] to control xplr session using:
- [Keyboard inputs][11]
- [Shell Commands][12]
- [Lua Functions][13]
- [Hooks][22]
- Easy, typesafe message passing with `-m MSG` or `-M MSG` subcommands.
- [Readline-like input buffer][9] with customizable behavior to read user
- [Readline-like input buffer][9] with customizable behaviour to read user
inputs.
- [Switchable recover mode][7] that saves you from doing unwanted things when
in a hurry.
- [Switchable recover mode][7] that saves you from doing unwanted things when in a
hurry.
- [Customizable layouts][1] with built-in panels. For e.g.
- **Selection list** to show you the selected paths in real-time.
- **Help menu** to show you the available keys bindings in each mode.
@ -72,7 +69,6 @@ Some of the coolest features xplr provide beside the basic stuff:
- [Custom file properties][17] with custom colors can be displayed in the table.
- [FIFO manager][19] to manage a FIFO file that can be used to
[integrate with previewers][6].
- [Virtual root][21] with `--vroot` and `:v` key bindings.
- **Different quit options:**
- Quit with success without any output (`q`).
- Quit with success and the result printed on stdout (`enter`).
@ -84,16 +80,18 @@ Some of the coolest features xplr provide beside the basic stuff:
(`:` `q` `s`).
- Quit with failure (`ctrl-c`).
**Q.** What features should be added here? [let us know][20].
[1]: layouts.md
[2]: configure-key-bindings.md
[3]: awesome-plugins.md
[4]: https://github.com/sayanarijit/xplr/tree/main/benches
[5]: https://github.com/sayanarijit/xplr/discussions/183
[6]: https://github.com/sayanarijit/xplr/pull/229
[7]: modes.md#xplrconfigmodesbuiltinrecover
[7]: general-config.md#enable_recover_mode
[8]: default-key-bindings.md
[9]: https://github.com/sayanarijit/xplr/pull/397
[10]: messages.md
[10]: message.md
[11]: configure-key-bindings.md
[12]: mode.md#input-pipe
[13]: lua-function-calls.md
@ -102,6 +100,5 @@ Some of the coolest features xplr provide beside the basic stuff:
[16]: awesome-hacks.md
[17]: node_types.md
[18]: https://github.com/sayanarijit/xplr/blob/main/src/init.lua
[19]: messages.md#startfifo
[21]: messages.md#virtual-root
[22]: configuration.md#hooks
[19]: message.md#startfifo
[20]: community.md

@ -2,13 +2,15 @@
#### Example: Defining Custom Layout
[![layout.png][23]][24]
```lua
xplr.config.layouts.builtin.default = {
Horizontal = {
config = {
margin = 1,
horizontal_margin = 1,
vertical_margin = 1,
horizontal_margin = 2,
vertical_margin = 3,
constraints = {
{ Percentage = 50 },
{ Percentage = 50 },
@ -22,22 +24,7 @@ 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 │
╰────────────────────╯╰────────────────────╯
```
A layout is a [sum type][56] can be one of the following:
A layout can be one of the following:
- [Nothing][8]
- [Table][9]
@ -45,61 +32,56 @@ A layout is a [sum type][56] can be one of the following:
- [Selection][11]
- [HelpMenu][12]
- [SortAndFilter][13]
- [Static][25]
- [Dynamic][26]
- [CustomContent][25]
- [Horizontal][14]
- [Vertical][16]
- CustomContent (deprecated, use `Static` or `Dynamic`)
### Nothing
This layout contains a blank panel.
Type: "Nothing"
Example: "Nothing"
### Table
This layout contains the table displaying the files and directories in the current
directory.
This layout contains the table displaying the files and directories in the
current directory.
### InputAndLogs
This layout contains the panel displaying the input prompt and logs.
Type: "InputAndLogs"
Example: "InputAndLogs"
### Selection
This layout contains the panel displaying the selected paths.
Type: "Selection"
Example: "Selection"
### HelpMenu
This layout contains the panel displaying the help menu for the current mode in
real-time.
Type: "HelpMenu"
Example: "HelpMenu"
### SortAndFilter
This layout contains the panel displaying the pipeline of sorters and filters applied on
the list of paths being displayed.
Type: "SortAndFilter"
This layout contains the panel displaying the pipeline of sorters and filters
applied of the list of paths being displayed.
### Static
Example: "SortAndFilter"
This is a custom layout to render static content.
### Custom Content
Type: { Static = [Custom Panel][27] }
### Dynamic
Custom content is a special layout to render something custom.
It contains the following information:
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].
- [title][33]
- [body][34]
Type: { Dynamic = "[Content Renderer][35]" }
Example: { CustomContent = { title = [title][33], body = [body][34] }
### Horizontal
@ -110,7 +92,7 @@ It contains the following information:
- [config][15]
- [splits][17]
Type: { Vertical = { config = [Layout Config][15], splits = { [Layout][17], ... } }
Example: { Horizontal = { config = [config][15], splits = [splits][17] }
### Vertical
@ -121,7 +103,7 @@ It contains the following information:
- [config][15]
- [splits][17]
Type: { Vertical = { config = [Layout Config][15], splits = { [Layout][17], ... } }
Example: { Vertical = { config = [config][15], splits = [splits][17] }
## Layout Config
@ -158,7 +140,7 @@ The constraints applied on the layout.
## Constraint
A constraint is a [sum type][56] can be one of the following:
A constraint can be one of the following:
- { Percentage = int }
- { Ratio = { int, int } }
@ -178,311 +160,195 @@ A constraint is a [sum type][56] can be one of the following:
- { MinLessThanLayoutHeight = int }
- { MinLessThanLayoutWidth = int }
TODO: document each constraint.
## splits
Type: list of [Layout][3]
The list of child layouts to fit into the parent layout.
## Custom Panel
## title
Type: nullable string
The title of the panel.
## body
Custom panel is a [sum type][56] can be one of the following:
Type: [Content Body][26]
- [CustomParagraph][29]
- [CustomList][30]
- [CustomTable][31]
- [CustomLayout][55]
The body of the panel.
### CustomParagraph
## Content Body
Content body can be one of the following:
- [StaticParagraph][27]
- [DynamicParagraph][28]
- [StaticList][29]
- [DynamicList][30]
- [StaticTable][31]
- [DynamicTable][32]
## Static Paragraph
A paragraph to render. It contains the following fields:
- **ui** (nullable [Panel UI Config][32]): Optional UI config for the panel.
- **body** (string): The string to render.
- **render** (string): The string to render.
#### Example: Render a custom static paragraph
```lua
xplr.config.layouts.builtin.default = {
Static = {
CustomParagraph = {
ui = { title = { format = " custom title " } },
body = "custom body",
CustomContent = {
title = "custom title",
body = {
StaticParagraph = { render = "custom body" },
},
},
}
```
Result:
## Dynamic Paragraph
```
╭ custom title ────────╮
│custom body │
│ │
│ │
╰──────────────────────╯
```
A [Lua function][35] to render a custom paragraph.
It contains the following fields:
- **render** (string): The [lua function][35] that returns the paragraph to
render.
#### Example: Render a custom dynamic paragraph
```lua
xplr.config.layouts.builtin.default = { Dynamic = "custom.render_layout" }
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = { DynamicParagraph = { render = "custom.render_layout" } },
},
}
xplr.fn.custom.render_layout = function(ctx)
return {
CustomParagraph = {
ui = { title = { format = ctx.app.pwd } },
body = xplr.util.to_yaml(ctx.app.focused_node),
},
}
return ctx.app.pwd
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
## Static List
A list to render. It contains the following fields:
- **ui** (nullable [Panel UI Config][32]): Optional UI config for the panel.
- **body** (list of string): The list of strings to display.
- **render** (list of string): The list to render.
#### Example: Render a custom static list
```lua
xplr.config.layouts.builtin.default = {
Static = {
CustomList = {
ui = { title = { format = " custom title " } },
body = { "1", "2", "3" },
CustomContent = {
title = "custom title",
body = {
StaticList = { render = { "1", "2", "3" } },
},
},
}
```
Result:
## Dynamic List
```
╭ custom title ─────────────╮
│1 │
│2 │
│3 │
│ │
╰───────────────────────────╯
```
A [Lua function][35] to render a custom list.
It contains the following fields:
- **render** (string): The [lua function][35] that returns the list to render.
#### Example: Render a custom dynamic list
```lua
xplr.config.layouts.builtin.default = { Dynamic = "custom.render_layout" }
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = { DynamicList = { render = "custom.render_layout" } },
},
}
xplr.fn.custom.render_layout = function(ctx)
return {
CustomList = {
ui = { title = { format = ctx.app.pwd } },
body = {
(ctx.app.focused_node or {}).relative_path or "",
ctx.app.version,
tostring(ctx.app.pid),
},
},
ctx.app.pwd,
ctx.app.version,
tostring(ctx.app.pid),
}
end
```
Result:
## Static Table
```
╭/home/sayanarijit──────────╮
│Desktop │
│0.21.2 │
│17336 │
│ │
│ │
╰───────────────────────────╯
```
## CustomTable
A table to render. It contains the following fields:
A custom table to render. It contains the following fields:
- **ui** (nullable [Panel UI Config][32]): Optional UI config for the panel.
- **widths** (list of [Constraint][22]): Width of the columns.
- **col_spacing** (nullable int): Spacing between columns. Defaults to 1.
- **body** (list of list of string): The rows and columns to render.
- **render** (list of list of string): The rows and columns to render.
#### Example: Render a custom static table
```lua
xplr.config.layouts.builtin.default = {
Static = {
CustomTable = {
ui = { title = { format = " custom title " } },
widths = {
{ Percentage = 50 },
{ Percentage = 50 },
},
body = {
{ "a", "b" },
{ "c", "d" },
CustomContent = {
title = "custom title",
body = {
StaticTable = {
widths = {
{ Percentage = 50 },
{ Percentage = 50 },
},
col_spacing = 1,
render = {
{ "a", "b" },
{ "c", "d" },
},
},
},
},
}
```
Result:
## Dynamic Table
```
╭ custom title ────────────────────╮
│a b │
│c d │
│ │
│ │
│ │
╰──────────────────────────────────╯
```
A [Lua function][35] to render a custom table.
It contains the following fields:
- **widths** (list of [Constraint][22]): Width of the columns.
- **col_spacing** (nullable int): Spacing between columns. Defaults to 1.
- **render** (string): The [lua function][35] that returns the table to render.
#### Example: Render a custom dynamic table
```lua
xplr.config.layouts.builtin.default = {Dynamic = "custom.render_layout" }
xplr.fn.custom.render_layout = function(ctx)
return {
CustomTable = {
ui = { title = { format = ctx.app.pwd } },
widths = {
{ Percentage = 50 },
{ Percentage = 50 },
},
body = {
{ "", "" },
{ "Layout height", tostring(ctx.layout_size.height) },
{ "Layout width", tostring(ctx.layout_size.width) },
{ "", "" },
{ "Screen height", tostring(ctx.screen_size.height) },
{ "Screen width", tostring(ctx.screen_size.width) },
xplr.config.layouts.builtin.default = {
CustomContent = {
title = "custom title",
body = {
DynamicTable = {
widths = {
{ Percentage = 50 },
{ Percentage = 50 },
},
col_spacing = 1,
render = "custom.render_layout",
},
},
}
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" } } },
},
return {
{ "", "" },
{ "Layout height", tostring(ctx.layout_size.height) },
{ "Layout width", tostring(ctx.layout_size.width) },
{ "", "" },
{ "Screen height", tostring(ctx.screen_size.height) },
{ "Screen width", tostring(ctx.screen_size.width) },
}
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
It contains the following optional fields:
- **title** ({ format = "string", style = [Style][33] }): the title of the panel.
- **style** ([Style][33]): The style of the panel body.
- **borders** (nullable list of [Border][34]): The shape of the borders.
- **border_type** ([Border Type][54]): The type of the borders.
- **border_style** ([Style][33]): The style of the borders.
## Content Renderer
It is a Lua function that receives [a special argument][36] as input and
@ -495,7 +361,6 @@ It contains the following information:
- [layout_size][37]
- [screen_size][37]
- [scrolltop][57]
- [app][38]
### Size
@ -509,24 +374,16 @@ It contains the following information:
Every field is of integer type.
### scrolltop
Type: integer
The start index of the visible nodes in the table.
### app
This is a lightweight version of the [Lua Context][39]. In this context, the
heavyweight fields like [directory_buffer][50] are omitted for performance
reasons.
Hence, only the following fields are available.
Hence, only the following fields are avilable.
- [version][40]
- [pwd][41]
- [initial_pwd][53]
- [vroot][52]
- [focused_node][42]
- [selection][43]
- [mode][44]
@ -562,16 +419,18 @@ Hence, only the following fields are available.
[20]: #vertical_margin
[21]: #constraints
[22]: #constraint
[25]: #static
[26]: #dynamic
[27]: #custom-panel
[28]: configuration.md#function
[29]: #customparagraph
[30]: #customlist
[31]: #customtable
[32]: #panel-ui-config
[33]: style.md#style
[34]: borders.md#border
[23]: https://s6.gifyu.com/images/layout.png
[24]: https://gifyu.com/image/1X38
[25]: #custom-content
[26]: #content-body
[27]: #static-paragraph
[28]: #dynamic-paragraph
[29]: #static-list
[30]: #dynamic-list
[31]: #static-table
[32]: #dynamic-table
[33]: #title
[34]: #body
[35]: #content-renderer
[36]: #content-renderer-argument
[37]: #size
@ -589,9 +448,3 @@ Hence, only the following fields are available.
[49]: lua-function-calls.md#explorer_config
[50]: lua-function-calls.md#directory_buffer
[51]: layouts.md
[52]: lua-function-calls.md#vroot
[53]: lua-function-calls.md#initial_pwd
[54]: borders.md#border-type
[55]: #customlayout
[56]: sum-type.md
[57]: #scrolltop

@ -12,13 +12,15 @@ You can add new panels in `xplr.config.layouts.custom`.
##### Example: Defining Custom Layout
![demo](https://s6.gifyu.com/images/layout.png)
```lua
xplr.config.layouts.builtin.default = {
Horizontal = {
config = {
margin = 1,
horizontal_margin = 1,
vertical_margin = 1,
horizontal_margin = 2,
vertical_margin = 3,
constraints = {
{ Percentage = 50 },
{ Percentage = 50 },
@ -32,21 +34,6 @@ 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
The default layout

@ -8,37 +8,6 @@ When called the function receives a [special argument][14] that
contains some useful information. The function can optionally return a list of
messages which will be handled by xplr.
## Example: Using Lua Function Calls
```lua
-- Define the function
xplr.fn.custom.ask_name_and_greet = function(app)
print("What's your name?")
local name = io.read()
local greeting = "Hello " .. name .. "!"
local message = greeting .. " You are inside " .. app.pwd
return {
{ LogSuccess = message },
}
end
-- Map the function to a key (space)
xplr.config.modes.builtin.default.key_bindings.on_key.space = {
help = "ask name and greet",
messages = {
{ CallLua = "custom.ask_name_and_greet" }
}
}
-- Now, when you press "space" in default mode, you will be prompted for your
-- name. Enter your name to receive a nice greeting and to know your location.
```
Visit the [xplr.util][85] API docs for some useful utility / helper functions
that you can use in your Lua function calls.
## Lua Context
This is a special argument passed to the lua functions when called using the
@ -48,8 +17,6 @@ It contains the following information:
- [version][29]
- [pwd][31]
- [initial_pwd][76]
- [vroot][75]
- [focused_node][32]
- [directory_buffer][33]
- [selection][34]
@ -72,19 +39,7 @@ xplr version. Can be used to test compatibility.
Type: string
The present working directory.
### initial_pwd
Type: string
The initial working directory when xplr started.
### vroot
Type: nullable string
The current virtual root.
The present working directory/
### focused_node
@ -136,9 +91,7 @@ The session path.
### explorer_config
Type: [Explorer Config][66]
The configuration for exploring paths.
[TODO][66]
### history
@ -346,35 +299,33 @@ Type: list of string
Visited paths.
## Explorer Config
Explorer config contains the following fields:
- [filters][77]
- [sorters][78]
- [searcher][79]
### filters
List of filters to apply.
Type: list of [Node Filter Applicable][80]
### sorters
Add list or sorters to the pipeline.
Type: list of [Node Sorter Applicable][81]
## Example: Using Lua Function Calls
### searcher
```lua
-- Define the function
xplr.fn.custom.ask_name_and_greet = function(app)
print("What's your name?")
The searcher to use (if any).
local name = io.read()
local greeting = "Hello " .. name .. "!"
local message = greeting .. " You are inside " .. app.pwd
Type: nullable [Node Searcher Applicable][82]
return {
{ LogSuccess = message },
}
end
## Also See:
-- Map the function to a key (space)
xplr.config.modes.builtin.default.key_bindings.on_key.space = {
help = "ask name and greet",
messages = {
{ CallLua = "custom.ask_name_and_greet" }
}
}
- [xplr.util][85]
-- Now, when you press "space" in default mode, you will be prompted for your
-- name. Enter your name to receive a nice greeting and to know your location.
```
[7]: https://www.json.org
[8]: modes.md#mode
@ -424,7 +375,7 @@ Type: nullable [Node Searcher Applicable][82]
[63]: #nodes
[64]: #total
[65]: #focus
[66]: #explorer-config
[66]: https://docs.rs/xplr/latest/xplr/app/struct.ExplorerConfig.html
[67]: #history
[68]: #loc
[69]: #paths
@ -433,12 +384,3 @@ Type: nullable [Node Searcher Applicable][82]
[72]: #last_modified
[73]: #uid
[74]: #gid
[75]: #vroot
[76]: #initial_pwd
[77]: #filters
[78]: #sorters
[79]: #searcher
[80]: filtering.md#node-filter-applicable
[81]: sorting.md#node-sorter-applicable
[82]: searching.md#node-searcher-applicable
[85]: xplr.util.md

@ -1,9 +1,7 @@
# Message
You can think of xplr as a server. Just like web servers listen to HTTP
requests, xplr listens to messages.
A message is a [sum type][9] that can have [these possible values][1].
requests, xplr listens to [messages][1].
You can send these messages to an xplr session in the following ways:
@ -15,35 +13,22 @@ You can send these messages to an xplr session in the following ways:
### Format
To send messages using the [key bindings][2] or [Lua function calls][3],
messages are represented in [Lua][5] syntax.
For example:
To send messages using the [key bindings][2] or
[Lua function calls][3], messages are represented in
[Lua][5] syntax. For example:
- `"Quit"`
- `{ FocusPath = "/path/to/file" }`
- `{ Call = { command = "bash", args = { "-c", "read -p test" } } }`
However, to send messages using the [input pipe][4], they need to be
represented using [YAML][6] (or [JSON][7]) syntax.
For example:
represented using
[YAML][6] (or [JSON][7]) syntax. For example:
- `Quit`
- `FocusPath: "/path/to/file"`
- `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:
- [Full List of Messages][1]
@ -55,5 +40,3 @@ For example:
[5]: https://www.lua.org/
[6]: http://yaml.org/
[7]: https://www.json.org
[8]: https://github.com/sayanarijit/jf
[9]: sum-type.md

@ -1,13 +1,12 @@
# Full List of Messages
xplr [messages][1] categorized based on their purpose.
xplr messages categorized based on their purpose.
## Categories
- [Exploring](#exploring)
- [Screen](#screen)
- [Navigation](#navigation)
- [Virtual Root](#virtual-root)
- [Reading Input](#reading-input)
- [Switching Mode](#switching-mode)
- [Switching Layout](#switching-layout)
@ -16,7 +15,6 @@ xplr [messages][1] categorized based on their purpose.
- [Select Operations](#select-operations)
- [Filter Operations](#filter-operations)
- [Sort Operations](#sort-operations)
- [Search Operations](#search-operations)
- [Mouse Operations](#mouse-operations)
- [Fifo Operations](#fifo-operations)
- [Logging](#logging)
@ -97,15 +95,6 @@ Example:
- Lua: `"FocusNext"`
- YAML: `FocusNext`
#### FocusNextSelection
Focus on the next selected node.
Example:
- Lua: `"FocusNextSelection"`
- YAML: `FocusNextSelection`
#### FocusNextByRelativeIndex
Focus on the `n`th node relative to the current focus where `n` is a
@ -137,15 +126,6 @@ Example:
- Lua: `"FocusPrevious"`
- YAML: `FocusPrevious`
#### FocusPreviousSelection
Focus on the previous selection item.
Example:
- Lua: `"FocusPreviousSelection"`
- YAML: `FocusPreviousSelection`
#### FocusPreviousByRelativeIndex
Focus on the `-n`th node relative to the current focus where `n` is a
@ -228,6 +208,8 @@ Example:
#### FocusByFileName
**YAML:** `FocusByFileName: string`
Focus on the file by name from the present working directory.
Type: { FocusByFileName = "string" }
@ -237,42 +219,6 @@ Example:
- Lua: `{ FocusByFileName = "filename.ext" }`
- YAML: `FocusByFileName: filename.ext`
#### ScrollUp
Scroll up by terminal height.
Example:
- Lua: `"ScrollUp"`
- YAML: `ScrollUp`
#### ScrollDown
Scroll down by terminal height.
Example:
- Lua: `"ScrollDown"`
- YAML: `ScrollDown`
#### ScrollUpHalf
Scroll up by half of terminal height.
Example:
- Lua: `"ScrollUpHalf"`
- YAML: `ScrollUpHalf`
#### ScrollDownHalf
Scroll down by half of terminal height.
Example:
- Lua: `"ScrollDownHalf"`
- YAML: `ScrollDownHalf`
#### ChangeDirectory
Change the present working directory ($PWD)
@ -320,75 +266,14 @@ Example:
- Lua: `"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
Follow the symlink under focus to its actual location.
Example:
- Lua: `"FollowSymlink"`
- YAML: `FollowSymlink`
### Virtual Root
#### SetVroot
Sets the virtual root for isolating xplr navigation, similar to
`--vroot`, but temporary (can be reset back to initial value).
If the $PWD is outside the vroot, xplr will automatically enter vroot.
Type: { SetVroot = "string" }
Example:
- Lua: `{ SetVroot = "/tmp" }`
- YAML: `SetVroot: /tmp`
#### UnsetVroot
Unset the virtual root temporarily (can be reset back to the initial
value).
Example:
- Lua: `"UnsetVroot"`
- YAML: `UnsetVroot`
#### ToggleVroot
Toggle virtual root between unset, initial value and $PWD.
Example:
- Lua: `"ToggleVroot"`
- YAML: `ToggleVroot`
#### ResetVroot
Reset the virtual root back to the initial value.
Example:
- Lua: `"ResetVroot"`
- YAML: `ResetVroot`
Lua: `"FollowSymlink"`
YAML: `FollowSymlink`
### Reading Input
@ -407,7 +292,7 @@ Example:
Update the input buffer using cursor based operations.
Type: { UpdateInputBuffer = [Input Operation](https://xplr.dev/en/input-operation) }
Type: { UpdateInputBuffer = [Input Opertaion](https://xplr.dev/en/input-operation) }
Example:
@ -436,7 +321,7 @@ Example:
#### BufferInputFromKey
Append/buffer the character read from a keyboard input into the
Append/buffer the characted read from a keyboard input into the
input buffer.
Example:
@ -498,7 +383,8 @@ Example:
- YAML: SwitchMode: default
> **NOTE:** To be specific about which mode to switch to, use
> `SwitchModeBuiltin` or `SwitchModeCustom` instead.
> `SwitchModeBuiltinKeepingInputBuffer` or
> `SwitchModeCustomKeepingInputBuffer` instead.
#### SwitchModeKeepingInputBuffer
@ -626,78 +512,54 @@ Example:
#### Call
Like `Call0`, but it uses `\n` as the delimiter in input/output pipes,
hence it cannot handle files with `\n` in the name.
You may want to use `Call0` instead.
#### Call0
Call a shell command with the given arguments.
Note that the arguments will be shell-escaped.
So to read the variables, the `-c` option of the shell
can be used.
You may need to pass `ExplorePwd` depending on the expectation.
You may need to pass `ExplorePwd` depening on the expectation.
Type: { Call0 = { command = "string", args = { "list", "of", "string" } }
Type: { Call = { command = string, args = { "list", "of", "string" } }
Example:
- Lua: `{ Call0 = { command = "bash", args = { "-c", "read -p test" } } }`
- YAML: `Call0: { command: bash, args: ["-c", "read -p test"] }`
- Lua: `{ Call = { command = "bash", args = { "-c", "read -p test" } } }`
- YAML: `Call: { command: bash, args: ["-c", "read -p test"] }`
#### CallSilently
Like `CallSilently0`, but it uses `\n` as the delimiter in input/output
pipes, hence it cannot handle files with `\n` in the name.
You may want to use `CallSilently0` instead.
#### CallSilently0
Like `Call0` but without the flicker. The stdin, stdout
Like `Call` but without the flicker. The stdin, stdout
stderr will be piped to null. So it's non-interactive.
Type: { CallSilently0 = { command = "string", args = {"list", "of", "string"} } }
Type: { CallSilently = "string" }
Example:
- Lua: `{ CallSilently0 = { command = "tput", args = { "bell" } } }`
- YAML: `CallSilently0: { command: tput, args: ["bell"] }`
- Lua: `{ CallSilently = { command = "tput", args = { "bell" } } }`
- YAML: `CallSilently: { command: tput, args: ["bell"] }`
#### BashExec
Like `BashExec0`, but it uses `\n` as the delimiter in input/output
pipes, hence it cannot handle files with `\n` in the name.
You may want to use `BashExec0` instead.
#### BashExec0
An alias to `Call: {command: bash, args: ["-c", "{string}"], silent: false}`
where `{string}` is the given value.
Type: { BashExec0 = "string" }
Type: { BashExec = "string" }
Example:
- Lua: `{ BashExec0 = "read -p test" }`
- YAML: `BashExec0: "read -p test"`
- Lua: `{ BashExec = "read -p test" }`
- YAML: `BashExec: "read -p test"`
#### BashExecSilently
Like `BashExecSilently0`, but it uses `\n` as the delimiter in
input/output pipes, hence it cannot handle files with `\n` in the name.
You may want to use `BashExecSilently0` instead.
#### BashExecSilently0
Like `BashExec0` but without the flicker. The stdin, stdout
Like `BashExec` but without the flicker. The stdin, stdout
stderr will be piped to null. So it's non-interactive.
Type: { BashExecSilently0 = "string" }
Type: { BashExecSilently = "string" }
Example:
- Lua: `{ BashExecSilently0 = "tput bell" }`
- YAML: `BashExecSilently0: "tput bell"`
- Lua: `{ BashExecSilently = "tput bell" }`
- YAML: `BashExecSilently: "tput bell"`
### Calling Lua Functions
@ -861,8 +723,6 @@ Example:
Add a [filter](https://xplr.dev/en/filtering#filter) to exclude nodes
while exploring directories.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Filters get automatically cleared when changing directories.
Type: { AddNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering#filter), input = "string" }
@ -874,7 +734,6 @@ Example:
#### RemoveNodeFilter
Remove an existing [filter](https://xplr.dev/en/filtering#filter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { RemoveNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
@ -887,7 +746,6 @@ Example:
Remove a [filter](https://xplr.dev/en/filtering#filter) if it exists,
else, add a it.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { ToggleNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
@ -900,7 +758,6 @@ Example:
Add a node [filter](https://xplr.dev/en/filtering#filter) reading the
input from the buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { AddNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
@ -913,7 +770,6 @@ Example:
Remove a node [filter](https://xplr.dev/en/filtering#filter) reading
the input from the buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { RemoveNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
@ -925,7 +781,6 @@ Example:
#### RemoveLastNodeFilter
Remove the last node [filter](https://xplr.dev/en/filtering).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
@ -936,7 +791,6 @@ Example:
Reset the node [filters](https://xplr.dev/en/filtering) back to the
default configuration.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
@ -946,7 +800,6 @@ Example:
#### ClearNodeFilters
Clear all the node [filters](https://xplr.dev/en/filtering).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
@ -959,7 +812,6 @@ Example:
Add a [sorter](https://xplr.dev/en/sorting#sorter) to sort nodes while
exploring directories.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { AddNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
@ -971,7 +823,6 @@ Example:
#### RemoveNodeSorter
Remove an existing [sorter](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { RemoveNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
@ -983,7 +834,6 @@ Example:
#### ReverseNodeSorter
Reverse a node [sorter](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { ReverseNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
@ -996,7 +846,6 @@ Example:
Remove a [sorter](https://xplr.dev/en/sorting#sorter) if it exists,
else, add a it.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Type: { ToggleNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
@ -1008,7 +857,6 @@ Example:
#### ReverseNodeSorters
Reverse the node [sorters](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
@ -1018,7 +866,6 @@ Example:
#### RemoveLastNodeSorter
Remove the last node [sorter](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
@ -1029,7 +876,6 @@ Example:
Reset the node [sorters](https://xplr.dev/en/sorting#sorter) back to
the default configuration.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
@ -1039,196 +885,12 @@ Example:
#### ClearNodeSorters
Clear all the node [sorters](https://xplr.dev/en/sorting#sorter).
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
- Lua: `"ClearNodeSorters"`
- YAML: `ClearNodeSorters`
### Search Operations
#### Search
Search files using the current or default (fuzzy) search algorithm.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Type: { Search = "string" }
Example:
- Lua: `{ Search = "pattern" }`
- YAML: `Search: pattern`
#### SearchFromInput
Calls `Search` with the input taken from the input buffer.
Example:
- Lua: `"SearchFromInput"`
- YAML: `SearchFromInput`
#### SearchFuzzy
Search files using fuzzy match algorithm.
It keeps the filters, but overrides the sorters.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Type: { SearchFuzzy = "string" }
Example:
- Lua: `{ SearchFuzzy = "pattern" }`
- YAML: `SearchFuzzy: pattern`
#### SearchFuzzyFromInput
Calls `SearchFuzzy` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Example:
- Lua: `"SearchFuzzyFromInput"`
- YAML: `SearchFuzzyFromInput`
#### SearchFuzzyUnordered
Like `SearchFuzzy`, but doesn't not perform rank based sorting.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Type: { SearchFuzzyUnordered = "string" }
Example:
- Lua: `{ SearchFuzzyUnordered = "pattern" }`
- YAML: `SearchFuzzyUnordered: pattern`
#### SearchFuzzyUnorderedFromInput
Calls `SearchFuzzyUnordered` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Example:
- Lua: `"SearchFuzzyUnorderedFromInput"`
- YAML: `SearchFuzzyUnorderedFromInput`
#### SearchRegex
Search files using regex match algorithm.
It keeps the filters, but overrides the sorters.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Type: { SearchRegex = "string" }
Example:
- Lua: `{ SearchRegex = "pattern" }`
- YAML: `SearchRegex: pattern`
#### SearchRegexFromInput
Calls `SearchRegex` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Example:
- Lua: `"SearchRegexFromInput"`
- YAML: `SearchRegexFromInput`
#### SearchRegexUnordered
Like `SearchRegex`, but doesn't not perform rank based sorting.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Type: { SearchRegexUnordered = "string" }
Example:
- Lua: `{ SearchRegexUnordered = "pattern" }`
- YAML: `SearchRegexUnordered: pattern`
#### SearchRegexUnorderedFromInput
Calls `SearchRegexUnordered` with the input taken from the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
It gets reset automatically when changing directory.
Example:
- Lua: `"SearchRegexUnorderedFromInput"`
- YAML: `SearchRegexUnorderedFromInput`
#### ToggleSearchAlgorithm
Toggles between different search algorithms, without changing the input
buffer
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
- Lua: `"ToggleSearchAlgorithm"`
- YAML: `ToggleSearchAlgorithm`
#### EnableSearchOrder
Enables ranked search without changing the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
- Lua: `"EnableOrderedSearch"`
- YAML: `EnableSearchOrder`
#### DisableSearchOrder
Disabled ranked search without changing the input buffer.
You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
Example:
- Lua: `"DisableSearchOrder"`
- YAML: `DisableSearchOrder`
#### ToggleSearchOrder
Toggles ranked search without changing the input buffer.
Example:
- Lua: `"ToggleSearchOrder"`
- YAML: `ToggleSearchOrder`
#### AcceptSearch
Accepts the search by keeping the latest focus while in search mode.
Automatically calls `ExplorePwd`.
Example:
- Lua: `"AcceptSearch"`
- YAML: `AcceptSearch`
#### CancelSearch
Cancels the search by discarding the latest focus and recovering
the focus before search.
Automatically calls `ExplorePwd`.
Example:
- Lua: `"CancelSearch"`
- YAML: `CancelSearch`
### Mouse Operations
#### EnableMouse
@ -1282,7 +944,7 @@ Example:
#### ToggleFifo
Toggle between {Start|Stop}Fifo
Toggle betwen {Start|Stop}Fifo
Type: { ToggleFifo = "string" }
@ -1422,6 +1084,4 @@ Example:
## Also See:
- [Message][1]
[1]: message.md
- [Message](message.md)

@ -35,18 +35,6 @@ The builtin go to path 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
The builtin selection ops mode.
@ -149,18 +137,6 @@ The builtin switch layout mode.
Type: [Mode](https://xplr.dev/en/mode)
#### xplr.config.modes.builtin.vroot
The builtin vroot mode.
Type: [Mode](https://xplr.dev/en/mode)
#### xplr.config.modes.builtin.edit_permissions
The builtin edit permissions mode.
Type: [Mode](https://xplr.dev/en/mode)
#### xplr.config.modes.custom
This is where you define custom modes.

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

@ -7,15 +7,25 @@ Once [installed][1], use the following steps to setup and run xplr.
```bash
mkdir -p ~/.config/xplr
version="$(xplr --version | awk '{print $2}')"
version="$(xplr | grep ^version: | cut -d' ' -f 2)"
echo "version = '${version:?}'" > ~/.config/xplr/init.lua
# When the app loads, press `#`
echo version = '"'${version:?}'"' > ~/.config/xplr/init.lua
```
Then
**[copy from here][2]**
and remove / comment out what you don't want to customize.
> **Note:** You don't generally need to create the config file. You can use the
> default configuration for basic operations. However, creating the config file
> is recommended because the project is in its early stage and the defaults
> might change. Creating the config file will save you from unexpected behavior
> when you [upgrade][3].
> Also, the default configuration is meant to be overwritten to suit your
> workflow.
## Run
```

@ -1,77 +0,0 @@
# Searching
xplr supports searching paths using different algorithm. The search mechanism
generally appears between filters and sorters in the `Sort & filter` panel.
Example:
```
fzy:foo↓
```
This line means that the nodes visible on the table are being filtered using the
[fuzzy matching][1] algorithm on the input `foo`. The arrow means that ranking based
ordering is being applied, i.e. [sorters][2] are being ignored.
## Node Searcher Applicable
Node Searcher contains the following fields:
- [pattern][3]
- [recoverable_focus][4]
- [algorithm][5]
- [unordered][7]
### pattern
The patterns used to search.
Type: string
### recoverable_focus
Where to focus when search is cancelled.
Type: nullable string
### algorithm
Search algorithm to use. Defaults to the value set in
[xplr.config.general.search.algorithm][8].
It can be one of the following:
- Fuzzy
- Regex
### unordered
Whether to skip ordering the search result by algorithm based ranking. Defaults
to the value set in [xplr.config.general.search.unordered][9].
Type: boolean
## Example:
```lua
local searcher = {
pattern = "pattern to search",
recoverable_focus = "/path/to/focus/on/cancel",
algorithm = "Fuzzy",
unordered = false,
}
xplr.util.explore({ searcher = searcher })
```
See [xplr.util.explore][6].
[1]: https://en.wikipedia.org/wiki/Approximate_string_matching
[2]: sorting.md
[3]: #pattern
[4]: #recoverable_focus
[5]: #algorithm
[6]: xplr.util.md#xplrutilexplore
[7]: #unordered
[8]: general-config.md#xplrconfiggeneralsearchalgorithm
[9]: general-config.md#xplrconfiggeneralsearchunordered

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

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

@ -1,96 +0,0 @@
# 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

@ -0,0 +1,34 @@
# TODO
- [x] Saner key bindings.
- [x] Pipes.
- [x] Native search & filter.
- [x] Create, copy, move, delete files directly.
- [x] logging support.
- [x] Version compatibility instructions.
- [x] Implement CLI arguments.
- ~Add support for tabs and/or panes (non native)~ [hacked][1] | [discussion][2]
- ~Implement bookmarks.~ [hacked][3]
- [x] Add sorting support.
- [x] Add filter support.
- [x] File previews.
- [x] Implement plugins support (or some way to easily share configuration).
- [x] Bigger (and better) help menu.
- [x] Offline docs.
- [ ] Support for background services
- [x] ~Customize~ switch UI at run-time.
- [ ] More tests and benchmarks.
- [ ] Measure code coverage.
- [ ] Improve the [vim plugin][4].
- [ ] Cleanup, refactor, optimize.
[add more][5]
Like this project so far? **[Please consider contributing][6].**
[1]: https://github.com/sayanarijit/xplr/wiki/Hacks#spawn-multiple-sessions-in-different-windows
[2]: https://github.com/sayanarijit/xplr/discussions/15
[3]: https://github.com/sayanarijit/xplr/wiki/Hacks#bookmark
[4]: https://github.com/sayanarijit/xplr.vim
[5]: https://github.com/sayanarijit/xplr/discussions/2
[6]: contribute.md

@ -32,149 +32,20 @@ Knowing that we use the `{major}.{minor}.{patch}` versioning format,
e.g.
- `1.0.0` -> `1.0.x`: Patch (fully compatible).
- `1.0.0` -> `1.0.x`: Bug fix (fully compatible).
- `1.0.0` -> `1.x.x`: Only backwards compatible. You can't generally use for
e.g. `app-1.0.0` with `config-1.1.0`. But vice versa is fine.
- `1.0.0` -> `x.x.x`: Not compatible at all.
Note that until we're `v1`, we'll be using the `{minor}` version number as
`{major}`, and the `{patch}` number as `{minor}` to determine
`{major}`, and the `{patch}` fix number as `{minor}` to determine
compatibility.
</details>
### Instructions
#### [v0.20.2][48] -> [v0.21.9][49]
- Some plugins might stop rendering colors. Wait for them to update.
- Rename `xplr.config.general.sort_and_filter_ui.search_identifier` to
`xplr.config.general.sort_and_filter_ui.search_identifiers`.
- Resolved Node API will not contain the `permissions` field anymore.
Use the utility function `xplr.util.node` to get its permissions.
- Layout `CustomContent` has been undocumented. It will stay for compatibility,
but you should prefer using the following new layouts, because they support
custom title:
- Static
- Dynamic
- Use the new messages for improved search operations:
- Search
- SearchFromInput
- SearchFuzzyUnordered
- SearchFuzzyUnorderedFromInput
- SearchRegex
- SearchRegexFromInput
- SearchRegexUnordered
- SearchRegexUnorderedFromInput
- ToggleSearchAlgorithm
- EnableSearchOrder
- DisableSearchOrder
- ToggleSearchOrder
- Use skim's [search syntax][50] to customize the search.
- Set your preferred search algorithm and ordering:
`xplr.config.general.search.algorithm = "Fuzzy" -- or "Regex"`.
`xplr.config.general.search.unordered = false -- or true`
- You need to clear the selection list manually after performing batch
operation like copy, softlink creation etc.
- Use the following new key bindings:
- `:sl` to list selection in a $PAGER.
- `:ss` to create softlink of the selected items.
- `:sh` to create hardlink of the selected items.
- `:se` to edit selection list in your $EDITOR.
- Better conflict handling: prompt for action.
- Navigate between the selected paths using the following messages:
- FocusPreviousSelection (`ctrl-p`)
- FocusNextSelection (`ctrl-n`)
- Use `LS_COLORS` environment variable, along with the following utility
- functions for applying better styling/theaming.
- xplr.util.lscolor
- xplr.util.paint
- xplr.util.textwrap
- xplr.util.style_mix
- Use new the fields in Column Renderer Argument:
- style
- permissions
- Use the following config to specify how the paths in selection list should be
rendered:
- xplr.config.general.selection.item.format
- xplr.config.general.selection.item.style
- Use the following utility functions to work with the file permissions:
- xplr.util.permissions_rwx
- xplr.util.permissions_octal
- Type `:p` to edit file permissions interactively.
- Also check out the following utility functions:
- xplr.util.layout_replace
- xplr.util.relative_to
- xplr.util.shorthand
- xplr.util.clone
- xplr.util.exists
- xplr.util.is_dir
- xplr.util.is_file
- xplr.util.is_symlink
- xplr.util.is_absolute
- xplr.util.path_split
- xplr.util.node
- xplr.util.node_type
- xplr.util.shell_escape
- Executables will me marked with the mime type: `application/x-executable`.
- macOS legacy coreutils will be generally supported, but please update it.
- 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.
#### [v0.19.4][47] -> [v0.20.2][48]
- BREAKING: xplr shell (`:!`) will default to null (`\0`) delimited pipes, as
opposed to newline (`\n`) delimited ones (i.e. will use `Call0` instead of
`Call`).
- Use new messages for safer file path handling (`\0` delimited):
- Call0
- CallSilently0
- BashExec0
- BashExecSilently0
- Use new sub-commands for safer message passing:
- `-m FORMAT [ARGUMENT]...` / `--pipe-msg-in FORMAT [ARGUMENT]...`
- `-M FORMAT [ARGUMENT]...` / `--print-msg-in FORMAT [ARGUMENT]...`
Where FORMAT is a YAML string that may contain `%s`, `%q` and `%%`
placeholders and ARGUMENT is the value per placeholder. See `init.lua`.
- Following hooks can be defined in the config files using an optional
`return { on_* = { list, of, messages }, ... }` statement at the end.
- on_load
- on_focus_change
- on_directory_change
- on_mode_switch (since v0.20.2)
- on_layout_switch (since v0.20.2)
- Use `--vroot` to isolate navigation of an xplr session inside a specific
directory. Interaction still requires passing full path, and shell,
lua functions etc still can access paths outside vroot.
- Use the following messages to switch vroot at runtime, or the use key
bindings available in the new builtin mode "vroot" (mapped to `:` `v`).
- SetVroot
- UnsetVroot
- ToggleVroot
- ResetVroot
- Use `$XPLR_INITIAL_PWD` and Lua equivalent to implement workspace like
features without using virtual root. Use keys `gi` to go to the initial
working directory from anywhere.
- Use the convenient `xplr.util` utility functions in your Lua function calls.
See xplr.util API docs.
#### [v0.18.0][46] -> [v0.19.4][47]
#### [v0.18.0][46] -> [v0.19.1][47]
- BREAKING: The builtin modes cannot be accessed using space separated names
anymore. Use underscore separated mode names. For e.g.
@ -206,20 +77,8 @@ Thanks to @noahmayr for contributing to a major part of this release.
- Fixed applying regex based filters via the CLI and `$XPLR_PIPE_MSG_IN` pipe.
- You can use the `prompt` field to define input prompt for each mode, instead
of using the `SetInputPrompt` message.
- Since version v0.19.4, the native search will default to skim-v2 based fuzzy
matching. `esc` while in search mode will recover the initial focus. People
who prefer the regex based search, can use the `regex-search.xplr` plugin.
The following messages will be available for search based operations:
- SearchFuzzy
- SearchFuzzyFromInput
- AcceptSearch
- CancelSearch
- Since version v0.19.4, quick scrolling operations are supported using the
following messages and keys:
- ScrollUp -------- page-up
- ScrollDown ------ page-down
- ScrollUpHalf ---- {
- ScrollDownHalf -- }
<sub>Like this project so far? **[Please consider contributing][5]**.</sub>
#### [v0.17.6][45] -> [v0.18.0][46]
@ -319,7 +178,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`.
- Try running `xplr --help`. Yes, CLI has been implemented.
- 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 explicitly.
path when called. So, there's no need to pipe the focus path explicitely.
- 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.
- Since version `v0.14.4`, `$XPLR_SESSION_PATH` can be used to dump session
@ -330,7 +189,7 @@ Thanks to @noahmayr for contributing to a major part of this release.
#### [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).
- Each `node_types` config will inherit defaults from matching less specific `node_types` config and overwrite them.
- Each `node_types` config will inherit defaults from matching less specifig `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.
#### [v0.11.1][11] -> [v0.12.1][6]
@ -486,6 +345,7 @@ Else do the following:
[2]: https://github.com/sayanarijit/xplr/releases/tag/v0.13.7
[3]: https://github.com/sayanarijit/xplr/releases/tag/v0.14.7
[4]: https://github.com/sayanarijit/xplr/pull/229#issue-662426960
[5]: contribute.md
[6]: https://github.com/sayanarijit/xplr/releases/tag/v0.12.1
[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
@ -527,7 +387,4 @@ Else do the following:
[44]: https://github.com/sayanarijit/xplr/releases/tag/v0.16.4
[45]: https://github.com/sayanarijit/xplr/releases/tag/v0.17.6
[46]: https://github.com/sayanarijit/xplr/releases/tag/v0.18.0
[47]: https://github.com/sayanarijit/xplr/releases/tag/v0.19.4
[48]: https://github.com/sayanarijit/xplr/releases/tag/v0.20.2
[49]: https://github.com/sayanarijit/xplr/releases/tag/v0.21.9
[50]: https://github.com/lotabout/skim#search-syntax
[47]: https://github.com/sayanarijit/xplr/releases/tag/v0.19.1

@ -19,7 +19,8 @@ A minimal plugin should confirm to the following structure:
└── init.lua
```
You can also use [this template][2].
You can also use
[this template][2].
### README.md
@ -50,18 +51,6 @@ to append `.xplr` to the name to make them distinguishable. Similar to the
Finally, after publishing, don't hesitate to
[let us know][4].
## Best practices
- Try not to execute a lot of commands at startup, it may make xplr slow to
start.
- When executing commands, prefer `Call0` over `Call`, `BashExec0` over
`BashExec` and so on. File names may contain newline characters
(e.g. `foo$'\n'bar`).
- File names may also contain quotes. Avoid writing directly to
`$XPLR_PIPE_MSG_IN`. Use `xplr -m` / `xplr --pipe-msg-in` instead.
- Check for empty variables using the syntax `${FOO:?}` or use a default value
`${FOO:-defaultvalue}`.
## Examples
Visit [Awesome Plugins][5] for xplr plugin examples.
@ -69,14 +58,15 @@ Visit [Awesome Plugins][5] for xplr plugin examples.
## Also See
- [Tip: A list of hacks yet to make it as Lua plugins][15]
- [Tip: Some UI and theming tips][12]
- [Tip: Some UI and themeing 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]
- [Example: Using Environment Variables and Pipes][7]
- [Example: Using Lua Function Calls][8]
- [Example: Defining Custom Layout][9]
- [Example: Customizing Table Renderer][10]
- [Example: Render a custom dynamic table][11]
- [Example: Implementing a directory visit counter][16]
[1]: https://www.lua.org
[2]: https://github.com/sayanarijit/plugin-template1.xplr
@ -90,5 +80,6 @@ Visit [Awesome Plugins][5] for xplr plugin examples.
[10]: column-renderer.md#example-customizing-table-renderer
[11]: layout.md#example-render-a-custom-dynamic-table
[12]: https://github.com/sayanarijit/xplr/discussions/274
[15]: awesome-hacks.md
[16]: https://github.com/sayanarijit/xplr/discussions/529#discussioncomment-4073734
[13]: https://github.com/sayanarijit/xplr/discussions/273
[14]: https://github.com/sayanarijit/xplr/discussions/250
[15]: https://github.com/sayanarijit/xplr/wiki/Hacks

@ -1,528 +0,0 @@
### xplr.util.version
Get the xplr version details.
Type: function() -> { major: number, minor: number, patch: number }
Example:
```lua
xplr.util.version()
-- { 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
Clone/deepcopy a Lua value. Doesn't work with functions.
Type: function( value ) -> value
Example:
```lua
local val = { foo = "bar" }
local val_clone = xplr.util.clone(val)
val.foo = "baz"
print(val_clone.foo)
-- "bar"
```
### xplr.util.exists
Check if the given path exists.
Type: function( path:string ) -> boolean
Example:
```lua
xplr.util.exists("/foo/bar")
-- true
```
### xplr.util.is_dir
Check if the given path is a directory.
Type: function( path:string ) -> boolean
Example:
```lua
xplr.util.is_dir("/foo/bar")
-- true
```
### xplr.util.is_file
Check if the given path is a file.
Type: function( path:string ) -> boolean
Example:
```lua
xplr.util.is_file("/foo/bar")
-- true
```
### xplr.util.is_symlink
Check if the given path is a symlink.
Type: function( path:string ) -> boolean
Example:
```lua
xplr.util.is_file("/foo/bar")
-- true
```
### xplr.util.is_absolute
Check if the given path is an absolute path.
Type: function( path:string ) -> boolean
Example:
```lua
xplr.util.is_absolute("/foo/bar")
-- true
```
### xplr.util.path_split
Split a path into its components.
Type: function( path:string ) -> boolean
Example:
```lua
xplr.util.path_split("/foo/bar")
-- { "/", "foo", "bar" }
xplr.util.path_split(".././foo")
-- { "..", "foo" }
```
### xplr.util.node
Get [Node][5] information of a given path.
Doesn't check if the path exists.
Returns nil if the path is "/".
Errors out if absolute path can't be obtained.
Type: function( path:string ) -> [Node][5]|nil
Example:
```lua
xplr.util.node("./bar")
-- { parent = "/pwd", relative_path = "bar", absolute_path = "/pwd/bar", ... }
xplr.util.node("/")
-- nil
```
### xplr.util.node_type
Get the configured [Node Type][6] of a given [Node][5].
Type: function( [Node][5], [xplr.config.node_types][7]|nil ) -> [Node Type][6]
If the second argument is missing, global config `xplr.config.node_types`
will be used.
Example:
```lua
xplr.util.node_type(app.focused_node)
-- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... }
xplr.util.node_type(xplr.util.node("/foo/bar"), xplr.config.node_types)
-- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... }
```
### xplr.util.dirname
Get the directory name of a given path.
Type: function( path:string ) -> path:string|nil
Example:
```lua
xplr.util.dirname("/foo/bar")
-- "/foo"
```
### xplr.util.basename
Get the base name of a given path.
Type: function( path:string ) -> path:string|nil
Example:
```lua
xplr.util.basename("/foo/bar")
-- "bar"
```
### xplr.util.absolute
Get the absolute path of the given path by prepending $PWD.
It doesn't check if the path exists.
Type: function( path:string ) -> path:string
Example:
```lua
xplr.util.absolute("foo/bar")
-- "/tmp/foo/bar"
```
### xplr.util.relative_to
Get the relative path based on the given base path or current working dir.
Will error if it fails to determine a relative path.
Type: function( path:string, options:table|nil ) -> path:string
Options type: { base:string|nil, with_prefix_dots:bookean|nil, without_suffix_dots:boolean|nil }
- If `base` path is given, the path will be relative to it.
- If `with_prefix_dots` is true, the path will always start with dots `..` / `.`
- If `without_suffix_dots` is true, the name will be visible instead of dots `..` / `.`
Example:
```lua
xplr.util.relative_to("/present/working/directory")
-- "."
xplr.util.relative_to("/present/working/directory/foo")
-- "foo"
xplr.util.relative_to("/present/working/directory/foo", { with_prefix_dots = true })
-- "./foo"
xplr.util.relative_to("/present/working/directory", { without_suffix_dots = true })
-- "../directory"
xplr.util.relative_to("/present/working")
-- ".."
xplr.util.relative_to("/present/working", { without_suffix_dots = true })
-- "../../working"
xplr.util.relative_to("/present/working/directory", { base = "/present/foo/bar" })
-- "../../working/directory"
```
### xplr.util.shorten
Shorten the given absolute path using the following rules:
- either relative to your home dir if it makes sense
- or relative to the current working directory
- or absolute path if it makes the most sense
Type: Similar to `xplr.util.relative_to`
Example:
```lua
xplr.util.shorten("/home/username/.config")
-- "~/.config"
xplr.util.shorten("/present/working/directory")
-- "."
xplr.util.shorten("/present/working/directory/foo")
-- "foo"
xplr.util.shorten("/present/working/directory/foo", { with_prefix_dots = true })
-- "./foo"
xplr.util.shorten("/present/working/directory", { without_suffix_dots = true })
-- "../directory"
xplr.util.shorten("/present/working/directory", { base = "/present/foo/bar" })
-- "../../working/directory"
xplr.util.shorten("/tmp")
-- "/tmp"
```
### xplr.util.explore
Explore directories with the given explorer config.
Type: function( path:string, [ExplorerConfig][1]|nil ) -> { [Node][2], ... }
Example:
```lua
xplr.util.explore("/tmp")
-- { { absolute_path = "/tmp/a", ... }, ... }
xplr.util.explore("/tmp", app.explorer_config)
-- { { absolute_path = "/tmp/a", ... }, ... }
```
### xplr.util.shell_execute
Execute shell commands safely.
Type: function( program:string, args:{ string, ... }|nil ) -> { stdout = string, stderr = string, returncode = number|nil }
Example:
```lua
xplr.util.shell_execute("pwd")
-- { stdout = "/present/working/directory", stderr = "", returncode = 0 }
xplr.util.shell_execute("bash", {"-c", "xplr --help"})
-- { stdout = "xplr...", stderr = "", returncode = 0 }
```
### xplr.util.shell_quote
Quote commands and paths safely.
Type: function( string ) -> string
Example:
```lua
xplr.util.shell_quote("a'b\"c")
-- 'a'"'"'b"c'
```
### xplr.util.shell_escape
Escape commands and paths safely.
Type: function( string ) -> string
Example:
```lua
xplr.util.shell_escape("a'b\"c")
-- "\"a'b\\\"c\""
```
### xplr.util.from_json
Load JSON string into Lua value.
Type: function( string ) -> any
Example:
```lua
xplr.util.from_json([[{"foo": "bar"}]])
-- { foo = "bar" }
```
### xplr.util.to_json
Dump Lua value into JSON (i.e. also YAML) string.
Type: function( value ) -> string
Example:
```lua
xplr.util.to_json({ foo = "bar" })
-- [[{ "foo": "bar" }]]
xplr.util.to_json({ foo = "bar" }, { pretty = true })
-- [[{
-- "foo": "bar"
-- }]]
```
### xplr.util.from_yaml
Load YAML (i.e. also JSON) string into Lua value.
Type: function( string ) -> value
Example:
```lua
xplr.util.from_yaml([[{foo: bar}]])
-- { foo = "bar" }
```
### xplr.util.to_yaml
Dump Lua value into YAML string.
Type: function( value ) -> string
Example:
```lua
xplr.util.to_yaml({ foo = "bar" })
-- "foo: bar"
```
### xplr.util.lscolor
Get a [Style][3] object for the given path based on the LS_COLORS
environment variable.
Type: function( path:string ) -> [Style][3]
Example:
```lua
xplr.util.lscolor("Desktop")
-- { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} }
```
### xplr.util.paint
Apply style (escape sequence) to string using a given [Style][3] object.
Type: function( string, [Style][3]|nil ) -> string
Example:
```lua
xplr.util.paint("Desktop", { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} })
-- "\u001b[31mDesktop\u001b[0m"
```
### xplr.util.style_mix
Mix multiple [Style][3] objects into one.
Type: function( { [Style][3], [Style][3], ... } ) -> [Style][3]
Example:
```lua
xplr.util.style_mix({{ fg = "Red" }, { bg = "Blue" }, { add_modifiers = {"Bold"} }})
-- { fg = "Red", bg = "Blue", add_modifiers = { "Bold" }, sub_modifiers = {} }
```
### xplr.util.textwrap
Wrap the given text to fit the specified width.
It will try to not split words when possible.
Type: function( string, options:number|table ) -> { string, ...}
Options type: { width = number, initial_indent = string|nil, subsequent_indent = string|nil, break_words = boolean|nil }
Example:
```lua
xplr.util.textwrap("this will be cut off", 11)
-- { "this will', 'be cut off" }
xplr.util.textwrap(
"this will be cut off",
{ width = 12, initial_indent = "", subsequent_indent = " ", break_words = false }
)
-- { "this will be", " cut off" }
```
### xplr.util.layout_replace
Find the target layout in the given layout and replace it with the replacement layout,
returning a new layout.
Type: function( layout:[Layout][4], target:[Layout][4], replacement:[Layout][4] ) -> layout:[Layout][4]
Example:
```lua
local layout = {
Horizontal = {
splits = {
"Table", -- Target
"HelpMenu",
},
config = ...,
}
}
xplr.util.layout_replace(layout, "Table", "Selection")
-- {
-- Horizontal = {
-- splits = {
-- "Selection", -- Replacement
-- "HelpMenu",
-- },
-- config = ...
-- }
-- }
```
### xplr.util.permissions_rwx
Convert [Permission][8] to rwxrwxrwx representation with special bits.
Type: function( [Permission][8] ) -> string
Example:
```lua
xplr.util.permissions_rwx({ user_read = true })
-- "r--------"
xplr.util.permissions_rwx(app.focused_node.permission)
-- "rwxrwsrwT"
```
### xplr.util.permissions_octal
Convert [Permission][8] to octal representation.
Type: function( [Permission][8] ) -> { number, number, number, number }
Example:
```lua
xplr.util.permissions_octal({ user_read = true })
-- { 0, 4, 0, 0 }
xplr.util.permissions_octal(app.focused_node.permission)
-- { 0, 7, 5, 4 }
```
[1]: https://xplr.dev/en/lua-function-calls#explorer-config
[2]: https://xplr.dev/en/lua-function-calls#node
[3]: https://xplr.dev/en/style
[4]: https://xplr.dev/en/layout
[5]: https://xplr.dev/en/lua-function-calls#node
[6]: https://xplr.dev/en/node-type
[7]: https://xplr.dev/en/node_types
[8]: https://xplr.dev/en/column-renderer#permission

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

@ -0,0 +1 @@
../en/book

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

@ -1,11 +0,0 @@
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

@ -4,12 +4,13 @@ import os
from dataclasses import dataclass
from typing import List
# Messages --------------------------------------------------------------------
MESSAGES_DOC_TEMPLATE = """
# Full List of Messages
xplr [messages][1] categorized based on their purpose.
xplr messages categorized based on their purpose.
## Categories
@ -19,9 +20,7 @@ xplr [messages][1] categorized based on their purpose.
## Also See:
- [Message][1]
[1]: message.md
- [Message](message.md)
""".strip()
CONFIGURATION_DOC_TEMPLATE = """
@ -135,7 +134,6 @@ def gen_configuration():
- node_types.md
- layouts.md
- modes.md
- modes.md
"""
path = "./src/init.lua"
@ -159,7 +157,6 @@ def gen_configuration():
line.startswith("-- # Configuration ")
or line.startswith("-- ## Config ")
or line.startswith("-- ## Function ")
or line.startswith("-- ## On Load ")
):
reading = configuration
@ -219,51 +216,6 @@ def gen_configuration():
print(doc, file=f)
# xplr.util -------------------------------------------------------------------
@dataclass
class Function:
doc: List[str]
name: str
def gen_xplr_util():
path = "./src/lua/util.rs"
functions: List[Function] = []
with open(path) as f:
lines = iter(f.read().splitlines())
reading = None
for line in lines:
if line.startswith("///"):
if reading:
reading.doc.append(line[4:])
else:
reading = Function(doc=[line[4:]], name="")
if line.startswith("pub fn") and reading:
reading.name = "\n### xplr.util." + line.split("<")[0].split()[-1] + "\n"
functions.append(reading)
reading = None
continue
with open("./docs/en/src/xplr.util.md", "w") as f:
for function in functions:
print(function.name)
print(function.name, file=f)
print("\n".join(function.doc))
print("\n".join(function.doc), file=f)
if reading:
print("\n".join(reading.doc), file=f)
def format_docs():
os.system("prettier --write docs/en/src")
@ -271,7 +223,6 @@ def format_docs():
def main():
gen_messages()
gen_configuration()
gen_xplr_util()
format_docs()

@ -1,26 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1704262187,
"narHash": "sha256-N4j9qghlp/Eb3Z11WF7Cb9U91AXwpascUbLH7YKMcLc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "65f0d241783c94a08e4c9a3870736fc8854dd520",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

@ -1,70 +0,0 @@
{
description = "xplr - A hackable, minimal, fast TUI file explorer";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
};
outputs = inputs@{ self, nixpkgs, ... }:
let
lib = nixpkgs.lib;
darwin = [ "x86_64-darwin" "aarch64-darwin" ];
linux = [ "x86_64-linux" "x86_64-linux-musl" "aarch64-linux" "aarch64-linux-android" "i86_64-linux" ];
allSystems = darwin ++ linux;
forEachSystem = systems: f: lib.genAttrs systems (system: f system);
forAllSystems = forEachSystem allSystems;
in
{
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
in
rec {
# e.g. nix build .#xplr
xplr = pkgs.rustPlatform.buildRustPackage rec {
name = "xplr";
src = ./.;
cargoLock = {
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);
devShells = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
devRequirements = with pkgs; [
gcc
gnumake
clippy
rustc
cargo
rustfmt
rust-analyzer
];
in
{
default = pkgs.mkShell {
RUST_BACKTRACE = 1;
# For cross compilation
NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM = 1;
buildInputs = devRequirements;
packages = devRequirements;
};
}
);
};
}

@ -1,26 +0,0 @@
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

File diff suppressed because it is too large Load Diff

@ -1,12 +1,13 @@
#![allow(clippy::too_many_arguments)]
use std::env;
use xplr::cli::{self, Cli};
use xplr::cli::Cli;
use xplr::runner;
fn main() {
let cli = Cli::parse(env::args()).unwrap_or_else(|e| {
eprintln!("error: {e}");
eprintln!("error: {}", e);
std::process::exit(1);
});
@ -15,32 +16,27 @@ fn main() {
xplr [FLAG]... [OPTION]... [PATH] [SELECTION]..."###;
let flags = r###"
- Reads new-line (\n) separated paths from stdin
-- Denotes the end of command-line flags and options
--force-focus Focuses on the given <PATH>, even if it is a directory
-h, --help Prints help information
-m, --pipe-msg-in Helps safely passing messages to the active xplr
session, use %%, %s and %q as the placeholders
-M, --print-msg-in Like --pipe-msg-in, but prints the message instead of
passing to the active xplr session
--print-pwd-as-result Prints the present working directory when quitting
with `PrintResultAndQuit`
--read-only Enables read-only mode (config.general.read_only)
--read0 Reads paths separated using the null character (\0)
--write0 Prints paths separated using the null character (\0)
-0 --null Combines --read0 and --write0
-V, --version Prints version information"###;
- Reads new-line (\n) separated paths from stdin
-- Denotes the end of command-line flags and options
--force-focus Focuses on the given <PATH>, even if it is a directory
-h, --help Prints help information
--print-pwd-as-result Prints the present working directory when quitting
with `PrintResultAndQuit`
--read-only Enables read-only mode (config.general.read_only)
--read0 Reads paths separated using the null character (\0)
--write0 Prints paths separated using the null character (\0)
-0 --null Combines --read0 and --write0
-V, --version Prints version information"###;
let options = r###"
-c, --config <PATH> Specifies a custom config file (default is
"$HOME/.config/xplr/init.lua")
-C, --extra-config <PATH>... Specifies extra config files to load
--on-load <MESSAGE>... Sends messages when xplr loads
--vroot <PATH> Treats the specified path as the virtual root"###;
-c, --config <PATH> Specifies a custom config file (default is
"$HOME/.config/xplr/init.lua")
-C, --extra-config <PATH>... Specifies extra config files to load
--on-load <MESSAGE>... Sends messages when xplr loads"###;
let args = r###"
<PATH> Path to focus on, or enter if directory, (default is `.`)
<SELECTION>... Paths to select, requires <PATH> to be set explicitly"###;
<PATH> Path to focus on, or enter if directory, (default is `.`)
<SELECTION>... Paths to select, requires <PATH> to be set explicitly"###;
let help = format!(
"xplr {}\n{}\n{}\n\nUSAGE:{}\n\nFLAGS:{}\n\nOPTIONS:{}\n\nARGS:{}",
@ -54,28 +50,26 @@ fn main() {
);
let help = help.trim();
println!("{help}");
println!("{}", help);
} else if cli.version {
println!("xplr {}", xplr::app::VERSION);
} else if !cli.pipe_msg_in.is_empty() {
if let Err(err) = cli::pipe_msg_in(cli.pipe_msg_in) {
eprintln!("error: {err}");
std::process::exit(1);
}
} else if !cli.print_msg_in.is_empty() {
if let Err(err) = cli::print_msg_in(cli.print_msg_in) {
eprintln!("error: {err}");
std::process::exit(1);
}
} else {
let write0 = cli.write0;
match runner::from_cli(cli).and_then(|a| a.run()) {
Ok(Some(out)) => {
print!("{out}");
Ok(Some(mut out)) => {
if write0 {
out = out
.trim_end()
.chars()
.map(|c| if c == '\n' { '\0' } else { c })
.collect();
}
print!("{}", out);
}
Ok(None) => {}
Err(err) => {
if !err.to_string().is_empty() {
eprintln!("error: {err}");
eprintln!("error: {}", err);
};
std::process::exit(1);

@ -1,17 +1,14 @@
use crate::{app, yaml};
use anyhow::{bail, Context, Result};
use app::ExternalMsg;
use path_absolutize::*;
use serde_json as json;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use anyhow::{bail, Result};
use std::collections::VecDeque;
use std::env;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::{env, fs};
use crate::app;
/// The arguments to pass
#[derive(Debug, Clone, Default)]
pub struct Cli {
pub bin: String,
pub version: bool,
pub help: bool,
pub read_only: bool,
@ -19,48 +16,37 @@ pub struct Cli {
pub print_pwd_as_result: bool,
pub read0: bool,
pub write0: bool,
pub vroot: Option<PathBuf>,
pub config: Option<PathBuf>,
pub extra_config: Vec<PathBuf>,
pub on_load: Vec<app::ExternalMsg>,
pub pipe_msg_in: Vec<String>,
pub print_msg_in: Vec<String>,
pub paths: Vec<PathBuf>,
}
impl Cli {
fn read_path(arg: &str) -> Result<PathBuf> {
fn read_path(&mut self, arg: &str) -> Result<()> {
if arg.is_empty() {
bail!("empty string passed")
};
let path = PathBuf::from(arg).absolutize()?.to_path_buf();
let path = PathBuf::from(arg);
if path.exists() {
Ok(path)
self.paths.push(path);
Ok(())
} else {
bail!("path doesn't exist: {}", path.to_string_lossy())
bail!("path doesn't exist: {}", path.to_string_lossy().to_string())
}
}
/// Parse arguments from the command-line
pub fn parse(args: env::Args) -> Result<Self> {
let mut args: VecDeque<String> = args.skip(1).collect();
let mut cli = Self::default();
let mut args = args.peekable();
cli.bin = args
.next()
.map(which::which)
.context("failed to parse xplr binary path")?
.context("failed to find xplr binary path")?
.absolutize()?
.to_path_buf()
.to_string_lossy()
.to_string();
let mut flag_ends = false;
while let Some(arg) = args.next() {
while let Some(arg) = args.pop_front() {
if flag_ends {
cli.paths.push(Cli::read_path(&arg)?);
cli.read_path(&arg)?;
} else {
match arg.as_str() {
// Flags
@ -68,12 +54,11 @@ impl Cli {
let reader = BufReader::new(std::io::stdin());
if cli.read0 {
for path in reader.split(b'\0') {
cli.paths
.push(Cli::read_path(&String::from_utf8(path?)?)?);
cli.read_path(&String::from_utf8(path?)?)?;
}
} else {
for path in reader.lines() {
cli.paths.push(Cli::read_path(&path?)?);
cli.read_path(&path?)?;
}
};
}
@ -105,34 +90,30 @@ impl Cli {
// Options
"-c" | "--config" => {
cli.config = Some(
args.next()
.map(|a| Cli::read_path(&a))
.with_context(|| format!("usage: xplr {arg} PATH"))??,
);
}
"--vroot" => {
cli.vroot = Some(
args.next()
.map(|a| Cli::read_path(&a))
.with_context(|| format!("usage: xplr {arg} PATH"))??,
);
cli.config = args.pop_front().map(PathBuf::from)
}
"-C" | "--extra-config" => {
while let Some(path) =
args.next_if(|path| !path.starts_with('-'))
{
cli.extra_config.push(Cli::read_path(&path)?);
while let Some(path) = args.pop_front() {
if path.starts_with('-') {
args.push_front(path);
break;
} else {
cli.extra_config.push(PathBuf::from(path));
}
}
}
"--read-only" => cli.read_only = true,
"--on-load" => {
while let Some(msg) = args.next_if(|msg| !msg.starts_with('-')) {
cli.on_load.push(yaml::from_str(&msg)?);
while let Some(msg) = args.pop_front() {
if msg.starts_with('-') {
args.push_front(msg);
break;
} else {
cli.on_load.push(serde_yaml::from_str(&msg)?);
}
}
}
@ -144,30 +125,9 @@ impl Cli {
cli.print_pwd_as_result = true;
}
"-m" | "--pipe-msg-in" => {
cli.pipe_msg_in.extend(args.by_ref());
if cli.pipe_msg_in.is_empty() {
bail!("usage: xplr {} FORMAT [ARGUMENT]...", arg)
}
}
"-M" | "--print-msg-in" => {
cli.print_msg_in.extend(args.by_ref());
if cli.print_msg_in.is_empty() {
bail!("usage: xplr {} FORMAT [ARGUMENT]...", arg)
}
}
// path
path => {
if path.starts_with('-') && !flag_ends {
bail!(
"invalid argument: {0:?}, try `-- {0:?}` or `--help`",
path
)
} else {
cli.paths.push(Cli::read_path(path)?);
}
cli.read_path(path)?;
}
}
}
@ -175,45 +135,3 @@ impl Cli {
Ok(cli)
}
}
pub fn pipe_msg_in(args: Vec<String>) -> Result<()> {
let mut msg = fmt_msg_in(args)?;
if let Ok(path) = std::env::var("XPLR_PIPE_MSG_IN") {
let delimiter = fs::read(&path)?
.first()
.cloned()
.context("failed to detect delimmiter")?;
msg.push(delimiter.into());
File::options()
.append(true)
.open(&path)?
.write_all(msg.as_bytes())?;
} else {
println!("{msg}");
};
Ok(())
}
pub fn print_msg_in(args: Vec<String>) -> Result<()> {
let msg = fmt_msg_in(args)?;
print!("{msg}");
Ok(())
}
fn fmt_msg_in(args: Vec<String>) -> Result<String> {
let msg = match jf::format(args.into_iter().map(Into::into)) {
Ok(msg) => msg,
Err(jf::Error::Jf(e)) => bail!("xplr -m: {e}"),
Err(jf::Error::Json(e)) => bail!("xplr -m: json: {e}"),
Err(jf::Error::Yaml(e)) => bail!("xplr -m: yaml: {e}"),
Err(jf::Error::Io(e)) => bail!("xplr -m: io: {e}"),
};
// validate
let _: ExternalMsg = json::from_str(&msg)?;
Ok(msg)
}

@ -1,224 +0,0 @@
// Things of the past, mostly bad decisions, which cannot erased, stays in this
// haunted module.
use crate::app;
use crate::lua;
use crate::ui::block;
use crate::ui::string_to_text;
use crate::ui::Constraint;
use crate::ui::ContentRendererArg;
use crate::ui::UI;
use serde::{Deserialize, Serialize};
use tui::layout::Constraint as TuiConstraint;
use tui::layout::Rect as TuiRect;
use tui::widgets::Cell;
use tui::widgets::List;
use tui::widgets::ListItem;
use tui::widgets::Paragraph;
use tui::widgets::Row;
use tui::widgets::Table;
use tui::Frame;
/// A cursed enum from crate::ui.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub enum ContentBody {
/// A paragraph to render
StaticParagraph { render: String },
/// A Lua function that returns a paragraph to render
DynamicParagraph { render: String },
/// List to render
StaticList { render: Vec<String> },
/// A Lua function that returns lines to render
DynamicList { render: String },
/// A table to render
StaticTable {
widths: Vec<Constraint>,
col_spacing: Option<u16>,
render: Vec<Vec<String>>,
},
/// A Lua function that returns a table to render
DynamicTable {
widths: Vec<Constraint>,
col_spacing: Option<u16>,
render: String,
},
}
/// A cursed struct from crate::ui.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CustomContent {
pub title: Option<String>,
pub body: ContentBody,
}
/// A cursed function from crate::ui.
pub fn draw_custom_content(
ui: &mut UI,
f: &mut Frame,
layout_size: TuiRect,
app: &app::App,
content: CustomContent,
) {
let config = app.config.general.panel_ui.default.clone();
let title = content.title;
let body = content.body;
match body {
ContentBody::StaticParagraph { render } => {
let render = string_to_text(render);
let content = Paragraph::new(render).block(block(
config,
title.map(|t| format!(" {t} ")).unwrap_or_default(),
));
f.render_widget(content, layout_size);
}
ContentBody::DynamicParagraph { render } => {
let ctx = ContentRendererArg {
app: app.to_lua_ctx_light(),
layout_size: layout_size.into(),
screen_size: ui.screen_size.into(),
scrolltop: ui.scrolltop as u16,
};
let render = lua::serialize(ui.lua, &ctx)
.map(|arg| {
lua::call(ui.lua, &render, arg).unwrap_or_else(|e| format!("{e:?}"))
})
.unwrap_or_else(|e| e.to_string());
let render = string_to_text(render);
let content = Paragraph::new(render).block(block(
config,
title.map(|t| format!(" {t} ")).unwrap_or_default(),
));
f.render_widget(content, layout_size);
}
ContentBody::StaticList { render } => {
let items = render
.into_iter()
.map(string_to_text)
.map(ListItem::new)
.collect::<Vec<ListItem>>();
let content = List::new(items).block(block(
config,
title.map(|t| format!(" {t} ")).unwrap_or_default(),
));
f.render_widget(content, layout_size);
}
ContentBody::DynamicList { render } => {
let ctx = ContentRendererArg {
app: app.to_lua_ctx_light(),
layout_size: layout_size.into(),
screen_size: ui.screen_size.into(),
scrolltop: ui.scrolltop as u16,
};
let items = lua::serialize(ui.lua, &ctx)
.map(|arg| {
lua::call(ui.lua, &render, arg)
.unwrap_or_else(|e| vec![format!("{e:?}")])
})
.unwrap_or_else(|e| vec![e.to_string()])
.into_iter()
.map(string_to_text)
.map(ListItem::new)
.collect::<Vec<ListItem>>();
let content = List::new(items).block(block(
config,
title.map(|t| format!(" {t} ")).unwrap_or_default(),
));
f.render_widget(content, layout_size);
}
ContentBody::StaticTable {
widths,
col_spacing,
render,
} => {
let rows = render
.into_iter()
.map(|cols| {
Row::new(
cols.into_iter()
.map(string_to_text)
.map(Cell::from)
.collect::<Vec<Cell>>(),
)
})
.collect::<Vec<Row>>();
let widths = widths
.into_iter()
.map(|w| w.to_tui(ui.screen_size, layout_size))
.collect::<Vec<TuiConstraint>>();
let content = Table::new(rows, widths)
.column_spacing(col_spacing.unwrap_or(1))
.block(block(
config,
title.map(|t| format!(" {t} ")).unwrap_or_default(),
));
f.render_widget(content, layout_size);
}
ContentBody::DynamicTable {
widths,
col_spacing,
render,
} => {
let ctx = ContentRendererArg {
app: app.to_lua_ctx_light(),
layout_size: layout_size.into(),
screen_size: ui.screen_size.into(),
scrolltop: ui.scrolltop as u16,
};
let rows = lua::serialize(ui.lua, &ctx)
.map(|arg| {
lua::call(ui.lua, &render, arg)
.unwrap_or_else(|e| vec![vec![format!("{e:?}")]])
})
.unwrap_or_else(|e| vec![vec![e.to_string()]])
.into_iter()
.map(|cols| {
Row::new(
cols.into_iter()
.map(string_to_text)
.map(Cell::from)
.collect::<Vec<Cell>>(),
)
})
.collect::<Vec<Row>>();
let widths = widths
.into_iter()
.map(|w| w.to_tui(ui.screen_size, layout_size))
.collect::<Vec<TuiConstraint>>();
let mut content = Table::new(rows, &widths).block(block(
config,
title.map(|t| format!(" {t} ")).unwrap_or_default(),
));
if let Some(col_spacing) = col_spacing {
content = content.column_spacing(col_spacing);
};
f.render_widget(content, layout_size);
}
}
}

@ -3,8 +3,6 @@ use crate::app::HelpMenuLine;
use crate::app::NodeFilter;
use crate::app::NodeSorter;
use crate::app::NodeSorterApplicable;
use crate::node::Node;
use crate::search::SearchAlgorithm;
use crate::ui::Border;
use crate::ui::BorderType;
use crate::ui::Constraint;
@ -55,7 +53,7 @@ pub struct NodeTypeConfig {
impl NodeTypeConfig {
pub fn extend(mut self, other: &Self) -> Self {
self.style = self.style.extend(&other.style);
self.meta.extend(other.meta.clone());
self.meta.extend(other.meta.to_owned());
self
}
}
@ -82,41 +80,7 @@ pub struct NodeTypesConfig {
pub special: HashMap<String, NodeTypeConfig>,
}
impl NodeTypesConfig {
pub fn get(&self, node: &Node) -> NodeTypeConfig {
let mut node_type = if node.is_symlink {
self.symlink.clone()
} else if node.is_dir {
self.directory.clone()
} else {
self.file.clone()
};
let mut me = node.mime_essence.splitn(2, '/');
let mimetype: String = me.next().map(|s| s.into()).unwrap_or_default();
let mimesub: String = me.next().map(|s| s.into()).unwrap_or_default();
if let Some(conf) = self
.mime_essence
.get(&mimetype)
.and_then(|t| t.get(&mimesub).or_else(|| t.get("*")))
{
node_type = node_type.extend(conf);
}
if let (Some(conf), false) = (self.extension.get(&node.extension), node.is_dir) {
node_type = node_type.extend(conf);
}
if let Some(conf) = self.special.get(&node.relative_path) {
node_type = node_type.extend(conf);
}
node_type
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UiConfig {
#[serde(default)]
@ -129,7 +93,7 @@ pub struct UiConfig {
pub style: Style,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct UiElement {
#[serde(default)]
@ -141,7 +105,7 @@ pub struct UiElement {
impl UiElement {
pub fn extend(mut self, other: &Self) -> Self {
self.format = other.format.clone().or(self.format);
self.format = other.format.to_owned().or(self.format);
self.style = self.style.extend(&other.style);
self
}
@ -182,23 +146,6 @@ pub struct TableConfig {
pub col_widths: Option<Vec<Constraint>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SelectionConfig {
#[serde(default)]
pub item: UiElement,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchConfig {
#[serde(default)]
pub algorithm: SearchAlgorithm,
#[serde(default)]
pub unordered: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LogsConfig {
@ -225,16 +172,6 @@ pub struct SortDirectionIdentifiersUi {
pub reverse: UiElement,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SearchDirectionIdentifiersUi {
#[serde(default)]
pub ordered: UiElement,
#[serde(default)]
pub unordered: UiElement,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SortAndFilterUi {
@ -252,12 +189,6 @@ pub struct SortAndFilterUi {
#[serde(default)]
pub filter_identifiers: HashMap<NodeFilter, UiElement>,
#[serde(default)]
pub search_direction_identifiers: SearchDirectionIdentifiersUi,
#[serde(default)]
pub search_identifiers: HashMap<SearchAlgorithm, UiElement>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -315,12 +246,6 @@ pub struct GeneralConfig {
#[serde(default)]
pub table: TableConfig,
#[serde(default)]
pub selection: SelectionConfig,
#[serde(default)]
pub search: SearchConfig,
#[serde(default)]
pub default_ui: UiConfig,
@ -353,12 +278,6 @@ pub struct GeneralConfig {
#[serde(default)]
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)]
@ -437,7 +356,7 @@ impl KeyBindings {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Mode {
#[serde(default)]
@ -621,7 +540,7 @@ impl ModesConfig {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct PanelUiConfig {
#[serde(default)]
@ -644,8 +563,8 @@ impl PanelUiConfig {
pub fn extend(mut self, other: &Self) -> Self {
self.title = self.title.extend(&other.title);
self.style = self.style.extend(&other.style);
self.borders = other.borders.clone().or(self.borders);
self.border_type = other.border_type.or(self.border_type);
self.borders = other.borders.to_owned().or(self.borders);
self.border_type = other.border_type.to_owned().or(self.border_type);
self.border_style = self.border_style.extend(&other.border_style);
self
}
@ -690,40 +609,3 @@ pub struct Config {
#[serde(default)]
pub modes: ModesConfig,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hooks {
#[serde(default)]
pub on_load: Vec<ExternalMsg>,
#[serde(default)]
pub on_directory_change: Vec<ExternalMsg>,
#[serde(default)]
pub on_focus_change: Vec<ExternalMsg>,
#[serde(default)]
pub on_mode_switch: Vec<ExternalMsg>,
#[serde(default)]
pub on_layout_switch: Vec<ExternalMsg>,
#[serde(default)]
pub on_selection_change: Vec<ExternalMsg>,
// TODO After cleanup or Runner::run
// #[serde(default)]
// pub before_quit: Vec<ExternalMsg>,
}
impl Hooks {
pub fn extend(mut self, other: Self) -> Self {
self.on_load.extend(other.on_load);
self.on_directory_change.extend(other.on_directory_change);
self.on_focus_change.extend(other.on_focus_change);
self.on_mode_switch.extend(other.on_mode_switch);
self.on_layout_switch.extend(other.on_layout_switch);
self.on_selection_change.extend(other.on_selection_change);
self
}
}

@ -1,6 +1,5 @@
use crate::node::Node;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct DirectoryBuffer {
@ -8,9 +7,6 @@ pub struct DirectoryBuffer {
pub nodes: Vec<Node>,
pub total: usize,
pub focus: usize,
#[serde(skip, default = "now")]
pub explored_at: OffsetDateTime,
}
impl DirectoryBuffer {
@ -21,7 +17,6 @@ impl DirectoryBuffer {
nodes,
total,
focus,
explored_at: now(),
}
}
@ -29,9 +24,3 @@ impl DirectoryBuffer {
self.nodes.get(self.focus)
}
}
fn now() -> OffsetDateTime {
OffsetDateTime::now_local()
.ok()
.unwrap_or_else(OffsetDateTime::now_utc)
}

@ -1,26 +0,0 @@
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()
}

@ -7,9 +7,14 @@ use std::path::PathBuf;
use std::sync::mpsc::Sender;
use std::thread;
pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result<Vec<Node>> {
let dirs = fs::read_dir(parent)?;
let nodes = dirs
pub(crate) fn explore_sync(
config: ExplorerConfig,
parent: PathBuf,
focused_path: Option<PathBuf>,
fallback_focus: usize,
) -> Result<DirectoryBuffer> {
let dirs = fs::read_dir(&parent)?;
let mut nodes = dirs
.filter_map(|d| {
d.ok().map(|e| {
e.path()
@ -19,44 +24,19 @@ pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result<Vec<Node>> {
})
})
.map(|name| Node::new(parent.to_string_lossy().to_string(), name))
.filter(|n| config.filter(n));
let mut nodes = if let Some(searcher) = config.searcher.as_ref() {
searcher.search(nodes)
} else {
nodes.collect()
};
let is_ordered_search = config
.searcher
.as_ref()
.map(|s| !s.unordered)
.unwrap_or(false);
.filter(|n| config.filter(n))
.collect::<Vec<Node>>();
if !is_ordered_search {
nodes.sort_by(|a, b| config.sort(a, b));
}
Ok(nodes)
}
nodes.sort_by(|a, b| config.sort(a, b));
pub(crate) fn explore_sync(
config: ExplorerConfig,
parent: PathBuf,
focused_path: Option<PathBuf>,
fallback_focus: usize,
) -> Result<DirectoryBuffer> {
let nodes = explore(&parent, &config)?;
let focus_index = if config.searcher.is_some() {
0
} else if let Some(focus) = focused_path {
let focus_index = if let Some(focus) = focused_path {
let focus_str = focus.to_string_lossy().to_string();
nodes
.iter()
.enumerate()
.find(|(_, n)| n.relative_path == focus_str)
.map(|(i, _)| i)
.unwrap_or_else(|| fallback_focus.min(nodes.len().saturating_sub(1)))
.unwrap_or_else(|| fallback_focus.min(nodes.len().max(1) - 1))
} else {
0
};

File diff suppressed because it is too large Load Diff

@ -18,18 +18,6 @@ pub enum Key {
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Num0,
Num1,
@ -209,13 +197,13 @@ impl std::fmt::Display for Key {
_ => c.to_string(),
})
.unwrap_or_else(|| {
serde_yaml::to_value(self)
serde_yaml::to_value(&self)
.ok()
.and_then(|v| v.as_str().map(|v| v.to_string()))
.unwrap_or_default()
});
write!(f, "{key_str}")
write!(f, "{}", key_str)
}
}
@ -225,20 +213,19 @@ impl Key {
use Key::*;
match self {
Backspace | CtrlH => Some(DeletePreviousCharacter),
Backspace => Some(DeletePreviousCharacter),
Delete => Some(DeleteNextCharacter),
Tab => Some(InsertCharacter('\t')),
Space => Some(InsertCharacter(' ')),
Left | CtrlB => Some(GoToPreviousCharacter),
Left => Some(GoToPreviousCharacter),
CtrlLeft => Some(GoToPreviousWord),
Right | CtrlF => Some(GoToNextCharacter),
Right => Some(GoToNextCharacter),
CtrlRight => Some(GoToNextWord),
CtrlU => Some(DeleteLine),
CtrlW => Some(DeletePreviousWord),
CtrlDelete => Some(DeleteNextWord),
CtrlA | Home => Some(GoToStart),
CtrlE | End => Some(GoToEnd),
CtrlK => Some(DeleteTillEnd),
CtrlA => Some(GoToStart),
CtrlE => Some(GoToEnd),
key => key.to_char().map(InsertCharacter),
}
}
@ -351,19 +338,7 @@ impl Key {
KeyCode::F(9) => Key::F9,
KeyCode::F(10) => Key::F10,
KeyCode::F(11) => Key::F11,
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::F(12) => Key::F12,
KeyCode::Backspace => Key::Backspace,
KeyCode::Left => Key::Left,
@ -647,7 +622,7 @@ impl Key {
Self::ShiftZ => Some('Z'),
Self::Space => Some(' '),
Self::Special(c) => Some(*c),
Self::Special(c) => Some(c.to_owned()),
_ => None,
}
@ -731,6 +706,12 @@ impl From<char> for Key {
}
}
impl From<String> for Key {
fn from(string: String) -> Self {
string.into()
}
}
impl From<&str> for Key {
fn from(string: &str) -> Self {
if string.len() == 1 {

@ -4,24 +4,19 @@
pub mod app;
pub mod cli;
pub mod compat;
pub mod config;
pub mod directory_buffer;
pub mod dirs;
pub mod event_reader;
pub mod explorer;
pub mod input;
pub mod lua;
pub mod msg;
pub mod node;
pub mod path;
pub mod permissions;
pub mod pipe;
pub mod pwd_watcher;
pub mod runner;
pub mod search;
pub mod ui;
pub mod yaml;
#[cfg(test)]
mod tests {

@ -1,19 +1,16 @@
use crate::app::VERSION;
use crate::config::Config;
use crate::config::Hooks;
use anyhow::bail;
use anyhow::Error;
use anyhow::Result;
use mlua::Lua;
use mlua::LuaSerdeExt;
use mlua::SerializeOptions;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Serialize;
use std::fs;
pub mod util;
const DEFAULT_LUA_SCRIPT: &str = include_str!("../init.lua");
const DEFAULT_LUA_SCRIPT: &str = include_str!("init.lua");
const UPGRADE_GUIDE_LINK: &str = "https://xplr.dev/en/upgrade-guide.html";
pub fn serialize<'lua, T: Serialize + Sized>(
@ -29,15 +26,15 @@ fn parse_version(version: &str) -> Result<(u16, u16, u16, Option<u16>)> {
let major = configv.next().unwrap_or_default().parse::<u16>()?;
let minor = configv.next().unwrap_or_default().parse::<u16>()?;
let patch = configv
let bugfix = configv
.next()
.and_then(|s| s.split('-').next())
.unwrap_or_default()
.parse::<u16>()?;
let pre = configv.next().unwrap_or_default().parse::<u16>().ok();
let beta = configv.next().unwrap_or_default().parse::<u16>().ok();
Ok((major, minor, patch, pre))
Ok((major, minor, bugfix, beta))
}
/// Check the config version and notify users.
@ -60,15 +57,12 @@ pub fn check_version(version: &str, path: &str) -> Result<()> {
}
/// Used to initialize Lua globals
pub fn init(lua: &Lua) -> Result<(Config, Option<Hooks>)> {
pub fn init(lua: &Lua) -> Result<Config> {
let config = Config::default();
let globals = lua.globals();
let util = util::create_table(lua)?;
let lua_xplr = lua.create_table()?;
lua_xplr.set("config", serialize(lua, &config)?)?;
lua_xplr.set("util", util)?;
let lua_xplr_fn = lua.create_table()?;
let lua_xplr_fn_builtin = lua.create_table()?;
@ -79,28 +73,20 @@ pub fn init(lua: &Lua) -> Result<(Config, Option<Hooks>)> {
lua_xplr.set("fn", lua_xplr_fn)?;
globals.set("xplr", lua_xplr)?;
let hooks: Option<Hooks> = lua
.load(DEFAULT_LUA_SCRIPT)
.set_name("xplr init")
.call(())
.and_then(|v| lua.from_value(v))?;
lua.load(DEFAULT_LUA_SCRIPT).set_name("init")?.exec()?;
let lua_xplr: mlua::Table = globals.get("xplr")?;
let config: Config = lua.from_value(lua_xplr.get("config")?)?;
Ok((config, hooks))
Ok(config)
}
/// Used to extend Lua globals
pub fn extend(lua: &Lua, path: &str) -> Result<(Config, Option<Hooks>)> {
pub fn extend(lua: &Lua, path: &str) -> Result<Config> {
let globals = lua.globals();
let script = fs::read_to_string(path)?;
let hooks: Option<Hooks> = lua
.load(&script)
.set_name(path)
.call(())
.and_then(|v| lua.from_value(v))?;
lua.load(&script).set_name("init")?.exec()?;
let version: String = match globals.get("version").and_then(|v| lua.from_value(v)) {
Ok(v) => v,
@ -112,7 +98,7 @@ pub fn extend(lua: &Lua, path: &str) -> Result<(Config, Option<Hooks>)> {
let lua_xplr: mlua::Table = globals.get("xplr")?;
let config: Config = lua.from_value(lua_xplr.get("config")?)?;
Ok((config, hooks))
Ok(config)
}
fn resolve_fn_recursive<'lua, 'a>(
@ -138,12 +124,12 @@ pub fn resolve_fn<'lua>(
resolve_fn_recursive(globals, path.split('.'))
}
pub fn call<'lua, R: DeserializeOwned>(
pub fn call<'lua, R: Deserialize<'lua>>(
lua: &'lua Lua,
func: &str,
arg: mlua::Value<'lua>,
) -> Result<R> {
let func = format!("xplr.fn.{func}");
let func = format!("xplr.fn.{}", func);
let func = resolve_fn(&lua.globals(), &func)?;
let res: mlua::Value = func.call(arg)?;
let res: R = lua.from_value(res)?;
@ -160,24 +146,24 @@ mod tests {
assert!(check_version(VERSION, "foo path").is_ok());
// Current release if OK
assert!(check_version("0.21.9", "foo path").is_ok());
assert!(check_version("0.19.1", "foo path").is_ok());
// Prev major release is ERR
// - Not yet
// Prev minor release is ERR (Change when we get to v1)
assert!(check_version("0.20.9", "foo path").is_err());
assert!(check_version("0.18.1", "foo path").is_err());
// Prev bugfix release is OK
assert!(check_version("0.21.8", "foo path").is_ok());
assert!(check_version("0.19.0", "foo path").is_ok());
// Next major release is ERR
assert!(check_version("1.20.9", "foo path").is_err());
assert!(check_version("1.19.1", "foo path").is_err());
// Next minor release is ERR
assert!(check_version("0.22.9", "foo path").is_err());
assert!(check_version("0.20.1", "foo path").is_err());
// Next bugfix release is ERR (Change when we get to v1)
assert!(check_version("0.21.10", "foo path").is_err());
assert!(check_version("0.19.2", "foo path").is_err());
}
}

@ -1,904 +0,0 @@
use crate::app::VERSION;
use crate::config::NodeTypesConfig;
use crate::explorer;
use crate::lua;
use crate::msg::in_::external::ExplorerConfig;
use crate::node::Node;
use crate::path;
use crate::path::RelativityConfig;
use crate::permissions::Octal;
use crate::permissions::Permissions;
use crate::ui;
use crate::ui::Layout;
use crate::ui::Style;
use crate::ui::WrapOptions;
use anyhow::Result;
use lazy_static::lazy_static;
use lscolors::LsColors;
use mlua::Error as LuaError;
use mlua::Lua;
use mlua::LuaSerdeExt;
use mlua::Table;
use mlua::Value;
use path_absolutize::*;
use serde::de::Error;
use serde::{Deserialize, Serialize};
use serde_json as json;
use serde_yaml as yaml;
use std::borrow::Cow;
use std::path::PathBuf;
use std::process::Command;
lazy_static! {
static ref LS_COLORS: LsColors = LsColors::from_env().unwrap_or_default();
}
/// Get the xplr version details.
///
/// Type: function() -> { major: number, minor: number, patch: number }
///
/// Example:
///
/// ```lua
/// xplr.util.version()
/// -- { major = 0, minor = 0, patch = 0 }
/// ```
pub fn version<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
#[derive(Debug, Default, Serialize, Deserialize)]
struct Version {
major: u16,
minor: u16,
patch: u16,
}
let func = lua.create_function(|lua, ()| {
let (major, minor, patch, _) =
lua::parse_version(VERSION).map_err(LuaError::custom)?;
let version = Version {
major,
minor,
patch,
};
let res = lua::serialize(lua, &version).map_err(LuaError::custom)?;
Ok(res)
})?;
util.set("version", func)?;
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.
///
/// Type: function( value ) -> value
///
/// Example:
///
/// ```lua
/// local val = { foo = "bar" }
/// local val_clone = xplr.util.clone(val)
/// val.foo = "baz"
/// print(val_clone.foo)
/// -- "bar"
/// ```
pub fn clone<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(move |lua, value: Value| {
lua::serialize(lua, &value).map_err(LuaError::custom)
})?;
util.set("clone", func)?;
Ok(util)
}
/// Check if the given path exists.
///
/// Type: function( path:string ) -> boolean
///
/// Example:
///
/// ```lua
/// xplr.util.exists("/foo/bar")
/// -- true
/// ```
pub fn exists<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(move |_, path: String| Ok(PathBuf::from(path).exists()))?;
util.set("exists", func)?;
Ok(util)
}
/// Check if the given path is a directory.
///
/// Type: function( path:string ) -> boolean
///
/// Example:
///
/// ```lua
/// xplr.util.is_dir("/foo/bar")
/// -- true
/// ```
pub fn is_dir<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(move |_, path: String| Ok(PathBuf::from(path).is_dir()))?;
util.set("is_dir", func)?;
Ok(util)
}
/// Check if the given path is a file.
///
/// Type: function( path:string ) -> boolean
///
/// Example:
///
/// ```lua
/// xplr.util.is_file("/foo/bar")
/// -- true
/// ```
pub fn is_file<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(move |_, path: String| Ok(PathBuf::from(path).is_file()))?;
util.set("is_file", func)?;
Ok(util)
}
/// Check if the given path is a symlink.
///
/// Type: function( path:string ) -> boolean
///
/// Example:
///
/// ```lua
/// xplr.util.is_file("/foo/bar")
/// -- true
/// ```
pub fn is_symlink<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua
.create_function(move |_, path: String| Ok(PathBuf::from(path).is_symlink()))?;
util.set("is_symlink", func)?;
Ok(util)
}
/// Check if the given path is an absolute path.
///
/// Type: function( path:string ) -> boolean
///
/// Example:
///
/// ```lua
/// xplr.util.is_absolute("/foo/bar")
/// -- true
/// ```
pub fn is_absolute<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua
.create_function(move |_, path: String| Ok(PathBuf::from(path).is_absolute()))?;
util.set("is_absolute", func)?;
Ok(util)
}
/// Split a path into its components.
///
/// Type: function( path:string ) -> boolean
///
/// Example:
///
/// ```lua
/// xplr.util.path_split("/foo/bar")
/// -- { "/", "foo", "bar" }
///
/// xplr.util.path_split(".././foo")
/// -- { "..", "foo" }
/// ```
pub fn path_split<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(move |_, path: String| {
let components: Vec<String> = PathBuf::from(path)
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect();
Ok(components)
})?;
util.set("path_split", func)?;
Ok(util)
}
/// Get [Node][5] information of a given path.
/// Doesn't check if the path exists.
/// Returns nil if the path is "/".
/// Errors out if absolute path can't be obtained.
///
/// Type: function( path:string ) -> [Node][5]|nil
///
/// Example:
///
/// ```lua
/// xplr.util.node("./bar")
/// -- { parent = "/pwd", relative_path = "bar", absolute_path = "/pwd/bar", ... }
///
/// xplr.util.node("/")
/// -- nil
/// ```
pub fn node<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(move |lua, path: String| {
let path = PathBuf::from(path);
let abs = path.absolutize()?;
match (abs.parent(), abs.file_name()) {
(Some(parent), Some(name)) => {
let node = Node::new(
parent.to_string_lossy().to_string(),
name.to_string_lossy().to_string(),
);
Ok(lua::serialize(lua, &node).map_err(LuaError::custom)?)
}
(_, _) => Ok(Value::Nil),
}
})?;
util.set("node", func)?;
Ok(util)
}
/// Get the configured [Node Type][6] of a given [Node][5].
///
/// Type: function( [Node][5], [xplr.config.node_types][7]|nil ) -> [Node Type][6]
///
/// If the second argument is missing, global config `xplr.config.node_types`
/// will be used.
///
/// Example:
///
/// ```lua
/// xplr.util.node_type(app.focused_node)
/// -- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... }
///
/// xplr.util.node_type(xplr.util.node("/foo/bar"), xplr.config.node_types)
/// -- { style = { fg = "Red", ... }, meta = { icon = "", ... } ... }
/// ```
pub fn node_type<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(move |lua, (node, config): (Table, Option<Table>)| {
let node: Node = lua.from_value(Value::Table(node))?;
let config: Table = if let Some(config) = config {
config
} else {
lua.globals()
.get::<_, Table>("xplr")?
.get::<_, Table>("config")?
.get::<_, Table>("node_types")?
};
let config: NodeTypesConfig = lua.from_value(Value::Table(config))?;
let node_type = config.get(&node);
let node_type = lua::serialize(lua, &node_type).map_err(LuaError::custom)?;
Ok(node_type)
})?;
util.set("node_type", func)?;
Ok(util)
}
/// Get the directory name of a given path.
///
/// Type: function( path:string ) -> path:string|nil
///
/// Example:
///
/// ```lua
/// xplr.util.dirname("/foo/bar")
/// -- "/foo"
/// ```
pub fn dirname<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|_, path: String| {
let parent = PathBuf::from(path)
.parent()
.map(|p| p.to_string_lossy().to_string());
Ok(parent)
})?;
util.set("dirname", func)?;
Ok(util)
}
/// Get the base name of a given path.
///
/// Type: function( path:string ) -> path:string|nil
///
/// Example:
///
/// ```lua
/// xplr.util.basename("/foo/bar")
/// -- "bar"
/// ```
pub fn basename<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|_, path: String| {
let parent = PathBuf::from(path)
.file_name()
.map(|p| p.to_string_lossy().to_string());
Ok(parent)
})?;
util.set("basename", func)?;
Ok(util)
}
/// Get the absolute path of the given path by prepending $PWD.
/// It doesn't check if the path exists.
///
/// Type: function( path:string ) -> path:string
///
/// Example:
///
/// ```lua
/// xplr.util.absolute("foo/bar")
/// -- "/tmp/foo/bar"
/// ```
pub fn absolute<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|_, path: String| {
let abs = PathBuf::from(path)
.absolutize()?
.to_string_lossy()
.to_string();
Ok(abs)
})?;
util.set("absolute", func)?;
Ok(util)
}
/// Get the relative path based on the given base path or current working dir.
/// Will error if it fails to determine a relative path.
///
/// Type: function( path:string, options:table|nil ) -> path:string
///
/// Options type: { base:string|nil, with_prefix_dots:bookean|nil, without_suffix_dots:boolean|nil }
///
/// - If `base` path is given, the path will be relative to it.
/// - If `with_prefix_dots` is true, the path will always start with dots `..` / `.`
/// - If `without_suffix_dots` is true, the name will be visible instead of dots `..` / `.`
///
/// Example:
///
/// ```lua
/// xplr.util.relative_to("/present/working/directory")
/// -- "."
///
/// xplr.util.relative_to("/present/working/directory/foo")
/// -- "foo"
///
/// xplr.util.relative_to("/present/working/directory/foo", { with_prefix_dots = true })
/// -- "./foo"
///
/// xplr.util.relative_to("/present/working/directory", { without_suffix_dots = true })
/// -- "../directory"
///
/// xplr.util.relative_to("/present/working")
/// -- ".."
///
/// xplr.util.relative_to("/present/working", { without_suffix_dots = true })
/// -- "../../working"
///
/// xplr.util.relative_to("/present/working/directory", { base = "/present/foo/bar" })
/// -- "../../working/directory"
/// ```
pub fn relative_to<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, (path, config): (String, Option<Table>)| {
let config: Option<RelativityConfig<String>> =
lua.from_value(config.map(Value::Table).unwrap_or(Value::Nil))?;
path::relative_to(path, config.as_ref())
.map(|p| p.to_string_lossy().to_string())
.map_err(LuaError::custom)
})?;
util.set("relative_to", func)?;
Ok(util)
}
/// Shorten the given absolute path using the following rules:
/// - either relative to your home dir if it makes sense
/// - or relative to the current working directory
/// - or absolute path if it makes the most sense
///
/// Type: Similar to `xplr.util.relative_to`
///
/// Example:
///
/// ```lua
/// xplr.util.shorten("/home/username/.config")
/// -- "~/.config"
///
/// xplr.util.shorten("/present/working/directory")
/// -- "."
///
/// xplr.util.shorten("/present/working/directory/foo")
/// -- "foo"
///
/// xplr.util.shorten("/present/working/directory/foo", { with_prefix_dots = true })
/// -- "./foo"
///
/// xplr.util.shorten("/present/working/directory", { without_suffix_dots = true })
/// -- "../directory"
///
/// xplr.util.shorten("/present/working/directory", { base = "/present/foo/bar" })
/// -- "../../working/directory"
///
/// xplr.util.shorten("/tmp")
/// -- "/tmp"
/// ```
pub fn shorten<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(move |lua, (path, config): (String, Option<Table>)| {
let config: Option<RelativityConfig<String>> =
lua.from_value(config.map(Value::Table).unwrap_or(Value::Nil))?;
path::shorten(path, config.as_ref()).map_err(LuaError::custom)
})?;
util.set("shorten", func)?;
Ok(util)
}
/// Explore directories with the given explorer config.
///
/// Type: function( path:string, [ExplorerConfig][1]|nil ) -> { [Node][2], ... }
///
/// Example:
///
/// ```lua
///
/// xplr.util.explore("/tmp")
/// -- { { absolute_path = "/tmp/a", ... }, ... }
///
/// xplr.util.explore("/tmp", app.explorer_config)
/// -- { { absolute_path = "/tmp/a", ... }, ... }
/// ```
pub fn explore<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, (path, config): (String, Option<Table>)| {
let config: ExplorerConfig = if let Some(cfg) = config {
lua.from_value(Value::Table(cfg))?
} else {
ExplorerConfig::default()
};
let nodes = explorer::explore(&PathBuf::from(path), &config)
.map_err(LuaError::custom)?;
let res = lua::serialize(lua, &nodes).map_err(LuaError::custom)?;
Ok(res)
})?;
util.set("explore", func)?;
Ok(util)
}
/// Execute shell commands safely.
///
/// Type: function( program:string, args:{ string, ... }|nil ) -> { stdout = string, stderr = string, returncode = number|nil }
///
/// Example:
///
/// ```lua
/// xplr.util.shell_execute("pwd")
/// -- { stdout = "/present/working/directory", stderr = "", returncode = 0 }
///
/// xplr.util.shell_execute("bash", {"-c", "xplr --help"})
/// -- { stdout = "xplr...", stderr = "", returncode = 0 }
/// ```
pub fn shell_execute<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(|lua, (program, args): (String, Option<Vec<String>>)| {
let mut cmd = Command::new(program);
let mut cmd_ref = &mut cmd;
if let Some(args) = args {
cmd_ref = cmd_ref.args(args)
};
let output = cmd_ref.output()?;
let res = lua.create_table()?;
res.set("stdout", String::from_utf8_lossy(&output.stdout))?;
res.set("stderr", String::from_utf8_lossy(&output.stderr))?;
res.set("returncode", output.status.code())?;
Ok(res)
})?;
util.set("shell_execute", func)?;
Ok(util)
}
/// Quote commands and paths safely.
///
/// Type: function( string ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.shell_quote("a'b\"c")
/// -- 'a'"'"'b"c'
/// ```
pub fn shell_quote<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|_, string: String| {
Ok(format!("'{}'", string.replace('\'', r#"'"'"'"#)))
})?;
util.set("shell_quote", func)?;
Ok(util)
}
/// Escape commands and paths safely.
///
/// Type: function( string ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.shell_escape("a'b\"c")
/// -- "\"a'b\\\"c\""
/// ```
pub fn shell_escape<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(move |_, string: String| {
let val = path::escape(&string).to_string();
Ok(val)
})?;
util.set("shell_escape", func)?;
Ok(util)
}
/// Load JSON string into Lua value.
///
/// Type: function( string ) -> any
///
/// Example:
///
/// ```lua
/// xplr.util.from_json([[{"foo": "bar"}]])
/// -- { foo = "bar" }
/// ```
pub fn from_json<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, string: String| {
let val = json::from_str::<yaml::Value>(&string).map_err(LuaError::custom)?;
lua::serialize(lua, &val).map_err(Error::custom)
})?;
util.set("from_json", func)?;
Ok(util)
}
/// Dump Lua value into JSON (i.e. also YAML) string.
///
/// Type: function( value ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.to_json({ foo = "bar" })
/// -- [[{ "foo": "bar" }]]
///
/// xplr.util.to_json({ foo = "bar" }, { pretty = true })
/// -- [[{
/// -- "foo": "bar"
/// -- }]]
/// ```
pub fn to_json<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
#[derive(Debug, Default, Serialize, Deserialize)]
struct Options {
pretty: bool,
}
let func =
lua.create_function(|lua, (value, options): (Value, Option<Table>)| {
let options: Options = if let Some(o) = options {
lua.from_value(Value::Table(o))?
} else {
Default::default()
};
if options.pretty {
json::to_string_pretty(&value).map_err(Error::custom)
} else {
json::to_string(&value).map_err(Error::custom)
}
})?;
util.set("to_json", func)?;
Ok(util)
}
/// Load YAML (i.e. also JSON) string into Lua value.
///
/// Type: function( string ) -> value
///
/// Example:
///
/// ```lua
/// xplr.util.from_yaml([[{foo: bar}]])
/// -- { foo = "bar" }
/// ```
pub fn from_yaml<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, string: String| {
let val = yaml::from_str::<yaml::Value>(&string).map_err(LuaError::custom)?;
lua::serialize(lua, &val).map_err(Error::custom)
})?;
util.set("from_yaml", func)?;
Ok(util)
}
/// Dump Lua value into YAML string.
///
/// Type: function( value ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.to_yaml({ foo = "bar" })
/// -- "foo: bar"
/// ```
pub fn to_yaml<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Options {
pretty: bool,
}
let func = lua.create_function(|_, value: Value| {
yaml::to_string(&value).map_err(Error::custom)
})?;
util.set("to_yaml", func)?;
Ok(util)
}
/// Get a [Style][3] object for the given path based on the LS_COLORS
/// environment variable.
///
/// Type: function( path:string ) -> [Style][3]
///
/// Example:
///
/// ```lua
/// xplr.util.lscolor("Desktop")
/// -- { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} }
/// ```
pub fn lscolor<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(move |lua, path: String| {
let style = LS_COLORS
.style_for_path(path)
.map(Style::from)
.unwrap_or_default();
lua::serialize(lua, &style).map_err(LuaError::custom)
})?;
util.set("lscolor", func)?;
Ok(util)
}
/// Apply style (escape sequence) to string using a given [Style][3] object.
///
/// Type: function( string, [Style][3]|nil ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.paint("Desktop", { fg = "Red", bg = nil, add_modifiers = {}, sub_modifiers = {} })
/// -- "\u001b[31mDesktop\u001b[0m"
/// ```
pub fn paint<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func =
lua.create_function(|lua, (string, style): (String, Option<Table>)| {
if *ui::NO_COLOR {
return Ok(string);
}
if let Some(style) = style {
let style: Style = lua.from_value(Value::Table(style))?;
let ansi_style: nu_ansi_term::Style = style.into();
Ok::<String, LuaError>(ansi_style.paint(string).to_string())
} else {
Ok(string)
}
})?;
util.set("paint", func)?;
Ok(util)
}
/// Mix multiple [Style][3] objects into one.
///
/// Type: function( { [Style][3], [Style][3], ... } ) -> [Style][3]
///
/// Example:
///
/// ```lua
/// xplr.util.style_mix({{ fg = "Red" }, { bg = "Blue" }, { add_modifiers = {"Bold"} }})
/// -- { fg = "Red", bg = "Blue", add_modifiers = { "Bold" }, sub_modifiers = {} }
/// ```
pub fn style_mix<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, styles: Vec<Table>| {
let mut style = Style::default();
for other in styles {
let other: Style = lua.from_value(Value::Table(other))?;
style = style.extend(&other);
}
lua::serialize(lua, &style).map_err(LuaError::custom)
})?;
util.set("style_mix", func)?;
Ok(util)
}
/// Wrap the given text to fit the specified width.
/// It will try to not split words when possible.
///
/// Type: function( string, options:number|table ) -> { string, ...}
///
/// Options type: { width = number, initial_indent = string|nil, subsequent_indent = string|nil, break_words = boolean|nil }
///
/// Example:
///
/// ```lua
/// xplr.util.textwrap("this will be cut off", 11)
/// -- { "this will', 'be cut off" }
///
/// xplr.util.textwrap(
/// "this will be cut off",
/// { width = 12, initial_indent = "", subsequent_indent = " ", break_words = false }
/// )
/// -- { "this will be", " cut off" }
/// ```
pub fn textwrap<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, (text, options): (String, Value)| {
let lines = match lua.from_value::<usize>(options.clone()) {
Ok(width) => textwrap::wrap(&text, width),
Err(_) => {
let options = lua.from_value::<WrapOptions>(options)?;
textwrap::wrap(&text, options.get_options())
}
};
Ok(lines.iter().map(Cow::to_string).collect::<Vec<String>>())
})?;
util.set("textwrap", func)?;
Ok(util)
}
/// Find the target layout in the given layout and replace it with the replacement layout,
/// returning a new layout.
///
/// Type: function( layout:[Layout][4], target:[Layout][4], replacement:[Layout][4] ) -> layout:[Layout][4]
///
/// Example:
///
/// ```lua
/// local layout = {
/// Horizontal = {
/// splits = {
/// "Table", -- Target
/// "HelpMenu",
/// },
/// config = ...,
/// }
/// }
///
/// xplr.util.layout_replace(layout, "Table", "Selection")
/// -- {
/// -- Horizontal = {
/// -- splits = {
/// -- "Selection", -- Replacement
/// -- "HelpMenu",
/// -- },
/// -- config = ...
/// -- }
/// -- }
/// ```
pub fn layout_replace<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(
move |lua, (layout, target, replacement): (Value, Value, Value)| {
let layout: Layout = lua.from_value(layout)?;
let target: Layout = lua.from_value(target)?;
let replacement: Layout = lua.from_value(replacement)?;
let res = layout.replace(&target, &replacement);
let res = lua::serialize(lua, &res).map_err(LuaError::custom)?;
Ok(res)
},
)?;
util.set("layout_replace", func)?;
Ok(util)
}
/// Convert [Permission][8] to rwxrwxrwx representation with special bits.
///
/// Type: function( [Permission][8] ) -> string
///
/// Example:
///
/// ```lua
/// xplr.util.permissions_rwx({ user_read = true })
/// -- "r--------"
///
/// xplr.util.permissions_rwx(app.focused_node.permission)
/// -- "rwxrwsrwT"
/// ```
pub fn permissions_rwx<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, permission: Table| {
let permissions: Permissions = lua.from_value(Value::Table(permission))?;
let permissions = permissions.to_string();
Ok(permissions)
})?;
util.set("permissions_rwx", func)?;
Ok(util)
}
/// Convert [Permission][8] to octal representation.
///
/// Type: function( [Permission][8] ) -> { number, number, number, number }
///
/// Example:
///
/// ```lua
/// xplr.util.permissions_octal({ user_read = true })
/// -- { 0, 4, 0, 0 }
///
/// xplr.util.permissions_octal(app.focused_node.permission)
/// -- { 0, 7, 5, 4 }
/// ```
pub fn permissions_octal<'a>(util: Table<'a>, lua: &Lua) -> Result<Table<'a>> {
let func = lua.create_function(|lua, permission: Table| {
let permissions: Permissions = lua.from_value(Value::Table(permission))?;
let permissions: Octal = permissions.into();
let permissions = lua::serialize(lua, &permissions).map_err(LuaError::custom)?;
Ok(permissions)
})?;
util.set("permissions_octal", func)?;
Ok(util)
}
///
/// [1]: https://xplr.dev/en/lua-function-calls#explorer-config
/// [2]: https://xplr.dev/en/lua-function-calls#node
/// [3]: https://xplr.dev/en/style
/// [4]: https://xplr.dev/en/layout
/// [5]: https://xplr.dev/en/lua-function-calls#node
/// [6]: https://xplr.dev/en/node-type
/// [7]: https://xplr.dev/en/node_types
/// [8]: https://xplr.dev/en/column-renderer#permission
pub(crate) fn create_table(lua: &Lua) -> Result<Table> {
let mut util = lua.create_table()?;
util = version(util, lua)?;
util = debug(util, lua)?;
util = clone(util, lua)?;
util = exists(util, lua)?;
util = is_dir(util, lua)?;
util = is_file(util, lua)?;
util = is_symlink(util, lua)?;
util = is_absolute(util, lua)?;
util = path_split(util, lua)?;
util = node(util, lua)?;
util = node_type(util, lua)?;
util = dirname(util, lua)?;
util = basename(util, lua)?;
util = absolute(util, lua)?;
util = relative_to(util, lua)?;
util = shorten(util, lua)?;
util = explore(util, lua)?;
util = shell_execute(util, lua)?;
util = shell_quote(util, lua)?;
util = shell_escape(util, lua)?;
util = from_json(util, lua)?;
util = to_json(util, lua)?;
util = from_yaml(util, lua)?;
util = to_yaml(util, lua)?;
util = lscolor(util, lua)?;
util = paint(util, lua)?;
util = style_mix(util, lua)?;
util = textwrap(util, lua)?;
util = layout_replace(util, lua)?;
util = permissions_rwx(util, lua)?;
util = permissions_octal(util, lua)?;
Ok(util)
}

@ -1,11 +1,8 @@
use crate::app::Node;
use crate::input::InputOperation;
use crate::search::PathItem;
use crate::search::SearchAlgorithm;
use crate::{app::Node, input::InputOperation};
use indexmap::IndexSet;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, sync::Arc};
use std::cmp::Ordering;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum ExternalMsg {
@ -77,14 +74,6 @@ pub enum ExternalMsg {
/// - YAML: `FocusNext`
FocusNext,
/// Focus on the next selected node.
///
/// Example:
///
/// - Lua: `"FocusNextSelection"`
/// - YAML: `FocusNextSelection`
FocusNextSelection,
/// Focus on the `n`th node relative to the current focus where `n` is a
/// given value.
///
@ -113,14 +102,6 @@ pub enum ExternalMsg {
/// - YAML: `FocusPrevious`
FocusPrevious,
/// Focus on the previous selection item.
///
/// Example:
///
/// - Lua: `"FocusPreviousSelection"`
/// - YAML: `FocusPreviousSelection`
FocusPreviousSelection,
/// Focus on the `-n`th node relative to the current focus where `n` is a
/// given value.
///
@ -193,6 +174,9 @@ pub enum ExternalMsg {
/// - YAML: `FocusByIndexFromInput`
FocusByIndexFromInput,
///
/// **YAML:** `FocusByFileName: string`
///
/// Focus on the file by name from the present working directory.
///
/// Type: { FocusByFileName = "string" }
@ -203,38 +187,6 @@ pub enum ExternalMsg {
/// - YAML: `FocusByFileName: filename.ext`
FocusByFileName(String),
/// Scroll up by terminal height.
///
/// Example:
///
/// - Lua: `"ScrollUp"`
/// - YAML: `ScrollUp`
ScrollUp,
/// Scroll down by terminal height.
///
/// Example:
///
/// - Lua: `"ScrollDown"`
/// - YAML: `ScrollDown`
ScrollDown,
/// Scroll up by half of terminal height.
///
/// Example:
///
/// - Lua: `"ScrollUpHalf"`
/// - YAML: `ScrollUpHalf`
ScrollUpHalf,
/// Scroll down by half of terminal height.
///
/// Example:
///
/// - Lua: `"ScrollDownHalf"`
/// - YAML: `ScrollDownHalf`
ScrollDownHalf,
/// Change the present working directory ($PWD)
///
/// Type: { ChangeDirectory = "string" }
@ -277,69 +229,15 @@ pub enum ExternalMsg {
/// - YAML: `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.
///
/// Example:
///
/// - Lua: `"FollowSymlink"`
/// - YAML: `FollowSymlink`
/// Lua: `"FollowSymlink"`
/// YAML: `FollowSymlink`
FollowSymlink,
/// ### Virtual Root ------------------------------------------------------
/// Sets the virtual root for isolating xplr navigation, similar to
/// `--vroot`, but temporary (can be reset back to initial value).
/// If the $PWD is outside the vroot, xplr will automatically enter vroot.
///
/// Type: { SetVroot = "string" }
///
/// Example:
///
/// - Lua: `{ SetVroot = "/tmp" }`
/// - YAML: `SetVroot: /tmp`
SetVroot(String),
/// Unset the virtual root temporarily (can be reset back to the initial
/// value).
///
/// Example:
///
/// - Lua: `"UnsetVroot"`
/// - YAML: `UnsetVroot`
UnsetVroot,
/// Toggle virtual root between unset, initial value and $PWD.
///
/// Example:
///
/// - Lua: `"ToggleVroot"`
/// - YAML: `ToggleVroot`
ToggleVroot,
/// Reset the virtual root back to the initial value.
///
/// Example:
///
/// - Lua: `"ResetVroot"`
/// - YAML: `ResetVroot`
ResetVroot,
/// ### Reading Input -----------------------------------------------------
/// Set the input prompt temporarily, until the input buffer is reset.
@ -354,7 +252,7 @@ pub enum ExternalMsg {
/// Update the input buffer using cursor based operations.
///
/// Type: { UpdateInputBuffer = [Input Operation](https://xplr.dev/en/input-operation) }
/// Type: { UpdateInputBuffer = [Input Opertaion](https://xplr.dev/en/input-operation) }
///
/// Example:
///
@ -380,7 +278,7 @@ pub enum ExternalMsg {
/// - YAML: `BufferInput: foo`
BufferInput(String),
/// Append/buffer the character read from a keyboard input into the
/// Append/buffer the characted read from a keyboard input into the
/// input buffer.
///
/// Example:
@ -437,7 +335,8 @@ pub enum ExternalMsg {
/// - YAML: SwitchMode: default
///
/// > **NOTE:** To be specific about which mode to switch to, use
/// > `SwitchModeBuiltin` or `SwitchModeCustom` instead.
/// > `SwitchModeBuiltinKeepingInputBuffer` or
/// > `SwitchModeCustomKeepingInputBuffer` instead.
SwitchMode(String),
/// Switch input [mode](https://xplr.dev/en/modes).
@ -554,72 +453,52 @@ pub enum ExternalMsg {
/// ### Executing Commands ------------------------------------------------
/// Like `Call0`, but it uses `\n` as the delimiter in input/output pipes,
/// hence it cannot handle files with `\n` in the name.
/// You may want to use `Call0` instead.
Call(Command),
/// Call a shell command with the given arguments.
/// Note that the arguments will be shell-escaped.
/// So to read the variables, the `-c` option of the shell
/// can be used.
/// You may need to pass `ExplorePwd` depending on the expectation.
/// You may need to pass `ExplorePwd` depening on the expectation.
///
/// Type: { Call0 = { command = "string", args = { "list", "of", "string" } }
/// Type: { Call = { command = string, args = { "list", "of", "string" } }
///
/// Example:
///
/// - Lua: `{ Call0 = { command = "bash", args = { "-c", "read -p test" } } }`
/// - YAML: `Call0: { command: bash, args: ["-c", "read -p test"] }`
Call0(Command),
/// Like `CallSilently0`, but it uses `\n` as the delimiter in input/output
/// pipes, hence it cannot handle files with `\n` in the name.
/// You may want to use `CallSilently0` instead.
CallSilently(Command),
/// - Lua: `{ Call = { command = "bash", args = { "-c", "read -p test" } } }`
/// - YAML: `Call: { command: bash, args: ["-c", "read -p test"] }`
Call(Command),
/// Like `Call0` but without the flicker. The stdin, stdout
/// Like `Call` but without the flicker. The stdin, stdout
/// stderr will be piped to null. So it's non-interactive.
///
/// Type: { CallSilently0 = { command = "string", args = {"list", "of", "string"} } }
/// Type: { CallSilently = "string" }
///
/// Example:
///
/// - Lua: `{ CallSilently0 = { command = "tput", args = { "bell" } } }`
/// - YAML: `CallSilently0: { command: tput, args: ["bell"] }`
CallSilently0(Command),
/// Like `BashExec0`, but it uses `\n` as the delimiter in input/output
/// pipes, hence it cannot handle files with `\n` in the name.
/// You may want to use `BashExec0` instead.
BashExec(String),
/// - Lua: `{ CallSilently = { command = "tput", args = { "bell" } } }`
/// - YAML: `CallSilently: { command: tput, args: ["bell"] }`
CallSilently(Command),
/// An alias to `Call: {command: bash, args: ["-c", "{string}"], silent: false}`
/// where `{string}` is the given value.
///
/// Type: { BashExec0 = "string" }
/// Type: { BashExec = "string" }
///
/// Example:
///
/// - Lua: `{ BashExec0 = "read -p test" }`
/// - YAML: `BashExec0: "read -p test"`
BashExec0(String),
/// Like `BashExecSilently0`, but it uses `\n` as the delimiter in
/// input/output pipes, hence it cannot handle files with `\n` in the name.
/// You may want to use `BashExecSilently0` instead.
BashExecSilently(String),
/// - Lua: `{ BashExec = "read -p test" }`
/// - YAML: `BashExec: "read -p test"`
BashExec(String),
/// Like `BashExec0` but without the flicker. The stdin, stdout
/// Like `BashExec` but without the flicker. The stdin, stdout
/// stderr will be piped to null. So it's non-interactive.
///
/// Type: { BashExecSilently0 = "string" }
/// Type: { BashExecSilently = "string" }
///
/// Example:
///
/// - Lua: `{ BashExecSilently0 = "tput bell" }`
/// - YAML: `BashExecSilently0: "tput bell"`
BashExecSilently0(String),
/// - Lua: `{ BashExecSilently = "tput bell" }`
/// - YAML: `BashExecSilently: "tput bell"`
BashExecSilently(String),
/// ### Calling Lua Functions ----------------------------------------------
@ -767,8 +646,6 @@ pub enum ExternalMsg {
/// Add a [filter](https://xplr.dev/en/filtering#filter) to exclude nodes
/// while exploring directories.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// Filters get automatically cleared when changing directories.
///
/// Type: { AddNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering#filter), input = "string" }
///
@ -779,7 +656,6 @@ pub enum ExternalMsg {
AddNodeFilter(NodeFilterApplicable),
/// Remove an existing [filter](https://xplr.dev/en/filtering#filter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { RemoveNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
///
@ -791,7 +667,6 @@ pub enum ExternalMsg {
/// Remove a [filter](https://xplr.dev/en/filtering#filter) if it exists,
/// else, add a it.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { ToggleNodeFilter = { filter = [Filter](https://xplr.dev/en/filtering), input = "string" }
///
@ -803,7 +678,6 @@ pub enum ExternalMsg {
/// Add a node [filter](https://xplr.dev/en/filtering#filter) reading the
/// input from the buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { AddNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
///
@ -815,7 +689,6 @@ pub enum ExternalMsg {
/// Remove a node [filter](https://xplr.dev/en/filtering#filter) reading
/// the input from the buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { RemoveNodeFilterFromInput = [Filter](https://xplr.dev/en/filtering) }
///
@ -826,7 +699,6 @@ pub enum ExternalMsg {
RemoveNodeFilterFromInput(NodeFilter),
/// Remove the last node [filter](https://xplr.dev/en/filtering).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -836,7 +708,6 @@ pub enum ExternalMsg {
/// Reset the node [filters](https://xplr.dev/en/filtering) back to the
/// default configuration.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -845,7 +716,6 @@ pub enum ExternalMsg {
ResetNodeFilters,
/// Clear all the node [filters](https://xplr.dev/en/filtering).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -857,7 +727,6 @@ pub enum ExternalMsg {
/// Add a [sorter](https://xplr.dev/en/sorting#sorter) to sort nodes while
/// exploring directories.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { AddNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
///
@ -868,7 +737,6 @@ pub enum ExternalMsg {
AddNodeSorter(NodeSorterApplicable),
/// Remove an existing [sorter](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { RemoveNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
///
@ -879,7 +747,6 @@ pub enum ExternalMsg {
RemoveNodeSorter(NodeSorter),
/// Reverse a node [sorter](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { ReverseNodeSorter = [Sorter](https://xplr.dev/en/sorting#sorter) }
///
@ -891,7 +758,6 @@ pub enum ExternalMsg {
/// Remove a [sorter](https://xplr.dev/en/sorting#sorter) if it exists,
/// else, add a it.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Type: { ToggleNodeSorter = { sorter = [Sorter](https://xplr.dev/en/sorting#sorter), reverse = bool } }
///
@ -902,7 +768,6 @@ pub enum ExternalMsg {
ToggleNodeSorter(NodeSorterApplicable),
/// Reverse the node [sorters](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -911,7 +776,6 @@ pub enum ExternalMsg {
ReverseNodeSorters,
/// Remove the last node [sorter](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -921,7 +785,6 @@ pub enum ExternalMsg {
/// Reset the node [sorters](https://xplr.dev/en/sorting#sorter) back to
/// the default configuration.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -930,7 +793,6 @@ pub enum ExternalMsg {
ResetNodeSorters,
/// Clear all the node [sorters](https://xplr.dev/en/sorting#sorter).
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
@ -938,173 +800,6 @@ pub enum ExternalMsg {
/// - YAML: `ClearNodeSorters`
ClearNodeSorters,
/// ### Search Operations --------------------------------------------------
/// Search files using the current or default (fuzzy) search algorithm.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Type: { Search = "string" }
///
/// Example:
///
/// - Lua: `{ Search = "pattern" }`
/// - YAML: `Search: pattern`
Search(String),
/// Calls `Search` with the input taken from the input buffer.
///
/// Example:
///
/// - Lua: `"SearchFromInput"`
/// - YAML: `SearchFromInput`
SearchFromInput,
/// Search files using fuzzy match algorithm.
/// It keeps the filters, but overrides the sorters.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Type: { SearchFuzzy = "string" }
///
/// Example:
///
/// - Lua: `{ SearchFuzzy = "pattern" }`
/// - YAML: `SearchFuzzy: pattern`
SearchFuzzy(String),
/// Calls `SearchFuzzy` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Example:
///
/// - Lua: `"SearchFuzzyFromInput"`
/// - YAML: `SearchFuzzyFromInput`
SearchFuzzyFromInput,
/// Like `SearchFuzzy`, but doesn't not perform rank based sorting.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Type: { SearchFuzzyUnordered = "string" }
///
/// Example:
///
/// - Lua: `{ SearchFuzzyUnordered = "pattern" }`
/// - YAML: `SearchFuzzyUnordered: pattern`
SearchFuzzyUnordered(String),
/// Calls `SearchFuzzyUnordered` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Example:
///
/// - Lua: `"SearchFuzzyUnorderedFromInput"`
/// - YAML: `SearchFuzzyUnorderedFromInput`
SearchFuzzyUnorderedFromInput,
/// Search files using regex match algorithm.
/// It keeps the filters, but overrides the sorters.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Type: { SearchRegex = "string" }
///
/// Example:
///
/// - Lua: `{ SearchRegex = "pattern" }`
/// - YAML: `SearchRegex: pattern`
SearchRegex(String),
/// Calls `SearchRegex` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Example:
///
/// - Lua: `"SearchRegexFromInput"`
/// - YAML: `SearchRegexFromInput`
SearchRegexFromInput,
/// Like `SearchRegex`, but doesn't not perform rank based sorting.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Type: { SearchRegexUnordered = "string" }
///
/// Example:
///
/// - Lua: `{ SearchRegexUnordered = "pattern" }`
/// - YAML: `SearchRegexUnordered: pattern`
SearchRegexUnordered(String),
/// Calls `SearchRegexUnordered` with the input taken from the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
/// It gets reset automatically when changing directory.
///
/// Example:
///
/// - Lua: `"SearchRegexUnorderedFromInput"`
/// - YAML: `SearchRegexUnorderedFromInput`
SearchRegexUnorderedFromInput,
/// Toggles between different search algorithms, without changing the input
/// buffer
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
/// - Lua: `"ToggleSearchAlgorithm"`
/// - YAML: `ToggleSearchAlgorithm`
ToggleSearchAlgorithm,
/// Enables ranked search without changing the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
/// - Lua: `"EnableOrderedSearch"`
/// - YAML: `EnableSearchOrder`
EnableSearchOrder,
/// Disabled ranked search without changing the input buffer.
/// You need to call `ExplorePwd` or `ExplorePwdAsync` explicitly.
///
/// Example:
///
/// - Lua: `"DisableSearchOrder"`
/// - YAML: `DisableSearchOrder`
DisableSearchOrder,
/// Toggles ranked search without changing the input buffer.
///
/// Example:
///
/// - Lua: `"ToggleSearchOrder"`
/// - YAML: `ToggleSearchOrder`
ToggleSearchOrder,
/// Accepts the search by keeping the latest focus while in search mode.
/// Automatically calls `ExplorePwd`.
///
/// Example:
///
/// - Lua: `"AcceptSearch"`
/// - YAML: `AcceptSearch`
AcceptSearch,
/// Cancels the search by discarding the latest focus and recovering
/// the focus before search.
/// Automatically calls `ExplorePwd`.
///
/// Example:
///
/// - Lua: `"CancelSearch"`
/// - YAML: `CancelSearch`
CancelSearch,
/// ### Mouse Operations ---------------------------------------------------
/// Enable mouse
@ -1151,7 +846,7 @@ pub enum ExternalMsg {
/// - YAML: `StopFifo`
StopFifo,
/// Toggle between {Start|Stop}Fifo
/// Toggle betwen {Start|Stop}Fifo
///
/// Type: { ToggleFifo = "string" }
///
@ -1284,13 +979,9 @@ impl ExternalMsg {
!matches!(
self,
Self::Call(_)
| Self::Call0(_)
| Self::CallSilently(_)
| Self::CallSilently0(_)
| Self::BashExec(_)
| Self::BashExec0(_)
| Self::BashExecSilently(_)
| Self::BashExecSilently0(_)
| Self::CallLua(_)
| Self::CallLuaSilently(_)
| Self::LuaEval(_)
@ -1793,93 +1484,10 @@ impl NodeFilterApplicable {
}
}
#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct NodeSearcherApplicable {
pub pattern: String,
#[serde(default)]
pub recoverable_focus: Option<String>,
#[serde(default)]
pub algorithm: SearchAlgorithm,
#[serde(default)]
pub unordered: bool,
}
impl NodeSearcherApplicable {
pub fn new(
pattern: String,
recoverable_focus: Option<String>,
algorithm: SearchAlgorithm,
unordered: bool,
) -> Self {
Self {
pattern,
recoverable_focus,
algorithm,
unordered,
}
}
pub fn search<I>(&self, nodes: I) -> Vec<Node>
where
I: IntoIterator<Item = Node>,
{
let engine = self.algorithm.engine(&self.pattern);
let ranked_nodes = nodes.into_iter().filter_map(|n| {
let item = Arc::new(PathItem::from(n.relative_path.clone()));
engine.match_item(item).map(|res| (n, res.rank))
});
if self.unordered {
ranked_nodes.map(|(n, _)| n).collect()
} else {
let mut ranked_nodes = ranked_nodes.collect::<Vec<_>>();
ranked_nodes.sort_by(|(_, s1), (_, s2)| s1.cmp(s2));
ranked_nodes.into_iter().map(|(n, _)| n).collect()
}
}
pub fn enable_search_order(self) -> Self {
Self {
unordered: false,
..self
}
}
pub fn disable_search_order(self) -> Self {
Self {
unordered: true,
..self
}
}
pub fn toggle_search_order(self) -> Self {
Self {
unordered: !self.unordered,
..self
}
}
pub fn toggle_algorithm(self) -> Self {
Self {
algorithm: self.algorithm.toggle(),
..self
}
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExplorerConfig {
#[serde(default)]
pub filters: IndexSet<NodeFilterApplicable>,
#[serde(default)]
pub sorters: IndexSet<NodeSorterApplicable>,
#[serde(default)]
pub searcher: Option<NodeSearcherApplicable>,
}
impl ExplorerConfig {

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

@ -8,30 +8,24 @@ pub enum MsgOut {
ExploreParentsAsync,
Refresh,
ClearScreen,
Quit,
Debug(String),
Call(Command),
Call0(Command),
CallSilently(Command),
CallSilently0(Command),
CallLua(String),
CallLuaSilently(String),
LuaEval(String),
LuaEvalSilently(String),
Enque(Task),
EnableMouse,
DisableMouse,
ToggleMouse,
StartFifo(String),
StopFifo,
ToggleFifo(String),
ScrollUp,
ScrollDown,
ScrollUpHalf,
ScrollDownHalf,
Quit,
PrintPwdAndQuit,
PrintFocusPathAndQuit,
PrintSelectionAndQuit,
PrintResultAndQuit,
PrintAppStateAndQuit,
Enqueue(Task),
}

@ -1,5 +1,5 @@
use crate::permissions::Permissions;
use humansize::{format_size, DECIMAL};
use humansize::{file_size_opts as options, FileSize};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::os::unix::prelude::MetadataExt;
@ -7,21 +7,15 @@ use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
fn to_human_size(size: u64) -> String {
format_size(size, DECIMAL)
size.file_size(options::CONVENTIONAL)
.unwrap_or_else(|_| format!("{} B", size))
}
fn mime_essence(
path: &Path,
is_dir: bool,
extension: &str,
is_executable: bool,
) -> String {
fn mime_essence(path: &Path, is_dir: bool) -> String {
if is_dir {
String::from("inode/directory")
} else if extension.is_empty() && is_executable {
String::from("application/x-executable")
} else {
mime_guess::from_path(path)
mime_guess::from_path(&path)
.first()
.map(|m| m.essence_str().to_string())
.unwrap_or_default()
@ -51,43 +45,29 @@ impl ResolvedNode {
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let (
is_dir,
is_file,
is_readonly,
size,
permissions,
created,
last_modified,
uid,
gid,
) = path
.metadata()
.map(|m| {
(
m.is_dir(),
m.is_file(),
m.permissions().readonly(),
m.len(),
Permissions::from(&m),
m.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos()),
m.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos()),
m.uid(),
m.gid(),
)
})
.unwrap_or((false, false, false, 0, Default::default(), None, None, 0, 0));
let is_executable = permissions.user_execute
|| permissions.group_execute
|| permissions.other_execute;
let mime_essence = mime_essence(&path, is_dir, &extension, is_executable);
let (is_dir, is_file, is_readonly, size, created, last_modified, uid, gid) =
path.metadata()
.map(|m| {
(
m.is_dir(),
m.is_file(),
m.permissions().readonly(),
m.len(),
m.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos()),
m.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_nanos()),
m.uid(),
m.gid(),
)
})
.unwrap_or((false, false, false, 0, None, None, 0, 0));
let mime_essence = mime_essence(&path, is_dir);
let human_size = to_human_size(size);
Self {
@ -198,11 +178,7 @@ impl Node {
)
});
let is_executable = permissions.user_execute
|| permissions.group_execute
|| permissions.other_execute;
let mime_essence = mime_essence(&path, is_dir, &extension, is_executable);
let mime_essence = mime_essence(&path, is_dir);
let human_size = to_human_size(size);
Self {

@ -1,499 +0,0 @@
use crate::dirs;
use anyhow::{bail, Result};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
pub use snailquote::escape;
use std::path::{Component, Path, PathBuf};
lazy_static! {
pub static ref HOME: Option<PathBuf> = dirs::home_dir();
}
// Stolen from https://github.com/Manishearth/pathdiff/blob/master/src/lib.rs
pub fn diff<P, B>(path: P, base: B) -> Result<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = base.as_ref();
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Ok(PathBuf::from(path))
} else {
let path = path.to_string_lossy();
bail!("{path}: is not absolute")
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(Component::CurDir)) => comps.push(a),
(Some(_), Some(Component::ParentDir)) => {
let path = path.to_string_lossy();
let base = base.to_string_lossy();
bail!("{base} is not a parent of {path}")
}
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Ok(comps.iter().map(|c| c.as_os_str()).collect())
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RelativityConfig<B: AsRef<Path>> {
base: Option<B>,
with_prefix_dots: Option<bool>,
without_suffix_dots: Option<bool>,
}
impl<B: AsRef<Path>> RelativityConfig<B> {
pub fn with_base(mut self, base: B) -> Self {
self.base = Some(base);
self
}
pub fn with_prefix_dots(mut self) -> Self {
self.with_prefix_dots = Some(true);
self
}
pub fn without_suffix_dots(mut self) -> Self {
self.without_suffix_dots = Some(true);
self
}
}
pub fn relative_to<P, B>(
path: P,
config: Option<&RelativityConfig<B>>,
) -> Result<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = match config.and_then(|c| c.base.as_ref()) {
Some(base) => PathBuf::from(base.as_ref()),
None => std::env::current_dir()?,
};
let diff = diff(path, base)?;
let relative = if diff.to_str() == Some("") {
".".into()
} else {
diff
};
let relative = if config.and_then(|c| c.with_prefix_dots).unwrap_or(false)
&& !relative.starts_with(".")
&& !relative.starts_with("..")
{
PathBuf::from(".").join(relative)
} else {
relative
};
let relative = if !config.and_then(|c| c.without_suffix_dots).unwrap_or(false) {
relative
} else if relative.ends_with(".") {
match (path.parent(), path.file_name()) {
(Some(_), Some(name)) => PathBuf::from("..").join(name),
(_, _) => relative,
}
} else if relative.ends_with("..") {
match (path.parent(), path.file_name()) {
(Some(parent), Some(name)) => {
if parent.parent().is_some() {
relative.join("..").join(name)
} else {
// always prefer absolute path if it's a child of the root directory
// to guarantee that the basename is included
path.into()
}
}
(_, _) => relative,
}
} else {
relative
};
Ok(relative)
}
pub fn shorten<P, B>(path: P, config: Option<&RelativityConfig<B>>) -> Result<String>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let pathstring = path.to_string_lossy().to_string();
let relative = relative_to(path, config)?;
let relative = relative.to_string_lossy().to_string();
let fromhome = HOME
.as_ref()
.and_then(|h| {
path.strip_prefix(h).ok().map(|p| {
if p.to_str() == Some("") {
"~".into()
} else {
PathBuf::from("~").join(p).to_string_lossy().to_string()
}
})
})
.unwrap_or(pathstring);
if relative.len() < fromhome.len() {
Ok(relative)
} else {
Ok(fromhome)
}
}
#[cfg(test)]
mod tests {
use super::*;
type Config<'a> = Option<&'a RelativityConfig<String>>;
const NONE: Config = Config::None;
fn default<'a>() -> RelativityConfig<&'a str> {
Default::default()
}
#[test]
fn test_relative_to_pwd() {
let path = std::env::current_dir().unwrap();
let relative = relative_to(&path, NONE).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative =
relative_to(&path, Some(&default().without_suffix_dots())).unwrap();
assert_eq!(
relative,
PathBuf::from("..").join(path.file_name().unwrap())
);
let relative = relative_to(
&path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(
relative,
PathBuf::from("..").join(path.file_name().unwrap())
);
}
#[test]
fn test_relative_to_parent() {
let path = std::env::current_dir().unwrap().join("docs");
let parent = path.parent().unwrap();
let base = default().with_base(path.to_str().unwrap());
let relative = relative_to(parent, Some(&base)).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative =
relative_to(parent, Some(&base.clone().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative =
relative_to(parent, Some(&base.clone().without_suffix_dots())).unwrap();
assert_eq!(
relative,
PathBuf::from("../..").join(parent.file_name().unwrap())
);
let relative = relative_to(
parent,
Some(&base.clone().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(
relative,
PathBuf::from("../..").join(parent.file_name().unwrap())
);
}
#[test]
fn test_relative_to_file() {
let path = std::env::current_dir().unwrap().join("foo").join("bar");
let relative = relative_to(&path, NONE).unwrap();
assert_eq!(relative, PathBuf::from("foo/bar"));
let relative = relative_to(&path, Some(&default().with_prefix_dots())).unwrap();
assert_eq!(relative, PathBuf::from("./foo/bar"));
let relative = relative_to(
&path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(relative, PathBuf::from("./foo/bar"));
}
#[test]
fn test_relative_to_root() {
let relative = relative_to("/foo", Some(&default().with_base("/"))).unwrap();
assert_eq!(relative, PathBuf::from("foo"));
let relative = relative_to(
"/foo",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("./foo"));
let relative = relative_to("/", Some(&default().with_base("/"))).unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to(
"/",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("."));
let relative = relative_to("/", Some(&default().with_base("/foo"))).unwrap();
assert_eq!(relative, PathBuf::from(".."));
let relative = relative_to(
"/",
Some(
&default()
.with_base("/foo")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from(".."));
}
#[test]
fn test_relative_to_base() {
let path = "/some/directory";
let base = "/another/foo/bar";
let relative = relative_to(path, Some(&default().with_base(base))).unwrap();
assert_eq!(relative, PathBuf::from("../../../some/directory"));
let relative = relative_to(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(relative, PathBuf::from("../../../some/directory"));
}
#[test]
fn test_shorten_home() {
let path = HOME.as_ref().unwrap();
let res = shorten(path, NONE).unwrap();
assert_eq!(res, "~");
let res = shorten(
path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~");
let res = shorten(
path,
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~");
let res = shorten(path.join("foo"), NONE).unwrap();
assert_eq!(res, "~/foo");
let res = shorten(
path.join("foo"),
Some(&default().with_prefix_dots().without_suffix_dots()),
)
.unwrap();
assert_eq!(res, "~/foo");
let res = shorten(format!("{}foo", path.to_string_lossy()), NONE).unwrap();
assert_ne!(res, "~/foo");
assert_eq!(res, format!("{}foo", path.to_string_lossy()));
}
#[test]
fn test_shorten_base() {
let path = "/present/working/directory";
let base = "/present/foo/bar";
let res = shorten(path, Some(&default().with_base(base))).unwrap();
assert_eq!(res, "../../working/directory");
let res = shorten(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../../working/directory");
}
#[test]
fn test_shorten_pwd() {
let path = "/present/working/directory";
let res = shorten(path, Some(&default().with_base(path))).unwrap();
assert_eq!(res, ".");
let res = shorten(
path,
Some(
&default()
.with_base(path)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../directory");
}
#[test]
fn test_shorten_parent() {
let path = "/present/working";
let base = "/present/working/directory";
let res = shorten(path, Some(&default().with_base(base))).unwrap();
assert_eq!(res, "..");
let res = shorten(
path,
Some(
&default()
.with_base(base)
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "../../working");
}
#[test]
fn test_shorten_root() {
let res = shorten("/", Some(&default().with_base("/"))).unwrap();
assert_eq!(res, "/");
let res = shorten(
"/",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/");
let res = shorten("/foo", Some(&default().with_base("/"))).unwrap();
assert_eq!(res, "foo");
let res = shorten(
"/foo",
Some(
&default()
.with_base("/")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/foo");
let res = shorten(
"/",
Some(
&default()
.with_base("/foo")
.with_prefix_dots()
.without_suffix_dots(),
),
)
.unwrap();
assert_eq!(res, "/");
}
#[test]
fn test_path_escape() {
let text = "foo".to_string();
assert_eq!(escape(&text), "foo");
let text = "foo bar".to_string();
assert_eq!(escape(&text), "'foo bar'");
let text = "foo\nbar".to_string();
assert_eq!(escape(&text), "\"foo\\nbar\"");
let text = "foo$bar".to_string();
assert_eq!(escape(&text), "'foo$bar'");
let text = "foo'$\n'bar".to_string();
assert_eq!(escape(&text), "\"foo'\\$\\n'bar\"");
let text = "a'b\"c".to_string();
assert_eq!(escape(&text), "\"a'b\\\"c\"");
}
}

@ -1,52 +1,27 @@
// Stolen from https://github.com/Peltoche/lsd/blob/master/src/meta/permissions.rs
use serde::{Deserialize, Serialize};
use std::{fmt::Display, fs::Metadata};
use std::fs::Metadata;
pub type RWX = (char, char, char, char, char, char, char, char, char);
pub type Octal = (u8, u8, u8, u8);
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Hash)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Hash, Default)]
pub struct Permissions {
#[serde(default)]
pub user_read: bool,
#[serde(default)]
pub user_write: bool,
#[serde(default)]
pub user_execute: bool,
#[serde(default)]
pub group_read: bool,
#[serde(default)]
pub group_write: bool,
#[serde(default)]
pub group_execute: bool,
#[serde(default)]
pub other_read: bool,
#[serde(default)]
pub other_write: bool,
#[serde(default)]
pub other_execute: bool,
#[serde(default)]
pub sticky: bool,
#[serde(default)]
pub setgid: bool,
#[serde(default)]
pub setuid: bool,
}
impl Permissions {}
impl<'a> From<&'a Metadata> for Permissions {
#[cfg(unix)]
fn from(meta: &Metadata) -> Self {
@ -80,68 +55,6 @@ impl<'a> From<&'a Metadata> for Permissions {
}
}
impl Display for Permissions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (ur, uw, ux, gr, gw, gx, or, ow, ox) = (*self).into();
write!(f, "{ur}{uw}{ux}{gr}{gw}{gx}{or}{ow}{ox}")
}
}
impl Into<RWX> for Permissions {
fn into(self) -> RWX {
let bit = |bit: bool, chr: char| {
if bit {
chr
} else {
'-'
}
};
let ur = bit(self.user_read, 'r');
let uw = bit(self.user_write, 'w');
let ux = match (self.user_execute, self.setuid) {
(true, true) => 's',
(true, false) => 'x',
(false, true) => 'S',
(false, false) => '-',
};
let gr = bit(self.group_read, 'r');
let gw = bit(self.group_write, 'w');
let gx = match (self.group_execute, self.setgid) {
(true, true) => 's',
(true, false) => 'x',
(false, true) => 'S',
(false, false) => '-',
};
let or = bit(self.other_read, 'r');
let ow = bit(self.other_write, 'w');
let ox = match (self.other_execute, self.sticky) {
(true, true) => 't',
(true, false) => 'x',
(false, true) => 'T',
(false, false) => '-',
};
(ur, uw, ux, gr, gw, gx, or, ow, ox)
}
}
impl Into<Octal> for Permissions {
fn into(self) -> Octal {
let bits_to_octal =
|r: bool, w: bool, x: bool| (r as u8) * 4 + (w as u8) * 2 + (x as u8);
(
bits_to_octal(self.setuid, self.setgid, self.sticky),
bits_to_octal(self.user_read, self.user_write, self.user_execute),
bits_to_octal(self.group_read, self.group_write, self.group_execute),
bits_to_octal(self.other_read, self.other_write, self.other_execute),
)
}
}
// More readable aliases for the permission bits exposed by libc.
#[allow(trivial_numeric_casts)]
#[cfg(unix)]

@ -1,5 +1,4 @@
use crate::app::ExternalMsg;
use crate::yaml;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
@ -55,12 +54,12 @@ impl Pipe {
}
}
pub fn read_all(pipe: &str, delimiter: char) -> Result<Vec<ExternalMsg>> {
pub fn read_all(pipe: &str) -> Result<Vec<ExternalMsg>> {
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(false)
.open(pipe)?;
.open(&pipe)?;
let mut in_str = String::new();
file.read_to_string(&mut in_str)?;
@ -68,10 +67,8 @@ pub fn read_all(pipe: &str, delimiter: char) -> Result<Vec<ExternalMsg>> {
if !in_str.is_empty() {
let mut msgs = vec![];
for msg in in_str.trim_matches(delimiter).split(delimiter) {
if !msg.is_empty() {
msgs.push(yaml::from_str(msg)?);
}
for msg in in_str.lines().map(|s| serde_yaml::from_str(s.trim())) {
msgs.push(msg?);
}
Ok(msgs)
} else {

@ -8,9 +8,7 @@ use crate::explorer;
use crate::lua;
use crate::pipe;
use crate::pwd_watcher;
use crate::ui::NO_COLOR;
use crate::ui::UI;
use crate::yaml;
use crate::ui;
use anyhow::{bail, Error, Result};
use crossterm::event;
use crossterm::execute;
@ -18,21 +16,19 @@ use crossterm::terminal as term;
use mlua::LuaSerdeExt;
use mlua::Value;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::process::{Command, ExitStatus, Stdio};
use std::sync::mpsc;
use tui::backend::CrosstermBackend;
use tui::Terminal;
use tui_input::Input;
pub fn get_tty() -> Result<fs::File> {
let tty = "/dev/tty";
match fs::OpenOptions::new().read(true).write(true).open(tty) {
match fs::OpenOptions::new().read(true).write(true).open(&tty) {
Ok(f) => Ok(f),
Err(e) => {
bail!(format!("could not open {tty}. {e}"))
bail!(format!("Failed to open {}. {}", tty, e))
}
}
}
@ -41,14 +37,15 @@ pub fn get_tty() -> Result<fs::File> {
// returns physical path. As a workaround, this function tries to use `PWD`
// environment variable that is configured by shell.
fn get_current_dir() -> Result<PathBuf, std::io::Error> {
let cur = std::env::current_dir();
if let Ok(pwd) = std::env::var("PWD") {
if pwd.is_empty() {
std::env::current_dir()
cur
} else {
Ok(PathBuf::from(pwd))
}
} else {
std::env::current_dir()
cur
}
}
@ -63,30 +60,7 @@ fn call_lua_heavy(
lua::call(lua, func, arg)
}
fn call(
mut app: app::App,
cmd: app::Command,
silent: bool,
terminal: &mut Terminal<CrosstermBackend<File>>,
event_reader: &mut EventReader,
mouse_enabled: &mut bool,
delimiter: char,
) -> Result<app::App> {
if !silent {
if *mouse_enabled {
execute!(terminal.backend_mut(), event::DisableMouseCapture)
.unwrap_or_default();
}
event_reader.stop();
terminal.clear()?;
terminal.set_cursor(0, 0)?;
term::disable_raw_mode()?;
terminal.show_cursor()?;
}
app.write_pipes(delimiter)?;
fn call(app: &app::App, cmd: app::Command, silent: bool) -> Result<ExitStatus> {
let focus_index = app
.directory_buffer
.as_ref()
@ -100,20 +74,17 @@ fn call(
(get_tty()?.into(), get_tty()?.into(), get_tty()?.into())
};
let input_buffer = app
.input
.buffer
.as_ref()
.map(Input::to_string)
.unwrap_or_default();
let status = Command::new(cmd.command.clone())
.env("XPLR", &app.bin)
.env("XPLR_VROOT", &app.vroot.clone().unwrap_or_default())
.env("XPLR_APP_VERSION", &app.version)
Command::new(cmd.command.clone())
.env("XPLR_APP_VERSION", app.version.clone())
.env("XPLR_PID", &app.pid.to_string())
.env("XPLR_INPUT_BUFFER", input_buffer)
.env("XPLR_INITIAL_PWD", &app.initial_pwd)
.env(
"XPLR_INPUT_BUFFER",
app.input
.buffer
.as_ref()
.map(|i| i.value().to_string())
.unwrap_or_default(),
)
.env("XPLR_FOCUS_PATH", app.focused_node_str())
.env("XPLR_FOCUS_INDEX", focus_index)
.env("XPLR_SESSION_PATH", &app.session_path)
@ -136,55 +107,13 @@ fn call(
.stderr(stderr)
.args(cmd.args)
.status()
.map(|s| {
if s.success() {
Ok(())
} else {
Err(format!("process exited with code {s}"))
}
})
.unwrap_or_else(|e| Err(e.to_string()));
match pipe::read_all(&app.pipe.msg_in, delimiter) {
Ok(msgs) => {
app = app.handle_batch_external_msgs(msgs)?;
}
Err(err) => {
app = app.log_error(err.to_string())?;
}
};
app.cleanup_pipes()?;
if let Err(e) = status {
app = app.log_error(e)?;
};
if !silent {
terminal.clear()?;
term::enable_raw_mode()?;
terminal.hide_cursor()?;
event_reader.start();
if *mouse_enabled {
match execute!(terminal.backend_mut(), event::EnableMouseCapture) {
Ok(_) => {
*mouse_enabled = true;
}
Err(e) => {
app = app.log_error(e.to_string())?;
}
}
}
}
Ok(app)
.map_err(Error::new)
}
fn start_fifo(path: &str, focus_path: &str) -> Result<fs::File> {
match fs::OpenOptions::new().write(true).open(path) {
Ok(mut file) => {
writeln!(file, "{focus_path}")?;
writeln!(file, "{}", focus_path)?;
Ok(file)
}
Err(e) => Err(e.into()),
@ -192,8 +121,6 @@ fn start_fifo(path: &str, focus_path: &str) -> Result<fs::File> {
}
pub struct Runner {
bin: String,
vroot: Option<PathBuf>,
pwd: PathBuf,
focused_path: Option<PathBuf>,
config_file: Option<PathBuf>,
@ -202,7 +129,6 @@ pub struct Runner {
read_only: bool,
print_pwd_as_result: bool,
selection: Vec<PathBuf>,
delimiter: char,
}
impl Runner {
@ -213,22 +139,24 @@ impl Runner {
/// Create a new runner object passing the given arguments
pub fn from_cli(cli: Cli) -> Result<Self> {
let currdir = get_current_dir()?;
let mut paths = cli.paths.into_iter();
let mut pwd = paths
.next()
.or_else(|| cli.vroot.clone())
.unwrap_or_else(|| currdir.clone());
let basedir = get_current_dir()?;
let basedir_clone = basedir.clone();
let mut paths = cli.paths.into_iter().map(|p| {
if p.is_relative() {
basedir_clone.join(p)
} else {
p
}
});
let mut pwd = paths.next().unwrap_or_else(|| basedir.clone());
let mut focused_path = None;
if cli.force_focus || pwd.is_file() {
focused_path = pwd.file_name().map(|p| p.into());
pwd = pwd.parent().map(|p| p.into()).unwrap_or(currdir);
pwd = pwd.parent().map(|p| p.into()).unwrap_or(basedir);
}
Ok(Self {
bin: cli.bin,
vroot: cli.vroot,
pwd,
focused_path,
config_file: cli.config,
@ -237,7 +165,6 @@ impl Runner {
read_only: cli.read_only,
print_pwd_as_result: cli.print_pwd_as_result,
selection: paths.collect(),
delimiter: if cli.write0 { '\0' } else { '\n' },
})
}
@ -245,14 +172,8 @@ impl Runner {
pub fn run(self) -> Result<Option<String>> {
// Why unsafe? See https://github.com/sayanarijit/xplr/issues/309
let lua = unsafe { mlua::Lua::unsafe_new() };
let mut app = app::App::create(
self.bin,
self.vroot,
self.pwd,
&lua,
self.config_file,
self.extra_config_files,
)?;
let mut app =
app::App::create(self.pwd, &lua, self.config_file, self.extra_config_files)?;
app.config.general.read_only = self.read_only;
fs::create_dir_all(app.session_path.clone())?;
@ -286,7 +207,7 @@ impl Runner {
tx_pwd_watcher.send(app.pwd.clone())?;
let mut result = Ok(None);
let session_path = app.session_path.clone();
let session_path = app.session_path.to_owned();
term::enable_raw_mode()?;
@ -297,13 +218,12 @@ impl Runner {
execute!(stdout, term::EnterAlternateScreen)?;
let mut fifo: Option<fs::File> =
if let Some(path) = app.config.general.start_fifo.clone() {
if let Some(path) = app.config.general.start_fifo.as_ref() {
// TODO remove duplicate segment
match start_fifo(&path, &app.focused_node_str()) {
match start_fifo(path, &app.focused_node_str()) {
Ok(file) => Some(file),
Err(e) => {
app = app
.log_error(format!("could not start fifo {path:?}: {e}"))?;
app = app.log_error(e.to_string())?;
None
}
}
@ -317,14 +237,13 @@ impl Runner {
let mut mouse_enabled = app.config.general.enable_mouse;
if mouse_enabled {
if let Err(e) = execute!(stdout, event::EnableMouseCapture) {
app = app.log_error(format!("could not enable mouse: {e}"))?;
app = app.log_error(e.to_string())?;
}
}
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
terminal.clear()?;
// Threads
pwd_watcher::keep_watching(app.pwd.as_ref(), tx_msg_in.clone(), rx_pwd_watcher)?;
@ -332,19 +251,10 @@ impl Runner {
event_reader.start();
// Enqueue on_load messages
for msg in app.hooks.on_load.iter().chain(self.on_load.iter()) {
tx_msg_in.send(app::Task::new(app::MsgIn::External(msg.clone()), None))?;
for msg in self.on_load {
tx_msg_in.send(app::Task::new(app::MsgIn::External(msg), None))?;
}
// Refresh screen once after loading
tx_msg_in.send(app::Task::new(
app::MsgIn::External(app::ExternalMsg::Refresh),
None,
))?;
// UI
let mut ui = UI::new(&lua);
'outer: for task in rx_msg_in {
match app.handle_task(task) {
Ok(a) => {
@ -352,7 +262,11 @@ impl Runner {
while let Some(msg) = app.msg_out.pop_front() {
use app::MsgOut::*;
match msg {
Enqueue(task) => {
// NOTE: Do not schedule critical tasks via tx_msg_in in this loop.
// Try handling them immediately.
//
// TODO: Remove boilerplate code.
Enque(task) => {
tx_msg_in.send(task)?;
}
@ -362,70 +276,46 @@ impl Runner {
}
PrintPwdAndQuit => {
result = Ok(Some(app.pwd_str(self.delimiter)));
result = Ok(Some(format!("{}\n", app.pwd)));
break 'outer;
}
PrintFocusPathAndQuit => {
result = Ok(app.focused_node().map(|n| {
format!("{}{}", n.absolute_path, self.delimiter)
}));
result = Ok(app
.focused_node()
.map(|n| format!("{}\n", n.absolute_path)));
break 'outer;
}
PrintSelectionAndQuit => {
result = Ok(Some(app.selection_str(self.delimiter)));
result = Ok(Some(app.selection_str()));
break 'outer;
}
PrintResultAndQuit => {
result = if self.print_pwd_as_result {
Ok(Some(app.pwd_str(self.delimiter)))
Ok(Some(app.pwd_str()))
} else {
Ok(Some(app.result_str(self.delimiter)))
Ok(Some(app.result_str()))
};
break 'outer;
}
PrintAppStateAndQuit => {
let out = yaml::to_string(&app)?;
let out = serde_yaml::to_string(&app)?;
result = Ok(Some(out));
break 'outer;
}
Debug(path) => {
fs::write(path, yaml::to_string(&app)?)?;
fs::write(&path, serde_yaml::to_string(&app)?)?;
}
ClearScreen => {
terminal.clear()?;
}
ScrollUp => {
app = app.focus_previous_by_relative_index(
terminal.size()?.height as usize,
)?;
}
ScrollDown => {
app = app.focus_next_by_relative_index(
terminal.size()?.height as usize,
)?;
}
ScrollUpHalf => {
app = app.focus_previous_by_relative_index(
terminal.size()?.height as usize / 2,
)?;
}
ScrollDownHalf => {
app = app.focus_next_by_relative_index(
terminal.size()?.height as usize / 2,
)?;
}
ExplorePwdAsync => {
explorer::explore_async(
app.explorer_config.clone(),
@ -457,30 +347,21 @@ impl Runner {
}
Refresh => {
// Fifo
let focus = app.focused_node();
if focus != last_focus.as_ref() {
last_focus = focus.cloned();
// Fifo
if let Some(ref mut file) = fifo {
writeln!(file, "{}", app.focused_node_str())?;
};
// Hooks
if !app.hooks.on_focus_change.is_empty() {
let msgs = app.hooks.on_focus_change.clone();
app = app.handle_batch_external_msgs(msgs)?
}
last_focus = focus.cloned();
}
if app.pwd != last_pwd {
last_pwd.clone_from(&app.pwd);
// $PWD watcher
tx_pwd_watcher.send(app.pwd.clone())?;
// OSC 7: Change CWD
if !(*NO_COLOR) {
if !(*ui::NO_COLOR) {
write!(
terminal.backend_mut(),
"\x1b]7;file://{}{}\x1b\\",
@ -489,15 +370,11 @@ impl Runner {
)?;
}
// Hooks
if !app.hooks.on_directory_change.is_empty() {
let msgs = app.hooks.on_directory_change.clone();
app = app.handle_batch_external_msgs(msgs)?
}
last_pwd = app.pwd.clone();
}
// UI
terminal.draw(|f| ui.draw(f, &app))?;
terminal.draw(|f| ui::draw(f, &app, &lua))?;
}
EnableMouse => {
@ -510,9 +387,7 @@ impl Runner {
mouse_enabled = true;
}
Err(e) => {
app = app.log_error(format!(
"could not enable mouse: {e}"
))?;
app = app.log_error(e.to_string())?;
}
}
}
@ -540,9 +415,7 @@ impl Runner {
mouse_enabled = false;
}
Err(e) => {
app = app.log_error(format!(
"could not disable mouse: {e}"
))?;
app = app.log_error(e.to_string())?;
}
}
}
@ -552,9 +425,7 @@ impl Runner {
fifo = match start_fifo(&path, &app.focused_node_str()) {
Ok(file) => Some(file),
Err(e) => {
app = app.log_error(format!(
"could not start fifo {path:?}: {e}"
))?;
app = app.log_error(e.to_string())?;
None
}
}
@ -577,9 +448,7 @@ impl Runner {
{
Ok(file) => Some(file),
Err(e) => {
app = app.log_error(format!(
"could not toggle fifo {path:?}: {e}"
))?;
app = app.log_error(e.to_string())?;
None
}
}
@ -598,6 +467,37 @@ impl Runner {
};
}
CallSilently(cmd) => {
app.write_pipes()?;
let status = call(&app, cmd, true)
.map(|s| {
if s.success() {
Ok(())
} else {
Err(format!(
"process exited with code {}",
&s
))
}
})
.unwrap_or_else(|e| Err(e.to_string()));
match pipe::read_all(&app.pipe.msg_in) {
Ok(msgs) => {
app = app.handle_batch_external_msgs(msgs)?;
}
Err(err) => {
app = app.log_error(err.to_string())?;
}
};
app.cleanup_pipes()?;
if let Err(e) = status {
app = app.log_error(e.to_string())?;
};
}
CallLua(func) => {
execute!(
terminal.backend_mut(),
@ -761,51 +661,67 @@ impl Runner {
}
Call(cmd) => {
app = call(
app,
cmd,
false,
&mut terminal,
&mut event_reader,
&mut mouse_enabled,
'\n',
)?;
}
execute!(
terminal.backend_mut(),
event::DisableMouseCapture
)
.unwrap_or_default();
Call0(cmd) => {
app = call(
app,
cmd,
false,
&mut terminal,
&mut event_reader,
&mut mouse_enabled,
'\0',
)?;
}
event_reader.stop();
CallSilently(cmd) => {
app = call(
app,
cmd,
true,
&mut terminal,
&mut event_reader,
&mut mouse_enabled,
'\n',
)?;
}
terminal.clear()?;
terminal.set_cursor(0, 0)?;
term::disable_raw_mode()?;
terminal.show_cursor()?;
app.write_pipes()?;
let status = call(&app, cmd, false)
.map(|s| {
if s.success() {
Ok(())
} else {
Err(format!(
"process exited with code {}",
&s
))
}
})
.unwrap_or_else(|e| Err(e.to_string()));
// TODO remove duplicate segment
match pipe::read_all(&app.pipe.msg_in) {
Ok(msgs) => {
app = app.handle_batch_external_msgs(msgs)?;
}
Err(err) => {
app = app.log_error(err.to_string())?;
}
};
CallSilently0(cmd) => {
app = call(
app,
cmd,
true,
&mut terminal,
&mut event_reader,
&mut mouse_enabled,
'\0',
)?;
app.cleanup_pipes()?;
if let Err(e) = status {
app = app.log_error(e.to_string())?;
};
terminal.clear()?;
term::enable_raw_mode()?;
terminal.hide_cursor()?;
event_reader.start();
if mouse_enabled {
match execute!(
terminal.backend_mut(),
event::EnableMouseCapture
) {
Ok(_) => {
mouse_enabled = true;
}
Err(e) => {
app = app.log_error(e.to_string())?;
}
}
}
}
};
}

@ -1,50 +0,0 @@
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory};
use skim::{MatchEngine, MatchEngineFactory, SkimItem};
lazy_static! {
static ref FUZZY_FACTORY: ExactOrFuzzyEngineFactory =
ExactOrFuzzyEngineFactory::builder().build();
static ref REGEX_FACTORY: RegexEngineFactory = RegexEngineFactory::builder().build();
}
pub struct PathItem {
path: String,
}
impl From<String> for PathItem {
fn from(value: String) -> Self {
Self { path: value }
}
}
impl SkimItem for PathItem {
fn text(&self) -> std::borrow::Cow<str> {
std::borrow::Cow::from(&self.path)
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SearchAlgorithm {
#[default]
Fuzzy,
Regex,
}
impl SearchAlgorithm {
pub fn toggle(self) -> Self {
match self {
Self::Fuzzy => Self::Regex,
Self::Regex => Self::Fuzzy,
}
}
pub fn engine(&self, pattern: &str) -> Box<dyn MatchEngine> {
match self {
Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern),
Self::Regex => REGEX_FACTORY.create_engine(pattern),
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,22 +0,0 @@
use serde_yaml::with::singleton_map_recursive;
pub use serde_yaml::Result;
pub use serde_yaml::Value;
pub fn to_string<T>(value: &T) -> Result<String>
where
T: ?Sized + serde::Serialize,
{
let mut vec = Vec::with_capacity(128);
let mut serializer = serde_yaml::Serializer::new(&mut vec);
singleton_map_recursive::serialize(&value, &mut serializer)?;
String::from_utf8(vec).map_err(serde::ser::Error::custom)
}
pub fn from_str<'de, T>(s: &'de str) -> Result<T>
where
T: serde::Deserialize<'de>,
{
let deserializer = serde_yaml::Deserializer::from_str(s);
singleton_map_recursive::deserialize(deserializer)
}
Loading…
Cancel
Save