├── .envrc ├── docs ├── CNAME ├── landing │ ├── assets │ ├── js │ │ └── scripts.js │ └── index.html ├── en │ ├── theme │ │ └── favicon.png │ ├── src │ │ ├── quickstart.md │ │ ├── integration.md │ │ ├── plugin.md │ │ ├── key-bindings.md │ │ ├── post-install.md │ │ ├── input-operation.md │ │ ├── concept.md │ │ ├── node-type.md │ │ ├── borders.md │ │ ├── alternatives.md │ │ ├── mode.md │ │ ├── awesome-integrations.md │ │ ├── style.md │ │ ├── installing-plugins.md │ │ ├── message.md │ │ ├── SUMMARY.md │ │ ├── sorting.md │ │ ├── layouts.md │ │ ├── searching.md │ │ ├── filtering.md │ │ ├── debug-key-bindings.md │ │ ├── writing-plugins.md │ │ ├── sum-type.md │ │ ├── node_types.md │ │ ├── introduction.md │ │ ├── modes.md │ │ ├── configuration.md │ │ ├── install.md │ │ ├── configure-key-bindings.md │ │ ├── awesome-plugins.md │ │ ├── environment-variables-and-pipes.md │ │ └── column-renderer.md │ └── book.toml └── script │ ├── deploy-cf.sh │ └── generate.py ├── src ├── msg │ ├── mod.rs │ ├── in_ │ │ ├── mod.rs │ │ └── internal.rs │ └── out │ │ └── mod.rs ├── dirs.rs ├── yaml.rs ├── lib.rs ├── directory_buffer.rs ├── pwd_watcher.rs ├── pipe.rs ├── search.rs ├── event_reader.rs ├── permissions.rs ├── lua │ └── mod.rs ├── bin │ └── xplr.rs ├── cli.rs ├── compat.rs ├── node.rs └── explorer.rs ├── .codespellignore ├── assets ├── icon │ ├── xplr128.ico │ ├── xplr128.png │ ├── xplr16.ico │ ├── xplr16.png │ ├── xplr32.ico │ ├── xplr32.png │ ├── xplr64.ico │ ├── xplr64.png │ └── xplr.svg └── desktop │ └── xplr.desktop ├── rustfmt.toml ├── .gitignore ├── default.nix ├── examples └── run.rs ├── .github ├── FUNDING.yml └── workflows │ ├── nix-cache.yml │ ├── book.yml │ ├── ci.yml │ └── cd.yml ├── .cargo └── config.toml ├── RELEASE.md ├── flake.lock ├── snap └── snapcraft.yaml ├── LICENSE ├── CONTRIBUTING.md ├── flake.nix ├── Cargo.toml ├── README.md ├── benches └── criterion.rs └── CODE_OF_CONDUCT.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | xplr.dev 2 | -------------------------------------------------------------------------------- /docs/landing/assets: -------------------------------------------------------------------------------- 1 | ../../assets -------------------------------------------------------------------------------- /src/msg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod in_; 2 | pub mod out; 3 | -------------------------------------------------------------------------------- /.codespellignore: -------------------------------------------------------------------------------- 1 | ratatui 2 | crate 3 | ser 4 | enque 5 | noice 6 | ans 7 | -------------------------------------------------------------------------------- /assets/icon/xplr128.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr128.ico -------------------------------------------------------------------------------- /assets/icon/xplr128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr128.png -------------------------------------------------------------------------------- /assets/icon/xplr16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr16.ico -------------------------------------------------------------------------------- /assets/icon/xplr16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr16.png -------------------------------------------------------------------------------- /assets/icon/xplr32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr32.ico -------------------------------------------------------------------------------- /assets/icon/xplr32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr32.png -------------------------------------------------------------------------------- /assets/icon/xplr64.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr64.ico -------------------------------------------------------------------------------- /assets/icon/xplr64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/assets/icon/xplr64.png -------------------------------------------------------------------------------- /docs/en/theme/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayanarijit/xplr/HEAD/docs/en/theme/favicon.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | max_width = 89 3 | tab_spaces = 4 4 | use_field_init_shorthand = true 5 | -------------------------------------------------------------------------------- /docs/en/src/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | Nice to you have here! Let's quickly start with the 4 | following steps: 5 | 6 | - [Install][1] 7 | - [Post Install][2] 8 | 9 | [1]: install.md 10 | [2]: post-install.md 11 | -------------------------------------------------------------------------------- /docs/en/src/integration.md: -------------------------------------------------------------------------------- 1 | # Integration 2 | 3 | xplr is designed to integrate well with other tools and commands. It can be 4 | used as a file picker or a pluggable file manager. 5 | 6 | - [Awesome Integrations][1] 7 | 8 | [1]: awesome-integrations.md 9 | -------------------------------------------------------------------------------- /assets/desktop/xplr.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=xplr 4 | Comment=Terminal file explorer 5 | Exec=xplr 6 | Terminal=true 7 | Icon=xplr 8 | MimeType=inode/directory 9 | Categories=System;FileTools;FileManager;ConsoleOnly 10 | Keywords=File;Manager;Management;Explorer;Launcher 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /flamegraph.svg 3 | /perf.data 4 | /perf.data.old 5 | 6 | .vscode/ 7 | book/ 8 | 9 | # Vi[m] backups 10 | *.swp 11 | *~ 12 | 13 | # Jetbrains config 14 | .idea/ 15 | 16 | .venv/ 17 | 18 | # direnv 19 | .direnv/ 20 | 21 | # nix 22 | result 23 | 24 | # test files 25 | /init.lua 26 | -------------------------------------------------------------------------------- /docs/en/src/plugin.md: -------------------------------------------------------------------------------- 1 | # Plugin 2 | 3 | xplr supports pluggable Lua modules that can be used to easily configure or 4 | extend xplr UI and functionalities. 5 | 6 | - [Installing Plugins][1] 7 | - [Writing Plugins][2] 8 | - [Awesome Plugins][3] 9 | 10 | [1]: installing-plugins.md 11 | [2]: writing-plugins.md 12 | [3]: awesome-plugins.md 13 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).defaultNix 11 | -------------------------------------------------------------------------------- /src/msg/in_/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod external; 2 | pub mod internal; 3 | 4 | pub use external::ExternalMsg; 5 | pub use internal::InternalMsg; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 9 | pub enum MsgIn { 10 | Internal(internal::InternalMsg), 11 | External(external::ExternalMsg), 12 | } 13 | -------------------------------------------------------------------------------- /src/msg/in_/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::app::DirectoryBuffer; 2 | use crate::input::Key; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 6 | pub enum InternalMsg { 7 | AddLastFocus(String, Option), 8 | SetDirectory(DirectoryBuffer), 9 | HandleKey(Key), 10 | RefreshSelection, 11 | } 12 | -------------------------------------------------------------------------------- /examples/run.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | match xplr::runner::runner().and_then(|a| a.run()) { 3 | Ok(Some(out)) => print!("{}", out), 4 | Ok(None) => {} 5 | Err(err) => { 6 | if !err.to_string().is_empty() { 7 | eprintln!("error: {}", err); 8 | }; 9 | 10 | std::process::exit(1); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/script/deploy-cf.sh: -------------------------------------------------------------------------------- 1 | v="0.4.40" 2 | 3 | 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 \ 4 | && tar xzvf mdbook.tgz \ 5 | && ./mdbook build docs/en \ 6 | && mkdir dist \ 7 | && mv -v docs/en/book/html dist/en \ 8 | && mv -v assets dist \ 9 | && mv -v docs/landing/index.html docs/landing/css docs/landing/js dist \ 10 | && rm -v mdbook \ 11 | && rm -v mdbook.tgz 12 | -------------------------------------------------------------------------------- /docs/en/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ['Arijit Basu '] 3 | title = 'xplr book' 4 | description = 'A hackable, minimal, fast TUI file explorer' 5 | src = 'src' 6 | language = 'en' 7 | 8 | [output.html] 9 | site-url = '/xplr/en/' 10 | git-repository-url = 'https://github.com/sayanarijit/xplr' 11 | edit-url-template = 'https://github.com/sayanarijit/xplr/edit/main/docs/en/{path}' 12 | default-theme = 'dark' 13 | preferred-dark-theme = 'coal' 14 | 15 | [output.linkcheck] 16 | optional = true 17 | -------------------------------------------------------------------------------- /docs/en/src/key-bindings.md: -------------------------------------------------------------------------------- 1 | # Key Bindings 2 | 3 | Key bindings define how each keyboard input will be handled while in a specific 4 | [mode][4]. 5 | 6 | See the [Default key bindings][1] for example. 7 | 8 | To configure or work with key bindings, visit [Configure Key Bindings][2]. 9 | 10 | In case you need help debugging key bindings or to understand the system DYI 11 | way, refer to the [Debug Key Bindings][3] guide. 12 | 13 | [1]: default-key-bindings.md 14 | [2]: configure-key-bindings.md 15 | [3]: debug-key-bindings.md 16 | [4]: modes.md 17 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ sayanarijit ] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: 6 | ko_fi: 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | use lazy_static::lazy_static; 4 | use xdg::BaseDirectories; 5 | 6 | lazy_static! { 7 | pub static ref BASE_DIRS: BaseDirectories = BaseDirectories::new(); 8 | } 9 | 10 | pub fn home_dir() -> Option { 11 | home::home_dir() 12 | } 13 | 14 | pub fn config_dir() -> Option { 15 | BASE_DIRS.get_config_home() 16 | } 17 | 18 | pub fn runtime_dir() -> PathBuf { 19 | let Some(dir) = BASE_DIRS.get_runtime_directory().ok() else { 20 | return env::temp_dir(); 21 | }; 22 | dir.clone() 23 | } 24 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Why dynamic linking? 2 | # See https://github.com/sayanarijit/xplr/issues/309 3 | 4 | [target.x86_64-unknown-linux-gnu] 5 | rustflags = ["-C", "link-args=-rdynamic"] 6 | 7 | [target.aarch64-unknown-linux-gnu] 8 | rustflags = ["-C", "linker=aarch64-linux-gnu-gcc", "-C", "link-args=-rdynamic"] 9 | 10 | [target.aarch64-linux-android] 11 | rustflags = ["-C", "linker=aarch64-linux-android-clang", "-C", "link-args=-rdynamic", "-C", "default-linker-libraries"] 12 | 13 | [target.arm-unknown-linux-gnueabihf] 14 | rustflags = ["-C", "linker=arm-linux-gnueabihf-gcc", "-C", "link-args=-rdynamic"] 15 | -------------------------------------------------------------------------------- /docs/en/src/post-install.md: -------------------------------------------------------------------------------- 1 | # Post Install 2 | 3 | Once [installed][1], use the following steps to setup and run xplr. 4 | 5 | ## Create the customizable config file 6 | 7 | ```bash 8 | mkdir -p ~/.config/xplr 9 | 10 | version="$(xplr --version | awk '{print $2}')" 11 | 12 | echo "version = '${version:?}'" > ~/.config/xplr/init.lua 13 | ``` 14 | 15 | Then 16 | **[copy from here][2]** 17 | and remove / comment out what you don't want to customize. 18 | 19 | ## Run 20 | 21 | ``` 22 | xplr 23 | ``` 24 | 25 | [1]: install.md 26 | [2]: https://github.com/sayanarijit/xplr/blob/main/src/init.lua 27 | [3]: upgrade-guide.md 28 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | See [install.md](./docs/en/src/install.md#build-from-source) 4 | 5 | Note: xplr ships with vendored luajit. If the platform can't compile this, 6 | you need to compile using `--no-default-features` argument to avoid using 7 | vendored luajit, so that you can static link luajit yourself. 8 | 9 | # Release 10 | 11 | The final binary `target/release/xplr` can be shipped with the following assets 12 | 13 | - [License](./LICENSE) 14 | - [Desktop Entry](./assets/desktop/xplr.desktop) 15 | - [Desktop Icons](./assets/icon/) 16 | - [Offline Docs](./docs/en/src) 17 | - [Lua Configuration Example](./src/init.lua) 18 | -------------------------------------------------------------------------------- /docs/en/src/input-operation.md: -------------------------------------------------------------------------------- 1 | # Input Operation 2 | 3 | Cursor based input operation is a [sum type][3] can be one of the following: 4 | 5 | - { SetCursor = int } 6 | - { InsertCharacter = str } 7 | - "GoToPreviousCharacter" 8 | - "GoToNextCharacter" 9 | - "GoToPreviousWord" 10 | - "GoToNextWord" 11 | - "GoToStart" 12 | - "GoToEnd" 13 | - "DeletePreviousCharacter" 14 | - "DeleteNextCharacter" 15 | - "DeletePreviousWord" 16 | - "DeleteNextWord" 17 | - "DeleteLine" 18 | - "DeleteTillEnd" 19 | 20 | ## Also See: 21 | 22 | - [Message][1] 23 | - [Full List of Messages][2] 24 | 25 | [1]: message.md 26 | [2]: messages.md 27 | [3]: sum-type.md 28 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1765204905, 6 | "narHash": "sha256-xuFDaEr2jAHRWezVlkRc7B1zadcaD4NGcGx2DbpoyDU=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "96ca3529c27caa2ba3e8c17327ec551193cb12dd", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "repo": "nixpkgs", 15 | "type": "github" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | } 22 | } 23 | }, 24 | "root": "root", 25 | "version": 7 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/nix-cache.yml: -------------------------------------------------------------------------------- 1 | name: "Push Binary Cache for Nix" 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: nixbuild/nix-quick-install-action@v19 14 | with: 15 | nix_conf: experimental-features = nix-command flakes 16 | - uses: cachix/cachix-action@v11 17 | with: 18 | name: xplr 19 | authtoken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 20 | - run: nix profile install . 21 | - name: Run tests 22 | run: | 23 | xplr --version 24 | -------------------------------------------------------------------------------- /docs/en/src/concept.md: -------------------------------------------------------------------------------- 1 | # Concept 2 | 3 | These are the concepts that make xplr probably the most hackable terminal file 4 | explorer. 5 | 6 | - [Key Bindings][1] 7 | - [Node Type][2] 8 | - [Layout][3] 9 | - [Mode][4] 10 | - [Message][5] 11 | - [Borders][6] 12 | - [Style][7] 13 | - [Sorting][8] 14 | - [Filtering][9] 15 | - [Column Renderer][10] 16 | - [Lua Function Calls][11] 17 | - [Environment Variables and Pipes][12] 18 | 19 | [1]: key-bindings.md 20 | [2]: node-type.md 21 | [3]: layout.md 22 | [4]: mode.md 23 | [5]: message.md 24 | [6]: borders.md 25 | [7]: style.md 26 | [8]: sorting.md 27 | [9]: filtering.md 28 | [10]: column-renderer.md 29 | [11]: lua-function-calls.md 30 | [12]: environment-variables-and-pipes.md 31 | -------------------------------------------------------------------------------- /src/yaml.rs: -------------------------------------------------------------------------------- 1 | use serde_yaml::with::singleton_map_recursive; 2 | pub use serde_yaml::Result; 3 | pub use serde_yaml::Value; 4 | 5 | pub fn to_string(value: &T) -> Result 6 | where 7 | T: ?Sized + serde::Serialize, 8 | { 9 | let mut vec = Vec::with_capacity(128); 10 | let mut serializer = serde_yaml::Serializer::new(&mut vec); 11 | 12 | singleton_map_recursive::serialize(&value, &mut serializer)?; 13 | String::from_utf8(vec).map_err(serde::ser::Error::custom) 14 | } 15 | 16 | pub fn from_str<'de, T>(s: &'de str) -> Result 17 | where 18 | T: serde::Deserialize<'de>, 19 | { 20 | let deserializer = serde_yaml::Deserializer::from_str(s); 21 | singleton_map_recursive::deserialize(deserializer) 22 | } 23 | -------------------------------------------------------------------------------- /docs/en/src/node-type.md: -------------------------------------------------------------------------------- 1 | # Node Type 2 | 3 | A node-type contains the following fields: 4 | 5 | - [meta][4] 6 | - [style][5] 7 | 8 | ### meta 9 | 10 | Type: mapping of string and string 11 | 12 | A meta field can contain custom metadata about a node. By default, the "icon" 13 | metadata is set for the [directory][1], [file][2], and 14 | [symlink][3] nodes. 15 | 16 | Example: 17 | 18 | ```lua 19 | xplr.config.node_types.file = { 20 | meta = { 21 | icon = "f", 22 | foo = "bar", 23 | } 24 | } 25 | ``` 26 | 27 | ## Also See: 28 | 29 | - [xplr.config.node_types][6] 30 | 31 | [1]: node_types.md#xplrconfignode_typesdirectorymetaicon 32 | [2]: node_types.md#xplrconfignode_typesfilemetaicon 33 | [3]: node_types.md#xplrconfignode_typessymlinkmetaicon 34 | [4]: #meta 35 | [5]: style.md 36 | [6]: node_types.md 37 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: xplr 2 | version: git 3 | summary: A hackable, minimal, fast TUI file explorer 4 | description: | 5 | xplr is a terminal UI based file explorer 6 | that aims to increase our terminal productivity by being a flexible, 7 | interactive orchestrator for the ever growing awesome command-line 8 | utilities that work with the file-system. 9 | source-code: https://github.com/sayanarijit/xplr 10 | issues: https://github.com/sayanarijit/xplr/issues 11 | website: https://xplr.dev/ 12 | 13 | base: core20 14 | grade: devel # must be 'stable' to release into candidate/stable channels 15 | confinement: devmode # use 'strict' once you have the right plugs and slots 16 | 17 | 18 | parts: 19 | xplr: 20 | plugin: rust 21 | source: . 22 | 23 | apps: 24 | xplr: 25 | command: bin/xplr 26 | 27 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | #![allow(clippy::from_over_into)] 3 | #![allow(clippy::unnecessary_wraps)] 4 | 5 | pub mod app; 6 | pub mod cli; 7 | pub mod compat; 8 | pub mod config; 9 | pub mod directory_buffer; 10 | pub mod dirs; 11 | pub mod event_reader; 12 | pub mod explorer; 13 | pub mod input; 14 | pub mod lua; 15 | pub mod msg; 16 | pub mod node; 17 | pub mod path; 18 | pub mod permissions; 19 | pub mod pipe; 20 | pub mod pwd_watcher; 21 | pub mod runner; 22 | pub mod search; 23 | pub mod ui; 24 | pub mod yaml; 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | 29 | use super::*; 30 | 31 | #[test] 32 | fn test_upgrade_guide_has_latest_version() { 33 | let guide = include_str!("../docs/en/src/upgrade-guide.md"); 34 | assert!(guide.contains(app::VERSION)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/en/src/borders.md: -------------------------------------------------------------------------------- 1 | # Borders 2 | 3 | xplr allows customizing the shape and style of the borders. 4 | 5 | ### Border 6 | 7 | A border is a [sum type][2] that can be one of the following: 8 | 9 | - "Top" 10 | - "Right" 11 | - "Bottom" 12 | - "Left" 13 | 14 | ### Border Type 15 | 16 | A border type is a [sum type][2] that can be one of the following: 17 | 18 | - "Plain" 19 | - "Rounded" 20 | - "Double" 21 | - "Thick" 22 | 23 | ### Border Style 24 | 25 | The [style][1] of the borders. 26 | 27 | ## Example 28 | 29 | ```lua 30 | xplr.config.general.panel_ui.default.borders = { "Top", "Right", "Bottom", "Left" } 31 | xplr.config.general.panel_ui.default.border_type = "Thick" 32 | xplr.config.general.panel_ui.default.border_style.fg = "Black" 33 | xplr.config.general.panel_ui.default.border_style.bg = "Gray" 34 | ``` 35 | 36 | [1]: style.md#style 37 | [2]: sum-type.md 38 | -------------------------------------------------------------------------------- /src/msg/out/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::app::{Command, Task}; 4 | 5 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 6 | pub enum MsgOut { 7 | ExplorePwdAsync, 8 | ExploreParentsAsync, 9 | Refresh, 10 | ClearScreen, 11 | Debug(String), 12 | Call(Command), 13 | Call0(Command), 14 | CallSilently(Command), 15 | CallSilently0(Command), 16 | CallLua(String), 17 | CallLuaSilently(String), 18 | LuaEval(String), 19 | LuaEvalSilently(String), 20 | EnableMouse, 21 | DisableMouse, 22 | ToggleMouse, 23 | StartFifo(String), 24 | StopFifo, 25 | ToggleFifo(String), 26 | ScrollUp, 27 | ScrollDown, 28 | ScrollUpHalf, 29 | ScrollDownHalf, 30 | Quit, 31 | PrintPwdAndQuit, 32 | PrintFocusPathAndQuit, 33 | PrintSelectionAndQuit, 34 | PrintResultAndQuit, 35 | PrintAppStateAndQuit, 36 | Enqueue(Task), 37 | } 38 | -------------------------------------------------------------------------------- /src/directory_buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::node::Node; 2 | use serde::{Deserialize, Serialize}; 3 | use time::OffsetDateTime; 4 | 5 | #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] 6 | pub struct DirectoryBuffer { 7 | pub parent: String, 8 | pub nodes: Vec, 9 | pub total: usize, 10 | pub focus: usize, 11 | 12 | #[serde(skip, default = "now")] 13 | pub explored_at: OffsetDateTime, 14 | } 15 | 16 | impl DirectoryBuffer { 17 | pub fn new(parent: String, nodes: Vec, focus: usize) -> Self { 18 | let total = nodes.len(); 19 | Self { 20 | parent, 21 | nodes, 22 | total, 23 | focus, 24 | explored_at: now(), 25 | } 26 | } 27 | 28 | pub fn focused_node(&self) -> Option<&Node> { 29 | self.nodes.get(self.focus) 30 | } 31 | } 32 | 33 | fn now() -> OffsetDateTime { 34 | OffsetDateTime::now_local() 35 | .ok() 36 | .unwrap_or_else(OffsetDateTime::now_utc) 37 | } 38 | -------------------------------------------------------------------------------- /docs/en/src/alternatives.md: -------------------------------------------------------------------------------- 1 | # Alternatives 2 | 3 | These are the alternative TUI/CLI file managers/explorers you might want to check out (in no particular order). 4 | 5 | - [nnn][1] 6 | - [vifm][2] 7 | - [ranger][3] 8 | - [lf][4] 9 | - [joshuto][5] 10 | - [fff][6] 11 | - [mc][7] 12 | - [broot][8] 13 | - [hunter][9] 14 | - [noice][10] 15 | - [clifm][11] 16 | - [clifm][12] (non curses) 17 | - [felix][14] 18 | - [yazi][15] 19 | 20 | [add more][13] 21 | 22 | [1]: https://github.com/jarun/nnn/ 23 | [2]: https://github.com/vifm/vifm 24 | [3]: https://github.com/ranger/ranger 25 | [4]: https://github.com/gokcehan/lf 26 | [5]: https://github.com/kamiyaa/joshuto 27 | [6]: https://github.com/dylanaraps/fff 28 | [7]: https://github.com/MidnightCommander/mc 29 | [8]: https://github.com/Canop/broot 30 | [9]: https://github.com/rabite0/hunter 31 | [10]: https://git.2f30.org/noice/ 32 | [11]: https://github.com/pasqu4le/clifm 33 | [12]: https://github.com/leo-arch/clifm 34 | [13]: https://github.com/sayanarijit/xplr/edit/dev/docs/en/src/alternatives.md 35 | [14]: https://github.com/kyoheiu/felix 36 | [15]: https://github.com/sxyazi/yazi 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Arijit Basu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/en/src/mode.md: -------------------------------------------------------------------------------- 1 | # Mode 2 | 3 | A mode contains the following information: 4 | 5 | - [name][5] 6 | - [help][6] 7 | - [extra_help][7] 8 | - [key_bindings][9] 9 | - [layout][10] 10 | - [prompt][13] 11 | 12 | ### name 13 | 14 | Type: string 15 | 16 | This is the name of the mode visible in the help menu. 17 | 18 | ### help 19 | 20 | Type: nullable string 21 | 22 | If specified, the help menu will display this instead of the auto generated 23 | mappings. 24 | 25 | ### extra_help 26 | 27 | Type: nullable string 28 | 29 | If specified, the help menu will display this along-side the auto generated 30 | help menu. 31 | 32 | ### key_bindings 33 | 34 | Type: [Key Bindings][8] 35 | 36 | The key bindings available in that mode. 37 | 38 | ### layout 39 | 40 | Type: nullable [Layout][11] 41 | 42 | If specified, this layout will be used to render the UI. 43 | 44 | ### prompt 45 | 46 | Type: nullable string 47 | 48 | If set, this prompt will be displayed in the input buffer when in this mode. 49 | 50 | ## Also See: 51 | 52 | - [xplr.config.modes][12] 53 | 54 | [5]: #name 55 | [6]: #help 56 | [7]: #extra_help 57 | [8]: configure-key-bindings.md#key-bindings 58 | [9]: #key_bindings 59 | [10]: #layout 60 | [11]: layout.md#layout 61 | [12]: modes.md 62 | [13]: #prompt 63 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Book 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | deploy_en: 13 | name: Deploy book on gh-pages 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | # - name: Install mdBook 19 | # uses: peaceiris/actions-mdbook@v1 20 | - name: Render book 21 | run: | 22 | # From cloudflare pages 23 | curl -L https://github.com/rust-lang/mdBook/releases/download/v0.4.15/mdbook-v0.4.15-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 24 | mv docs/CNAME dist 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | allow_empty_commit: true 30 | keep_files: false 31 | publish_dir: dist 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | PUBLISH_BRANCH: gh-pages 35 | PUBLISH_DIR: dist 36 | -------------------------------------------------------------------------------- /docs/en/src/awesome-integrations.md: -------------------------------------------------------------------------------- 1 | # Awesome Integrations 2 | 3 | Here's a list of awesome xplr integrations that you might want to check out. 4 | 5 | If none of the following integrations work for you, you can create your own and 6 | [let us know][1]. 7 | 8 | ### Editor 9 | 10 | - [**fm-nvim**][10] Neovim plugin that lets you use your favorite terminal file managers from within Neovim. 11 | - [**vim-floaterm**][6] xplr integrated in vim-floaterm (Neo)vim plugin. 12 | - [**xplr.vim**][5] Pick files in Vim using xplr. 13 | 14 | ### Github 15 | 16 | - [**gh-xplr**][13] Explore GitHub repos using xplr via GitHub CLI. 17 | 18 | ### Shell 19 | 20 | - [**powerlevel10k**][7] Powerlevel10k prompt for xplr shell. 21 | 22 | ### Security Tools 23 | 24 | - [**gpg-tui**][8] Import GPG certificates using xplr. 25 | 26 | ## Also See: 27 | 28 | - [Awesome Hacks][11] 29 | - [Awesome Plugins][12] 30 | 31 | [1]: https://github.com/sayanarijit/xplr/discussions/categories/show-and-tell 32 | [2]: #editor 33 | [3]: #shell 34 | [4]: #security-tools 35 | [5]: https://github.com/sayanarijit/xplr.vim 36 | [6]: https://github.com/voldikss/vim-floaterm#xplr 37 | [7]: https://github.com/romkatv/powerlevel10k/blob/191d1b89e325ee3b6d2d75a394654aaf4f077a7c/internal/p10k.zsh#L4756-L4768 38 | [8]: https://github.com/orhun/gpg-tui#importreceive 39 | [10]: https://github.com/is0n/fm-nvim 40 | [11]: awesome-hacks.md 41 | [12]: awesome-plugins.md 42 | [13]: https://github.com/sayanarijit/gh-xplr 43 | -------------------------------------------------------------------------------- /docs/en/src/style.md: -------------------------------------------------------------------------------- 1 | # Style 2 | 3 | A style object contains the following information: 4 | 5 | - [fg][1] 6 | - [bg][2] 7 | - [add_modifiers][3] 8 | - [sub_modifiers][4] 9 | 10 | ### fg 11 | 12 | Type: nullable [Color][5] 13 | 14 | The foreground color. 15 | 16 | ### bg 17 | 18 | Type: nullable [Color][5] 19 | 20 | The background color. 21 | 22 | ### add_modifiers 23 | 24 | Type: nullable list of [Modifier][6] 25 | 26 | Modifiers to add. 27 | 28 | ### sub_modifiers 29 | 30 | Type: nullable list of [Modifier][6] 31 | 32 | Modifiers to remove. 33 | 34 | ## Color 35 | 36 | Color is a [sum type][7] that can be one of the following: 37 | 38 | - "Reset" 39 | - "Black" 40 | - "Red" 41 | - "Green" 42 | - "Yellow" 43 | - "Blue" 44 | - "Magenta" 45 | - "Cyan" 46 | - "Gray" 47 | - "DarkGray" 48 | - "LightRed" 49 | - "LightGreen" 50 | - "LightYellow" 51 | - "LightBlue" 52 | - "LightMagenta" 53 | - "LightCyan" 54 | - "White" 55 | - { Rgb = { int, int, int } } 56 | - { Indexed = int } 57 | 58 | ## Modifier 59 | 60 | Modifier is a [sum type][7] that can be one of the following: 61 | 62 | - "Bold" 63 | - "Dim" 64 | - "Italic" 65 | - "Underlined" 66 | - "SlowBlink" 67 | - "RapidBlink" 68 | - "Reversed" 69 | - "Hidden" 70 | - "CrossedOut" 71 | 72 | ## Example 73 | 74 | ```lua 75 | xplr.config.general.prompt.style.fg = "Red" 76 | xplr.config.general.prompt.style.bg = { Rgb = { 100, 150, 200 } } 77 | xplr.config.general.prompt.style.add_modifiers = { "Bold", "Italic" } 78 | xplr.config.general.prompt.style.sub_modifiers = { "Hidden" } 79 | ``` 80 | 81 | [1]: #fg 82 | [2]: #bg 83 | [3]: #add_modifiers 84 | [4]: #sub_modifiers 85 | [5]: #color 86 | [6]: #modifier 87 | [7]: sum-type.md 88 | -------------------------------------------------------------------------------- /docs/en/src/installing-plugins.md: -------------------------------------------------------------------------------- 1 | # Installing Plugins 2 | 3 | One way to install plugins is to use a plugin manager like [dtomvan/xpm.xplr][1]. 4 | 5 | But you can also install and manage plugins manually. 6 | 7 | ## Install Manually 8 | 9 | - Add the following line in `~/.config/xplr/init.lua` 10 | 11 | ```lua 12 | local home = os.getenv("HOME") 13 | package.path = home 14 | .. "/.config/xplr/plugins/?/init.lua;" 15 | .. home 16 | .. "/.config/xplr/plugins/?.lua;" 17 | .. package.path 18 | ``` 19 | 20 | - Clone the plugin 21 | 22 | ```bash 23 | mkdir -p ~/.config/xplr/plugins 24 | 25 | git clone https://github.com/sayanarijit/material-landscape2.xplr ~/.config/xplr/plugins/material-landscape2 26 | ``` 27 | 28 | - Require the module in `~/.config/xplr/init.lua` 29 | 30 | ```lua 31 | require("material-landscape2").setup() 32 | 33 | -- The setup arguments might differ for different plugins. 34 | -- Visit the project README for setup instructions. 35 | ``` 36 | 37 | ## Luarocks Support 38 | 39 | Some plugins may require [luarocks][2] to work. 40 | 41 | Setup luarocks with the following steps: 42 | 43 | - Install luarocks (via your package managers or follow the [official guide][2]). 44 | - Add `eval "$(luarocks path --lua-version 5.1)"` in your `.bashrc` or `.zshrc`. 45 | - Add the following lines in `~/.config/xplr/init.lua` 46 | 47 | ```lua 48 | package.path = os.getenv("LUA_PATH") .. ";" .. package.path 49 | package.cpath = os.getenv("LUA_CPATH") .. ";" .. package.cpath 50 | ``` 51 | 52 | Now you can install packages using luarocks. Be sure to append `--lua-version`. 53 | 54 | Example: 55 | 56 | ```bash 57 | luarocks install luafilesystem --local --lua-version 5.1 58 | ``` 59 | 60 | [1]: https://github.com/dtomvan/xpm.xplr 61 | [2]: https://luarocks.org 62 | -------------------------------------------------------------------------------- /docs/en/src/message.md: -------------------------------------------------------------------------------- 1 | # Message 2 | 3 | You can think of xplr as a server. Just like web servers listen to HTTP 4 | requests, xplr listens to messages. 5 | 6 | A message is a [sum type][9] that can have [these possible values][1]. 7 | 8 | You can send these messages to an xplr session in the following ways: 9 | 10 | - Via command-line (currently during start-up only, using `--on-load`) 11 | - Via [key bindings][2] 12 | - Via [Lua function calls][3] 13 | - Via shell command using the [input pipe][4] 14 | - Via socket (coming soon) 15 | 16 | ### Format 17 | 18 | To send messages using the [key bindings][2] or [Lua function calls][3], 19 | messages are represented in [Lua][5] syntax. 20 | 21 | For example: 22 | 23 | - `"Quit"` 24 | - `{ FocusPath = "/path/to/file" }` 25 | - `{ Call = { command = "bash", args = { "-c", "read -p test" } } }` 26 | 27 | However, to send messages using the [input pipe][4], they need to be 28 | represented using [YAML][6] (or [JSON][7]) syntax. 29 | 30 | For example: 31 | 32 | - `Quit` 33 | - `FocusPath: "/path/to/file"` 34 | - `Call: { command: bash, args: ["-c", "read -p test"] }` 35 | 36 | Use `"$XPLR" -m TEMPLATE [VALUE]...` command-line option to safely format 37 | `TEMPLATE` into a valid message. If uses [jf][8] to parse and render the 38 | template. And `$XPLR` (rather than `xplr`) makes sure that the correct version 39 | of the binary is being used. 40 | 41 | For example: 42 | 43 | - `"$XPLR" -m Quit` 44 | - `"$XPLR" -m 'FocusPath: %q' "/path/to/file"` 45 | - `"$XPLR" -m 'Call: { command: %q, args: [%*q] }' bash -c "read -p test"` 46 | 47 | ## Also See: 48 | 49 | - [Full List of Messages][1] 50 | 51 | [1]: messages.md 52 | [2]: key-bindings.md 53 | [3]: lua-function-calls.md 54 | [4]: environment-variables-and-pipes.md#input-pipe 55 | [5]: https://www.lua.org/ 56 | [6]: http://yaml.org/ 57 | [7]: https://www.json.org 58 | [8]: https://github.com/sayanarijit/jf 59 | [9]: sum-type.md 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you are new to GitHub, visit the [first-contributions instructions](https://github.com/firstcontributions/first-contributions/blob/master/README.md) to learn how to contribute on GitHub. 2 | 3 | If you are new to Rust, I recommend you to go through [the book](https://doc.rust-lang.org/book). 4 | 5 | To find issues you can help with, go though the list of [good first issues](https://github.com/sayanarijit/xplr/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) or issues labeled with [help wanted](https://github.com/sayanarijit/xplr/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22). 6 | 7 | If you have something to suggest or issues to report, you can [create a new issue](https://github.com/sayanarijit/xplr/issues/new). Be sure to explain in details the context and the outcome that you are lookign for. If reporting bugs, provide basic information like you OS version, `xplr` version, window manager and the terminal you are using. 8 | 9 | Once found or created an issue, let us know that you want to work on it by commenting in the issue. 10 | 11 | 12 | Development Guideline 13 | --------------------- 14 | 15 | Assuming that you have mentioned the issue you are working on and that you have forked and cloned the repository locally, in order to contribute by making changes to the code follow the steps below: 16 | 17 | - Make changes to the code 18 | 19 | - Test the changes 20 | 21 | ```bash 22 | cargo run 23 | 24 | cargo test 25 | ``` 26 | 27 | - Format code and get linting helps 28 | 29 | ```bash 30 | cargo fmt 31 | 32 | cargo clippy 33 | ``` 34 | 35 | - Commit, push and finally create a pull request. 36 | 37 | - Don't worry if you make a mistake, we will provide constructive feedback and guidance to improve the pull request. 38 | 39 | - If you encounter any situation that violates our [code of conduct](https://github.com/sayanarijit/xplr/blob/main/CODE_OF_CONDUCT.md) please report it to sayanarijit@gmail.com. 40 | -------------------------------------------------------------------------------- /docs/en/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # A hackable, minimal, fast TUI file explorer 2 | 3 | - [Introduction][1] 4 | - [Quickstart][2] 5 | - [Install][3] 6 | - [Post Install][4] 7 | - [Configuration][5] 8 | - [General][6] 9 | - [Node Types][10] 10 | - [Layouts][9] 11 | - [Modes][7] 12 | - [Concept][32] 13 | - [Sum Type][42] 14 | - [Key Bindings][27] 15 | - [Configure Key Bindings][28] 16 | - [Default Key Bindings][14] 17 | - [Debug Key Bindings][29] 18 | - [Node Type][33] 19 | - [Layout][34] 20 | - [Mode][35] 21 | - [Message][8] 22 | - [Full List of Messages][38] 23 | - [Input Operation][39] 24 | - [Borders][31] 25 | - [Style][11] 26 | - [Searching][41] 27 | - [Sorting][12] 28 | - [Filtering][13] 29 | - [Column Renderer][26] 30 | - [Lua Function Calls][36] 31 | - [xplr.util][40] 32 | - [Environment Variables and Pipes][37] 33 | - [Awesome Hacks][30] 34 | - [Plugin][15] 35 | - [Installing Plugins][16] 36 | - [Writing Plugins][17] 37 | - [Awesome Plugins][18] 38 | - [Integration][19] 39 | - [Awesome Integrations][20] 40 | - [Alternatives][22] 41 | - [Upgrade Guide][23] 42 | 43 | [1]: introduction.md 44 | [2]: quickstart.md 45 | [3]: install.md 46 | [4]: post-install.md 47 | [5]: configuration.md 48 | [6]: general-config.md 49 | [7]: modes.md 50 | [8]: message.md 51 | [9]: layouts.md 52 | [10]: node_types.md 53 | [11]: style.md 54 | [12]: sorting.md 55 | [13]: filtering.md 56 | [14]: default-key-bindings.md 57 | [15]: plugin.md 58 | [16]: installing-plugins.md 59 | [17]: writing-plugins.md 60 | [18]: awesome-plugins.md 61 | [19]: integration.md 62 | [20]: awesome-integrations.md 63 | [22]: alternatives.md 64 | [23]: upgrade-guide.md 65 | [26]: column-renderer.md 66 | [27]: key-bindings.md 67 | [28]: configure-key-bindings.md 68 | [29]: debug-key-bindings.md 69 | [30]: awesome-hacks.md 70 | [31]: borders.md 71 | [32]: concept.md 72 | [33]: node-type.md 73 | [34]: layout.md 74 | [35]: mode.md 75 | [36]: lua-function-calls.md 76 | [37]: environment-variables-and-pipes.md 77 | [38]: messages.md 78 | [39]: input-operation.md 79 | [40]: xplr.util.md 80 | [41]: searching.md 81 | [42]: sum-type.md 82 | -------------------------------------------------------------------------------- /docs/en/src/sorting.md: -------------------------------------------------------------------------------- 1 | # Sorting 2 | 3 | xplr supports sorting paths by different properties. The sorting mechanism 4 | works like a pipeline, which in visible in the `Sort & filter` panel. 5 | 6 | Example: 7 | 8 | ``` 9 | size↑ › [i]rel↓ › [c]dir↑ › [c]file↑ › sym↑ 10 | ``` 11 | 12 | This line means that the nodes visible in the table will be first sorted by 13 | it's size, then by case insensitive relative path, then by the 14 | canonical (symlink resolved) type of the node, and finally by whether or not 15 | the node is a symlink. 16 | 17 | The arrows denote the order. 18 | 19 | Each part of this pipeline is called [Node Sorter Applicable][1]. 20 | 21 | ## Node Sorter Applicable 22 | 23 | It contains the following information: 24 | 25 | - [sorter][2] 26 | - [reverse][3] 27 | 28 | ### sorter 29 | 30 | A sorter is a [sum type][4] that can be one of the following: 31 | 32 | - "ByRelativePath" 33 | - "ByIRelativePath" 34 | - "ByExtension" 35 | - "ByIsDir" 36 | - "ByIsFile" 37 | - "ByIsSymlink" 38 | - "ByIsBroken" 39 | - "ByIsReadonly" 40 | - "ByMimeEssence" 41 | - "BySize" 42 | - "ByCreated" 43 | - "ByLastModified" 44 | - "ByCanonicalAbsolutePath" 45 | - "ByICanonicalAbsolutePath" 46 | - "ByCanonicalExtension" 47 | - "ByCanonicalIsDir" 48 | - "ByCanonicalIsFile" 49 | - "ByCanonicalIsReadonly" 50 | - "ByCanonicalMimeEssence" 51 | - "ByCanonicalSize" 52 | - "ByCanonicalCreated" 53 | - "ByCanonicalLastModified" 54 | - "BySymlinkAbsolutePath" 55 | - "ByISymlinkAbsolutePath" 56 | - "BySymlinkExtension" 57 | - "BySymlinkIsDir" 58 | - "BySymlinkIsFile" 59 | - "BySymlinkIsReadonly" 60 | - "BySymlinkMimeEssence" 61 | - "BySymlinkSize" 62 | - "BySymlinkCreated" 63 | - "BySymlinkLastModified" 64 | 65 | ### reverse 66 | 67 | Type: boolean 68 | 69 | It defined the direction of the order. 70 | 71 | ## Example 72 | 73 | ```lua 74 | xplr.config.general.initial_sorting = { 75 | { sorter = "ByCanonicalIsDir", reverse = true }, 76 | { sorter = "ByIRelativePath", reverse = false }, 77 | } 78 | ``` 79 | 80 | This snippet defines the initial sorting logic to be applied when xplr loads. 81 | 82 | [1]: #node-sorter-applicable 83 | [2]: #sorter 84 | [3]: #reverse 85 | [4]: sum-type.md 86 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "xplr - A hackable, minimal, fast TUI file explorer"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs"; 6 | }; 7 | 8 | outputs = inputs@{ self, nixpkgs, ... }: 9 | let 10 | lib = nixpkgs.lib; 11 | 12 | darwin = [ "x86_64-darwin" "aarch64-darwin" ]; 13 | linux = [ "x86_64-linux" "x86_64-linux-musl" "aarch64-linux" "aarch64-linux-android" "i86_64-linux" ]; 14 | allSystems = darwin ++ linux; 15 | 16 | forEachSystem = systems: f: lib.genAttrs systems (system: f system); 17 | forAllSystems = forEachSystem allSystems; 18 | in 19 | { 20 | packages = forAllSystems (system: 21 | let 22 | pkgs = import nixpkgs { inherit system; }; 23 | in 24 | rec { 25 | # e.g. nix build .#xplr 26 | xplr = pkgs.rustPlatform.buildRustPackage rec { 27 | name = "xplr"; 28 | src = ./.; 29 | cargoLock = { 30 | lockFile = ./Cargo.lock; 31 | }; 32 | }; 33 | 34 | # e.g. nix build .#cross.x86_64-linux-musl.xplr --impure 35 | cross = forEachSystem (lib.filter (sys: sys != system) allSystems) (targetSystem: 36 | let 37 | crossPkgs = import nixpkgs { localSystem = system; crossSystem = targetSystem; }; 38 | in 39 | { inherit (crossPkgs) xplr; } 40 | ); 41 | } 42 | ); 43 | defaultPackage = forAllSystems (system: self.packages.${system}.xplr); 44 | devShells = forAllSystems (system: 45 | let 46 | pkgs = import nixpkgs { inherit system; }; 47 | devRequirements = with pkgs; [ 48 | gcc 49 | gnumake 50 | clippy 51 | rustc 52 | cargo 53 | rustfmt 54 | rust-analyzer 55 | ]; 56 | in 57 | { 58 | default = pkgs.mkShell { 59 | RUST_BACKTRACE = 1; 60 | 61 | # For cross compilation 62 | NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM = 1; 63 | 64 | buildInputs = devRequirements; 65 | packages = devRequirements; 66 | }; 67 | } 68 | ); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /assets/icon/xplr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/landing/js/scripts.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | // init feather icons 4 | feather.replace(); 5 | 6 | // init tooltip & popovers 7 | $('[data-toggle="tooltip"]').tooltip(); 8 | $('[data-toggle="popover"]').popover(); 9 | 10 | //page scroll 11 | $('a.page-scroll').bind('click', function (event) { 12 | var $anchor = $(this); 13 | $('html, body').stop().animate({ 14 | scrollTop: $($anchor.attr('href')).offset().top - 20 15 | }, 1000); 16 | event.preventDefault(); 17 | }); 18 | 19 | // slick slider 20 | $('.slick-about').slick({ 21 | slidesToShow: 1, 22 | slidesToScroll: 1, 23 | autoplay: true, 24 | autoplaySpeed: 3000, 25 | dots: true, 26 | arrows: false 27 | }); 28 | 29 | //toggle scroll menu 30 | var scrollTop = 0; 31 | $(window).scroll(function () { 32 | var scroll = $(window).scrollTop(); 33 | //adjust menu background 34 | if (scroll > 80) { 35 | if (scroll > scrollTop) { 36 | $('.smart-scroll').addClass('scrolling').removeClass('up'); 37 | } else { 38 | $('.smart-scroll').addClass('up'); 39 | } 40 | } else { 41 | // remove if scroll = scrollTop 42 | $('.smart-scroll').removeClass('scrolling').removeClass('up'); 43 | } 44 | 45 | scrollTop = scroll; 46 | 47 | // adjust scroll to top 48 | if (scroll >= 600) { 49 | $('.scroll-top').addClass('active'); 50 | } else { 51 | $('.scroll-top').removeClass('active'); 52 | } 53 | return false; 54 | }); 55 | 56 | // scroll top top 57 | $('.scroll-top').click(function () { 58 | $('html, body').stop().animate({ 59 | scrollTop: 0 60 | }, 1000); 61 | }); 62 | 63 | /**Theme switcher - DEMO PURPOSE ONLY */ 64 | $('.switcher-trigger').click(function () { 65 | $('.switcher-wrap').toggleClass('active'); 66 | }); 67 | $('.color-switcher ul li').click(function () { 68 | var color = $(this).attr('data-color'); 69 | $('#theme-color').attr("href", "css/" + color + ".css"); 70 | $('.color-switcher ul li').removeClass('active'); 71 | $(this).addClass('active'); 72 | }); 73 | }); -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bin]] 2 | name = 'xplr' 3 | 4 | [[bench]] 5 | name = 'criterion' 6 | harness = false 7 | path = './benches/criterion.rs' 8 | 9 | [package] 10 | name = 'xplr' 11 | version = '1.1.0' 12 | authors = ['Arijit Basu '] 13 | edition = '2021' 14 | description = 'A hackable, minimal, fast TUI file explorer' 15 | license = 'MIT' 16 | readme = 'README.md' 17 | repository = 'https://github.com/sayanarijit/xplr' 18 | homepage = 'https://xplr.dev' 19 | documentation = 'https://xplr.dev/en' 20 | keywords = ['terminal', 'file', 'explorer', 'manager', 'tui'] 21 | categories = ['command-line-interface', 'command-line-utilities'] 22 | include = ['src/**/*', 'docs/en/src/**/*', 'LICENSE', 'README.md'] 23 | 24 | [dependencies] 25 | libc = "0.2" 26 | humansize = "2.1" 27 | natord = "1.0" 28 | anyhow = "1.0" 29 | serde_yaml = "0.9" 30 | ansi-to-tui = "7.0" 31 | regex = "1.12" 32 | gethostname = "1.1" 33 | serde_json = "1.0" 34 | path-absolutize = "3.1" 35 | which = "8.0" 36 | nu-ansi-term = "0.50" 37 | textwrap = "0.16" 38 | snailquote = "0.3" 39 | skim = "0.18" 40 | time = { version = "0.3", features = [ 41 | "serde", 42 | "local-offset", 43 | "formatting", 44 | "macros", 45 | ] } 46 | jf = "0.6" 47 | xdg = "3.0" 48 | home = "0.5" 49 | rayon = "1.11" 50 | 51 | [dependencies.lscolors] 52 | version = "0.21" 53 | default-features = false 54 | features = ["nu-ansi-term"] 55 | 56 | [dependencies.lazy_static] 57 | version = "1.5" 58 | default-features = false 59 | 60 | [dependencies.mime_guess] 61 | version = "2.0" 62 | default-features = false 63 | 64 | [dependencies.tui] 65 | version = "0.29" 66 | default-features = false 67 | features = ['crossterm', 'serde'] 68 | package = 'ratatui' 69 | 70 | [dependencies.serde] 71 | version = "1.0" 72 | features = [] 73 | default-features = false 74 | 75 | [dependencies.indexmap] 76 | version = "2.12" 77 | features = ['serde'] 78 | 79 | [dependencies.mlua] 80 | version = "0.11" 81 | features = ['luajit', 'serialize', 'send'] 82 | 83 | [dependencies.tui-input] 84 | version = "0.14" 85 | features = ['serde'] 86 | 87 | [dev-dependencies] 88 | criterion = "0.6" 89 | assert_cmd = "2.0" 90 | 91 | [profile.release] 92 | lto = true 93 | codegen-units = 1 94 | panic = 'abort' 95 | strip = true 96 | 97 | [features] 98 | default = ["vendored-lua"] 99 | vendored-lua = ["mlua/vendored"] 100 | -------------------------------------------------------------------------------- /docs/en/src/layouts.md: -------------------------------------------------------------------------------- 1 | ### Layouts 2 | 3 | xplr layouts define the structure of the UI, i.e. how many panel we see, 4 | placement and size of the panels, how they look etc. 5 | 6 | This is configuration exposed via the `xplr.config.layouts` API. 7 | 8 | `xplr.config.layouts.builtin` contain some built-in panels which can be 9 | overridden, but you can't add or remove panels in it. 10 | 11 | You can add new panels in `xplr.config.layouts.custom`. 12 | 13 | ##### Example: Defining Custom Layout 14 | 15 | ```lua 16 | xplr.config.layouts.builtin.default = { 17 | Horizontal = { 18 | config = { 19 | margin = 1, 20 | horizontal_margin = 1, 21 | vertical_margin = 1, 22 | constraints = { 23 | { Percentage = 50 }, 24 | { Percentage = 50 }, 25 | } 26 | }, 27 | splits = { 28 | "Table", 29 | "HelpMenu", 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | Result: 36 | 37 | ``` 38 | ╭ /home ─────────────╮╭ Help [default] ────╮ 39 | │ ╭─── path ││. show hidden │ 40 | │ ├▸[ð Desktop/] ││/ search │ 41 | │ ├ ð Documents/ ││: action │ 42 | │ ├ ð Downloads/ ││? global help │ 43 | │ ├ ð GitHub/ ││G go to bottom │ 44 | │ ├ ð Music/ ││V select/unselect│ 45 | │ ├ ð Pictures/ ││ctrl duplicate as │ 46 | │ ├ ð Public/ ││ctrl next visit │ 47 | ╰────────────────────╯╰────────────────────╯ 48 | ``` 49 | 50 | #### xplr.config.layouts.builtin.default 51 | 52 | The default layout 53 | 54 | Type: [Layout](https://xplr.dev/en/layout) 55 | 56 | #### xplr.config.layouts.builtin.no_help 57 | 58 | The layout without help menu 59 | 60 | Type: [Layout](https://xplr.dev/en/layout) 61 | 62 | #### xplr.config.layouts.builtin.no_selection 63 | 64 | The layout without selection panel 65 | 66 | Type: [Layout](https://xplr.dev/en/layout) 67 | 68 | #### xplr.config.layouts.builtin.no_help_no_selection 69 | 70 | The layout without help menu and selection panel 71 | 72 | Type: [Layout](https://xplr.dev/en/layout) 73 | 74 | #### xplr.config.layouts.custom 75 | 76 | This is where you can define custom layouts 77 | 78 | Type: mapping of the following key-value pairs: 79 | 80 | - key: string 81 | - value: [Layout](https://xplr.dev/en/layout) 82 | 83 | Example: 84 | 85 | ```lua 86 | xplr.config.layouts.custom.example = "Nothing" -- Show a blank screen 87 | xplr.config.general.initial_layout = "example" -- Load the example layout 88 | ``` 89 | -------------------------------------------------------------------------------- /src/pwd_watcher.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Task; 2 | use crate::app::{ExternalMsg, MsgIn}; 3 | use anyhow::{Error, Result}; 4 | use std::path::PathBuf; 5 | use std::sync::mpsc::{Receiver, Sender}; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | pub fn keep_watching( 10 | pwd: &str, 11 | tx_msg_in: Sender, 12 | rx_pwd_watcher: Receiver, 13 | ) -> Result<()> { 14 | let mut pwd = PathBuf::from(pwd); 15 | let mut last_modified = pwd.metadata().and_then(|m| m.modified())?; 16 | 17 | thread::spawn(move || loop { 18 | if let Ok(new_pwd) = rx_pwd_watcher.try_recv() { 19 | pwd = PathBuf::from(new_pwd); 20 | last_modified = pwd 21 | .metadata() 22 | .and_then(|m| m.modified()) 23 | .unwrap_or(last_modified) 24 | } else if let Err(e) = pwd 25 | .metadata() 26 | .map_err(Error::new) 27 | .and_then(|m| m.modified().map_err(Error::new)) 28 | .and_then(|modified| { 29 | if modified != last_modified { 30 | last_modified = modified; 31 | let msg = MsgIn::External(ExternalMsg::ExplorePwdAsync); 32 | tx_msg_in.send(Task::new(msg, None)).map_err(Error::new) 33 | } else { 34 | thread::sleep(Duration::from_secs(1)); 35 | Result::Ok(()) 36 | } 37 | }) 38 | { 39 | let msg = MsgIn::External(ExternalMsg::LogError(e.to_string())); 40 | tx_msg_in.send(Task::new(msg, None)).unwrap_or_default(); 41 | thread::sleep(Duration::from_secs(1)); 42 | } 43 | }); 44 | Ok(()) 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | use std::sync::mpsc; 51 | 52 | #[test] 53 | fn test_pwd_watcher() { 54 | let (tx_msg_in, rx_msg_in) = mpsc::channel(); 55 | let (_, rx_pwd_watcher) = mpsc::channel(); 56 | 57 | let result = keep_watching("/tmp", tx_msg_in, rx_pwd_watcher); 58 | 59 | assert!(result.is_ok()); 60 | 61 | let file = "/tmp/__xplr_pwd_watcher_test__"; 62 | std::fs::write(file, "test").unwrap(); 63 | std::fs::remove_file(file).unwrap(); 64 | 65 | let task = rx_msg_in.recv().unwrap(); 66 | let msg = MsgIn::External(ExternalMsg::ExplorePwdAsync); 67 | assert_eq!(task, Task::new(msg, None)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/pipe.rs: -------------------------------------------------------------------------------- 1 | use crate::app::ExternalMsg; 2 | use crate::yaml; 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs; 6 | use std::io::prelude::*; 7 | use std::path::PathBuf; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | pub struct Pipe { 11 | pub path: String, 12 | pub msg_in: String, 13 | pub selection_out: String, 14 | pub result_out: String, 15 | pub directory_nodes_out: String, 16 | pub global_help_menu_out: String, 17 | pub logs_out: String, 18 | pub history_out: String, 19 | } 20 | 21 | impl Pipe { 22 | pub fn from_session_path(path: &str) -> Result { 23 | let path = PathBuf::from(path).join("pipe"); 24 | 25 | let msg_in = path.join("msg_in").to_string_lossy().to_string(); 26 | 27 | let selection_out = path.join("selection_out").to_string_lossy().to_string(); 28 | 29 | let result_out = path.join("result_out").to_string_lossy().to_string(); 30 | 31 | let directory_nodes_out = path 32 | .join("directory_nodes_out") 33 | .to_string_lossy() 34 | .to_string(); 35 | 36 | let global_help_menu_out = path 37 | .join("global_help_menu_out") 38 | .to_string_lossy() 39 | .to_string(); 40 | 41 | let logs_out = path.join("logs_out").to_string_lossy().to_string(); 42 | 43 | let history_out = path.join("history_out").to_string_lossy().to_string(); 44 | 45 | Ok(Self { 46 | path: path.to_string_lossy().to_string(), 47 | msg_in, 48 | selection_out, 49 | result_out, 50 | directory_nodes_out, 51 | global_help_menu_out, 52 | logs_out, 53 | history_out, 54 | }) 55 | } 56 | } 57 | 58 | pub fn read_all(pipe: &str, delimiter: char) -> Result> { 59 | let mut file = fs::OpenOptions::new() 60 | .read(true) 61 | .write(true) 62 | .create(false) 63 | .open(pipe)?; 64 | 65 | let mut in_str = String::new(); 66 | file.read_to_string(&mut in_str)?; 67 | file.set_len(0)?; 68 | 69 | if !in_str.is_empty() { 70 | let mut msgs = vec![]; 71 | for msg in in_str.trim_matches(delimiter).split(delimiter) { 72 | if !msg.is_empty() { 73 | msgs.push(yaml::from_str(msg)?); 74 | } 75 | } 76 | Ok(msgs) 77 | } else { 78 | Ok(vec![]) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/en/src/searching.md: -------------------------------------------------------------------------------- 1 | # Searching 2 | 3 | xplr supports searching paths using different algorithm. The search mechanism 4 | generally appears between filters and sorters in the `Sort & filter` panel. 5 | 6 | Example: 7 | 8 | ``` 9 | fzy:foo↓ 10 | ``` 11 | 12 | This line means that the nodes visible on the table are being filtered using the 13 | [fuzzy matching][1] algorithm on the input `foo`. The arrow means that ranking based 14 | ordering is being applied, i.e. [sorters][2] are being ignored. 15 | 16 | ## Node Searcher Applicable 17 | 18 | Node Searcher contains the following fields: 19 | 20 | - [pattern][3] 21 | - [recoverable_focus][4] 22 | - [algorithm][5] 23 | - [unordered][7] 24 | - [exact_mode][10] 25 | - [rank_criteria][11] 26 | 27 | ### pattern 28 | 29 | The patterns used to search. 30 | 31 | Type: string 32 | 33 | ### recoverable_focus 34 | 35 | Where to focus when search is cancelled. 36 | 37 | Type: nullable string 38 | 39 | ### algorithm 40 | 41 | Search algorithm to use. Defaults to the value set in 42 | [xplr.config.general.search.algorithm][8]. 43 | 44 | It can be one of the following: 45 | 46 | - Fuzzy 47 | - Regex 48 | 49 | ### unordered 50 | 51 | Whether to skip ordering the search result by algorithm based ranking. Defaults 52 | to the value set in [xplr.config.general.search.unordered][9]. 53 | 54 | Type: boolean 55 | 56 | ### exact_mode 57 | 58 | Whether to search in exact mode. Defaults to `false`. 59 | 60 | Type: boolean 61 | 62 | ### rank_criteria 63 | 64 | Ranking criteria to use. Defaults to `nil`. 65 | 66 | Type: nullable list of the following options: 67 | 68 | - Score 69 | - NegScore 70 | - Begin 71 | - NegBegin 72 | - End 73 | - NegEnd 74 | - Length 75 | - NegLength 76 | - Index 77 | - NegIndex 78 | 79 | ## Example: 80 | 81 | ```lua 82 | local searcher = { 83 | pattern = "pattern to search", 84 | recoverable_focus = "/path/to/focus/on/cancel", 85 | algorithm = "Fuzzy", 86 | unordered = false, 87 | exact_mode = false, 88 | rank_criteria = { "Score", "Begin", "End", "Length" }, 89 | } 90 | 91 | xplr.util.explore({ searcher = searcher }) 92 | ``` 93 | 94 | See [xplr.util.explore][6]. 95 | 96 | [1]: https://en.wikipedia.org/wiki/Approximate_string_matching 97 | [2]: sorting.md 98 | [3]: #pattern 99 | [4]: #recoverable_focus 100 | [5]: #algorithm 101 | [6]: xplr.util.md#xplrutilexplore 102 | [7]: #unordered 103 | [8]: general-config.md#xplrconfiggeneralsearchalgorithm 104 | [9]: general-config.md#xplrconfiggeneralsearchunordered 105 | [10]: #exact_mode 106 | [11]: #rank_criteria 107 | -------------------------------------------------------------------------------- /docs/en/src/filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | xplr supports filtering paths by different properties. The filtering mechanism 4 | works like a pipeline, which in visible in the `Sort & filter` panel. 5 | 6 | Example: 7 | 8 | ``` 9 | rel!^. › [i]abs=~abc › [i]rel!~xyz 10 | ``` 11 | 12 | This line means that the nodes visible on the table will first be filtered by 13 | the condition: _relative path does not start with `.`_, then by the condition: 14 | _absolute path contains `abc` (case insensitive)_, and finally by the 15 | condition: _relative path does not contain `xyz`_ (case insensitive). 16 | 17 | Each part of this pipeline is called [Node Filter Applicable][1]. 18 | 19 | ## Node Filter Applicable 20 | 21 | It contains the following information: 22 | 23 | - [filter][2] 24 | - [input][3] 25 | 26 | ### filter 27 | 28 | A filter is a [sum type][5] that can be one of the following: 29 | 30 | - "RelativePathIs" 31 | - "RelativePathIsNot" 32 | - "IRelativePathIs" 33 | - "IRelativePathIsNot" 34 | - "RelativePathDoesStartWith" 35 | - "RelativePathDoesNotStartWith" 36 | - "IRelativePathDoesStartWith" 37 | - "IRelativePathDoesNotStartWith" 38 | - "RelativePathDoesContain" 39 | - "RelativePathDoesNotContain" 40 | - "IRelativePathDoesContain" 41 | - "IRelativePathDoesNotContain" 42 | - "RelativePathDoesEndWith" 43 | - "RelativePathDoesNotEndWith" 44 | - "IRelativePathDoesEndWith" 45 | - "IRelativePathDoesNotEndWith" 46 | - "RelativePathDoesMatchRegex" 47 | - "RelativePathDoesNotMatchRegex" 48 | - "IRelativePathDoesMatchRegex" 49 | - "IRelativePathDoesNotMatchRegex" 50 | - "AbsolutePathIs" 51 | - "AbsolutePathIsNot" 52 | - "IAbsolutePathIs" 53 | - "IAbsolutePathIsNot" 54 | - "AbsolutePathDoesStartWith" 55 | - "AbsolutePathDoesNotStartWith" 56 | - "IAbsolutePathDoesStartWith" 57 | - "IAbsolutePathDoesNotStartWith" 58 | - "AbsolutePathDoesContain" 59 | - "AbsolutePathDoesNotContain" 60 | - "IAbsolutePathDoesContain" 61 | - "IAbsolutePathDoesNotContain" 62 | - "AbsolutePathDoesEndWith" 63 | - "AbsolutePathDoesNotEndWith" 64 | - "IAbsolutePathDoesEndWith" 65 | - "IAbsolutePathDoesNotEndWith" 66 | - "AbsolutePathDoesMatchRegex" 67 | - "AbsolutePathDoesNotMatchRegex" 68 | - "IAbsolutePathDoesMatchRegex" 69 | - "IAbsolutePathDoesNotMatchRegex" 70 | 71 | ### input 72 | 73 | Type: string 74 | 75 | The input for the condition. 76 | 77 | ## Example: 78 | 79 | ```lua 80 | ToggleNodeFilter = { 81 | filter = "RelativePathDoesNotStartWith", 82 | input = "." 83 | } 84 | ``` 85 | 86 | Here, `ToggleNodeFilter` is a [message][4] that adds or removes 87 | (toggles) the filter applied. 88 | 89 | [1]: #node-filter-applicable 90 | [2]: #filter 91 | [3]: #input 92 | [4]: message.md 93 | [5]: sum-type.md 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ▸[▓▓ xplr] 3 |

