mirror of https://github.com/sayanarijit/xplr
Compare commits
219 Commits
Author | SHA1 | Date |
---|---|---|
|
68fb6fa1a2 | 1 week ago |
|
a82ea6a3e5 | 1 week ago |
|
e13dd21728 | 1 week ago |
|
182a201b0d | 2 months ago |
|
e0b0466e42 | 2 months ago |
|
805e1594ed | 2 months ago |
|
41648ced34 | 2 months ago |
|
89d7bccce8 | 2 months ago |
|
e15c1e8a8c | 2 months ago |
|
8afdf9e478 | 2 months ago |
|
a48dae008c | 2 months ago |
|
ad8afa9d38 | 2 months ago |
|
c2a11059c8 | 2 months ago |
|
6d7ccce282 | 2 months ago |
|
90df0a2b5a | 2 months ago |
|
ce52bcdf94 | 2 months ago |
|
6fb0781fe4 | 2 months ago |
|
c1bb251fef | 3 months ago |
|
976530ba70 | 3 months ago |
|
96da7e1da8 | 3 months ago |
|
96ffe8680b | 3 months ago |
|
1600ad9a9c | 3 months ago |
|
2a3d056bf1 | 3 months ago |
|
91276f6871 | 3 months ago |
|
00bd54abe9 | 3 months ago |
|
95621af9eb | 3 months ago |
|
5240b3904b | 3 months ago |
|
a6fb695ff9 | 3 months ago |
|
fd40de26e7 | 3 months ago |
|
87805509c5 | 3 months ago |
|
4aa367ca7c | 3 months ago |
|
01606e0e60 | 3 months ago |
|
e834242f5d | 3 months ago |
|
7c6dffc2c6 | 3 months ago |
|
d5217f6677 | 3 months ago |
|
0285f0824c | 3 months ago |
|
a6b19425ae | 3 months ago |
|
9db8b2cc19 | 3 months ago |
|
68500f3a8e | 4 months ago |
|
ded2e108bf | 5 months ago |
|
6e8f3da971 | 5 months ago |
|
d76a70fed4 | 5 months ago |
|
16673963aa | 5 months ago |
|
b0ef9a5190 | 5 months ago |
|
b70337708c | 5 months ago |
|
9127d15494 | 5 months ago |
|
66d9f7e586 | 5 months ago |
|
eab47a9044 | 5 months ago |
|
a9e3752f56 | 5 months ago |
|
470bea1265 | 5 months ago |
|
cc578aaf0a | 6 months ago |
|
50e81853fe | 6 months ago |
|
414b45e4fd | 6 months ago |
|
75dabeb283 | 6 months ago |
|
1629398adf | 7 months ago |
|
dd8bb74dd4 | 7 months ago |
|
1dc5eae8fc | 7 months ago |
|
484b94a961 | 8 months ago |
|
50d9d1c54b | 9 months ago |
|
c7c3d2d7f6 | 9 months ago |
|
1441275860 | 11 months ago |
|
8af1647c09 | 11 months ago |
|
22b5fca8d9 | 11 months ago |
|
4a3f18100d | 11 months ago |
|
6df168f8c1 | 11 months ago |
|
eeb7b5d684 | 11 months ago |
|
9a7ff5846d | 11 months ago |
|
1b2226512f | 11 months ago |
|
56472998f5 | 11 months ago |
|
bf7ae3f748 | 11 months ago |
|
94ba22bbcc | 11 months ago |
|
567a6201a8 | 11 months ago |
|
54d6d19003 | 11 months ago |
|
4aeb3dd7c8 | 12 months ago |
|
5626422ba4 | 12 months ago |
|
1941355128 | 12 months ago |
|
2f78691333 | 12 months ago |
|
a2fbf759dd | 12 months ago |
|
bc7f3cbbcf | 12 months ago |
|
ad50342260 | 12 months ago |
|
313c61db96 | 12 months ago |
|
255517c2a9 | 12 months ago |
|
9844ae1476 | 12 months ago |
|
d282032b3d | 12 months ago |
|
ba26752f6c | 12 months ago |
|
0cc8723e8e | 12 months ago |
|
2f3c2ea0e4 | 12 months ago |
|
219ee68152 | 12 months ago |
|
859d888bde | 12 months ago |
|
f84d9d5c6a | 12 months ago |
|
3fcfb1dbef | 12 months ago |
|
4c51f0affe | 12 months ago |
|
9d1bd99fd4 | 12 months ago |
|
8209988ba6 | 12 months ago |
|
33c5aa9f14 | 12 months ago |
|
cae50e4bcf | 12 months ago |
|
048b1c701a | 1 year ago |
|
508f4b980b | 1 year ago |
|
28c9e0e3a0 | 1 year ago |
|
4ccd9796c4 | 1 year ago |
|
36a7f1dc17 | 1 year ago |
|
2cc8e0c510 | 1 year ago |
|
27bc1217b3 | 1 year ago |
|
ab90381fda | 1 year ago |
|
2a775371f6 | 1 year ago |
|
3bee8060c7 | 1 year ago |
|
97e30e2a6f | 1 year ago |
|
7c26c48e18 | 1 year ago |
|
17269ab17f | 1 year ago |
|
8aff0ba918 | 1 year ago |
|
4228a71ed9 | 1 year ago |
|
252a1f5c37 | 1 year ago |
|
4f0db1f3e3 | 1 year ago |
|
8cca2d3566 | 1 year ago |
|
b995be0089 | 1 year ago |
|
c79175764b | 1 year ago |
|
e0d683b13a | 1 year ago |
|
59279b816d | 1 year ago |
|
8c4f744bb1 | 1 year ago |
|
d80b1b4db8 | 1 year ago |
|
5f07e6143f | 1 year ago |
|
43c88b4873 | 1 year ago |
|
d52ccac8ba | 1 year ago |
|
26d79bd799 | 1 year ago |
|
553b4ed3d6 | 1 year ago |
|
e9fc643bd9 | 1 year ago |
|
3afccf2a54 | 1 year ago |
|
1d9d5f5145 | 1 year ago |
|
0715e242ef | 1 year ago |
|
006c655e3a | 1 year ago |
|
105e770f58 | 1 year ago |
|
d6e33e68e3 | 1 year ago |
|
078da205ca | 1 year ago |
|
caa365b4a0 | 1 year ago |
|
4c4e7f41b4 | 2 years ago |
|
e6e701b371 | 2 years ago |
|
0cd5a9163d | 2 years ago |
|
ac958c9532 | 2 years ago |
|
7fbcd18bb4 | 2 years ago |
|
1369fcea9a | 2 years ago |
|
d17489de8d | 2 years ago |
|
57492b84c0 | 2 years ago |
|
d719700122 | 2 years ago |
|
011d3d4a68 | 2 years ago |
|
efb4d605c5 | 2 years ago |
|
e559b96e31 | 2 years ago |
|
d4edf3302f | 2 years ago |
|
c382768f23 | 2 years ago |
|
6558ba8092 | 2 years ago |
|
91c87a3901 | 2 years ago |
|
25798009ad | 2 years ago |
|
104dc0534b | 2 years ago |
|
919a492131 | 2 years ago |
|
399fd183d4 | 2 years ago |
|
053615b041 | 2 years ago |
|
76224c42e1 | 2 years ago |
|
ca4f4f3f45 | 2 years ago |
|
a0c19025dd | 2 years ago |
|
69db7fa9e6 | 2 years ago |
|
555bc02e10 | 2 years ago |
|
bbb1026ff8 | 2 years ago |
|
aea17b415b | 2 years ago |
|
7e70487511 | 2 years ago |
|
15e7123aba | 2 years ago |
|
133ba120af | 2 years ago |
|
77164ff98a | 2 years ago |
|
ea8a1fcd46 | 2 years ago |
|
70cb745c9f | 2 years ago |
|
15979e4974 | 2 years ago |
|
49c4729047 | 2 years ago |
|
01060ed025 | 2 years ago |
|
74318435e8 | 2 years ago |
|
553f6b9bc6 | 2 years ago |
|
71bd2e2776 | 2 years ago |
|
94154c56df | 2 years ago |
|
5c4dec05ed | 2 years ago |
|
289556f452 | 2 years ago |
|
eba9de5a44 | 2 years ago |
|
e3a5f3c044 | 2 years ago |
|
a62b72bf2a | 2 years ago |
|
00ffd077aa | 2 years ago |
|
bafe15e25e | 2 years ago |
|
fc798aad97 | 2 years ago |
|
deb28fa14a | 2 years ago |
|
723dd6ec2c | 2 years ago |
|
4a34780eb4 | 2 years ago |
|
b05e702536 | 2 years ago |
|
baa8759d68 | 2 years ago |
|
52c8e05a1e | 2 years ago |
|
65eee2dc90 | 2 years ago |
|
77852b435f | 2 years ago |
|
060544a2b8 | 2 years ago |
|
e51818dfc1 | 2 years ago |
|
f8b16a7ddd | 2 years ago |
|
85c4253782 | 2 years ago |
|
1d829c36e6 | 2 years ago |
|
7c730557f2 | 2 years ago |
|
93e9b2b2ae | 2 years ago |
|
3fb174cdc0 | 2 years ago |
|
111a648818 | 2 years ago |
|
1e820030a0 | 2 years ago |
|
bffe1d43ec | 2 years ago |
|
fbe6b2be10 | 2 years ago |
|
88fe71779b | 2 years ago |
|
91e3990df1 | 2 years ago |
|
7b8f38df5b | 2 years ago |
|
2b5755aa8a | 2 years ago |
|
895d55ca23 | 2 years ago |
|
1d20039fae | 2 years ago |
|
c93202b649 | 2 years ago |
|
89b3731b56 | 2 years ago |
|
ae714e9713 | 2 years ago |
|
a4f98f0b63 | 2 years ago |
|
bd478ff64b | 2 years ago |
|
57483bef41 | 2 years ago |
|
93bd53bbcb | 2 years ago |
|
81e83365f2 | 2 years ago |
|
58c572d77a | 2 years ago |
|
f2713d9549 | 2 years ago |
@ -1,4 +1,14 @@
|
||||
# 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"]
|
||||
|
@ -0,0 +1,6 @@
|
||||
ratatui
|
||||
crate
|
||||
ser
|
||||
enque
|
||||
noice
|
||||
ans
|
@ -0,0 +1,23 @@
|
||||
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
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,17 @@
|
||||
Process
|
||||
-------
|
||||
- [ ] Update `Cargo.toml` version.
|
||||
- [ ] Update `config.yml` version.
|
||||
- [ ] Update `Config::is_compatible()` and `Config::upgrade_notification()`.
|
||||
- [ ] Publish GitHub tag.
|
||||
- [ ] Update Wiki and docs.
|
||||
# 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)
|
||||
|
@ -0,0 +1,10 @@
|
||||
(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
|
@ -1,12 +0,0 @@
|
||||
# Community
|
||||
|
||||
Building an active community of awesome people and learning stuff together is
|
||||
one of my reasons to publish this tool and maintain it. Hence, please feel free
|
||||
to reach out via your preferred way.
|
||||
|
||||
- Real-time chat lovers can join our [**matrix room**][3] or [**discord channel**][1].
|
||||
- Forum discussion veterans can [**start a new GitHub discussion**][2].
|
||||
|
||||
[1]: https://discord.gg/JmasSPCcz3
|
||||
[2]: https://github.com/sayanarijit/xplr/discussions
|
||||
[3]: https://matrix.to/#/#xplr-pub:matrix.org
|
@ -1,32 +0,0 @@
|
||||
If you like xplr, and want to contribute, that would be really awesome.
|
||||
|
||||
You can contribute to this project in the following ways
|
||||
|
||||
- Contribute your time and expertise (read [CONTRIBUTING.md][1] for instructions).
|
||||
|
||||
- **Developers:** You can help me improve my code, fix things, implement features etc.
|
||||
- **Repository maintainers:** You can save the users from the pain of managing xplr in their system manually.
|
||||
- **Code Reviewers:** Teach me your ways of code.
|
||||
- **Designers:** You can make the logo even more awesome, donate stickers and blog post worthy pictures.
|
||||
- **Bloggers, YouTubers & broadcasters:** You can help spread the word.
|
||||
|
||||
- Contribute by donating or sponsoring me via any of the following ways.
|
||||
- [GitHub Sponsors][5]
|
||||
- [Open Collective][2]
|
||||
- [ko-fi][3]
|
||||
- [liberapay][6]
|
||||
- [PayPal][7]
|
||||
|
||||
For further queries or concern related to `xplr`, [just ask us][4].
|
||||
|
||||
### Backers
|
||||
|
||||
<a href="https://opencollective.com/xplr#backer"><img src="https://opencollective.com/xplr/tiers/backer.svg?width=890" /></a>
|
||||
|
||||
[1]: https://github.com/sayanarijit/xplr/blob/main/CONTRIBUTING.md
|
||||
[2]: https://opencollective.com/xplr
|
||||
[3]: https://ko-fi.com/sayanarijit
|
||||
[4]: community.md
|
||||
[5]: https://github.com/sponsors/sayanarijit?o=esb
|
||||
[6]: https://liberapay.com/sayanarijit
|
||||
[7]: https://paypal.me/sayanarijit
|
@ -0,0 +1,77 @@
|
||||
# Searching
|
||||
|
||||
xplr supports searching paths using different algorithm. The search mechanism
|
||||
generally appears between filters and sorters in the `Sort & filter` panel.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
fzy:foo↓
|
||||
```
|
||||
|
||||
This line means that the nodes visible on the table are being filtered using the
|
||||
[fuzzy matching][1] algorithm on the input `foo`. The arrow means that ranking based
|
||||
ordering is being applied, i.e. [sorters][2] are being ignored.
|
||||
|
||||
## Node Searcher Applicable
|
||||
|
||||
Node Searcher contains the following fields:
|
||||
|
||||
- [pattern][3]
|
||||
- [recoverable_focus][4]
|
||||
- [algorithm][5]
|
||||
- [unordered][7]
|
||||
|
||||
### pattern
|
||||
|
||||
The 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
|
@ -0,0 +1,96 @@
|
||||
# Sum Type
|
||||
|
||||
> This section isn't specific to xplr. However, since xplr configuration makes
|
||||
> heavy use of this particular data type, even though it isn't available in
|
||||
> most of the mainstream programming languages (yet), making it a wild or
|
||||
> unfamiliar concept for many, it's worth doing a quick introduction here.
|
||||
>
|
||||
> If you're already familiar with [Sum Type / Tagged Union][1] (e.g. Rust's
|
||||
> enum), you can skip ahead.
|
||||
|
||||
While reading this doc, you'll come across some data types like [Layout][2],
|
||||
[Color][4], [Message][3] etc. that says something like "x is a sum type that
|
||||
can be any of the following", and then you'll see a list of strings and/or lua
|
||||
tables just below.
|
||||
|
||||
Yes, they are actually sum types, i.e. they can be any of the given set of
|
||||
tagged variants listed there.
|
||||
|
||||
Notice the word "be". Unlike classes or structs (aka product types), they can't
|
||||
"have" values, they can only "be" the value, or rather, be one of the possible
|
||||
set of values.
|
||||
|
||||
Also notice the word "tagged". Unlike the single variant `null`, or the dual
|
||||
variant `boolean` types, the variants of sum types are tagged (i.e. named), and
|
||||
may further have, or be, value or values of any data type.
|
||||
|
||||
A simple example of a sum type is an enum. Many programming languages have
|
||||
them, but only a few modern programming languages allow nesting other types
|
||||
into a sum type.
|
||||
|
||||
```rust
|
||||
enum Color {
|
||||
Red,
|
||||
Green,
|
||||
}
|
||||
```
|
||||
|
||||
Here, `Color` can be one of two possible set of values: `Red` and `Green`, just
|
||||
like `boolean`, but unlike `boolean`, being tagged allows `Color` to have more
|
||||
than two variants if required, by changing the definition.
|
||||
|
||||
e.g.
|
||||
|
||||
```rust
|
||||
enum Color {
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
}
|
||||
```
|
||||
|
||||
We'd document it here as:
|
||||
|
||||
> Result is a sum type that can be one of the following:
|
||||
>
|
||||
> - "Red"
|
||||
> - "Green"
|
||||
> - "Blue"
|
||||
|
||||
But some languages (like Rust, Haskell, Elm etc.) go even further, allowing us
|
||||
to associate each branch of the enum with further nested types like:
|
||||
|
||||
```rust
|
||||
enum Layout {
|
||||
Table,
|
||||
HelpMenu,
|
||||
Horizontal {
|
||||
config: LayoutConfig, // A product type (similar to class/struct)
|
||||
splits: Vec<Layout> // A list of "Layout"s (i.e. list of sum types)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Here, as we can see, unlike the first example, some of `Layout`'s possible
|
||||
variants can have further nested types associated with them. Note that
|
||||
`Horizontal` here can have a sum type (e.g. enum), or a product type (e.g.
|
||||
class/struct), or both (any number of them actually) nested in it. But the
|
||||
nested values will only exist when `Layout` is `Horizontal`.
|
||||
|
||||
We'd document it here as:
|
||||
|
||||
> Layout is a sum type that can be one of the following:
|
||||
>
|
||||
> - "Table"
|
||||
> - "HelpMenu"
|
||||
> - { Horizontal = { config = Layout Config, splits = { Layout, ... } }
|
||||
|
||||
And then we'd go on documenting whatever `Layout Config` is.
|
||||
|
||||
So, there you go. This is exactly what sum types are - glorified enums that can
|
||||
have nested types in each branch.
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/Tagged_union
|
||||
[2]: layout.md
|
||||
[3]: message.md
|
||||
[4]: style.md#color
|
@ -1,34 +0,0 @@
|
||||
# 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
|
@ -0,0 +1,528 @@
|
||||
### 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 +0,0 @@
|
||||
../en/book
|
@ -0,0 +1,11 @@
|
||||
v="0.4.40"
|
||||
|
||||
curl -L https://github.com/rust-lang/mdBook/releases/download/v$v/mdbook-v$v-x86_64-unknown-linux-gnu.tar.gz -o mdbook.tgz \
|
||||
&& tar xzvf mdbook.tgz \
|
||||
&& ./mdbook build docs/en \
|
||||
&& mkdir dist \
|
||||
&& mv -v docs/en/book/html dist/en \
|
||||
&& mv -v assets dist \
|
||||
&& mv -v docs/landing/index.html docs/landing/css docs/landing/js dist \
|
||||
&& rm -v mdbook \
|
||||
&& rm -v mdbook.tgz
|
@ -0,0 +1,26 @@
|
||||
{
|
||||
"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
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
name: xplr
|
||||
version: git
|
||||
summary: A hackable, minimal, fast TUI file explorer
|
||||
description: |
|
||||
xplr is a terminal UI based file explorer
|
||||
that aims to increase our terminal productivity by being a flexible,
|
||||
interactive orchestrator for the ever growing awesome command-line
|
||||
utilities that work with the file-system.
|
||||
source-code: https://github.com/sayanarijit/xplr
|
||||
issues: https://github.com/sayanarijit/xplr/issues
|
||||
website: https://xplr.dev/
|
||||
|
||||
base: core20
|
||||
grade: devel # must be 'stable' to release into candidate/stable channels
|
||||
confinement: devmode # use 'strict' once you have the right plugs and slots
|
||||
|
||||
|
||||
parts:
|
||||
xplr:
|
||||
plugin: rust
|
||||
source: .
|
||||
|
||||
apps:
|
||||
xplr:
|
||||
command: bin/xplr
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,224 @@
|
||||
// Things of the past, mostly bad decisions, which cannot erased, stays in this
|
||||
// haunted module.
|
||||
|
||||
use crate::app;
|
||||
use crate::lua;
|
||||
use crate::ui::block;
|
||||
use crate::ui::string_to_text;
|
||||
use crate::ui::Constraint;
|
||||
use crate::ui::ContentRendererArg;
|
||||
use 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref BASE_DIRS: Option<BaseDirectories> = BaseDirectories::new().ok();
|
||||
}
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
home::home_dir()
|
||||
}
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
BASE_DIRS.as_ref().map(|base| base.get_config_home())
|
||||
}
|
||||
|
||||
pub fn runtime_dir() -> PathBuf {
|
||||
let Some(dir) = BASE_DIRS
|
||||
.as_ref()
|
||||
.and_then(|base| base.get_runtime_directory().ok())
|
||||
else {
|
||||
return env::temp_dir();
|
||||
};
|
||||
dir.clone()
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,904 @@
|
||||
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)
|
||||
}
|
@ -0,0 +1,499 @@
|
||||
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\"");
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory};
|
||||
use skim::{MatchEngine, MatchEngineFactory, SkimItem};
|
||||
|
||||
lazy_static! {
|
||||
static ref FUZZY_FACTORY: ExactOrFuzzyEngineFactory =
|
||||
ExactOrFuzzyEngineFactory::builder().build();
|
||||
static ref REGEX_FACTORY: RegexEngineFactory = RegexEngineFactory::builder().build();
|
||||
}
|
||||
|
||||
pub struct PathItem {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl From<String> for PathItem {
|
||||
fn from(value: String) -> Self {
|
||||
Self { path: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl SkimItem for PathItem {
|
||||
fn text(&self) -> std::borrow::Cow<str> {
|
||||
std::borrow::Cow::from(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum SearchAlgorithm {
|
||||
#[default]
|
||||
Fuzzy,
|
||||
Regex,
|
||||
}
|
||||
|
||||
impl SearchAlgorithm {
|
||||
pub fn toggle(self) -> Self {
|
||||
match self {
|
||||
Self::Fuzzy => Self::Regex,
|
||||
Self::Regex => Self::Fuzzy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn engine(&self, pattern: &str) -> Box<dyn MatchEngine> {
|
||||
match self {
|
||||
Self::Fuzzy => FUZZY_FACTORY.create_engine(pattern),
|
||||
Self::Regex => REGEX_FACTORY.create_engine(pattern),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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…
Reference in New Issue