4 | 5 |

6 | A hackable, minimal, fast TUI file explorer 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 |

18 | 19 | https://user-images.githubusercontent.com/11632726/166747867-8a4573f2-cb2f-43a6-a23d-c99fc30c6594.mp4 20 | 21 |

22 | 23 |

24 | [Install] 25 | [Documentation] 26 | [Hacks] 27 | [Plugins] 28 | [Integrations] 29 |

30 | 31 | xplr is a terminal UI based file explorer that aims to increase our terminal 32 | productivity by being a flexible, interactive orchestrator for the ever growing 33 | awesome command-line utilities that work with the file-system. 34 | 35 | To achieve its goal, xplr strives to be a fast, minimal and more importantly, 36 | hackable file explorer. 37 | 38 | xplr is not meant to be a replacement for the standard shell commands or the 39 | GUI file managers. Rather, it aims to [integrate them all][14] and expose an 40 | intuitive, scriptable, [keyboard controlled][2], 41 | [real-time visual interface][1], also being an ideal candidate for [further 42 | integration][15], enabling you to achieve insane terminal productivity. 43 | 44 | ## Introductions & Reviews 45 | 46 | - [[VIDEO] XPLR: Insanely Hackable Lua File Manager ~ Brodie Robertson](https://youtu.be/MaVRtYh1IRU) 47 | 48 | - [[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) 49 | 50 | - [[Article] FOSSPicks - Linux Magazine]() 51 | 52 | ## Packaging 53 | 54 | Package maintainers please refer to the [RELEASE.md](./RELEASE.md). 55 | 56 | 57 | 58 | ## Backers 59 | 60 | 61 | 62 | [1]: https://xplr.dev/en/layouts 63 | [2]: https://xplr.dev/en/configure-key-bindings 64 | [14]: https://xplr.dev/en/awesome-plugins#integration 65 | [15]: https://xplr.dev/en/awesome-integrations 66 | -------------------------------------------------------------------------------- /docs/en/src/debug-key-bindings.md: -------------------------------------------------------------------------------- 1 | # Debug Key Bindings 2 | 3 | If you need help debugging or understanding key bindings DYI way, you can 4 | create a `test.lua` file with the following script, launch xplr with 5 | `xplr --extra-config test.lua`, press `#` and play around. 6 | 7 | ```lua 8 | -- The global key bindings inherited by all the modes. 9 | xplr.config.general.global_key_bindings = { 10 | on_key = { 11 | esc = { 12 | help = "escape", 13 | messages = { 14 | { LogInfo = "global on_key(esc) called" }, 15 | "PopMode", 16 | }, 17 | }, 18 | ["ctrl-c"] = { 19 | help = "terminate", 20 | messages = { 21 | "Terminate", 22 | }, 23 | }, 24 | }, 25 | } 26 | 27 | -- Press `#` to enter the `debug key bindings` mode. 28 | xplr.config.modes.builtin.default.key_bindings.on_key["#"] = { 29 | help = "test", 30 | messages = { 31 | "PopMode", 32 | { SwitchModeCustom = "debug_key_bindings" }, 33 | }, 34 | } 35 | 36 | -- The `debug key bindings` mode. 37 | xplr.config.modes.custom.debug_key_bindings = { 38 | name = "debug key bindings", 39 | key_bindings = { 40 | on_key = { 41 | ["1"] = { 42 | messages = { 43 | { LogInfo = "on_key(1) called" }, 44 | }, 45 | }, 46 | a = { 47 | messages = { 48 | { LogInfo = "on_key(a) called" }, 49 | }, 50 | }, 51 | ["`"] = { 52 | messages = { 53 | { LogInfo = "on_key(`) called" }, 54 | }, 55 | }, 56 | tab = { 57 | messages = { 58 | { LogInfo = "on_key(tab) called" }, 59 | }, 60 | }, 61 | f1 = { 62 | messages = { 63 | { LogInfo = "on_key(f1) called" }, 64 | }, 65 | }, 66 | }, 67 | on_alphabet = { 68 | messages = { 69 | { LogInfo = "on_alphabet called" }, 70 | }, 71 | }, 72 | on_number = { 73 | messages = { 74 | { LogInfo = "on_number called" }, 75 | }, 76 | }, 77 | -- on_alphanumeric = { 78 | -- messages = { 79 | -- { LogInfo = "on_alphanumeric called" }, 80 | -- }, 81 | -- }, 82 | on_special_character = { 83 | messages = { 84 | { LogInfo = "on_special_character called" }, 85 | }, 86 | }, 87 | -- on_character = { 88 | -- messages = { 89 | -- { LogInfo = "on_character called" }, 90 | -- }, 91 | -- }, 92 | on_navigation = { 93 | messages = { 94 | { LogInfo = "on_navigation called" }, 95 | }, 96 | }, 97 | on_function = { 98 | messages = { 99 | { LogInfo = "on_function called" }, 100 | }, 101 | }, 102 | default = { 103 | messages = { 104 | { LogInfo = "default called" }, 105 | }, 106 | }, 107 | }, 108 | } 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/en/src/writing-plugins.md: -------------------------------------------------------------------------------- 1 | # Writing Plugins 2 | 3 | Anyone who can write [Lua][1] code, can write xplr plugins. 4 | 5 | Just follow the instructions and best practices: 6 | 7 | ## Naming 8 | 9 | xplr plugins are named using hiphen (`-`) separated words that may also include 10 | integers. They will be plugged using the `require()` function in Lua. 11 | 12 | ## Structure 13 | 14 | A minimal plugin should confirm to the following structure: 15 | 16 | ``` 17 | . 18 | ├── README.md 19 | └── init.lua 20 | ``` 21 | 22 | You can also use [this template][2]. 23 | 24 | ### README.md 25 | 26 | This is where you document what the plugin does, how to use it, etc. 27 | 28 | ### init.lua 29 | 30 | This file is executed to load the plugin. It should expose a `setup()` 31 | function, which will be used by the users to setup the plugin. 32 | 33 | Example: 34 | 35 | ```lua 36 | local function setup(args) 37 | local xplr = xplr 38 | -- do stuff with xplr 39 | end 40 | 41 | return { setup = setup } 42 | ``` 43 | 44 | ## Publishing 45 | 46 | When publishing plugins on GitHub or other repositories, it's a best practice 47 | to append `.xplr` to the name to make them distinguishable. Similar to the 48 | `*.nvim` naming convention for [Neovim][3] plugins. 49 | 50 | Finally, after publishing, don't hesitate to 51 | [let us know][4]. 52 | 53 | ## Best practices 54 | 55 | - Try not to execute a lot of commands at startup, it may make xplr slow to 56 | start. 57 | - When executing commands, prefer `Call0` over `Call`, `BashExec0` over 58 | `BashExec` and so on. File names may contain newline characters 59 | (e.g. `foo$'\n'bar`). 60 | - File names may also contain quotes. Avoid writing directly to 61 | `$XPLR_PIPE_MSG_IN`. Use `xplr -m` / `xplr --pipe-msg-in` instead. 62 | - Check for empty variables using the syntax `${FOO:?}` or use a default value 63 | `${FOO:-defaultvalue}`. 64 | 65 | ## Examples 66 | 67 | Visit [Awesome Plugins][5] for xplr plugin examples. 68 | 69 | ## Also See 70 | 71 | - [Tip: A list of hacks yet to make it as Lua plugins][15] 72 | - [Tip: Some UI and theming tips][12] 73 | - [Tutorial: Adding a New Mode][6] 74 | - [Example: Using Environment Variables and Pipes][7] 75 | - [Example: Using Lua Function Calls][8] 76 | - [Example: Defining Custom Layout][9] 77 | - [Example: Customizing Table Renderer][10] 78 | - [Example: Render a custom dynamic table][11] 79 | - [Example: Implementing a directory visit counter][16] 80 | 81 | [1]: https://www.lua.org 82 | [2]: https://github.com/sayanarijit/plugin-template1.xplr 83 | [3]: https://neovim.io 84 | [4]: https://github.com/sayanarijit/xplr/discussions/categories/show-and-tell 85 | [5]: awesome-plugins.md 86 | [6]: configure-key-bindings.md#tutorial-adding-a-new-mode 87 | [7]: environment-variables-and-pipes.md#example-using-environment-variables-and-pipes 88 | [8]: lua-function-calls.md#example-using-lua-function-calls 89 | [9]: layout.md#example-defining-custom-layout 90 | [10]: column-renderer.md#example-customizing-table-renderer 91 | [11]: layout.md#example-render-a-custom-dynamic-table 92 | [12]: https://github.com/sayanarijit/xplr/discussions/274 93 | [15]: awesome-hacks.md 94 | [16]: https://github.com/sayanarijit/xplr/discussions/529#discussioncomment-4073734 95 | -------------------------------------------------------------------------------- /docs/en/src/sum-type.md: -------------------------------------------------------------------------------- 1 | # Sum Type 2 | 3 | > This section isn't specific to xplr. However, since xplr configuration makes 4 | > heavy use of this particular data type, even though it isn't available in 5 | > most of the mainstream programming languages (yet), making it a wild or 6 | > unfamiliar concept for many, it's worth doing a quick introduction here. 7 | > 8 | > If you're already familiar with [Sum Type / Tagged Union][1] (e.g. Rust's 9 | > enum), you can skip ahead. 10 | 11 | While reading this doc, you'll come across some data types like [Layout][2], 12 | [Color][4], [Message][3] etc. that says something like "x is a sum type that 13 | can be any of the following", and then you'll see a list of strings and/or lua 14 | tables just below. 15 | 16 | Yes, they are actually sum types, i.e. they can be any of the given set of 17 | tagged variants listed there. 18 | 19 | Notice the word "be". Unlike classes or structs (aka product types), they can't 20 | "have" values, they can only "be" the value, or rather, be one of the possible 21 | set of values. 22 | 23 | Also notice the word "tagged". Unlike the single variant `null`, or the dual 24 | variant `boolean` types, the variants of sum types are tagged (i.e. named), and 25 | may further have, or be, value or values of any data type. 26 | 27 | A simple example of a sum type is an enum. Many programming languages have 28 | them, but only a few modern programming languages allow nesting other types 29 | into a sum type. 30 | 31 | ```rust 32 | enum Color { 33 | Red, 34 | Green, 35 | } 36 | ``` 37 | 38 | Here, `Color` can be one of two possible set of values: `Red` and `Green`, just 39 | like `boolean`, but unlike `boolean`, being tagged allows `Color` to have more 40 | than two variants if required, by changing the definition. 41 | 42 | e.g. 43 | 44 | ```rust 45 | enum Color { 46 | Red, 47 | Green, 48 | Blue, 49 | } 50 | ``` 51 | 52 | We'd document it here as: 53 | 54 | > Result is a sum type that can be one of the following: 55 | > 56 | > - "Red" 57 | > - "Green" 58 | > - "Blue" 59 | 60 | But some languages (like Rust, Haskell, Elm etc.) go even further, allowing us 61 | to associate each branch of the enum with further nested types like: 62 | 63 | ```rust 64 | enum Layout { 65 | Table, 66 | HelpMenu, 67 | Horizontal { 68 | config: LayoutConfig, // A product type (similar to class/struct) 69 | splits: Vec // A list of "Layout"s (i.e. list of sum types) 70 | }, 71 | } 72 | ``` 73 | 74 | Here, as we can see, unlike the first example, some of `Layout`'s possible 75 | variants can have further nested types associated with them. Note that 76 | `Horizontal` here can have a sum type (e.g. enum), or a product type (e.g. 77 | class/struct), or both (any number of them actually) nested in it. But the 78 | nested values will only exist when `Layout` is `Horizontal`. 79 | 80 | We'd document it here as: 81 | 82 | > Layout is a sum type that can be one of the following: 83 | > 84 | > - "Table" 85 | > - "HelpMenu" 86 | > - { Horizontal = { config = Layout Config, splits = { Layout, ... } } 87 | 88 | And then we'd go on documenting whatever `Layout Config` is. 89 | 90 | So, there you go. This is exactly what sum types are - glorified enums that can 91 | have nested types in each branch. 92 | 93 | [1]: https://en.wikipedia.org/wiki/Tagged_union 94 | [2]: layout.md 95 | [3]: message.md 96 | [4]: style.md#color 97 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use skim::item::RankBuilder; 5 | use skim::prelude::{ExactOrFuzzyEngineFactory, RegexEngineFactory}; 6 | use skim::{MatchEngine, MatchEngineFactory, SkimItem}; 7 | 8 | pub struct PathItem { 9 | path: String, 10 | } 11 | 12 | impl From for PathItem { 13 | fn from(value: String) -> Self { 14 | Self { path: value } 15 | } 16 | } 17 | 18 | impl SkimItem for PathItem { 19 | fn text(&self) -> std::borrow::Cow<'_, str> { 20 | std::borrow::Cow::from(&self.path) 21 | } 22 | } 23 | 24 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] 25 | pub enum RankCriteria { 26 | Score, 27 | NegScore, 28 | Begin, 29 | NegBegin, 30 | End, 31 | NegEnd, 32 | Length, 33 | NegLength, 34 | Index, 35 | NegIndex, 36 | } 37 | 38 | impl Into for RankCriteria { 39 | fn into(self) -> skim::prelude::RankCriteria { 40 | match self { 41 | Self::Score => skim::prelude::RankCriteria::Score, 42 | Self::NegScore => skim::prelude::RankCriteria::NegScore, 43 | Self::Begin => skim::prelude::RankCriteria::Begin, 44 | Self::NegBegin => skim::prelude::RankCriteria::NegBegin, 45 | Self::End => skim::prelude::RankCriteria::End, 46 | Self::NegEnd => skim::prelude::RankCriteria::NegEnd, 47 | Self::Length => skim::prelude::RankCriteria::Length, 48 | Self::NegLength => skim::prelude::RankCriteria::NegLength, 49 | Self::Index => skim::prelude::RankCriteria::Index, 50 | Self::NegIndex => skim::prelude::RankCriteria::NegIndex, 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] 56 | #[serde(deny_unknown_fields)] 57 | pub enum SearchAlgorithm { 58 | #[default] 59 | Fuzzy, 60 | Regex, 61 | } 62 | 63 | impl SearchAlgorithm { 64 | pub fn toggle(self) -> Self { 65 | match self { 66 | Self::Fuzzy => Self::Regex, 67 | Self::Regex => Self::Fuzzy, 68 | } 69 | } 70 | 71 | pub fn engine( 72 | &self, 73 | pattern: &str, 74 | exact_mode: bool, 75 | rank_criteria: Option>, 76 | ) -> Box { 77 | let criteria = rank_criteria.map_or_else( 78 | || { 79 | vec![ 80 | RankCriteria::Score, 81 | RankCriteria::Begin, 82 | RankCriteria::End, 83 | RankCriteria::Length, 84 | ] 85 | }, 86 | Into::into, 87 | ); 88 | 89 | let rank_builder = 90 | RankBuilder::new(criteria.into_iter().map(Into::into).collect()); 91 | 92 | match self { 93 | Self::Fuzzy => ExactOrFuzzyEngineFactory::builder() 94 | .exact_mode(exact_mode) 95 | .rank_builder(Arc::new(rank_builder)) 96 | .build() 97 | .create_engine(pattern), 98 | 99 | Self::Regex => RegexEngineFactory::builder() 100 | .rank_builder(Arc::new(rank_builder)) 101 | .build() 102 | .create_engine(pattern), 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /docs/en/src/node_types.md: -------------------------------------------------------------------------------- 1 | ### Node Types 2 | 3 | This section defines how to deal with different kinds of nodes (files, 4 | directories, symlinks etc.) based on their properties. 5 | 6 | One node can fall into multiple categories. For example, a node can have the 7 | _extension_ `md`, and also be a _file_. In that case, the properties from 8 | the more specific category i.e. _extension_ will be used. 9 | 10 | This can be configured using the `xplr.config.node_types` Lua API. 11 | 12 | #### xplr.config.node_types.directory.style 13 | 14 | The style for the directory nodes 15 | 16 | Type: [Style](https://xplr.dev/en/style) 17 | 18 | #### xplr.config.node_types.directory.meta.icon 19 | 20 | Metadata for the directory nodes. 21 | You can set as many metadata as you want. 22 | 23 | Type: nullable string 24 | 25 | Example: 26 | 27 | ```lua 28 | xplr.config.node_types.directory.meta.foo = "foo" 29 | xplr.config.node_types.directory.meta.bar = "bar" 30 | ``` 31 | 32 | #### xplr.config.node_types.file.style 33 | 34 | The style for the file nodes. 35 | 36 | Type: [Style](https://xplr.dev/en/style) 37 | 38 | #### xplr.config.node_types.file.meta.icon 39 | 40 | Metadata for the file nodes. 41 | You can set as many metadata as you want. 42 | 43 | Type: nullable string 44 | 45 | Example: 46 | 47 | ```lua 48 | xplr.config.node_types.file.meta.foo = "foo" 49 | xplr.config.node_types.file.meta.bar = "bar" 50 | ``` 51 | 52 | #### xplr.config.node_types.symlink.style 53 | 54 | The style for the symlink nodes. 55 | 56 | Type: [Style](https://xplr.dev/en/style) 57 | 58 | #### xplr.config.node_types.symlink.meta.icon 59 | 60 | Metadata for the symlink nodes. 61 | You can set as many metadata as you want. 62 | 63 | Type: nullable string 64 | 65 | Example: 66 | 67 | ```lua 68 | xplr.config.node_types.symlink.meta.foo = "foo" 69 | xplr.config.node_types.symlink.meta.bar = "bar" 70 | ``` 71 | 72 | #### xplr.config.node_types.mime_essence 73 | 74 | Metadata and style based on mime types. 75 | It is possible to use the wildcard `*` to match all mime sub types. It will 76 | be overwritten by the more specific sub types that are defined. 77 | 78 | Type: mapping of the following key-value pairs: 79 | 80 | - key: string 81 | - value: 82 | - key: string 83 | - value: [Node Type](https://xplr.dev/en/node-type) 84 | 85 | Example: 86 | 87 | ```lua 88 | xplr.config.node_types.mime_essence = { 89 | application = { 90 | -- application/* 91 | ["*"] = { meta = { icon = "a" } }, 92 | 93 | -- application/pdf 94 | pdf = { meta = { icon = "" }, style = { fg = "Blue" } }, 95 | 96 | -- application/zip 97 | zip = { meta = { icon = ""} }, 98 | }, 99 | } 100 | ``` 101 | 102 | #### xplr.config.node_types.extension 103 | 104 | Metadata and style based on extension. 105 | 106 | Type: mapping of the following key-value pairs: 107 | 108 | - key: string 109 | - value: [Node Type](https://xplr.dev/en/node-type) 110 | 111 | Example: 112 | 113 | ```lua 114 | xplr.config.node_types.extension.md = { meta = { icon = "" }, style = { fg = "Blue" } } 115 | xplr.config.node_types.extension.rs = { meta = { icon = "🦀" } } 116 | ``` 117 | 118 | #### xplr.config.node_types.special 119 | 120 | Metadata and style based on special file names. 121 | 122 | Type: mapping of the following key-value pairs: 123 | 124 | - key: string 125 | - value: [Node Type](https://xplr.dev/en/node-type) 126 | 127 | Example: 128 | 129 | ```lua 130 | xplr.config.node_types.special["Cargo.toml"] = { meta = { icon = "" } } 131 | xplr.config.node_types.special["Downloads"] = { meta = { icon = "" }, style = { fg = "Blue" } } 132 | ``` 133 | -------------------------------------------------------------------------------- /src/event_reader.rs: -------------------------------------------------------------------------------- 1 | use crate::app::Task; 2 | use crate::app::{ExternalMsg, InternalMsg, MsgIn}; 3 | use crate::input::Key; 4 | use anyhow::Error; 5 | use std::sync::mpsc; 6 | use std::sync::mpsc::{Receiver, Sender}; 7 | use std::thread; 8 | use std::time::Duration; 9 | use tui::crossterm::event::{self, Event, MouseEventKind}; 10 | 11 | pub(crate) struct EventReader { 12 | task_sender: Sender, 13 | stopper: Option<(Sender, Receiver<()>)>, 14 | } 15 | 16 | impl EventReader { 17 | pub(crate) fn new(task_sender: Sender) -> Self { 18 | Self { 19 | task_sender, 20 | stopper: None, 21 | } 22 | } 23 | 24 | pub(crate) fn start(&mut self) { 25 | let sender = self.task_sender.clone(); 26 | let (tx_stopper, rx_stopper) = mpsc::channel(); 27 | let (tx_ack, rx_ack) = mpsc::channel(); 28 | self.stopper = Some((tx_stopper, rx_ack)); 29 | 30 | thread::spawn(move || { 31 | keep_reading(sender, rx_stopper, tx_ack); 32 | }); 33 | } 34 | 35 | pub(crate) fn stop(&self) { 36 | if let Some((stopper, ack)) = &self.stopper { 37 | stopper.send(true).unwrap_or_default(); // Let's not panic when xplr stops. 38 | ack.recv().unwrap_or_default(); 39 | } 40 | } 41 | } 42 | 43 | fn keep_reading( 44 | tx_msg_in: Sender, 45 | rx_stopper: Receiver, 46 | tx_ack: Sender<()>, 47 | ) { 48 | loop { 49 | if rx_stopper.try_recv().unwrap_or(false) { 50 | tx_ack.send(()).unwrap(); 51 | break; 52 | } else if event::poll(std::time::Duration::from_millis(150)).unwrap_or_default() 53 | { 54 | // NOTE: The poll timeout need to stay low, else spawning sub subshell 55 | // and start typing immediately will cause panic. 56 | // To reproduce, press `:`, then press and hold `!`. 57 | let res = match event::read() { 58 | Ok(Event::Key(key)) => { 59 | let key = Key::from_event(key); 60 | let msg = MsgIn::Internal(InternalMsg::HandleKey(key)); 61 | tx_msg_in 62 | .send(Task::new(msg, Some(key))) 63 | .map_err(Error::new) 64 | } 65 | 66 | Ok(Event::Mouse(evt)) => match evt.kind { 67 | MouseEventKind::ScrollUp => { 68 | let msg = MsgIn::External(ExternalMsg::FocusPrevious); 69 | tx_msg_in.send(Task::new(msg, None)).map_err(Error::new) 70 | } 71 | 72 | MouseEventKind::ScrollDown => { 73 | let msg = MsgIn::External(ExternalMsg::FocusNext); 74 | tx_msg_in.send(Task::new(msg, None)).map_err(Error::new) 75 | } 76 | _ => Ok(()), 77 | }, 78 | 79 | Ok(Event::Resize(_, _)) => { 80 | let msg = MsgIn::External(ExternalMsg::Refresh); 81 | tx_msg_in.send(Task::new(msg, None)).map_err(Error::new) 82 | } 83 | 84 | Ok(Event::FocusGained) => Ok(()), 85 | Ok(Event::FocusLost) => Ok(()), 86 | 87 | Ok(Event::Paste(text)) => { 88 | let msg = MsgIn::External(ExternalMsg::BufferInput(text)); 89 | tx_msg_in.send(Task::new(msg, None)).map_err(Error::new) 90 | } 91 | 92 | Err(e) => Err(Error::new(e)), 93 | }; 94 | 95 | if let Err(e) = res { 96 | tx_msg_in 97 | .send(Task::new( 98 | MsgIn::External(ExternalMsg::LogError(e.to_string())), 99 | None, 100 | )) 101 | .unwrap_or_default(); // Let's not panic when xplr stops 102 | thread::sleep(Duration::from_secs(1)); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /benches/criterion.rs: -------------------------------------------------------------------------------- 1 | use crate::app; 2 | use crate::ui; 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use std::fs; 5 | use tui::backend::CrosstermBackend; 6 | use tui::crossterm::execute; 7 | use tui::crossterm::terminal as term; 8 | use tui::Terminal; 9 | use xplr::runner::get_tty; 10 | use xplr::*; 11 | 12 | const PWD: &str = "/tmp/xplr_bench"; 13 | 14 | fn navigation_benchmark(c: &mut Criterion) { 15 | fs::create_dir_all(PWD).unwrap(); 16 | (1..10000).for_each(|i| { 17 | fs::File::create(std::path::Path::new(PWD).join(i.to_string())).unwrap(); 18 | }); 19 | 20 | let lua = mlua::Lua::new(); 21 | let mut app = 22 | app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into()) 23 | .expect("failed to create app"); 24 | 25 | app = app 26 | .clone() 27 | .handle_task(app::Task::new( 28 | app::MsgIn::External(app::ExternalMsg::ChangeDirectory(PWD.into())), 29 | None, 30 | )) 31 | .unwrap(); 32 | 33 | c.bench_function("focus next item", |b| { 34 | b.iter(|| { 35 | app.clone() 36 | .handle_task(app::Task::new( 37 | app::MsgIn::External(app::ExternalMsg::FocusNext), 38 | None, 39 | )) 40 | .unwrap() 41 | }) 42 | }); 43 | 44 | c.bench_function("focus previous item", |b| { 45 | b.iter(|| { 46 | app.clone() 47 | .handle_task(app::Task::new( 48 | app::MsgIn::External(app::ExternalMsg::FocusPrevious), 49 | None, 50 | )) 51 | .unwrap() 52 | }) 53 | }); 54 | 55 | c.bench_function("focus first item", |b| { 56 | b.iter(|| { 57 | app.clone() 58 | .handle_task(app::Task::new( 59 | app::MsgIn::External(app::ExternalMsg::FocusFirst), 60 | None, 61 | )) 62 | .unwrap() 63 | }) 64 | }); 65 | 66 | c.bench_function("focus last item", |b| { 67 | b.iter(|| { 68 | app.clone() 69 | .handle_task(app::Task::new( 70 | app::MsgIn::External(app::ExternalMsg::FocusLast), 71 | None, 72 | )) 73 | .unwrap() 74 | }) 75 | }); 76 | 77 | c.bench_function("leave and enter directory", |b| { 78 | b.iter(|| { 79 | app.clone() 80 | .handle_task(app::Task::new( 81 | app::MsgIn::External(app::ExternalMsg::Back), 82 | None, 83 | )) 84 | .unwrap() 85 | .handle_task(app::Task::new( 86 | app::MsgIn::External(app::ExternalMsg::Enter), 87 | None, 88 | )) 89 | .unwrap() 90 | }) 91 | }); 92 | } 93 | 94 | fn draw_benchmark(c: &mut Criterion) { 95 | fs::create_dir_all(PWD).unwrap(); 96 | (1..10000).for_each(|i| { 97 | fs::File::create(std::path::Path::new(PWD).join(i.to_string())).unwrap(); 98 | }); 99 | 100 | let lua = mlua::Lua::new(); 101 | let mut ui = ui::UI::new(&lua); 102 | let mut app = 103 | app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into()) 104 | .expect("failed to create app"); 105 | 106 | app = app 107 | .clone() 108 | .handle_task(app::Task::new( 109 | app::MsgIn::External(app::ExternalMsg::ChangeDirectory(PWD.into())), 110 | None, 111 | )) 112 | .unwrap(); 113 | 114 | term::enable_raw_mode().unwrap(); 115 | let mut stdout = get_tty().unwrap(); 116 | // let mut stdout = stdout.lock(); 117 | execute!(stdout, term::EnterAlternateScreen).unwrap(); 118 | // let stdout = MouseTerminal::from(stdout); 119 | let backend = CrosstermBackend::new(stdout); 120 | let mut terminal = Terminal::new(backend).unwrap(); 121 | terminal.hide_cursor().unwrap(); 122 | 123 | c.bench_function("draw on terminal", |b| { 124 | b.iter(|| { 125 | terminal.draw(|f| ui.draw(f, &app)).unwrap(); 126 | }) 127 | }); 128 | 129 | terminal.clear().unwrap(); 130 | terminal.set_cursor_position((0, 0)).unwrap(); 131 | execute!(terminal.backend_mut(), term::LeaveAlternateScreen).unwrap(); 132 | term::disable_raw_mode().unwrap(); 133 | terminal.show_cursor().unwrap(); 134 | } 135 | 136 | criterion_group!(benches, navigation_benchmark, draw_benchmark); 137 | criterion_main!(benches); 138 | -------------------------------------------------------------------------------- /docs/en/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | xplr is a terminal UI based file explorer that aims to increase our terminal 4 | productivity by being a flexible, interactive orchestrator for the ever growing 5 | awesome command-line utilities that work with the file-system. 6 | 7 | To achieve its goal, xplr strives to be a fast, minimal and more importantly, 8 | hackable file explorer. 9 | 10 | xplr is not meant to be a replacement for the standard shell commands or the 11 | GUI file managers. Rather, it aims to [integrate them all][14] and expose an 12 | intuitive, scriptable, [keyboard controlled][2], 13 | [real-time visual interface][1], also being an ideal candidate for [further 14 | integration][15], enabling you to achieve insane terminal productivity. 15 | 16 | ## Concept 17 | 18 | ### Hackable 19 | 20 | xplr is built with configurability in mind. So it allows you to perform a vast 21 | set of operations and make it look and behave just the way you want. 22 | 23 | A few things you can do with the xplr configuration 24 | 25 | - [Hacks][16] 26 | - [Plugins][3] 27 | - [Integrations][15] 28 | 29 | ### Fast 30 | 31 | Although speed is not the primary concern, xplr is already fast enough so that 32 | you can take it out for a walk into your `node_modules` or `/nix/store` any 33 | time you want, and it will only get faster. Still, if you feel like it's 34 | somehow making you slow, just report it. Most probably we're just waiting for 35 | someone to complain. 36 | 37 | **Tip:** A quick and easy way to optimize the UI rendering is reducing the 38 | number of columns in the table. 39 | 40 | ### Minimal 41 | 42 | xplr is being referred to as a _File Explorer_, not a _File Manager_. This 43 | is because at the core, xplr is only an explorer, and [outsources][18] the file 44 | management operations to external commands. This helps xplr stay minimal, and 45 | focus only on doing what it does best. 46 | 47 | So, just like speed, minimalism isn't as as aggressively pursued as 48 | hackability. xplr simply prefers to stay minimal and looks for the opportunity 49 | to lose some kb if it makes sense. 50 | 51 | ## Features 52 | 53 | Some of the coolest features xplr provide beside the basic stuff: 54 | 55 | - [Embedded LuaJIT][5] for portability and extensibility. 56 | - [A simple modal system based on message passing][10] to control xplr session 57 | using: 58 | - [Keyboard inputs][11] 59 | - [Shell Commands][12] 60 | - [Lua Functions][13] 61 | - [Hooks][22] 62 | - Easy, typesafe message passing with `-m MSG` or `-M MSG` subcommands. 63 | - [Readline-like input buffer][9] with customizable behavior to read user 64 | inputs. 65 | - [Switchable recover mode][7] that saves you from doing unwanted things when 66 | in a hurry. 67 | - [Customizable layouts][1] with built-in panels. For e.g. 68 | - **Selection list** to show you the selected paths in real-time. 69 | - **Help menu** to show you the available keys bindings in each mode. 70 | - **Input & logs** to read input and display logs. 71 | - **Filter and sort pipeline** to show you the applied filters and sorters. 72 | - [Custom file properties][17] with custom colors can be displayed in the table. 73 | - [FIFO manager][19] to manage a FIFO file that can be used to 74 | [integrate with previewers][6]. 75 | - [Virtual root][21] with `--vroot` and `:v` key bindings. 76 | - **Different quit options:** 77 | - Quit with success without any output (`q`). 78 | - Quit with success and the result printed on stdout (`enter`). 79 | - Quit with success and the present working directory printed on stdout 80 | (`:` `q` `p`). 81 | - Quit with success and the path under focus printed on stdout 82 | (`:` `q` `f`). 83 | - Quit with success and the selection printed on stdout 84 | (`:` `q` `s`). 85 | - Quit with failure (`ctrl-c`). 86 | 87 | [1]: layouts.md 88 | [2]: configure-key-bindings.md 89 | [3]: awesome-plugins.md 90 | [4]: https://github.com/sayanarijit/xplr/tree/main/benches 91 | [5]: https://github.com/sayanarijit/xplr/discussions/183 92 | [6]: https://github.com/sayanarijit/xplr/pull/229 93 | [7]: modes.md#xplrconfigmodesbuiltinrecover 94 | [8]: default-key-bindings.md 95 | [9]: https://github.com/sayanarijit/xplr/pull/397 96 | [10]: messages.md 97 | [11]: configure-key-bindings.md 98 | [12]: environment-variables-and-pipes.md 99 | [13]: lua-function-calls.md 100 | [14]: awesome-plugins.md#integration 101 | [15]: awesome-integrations.md 102 | [16]: awesome-hacks.md 103 | [17]: node_types.md 104 | [18]: https://github.com/sayanarijit/xplr/blob/main/src/init.lua 105 | [19]: messages.md#startfifo 106 | [21]: messages.md#virtual-root 107 | [22]: configuration.md#hooks 108 | -------------------------------------------------------------------------------- /docs/en/src/modes.md: -------------------------------------------------------------------------------- 1 | ### Modes 2 | 3 | xplr is a modal file explorer. That means the users switch between different 4 | modes, each containing a different set of key bindings to avoid clashes. 5 | Users can switch between these modes at run-time. 6 | 7 | The modes can be configured using the `xplr.config.modes` Lua API. 8 | 9 | `xplr.config.modes.builtin` contain some built-in modes which can be 10 | overridden, but you can't add or remove modes in it. 11 | 12 | #### xplr.config.modes.builtin.default 13 | 14 | The builtin default mode. 15 | Visit the [Default Key Bindings](https://xplr.dev/en/default-key-bindings) 16 | to see what each mode does. 17 | 18 | Type: [Mode](https://xplr.dev/en/mode) 19 | 20 | #### xplr.config.modes.builtin.debug_error 21 | 22 | The builtin debug error mode. 23 | 24 | Type: [Mode](https://xplr.dev/en/mode) 25 | 26 | #### xplr.config.modes.builtin.recover 27 | 28 | The builtin recover mode. 29 | 30 | Type: [Mode](https://xplr.dev/en/mode) 31 | 32 | #### xplr.config.modes.builtin.go_to_path 33 | 34 | The builtin go to path mode. 35 | 36 | Type: [Mode](https://xplr.dev/en/mode) 37 | 38 | #### xplr.config.modes.builtin.move_to 39 | 40 | The builtin move_to mode. 41 | 42 | Type: [Mode](https://xplr.dev/en/mode) 43 | 44 | #### xplr.config.modes.builtin.copy_to 45 | 46 | The builtin copy_to mode. 47 | 48 | Type: [Mode](https://xplr.dev/en/mode) 49 | 50 | #### xplr.config.modes.builtin.selection_ops 51 | 52 | The builtin selection ops mode. 53 | 54 | Type: [Mode](https://xplr.dev/en/mode) 55 | 56 | #### xplr.config.modes.builtin.create 57 | 58 | The builtin create mode. 59 | 60 | Type: [Mode](https://xplr.dev/en/mode) 61 | 62 | #### xplr.config.modes.builtin.create_directory 63 | 64 | The builtin create directory mode. 65 | 66 | Type: [Mode](https://xplr.dev/en/mode) 67 | 68 | #### xplr.config.modes.builtin.create_file 69 | 70 | The builtin create file mode. 71 | 72 | Type: [Mode](https://xplr.dev/en/mode) 73 | 74 | #### xplr.config.modes.builtin.create_conditional 75 | 76 | The builtin create conditional mode. 77 | 78 | Type: [Mode](https://xplr.dev/en/mode) 79 | 80 | #### xplr.config.modes.builtin.number 81 | 82 | The builtin number mode. 83 | 84 | Type: [Mode](https://xplr.dev/en/mode) 85 | 86 | #### xplr.config.modes.builtin.go_to 87 | 88 | The builtin go to mode. 89 | 90 | Type: [Mode](https://xplr.dev/en/mode) 91 | 92 | #### xplr.config.modes.builtin.rename 93 | 94 | The builtin rename mode. 95 | 96 | Type: [Mode](https://xplr.dev/en/mode) 97 | 98 | #### xplr.config.modes.builtin.duplicate_as 99 | 100 | The builtin duplicate as mode. 101 | 102 | Type: [Mode](https://xplr.dev/en/mode) 103 | 104 | #### xplr.config.modes.builtin.delete 105 | 106 | The builtin delete mode. 107 | 108 | Type: [Mode](https://xplr.dev/en/mode) 109 | 110 | #### xplr.config.modes.builtin.action 111 | 112 | The builtin action mode. 113 | 114 | Type: [Mode](https://xplr.dev/en/mode) 115 | 116 | #### xplr.config.modes.builtin.quit 117 | 118 | The builtin quit mode. 119 | 120 | Type: [Mode](https://xplr.dev/en/mode) 121 | 122 | #### xplr.config.modes.builtin.search 123 | 124 | The builtin search mode. 125 | 126 | Type: [Mode](https://xplr.dev/en/mode) 127 | 128 | #### xplr.config.modes.builtin.filter 129 | 130 | The builtin filter mode. 131 | 132 | Type: [Mode](https://xplr.dev/en/mode) 133 | 134 | #### xplr.config.modes.builtin.relative_path_does_match_regex 135 | 136 | The builtin relative_path_does_match_regex mode. 137 | 138 | Type: [Mode](https://xplr.dev/en/mode) 139 | 140 | #### xplr.config.modes.builtin.relative_path_does_not_match_regex 141 | 142 | The builtin relative_path_does_not_match_regex mode. 143 | 144 | Type: [Mode](https://xplr.dev/en/mode) 145 | 146 | #### xplr.config.modes.builtin.sort 147 | 148 | The builtin sort mode. 149 | 150 | Type: [Mode](https://xplr.dev/en/mode) 151 | 152 | #### xplr.config.modes.builtin.switch_layout 153 | 154 | The builtin switch layout mode. 155 | 156 | Type: [Mode](https://xplr.dev/en/mode) 157 | 158 | #### xplr.config.modes.builtin.vroot 159 | 160 | The builtin vroot mode. 161 | 162 | Type: [Mode](https://xplr.dev/en/mode) 163 | 164 | #### xplr.config.modes.builtin.edit_permissions 165 | 166 | The builtin edit permissions mode. 167 | 168 | Type: [Mode](https://xplr.dev/en/mode) 169 | 170 | #### xplr.config.modes.custom 171 | 172 | This is where you define custom modes. 173 | 174 | Type: mapping of the following key-value pairs: 175 | 176 | - key: string 177 | - value: [Mode](https://xplr.dev/en/mode) 178 | 179 | Example: 180 | 181 | ```lua 182 | xplr.config.modes.custom.example = { 183 | name = "example", 184 | key_bindings = { 185 | on_key = { 186 | enter = { 187 | help = "default mode", 188 | messages = { 189 | "PopMode", 190 | { SwitchModeBuiltin = "default" }, 191 | }, 192 | }, 193 | }, 194 | }, 195 | } 196 | 197 | xplr.config.general.initial_mode = "example" 198 | ``` 199 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: dtolnay/rust-toolchain@stable 16 | with: 17 | toolchain: stable 18 | - run: cargo check 19 | 20 | fmt: 21 | name: Rustfmt 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: dtolnay/rust-toolchain@stable 26 | with: 27 | toolchain: stable 28 | components: rustfmt 29 | - run: cargo fmt --all -- --check 30 | 31 | clippy: 32 | name: Clippy 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: dtolnay/rust-toolchain@stable 37 | with: 38 | toolchain: stable 39 | components: clippy 40 | - run: cargo clippy -- -D warnings 41 | 42 | spellcheck: 43 | name: Spellcheck 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | - uses: codespell-project/actions-codespell@v1 48 | with: 49 | ignore_words_file: .codespellignore 50 | 51 | linkcheck: 52 | name: Markdown Link Check 53 | runs-on: ubuntu-latest 54 | continue-on-error: true 55 | steps: 56 | - uses: actions/checkout@v3 57 | - uses: tcort/github-action-markdown-link-check@v1 58 | with: 59 | folder-path: "docs/en/src" 60 | 61 | linkspector: 62 | name: Linkspector 63 | runs-on: ubuntu-latest 64 | continue-on-error: true 65 | steps: 66 | - uses: actions/checkout@v3 67 | - uses: UmbrellaDocs/action-linkspector@v1 68 | with: 69 | workdir: "docs/en/src" 70 | 71 | test: 72 | name: Test Suite 73 | runs-on: ${{ matrix.os }} 74 | needs: 75 | - check 76 | - fmt 77 | - clippy 78 | - spellcheck 79 | strategy: 80 | matrix: 81 | build: 82 | - macos 83 | - macos-aarch64 84 | - linux 85 | # - linux-musl 86 | - linux-aarch64 87 | - linux-arm 88 | rust: [stable] 89 | include: 90 | # See the list: https://github.com/cross-rs/cross 91 | 92 | - build: macos 93 | os: macos-latest 94 | target: x86_64-apple-darwin 95 | 96 | - build: macos-aarch64 97 | os: macos-latest 98 | target: aarch64-apple-darwin 99 | 100 | - build: linux 101 | os: ubuntu-latest 102 | target: x86_64-unknown-linux-gnu 103 | 104 | # - build: linux-musl 105 | # os: ubuntu-latest 106 | # target: x86_64-unknown-linux-musl 107 | 108 | - build: linux-aarch64 109 | os: ubuntu-latest 110 | target: aarch64-unknown-linux-gnu 111 | 112 | - build: linux-arm 113 | os: ubuntu-latest 114 | target: arm-unknown-linux-gnueabihf 115 | 116 | env: 117 | RUST_BACKTRACE: full 118 | steps: 119 | - uses: actions/checkout@v3 120 | 121 | - name: Installing Rust toolchain 122 | uses: dtolnay/rust-toolchain@stable 123 | with: 124 | toolchain: ${{ matrix.rust }} 125 | target: ${{ matrix.target }} 126 | 127 | - name: Installing needed macOS dependencies 128 | if: matrix.os == 'macos-latest' 129 | run: brew install openssl@1.1 130 | 131 | - name: Installing needed Ubuntu dependencies 132 | if: matrix.os == 'ubuntu-latest' 133 | run: | 134 | sudo apt-get update --fix-missing 135 | sudo apt-get install -y --no-install-recommends liblua5.1-0-dev libluajit-5.1-dev gcc pkg-config curl git make ca-certificates 136 | 137 | # - if: matrix.build == 'linux-musl' 138 | # run: sudo apt-get install -y musl-tools 139 | 140 | - if: matrix.build == 'linux-aarch64' 141 | run: sudo apt-get install -y gcc-aarch64-linux-gnu 142 | 143 | - if: matrix.build == 'linux-arm' 144 | run: | 145 | sudo apt-get install -y gcc-multilib 146 | sudo apt-get install -y gcc-arm-linux-gnueabihf 147 | sudo ln -s /usr/include/asm-generic/ /usr/include/asm 148 | 149 | - run: cargo build --target ${{ matrix.target }} 150 | 151 | - if: matrix.build == 'macos' || matrix.build == 'linux' 152 | run: cargo test --target ${{ matrix.target }} 153 | 154 | # bench: 155 | # name: Benchmarks 156 | # runs-on: ubuntu-latest 157 | # steps: 158 | # - uses: actions/checkout@v3 159 | # - uses: dtolnay/rust-toolchain@stable 160 | # with: 161 | # toolchain: stable 162 | # # These dependencies are required for `clipboard` 163 | # - run: sudo apt-get install -y -qq libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev 164 | # - run: cargo bench 165 | -------------------------------------------------------------------------------- /docs/en/src/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | xplr can be configured using [Lua][1] via a special file named `init.lua`, 4 | which can be placed in `~/.config/xplr/` (local to user) or `/etc/xplr/` 5 | (global) depending on the use case. 6 | 7 | When xplr loads, it first executes the [built-in init.lua][2] to set the 8 | default values, which is then overwritten by another config file, if found 9 | using the following lookup order: 10 | 11 | 1. `--config /path/to/init.lua` 12 | 2. `~/.config/xplr/init.lua` 13 | 3. `/etc/xplr/init.lua` 14 | 15 | The first one found will be loaded by xplr and the lookup will stop. 16 | 17 | The loaded config can be further extended using the `-C` or `--extra-config` 18 | command-line option. 19 | 20 | [1]: https://www.lua.org 21 | [2]: https://github.com/sayanarijit/xplr/blob/main/src/init.lua 22 | [3]: https://xplr.dev/en/upgrade-guide 23 | 24 | ## Config 25 | 26 | The xplr configuration, exposed via `xplr.config` Lua API contains the 27 | following sections. 28 | 29 | See: 30 | 31 | - [xplr.config.general](https://xplr.dev/en/general-config) 32 | - [xplr.config.node_types](https://xplr.dev/en/node_types) 33 | - [xplr.config.layouts](https://xplr.dev/en/layouts) 34 | - [xplr.config.modes](https://xplr.dev/en/modes) 35 | 36 | ## Function 37 | 38 | While `xplr.config` defines all the static parts of the configuration, 39 | `xplr.fn` defines all the dynamic parts using functions. 40 | 41 | See: [Lua Function Calls](https://xplr.dev/en/lua-function-calls) 42 | 43 | As always, `xplr.fn.builtin` is where the built-in functions are defined 44 | that can be overwritten. 45 | 46 | #### xplr.fn.builtin.fmt_general_table_row_cols_0 47 | 48 | Renders the first column in the table 49 | 50 | #### xplr.fn.builtin.fmt_general_table_row_cols_1 51 | 52 | Renders the second column in the table 53 | 54 | #### xplr.fn.builtin.fmt_general_table_row_cols_2 55 | 56 | Renders the third column in the table 57 | 58 | #### xplr.fn.builtin.fmt_general_table_row_cols_3 59 | 60 | Renders the fourth column in the table 61 | 62 | #### xplr.fn.builtin.fmt_general_table_row_cols_4 63 | 64 | Renders the fifth column in the table 65 | 66 | #### xplr.fn.builtin.try_complete_path 67 | 68 | DEPRECATED: This function is just for compatibility. 69 | Use message `TryCompletePath` instead. 70 | 71 | #### xplr.fn.custom 72 | 73 | This is where the custom functions can be added. 74 | 75 | There is currently no restriction on what kind of functions can be defined 76 | in `xplr.fn.custom`. 77 | 78 | You can also use nested tables such as 79 | `xplr.fn.custom.my_plugin.my_function` to define custom functions. 80 | 81 | ## Hooks 82 | 83 | This section of the configuration cannot be overwritten by another config 84 | file or plugin, since this is an optional lua return statement specific to 85 | each config file. It can be used to define things that should be explicit 86 | for reasons like performance concerns, such as hooks. 87 | 88 | Plugins should expose the hooks, and require users to subscribe to them 89 | explicitly. 90 | 91 | Example: 92 | 93 | ```lua 94 | return { 95 | -- Add messages to send when the xplr loads. 96 | -- This is similar to the `--on-load` command-line option. 97 | -- 98 | -- Type: list of [Message](https://xplr.dev/en/message#message)s 99 | on_load = { 100 | { LogSuccess = "Configuration successfully loaded!" }, 101 | { CallLuaSilently = "custom.some_plugin_with_hooks.on_load" }, 102 | }, 103 | 104 | -- Add messages to send when the directory changes. 105 | -- 106 | -- Type: list of [Message](https://xplr.dev/en/message#message)s 107 | on_directory_change = { 108 | { LogSuccess = "Changed directory" }, 109 | { CallLuaSilently = "custom.some_plugin_with_hooks.on_directory_change" }, 110 | }, 111 | 112 | -- Add messages to send when the focus changes. 113 | -- 114 | -- Type: list of [Message](https://xplr.dev/en/message#message)s 115 | on_focus_change = { 116 | { LogSuccess = "Changed focus" }, 117 | { CallLuaSilently = "custom.some_plugin_with_hooks.on_focus_change" }, 118 | } 119 | 120 | -- Add messages to send when the mode is switched. 121 | -- 122 | -- Type: list of [Message](https://xplr.dev/en/message#message)s 123 | on_mode_switch = { 124 | { LogSuccess = "Switched mode" }, 125 | { CallLuaSilently = "custom.some_plugin_with_hooks.on_mode_switch" }, 126 | } 127 | 128 | -- Add messages to send when the layout is switched 129 | -- 130 | -- Type: list of [Message](https://xplr.dev/en/message#message)s 131 | on_layout_switch = { 132 | { LogSuccess = "Switched layout" }, 133 | { CallLuaSilently = "custom.some_plugin_with_hooks.on_layout_switch" }, 134 | } 135 | 136 | -- Add messages to send when the selection changes 137 | -- 138 | -- Type: list of [Message](https://xplr.dev/en/message#message)s 139 | on_selection_change = { 140 | { LogSuccess = "Selection changed" }, 141 | { CallLuaSilently = "custom.some_plugin_with_hooks.on_selection_change" }, 142 | } 143 | } 144 | ``` 145 | 146 | --- 147 | 148 | > Note: 149 | > 150 | > It's not recommended to copy the entire configuration, unless you want to 151 | > freeze it and miss out on useful updates to the defaults. 152 | > 153 | > Instead, you can use this as a reference to overwrite only the parts you 154 | > want to update. 155 | > 156 | > If you still want to copy the entire configuration, make sure to put your 157 | > customization before the return statement. 158 | -------------------------------------------------------------------------------- /src/permissions.rs: -------------------------------------------------------------------------------- 1 | // Stolen from https://github.com/Peltoche/lsd/blob/master/src/meta/permissions.rs 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::{fmt::Display, fs::Metadata}; 5 | 6 | pub type RWX = (char, char, char, char, char, char, char, char, char); 7 | pub type Octal = (u8, u8, u8, u8); 8 | 9 | #[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Hash)] 10 | pub struct Permissions { 11 | #[serde(default)] 12 | pub user_read: bool, 13 | 14 | #[serde(default)] 15 | pub user_write: bool, 16 | 17 | #[serde(default)] 18 | pub user_execute: bool, 19 | 20 | #[serde(default)] 21 | pub group_read: bool, 22 | 23 | #[serde(default)] 24 | pub group_write: bool, 25 | 26 | #[serde(default)] 27 | pub group_execute: bool, 28 | 29 | #[serde(default)] 30 | pub other_read: bool, 31 | 32 | #[serde(default)] 33 | pub other_write: bool, 34 | 35 | #[serde(default)] 36 | pub other_execute: bool, 37 | 38 | #[serde(default)] 39 | pub sticky: bool, 40 | 41 | #[serde(default)] 42 | pub setgid: bool, 43 | 44 | #[serde(default)] 45 | pub setuid: bool, 46 | } 47 | 48 | impl Permissions {} 49 | 50 | impl From<&Metadata> for Permissions { 51 | #[cfg(unix)] 52 | fn from(meta: &Metadata) -> Self { 53 | use std::os::unix::fs::PermissionsExt; 54 | 55 | let bits = meta.permissions().mode(); 56 | let has_bit = |bit| bits & bit == bit; 57 | 58 | Self { 59 | user_read: has_bit(modes::USER_READ), 60 | user_write: has_bit(modes::USER_WRITE), 61 | user_execute: has_bit(modes::USER_EXECUTE), 62 | 63 | group_read: has_bit(modes::GROUP_READ), 64 | group_write: has_bit(modes::GROUP_WRITE), 65 | group_execute: has_bit(modes::GROUP_EXECUTE), 66 | 67 | other_read: has_bit(modes::OTHER_READ), 68 | other_write: has_bit(modes::OTHER_WRITE), 69 | other_execute: has_bit(modes::OTHER_EXECUTE), 70 | 71 | sticky: has_bit(modes::STICKY), 72 | setgid: has_bit(modes::SETGID), 73 | setuid: has_bit(modes::SETUID), 74 | } 75 | } 76 | 77 | #[cfg(windows)] 78 | fn from(_: &Metadata) -> Self { 79 | panic!("Cannot get permissions from metadata on Windows") 80 | } 81 | } 82 | 83 | impl Display for Permissions { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | let (ur, uw, ux, gr, gw, gx, or, ow, ox) = (*self).into(); 86 | write!(f, "{ur}{uw}{ux}{gr}{gw}{gx}{or}{ow}{ox}") 87 | } 88 | } 89 | 90 | impl Into for Permissions { 91 | fn into(self) -> RWX { 92 | let bit = |bit: bool, chr: char| { 93 | if bit { 94 | chr 95 | } else { 96 | '-' 97 | } 98 | }; 99 | 100 | let ur = bit(self.user_read, 'r'); 101 | let uw = bit(self.user_write, 'w'); 102 | let ux = match (self.user_execute, self.setuid) { 103 | (true, true) => 's', 104 | (true, false) => 'x', 105 | (false, true) => 'S', 106 | (false, false) => '-', 107 | }; 108 | 109 | let gr = bit(self.group_read, 'r'); 110 | let gw = bit(self.group_write, 'w'); 111 | let gx = match (self.group_execute, self.setgid) { 112 | (true, true) => 's', 113 | (true, false) => 'x', 114 | (false, true) => 'S', 115 | (false, false) => '-', 116 | }; 117 | 118 | let or = bit(self.other_read, 'r'); 119 | let ow = bit(self.other_write, 'w'); 120 | let ox = match (self.other_execute, self.sticky) { 121 | (true, true) => 't', 122 | (true, false) => 'x', 123 | (false, true) => 'T', 124 | (false, false) => '-', 125 | }; 126 | 127 | (ur, uw, ux, gr, gw, gx, or, ow, ox) 128 | } 129 | } 130 | 131 | impl Into for Permissions { 132 | fn into(self) -> Octal { 133 | let bits_to_octal = 134 | |r: bool, w: bool, x: bool| (r as u8) * 4 + (w as u8) * 2 + (x as u8); 135 | 136 | ( 137 | bits_to_octal(self.setuid, self.setgid, self.sticky), 138 | bits_to_octal(self.user_read, self.user_write, self.user_execute), 139 | bits_to_octal(self.group_read, self.group_write, self.group_execute), 140 | bits_to_octal(self.other_read, self.other_write, self.other_execute), 141 | ) 142 | } 143 | } 144 | 145 | // More readable aliases for the permission bits exposed by libc. 146 | #[allow(trivial_numeric_casts)] 147 | #[cfg(unix)] 148 | mod modes { 149 | pub type Mode = u32; 150 | // The `libc::mode_t` type’s actual type varies, but the value returned 151 | // from `metadata.permissions().mode()` is always `u32`. 152 | 153 | pub const USER_READ: Mode = libc::S_IRUSR as Mode; 154 | pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; 155 | pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; 156 | 157 | pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; 158 | pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; 159 | pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; 160 | 161 | pub const OTHER_READ: Mode = libc::S_IROTH as Mode; 162 | pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; 163 | pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; 164 | 165 | pub const STICKY: Mode = libc::S_ISVTX as Mode; 166 | pub const SETGID: Mode = libc::S_ISGID as Mode; 167 | pub const SETUID: Mode = libc::S_ISUID as Mode; 168 | } 169 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socioeconomic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | email( sayanarijit at gmail dot com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/lua/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::VERSION; 2 | use crate::config::Config; 3 | use crate::config::Hooks; 4 | use anyhow::bail; 5 | use anyhow::Error; 6 | use anyhow::Result; 7 | use mlua::Lua; 8 | use mlua::LuaSerdeExt; 9 | use mlua::SerializeOptions; 10 | use serde::de::DeserializeOwned; 11 | use serde::Serialize; 12 | use std::fs; 13 | 14 | pub mod util; 15 | 16 | const DEFAULT_LUA_SCRIPT: &str = include_str!("../init.lua"); 17 | const UPGRADE_GUIDE_LINK: &str = "https://xplr.dev/en/upgrade-guide"; 18 | 19 | pub fn serialize( 20 | lua: &mlua::Lua, 21 | value: &T, 22 | ) -> Result { 23 | lua.to_value_with(value, SerializeOptions::new().serialize_none_to_null(false)) 24 | .map_err(Error::from) 25 | } 26 | 27 | fn parse_version(version: &str) -> Result<(u16, u16, u16, Option)> { 28 | let mut configv = version.split('.'); 29 | 30 | let major = configv.next().unwrap_or_default().parse::()?; 31 | let minor = configv.next().unwrap_or_default().parse::()?; 32 | let patch = configv 33 | .next() 34 | .and_then(|s| s.split('-').next()) 35 | .unwrap_or_default() 36 | .parse::()?; 37 | 38 | let pre = configv.next().unwrap_or_default().parse::().ok(); 39 | 40 | Ok((major, minor, patch, pre)) 41 | } 42 | 43 | /// Check the config version and notify users. 44 | pub fn check_version(version: &str, path: &str) -> Result<()> { 45 | let (rmajor, rminor, rbugfix, rbeta) = parse_version(VERSION)?; 46 | let (smajor, sminor, sbugfix, sbeta) = parse_version(version)?; 47 | 48 | if rmajor == smajor && rminor >= sminor && rbugfix >= sbugfix && rbeta == sbeta { 49 | Ok(()) 50 | } else { 51 | bail!( 52 | "incompatible script version in: {}. The script version is: {}, the required version is: {}. Visit {}", 53 | path, 54 | version, 55 | VERSION, 56 | UPGRADE_GUIDE_LINK, 57 | ) 58 | } 59 | } 60 | 61 | /// Used to initialize Lua globals 62 | pub fn init(lua: &Lua) -> Result<(Config, Option)> { 63 | let config = Config::default(); 64 | let globals = lua.globals(); 65 | 66 | let util = util::create_table(lua)?; 67 | 68 | let lua_xplr = lua.create_table()?; 69 | lua_xplr.set("config", serialize(lua, &config)?)?; 70 | lua_xplr.set("util", util)?; 71 | 72 | let lua_xplr_fn = lua.create_table()?; 73 | let lua_xplr_fn_builtin = lua.create_table()?; 74 | let lua_xplr_fn_custom = lua.create_table()?; 75 | 76 | lua_xplr_fn.set("builtin", lua_xplr_fn_builtin)?; 77 | lua_xplr_fn.set("custom", lua_xplr_fn_custom)?; 78 | lua_xplr.set("fn", lua_xplr_fn)?; 79 | globals.set("xplr", lua_xplr)?; 80 | 81 | let hooks: Option = lua 82 | .load(DEFAULT_LUA_SCRIPT) 83 | .set_name("xplr init") 84 | .call(()) 85 | .and_then(|v| lua.from_value(v))?; 86 | 87 | let lua_xplr: mlua::Table = globals.get("xplr")?; 88 | let config: Config = lua.from_value(lua_xplr.get("config")?)?; 89 | Ok((config, hooks)) 90 | } 91 | 92 | /// Used to extend Lua globals 93 | pub fn extend(lua: &Lua, path: &str) -> Result<(Config, Option)> { 94 | let globals = lua.globals(); 95 | 96 | let script = fs::read(path)?; 97 | 98 | let hooks: Option = lua 99 | .load(&script) 100 | .set_name(path) 101 | .call(()) 102 | .and_then(|v| lua.from_value(v))?; 103 | 104 | let version: String = match globals.get("version").and_then(|v| lua.from_value(v)) { 105 | Ok(v) => v, 106 | Err(_) => bail!("'version' must be defined globally in {}", path), 107 | }; 108 | 109 | check_version(&version, path)?; 110 | 111 | let lua_xplr: mlua::Table = globals.get("xplr")?; 112 | 113 | let config: Config = lua.from_value(lua_xplr.get("config")?)?; 114 | Ok((config, hooks)) 115 | } 116 | 117 | fn resolve_fn_recursive<'a>( 118 | table: &mlua::Table, 119 | mut path: impl Iterator, 120 | ) -> Result { 121 | if let Some(nxt) = path.next() { 122 | match table.get(nxt)? { 123 | mlua::Value::Table(t) => resolve_fn_recursive(&t, path), 124 | mlua::Value::Function(f) => Ok(f), 125 | t => bail!("{:?} is not a function", t), 126 | } 127 | } else { 128 | bail!("Invalid path") 129 | } 130 | } 131 | 132 | /// This function resolves paths like `builtin.func_foo`, `custom.func_bar` into lua functions. 133 | pub fn resolve_fn(globals: &mlua::Table, path: &str) -> Result { 134 | resolve_fn_recursive(globals, path.split('.')) 135 | } 136 | 137 | pub fn call(lua: &Lua, func: &str, arg: mlua::Value) -> Result { 138 | let func = format!("xplr.fn.{func}"); 139 | let func = resolve_fn(&lua.globals(), &func)?; 140 | let res: mlua::Value = func.call(arg)?; 141 | let res: R = lua.from_value(res)?; 142 | Ok(res) 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | 148 | use super::*; 149 | 150 | #[test] 151 | fn test_compatibility() { 152 | assert!(check_version(VERSION, "foo path").is_ok()); 153 | 154 | // Current release if OK 155 | assert!(check_version("1.1.0", "foo path").is_ok()); 156 | 157 | // Prev major release is ERR 158 | assert!(check_version("0.20.2", "foo path").is_err()); 159 | 160 | // Prev minor release is OK 161 | assert!(check_version("1.0.0", "foo path").is_ok()); 162 | 163 | // Prev bugfix release is OK 164 | // assert!(check_version("1.1.-1", "foo path").is_ok()); 165 | 166 | // Next major release is ERR 167 | assert!(check_version("2.0.0", "foo path").is_err()); 168 | 169 | // Next minor release is ERR 170 | assert!(check_version("1.2.0", "foo path").is_err()); 171 | 172 | // Next bugfix release is ERR (Change when we get to v1) 173 | assert!(check_version("1.1.1", "foo path").is_err()); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Deployment 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | name: Publishing for ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | build: 16 | - macos 17 | - macos-aarch64 18 | - linux 19 | # - linux-musl 20 | - linux-aarch64 21 | - linux-arm 22 | rust: [stable] 23 | include: 24 | # See the list: https://github.com/cross-rs/cross 25 | 26 | - build: macos 27 | os: macos-latest 28 | target: x86_64-apple-darwin 29 | 30 | - build: macos-aarch64 31 | os: macos-latest 32 | target: aarch64-apple-darwin 33 | 34 | - build: linux 35 | os: ubuntu-latest 36 | target: x86_64-unknown-linux-gnu 37 | 38 | # - build: linux-musl 39 | # os: ubuntu-latest 40 | # target: x86_64-unknown-linux-musl 41 | 42 | - build: linux-aarch64 43 | os: ubuntu-latest 44 | target: aarch64-unknown-linux-gnu 45 | 46 | - build: linux-arm 47 | os: ubuntu-latest 48 | target: arm-unknown-linux-gnueabihf 49 | 50 | steps: 51 | - uses: actions/checkout@v3 52 | 53 | - name: Installing Rust toolchain 54 | uses: dtolnay/rust-toolchain@stable 55 | with: 56 | toolchain: ${{ matrix.rust }} 57 | target: ${{ matrix.target }} 58 | 59 | - name: Installing needed macOS dependencies 60 | if: matrix.os == 'macos-latest' 61 | run: brew install openssl@1.1 62 | 63 | - name: Installing needed Ubuntu dependencies 64 | if: matrix.os == 'ubuntu-latest' 65 | run: | 66 | sudo apt-get update --fix-missing 67 | sudo apt-get install -y --no-install-recommends liblua5.1-0-dev libluajit-5.1-dev gcc pkg-config curl git make ca-certificates 68 | sudo apt-get install -y snapd 69 | # sudo snap install snapcraft --classic 70 | # sudo snap install multipass --classic --beta 71 | 72 | # - if: matrix.build == 'linux-musl' 73 | # run: sudo apt-get install -y musl-tools 74 | 75 | - if: matrix.build == 'linux-aarch64' 76 | run: sudo apt-get install -y gcc-aarch64-linux-gnu 77 | 78 | - if: matrix.build == 'linux-arm' 79 | run: | 80 | sudo apt-get install -y gcc-multilib 81 | sudo apt-get install -y gcc-arm-linux-gnueabihf 82 | sudo ln -s /usr/include/asm-generic/ /usr/include/asm 83 | 84 | - name: Running cargo build 85 | run: cargo build --locked --release --target ${{ matrix.target }} 86 | 87 | # - name: Running snapcraft build 88 | # run: | 89 | # snapcraft 90 | # printf ' [ INFO ] generated files include:\n' 91 | # command ls -Al | grep "\.snap" | awk '{ print $9 }' 92 | # mv ./*.snap ./xplr.snap 93 | 94 | - name: Install gpg secret key 95 | run: | 96 | cat <(echo -e "${{ secrets.GPG_SECRET }}") | gpg --batch --import 97 | gpg --list-secret-keys --keyid-format LONG 98 | 99 | - name: Packaging final binary 100 | shell: bash 101 | run: | 102 | cd target/${{ matrix.target }}/release 103 | BINARY_NAME=xplr 104 | RELEASE_NAME=$BINARY_NAME-${{ matrix.build }} 105 | tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME 106 | shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.sha256 107 | cat <(echo "${{ secrets.GPG_PASS }}") | gpg --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor $RELEASE_NAME.tar.gz 108 | 109 | - name: Releasing assets 110 | uses: softprops/action-gh-release@v1 111 | with: 112 | files: | 113 | target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.tar.gz 114 | target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.sha256 115 | target/${{ matrix.target }}/release/xplr-${{ matrix.build }}.tar.gz.asc 116 | xplr.snap 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | 120 | # - name: Cleaning snapcraft 121 | # run: | 122 | # command rm --verbose ./*.snap 123 | # snapcraft clean 124 | 125 | publish-gpg-signature: 126 | name: Publishing GPG signature 127 | runs-on: ubuntu-latest 128 | steps: 129 | - uses: actions/checkout@v3 130 | - name: Install gpg secret key 131 | run: | 132 | cat <(echo -e "${{ secrets.GPG_SECRET }}") | gpg --batch --import 133 | gpg --list-secret-keys --keyid-format LONG 134 | 135 | - name: Signing archive with GPG 136 | run: | 137 | VERSION=${GITHUB_REF##*v} 138 | git -c tar.tar.gz.command='gzip -cn' archive -o xplr-${VERSION:?}.tar.gz --format tar.gz --prefix "xplr-${VERSION:?}/" "v${VERSION}" 139 | cat <(echo "${{ secrets.GPG_PASS }}") | gpg --pinentry-mode loopback --passphrase-fd 0 --detach-sign --armor "xplr-${VERSION:?}.tar.gz" 140 | mv "xplr-${VERSION:?}.tar.gz.asc" "source.tar.gz.asc" 141 | 142 | - name: Releasing GPG signature 143 | uses: softprops/action-gh-release@v1 144 | with: 145 | files: | 146 | source.tar.gz.asc 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | 150 | publish-cargo: 151 | name: Publishing to Cargo 152 | runs-on: ubuntu-latest 153 | steps: 154 | - uses: actions/checkout@v3 155 | 156 | - uses: dtolnay/rust-toolchain@stable 157 | with: 158 | toolchain: stable 159 | 160 | - run: | 161 | sudo apt-get update --fix-missing 162 | sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev 163 | 164 | - run: cargo publish --allow-dirty 165 | env: 166 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_KEY }} 167 | -------------------------------------------------------------------------------- /docs/en/src/install.md: -------------------------------------------------------------------------------- 1 | # Try in Docker 2 | 3 | If you prefer to try it before installing, here's the snippet for your 4 | convenience. 5 | 6 | ```bash 7 | docker run -w / -it --rm ubuntu sh -uec ' 8 | apt-get update -y 9 | apt-get install -y wget tar vim less 10 | wget https://github.com/sayanarijit/xplr/releases/latest/download/xplr-linux.tar.gz 11 | tar -xzvf xplr-linux.tar.gz 12 | ./xplr 13 | ' 14 | ``` 15 | 16 | # Install 17 | 18 | You can install xplr using one of the following ways. Each has their own 19 | advantages and limitations. 20 | 21 | For example, the [Direct Download][1], [From crates.io][2], and 22 | [Build From Source][3] methods allow the users to install the latest possible 23 | version of xplr, but they have one common drawback - the user will need to keep 24 | an eye on the releases, and manually upgrade xplr when a new version is 25 | available. 26 | 27 | One way to keep an eye on the releases is to [watch the repository][4]. 28 | 29 | ## Community Maintained Repositories 30 | 31 | xplr can be installed from one of the following community maintained 32 | repositories: 33 | 34 | [![packaging status][5]][6] 35 | 36 | ### Cross-platform 37 | 38 | #### [Nixpkgs][10] 39 | 40 | ``` 41 | nix-env -f https://github.com/NixOS/nixpkgs/tarball/master -iA xplr 42 | ``` 43 | 44 | Or 45 | 46 | ```nix 47 | # configuration.nix or darwin-configuration.nix 48 | environment.systemPackages = with nixpkgs; [ 49 | xplr 50 | # ... 51 | ]; 52 | ``` 53 | 54 | #### [Home Manager][30] 55 | 56 | ```nix 57 | # home.nix 58 | home.packages = with nixpkgs; [ 59 | xplr 60 | # ... 61 | ]; 62 | ``` 63 | 64 | Or 65 | 66 | ```nix 67 | # home.nix 68 | programs.xplr = { 69 | enable = true; 70 | 71 | # Optional params: 72 | plugins = { 73 | tree-view = fetchFromGitHub { 74 | owner = "sayanarijit"; 75 | repo = "tree-view.xplr"; 76 | }; 77 | local-plugin = "/home/user/.config/xplr/plugins/local-plugin"; 78 | }; 79 | extraConfig = '' 80 | require("tree-view").setup() 81 | require("local-plugin").setup() 82 | ''; 83 | }; 84 | ``` 85 | 86 | ### Arch Linux 87 | 88 | (same for Manjaro Linux) 89 | 90 | #### [Official Community Repo][7] 91 | 92 | ``` 93 | sudo pacman -S xplr 94 | ``` 95 | 96 | #### [AUR][8] 97 | 98 | Git version: 99 | 100 | ``` 101 | paru -S xplr-git 102 | ``` 103 | 104 | ### Alpine Linux 105 | 106 | #### [Edge Testing Repo][27] 107 | 108 | ``` 109 | # Add the following line in /etc/apk/repositories: 110 | # https://dl-cdn.alpinelinux.org/alpine/edge/testing 111 | 112 | apk add xplr bash less 113 | ``` 114 | 115 | ### Void Linux 116 | 117 | #### [void-templates by shubham][9] 118 | 119 | ### Gentoo 120 | 121 | #### [Overlay GURU][28] 122 | 123 | ### macOS 124 | 125 | Make sure you have the latest version of [GNU core utilities][29] installed. 126 | 127 | #### [MacPorts][11] 128 | 129 | ``` 130 | sudo port selfupdate 131 | sudo port install xplr 132 | ``` 133 | 134 | #### [Homebrew][12] 135 | 136 | Stable branch: 137 | 138 | ``` 139 | brew install xplr 140 | ``` 141 | 142 | HEAD branch: 143 | 144 | ``` 145 | brew install --head xplr 146 | ``` 147 | 148 | ### FreeBSD 149 | 150 | #### [ports][13] 151 | 152 | ``` 153 | pkg install xplr 154 | ``` 155 | 156 | Or 157 | 158 | ``` 159 | cd /usr/ports/misc/xplr 160 | make install clean 161 | ``` 162 | 163 | ### NetBSD 164 | 165 | #### [pkgsrc][14] 166 | 167 | ``` 168 | pkgin install xplr 169 | ``` 170 | 171 | Or 172 | 173 | ``` 174 | cd /usr/pkgsrc/sysutils/xplr 175 | make install 176 | ``` 177 | 178 | ## Direct Download 179 | 180 | One can directly download the standalone binary from the 181 | [releases][15]. 182 | 183 | Currently, the following options are available for direct download: 184 | 185 | - [GNU/Linux][16] 186 | - [macOS][17] 187 | 188 | Command-line instructions: 189 | 190 | ```bash 191 | platform="linux" # or "macos" 192 | 193 | # Download 194 | wget https://github.com/sayanarijit/xplr/releases/latest/download/xplr-$platform.tar.gz 195 | 196 | # Extract 197 | tar xzvf xplr-$platform.tar.gz 198 | 199 | # Place in $PATH 200 | sudo mv xplr /usr/local/bin/ 201 | ``` 202 | 203 | ## From [crates.io][18] 204 | 205 | Prerequisites: 206 | 207 | - [Rust toolchain][19], 208 | - [gcc][20] 209 | - [make][21] 210 | 211 | Command-line instructions: 212 | 213 | ```bash 214 | cargo install --locked --force xplr 215 | ``` 216 | 217 | ## Build From Source 218 | 219 | Prerequisites: 220 | 221 | - [git][22] 222 | - [Rust toolchain][19] 223 | - [gcc][20] 224 | - [make][21] 225 | 226 | Command-line instructions: 227 | 228 | ```bash 229 | # Clone the repository 230 | git clone https://github.com/sayanarijit/xplr.git 231 | cd xplr 232 | 233 | # Build 234 | cargo build --locked --release --bin xplr 235 | 236 | # Place in $PATH 237 | sudo cp target/release/xplr /usr/local/bin/ 238 | ``` 239 | 240 | [1]: #direct-download 241 | [2]: #from-cratesio 242 | [3]: #build-from-source 243 | [4]: https://github.com/sayanarijit/xplr/watchers 244 | [5]: https://repology.org/badge/vertical-allrepos/xplr.svg 245 | [6]: https://repology.org/project/xplr/versions 246 | [7]: https://archlinux.org/packages/extra/x86_64/xplr 247 | [8]: https://aur.archlinux.org/packages/?O=0&SeB=n&K=xplr&outdated=&SB=n&SO=a&PP=50&do_Search=Go 248 | [9]: https://github.com/shubham-cpp/void-pkg-templates 249 | [10]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/xp/xplr/package.nix 250 | [11]: https://ports.macports.org/port/xplr 251 | [12]: https://formulae.brew.sh/formula/xplr 252 | [13]: https://cgit.freebsd.org/ports/plain/misc/xplr/ 253 | [14]: https://pkgsrc.se/sysutils/xplr 254 | [15]: https://github.com/sayanarijit/xplr/releases 255 | [16]: https://github.com/sayanarijit/xplr/releases/latest/download/xplr-linux.tar.gz 256 | [17]: https://github.com/sayanarijit/xplr/releases/latest/download/xplr-macos.tar.gz 257 | [18]: https://crates.io/crates/xplr 258 | [19]: https://www.rust-lang.org/tools/install 259 | [20]: https://gcc.gnu.org/ 260 | [21]: https://www.gnu.org/software/make/ 261 | [22]: https://git-scm.com/ 262 | [23]: https://github.com/sayanarijit/xplr/assets/11632726/3b61e8c8-76f0-48e8-8734-50e9e7e495b7 263 | [25]: https://gifyu.com/image/tF2D 264 | [27]: https://pkgs.alpinelinux.org/packages?name=xplr 265 | [28]: https://gpo.zugaina.org/Overlays/guru/app-misc/xplr 266 | [29]: https://formulae.brew.sh/formula/coreutils 267 | [30]: https://github.com/nix-community/home-manager/blob/master/modules/programs/xplr.nix 268 | -------------------------------------------------------------------------------- /docs/en/src/configure-key-bindings.md: -------------------------------------------------------------------------------- 1 | # Configure Key Bindings 2 | 3 | In xplr, each keyboard input passes through a bunch of handlers (e.g. `on_key`, 4 | `on_number`, `default` etc.) in a given order. If any of the handlers is 5 | configured to with an [action][16], it will intercept the key and produce 6 | [messages][18] for xplr to handle. 7 | 8 | Try [debug key bindings][31] to understand how key bindings actually work. 9 | 10 | ## Key Bindings 11 | 12 | Key bindings contains the following information: 13 | 14 | - [on_key][10] 15 | - [on_alphabet][11] 16 | - [on_number][12] 17 | - [on_alphanumeric][32] 18 | - [on_special_character][13] 19 | - [on_character][33] 20 | - [on_navigation][34] 21 | - [on_function][35] 22 | - [default][14] 23 | 24 | ### on_key 25 | 26 | Type: mapping of [Key][15] to nullable [Action][16] 27 | 28 | Defines what to do when an exact key is pressed. 29 | 30 | ### on_alphabet 31 | 32 | Type: nullable [Action][16] 33 | 34 | An action to perform if the keyboard input is an alphabet and is not mapped via 35 | the [on_key][10] field. 36 | 37 | ### on_number 38 | 39 | Type: nullable [Action][16] 40 | 41 | An action to perform if the keyboard input is a number and is not mapped via 42 | the [on_key][10] field. 43 | 44 | ### on_alphanumeric 45 | 46 | Type: nullable [Action][16] 47 | 48 | An action to perform if the keyboard input is alphanumeric and is not mapped 49 | via the [on_key][10], [on_alphabet][11] or [on_number][12] field. 50 | 51 | ### on_special_character 52 | 53 | Type: nullable [Action][16] 54 | 55 | An action to perform if the keyboard input is a special character and is not 56 | mapped via the [on_key][10] field. 57 | 58 | ### on_character 59 | 60 | Type: nullable [Action][16] 61 | 62 | An action to perform if the keyboard input is a character and is not mapped 63 | via the [on_key][10], [on_alphabet][11], [on_number][12], [on_alphanumeric][32] 64 | or [on_special_character][13] field. 65 | 66 | ### on_navigation 67 | 68 | Type: nullable [Action][16] 69 | 70 | An action to perform if the keyboard input is a navigation key and is not 71 | mapped via the [on_key][10] field. 72 | 73 | ### on_function 74 | 75 | Type: nullable [Action][16] 76 | 77 | An action to perform if the keyboard input is a function key and is not mapped 78 | via the [on_key][10] field. 79 | 80 | ### default 81 | 82 | Type: nullable [Action][16] 83 | 84 | Default action to perform in case if a keyboard input not mapped via any of the 85 | `on_*` fields mentioned above. 86 | 87 | ## Key 88 | 89 | A key is a [sum type][36] can be one of the following: 90 | 91 | - 0, 1, ... 9 92 | - a, b, ... z 93 | - A, B, ... Z 94 | - f1, f2, ... f12 95 | - backspace 96 | - left 97 | - right 98 | - up 99 | - down 100 | - home 101 | - end 102 | - page-up 103 | - page-down 104 | - back-tab 105 | - delete 106 | - insert 107 | - enter 108 | - tab 109 | - esc 110 | - ctrl-a, ctrl-b, ... ctrl-z 111 | - ctrl-backspace, ctrl-left, ... ctrl-esc 112 | - alt-a, alt-b, ... alt-z 113 | 114 | And finally, the special characters - including space (`" "`) with their `ctrl` 115 | bindings. 116 | 117 | ## Action 118 | 119 | An action contains the following information: 120 | 121 | - [help][1] 122 | - [messages][17] 123 | 124 | ### help 125 | 126 | Type: nullable string 127 | 128 | Description of what it does. If unspecified, it will be excluded from the help 129 | menu. 130 | 131 | ### messages 132 | 133 | Type: A list of [Message][18] to send. 134 | 135 | The list of messages to send when a key is pressed. 136 | 137 | ## Tutorial: Adding a New Mode 138 | 139 | Assuming xplr is [installed][19] and [setup][20], let's 140 | add our own mode to integrate xplr with [fzf][21]. 141 | 142 | We'll call it `fzxplr` mode. 143 | 144 | First, let's add a custom mode called `fzxplr`, and map the key `F` to an 145 | action that will call `fzf` to search and focus on a file or enter into a 146 | directory. 147 | 148 | ```lua 149 | xplr.config.modes.custom.fzxplr = { 150 | name = "fzxplr", 151 | key_bindings = { 152 | on_key = { 153 | F = { 154 | help = "search", 155 | messages = { 156 | { 157 | BashExec = [===[ 158 | PTH=$(cat "${XPLR_PIPE_DIRECTORY_NODES_OUT:?}" | awk -F/ '{print $NF}' | fzf) 159 | if [ -d "$PTH" ]; then 160 | "$XPLR" -m 'ChangeDirectory: %q' "$PTH" 161 | else 162 | "$XPLR" -m 'FocusPath: %q' "$PTH" 163 | fi 164 | ]===] 165 | }, 166 | "PopMode", 167 | }, 168 | }, 169 | }, 170 | default = { 171 | messages = { 172 | "PopMode", 173 | }, 174 | }, 175 | }, 176 | } 177 | ``` 178 | 179 | As you can see, the key `F` in mode `fzxplr` (the name can be anything) 180 | executes a script in `bash`. 181 | 182 | `BashExec`, `PopMode`, `SwitchModeBuiltin`, `ChangeDirectory` and `FocusPath` 183 | are [messages][18], `$XPLR`, `$XPLR_PIPE_DIRECTORY_NODES_OUT` are 184 | [environment variables][22] exported by `xplr` before executing the command. 185 | They contain the path to the [input][23] and [output][24] pipes that allows 186 | external tools to interact with `xplr`. 187 | 188 | Now that we have our new mode ready, let's add an entry point to this mode via 189 | the `default` mode. 190 | 191 | ```lua 192 | xplr.config.modes.builtin.default.key_bindings.on_key["F"] = { 193 | help = "fzf mode", 194 | messages = { 195 | { SwitchModeCustom = "fzxplr" }, 196 | }, 197 | } 198 | ``` 199 | 200 | Now let's try out the new `xplr`-`fzf` integration. 201 | 202 | [![xplr-fzf.gif][25]][26] 203 | 204 | --- 205 | 206 | Visit [Awesome Plugins][27] for more [integration][28] options. 207 | 208 | [1]: #help 209 | [10]: #on_key 210 | [11]: #on_alphabet 211 | [12]: #on_number 212 | [13]: #on_special_character 213 | [14]: #default 214 | [15]: #key 215 | [16]: #action 216 | [17]: #messages 217 | [18]: message.md#message 218 | [19]: install.md 219 | [20]: post-install.md 220 | [21]: https://github.com/junegunn/fzf 221 | [22]: environment-variables-and-pipes.md#environment-variables 222 | [23]: environment-variables-and-pipes.md#input-pipe 223 | [24]: environment-variables-and-pipes.md#output-pipes 224 | [25]: https://s3.gifyu.com/images/xplr-fzf.gif 225 | [26]: https://gifyu.com/image/tW86 226 | [27]: awesome-plugins.md 227 | [28]: awesome-plugins.md#integration 228 | [31]: debug-key-bindings.md 229 | [32]: #on_alphanumeric 230 | [33]: #on_character 231 | [34]: #on_navigation 232 | [35]: #on_function 233 | [36]: sum-type.md 234 | -------------------------------------------------------------------------------- /docs/en/src/awesome-plugins.md: -------------------------------------------------------------------------------- 1 | # Awesome Plugins 2 | 3 | Here's a list of awesome xplr plugins that you might want to [check out][48]. If none 4 | of the following plugins work for you, it's very easy to 5 | [write your own][1]. 6 | 7 | ### Extension 8 | 9 | - [**sayanarijit/command-mode.xplr**][37] The missing command mode for xplr. 10 | - [**igorepst/context-switch.xplr**][42] Context switch plugin for xplr. 11 | - [**sayanarijit/dual-pane.xplr**][43] Implements support for dual-pane navigation into xplr. 12 | - [**sayanarijit/map.xplr**][38] Visually inspect and interactively execute batch commands using xplr. 13 | - [**sayanarijit/offline-docs.xplr**][51] Fetch the appropriate version of xplr docs and browse offline. 14 | - [**sayanarijit/regex-search.xplr**][55] Bring back the regex based search in xplr. 15 | - [**sayanarijit/registers.xplr**][49] Use multiple registers to store the selected paths. 16 | - [**sayanarijit/tree-view.xplr**][61] Hackable tree view for xplr 17 | - [**sayanarijit/tri-pane.xplr**][56] xplr plugin that implements ranger-like three pane layout. 18 | - [**sayanarijit/type-to-nav.xplr**][28] Inspired by [nnn's type-to-nav mode][29] for xplr, 19 | with some tweaks. 20 | - [**dtomvan/term.xplr**][39] Terminal integration for xplr. 21 | - [**sayanarijit/wl-clipboard.xplr**][52] Copy and paste with system clipboard using wl-clipboard 22 | - [**dtomvan/xpm.xplr**][47] The XPLR Plugin Manager. 23 | - [**emsquid/style.xplr**][57] Helper plugin that allows you to integrate xplr's [Style][58] anywhere. 24 | 25 | ### Integration 26 | 27 | - [**sayanarijit/alacritty.xplr**][33] [Alacritty][34] integration for xplr. 28 | - [**sayanarijit/dragon.xplr**][4] Drag and drop files using [dragon][5]. 29 | - [**sayanarijit/dua-cli.xplr**][6] Get the disk usage using [dua-cli][7] with selection 30 | support. 31 | - [**sayanarijit/fzf.xplr**][8] Fuzzy search using [fzf][9] to focus on a file or enter. 32 | - [**sayanarijit/find.xplr**][44] An interactive finder plugin to complement [map.xplr][38]. 33 | - [**Junker/nuke.xplr**][53] Open files in apps by file type or mime. 34 | - [**sayanarijit/nvim-ctrl.xplr**][35] Send files to running Neovim sessions using 35 | [nvim-ctrl][36]. 36 | - [**dtomvan/ouch.xplr**][40] This plugin uses [ouch][41] to compress and decompress files. 37 | - [**dtomvan/paste-rs.xplr**][23] Use this plugin to paste your files to 38 | [paste.rs][24], and open/delete them later using [fzf][9]. 39 | - [**sayanarijit/preview-tabbed.xplr**][10] Preview paths using suckless [tabbed][11] and 40 | [nnn preview-tabbed][12]. 41 | - [**sayanarijit/qrcp.xplr**][26] Send and receive files via QR code using [qrcp][27]. 42 | - [**sayanarijit/scp.xplr**][54] Integrate xplr with scp. 43 | - [**sayanarijit/trash-cli.xplr**][13] Trash files and directories using [trash-cli][14]. 44 | - [**LordMZTE/udisks.xplr**][65] Manage devices with UDisks. 45 | - [**sayanarijit/xclip.xplr**][15] Copy and paste with system clipboard using [xclip][16]. 46 | - [**sayanarijit/zoxide.xplr**][17] Change directory using the [zoxide][18] database. 47 | 48 | ### Theme 49 | 50 | - [**sayanarijit/material-landscape.xplr**][19] Material Landscape 51 | - [**sayanarijit/material-landscape2.xplr**][20] Material Landscape 2 52 | - [**sayanarijit/zentable.xplr**][31] A clean, distraction free xplr table UI 53 | - [**dy-sh/dysh-style.xplr**][63] Complements xplr theme with icons and highlighting. 54 | - [**prncss-xyz/icons.xplr**][30] An icon theme for xplr. 55 | - [**dtomvan/extra-icons.xplr**][50] Adds more icons to icons.xplr, compatible 56 | with zentable.xplr. 57 | - [**hartan/web-devicons.xplr**][59] Adds [nvim-web-devicons][60] to xplr with 58 | optional coloring 59 | - [**duganchen/one-table-column.xplr**][62] Moves file stats to a status bar. 60 | - [**dy-sh/get-rid-of-index.xplr**][64] Removes the index column. 61 | 62 | ## Also See: 63 | 64 | - [Awesome Hacks][45] 65 | - [Awesome Integrations][46] 66 | 67 | [1]: writing-plugins.md 68 | [2]: #integration 69 | [3]: #theme 70 | [4]: https://github.com/sayanarijit/dragon.xplr 71 | [5]: https://github.com/mwh/dragon 72 | [6]: https://github.com/sayanarijit/dua-cli.xplr 73 | [7]: https://github.com/Byron/dua-cli 74 | [8]: https://github.com/sayanarijit/fzf.xplr 75 | [9]: https://github.com/junegunn/fzf 76 | [10]: https://github.com/sayanarijit/preview-tabbed.xplr 77 | [11]: https://tools.suckless.org/tabbed/ 78 | [12]: https://github.com/jarun/nnn/blob/master/plugins/preview-tabbed 79 | [13]: https://github.com/sayanarijit/trash-cli.xplr 80 | [14]: https://github.com/andreafrancia/trash-cli 81 | [15]: https://github.com/sayanarijit/xclip.xplr 82 | [16]: https://github.com/astrand/xclip 83 | [17]: https://github.com/sayanarijit/zoxide.xplr 84 | [18]: https://github.com/ajeetdsouza/zoxide 85 | [19]: https://github.com/sayanarijit/material-landscape.xplr 86 | [20]: https://github.com/sayanarijit/material-landscape2.xplr 87 | [22]: https://github.com/sayanarijit/xargs.xplr 88 | [23]: https://github.com/dtomvan/paste-rs.xplr 89 | [24]: https://paste.rs 90 | [25]: https://github.com/sayanarijit/completion.xplr 91 | [26]: https://github.com/sayanarijit/qrcp.xplr 92 | [27]: https://github.com/claudiodangelis/qrcp 93 | [28]: https://github.com/sayanarijit/type-to-nav.xplr 94 | [29]: https://github.com/jarun/nnn/wiki/concepts#type-to-nav 95 | [30]: https://github.com/prncss-xyz/icons.xplr 96 | [31]: https://github.com/sayanarijit/zentable.xplr 97 | [32]: #extension 98 | [33]: https://github.com/sayanarijit/alacritty.xplr 99 | [34]: https://github.com/alacritty/alacritty 100 | [35]: https://github.com/sayanarijit/nvim-ctrl.xplr 101 | [36]: https://github.com/chmln/nvim-ctrl 102 | [37]: https://github.com/sayanarijit/command-mode.xplr 103 | [38]: https://github.com/sayanarijit/map.xplr 104 | [39]: https://github.com/dtomvan/term.xplr 105 | [40]: https://github.com/dtomvan/ouch.xplr 106 | [41]: https://github.com/ouch-org/ouch 107 | [42]: https://github.com/igorepst/context-switch.xplr 108 | [43]: https://github.com/sayanarijit/dual-pane.xplr 109 | [44]: https://github.com/sayanarijit/find.xplr 110 | [45]: awesome-hacks.md 111 | [46]: awesome-integrations.md 112 | [47]: https://github.com/dtomvan/xpm.xplr 113 | [48]: installing-plugins.md 114 | [49]: https://github.com/sayanarijit/registers.xplr 115 | [50]: https://github.com/dtomvan/extra-icons.xplr 116 | [51]: https://github.com/sayanarijit/offline-docs.xplr 117 | [52]: https://github.com/sayanarijit/wl-clipboard.xplr 118 | [53]: https://github.com/Junker/nuke.xplr 119 | [54]: https://github.com/sayanarijit/scp.xplr 120 | [55]: https://github.com/sayanarijit/regex-search.xplr 121 | [56]: https://github.com/sayanarijit/tri-pane.xplr 122 | [57]: https://github.com/emsquid/style.xplr 123 | [58]: style.md 124 | [59]: https://gitlab.com/hartan/web-devicons.xplr 125 | [60]: https://github.com/nvim-tree/nvim-web-devicons 126 | [61]: https://github.com/sayanarijit/tree-view.xplr 127 | [62]: https://github.com/duganchen/one-table-column.xplr 128 | [63]: https://github.com/dy-sh/dysh-style.xplr 129 | [64]: https://github.com/dy-sh/get-rid-of-index.xplr 130 | [65]: https://github.com/LordMZTE/udisks.xplr 131 | -------------------------------------------------------------------------------- /docs/en/src/environment-variables-and-pipes.md: -------------------------------------------------------------------------------- 1 | # Environment Variables and Pipes 2 | 3 | Alternative to `CallLua`, `CallLuaSilently` messages that call Lua functions, 4 | there are `Call0`, `CallSilently0`, `BashExec0`, `BashExecSilently0` messages 5 | that call shell commands. 6 | 7 | ### Example: Simple file opener using xdg-open and $XPLR_FOCUS_PATH 8 | 9 | ```lua 10 | xplr.config.modes.builtin.default.key_bindings.on_key["X"] = { 11 | help = "open", 12 | messages = { 13 | { 14 | BashExecSilently0 = [===[ 15 | xdg-open "${XPLR_FOCUS_PATH:?}" 16 | ]===], 17 | }, 18 | }, 19 | } 20 | ``` 21 | 22 | However, unlike the Lua functions, these shell commands have to read the useful 23 | information and send messages via environment variables and temporary files 24 | called "pipe"s. These environment variables and files are only available when 25 | a command is being executed. 26 | 27 | ### Example: Using Environment Variables and Pipes 28 | 29 | ```lua 30 | xplr.config.modes.builtin.default.key_bindings.on_key["space"] = { 31 | help = "ask name and greet", 32 | messages = { 33 | { 34 | BashExec0 = [===[ 35 | echo "What's your name?" 36 | 37 | read name 38 | greeting="Hello $name!" 39 | message="$greeting You are inside $PWD" 40 | 41 | "$XPLR" -m 'LogSuccess: %q' "$message" 42 | ]===] 43 | } 44 | } 45 | } 46 | 47 | -- Now, when you press "space" in default mode, you will be prompted for your 48 | -- name. Enter your name to receive a nice greeting and to know your location. 49 | ``` 50 | 51 | Visit the [**fzf integration tutorial**][19] for another example. 52 | 53 | To see the environment variables and pipes, invoke the shell by typing `:!` in default 54 | mode and run the following command: 55 | 56 | ``` 57 | env | grep ^XPLR 58 | ``` 59 | 60 | You will see something like: 61 | 62 | ``` 63 | XPLR=xplr 64 | XPLR_FOCUS_INDEX=0 65 | XPLR_MODE=action to 66 | XPLR_PIPE_SELECTION_OUT=/run/user/1000/xplr/session/122278/pipe/selection_out 67 | XPLR_INPUT_BUFFER= 68 | XPLR_PIPE_GLOBAL_HELP_MENU_OUT=/run/user/1000/xplr/session/122278/pipe/global_help_menu_out 69 | XPLR_PID=122278 70 | XPLR_PIPE_MSG_IN=/run/user/1000/xplr/session/122278/pipe/msg_in 71 | XPLR_PIPE_LOGS_OUT=/run/user/1000/xplr/session/122278/pipe/logs_out 72 | XPLR_PIPE_RESULT_OUT=/run/user/1000/xplr/session/122278/pipe/result_out 73 | XPLR_PIPE_HISTORY_OUT=/run/user/1000/xplr/session/122278/pipe/history_out 74 | XPLR_FOCUS_PATH=/home/sayanarijit/Documents/GitHub/xplr/docs/en/book 75 | XPLR_SESSION_PATH=/run/user/1000/xplr/session/122278 76 | XPLR_APP_VERSION=0.14.3 77 | XPLR_PIPE_DIRECTORY_NODES_OUT=/run/user/1000/xplr/session/122278/pipe/directory_nodes_out 78 | ``` 79 | 80 | The environment variables starting with `XPLR_PIPE_` are the temporary files 81 | called ["pipe"s][18]. 82 | 83 | The other variables are single-line variables containing simple information: 84 | 85 | - [XPLR][38] 86 | - [XPLR_APP_VERSION][30] 87 | - [XPLR_FOCUS_INDEX][31] 88 | - [XPLR_FOCUS_PATH][32] 89 | - [XPLR_INPUT_BUFFER][33] 90 | - [XPLR_INITIAL_PWD][40] 91 | - [XPLR_MODE][34] 92 | - [XPLR_PID][35] 93 | - [XPLR_SESSION_PATH][36] 94 | - [XPLR_VROOT][39] 95 | 96 | ### Environment variables 97 | 98 | #### XPLR 99 | 100 | The binary path of xplr command. 101 | 102 | #### XPLR_APP_VERSION 103 | 104 | Self-explanatory. 105 | 106 | #### XPLR_FOCUS_INDEX 107 | 108 | Contains the index of the currently focused item, as seen in 109 | [column-renderer/index][10]. 110 | 111 | #### XPLR_FOCUS_PATH 112 | 113 | Contains the full path of the currently focused node. 114 | 115 | #### XPLR_INITIAL_PWD 116 | 117 | The $PWD then xplr started. 118 | 119 | #### XPLR_INPUT_BUFFER 120 | 121 | The line currently in displaying in the xplr input buffer. For e.g. the search 122 | input while searching. See [Reading Input][37]. 123 | 124 | #### XPLR_MODE 125 | 126 | Contains the mode xplr is currently in, see [modes][11]. 127 | 128 | #### XPLR_PID 129 | 130 | Contains the process ID of the current xplr process. 131 | 132 | #### XPLR_SESSION_PATH 133 | 134 | Contains the current session path, like /tmp/runtime-"$USER"/xplr/session/"$XPLR_PID"/, 135 | you can find temporary files here, such as pipes. 136 | 137 | #### XPLR_VROOT 138 | 139 | Contains the path of current virtual root, is set. 140 | 141 | ### Pipes 142 | 143 | #### Input pipe 144 | 145 | Currently there is only one input pipe. 146 | 147 | - [XPLR_PIPE_MSG_IN][20] 148 | 149 | #### Output pipes 150 | 151 | `XPLR_PIPE_*_OUT` are the output pipes that contain data which cannot be 152 | exposed directly via environment variables, like multi-line strings. 153 | These pipes can be accessed as plain text files located in $XPLR_SESSION_PATH. 154 | 155 | Depending on the message (e.g. `Call` or `Call0`), each line will be separated 156 | by newline or null character (`\n` or `\0`). 157 | 158 | - [XPLR_PIPE_SELECTION_OUT][21] 159 | - [XPLR_PIPE_GLOBAL_HELP_MENU_OUT][22] 160 | - [XPLR_PIPE_LOGS_OUT][23] 161 | - [XPLR_PIPE_RESULT_OUT][24] 162 | - [XPLR_PIPE_HISTORY_OUT][25] 163 | - [XPLR_PIPE_DIRECTORY_NODES_OUT][26] 164 | 165 | #### XPLR_PIPE_MSG_IN 166 | 167 | Append new messages to this pipe in [YAML][27] (or [JSON][7]) syntax. These 168 | messages will be read and handled by xplr after the command execution. 169 | 170 | Depending on the message (e.g. `Call` or `Call0`) you need to separate each 171 | message using newline or null character (`\n` or `\0`). 172 | 173 | > **_NOTE:_** Since version `v0.20.0`, it's recommended to avoid writing 174 | > directly to this file, as safely escaping YAML strings is a lot of work. Use 175 | > `xplr -m` / `xplr --pipe-msg-in` to pass messages to xplr in a safer way. 176 | > 177 | > It uses [jf][41] syntax to safely convert an YAML template into a valid message. 178 | > 179 | > Example: `"$XPLR" -m 'ChangeDirectory: %q' "${HOME:?}"` 180 | 181 | #### XPLR_PIPE_SELECTION_OUT 182 | 183 | List of selected paths. 184 | 185 | #### XPLR_PIPE_GLOBAL_HELP_MENU_OUT 186 | 187 | The full help menu. 188 | 189 | #### XPLR_PIPE_LOGS_OUT 190 | 191 | List of logs. 192 | 193 | #### XPLR_PIPE_RESULT_OUT 194 | 195 | Result (selected paths if any, else the focused path) 196 | 197 | #### XPLR_PIPE_HISTORY_OUT 198 | 199 | List of last visited paths (similar to jump list in vim). 200 | 201 | #### XPLR_PIPE_DIRECTORY_NODES_OUT 202 | 203 | List of paths, filtered and sorted as displayed in the [files table][28]. 204 | 205 | [7]: https://www.json.org 206 | [10]: column-renderer.md#index 207 | [11]: modes.md#modes 208 | [18]: #pipes 209 | [19]: configure-key-bindings.md#tutorial-adding-a-new-mode 210 | [20]: #xplr_pipe_msg_in 211 | [21]: #xplr_pipe_selection_out 212 | [22]: #xplr_pipe_global_help_menu_out 213 | [23]: #xplr_pipe_logs_out 214 | [24]: #xplr_pipe_result_out 215 | [25]: #xplr_pipe_history_out 216 | [26]: #xplr_pipe_directory_nodes_out 217 | [27]: https://www.yaml.org 218 | [28]: layout.md#table 219 | [30]: #xplr_app_version 220 | [31]: #xplr_focus_index 221 | [32]: #xplr_focus_path 222 | [33]: #xplr_input_buffer 223 | [34]: #xplr_mode 224 | [35]: #xplr_pid 225 | [36]: #xplr_session_path 226 | [37]: messages.md#reading-input 227 | [38]: #xplr 228 | [39]: #xplr_vroot 229 | [40]: #xplr_initial_pwd 230 | [41]: https://github.com/sayanarijit/jf 231 | -------------------------------------------------------------------------------- /docs/en/src/column-renderer.md: -------------------------------------------------------------------------------- 1 | # Column Renderer 2 | 3 | A column renderer is a Lua function that receives a [special argument][1] and 4 | returns a string that will be displayed in each specific field of the 5 | [files table][2]. 6 | 7 | ## Example: Customizing Table Renderer 8 | 9 | ```lua 10 | xplr.fn.custom.fmt_simple_column = function(m) 11 | return m.prefix .. m.relative_path .. m.suffix 12 | end 13 | 14 | xplr.config.general.table.header.cols = { 15 | { format = " path" } 16 | } 17 | 18 | xplr.config.general.table.row.cols = { 19 | { format = "custom.fmt_simple_column" } 20 | } 21 | 22 | xplr.config.general.table.col_widths = { 23 | { Percentage = 100 } 24 | } 25 | 26 | -- With this config, you should only see a single column displaying the 27 | -- relative paths. 28 | ``` 29 | 30 | xplr by default provides the following column renderers: 31 | 32 | - `xplr.fn.builtin.fmt_general_table_row_cols_0` 33 | - `xplr.fn.builtin.fmt_general_table_row_cols_1` 34 | - `xplr.fn.builtin.fmt_general_table_row_cols_2` 35 | - `xplr.fn.builtin.fmt_general_table_row_cols_3` 36 | - `xplr.fn.builtin.fmt_general_table_row_cols_4` 37 | 38 | You can either overwrite these functions, or create new functions in 39 | `xplr.fn.custom` and point to them. 40 | 41 | Terminal colors are supported. 42 | 43 | ## Table Renderer Argument 44 | 45 | The special argument contains the following fields 46 | 47 | - [parent][3] 48 | - [relative_path][4] 49 | - [absolute_path][5] 50 | - [extension][6] 51 | - [is_symlink][7] 52 | - [is_broken][8] 53 | - [is_dir][9] 54 | - [is_file][10] 55 | - [is_readonly][11] 56 | - [mime_essence][12] 57 | - [size][13] 58 | - [human_size][14] 59 | - [permissions][15] 60 | - [created][34] 61 | - [last_modified][35] 62 | - [uid][36] 63 | - [gid][37] 64 | - [canonical][16] 65 | - [symlink][17] 66 | - [index][18] 67 | - [relative_index][19] 68 | - [is_before_focus][20] 69 | - [is_after_focus][21] 70 | - [tree][22] 71 | - [prefix][23] 72 | - [suffix][24] 73 | - [is_selected][25] 74 | - [is_focused][26] 75 | - [total][27] 76 | - [style][38] 77 | - [meta][28] 78 | 79 | ### parent 80 | 81 | Type: string 82 | 83 | The parent path of the node. 84 | 85 | ### relative_path 86 | 87 | Type: string 88 | 89 | The path relative to the parent, i.e. the file/directory name with extension. 90 | 91 | ### absolute_path 92 | 93 | Type: string 94 | 95 | The absolute path (without resolving symlinks) of the node. 96 | 97 | ### extension 98 | 99 | Type: string 100 | 101 | The extension of the node. 102 | 103 | ### is_symlink 104 | 105 | Type: boolean 106 | 107 | `true` if the node is a symlink. 108 | 109 | ### is_broken 110 | 111 | Type: boolean 112 | 113 | `true` if the node is a broken symlink. 114 | 115 | ### is_dir 116 | 117 | Type: boolean 118 | 119 | `true` if the node is a directory. 120 | 121 | ### is_file 122 | 123 | Type: boolean 124 | 125 | `true` if the node is a file. 126 | 127 | ### is_readonly 128 | 129 | Type: boolean 130 | 131 | `true` if the node is real-only. 132 | 133 | ### mime_essence 134 | 135 | Type: string 136 | 137 | The mime type of the node. For e.g. `text/csv`, `image/jpeg` etc. 138 | 139 | ### size 140 | 141 | Type: integer 142 | 143 | The size of the exact node. The size of a directory won't be calculated 144 | recursively. 145 | 146 | ### human_size 147 | 148 | Type: string 149 | 150 | Like [size][29] but in human readable format. 151 | 152 | ### permissions 153 | 154 | Type: [Permission][30] 155 | 156 | The [permissions][30] applied to the node. 157 | 158 | ### created 159 | 160 | Type: nullable integer 161 | 162 | Creation time in nanosecond since UNIX epoch. 163 | 164 | ### last_modified 165 | 166 | Type: nullable integer 167 | 168 | Last modification time in nanosecond since UNIX epoch. 169 | 170 | ### uid 171 | 172 | Type: integer 173 | 174 | User ID of the file owner. 175 | 176 | ### gid 177 | 178 | Type: integer 179 | 180 | Group ID of the file owner. 181 | 182 | ### canonical 183 | 184 | Type: nullable [Resolved Node Metadata][31] 185 | 186 | If the node is a symlink, it will hold information about the symlink resolved 187 | node. Else, it will hold information the actual node. It the symlink is broken, 188 | it will be null. 189 | 190 | ### symlink 191 | 192 | Type: nullable [Resolved Node Metadata][31] 193 | 194 | If the node is a symlink and is not broken, it will hold information about the 195 | symlink resolved node. However, it will never hold information about the actual 196 | node. It will instead be null. 197 | 198 | ### index 199 | 200 | Type: integer 201 | 202 | Index (starting from 0) of the node. 203 | 204 | ### relative_index 205 | 206 | Type: integer 207 | 208 | Relative index from the focused node (i.e. 0th node). 209 | 210 | ### is_before_focus 211 | 212 | Type: boolean 213 | 214 | `true` if the node is before the focused node. 215 | 216 | ### is_after_focus 217 | 218 | Type: boolean 219 | 220 | `true` if the node is after the focused node. 221 | 222 | ### tree 223 | 224 | Type: string 225 | 226 | The [tree component][32] based on the node's index. 227 | 228 | ### prefix 229 | 230 | Type: string 231 | 232 | The prefix applicable for the node. 233 | 234 | ### suffix 235 | 236 | Type: string 237 | 238 | The suffix applicable for the node. 239 | 240 | ### is_selected 241 | 242 | Type: boolean 243 | 244 | `true` if the node is selected. 245 | 246 | ### is_focused 247 | 248 | Type: boolean 249 | 250 | `true` if the node is under focus. 251 | 252 | ### total 253 | 254 | Type: integer 255 | 256 | The total number of the nodes. 257 | 258 | ### style 259 | 260 | Type: [Style][39] 261 | 262 | The applicable [style object][39] for the node. 263 | 264 | ### meta 265 | 266 | Type: mapping of string and string 267 | 268 | The applicable [meta object][33] for the node. 269 | 270 | ## Permission 271 | 272 | Permission contains the following fields: 273 | 274 | - user_read 275 | - user_write 276 | - user_execute 277 | - group_read 278 | - group_write 279 | - group_execute 280 | - other_read 281 | - other_write 282 | - other_execute 283 | - sticky 284 | - setgid 285 | - setuid 286 | 287 | Each field holds a boolean value. 288 | 289 | ## Resolved Node Metadata 290 | 291 | It contains the following fields. 292 | 293 | - [absolute_path][5] 294 | - [extension][6] 295 | - [is_dir][9] 296 | - [is_file][10] 297 | - [is_readonly][11] 298 | - [mime_essence][12] 299 | - [size][13] 300 | - [human_size][14] 301 | - [created][34] 302 | - [last_modified][35] 303 | - [uid][36] 304 | - [gid][37] 305 | 306 | [1]: #table-renderer-argument 307 | [2]: layout.md#table 308 | [3]: #parent 309 | [4]: #relative_path 310 | [5]: #absolute_path 311 | [6]: #extension 312 | [7]: #is_symlink 313 | [8]: #is_broken 314 | [9]: #is_dir 315 | [10]: #is_file 316 | [11]: #is_readonly 317 | [12]: #mime_essence 318 | [13]: #size 319 | [14]: #human_size 320 | [15]: #permissions 321 | [16]: #canonical 322 | [17]: #symlink 323 | [18]: #index 324 | [19]: #relative_index 325 | [20]: #is_before_focus 326 | [21]: #is_after_focus 327 | [22]: #tree 328 | [23]: #prefix 329 | [24]: #suffix 330 | [25]: #is_selected 331 | [26]: #is_focused 332 | [27]: #total 333 | [28]: #meta 334 | [29]: #size 335 | [30]: #permission 336 | [31]: #resolved-node-metadata 337 | [32]: general-config.md#xplrconfiggeneraltabletree 338 | [33]: node-type.md#meta 339 | [34]: #created 340 | [35]: #last_modified 341 | [36]: #uid 342 | [37]: #gid 343 | [38]: #style 344 | [39]: style.md#style 345 | -------------------------------------------------------------------------------- /src/bin/xplr.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | 3 | use std::env; 4 | use xplr::cli::{self, Cli}; 5 | use xplr::runner; 6 | 7 | fn main() { 8 | let cli = Cli::parse(env::args()).unwrap_or_else(|e| { 9 | eprintln!("error: {e}"); 10 | std::process::exit(1); 11 | }); 12 | 13 | if cli.help { 14 | let usage = r###" 15 | xplr [FLAG]... [OPTION]... [PATH] [SELECTION]..."###; 16 | 17 | let flags = r###" 18 | - Reads new-line (\n) separated paths from stdin 19 | -- Denotes the end of command-line flags and options 20 | --force-focus Focuses on the given , even if it is a directory 21 | -h, --help Prints help information 22 | -m, --pipe-msg-in Helps safely passing messages to the active xplr 23 | session, use %%, %s and %q as the placeholders 24 | -M, --print-msg-in Like --pipe-msg-in, but prints the message instead of 25 | passing to the active xplr session 26 | --print-pwd-as-result Prints the present working directory when quitting 27 | with `PrintResultAndQuit` 28 | --read-only Enables read-only mode (config.general.read_only) 29 | --read0 Reads paths separated using the null character (\0) 30 | --write0 Prints paths separated using the null character (\0) 31 | -0 --null Combines --read0 and --write0 32 | -V, --version Prints version information"###; 33 | 34 | let options = r###" 35 | -c, --config Specifies a custom config file (default is 36 | "$HOME/.config/xplr/init.lua") 37 | -C, --extra-config ... Specifies extra config files to load 38 | --on-load ... Sends messages when xplr loads 39 | --vroot Treats the specified path as the virtual root"###; 40 | 41 | let args = r###" 42 | Path to focus on, or enter if directory, (default is `.`) 43 | ... Paths to select, requires to be set explicitly"###; 44 | 45 | let help = format!( 46 | "xplr {}\n{}\n{}\n\nUSAGE:{}\n\nFLAGS:{}\n\nOPTIONS:{}\n\nARGS:{}", 47 | xplr::app::VERSION, 48 | env!("CARGO_PKG_AUTHORS"), 49 | env!("CARGO_PKG_DESCRIPTION"), 50 | usage, 51 | flags, 52 | options, 53 | args, 54 | ); 55 | let help = help.trim(); 56 | 57 | println!("{help}"); 58 | } else if cli.version { 59 | println!("xplr {}", xplr::app::VERSION); 60 | } else if !cli.pipe_msg_in.is_empty() { 61 | if let Err(err) = cli::pipe_msg_in(cli.pipe_msg_in) { 62 | eprintln!("error: {err}"); 63 | std::process::exit(1); 64 | } 65 | } else if !cli.print_msg_in.is_empty() { 66 | if let Err(err) = cli::print_msg_in(cli.print_msg_in) { 67 | eprintln!("error: {err}"); 68 | std::process::exit(1); 69 | } 70 | } else { 71 | match runner::from_cli(cli).and_then(|a| a.run()) { 72 | Ok(Some(out)) => { 73 | print!("{out}"); 74 | } 75 | Ok(None) => {} 76 | Err(err) => { 77 | if !err.to_string().is_empty() { 78 | eprintln!("error: {err}"); 79 | }; 80 | 81 | std::process::exit(1); 82 | } 83 | } 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use assert_cmd::Command; 90 | 91 | #[test] 92 | fn test_no_debug_in_lib() { 93 | // for pat in ["print!", "println!"].iter() { 94 | // Command::new("grep") 95 | // .args(&[ 96 | // "-R", 97 | // pat, 98 | // "src", 99 | // "--exclude-dir", 100 | // "bin/", 101 | // "--exclude-dir", 102 | // "rustc/", 103 | // ]) 104 | // .assert() 105 | // .failure(); 106 | // } 107 | // 108 | // TODO: fix github macos runner 109 | } 110 | 111 | #[test] 112 | fn test_cli_version() { 113 | Command::cargo_bin("xplr") 114 | .unwrap() 115 | .arg("--version") 116 | .assert() 117 | .success() 118 | .code(0) 119 | .stdout(format!("xplr {}\n", xplr::app::VERSION)) 120 | .stderr(""); 121 | 122 | Command::cargo_bin("xplr") 123 | .unwrap() 124 | .arg("-V") 125 | .assert() 126 | .success() 127 | .code(0) 128 | .stdout(format!("xplr {}\n", xplr::app::VERSION)) 129 | .stderr(""); 130 | } 131 | 132 | #[test] 133 | fn test_cli_help() { 134 | Command::cargo_bin("xplr") 135 | .unwrap() 136 | .arg("-h") 137 | .assert() 138 | .success() 139 | .code(0) 140 | .stderr(""); 141 | 142 | Command::cargo_bin("xplr") 143 | .unwrap() 144 | .arg("--help") 145 | .assert() 146 | .success() 147 | .code(0) 148 | .stderr(""); 149 | } 150 | 151 | // TODO fix GitHub CI failures 152 | // 153 | // #[test] 154 | // fn test_cli_path_arg_valid() { 155 | // Command::cargo_bin("xplr") 156 | // .unwrap() 157 | // .arg("src") 158 | // .arg("--on-load") 159 | // .arg("PrintResultAndQuit") 160 | // .assert() 161 | // .success() 162 | // .code(0) 163 | // .stderr(""); 164 | // 165 | // Command::cargo_bin("xplr") 166 | // .unwrap() 167 | // .arg("src") 168 | // .arg("--on-load") 169 | // .arg("PrintResultAndQuit") 170 | // .assert() 171 | // .success() 172 | // .code(0) 173 | // .stderr(""); 174 | // 175 | // Command::cargo_bin("xplr") 176 | // .unwrap() 177 | // .arg("--on-load") 178 | // .arg("PrintResultAndQuit") 179 | // .arg("--") 180 | // .arg("src") 181 | // .assert() 182 | // .success() 183 | // .code(0) 184 | // .stderr(""); 185 | // } 186 | // 187 | // #[test] 188 | // fn test_cli_path_stdin_valid() { 189 | // Command::cargo_bin("xplr") 190 | // .unwrap() 191 | // .arg("-") 192 | // .arg("--on-load") 193 | // .arg("PrintResultAndQuit") 194 | // .write_stdin("src\n") 195 | // .assert() 196 | // .success() 197 | // .code(0) 198 | // .stderr(""); 199 | // } 200 | // 201 | // #[test] 202 | // fn test_on_load_yaml_parsing() { 203 | // Command::cargo_bin("xplr") 204 | // .unwrap() 205 | // .arg("--on-load") 206 | // .arg("Call: {command: touch, args: [foo]}") 207 | // .arg("Quit") 208 | // .assert() 209 | // .success() 210 | // .code(0) 211 | // .stderr(""); 212 | // 213 | // std::fs::remove_file("foo").unwrap(); 214 | // } 215 | } 216 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::{app, yaml}; 2 | use anyhow::{bail, Context, Result}; 3 | use app::ExternalMsg; 4 | use path_absolutize::*; 5 | use serde_json as json; 6 | use std::fs::File; 7 | use std::io::{BufRead, BufReader, Write}; 8 | use std::path::PathBuf; 9 | use std::{env, fs}; 10 | 11 | /// The arguments to pass 12 | #[derive(Debug, Clone, Default)] 13 | pub struct Cli { 14 | pub bin: String, 15 | pub version: bool, 16 | pub help: bool, 17 | pub read_only: bool, 18 | pub force_focus: bool, 19 | pub print_pwd_as_result: bool, 20 | pub read0: bool, 21 | pub write0: bool, 22 | pub vroot: Option, 23 | pub config: Option, 24 | pub extra_config: Vec, 25 | pub on_load: Vec, 26 | pub pipe_msg_in: Vec, 27 | pub print_msg_in: Vec, 28 | pub paths: Vec, 29 | } 30 | 31 | impl Cli { 32 | fn read_path(arg: &str) -> Result { 33 | if arg.is_empty() { 34 | bail!("empty string passed") 35 | }; 36 | 37 | let path = PathBuf::from(arg).absolutize()?.to_path_buf(); 38 | if path.exists() { 39 | Ok(path) 40 | } else { 41 | bail!("path doesn't exist: {}", path.to_string_lossy()) 42 | } 43 | } 44 | 45 | /// Parse arguments from the command-line 46 | pub fn parse(args: env::Args) -> Result { 47 | let mut cli = Self::default(); 48 | let mut args = args.peekable(); 49 | cli.bin = args 50 | .next() 51 | .map(which::which) 52 | .context("failed to parse xplr binary path")? 53 | .context("failed to find xplr binary path")? 54 | .absolutize()? 55 | .to_path_buf() 56 | .to_string_lossy() 57 | .to_string(); 58 | 59 | let mut flag_ends = false; 60 | 61 | while let Some(arg) = args.next() { 62 | if flag_ends { 63 | cli.paths.push(Cli::read_path(&arg)?); 64 | } else { 65 | match arg.as_str() { 66 | // Flags 67 | "-" => { 68 | let reader = BufReader::new(std::io::stdin()); 69 | if cli.read0 { 70 | for path in reader.split(b'\0') { 71 | cli.paths 72 | .push(Cli::read_path(&String::from_utf8(path?)?)?); 73 | } 74 | } else { 75 | for path in reader.lines() { 76 | cli.paths.push(Cli::read_path(&path?)?); 77 | } 78 | }; 79 | } 80 | 81 | "-h" | "--help" => { 82 | cli.help = true; 83 | } 84 | 85 | "-V" | "--version" => { 86 | cli.version = true; 87 | } 88 | 89 | "--read0" => { 90 | cli.read0 = true; 91 | } 92 | 93 | "--write0" => { 94 | cli.write0 = true; 95 | } 96 | 97 | "-0" | "--null" => { 98 | cli.read0 = true; 99 | cli.write0 = true; 100 | } 101 | 102 | "--" => { 103 | flag_ends = true; 104 | } 105 | 106 | // Options 107 | "-c" | "--config" => { 108 | cli.config = Some( 109 | args.next() 110 | .map(|a| Cli::read_path(&a)) 111 | .with_context(|| format!("usage: xplr {arg} PATH"))??, 112 | ); 113 | } 114 | 115 | "--vroot" => { 116 | cli.vroot = Some( 117 | args.next() 118 | .map(|a| Cli::read_path(&a)) 119 | .with_context(|| format!("usage: xplr {arg} PATH"))??, 120 | ); 121 | } 122 | 123 | "-C" | "--extra-config" => { 124 | while let Some(path) = 125 | args.next_if(|path| !path.starts_with('-')) 126 | { 127 | cli.extra_config.push(Cli::read_path(&path)?); 128 | } 129 | } 130 | 131 | "--read-only" => cli.read_only = true, 132 | 133 | "--on-load" => { 134 | while let Some(msg) = args.next_if(|msg| !msg.starts_with('-')) { 135 | cli.on_load.push(yaml::from_str(&msg)?); 136 | } 137 | } 138 | 139 | "--force-focus" => { 140 | cli.force_focus = true; 141 | } 142 | 143 | "--print-pwd-as-result" => { 144 | cli.print_pwd_as_result = true; 145 | } 146 | 147 | "-m" | "--pipe-msg-in" => { 148 | cli.pipe_msg_in.extend(args.by_ref()); 149 | if cli.pipe_msg_in.is_empty() { 150 | bail!("usage: xplr {} FORMAT [ARGUMENT]...", arg) 151 | } 152 | } 153 | 154 | "-M" | "--print-msg-in" => { 155 | cli.print_msg_in.extend(args.by_ref()); 156 | if cli.print_msg_in.is_empty() { 157 | bail!("usage: xplr {} FORMAT [ARGUMENT]...", arg) 158 | } 159 | } 160 | 161 | // path 162 | path => { 163 | if path.starts_with('-') && !flag_ends { 164 | bail!( 165 | "invalid argument: {0:?}, try `-- {0:?}` or `--help`", 166 | path 167 | ) 168 | } else { 169 | cli.paths.push(Cli::read_path(path)?); 170 | } 171 | } 172 | } 173 | } 174 | } 175 | Ok(cli) 176 | } 177 | } 178 | 179 | pub fn pipe_msg_in(args: Vec) -> Result<()> { 180 | let mut msg = fmt_msg_in(args)?; 181 | 182 | if let Ok(path) = std::env::var("XPLR_PIPE_MSG_IN") { 183 | let delimiter = fs::read(&path)? 184 | .first() 185 | .cloned() 186 | .context("failed to detect delimmiter")?; 187 | 188 | msg.push(delimiter.into()); 189 | File::options() 190 | .append(true) 191 | .open(&path)? 192 | .write_all(msg.as_bytes())?; 193 | } else { 194 | println!("{msg}"); 195 | }; 196 | 197 | Ok(()) 198 | } 199 | 200 | pub fn print_msg_in(args: Vec) -> Result<()> { 201 | let msg = fmt_msg_in(args)?; 202 | print!("{msg}"); 203 | Ok(()) 204 | } 205 | 206 | fn fmt_msg_in(args: Vec) -> Result { 207 | let msg = match jf::format(args.into_iter().map(Into::into)) { 208 | Ok(msg) => msg, 209 | Err(jf::Error::Jf(e)) => bail!("xplr -m: {e}"), 210 | Err(jf::Error::Json(e)) => bail!("xplr -m: json: {e}"), 211 | Err(jf::Error::Yaml(e)) => bail!("xplr -m: yaml: {e}"), 212 | Err(jf::Error::Io(e)) => bail!("xplr -m: io: {e}"), 213 | }; 214 | 215 | // validate 216 | let _: ExternalMsg = json::from_str(&msg)?; 217 | 218 | Ok(msg) 219 | } 220 | -------------------------------------------------------------------------------- /docs/landing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | xplr - A hackable, minimal, fast TUI file explorer 11 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 32 | 33 | 34 | 40 | 46 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 88 | 89 | 90 | 94 | 95 | 96 | 97 | 101 | 102 | 106 | 107 | 108 | 112 | 113 | 114 | 115 | 116 |
117 |
118 | 169 | 170 |
171 |
172 |
173 | 174 |
175 |
176 |
182 |
183 |
184 | 185 | 186 |
187 |
188 |
189 |
190 |

xplr

191 |

A hackable, minimal, fast TUI file explorer

192 | 193 |
194 | 200 |
201 |
202 |
203 | 204 | 209 | Try or Install 210 | 211 | 212 |
213 |
214 |
215 |
216 | 217 | 218 |
219 | 220 |
221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | -------------------------------------------------------------------------------- /docs/script/generate.py: -------------------------------------------------------------------------------- 1 | """Generate docs from comments.""" 2 | 3 | import os 4 | from dataclasses import dataclass 5 | from typing import List 6 | 7 | # Messages -------------------------------------------------------------------- 8 | 9 | MESSAGES_DOC_TEMPLATE = """ 10 | # Full List of Messages 11 | 12 | xplr [messages][1] categorized based on their purpose. 13 | 14 | ## Categories 15 | 16 | {categories} 17 | 18 | {msgs} 19 | 20 | ## Also See: 21 | 22 | - [Message][1] 23 | 24 | [1]: message.md 25 | """.strip() 26 | 27 | CONFIGURATION_DOC_TEMPLATE = """ 28 | # Configuration 29 | 30 | {doc} 31 | 32 | """.strip() 33 | 34 | 35 | @dataclass 36 | class MsgSection: 37 | title: str | None 38 | body: List[str] 39 | 40 | 41 | @dataclass 42 | class MsgCategory: 43 | title: str 44 | sections: List[MsgSection] 45 | 46 | 47 | @dataclass 48 | class MsgResult: 49 | categories: List[MsgCategory] 50 | msgs: List[str] 51 | 52 | 53 | def gen_messages(): 54 | """Generate messages.md""" 55 | 56 | path = "./src/msg/in_/external.rs" 57 | res = [] 58 | reading = False 59 | 60 | with open(path) as f: 61 | lines = iter(f.read().splitlines()) 62 | 63 | for line in lines: 64 | line = line.strip() 65 | 66 | if line.startswith("pub enum ExternalMsg {"): 67 | reading = True 68 | continue 69 | 70 | if not reading: 71 | continue 72 | 73 | if line == "}": 74 | break 75 | 76 | if line.startswith("/// ### "): 77 | line = line.lstrip("/// ### ").rstrip("-").strip() 78 | sec = MsgSection(title=None, body=[]) 79 | cat = MsgCategory(title=line, sections=[sec]) 80 | res.append(cat) 81 | continue 82 | 83 | if line.startswith("/// "): 84 | line = line.lstrip("/// ").strip() 85 | res[-1].sections[-1].body.append(line) 86 | continue 87 | 88 | if not line or line == "///": 89 | res[-1].sections[-1].body.append("") 90 | continue 91 | 92 | if line.endswith(","): 93 | line = line.split(",")[0].split("(")[0] 94 | res[-1].sections[-1].title = line 95 | 96 | sec = MsgSection(title=None, body=[]) 97 | res[-1].sections.append(sec) 98 | continue 99 | 100 | result = MsgResult(categories=[], msgs=[]) 101 | 102 | for cat in res: 103 | slug = cat.title.lower().replace(" ", "-") 104 | result.categories.append( 105 | MsgCategory(title=f"- [{cat.title}](#{slug})", sections=[]) 106 | ) 107 | result.msgs.append(f"### {cat.title}") 108 | result.msgs.append("") 109 | 110 | for sec in cat.sections: 111 | if not sec.title: 112 | continue 113 | 114 | result.msgs.append(f"#### {sec.title}") 115 | result.msgs.append("") 116 | for line in sec.body: 117 | result.msgs.append(f"{line}") 118 | result.msgs.append("") 119 | 120 | messages = MESSAGES_DOC_TEMPLATE.format( 121 | categories="\n".join(c.title for c in result.categories), 122 | msgs="\n".join(result.msgs), 123 | ) 124 | 125 | print(messages) 126 | with open("./docs/en/src/messages.md", "w") as f: 127 | print(messages, file=f) 128 | 129 | 130 | # Configuration --------------------------------------------------------------- 131 | 132 | 133 | def gen_configuration(): 134 | """Generate the following docs. 135 | 136 | - configuration.md 137 | - general-config.md 138 | - node_types.md 139 | - layouts.md 140 | - modes.md 141 | - modes.md 142 | """ 143 | 144 | path = "./src/init.lua" 145 | 146 | configuration = [[]] 147 | general = [[]] 148 | node_types = [[]] 149 | layouts = [[]] 150 | modes = [[]] 151 | 152 | with open(path) as f: 153 | lines = iter(f.read().splitlines()) 154 | 155 | reading = None 156 | 157 | for line in lines: 158 | if line.startswith("---"): 159 | continue 160 | 161 | if ( 162 | line.startswith("-- # Configuration ") 163 | or line.startswith("-- ## Config ") 164 | or line.startswith("-- ## Function ") 165 | or line.startswith("-- ## On Load ") 166 | ): 167 | reading = configuration 168 | 169 | if line.startswith("-- ### General Configuration "): 170 | reading = general 171 | 172 | if line.startswith("-- ### Node Types "): 173 | reading = node_types 174 | 175 | if line.startswith("-- ### Layouts "): 176 | reading = layouts 177 | 178 | if line.startswith("-- ### Modes "): 179 | reading = modes 180 | 181 | if not reading: 182 | continue 183 | 184 | if line.startswith("-- ") or line == "--": 185 | if line.startswith("-- #") and line.endswith("--"): 186 | line = "\n{0}\n".format(line.rstrip("-")) 187 | 188 | reading[-1].append(line[3:]) 189 | continue 190 | 191 | if line.startswith("xplr.") and reading[-1]: 192 | reading[-1].insert(0, "\n#### {0}\n".format(line.split()[0])) 193 | continue 194 | 195 | if not line.strip() and reading[-1]: 196 | reading.append([]) 197 | continue 198 | 199 | with open("./docs/en/src/configuration.md", "w") as f: 200 | doc = "\n".join(["\n".join(c) for c in configuration]) 201 | print(doc) 202 | print(doc, file=f) 203 | 204 | with open("./docs/en/src/general-config.md", "w") as f: 205 | doc = "\n".join(["\n".join(c) for c in general]) 206 | print(doc) 207 | print(doc, file=f) 208 | 209 | with open("./docs/en/src/node_types.md", "w") as f: 210 | doc = "\n".join(["\n".join(c) for c in node_types]) 211 | print(doc) 212 | print(doc, file=f) 213 | 214 | with open("./docs/en/src/layouts.md", "w") as f: 215 | doc = "\n".join(["\n".join(c) for c in layouts]) 216 | print(doc) 217 | print(doc, file=f) 218 | 219 | with open("./docs/en/src/modes.md", "w") as f: 220 | doc = "\n".join(["\n".join(c) for c in modes]) 221 | print(doc) 222 | print(doc, file=f) 223 | 224 | 225 | # xplr.util ------------------------------------------------------------------- 226 | 227 | 228 | @dataclass 229 | class Function: 230 | doc: List[str] 231 | name: str 232 | 233 | 234 | def gen_xplr_util(): 235 | 236 | path = "./src/lua/util.rs" 237 | 238 | functions: List[Function] = [] 239 | 240 | with open(path) as f: 241 | lines = iter(f.read().splitlines()) 242 | 243 | reading = None 244 | 245 | for line in lines: 246 | if line.startswith("///"): 247 | if reading: 248 | reading.doc.append(line[4:]) 249 | else: 250 | reading = Function(doc=[line[4:]], name="") 251 | 252 | if line.startswith("pub fn") and reading: 253 | reading.name = "\n### xplr.util." + line.split("(", 1)[0].split()[-1] + "\n" 254 | functions.append(reading) 255 | reading = None 256 | continue 257 | 258 | with open("./docs/en/src/xplr.util.md", "w") as f: 259 | for function in functions: 260 | print(function.name) 261 | print(function.name, file=f) 262 | 263 | print("\n".join(function.doc)) 264 | print("\n".join(function.doc), file=f) 265 | 266 | if reading: 267 | print("\n".join(reading.doc), file=f) 268 | 269 | 270 | def format_docs(): 271 | os.system("prettier --write docs/en/src") 272 | 273 | 274 | def main(): 275 | gen_messages() 276 | gen_configuration() 277 | gen_xplr_util() 278 | format_docs() 279 | 280 | 281 | if __name__ == "__main__": 282 | main() 283 | -------------------------------------------------------------------------------- /src/compat.rs: -------------------------------------------------------------------------------- 1 | // Things of the past, mostly bad decisions, which cannot erased, stays in this 2 | // haunted module. 3 | 4 | use crate::app; 5 | use crate::lua; 6 | use crate::ui::block; 7 | use crate::ui::string_to_text; 8 | use crate::ui::Constraint; 9 | use crate::ui::ContentRendererArg; 10 | use crate::ui::UI; 11 | use serde::{Deserialize, Serialize}; 12 | use tui::layout::Constraint as TuiConstraint; 13 | use tui::layout::Rect as TuiRect; 14 | use tui::widgets::Cell; 15 | use tui::widgets::List; 16 | use tui::widgets::ListItem; 17 | use tui::widgets::Paragraph; 18 | use tui::widgets::Row; 19 | use tui::widgets::Table; 20 | use tui::Frame; 21 | 22 | /// A cursed enum from crate::ui. 23 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 24 | #[serde(deny_unknown_fields)] 25 | pub enum ContentBody { 26 | /// A paragraph to render 27 | StaticParagraph { render: String }, 28 | 29 | /// A Lua function that returns a paragraph to render 30 | DynamicParagraph { render: String }, 31 | 32 | /// List to render 33 | StaticList { render: Vec }, 34 | 35 | /// A Lua function that returns lines to render 36 | DynamicList { render: String }, 37 | 38 | /// A table to render 39 | StaticTable { 40 | widths: Vec, 41 | col_spacing: Option, 42 | render: Vec>, 43 | }, 44 | 45 | /// A Lua function that returns a table to render 46 | DynamicTable { 47 | widths: Vec, 48 | col_spacing: Option, 49 | render: String, 50 | }, 51 | } 52 | 53 | /// A cursed struct from crate::ui. 54 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 55 | #[serde(deny_unknown_fields)] 56 | pub struct CustomContent { 57 | pub title: Option, 58 | pub body: ContentBody, 59 | } 60 | 61 | /// A cursed function from crate::ui. 62 | pub fn draw_custom_content( 63 | ui: &mut UI, 64 | f: &mut Frame, 65 | layout_size: TuiRect, 66 | app: &app::App, 67 | content: CustomContent, 68 | ) { 69 | let config = app.config.general.panel_ui.default.clone(); 70 | let title = content.title; 71 | let body = content.body; 72 | 73 | match body { 74 | ContentBody::StaticParagraph { render } => { 75 | let render = string_to_text(render); 76 | let content = Paragraph::new(render).block(block( 77 | config, 78 | title.map(|t| format!(" {t} ")).unwrap_or_default(), 79 | )); 80 | f.render_widget(content, layout_size); 81 | } 82 | 83 | ContentBody::DynamicParagraph { render } => { 84 | let ctx = ContentRendererArg { 85 | app: app.to_lua_ctx_light(), 86 | layout_size: layout_size.into(), 87 | screen_size: ui.screen_size.into(), 88 | scrolltop: ui.scrolltop as u16, 89 | }; 90 | 91 | let render = lua::serialize(ui.lua, &ctx) 92 | .map(|arg| { 93 | lua::call(ui.lua, &render, arg).unwrap_or_else(|e| format!("{e:?}")) 94 | }) 95 | .unwrap_or_else(|e| e.to_string()); 96 | 97 | let render = string_to_text(render); 98 | 99 | let content = Paragraph::new(render).block(block( 100 | config, 101 | title.map(|t| format!(" {t} ")).unwrap_or_default(), 102 | )); 103 | f.render_widget(content, layout_size); 104 | } 105 | 106 | ContentBody::StaticList { render } => { 107 | let items = render 108 | .into_iter() 109 | .map(string_to_text) 110 | .map(ListItem::new) 111 | .collect::>(); 112 | 113 | let content = List::new(items).block(block( 114 | config, 115 | title.map(|t| format!(" {t} ")).unwrap_or_default(), 116 | )); 117 | f.render_widget(content, layout_size); 118 | } 119 | 120 | ContentBody::DynamicList { render } => { 121 | let ctx = ContentRendererArg { 122 | app: app.to_lua_ctx_light(), 123 | layout_size: layout_size.into(), 124 | screen_size: ui.screen_size.into(), 125 | scrolltop: ui.scrolltop as u16, 126 | }; 127 | 128 | let items = lua::serialize(ui.lua, &ctx) 129 | .map(|arg| { 130 | lua::call(ui.lua, &render, arg) 131 | .unwrap_or_else(|e| vec![format!("{e:?}")]) 132 | }) 133 | .unwrap_or_else(|e| vec![e.to_string()]) 134 | .into_iter() 135 | .map(string_to_text) 136 | .map(ListItem::new) 137 | .collect::>(); 138 | 139 | let content = List::new(items).block(block( 140 | config, 141 | title.map(|t| format!(" {t} ")).unwrap_or_default(), 142 | )); 143 | f.render_widget(content, layout_size); 144 | } 145 | 146 | ContentBody::StaticTable { 147 | widths, 148 | col_spacing, 149 | render, 150 | } => { 151 | let rows = render 152 | .into_iter() 153 | .map(|cols| { 154 | Row::new( 155 | cols.into_iter() 156 | .map(string_to_text) 157 | .map(Cell::from) 158 | .collect::>(), 159 | ) 160 | }) 161 | .collect::>(); 162 | 163 | let widths = widths 164 | .into_iter() 165 | .map(|w| w.to_tui(ui.screen_size, layout_size)) 166 | .collect::>(); 167 | 168 | let content = Table::new(rows, widths) 169 | .column_spacing(col_spacing.unwrap_or(1)) 170 | .block(block( 171 | config, 172 | title.map(|t| format!(" {t} ")).unwrap_or_default(), 173 | )); 174 | 175 | f.render_widget(content, layout_size); 176 | } 177 | 178 | ContentBody::DynamicTable { 179 | widths, 180 | col_spacing, 181 | render, 182 | } => { 183 | let ctx = ContentRendererArg { 184 | app: app.to_lua_ctx_light(), 185 | layout_size: layout_size.into(), 186 | screen_size: ui.screen_size.into(), 187 | scrolltop: ui.scrolltop as u16, 188 | }; 189 | 190 | let rows = lua::serialize(ui.lua, &ctx) 191 | .map(|arg| { 192 | lua::call(ui.lua, &render, arg) 193 | .unwrap_or_else(|e| vec![vec![format!("{e:?}")]]) 194 | }) 195 | .unwrap_or_else(|e| vec![vec![e.to_string()]]) 196 | .into_iter() 197 | .map(|cols| { 198 | Row::new( 199 | cols.into_iter() 200 | .map(string_to_text) 201 | .map(Cell::from) 202 | .collect::>(), 203 | ) 204 | }) 205 | .collect::>(); 206 | 207 | let widths = widths 208 | .into_iter() 209 | .map(|w| w.to_tui(ui.screen_size, layout_size)) 210 | .collect::>(); 211 | 212 | let mut content = Table::new(rows, &widths).block(block( 213 | config, 214 | title.map(|t| format!(" {t} ")).unwrap_or_default(), 215 | )); 216 | 217 | if let Some(col_spacing) = col_spacing { 218 | content = content.column_spacing(col_spacing); 219 | }; 220 | 221 | f.render_widget(content, layout_size); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use crate::permissions::Permissions; 2 | use humansize::{format_size, DECIMAL}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::cmp::Ordering; 5 | use std::os::unix::prelude::MetadataExt; 6 | use std::path::{Path, PathBuf}; 7 | use std::time::UNIX_EPOCH; 8 | 9 | fn to_human_size(size: u64) -> String { 10 | format_size(size, DECIMAL) 11 | } 12 | 13 | fn mime_essence( 14 | path: &Path, 15 | is_dir: bool, 16 | extension: &str, 17 | is_executable: bool, 18 | ) -> String { 19 | if is_dir { 20 | String::from("inode/directory") 21 | } else if extension.is_empty() && is_executable { 22 | String::from("application/x-executable") 23 | } else { 24 | mime_guess::from_path(path) 25 | .first() 26 | .map(|m| m.essence_str().to_string()) 27 | .unwrap_or_default() 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 32 | pub struct ResolvedNode { 33 | pub absolute_path: String, 34 | pub extension: String, 35 | pub is_dir: bool, 36 | pub is_file: bool, 37 | pub is_readonly: bool, 38 | pub mime_essence: String, 39 | pub size: u64, 40 | pub human_size: String, 41 | pub created: Option, 42 | pub last_modified: Option, 43 | pub uid: u32, 44 | pub gid: u32, 45 | } 46 | 47 | impl ResolvedNode { 48 | pub fn from(path: PathBuf) -> Self { 49 | let extension = path 50 | .extension() 51 | .map(|e| e.to_string_lossy().to_string()) 52 | .unwrap_or_default(); 53 | 54 | let ( 55 | is_dir, 56 | is_file, 57 | is_readonly, 58 | size, 59 | permissions, 60 | created, 61 | last_modified, 62 | uid, 63 | gid, 64 | ) = path 65 | .metadata() 66 | .map(|m| { 67 | ( 68 | m.is_dir(), 69 | m.is_file(), 70 | m.permissions().readonly(), 71 | m.len(), 72 | Permissions::from(&m), 73 | m.created() 74 | .ok() 75 | .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) 76 | .map(|d| d.as_nanos()), 77 | m.modified() 78 | .ok() 79 | .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) 80 | .map(|d| d.as_nanos()), 81 | m.uid(), 82 | m.gid(), 83 | ) 84 | }) 85 | .unwrap_or((false, false, false, 0, Default::default(), None, None, 0, 0)); 86 | 87 | let is_executable = permissions.user_execute 88 | || permissions.group_execute 89 | || permissions.other_execute; 90 | let mime_essence = mime_essence(&path, is_dir, &extension, is_executable); 91 | let human_size = to_human_size(size); 92 | 93 | Self { 94 | absolute_path: path.to_string_lossy().to_string(), 95 | extension, 96 | is_dir, 97 | is_file, 98 | is_readonly, 99 | mime_essence, 100 | size, 101 | human_size, 102 | created, 103 | last_modified, 104 | uid, 105 | gid, 106 | } 107 | } 108 | } 109 | 110 | #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] 111 | pub struct Node { 112 | pub parent: String, 113 | pub relative_path: String, 114 | pub absolute_path: String, 115 | pub extension: String, 116 | pub is_dir: bool, 117 | pub is_file: bool, 118 | pub is_symlink: bool, 119 | pub is_broken: bool, 120 | pub is_readonly: bool, 121 | pub mime_essence: String, 122 | pub size: u64, 123 | pub human_size: String, 124 | pub permissions: Permissions, 125 | pub created: Option, 126 | pub last_modified: Option, 127 | pub uid: u32, 128 | pub gid: u32, 129 | 130 | pub canonical: Option, 131 | pub symlink: Option, 132 | } 133 | 134 | impl Node { 135 | pub fn new(parent: String, relative_path: String) -> Self { 136 | let absolute_path = PathBuf::from(&parent) 137 | .join(&relative_path) 138 | .to_string_lossy() 139 | .to_string(); 140 | 141 | let path = PathBuf::from(&absolute_path); 142 | 143 | let extension = path 144 | .extension() 145 | .map(|e| e.to_string_lossy().to_string()) 146 | .unwrap_or_default(); 147 | 148 | let (is_broken, maybe_canonical_meta) = path 149 | .canonicalize() 150 | .map(|p| (false, Some(ResolvedNode::from(p)))) 151 | .unwrap_or_else(|_| (true, None)); 152 | 153 | let ( 154 | is_symlink, 155 | is_dir, 156 | is_file, 157 | is_readonly, 158 | size, 159 | permissions, 160 | created, 161 | last_modified, 162 | uid, 163 | gid, 164 | ) = path 165 | .symlink_metadata() 166 | .map(|m| { 167 | ( 168 | m.file_type().is_symlink(), 169 | m.is_dir(), 170 | m.is_file(), 171 | m.permissions().readonly(), 172 | m.len(), 173 | Permissions::from(&m), 174 | m.created() 175 | .ok() 176 | .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) 177 | .map(|d| d.as_nanos()), 178 | m.modified() 179 | .ok() 180 | .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) 181 | .map(|d| d.as_nanos()), 182 | m.uid(), 183 | m.gid(), 184 | ) 185 | }) 186 | .unwrap_or_else(|_| { 187 | ( 188 | false, 189 | false, 190 | false, 191 | false, 192 | 0, 193 | Permissions::default(), 194 | None, 195 | None, 196 | 0, 197 | 0, 198 | ) 199 | }); 200 | 201 | let is_executable = permissions.user_execute 202 | || permissions.group_execute 203 | || permissions.other_execute; 204 | 205 | let mime_essence = mime_essence(&path, is_dir, &extension, is_executable); 206 | let human_size = to_human_size(size); 207 | 208 | Self { 209 | parent, 210 | relative_path, 211 | absolute_path, 212 | extension, 213 | is_dir, 214 | is_file, 215 | is_symlink, 216 | is_broken, 217 | is_readonly, 218 | mime_essence, 219 | size, 220 | human_size, 221 | permissions, 222 | created, 223 | last_modified, 224 | uid, 225 | gid, 226 | canonical: maybe_canonical_meta.clone(), 227 | symlink: if is_symlink { 228 | maybe_canonical_meta 229 | } else { 230 | None 231 | }, 232 | } 233 | } 234 | } 235 | 236 | impl Ord for Node { 237 | fn cmp(&self, other: &Self) -> Ordering { 238 | // Notice that the we flip the ordering on costs. 239 | // In case of a tie we compare positions - this step is necessary 240 | // to make implementations of `PartialEq` and `Ord` consistent. 241 | other.relative_path.cmp(&self.relative_path) 242 | } 243 | } 244 | 245 | impl PartialOrd for Node { 246 | fn partial_cmp(&self, other: &Self) -> Option { 247 | Some(self.cmp(other)) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/explorer.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{ 2 | DirectoryBuffer, ExplorerConfig, ExternalMsg, InternalMsg, MsgIn, Node, Task, 3 | }; 4 | use crate::path; 5 | use anyhow::{Error, Result}; 6 | use path_absolutize::Absolutize; 7 | use rayon::prelude::*; 8 | use std::fs; 9 | use std::path::PathBuf; 10 | use std::sync::mpsc::Sender; 11 | use std::thread; 12 | 13 | pub fn explore(parent: &PathBuf, config: &ExplorerConfig) -> Result> { 14 | let dirs = fs::read_dir(parent)?; 15 | let nodes = dirs 16 | .par_bridge() 17 | .filter_map(|d| { 18 | d.ok().and_then(|e| { 19 | e.path() 20 | .file_name() 21 | .map(|n| n.to_string_lossy().to_string()) 22 | }) 23 | }) 24 | .map(|name| Node::new(parent.to_string_lossy().to_string(), name)) 25 | .filter(|n| config.filter(n)); 26 | 27 | let mut nodes = if let Some(searcher) = config.searcher.as_ref() { 28 | searcher.search(nodes) 29 | } else { 30 | nodes.collect() 31 | }; 32 | 33 | let is_ordered_search = config 34 | .searcher 35 | .as_ref() 36 | .map(|s| !s.unordered) 37 | .unwrap_or(false); 38 | 39 | if !is_ordered_search { 40 | nodes.par_sort_unstable_by(|a, b| config.sort(a, b)); 41 | } 42 | 43 | Ok(nodes) 44 | } 45 | 46 | pub(crate) fn explore_sync( 47 | config: ExplorerConfig, 48 | parent: PathBuf, 49 | focused_path: Option, 50 | fallback_focus: usize, 51 | ) -> Result { 52 | let nodes = explore(&parent, &config)?; 53 | let focus_index = if config.searcher.is_some() { 54 | 0 55 | } else if let Some(focus) = focused_path { 56 | let focus_str = focus.to_string_lossy().to_string(); 57 | nodes 58 | .iter() 59 | .enumerate() 60 | .find(|(_, n)| n.relative_path == focus_str) 61 | .map(|(i, _)| i) 62 | .unwrap_or_else(|| fallback_focus.min(nodes.len().saturating_sub(1))) 63 | } else { 64 | 0 65 | }; 66 | 67 | Ok(DirectoryBuffer::new( 68 | parent.to_string_lossy().to_string(), 69 | nodes, 70 | focus_index, 71 | )) 72 | } 73 | 74 | pub(crate) fn explore_async( 75 | config: ExplorerConfig, 76 | parent: PathBuf, 77 | focused_path: Option, 78 | fallback_focus: usize, 79 | tx_msg_in: Sender, 80 | ) { 81 | thread::spawn(move || { 82 | explore_sync(config, parent.clone(), focused_path, fallback_focus) 83 | .and_then(|buf| { 84 | tx_msg_in 85 | .send(Task::new( 86 | MsgIn::Internal(InternalMsg::SetDirectory(buf)), 87 | None, 88 | )) 89 | .map_err(Error::new) 90 | }) 91 | .unwrap_or_else(|e| { 92 | tx_msg_in 93 | .send(Task::new( 94 | MsgIn::External(ExternalMsg::LogError(e.to_string())), 95 | None, 96 | )) 97 | .unwrap_or_default(); // Let's not panic if xplr closes. 98 | }) 99 | }); 100 | } 101 | 102 | pub(crate) fn explore_recursive_async( 103 | config: ExplorerConfig, 104 | parent: PathBuf, 105 | focused_path: Option, 106 | fallback_focus: usize, 107 | tx_msg_in: Sender, 108 | ) { 109 | explore_async( 110 | config.clone(), 111 | parent.clone(), 112 | focused_path, 113 | fallback_focus, 114 | tx_msg_in.clone(), 115 | ); 116 | if let Some(grand_parent) = parent.parent() { 117 | explore_recursive_async( 118 | config, 119 | grand_parent.into(), 120 | parent.file_name().map(|p| p.into()), 121 | 0, 122 | tx_msg_in, 123 | ); 124 | } 125 | } 126 | 127 | pub(crate) fn try_complete_path( 128 | pwd: &str, 129 | partial_path: &str, 130 | ) -> Result> { 131 | let Ok(path) = PathBuf::from(partial_path).absolutize().map(PathBuf::from) else { 132 | return Ok(None); 133 | }; 134 | 135 | let (parent, fname) = if partial_path.ends_with("/") { 136 | (path.clone(), "".into()) 137 | } else { 138 | ( 139 | path.parent() 140 | .map(PathBuf::from) 141 | .unwrap_or_else(|| PathBuf::from(pwd)), 142 | path.file_name() 143 | .map(|f| f.to_string_lossy().to_string()) 144 | .unwrap_or_default(), 145 | ) 146 | }; 147 | 148 | let direntries = fs::read_dir(&parent)? 149 | .filter_map(|d| d.ok()) 150 | .map(|e| e.file_name().to_string_lossy().to_string()); 151 | 152 | let mut matches = direntries 153 | .filter(|n| n.starts_with(&fname)) 154 | .collect::>(); 155 | 156 | let count = matches.len(); 157 | let maybe_found = if count <= 1 { 158 | matches.pop() 159 | } else { 160 | let mut common_prefix = fname.clone(); 161 | let match1_suffix = matches[0].chars().skip(fname.len()); 162 | for c in match1_suffix { 163 | let new_common_prefix = format!("{common_prefix}{c}"); 164 | if matches.iter().all(|m| m.starts_with(&new_common_prefix)) { 165 | common_prefix = new_common_prefix; 166 | } else { 167 | break; 168 | } 169 | } 170 | 171 | Some(common_prefix) 172 | }; 173 | 174 | if let Some(found) = maybe_found { 175 | let config = path::RelativityConfig::::default() 176 | .without_tilde() 177 | .with_prefix_dots() 178 | .without_suffix_dots(); 179 | 180 | let completion = parent.join(found); 181 | let short = path::shorten::<_, PathBuf>(&completion, Some(&config))?; 182 | 183 | if completion.is_dir() && !short.ends_with("/") { 184 | Ok(Some(format!("{short}/"))) 185 | } else { 186 | Ok(Some(short)) 187 | } 188 | } else { 189 | Ok(None) 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | 197 | #[test] 198 | fn test_explore_sync() { 199 | let config = ExplorerConfig::default(); 200 | let path = PathBuf::from("."); 201 | 202 | let r = explore_sync(config, path, None, 0); 203 | 204 | assert!(r.is_ok()); 205 | } 206 | 207 | #[test] 208 | fn test_failed_explore_sync() { 209 | let config = ExplorerConfig::default(); 210 | let path = PathBuf::from("/there/is/no/path"); 211 | 212 | let r = explore_sync(config, path, None, 0); 213 | 214 | assert!(r.is_err()); 215 | } 216 | 217 | fn extract_dirbuf_from_msg(msg: MsgIn) -> DirectoryBuffer { 218 | assert!(matches!(msg, MsgIn::Internal(_))); 219 | 220 | let msgin = match msg { 221 | MsgIn::Internal(m) => m, 222 | _ => panic!(), 223 | }; 224 | 225 | assert!(matches!(msgin, InternalMsg::SetDirectory(_))); 226 | 227 | match msgin { 228 | InternalMsg::SetDirectory(dbuf) => dbuf, 229 | _ => panic!(), 230 | } 231 | } 232 | 233 | use std::sync::mpsc; 234 | 235 | #[test] 236 | fn test_explore_async() { 237 | let config = ExplorerConfig::default(); 238 | let path = PathBuf::from("."); 239 | let (tx_msg_in, rx_msg_in) = mpsc::channel(); 240 | 241 | explore_async(config, path, None, 0, tx_msg_in.clone()); 242 | 243 | let task = rx_msg_in.recv().unwrap(); 244 | let dbuf = extract_dirbuf_from_msg(task.msg); 245 | 246 | assert_eq!(dbuf.parent, "."); 247 | 248 | drop(tx_msg_in); 249 | assert!(rx_msg_in.recv().is_err()); 250 | } 251 | 252 | //XXX: explore_recursive_async() generates messages with random order. 253 | // Discussing on GitHub (https://github.com/sayanarijit/xplr/issues/372) 254 | //#[test] 255 | //fn test_explore_recursive_async() { 256 | // let config = ExplorerConfig::default(); 257 | // let path = PathBuf::from("/usr/bin"); 258 | // let (tx_msg_in, rx_msg_in) = mpsc::channel(); 259 | 260 | // explore_recursive_async(config, path, None, 0, tx_msg_in.clone()); 261 | 262 | // let mut task = rx_msg_in.recv().unwrap(); 263 | // let mut dbuf = extract_dirbuf_from_msg(task.msg); 264 | 265 | // assert_eq!(dbuf.parent, "/"); 266 | 267 | // task = rx_msg_in.recv().unwrap(); 268 | // dbuf = extract_dirbuf_from_msg(task.msg); 269 | 270 | // assert_eq!(dbuf.parent, "/usr"); 271 | 272 | // task = rx_msg_in.recv().unwrap(); 273 | // dbuf = extract_dirbuf_from_msg(task.msg); 274 | 275 | // assert_eq!(dbuf.parent, "/usr/bin"); 276 | 277 | // drop(tx_msg_in); 278 | // assert!(rx_msg_in.recv().is_err()); 279 | //} 280 | } 281 | --------------------------------------------------------------------------------