├── .nvmrc ├── docs ├── public │ ├── .nojekyll │ ├── CNAME │ └── favicon.ico ├── src │ ├── env.d.ts │ ├── assets │ │ ├── fonts │ │ │ └── SymbolsNerdFont.ttf │ │ ├── logo_dark.svg │ │ └── logo_light.svg │ ├── components │ │ ├── Pls.astro │ │ ├── SocialIcons.astro │ │ ├── Version.astro │ │ ├── Footer.astro │ │ ├── DocBlock.astro │ │ └── Stars.astro │ ├── styles │ │ ├── layout.css │ │ ├── typography.css │ │ ├── color.css │ │ ├── brand.css │ │ ├── terminal.css │ │ └── font.css │ ├── content.config.ts │ └── content │ │ └── docs │ │ ├── index.mdx │ │ ├── features │ │ ├── alignment.mdx │ │ ├── colors.mdx │ │ ├── header.mdx │ │ ├── direction.mdx │ │ ├── upcoming.mdx │ │ ├── suffixes.mdx │ │ ├── name_filter.mdx │ │ ├── units.mdx │ │ ├── symlinks.mdx │ │ ├── icons.mdx │ │ ├── grid_view.mdx │ │ ├── collapse.mdx │ │ ├── type_filter.mdx │ │ ├── importance.mdx │ │ └── sorting.mdx │ │ ├── guides │ │ ├── contribute.mdx │ │ ├── paths.mdx │ │ ├── specs.mdx │ │ └── markup.mdx │ │ ├── about │ │ ├── intro.mdx │ │ ├── faq.mdx │ │ └── comparison.mdx │ │ └── cookbooks │ │ └── starters.mdx ├── .gitignore ├── tsconfig.json ├── justfile ├── package.json ├── .pls.yml └── astro.config.ts ├── examples ├── src │ └── examples │ │ ├── utils │ │ ├── __init__.py │ │ ├── io.py │ │ ├── pls.py │ │ ├── sub.py │ │ ├── main.py │ │ └── fs.py │ │ ├── __init__.py │ │ ├── confs │ │ ├── importance.yml │ │ ├── outer.yml │ │ ├── inner.yml │ │ ├── symlinks.yml │ │ ├── icons.yml │ │ ├── suffixes.yml │ │ ├── collapse.yml │ │ ├── specs.yml │ │ └── detail_view.yml │ │ ├── hero.py │ │ ├── colors.py │ │ ├── specs.py │ │ ├── grid_view.py │ │ ├── sorting.py │ │ ├── detail_view.py │ │ ├── paths.py │ │ ├── filtering.py │ │ ├── bench.py │ │ └── presentation.py ├── .env.template ├── .gitignore ├── .pls.yml ├── pyproject.toml └── justfile ├── .rustfmt.toml ├── pnpm-workspace.yaml ├── .github ├── FUNDING.yml ├── workflows │ ├── pack.yml │ └── ci.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── actions │ └── setup-env │ └── action.yml ├── .prettierignore ├── src ├── enums │ ├── entity.rs │ ├── collapse.rs │ ├── appearance.rs │ ├── sym.rs │ ├── perm.rs │ ├── unit_sys.rs │ └── icon.rs ├── output.rs ├── .pls.yml ├── traits.rs ├── models.rs ├── ext.rs ├── utils.rs ├── enums.rs ├── gfx.rs ├── utils │ ├── vectors.rs │ ├── urls.rs │ └── paths.rs ├── args.rs ├── fmt.rs ├── gfx │ ├── hash.rs │ └── svg.rs ├── main.rs ├── exc.rs ├── config.rs ├── models │ ├── window.rs │ ├── pls.rs │ └── spec.rs ├── ext │ ├── abs.rs │ └── ctime.rs ├── traits │ ├── sym.rs │ ├── name.rs │ └── imp.rs ├── args │ ├── input.rs │ ├── files_group.rs │ └── group.rs ├── output │ ├── table.rs │ ├── grid.rs │ └── cell.rs └── config │ ├── man.rs │ └── conf.rs ├── .editorconfig ├── .gitignore ├── prettier.config.mjs ├── .pls.yml ├── package.json ├── pkg └── brew │ ├── pls.template │ └── update_formula.sh ├── eslint.config.mjs ├── README.md ├── justfile ├── Cargo.toml └── .pre-commit-config.yaml /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /docs/public/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/CNAME: -------------------------------------------------------------------------------- 1 | pls.cli.rs 2 | -------------------------------------------------------------------------------- /examples/src/examples/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true # for a11y 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "docs" 3 | -------------------------------------------------------------------------------- /examples/.env.template: -------------------------------------------------------------------------------- 1 | PLS_BIN="./target/release/pls" 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - dhruvkb 3 | ko_fi: dhruvkb 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Autogenerated 2 | .astro 3 | 4 | # Lockfiles 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pls-rs/pls/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /src/enums/entity.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub enum Entity { 3 | User, 4 | Group, 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/src/assets/fonts/SymbolsNerdFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pls-rs/pls/HEAD/docs/src/assets/fonts/SymbolsNerdFont.ttf -------------------------------------------------------------------------------- /docs/src/components/Pls.astro: -------------------------------------------------------------------------------- 1 | pls 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | mod cell; 2 | mod grid; 3 | mod table; 4 | 5 | pub use cell::Cell; 6 | pub use grid::Grid; 7 | pub use table::Table; 8 | -------------------------------------------------------------------------------- /examples/src/examples/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | 4 | __pkg__ = __package__ 5 | __version__ = version(__pkg__) 6 | -------------------------------------------------------------------------------- /src/.pls.yml: -------------------------------------------------------------------------------- 1 | specs: 2 | - pattern: \.rs$ 3 | collapse: 4 | ext: "" # Collapse module index files under their corresponding directories. 5 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | mod detail; 2 | mod imp; 3 | mod name; 4 | mod sym; 5 | 6 | pub use detail::Detail; 7 | pub use imp::Imp; 8 | pub use name::Name; 9 | pub use sym::Sym; 10 | -------------------------------------------------------------------------------- /examples/src/examples/confs/importance.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | app_const: 3 | imp_styles: 4 | - [-2, "red"] 5 | - [-1, "yellow"] 6 | - [1, "green"] 7 | - [2, "bright_magenta"] 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | indent_size = 2 4 | 5 | [*.py] 6 | indent_size = 4 7 | 8 | [*.rs] 9 | indent_size = 4 10 | 11 | [justfile] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | dist/ 3 | 4 | # Astro generated types 5 | .astro/ 6 | 7 | # Dependencies 8 | node_modules/ 9 | 10 | # Generated examples 11 | src/examples/ 12 | -------------------------------------------------------------------------------- /docs/src/styles/layout.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | grid-template-columns: 1fr; 3 | } 4 | 5 | .hero .copy { 6 | align-items: center; 7 | } 8 | 9 | .hero .actions { 10 | justify-content: center; 11 | } 12 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Environment files 6 | .env 7 | 8 | # Virtual environment 9 | .venv/ 10 | 11 | # PDM 12 | .pdm-python 13 | *.egg-info/ 14 | -------------------------------------------------------------------------------- /docs/src/styles/typography.css: -------------------------------------------------------------------------------- 1 | span.math { 2 | font-family: serif; 3 | font-style: italic; 4 | } 5 | 6 | pre[data-language="bash"] .ec-line .code::before { 7 | content: "$ "; 8 | opacity: 0.5; 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/styles/color.css: -------------------------------------------------------------------------------- 1 | /* Additional colors */ 2 | :root { 3 | --color-background-tint: rgba(255, 255, 255, 0.03); 4 | } 5 | 6 | :root[data-theme="light"] { 7 | --color-background-tint: rgba(0, 0, 0, 0.03); 8 | } 9 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | mod node; 2 | mod owner; 3 | mod perm; 4 | mod pls; 5 | mod spec; 6 | mod window; 7 | 8 | pub use node::Node; 9 | pub use owner::OwnerMan; 10 | pub use perm::Perm; 11 | pub use pls::Pls; 12 | pub use spec::Spec; 13 | pub use window::Window; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE junk 2 | .vscode 3 | .idea 4 | 5 | # Node.js modules 6 | node_modules 7 | 8 | # Build artifacts 9 | target 10 | 11 | # Profiling artifacts 12 | flamegraph.svg 13 | report.json 14 | report.html 15 | 16 | # pre-commit executable 17 | pre-commit.pyz 18 | -------------------------------------------------------------------------------- /examples/src/examples/confs/outer.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | app_const: 3 | table: 4 | header_style: red bold underline 5 | entry_const: 6 | user_styles: 7 | curr: green bold 8 | other: green bold 9 | specs: 10 | - pattern: ^a$ 11 | style: red underline 12 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/components/*": ["src/components/*"], 8 | "@/examples/*": ["src/examples/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ext.rs: -------------------------------------------------------------------------------- 1 | //! This module contains extensions to the standard library. 2 | //! 3 | //! The public interface of this module consists of the following traits: 4 | //! 5 | //! * [`Abs`] 6 | //! * [`Ctime`] 7 | 8 | mod abs; 9 | mod ctime; 10 | 11 | pub use abs::Abs; 12 | pub use ctime::Ctime; 13 | -------------------------------------------------------------------------------- /examples/src/examples/hero.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from examples.utils.main import write_out 4 | 5 | 6 | PROJECT_ROOT = Path(__file__).parents[3] 7 | 8 | 9 | def hero(): 10 | write_out("--det=all", bench=PROJECT_ROOT, dest_name="hero") 11 | 12 | 13 | if __name__ == "__main__": 14 | hero() 15 | -------------------------------------------------------------------------------- /examples/src/examples/confs/inner.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | app_const: 3 | table: 4 | header_style: blue 5 | entry_const: 6 | nlink_styles: 7 | file_sing: blue 8 | timestamp_formats: 9 | mtime: "[year]-[month]-[day]" 10 | specs: 11 | - pattern: ^a$ 12 | style: blue 13 | icons: 14 | - file 15 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! This module contains an assortment of small utilities. 2 | //! 3 | //! The public interface of the module consists of sub-modules, each of which 4 | //! can contain any number of utility functions. 5 | //! 6 | //! * [`paths`] 7 | //! * [`urls`] 8 | //! * [`vectors`] 9 | 10 | pub mod paths; 11 | pub mod urls; 12 | pub mod vectors; 13 | -------------------------------------------------------------------------------- /docs/src/components/SocialIcons.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Props } from "@astrojs/starlight/props"; 3 | 4 | import Default from "@astrojs/starlight/components/SocialIcons.astro"; 5 | 6 | import Stars from "@/components/Stars.astro"; 7 | --- 8 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /examples/src/examples/confs/symlinks.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | entry_const: 3 | symlink: 4 | ok: 5 | sep: --> 6 | style: green 7 | broken: 8 | sep: ~~> 9 | style: red bold 10 | cyclic: 11 | sep: \<-> # '<' must be escaped 12 | style: yellow italic 13 | error: 14 | sep: -x- 15 | style: red italic 16 | -------------------------------------------------------------------------------- /examples/.pls.yml: -------------------------------------------------------------------------------- 1 | icons: 2 | python: "" # nf-seti-python 3 | specs: 4 | - pattern: \.py$ 5 | icons: 6 | - python 7 | style: rgb(255,212,59) 8 | - pattern: ^pyproject\.toml$ 9 | icons: 10 | - package 11 | - pattern: ^pdm\.lock$ 12 | icons: 13 | - lock 14 | importance: -1 15 | collapse: 16 | name: pyproject.toml 17 | -------------------------------------------------------------------------------- /examples/src/examples/colors.py: -------------------------------------------------------------------------------- 1 | from examples.bench import typ_bench 2 | from examples.utils.main import write_out 3 | 4 | 5 | def colors(): 6 | with typ_bench() as bench: 7 | write_out("--det=std", bench=bench, dest_name="on") 8 | write_out("--det=std", bench=bench, dest_name="off", env={"NO_COLOR": "true"}) 9 | 10 | 11 | if __name__ == "__main__": 12 | colors() 13 | -------------------------------------------------------------------------------- /examples/src/examples/confs/icons.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | icons: 3 | pls: "󰱫" # Override the built-in glyph for 'pls'. 4 | sock: "󰓚" # Define new icon-glyph mapping. 5 | entry_const: 6 | typ: 7 | fifo: 8 | icons: 9 | - fifo # Enable and use the built-in glyph for FIFO pipes. 10 | socket: 11 | icons: 12 | - sock # Enable and use a custom icon for sockets. 13 | -------------------------------------------------------------------------------- /docs/src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { docsLoader, i18nLoader } from "@astrojs/starlight/loaders"; 3 | import { docsSchema, i18nSchema } from "@astrojs/starlight/schema"; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }), 8 | }; 9 | -------------------------------------------------------------------------------- /examples/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "examples" 3 | version = "0.0.0" 4 | description = "Generate examples for `pls` documentation" 5 | authors = [ 6 | {name = "Dhruv Bhanushali", email = "dhruv_b@live.com"}, 7 | ] 8 | 9 | requires-python = "==3.12.*" 10 | dependencies = [ 11 | "python-decouple >=3.8, <4", 12 | ] 13 | 14 | [tool.pdm.dev-dependencies] 15 | dev = [ 16 | "ipython >=8.22.1, <9", 17 | ] 18 | -------------------------------------------------------------------------------- /examples/src/examples/confs/suffixes.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | entry_const: 3 | typ: 4 | file: 5 | suffix: "!" 6 | dir: 7 | suffix: ">" 8 | symlink: 9 | suffix: "↗" 10 | fifo: 11 | suffix: "-" 12 | socket: 13 | suffix: "↔" 14 | char_device: 15 | suffix: "󰗧" 16 | block_device: 17 | suffix: "" 18 | -------------------------------------------------------------------------------- /src/enums/collapse.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | #[serde(rename_all = "snake_case")] 5 | pub enum Collapse { 6 | /// Name-based collapsing matches this node with another having the exact 7 | /// given name. 8 | Name(String), 9 | /// Extension-based collapsing matches this node with another having the 10 | /// same base name and the given extension. 11 | Ext(String), 12 | } 13 | -------------------------------------------------------------------------------- /docs/justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := false 2 | set fallback 3 | 4 | # Show all available recipes. 5 | @_default: 6 | just --list --unsorted 7 | 8 | ######### 9 | # Setup # 10 | ######### 11 | 12 | # Install Node.js dependencies. 13 | install: 14 | pnpm install 15 | 16 | ########### 17 | # Recipes # 18 | ########### 19 | 20 | # Delete the built assets. 21 | clean: 22 | rm -rf dist/ 23 | 24 | # Remove all examples from the codebase. 25 | clean-eg: 26 | rm -rf src/examples/ 27 | -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | mod appearance; 2 | mod collapse; 3 | mod detail_field; 4 | mod entity; 5 | mod icon; 6 | mod perm; 7 | mod sort_field; 8 | mod sym; 9 | mod typ; 10 | mod unit_sys; 11 | 12 | pub use appearance::Appearance; 13 | pub use collapse::Collapse; 14 | pub use detail_field::DetailField; 15 | pub use entity::Entity; 16 | pub use icon::Icon; 17 | pub use perm::{Oct, Sym}; 18 | pub use sort_field::SortField; 19 | pub use sym::{SymState, SymTarget}; 20 | pub use typ::Typ; 21 | pub use unit_sys::UnitSys; 22 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | trailingComma: "es5", 4 | astroAllowShorthand: true, 5 | bracketSameLine: true, 6 | singleAttributePerLine: true, 7 | overrides: [ 8 | { 9 | files: ["*.astro"], 10 | options: { 11 | parser: "astro", 12 | }, 13 | }, 14 | { 15 | files: ["*.svg"], 16 | options: { 17 | parser: "html", 18 | }, 19 | }, 20 | ], 21 | proseWrap: "always", 22 | plugins: ["prettier-plugin-astro"], 23 | }; 24 | -------------------------------------------------------------------------------- /examples/src/examples/specs.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from examples.bench import ts_bench 4 | from examples.utils.main import write_out, copy_write_conf 5 | 6 | 7 | def specs(): 8 | with ts_bench() as bench: 9 | subprocess.run(["git", "init", str(bench.absolute())], check=True) 10 | write_out(bench=bench, dest_name="def") 11 | copy_write_conf(bench) 12 | write_out(bench=bench, dest_name="confd") 13 | write_out(bench=bench / "src", dest_name="confd_src") 14 | 15 | 16 | if __name__ == "__main__": 17 | specs() 18 | -------------------------------------------------------------------------------- /examples/src/examples/confs/collapse.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | app_const: 3 | tree: 4 | pipe_space: "| " 5 | space_space: " " # should be as wide as the other entries for alignment 6 | tee_dash: "+-- " 7 | bend_dash: "+-- " 8 | specs: 9 | - pattern: ^\.?\w{1}$ 10 | icons: 11 | - file 12 | - pattern: b 13 | collapse: 14 | name: a 15 | - pattern: c 16 | collapse: 17 | name: b 18 | - pattern: \.d 19 | collapse: 20 | name: b 21 | - pattern: e 22 | collapse: 23 | name: a 24 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "astro dev --host", 7 | "check": "astro check", 8 | "build": "astro build", 9 | "preview": "astro preview --host --port 4322", 10 | "astro": "astro", 11 | "fmt": "prettier --ignore-path .gitignore --ignore-path .prettierignore --write ." 12 | }, 13 | "dependencies": { 14 | "@astrojs/starlight": "^0.33.2", 15 | "astro": "^5.6.2", 16 | "astro-auto-import": "^0.4.4" 17 | }, 18 | "packageManager": "pnpm@9.15.5" 19 | } 20 | -------------------------------------------------------------------------------- /docs/.pls.yml: -------------------------------------------------------------------------------- 1 | icons: 2 | javascript: "󰌞" # nf-md-language_javascript 3 | typescript: "󰛦" # nf-md-language_typescript 4 | specs: 5 | - pattern: \.ts$ 6 | icons: 7 | - typescript 8 | style: rgb(49,120,198) 9 | - pattern: \.m?js$ 10 | icons: 11 | - javascript 12 | style: rgb(247,223,30) 13 | - pattern: \.astro$ 14 | icons: 15 | - rocket 16 | style: rgb(255,93,1) 17 | - pattern: ^package-lock\.json$ 18 | icons: 19 | - package 20 | - pattern: ^pnpm-lock.yaml$ 21 | icons: 22 | - lock 23 | importance: -1 24 | collapse: 25 | name: package.json 26 | -------------------------------------------------------------------------------- /docs/src/styles/brand.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --cp-red: #f38ba8; 3 | 4 | --color-brand: var(--cp-red); 5 | 6 | --sl-color-accent-high: var(--sl-color-red-high); 7 | --sl-color-accent: var(--sl-color-red); 8 | --sl-color-accent-low: var(--sl-color-red-low); 9 | } 10 | 11 | :root[data-theme="light"] { 12 | --cp-red: #d20f39; 13 | 14 | --sl-color-accent-high: var(--sl-color-red-high); 15 | --sl-color-accent: var(--sl-color-red); 16 | --sl-color-accent-low: var(--sl-color-red-low); 17 | } 18 | 19 | h1[data-page-title], 20 | a.site-title, 21 | code.pls { 22 | font-family: var(--__sl-font-mono); 23 | font-weight: bold; 24 | color: var(--color-brand); 25 | } 26 | -------------------------------------------------------------------------------- /src/gfx.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for working with graphics. 2 | //! 3 | //! Kitty terminal graphics protocol provides ways to render images in 4 | //! the terminal. We use this protocol to show icons beyond the standard 5 | //! collection present in Nerd Fonts. 6 | //! 7 | //! The public interface of the module consists of five functions: 8 | //! 9 | //! * [`compute_hash`] 10 | //! * [`is_supported`] 11 | //! * [`render_image`] 12 | //! * [`send_image`] 13 | //! * [`strip_image`] 14 | //! * [`get_rgba`] 15 | 16 | mod hash; 17 | mod kitty; 18 | mod svg; 19 | 20 | pub use hash::compute_hash; 21 | pub use kitty::{is_supported, render_image, send_image, strip_image}; 22 | pub use svg::get_rgba; 23 | -------------------------------------------------------------------------------- /src/utils/vectors.rs: -------------------------------------------------------------------------------- 1 | //! This module contains some helper functions for vector collections. 2 | //! 3 | //! The public interface of the module consists of one function: 4 | //! 5 | //! * [`dedup`] 6 | 7 | use std::collections::{HashSet, VecDeque}; 8 | 9 | /// Deduplicate a vector, by preserving the last appearance of a value. 10 | /// 11 | /// # Arguments 12 | /// 13 | /// * `vec` - the vector to deduplicate 14 | pub fn dedup(vec: Vec) -> Vec { 15 | let mut dedup = VecDeque::new(); 16 | 17 | let mut set = HashSet::new(); 18 | for item in vec.into_iter().rev() { 19 | if set.insert(item.clone()) { 20 | dedup.push_front(item); 21 | } 22 | } 23 | 24 | dedup.into() 25 | } 26 | -------------------------------------------------------------------------------- /.pls.yml: -------------------------------------------------------------------------------- 1 | icons: 2 | editorconfig: "" # nf-seti-editorconfig 3 | egg: "󰪯" # nf-md-egg 4 | rocket: "" # nf-oct-rocket 5 | specs: 6 | - pattern: ^docs$ 7 | icon: book 8 | - pattern: ^examples$ 9 | icon: egg 10 | - pattern: ^.idea$ 11 | importance: -2 12 | - pattern: ^src$ 13 | style: bold 14 | importance: 1 15 | - pattern: ^target$ 16 | icon: rocket 17 | - pattern: ^(justfile|README.md)$ 18 | style: green bold 19 | importance: 2 20 | - pattern: ^\.editorconfig$ 21 | icon: editorconfig 22 | - pattern: ^(pre-commit\.pyz|\.pre-commit-config\.yaml)$ 23 | icon: broom 24 | - pattern: ^(.prettierignore|prettier.config.mjs)$ 25 | icon: broom 26 | entry_const: 27 | typ: 28 | dir: 29 | style: cyan 30 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for working with paths entered as arguments to the 2 | //! application. 3 | //! 4 | //! Since `pls` can accept multiple paths as positional arguments, the module 5 | //! expresses them in terms of [`inputs`](Input) and [`groups`](Group). 6 | //! 7 | //! Each individual path is treated as one input. All directories given as 8 | //! inputs are mapped to [`one group each`](Group::Dir). All files given as 9 | //! input are collected into a [`single group`](Group::Files). 10 | //! 11 | //! The public interface of the module consists of two structs: 12 | //! 13 | //! * [`Group`] 14 | //! * [`Input`] 15 | 16 | mod dir_group; 17 | mod files_group; 18 | mod group; 19 | mod input; 20 | 21 | pub use group::Group; 22 | pub use input::Input; 23 | -------------------------------------------------------------------------------- /examples/src/examples/confs/specs.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | icons: 3 | javascript: "󰌞" # nf-md-language_javascript 4 | typescript: "󰛦" # nf-md-language_typescript 5 | specs: 6 | - pattern: \.ts$ 7 | icons: 8 | - typescript 9 | style: rgb(49,120,198) 10 | - pattern: \.js$ 11 | icons: 12 | - javascript 13 | style: rgb(247,223,30) 14 | importance: -1 15 | collapse: 16 | ext: ts 17 | - pattern: prettier 18 | icons: 19 | - broom 20 | - pattern: ^package\.json$ 21 | icons: 22 | - package 23 | - pattern: ^pnpm-lock\.yaml$ 24 | icons: 25 | - lock 26 | importance: -1 27 | collapse: 28 | name: package.json 29 | - pattern: ^(justfile|README.md)$ 30 | style: green bold 31 | importance: 2 32 | -------------------------------------------------------------------------------- /src/fmt.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for working with markup strings. 2 | //! 3 | //! Markup strings are a more convenient way to represent ANSI-formatted text. 4 | //! They use an HTML-like syntax instead of arcane escape sequences. 5 | //! 6 | //! For example, to render the string "Hello, World!" in bold, you would write 7 | //! `Hello, World!`. 8 | //! 9 | //! The tag consists of space separated directives. See [`fmt`](format::fmt) for 10 | //! a list of supported directives. Tags can be nested, with inner tags capable 11 | //! of overwriting directives from outer tags. 12 | //! 13 | //! The public interface of the module consists of two functions: 14 | //! 15 | //! * [`len`] 16 | //! * [`render`] 17 | 18 | mod format; 19 | mod markup; 20 | 21 | pub use markup::{len, render}; 22 | -------------------------------------------------------------------------------- /examples/src/examples/grid_view.py: -------------------------------------------------------------------------------- 1 | from examples.bench import grid_bench 2 | from examples.utils.main import write_out 3 | 4 | 5 | def grid_view(): 6 | with grid_bench() as bench: 7 | write_out("--grid=true", bench=bench, dest_name="on", env={"PLS_COLUMNS": "28"}) 8 | write_out(bench=bench, dest_name="off", env={"PLS_COLUMNS": "28"}) 9 | 10 | 11 | def direction(): 12 | with grid_bench() as bench: 13 | write_out( 14 | "--grid=true", 15 | "--down=true", 16 | bench=bench, 17 | dest_name="on", 18 | env={"PLS_COLUMNS": "28"}, 19 | ) 20 | write_out( 21 | "--grid=true", bench=bench, dest_name="off", env={"PLS_COLUMNS": "28"} 22 | ) 23 | 24 | 25 | if __name__ == "__main__": 26 | grid_view() 27 | direction() 28 | -------------------------------------------------------------------------------- /examples/src/examples/utils/io.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | DOCS_EXAMPLES = Path(__file__).parents[4] / "docs" / "src" / "examples" 4 | 5 | 6 | def write_content(dest_path: str, content: str): 7 | """ 8 | Write the given content to a file with the given path. 9 | 10 | The path is joined with the docs' ``examples`` directory. If the path 11 | contains a directory name which does not exist, it will be created. 12 | 13 | :param dest_path: the additional path of the output file 14 | :param content: the content to write inside the file 15 | """ 16 | 17 | dest_path = DOCS_EXAMPLES / dest_path 18 | if not dest_path.parent.exists(): 19 | dest_path.parent.mkdir(mode=0o755, parents=True) 20 | 21 | print(f"MDX file written to '{dest_path}'.") 22 | dest_path.write_text(content, encoding="utf-8") 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pls", 3 | "description": "pls is a prettier and powerful ls(1) for the pros.", 4 | "private": true, 5 | "version": "0.0.0", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "lint:fix": "pnpm lint --fix", 9 | "format": "prettier --write .", 10 | "format:check": "prettier --check .", 11 | "checks": "pnpm lint && pnpm format:check" 12 | }, 13 | "packageManager": "pnpm@9.15.3", 14 | "devDependencies": { 15 | "@eslint/compat": "^1.2.8", 16 | "@eslint/js": "^9.24.0", 17 | "@types/eslint__js": "^8.42.3", 18 | "eslint": "^9.24.0", 19 | "eslint-plugin-astro": "^1.3.1", 20 | "globals": "^15.15.0", 21 | "lint-staged": "^15.5.1", 22 | "prettier": "^3.5.3", 23 | "prettier-plugin-astro": "^0.14.1", 24 | "typescript": "^5.8.3", 25 | "typescript-eslint": "^8.29.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/gfx/hash.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | use std::path::Path; 3 | 4 | /// Compute the hash for a given path and size pair. This hash acts as the key 5 | /// for the icon cache. 6 | pub fn compute_hash(path: &Path, size: u8) -> u32 { 7 | let mut hasher = NumericHasher::default(); 8 | path.hash(&mut hasher); 9 | size.hash(&mut hasher); 10 | // Perform a lossy conversion to u32, throwing away the upper bits. 11 | hasher.finish() as u32 12 | } 13 | 14 | #[derive(Default)] 15 | struct NumericHasher { 16 | state: u32, 17 | } 18 | 19 | impl Hasher for NumericHasher { 20 | fn finish(&self) -> u64 { 21 | (self.state as u64) + 1 22 | } 23 | 24 | fn write(&mut self, bytes: &[u8]) { 25 | for &byte in bytes { 26 | // Example hash function: FNV-1a variant 27 | self.state = self.state.wrapping_mul(16777619) ^ (byte as u32); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := false 2 | set fallback 3 | 4 | # Show all available recipes. 5 | @_default: 6 | just --list --unsorted 7 | 8 | ######### 9 | # Setup # 10 | ######### 11 | 12 | # Install Python dependencies. 13 | install: 14 | pdm install 15 | 16 | ########### 17 | # Recipes # 18 | ########### 19 | 20 | # Create the `.env` file from the template file 21 | env: 22 | cp .env.template .env 23 | 24 | # Generate the specified examples file. 25 | gen file: 26 | pdm run src/examples/{{ file }}.py 27 | 28 | # Regenerate all examples. 29 | all: 30 | just gen hero 31 | just gen specs 32 | just gen detail_view 33 | just gen grid_view 34 | just gen presentation 35 | just gen filtering 36 | just gen sorting 37 | just gen colors 38 | just gen paths 39 | 40 | # Create a Python shell. 41 | shell: 42 | pdm run ipython 43 | -------------------------------------------------------------------------------- /examples/src/examples/utils/pls.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | 3 | from examples.utils.sub import run_cmd 4 | 5 | 6 | def run_pls(args: list[str], **kwargs) -> str: 7 | """ 8 | Run a ``pls`` command and return the output with ANSI codes. 9 | 10 | It assumes the project root to be the working directory and that a release 11 | build of ``pls`` is present on the ``$PATH``. 12 | 13 | All keyword arguments are forwarded as-is to ``run_cmd``. 14 | 15 | :return: the output of the ``pls`` command 16 | """ 17 | 18 | pls_bin = config("PLS_BIN", default="pls") 19 | cmd = [pls_bin, *args] 20 | print(f"Running command {cmd}") 21 | 22 | env = kwargs.pop("env", {}) 23 | if "NO_COLOR" not in env: 24 | env["CLICOLOR_FORCE"] = "true" 25 | proc = run_cmd(cmd, env=env, **kwargs) 26 | return f'```ansi frame="none"\n{proc.stdout}```\n' 27 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod config; 3 | mod enums; 4 | mod exc; 5 | mod ext; 6 | mod fmt; 7 | mod gfx; 8 | mod models; 9 | mod output; 10 | mod traits; 11 | mod utils; 12 | 13 | use crate::gfx::is_supported; 14 | use crate::models::Pls; 15 | use crate::models::Window; 16 | 17 | use log::debug; 18 | use std::sync::LazyLock; 19 | 20 | static PLS: LazyLock = LazyLock::new(|| { 21 | let window = Window::try_new(); 22 | let supports_gfx = match &window { 23 | Some(win) if win.ws_xpixel > 0 && win.ws_ypixel > 0 => is_supported(), 24 | _ => false, 25 | }; 26 | 27 | Pls { 28 | supports_gfx, 29 | window, 30 | ..Pls::default() 31 | } 32 | }); 33 | 34 | /// Create a `Pls` instance and immediately delegate to it. 35 | /// 36 | /// This is the entry point of the application. 37 | fn main() { 38 | env_logger::init(); 39 | debug!("Hello!"); 40 | 41 | PLS.cmd(); 42 | 43 | debug!("Bye!"); 44 | } 45 | -------------------------------------------------------------------------------- /src/exc.rs: -------------------------------------------------------------------------------- 1 | use crate::fmt::render; 2 | use std::fmt::{Display, Formatter, Result as FmtResult}; 3 | 4 | #[derive(Debug)] 5 | pub enum Exc { 6 | /// wraps all occurrences of errors in I/O operations 7 | Io(std::io::Error), 8 | /// wraps all occurrences of errors in SVG operations 9 | Svg(resvg::usvg::Error), 10 | Conf(figment::Error), 11 | /// wraps exceptions from the `xterm-query` crate 12 | Xterm(xterm_query::XQError), 13 | /// wraps all other errors 14 | Other(String), 15 | } 16 | 17 | impl Display for Exc { 18 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 19 | let attn = "error:"; 20 | let err = match self { 21 | Exc::Io(err) => err.to_string(), 22 | Exc::Conf(err) => err.to_string(), 23 | Exc::Svg(err) => err.to_string(), 24 | Exc::Other(text) => text.to_string(), 25 | Exc::Xterm(err) => err.to_string(), 26 | }; 27 | let msg = format!("{attn} {err}"); 28 | write!(f, "{}", render(msg)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/components/Version.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { owner = "pls-rs", repo = "pls" } = Astro.props; 3 | --- 4 | 5 | x.y.z 33 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: pls 3 | head: 4 | - tag: title 5 | content: pls - a prettier and powerful ls(1) for the pros 6 | description: 7 | pls is a prettier and powerful ls(1) for the pros. With clean, readable 8 | output, deep customisation and useful features, it is a pro's best friend. 9 | template: splash 10 | hero: 11 | tagline: 12 | pls is a prettier and powerful ls(1) 13 | for the pros. 14 | actions: 15 | - text: Get started 16 | link: /guides/get_started/ 17 | icon: right-arrow 18 | variant: primary 19 | - text: Learn more 20 | link: /about/intro/ 21 | icon: open-book 22 | - text: Get source 23 | link: https://github.com/pls-rs/pls/ 24 | icon: github 25 | --- 26 | 27 | import { Content as Hero } from "@/examples/hero/hero.mdx"; 28 | 29 | ```bash 30 | pls --det all 31 | ``` 32 | 33 | 34 | 35 | is a [**@dhruvkb**](https://dhruvkb.dev) project. 36 | -------------------------------------------------------------------------------- /examples/src/examples/sorting.py: -------------------------------------------------------------------------------- 1 | from examples.utils.main import write_out 2 | from examples.utils.fs import mkbigfile, fs 3 | 4 | 5 | def sorting(): 6 | with fs( 7 | ( 8 | "sorting", 9 | [ 10 | ("dir_a", []), 11 | ("file_b.txt", lambda p: mkbigfile(p, size=1024**1)), 12 | ("dir_c", []), 13 | ("file_d.txt", lambda p: mkbigfile(p, size=1024**2)), 14 | ("dir_e", []), 15 | ("file_f.txt", lambda p: mkbigfile(p, size=1024**0)), 16 | ], 17 | ) 18 | ) as bench: 19 | write_out(bench=bench, dest_name="def") 20 | write_out( 21 | "--det=ino", 22 | "--det=typ", 23 | "--det=size", 24 | "--sort=cat_", 25 | "--sort=size_", 26 | "--sort=ino", 27 | bench=bench, 28 | dest_name="cust", 29 | ) 30 | 31 | 32 | if __name__ == "__main__": 33 | sorting() 34 | -------------------------------------------------------------------------------- /pkg/brew/pls.template: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | # Source: https://github.com/pls-rs/pls/blob/main/pkg/brew/pls.template 3 | class Pls < Formula 4 | desc "Prettier and powerful ls for the pros" 5 | homepage "https://pls.cli.rs/" 6 | version "{{ VERSION }}" 7 | license "GPL-3.0-or-later" 8 | 9 | if OS.mac? 10 | url "{{ MAC_URL }}" 11 | sha256 "{{ MAC_SHA }}" 12 | elsif OS.linux? 13 | url "{{ LINUX_URL }}" 14 | sha256 "{{ LINUX_SHA }}" 15 | end 16 | 17 | depends_on "libgit2" 18 | 19 | def install 20 | bin.install "pls" 21 | end 22 | 23 | test do 24 | linkage_with_libgit2 = (bin/"pls").dynamically_linked_libraries.any? do |dll| 25 | next false unless dll.start_with?(HOMEBREW_PREFIX.to_s) 26 | 27 | File.realpath(dll) == (Formula["libgit2"].opt_lib/shared_library("libgit2")).realpath.to_s 28 | end 29 | 30 | assert linkage_with_libgit2, "No linkage with libgit2! Cargo is likely using a vendored version." 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for working with configuration and CLI arguments. 2 | //! 3 | //! `pls` supports customisation in two ways, through CLI arguments that change 4 | //! the output per session and through `.pls.yml` YAML files that can go deeper 5 | //! to tweak each individual string, change icons and add new node specs. 6 | //! Together they make `pls` the most customisable file lister. 7 | //! 8 | //! For example, the CLI arg `--det` controls what metadata columns must be 9 | //! shown in a given run, whereas the `.pls.yml` file can be used to change the 10 | //! individual name for these columns. 11 | //! 12 | //! The public interface of the module consists of five structs: 13 | //! 14 | //! * [`AppConst`] 15 | //! * [`Args`] 16 | //! * [`Conf`] 17 | //! * [`EntryConst`] 18 | //! * [`ConfMan`] 19 | 20 | mod app_const; 21 | mod args; 22 | mod conf; 23 | mod entry_const; 24 | mod man; 25 | 26 | pub use app_const::AppConst; 27 | pub use args::Args; 28 | pub use conf::Conf; 29 | pub use entry_const::EntryConst; 30 | pub use man::ConfMan; 31 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/alignment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alignment 3 | description: 4 | pls uses variable alignment when listing nodes to account for leading dots and 5 | to ensure that the first characters in node names line up. 6 | --- 7 | 8 | import { Content as AlignmentOn } from "@/examples/alignment/on.mdx"; 9 | import { Content as AlignmentOff } from "@/examples/alignment/off.mdx"; 10 | 11 | performs slight tweaks to alignment to account for leading dots. When 12 | alignment is enabled, the leading dots are moved one position to the left, so 13 | that the actual file names are aligned. Also the leading dots are slightly 14 | dimmed to reduce their visual prominence. 15 | 16 | ## Arguments 17 | 18 | `--align`/`-a` can be used to turn alignment on or off. File names are aligned 19 | by default because usually it's the name that's important. 20 | 21 | ### Examples 22 | 23 | ```bash 24 | pls # or --align=true or -a=true 25 | ``` 26 | 27 | 28 | 29 | ```bash 30 | pls --align=false # or -a=false 31 | ``` 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/pack.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | 9 | jobs: 10 | brew: 11 | name: Update tap 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | with: 17 | path: code 18 | 19 | - name: Checkout tap 20 | uses: actions/checkout@v4 21 | with: 22 | repository: pls-rs/homebrew-pls 23 | path: tap 24 | token: ${{ secrets.ACCESS_TOKEN }} 25 | 26 | - name: Update tap 27 | working-directory: code 28 | run: | 29 | ./pkg/brew/update_formula.sh "pkg/brew/pls.template" "$GITHUB_WORKSPACE/tap/Formula/pls.rb" 30 | 31 | - name: Commit and push changes 32 | working-directory: tap 33 | run: | 34 | if ! git diff-index --quiet HEAD; then 35 | git config user.name "Dhruv Bhanushali" 36 | git config user.email "hi@dhruvkb.dev" 37 | git commit --all --message "Update formula" 38 | git push origin main 39 | fi 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a feature that would make pls more useful. 4 | labels: "goal:feat" 5 | title: "" 6 | --- 7 | 8 | ## Problem 9 | 10 | 14 | 15 | ## Description 16 | 17 | 21 | 22 | ## Alternatives 23 | 24 | 29 | 30 | ## Additional information 31 | 32 | 36 | 37 | 41 | -------------------------------------------------------------------------------- /src/enums/appearance.rs: -------------------------------------------------------------------------------- 1 | /// This enum contains all the different ways a node can appear. 2 | /// 3 | /// A node can be a combination of appearances as well. For example, a node may be a tree parent as 4 | /// well as a tree child. When a node has no special appearances, it is a normal listing. 5 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 6 | pub enum Appearance { 7 | /// The node appears as the target of a symlink. 8 | /// 9 | /// The display text of the node is set to the symlink destination. It is 10 | /// not based on the name of the node. 11 | Symlink, 12 | /// The node appears as the child of another. 13 | /// 14 | /// The tree-drawing shapes are shown before the name of the node, which is 15 | /// the same as [`Appearance::Normal`]. 16 | TreeChild, 17 | /// The node appears as the parent of another. 18 | /// 19 | /// This provides the ability to use an alternative "open-folder" icon for 20 | /// directories. 21 | TreeParent, 22 | /// The node appears as an individual file being listed. 23 | /// 24 | /// The name of the node is shown exactly as it was passed to the CLI. It 25 | /// could be the name, or a relative/absolute path. 26 | SoloFile, 27 | } 28 | -------------------------------------------------------------------------------- /src/models/window.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_ushort, ioctl, STDOUT_FILENO, TIOCGWINSZ}; 2 | use log::warn; 3 | 4 | /// See http://www.delorie.com/djgpp/doc/libc/libc_495.html 5 | #[repr(C)] 6 | #[derive(Default)] 7 | pub struct Window { 8 | pub ws_row: c_ushort, /* rows, in characters */ 9 | pub ws_col: c_ushort, /* columns, in characters */ 10 | pub ws_xpixel: c_ushort, /* horizontal size, pixels */ 11 | pub ws_ypixel: c_ushort, /* vertical size, pixels */ 12 | } 13 | 14 | impl Window { 15 | /// Get a new `Window` instance with the terminal measurements. 16 | /// 17 | /// This function returns `None` if the ioctl call fails. 18 | pub fn try_new() -> Option { 19 | let mut win = Self::default(); 20 | #[allow(clippy::useless_conversion)] 21 | let r = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut win) }; 22 | if r == 0 && win.ws_row > 0 && win.ws_col > 0 { 23 | return Some(win); 24 | } 25 | warn!("Could not determine cell dimensions."); 26 | None 27 | } 28 | 29 | pub fn cell_width(&self) -> u8 { 30 | (self.ws_xpixel / self.ws_col) as u8 31 | } 32 | 33 | pub fn cell_height(&self) -> u8 { 34 | (self.ws_ypixel / self.ws_row) as u8 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/brew/update_formula.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REPO="pls-rs/pls" 4 | RELEASE=$(curl -s "https://api.github.com/repos/$REPO/releases" | jq -r '.[0]') 5 | 6 | VERSION=$(echo "$RELEASE" | jq -r '.name' | cut -c 2-) 7 | echo "Latest release is $VERSION." 8 | 9 | MAC_URL=$(echo "$RELEASE" | jq -r '.assets[] | select(.name | contains("apple-darwin")).browser_download_url') 10 | echo "Downloading macOS asset from $MAC_URL." 11 | 12 | curl -sL "$MAC_URL" -o /tmp/mac_asset 13 | MAC_SHA=$(shasum -a 256 /tmp/mac_asset | awk '{ print $1 }') 14 | echo "SHA256 for macOS asset is $MAC_SHA." 15 | 16 | LINUX_URL=$(echo "$RELEASE" | jq -r '.assets[] | select(.name | contains("unknown-linux-musl")).browser_download_url') 17 | echo "Downloading Linux asset from $LINUX_URL." 18 | 19 | curl -sL "$LINUX_URL" -o /tmp/linux_asset 20 | LINUX_SHA=$(shasum -a 256 /tmp/linux_asset | awk '{ print $1 }') 21 | echo "SHA256 for Linux asset is $LINUX_SHA." 22 | 23 | sed -e "s|{{ VERSION }}|$VERSION|g" \ 24 | -e "s|{{ MAC_URL }}|$MAC_URL|g" \ 25 | -e "s|{{ MAC_SHA }}|$MAC_SHA|g" \ 26 | -e "s|{{ LINUX_URL }}|$LINUX_URL|g" \ 27 | -e "s|{{ LINUX_SHA }}|$LINUX_SHA|g" "$1" > "$2" 28 | 29 | echo "Formula written!" 30 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/colors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Colors 3 | description: 4 | pls uses colors to differentiate between files of different types and 5 | programming languages and to style different parts of the output. 6 | --- 7 | 8 | import { Content as ColorsOn } from "@/examples/colors/on.mdx"; 9 | import { Content as ColorsOff } from "@/examples/colors/off.mdx"; 10 | 11 | makes a lot of use of colors throughout the output. This makes it very 12 | easy to grok the output and also makes it look pretty. 13 | 14 | ## Environment 15 | 16 | By default will display colors if the terminal supports it and will 17 | disable colors if the output is being piped to another command. 18 | 19 | also respects the `NO_COLOR` and `CLICOLOR_FORCE` environment variables 20 | that can be used forcefully disable or enable colors respectively. 21 | 22 | :::caution 23 | 24 | Turning off colors also turns off other ANSI-based styling such as bold, italic 25 | underlines etc. 26 | 27 | ::: 28 | 29 | ### Examples 30 | 31 | ```bash 32 | env NO_COLOR=true pls # or pls | cat 33 | ``` 34 | 35 | 36 | 37 | ```bash 38 | pls # or env CLICOLOR_FORCE=true pls | cat 39 | ``` 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/src/examples/utils/sub.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from pathlib import Path 4 | 5 | PROJECT_ROOT = project_root = Path(__file__).parents[4] 6 | 7 | 8 | def run_cmd(args: list[str], **kwargs) -> subprocess.CompletedProcess: 9 | """ 10 | Run the given command and return the completed process. 11 | 12 | All commands are executed from the working directory of the project root. 13 | 14 | All keyword arguments are forwarded to ``subprocess.run``. If the ``env`` 15 | keyword argument is provided, it will be merged into the system environment 16 | variables before forwarding. 17 | 18 | :param args: the argument vector to execute 19 | :param kwargs: keyword arguments to forward to ``subprocess.run`` 20 | :return: the completed process, irrespective of success or failure 21 | """ 22 | 23 | proc = subprocess.run( 24 | args, 25 | cwd=kwargs.pop("cwd", PROJECT_ROOT), 26 | capture_output=True, 27 | text=True, 28 | encoding="utf-8", 29 | env=os.environ.copy() | kwargs.pop("env", {}), 30 | **kwargs, 31 | ) 32 | if proc.returncode != 0: 33 | print(proc.stdout) 34 | print(proc.stderr) 35 | return proc 36 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import globals from "globals"; 5 | 6 | import { includeIgnoreFile } from "@eslint/compat"; 7 | 8 | import js from "@eslint/js"; 9 | import ts from "typescript-eslint"; 10 | import astro from "eslint-plugin-astro"; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const srcDir = path.dirname(__filename); 14 | 15 | const gitignorePath = path.resolve(srcDir, ".gitignore"); 16 | 17 | export default [ 18 | includeIgnoreFile(gitignorePath), 19 | 20 | { 21 | languageOptions: { 22 | ecmaVersion: "latest", 23 | sourceType: "module", 24 | globals: { 25 | ...globals.node, 26 | ...globals.browser, 27 | }, 28 | }, 29 | 30 | rules: { 31 | "import/prefer-default-export": "off", 32 | "@typescript-eslint/consistent-type-imports": "error", 33 | }, 34 | }, 35 | 36 | js.configs.recommended, 37 | ...ts.configs.strict, 38 | ...ts.configs.stylistic, 39 | ...astro.configs.recommended, 40 | 41 | // Type definitions 42 | { 43 | files: ["**/*.d.ts"], 44 | rules: { 45 | "@typescript-eslint/triple-slash-reference": "off", 46 | }, 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /docs/src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Props } from "@astrojs/starlight/props"; 3 | 4 | import EditLink from "@astrojs/starlight/components/EditLink.astro"; 5 | import Pagination from "@astrojs/starlight/components/Pagination.astro"; 6 | --- 7 | 8 | 20 | 21 | 48 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/header.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Header 3 | description: 4 | pls shows helpful headers for columns in the detail view and enables users to 5 | customise the text and style of these headers. 6 | --- 7 | 8 | import { Content as HeaderOn } from "@/examples/header/on.mdx"; 9 | import { Content as HeaderOff } from "@/examples/header/off.mdx"; 10 | 11 | shows headers above columns to help understand the output better. If not 12 | set, these headers intelligently appear when they are needed. 13 | 14 | ## Arguments 15 | 16 | `--header`/`-H` can be used to turn headers on or off. It is turned on by 17 | default if the details view is enabled. 18 | 19 | :::caution 20 | 21 | This flag has no effect unless the [detail view](/features/detail_view/) is 22 | enabled with `--det`/`-d`. 23 | 24 | ::: 25 | 26 | ### Examples 27 | 28 | ```bash 29 | pls --det=std # or --header=true or -H=true 30 | ``` 31 | 32 | 33 | 34 | ```bash 35 | pls --det=std --header=false # or -H=false 36 | ``` 37 | 38 | 39 | 40 | ## Configuration 41 | 42 | Using the configuration system, you can modify the column headers and change 43 | their appearance using styling rules. 44 | 45 | See the [detail view configuration](/features/detail_view#configuration) for an 46 | example. 47 | -------------------------------------------------------------------------------- /docs/src/styles/terminal.css: -------------------------------------------------------------------------------- 1 | /** Catppuccin color palette */ 2 | 3 | :root { 4 | /* Used as brand colors. */ 5 | --sol-red: #dc322f; 6 | --sol-magenta: #d33682; 7 | } 8 | 9 | pre.terminal { 10 | --black: #45475a; 11 | --bright-black: #585b70; 12 | --red: #f38ba8; 13 | --green: #a6e3a1; 14 | --yellow: #f9e2af; 15 | --blue: #89b4fa; 16 | --magenta: #f5c2e7; 17 | --cyan: #94e2d5; 18 | --white: #bac2de; 19 | --bright-white: #a6adc8; 20 | 21 | background-color: #1e1e2e; 22 | color: #cdd6f4; 23 | 24 | font-family: var(--__sl-font-mono); 25 | } 26 | 27 | :root[data-theme="light"] pre.terminal { 28 | --black: #bcc0cc; 29 | --red: #d20f39; 30 | --green: #40a02b; 31 | --yellow: #df8e1d; 32 | --blue: #1e66f5; 33 | --magenta: #ea76cb; 34 | --cyan: #179299; 35 | --white: #5c5f77; 36 | --bright-black: #acb0be; 37 | --bright-white: #6c6f85; 38 | 39 | background-color: #eff1f5; 40 | color: #4c4f69; 41 | } 42 | 43 | pre.terminal p { 44 | margin: -0.75rem -1rem; 45 | padding: 0.75rem 1rem; 46 | 47 | overflow-x: auto; 48 | } 49 | 50 | /* Join output code block into its preceding command block */ 51 | div.expressive-code:has(figure.is-terminal) 52 | + div.expressive-code:not(:has(figure.is-terminal)) { 53 | margin-top: 0; 54 | border-top: 1px solid var(--sl-color-gray-4); 55 | } 56 | -------------------------------------------------------------------------------- /src/ext/abs.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a trait [`Abs`], that can be used to extend `Path` and 2 | //! `PathBuf` with a method `abs` that converts a path to an absolute path. 3 | 4 | use std::env::current_dir; 5 | use std::path::{Path, PathBuf}; 6 | 7 | // ===== 8 | // Trait 9 | // ===== 10 | 11 | /// This trait provides a method `abs` that can be used to convert a path 12 | /// to an absolute path. 13 | pub trait Abs { 14 | /// Convert the given path to an absolute path. 15 | /// 16 | /// This function is appends the path to the current working directory if it 17 | /// is not already absolute and if the current working directory can be 18 | /// determined. In all other cases, the path will be returned as-is. 19 | fn abs(&self) -> PathBuf; 20 | } 21 | 22 | // =============== 23 | // Implementations 24 | // =============== 25 | 26 | impl Abs for Path { 27 | fn abs(&self) -> PathBuf { 28 | abs(self) 29 | } 30 | } 31 | 32 | impl Abs for PathBuf { 33 | fn abs(&self) -> PathBuf { 34 | abs(self) 35 | } 36 | } 37 | 38 | // ======= 39 | // Private 40 | // ======= 41 | 42 | fn abs

(path: P) -> PathBuf 43 | where 44 | P: AsRef, 45 | { 46 | let path = path.as_ref(); 47 | if !path.is_absolute() { 48 | if let Ok(cwd) = current_dir() { 49 | return cwd.join(path); 50 | } 51 | } 52 | path.to_path_buf() 53 | } 54 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/direction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Direction 3 | description: 4 | When using the grid view, pls can display node names either row-wise or 5 | column-wise based on the reading preferences of the user. 6 | --- 7 | 8 | import { Content as DirectionOn } from "@/examples/direction/on.mdx"; 9 | import { Content as DirectionOff } from "@/examples/direction/off.mdx"; 10 | 11 | In the grid view, can position nodes in one of two ways. 12 | 13 | - Down: will place nodes along a column and move on to the next column 14 | once the current one has been filled with a specific number of rows. 15 | 16 | - Across: will place nodes along a row and move on to the next row once 17 | the current one has been filled with a specific number of columns. 18 | 19 | ## Arguments 20 | 21 | `--down`/`-D` can be used to turn the downward direction on or off. It is turned 22 | off by default because writing row-wise requires fewer steps. 23 | 24 | :::caution 25 | 26 | This flag has no effect unless the [grid view](/features/grid_view) is enabled 27 | with `--grid`/`-g`. 28 | 29 | ::: 30 | 31 | ### Examples 32 | 33 | ```bash 34 | pls --grid=true # or --down=false or -D=false 35 | ``` 36 | 37 | 38 | 39 | ```bash 40 | pls --grid=true --down=true # or -D=true 41 | ``` 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/upcoming.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Upcoming 3 | description: 4 | pls is constantly evolving, adding new features and refinements to existing 5 | ones. These are some features that pls is planning to support soon. 6 | --- 7 | 8 | is constantly evolving with new features added, existing ones refined 9 | and bugs fixed. We have bold plans for to make it the best `ls(1)` 10 | alternative out there. To that end, here are some features that could 11 | support soon. 12 | 13 | - Git integration 14 | 15 | Git integration is coming soon and will enable the following sub-features: 16 | 17 | - `.gitignore` parsing for [importance](/features/importance/) levels 18 | - Git status codes in the [detail view](/features/detail_view/) with 19 | `--det=git` 20 | 21 | - Spec layers 22 | 23 | Currently, custom specs always have higher precedence than built-in ones. 24 | Defining specs in layers will enable them to be injected between, instead of 25 | after, the built-in specs. 26 | 27 | - Feature parity 28 | 29 | We are working on bringing feature parity between and 30 | [its competition](/about/comparison/). 31 | 32 | To make these features a reality, we need your help. If you are interested in 33 | contributing to , please see the 34 | [contribution guide](/guides/contribute/). 35 | -------------------------------------------------------------------------------- /docs/src/styles/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Monaspace"; 3 | src: url("https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.101/fonts/webfonts/MonaspaceNeon-Regular.woff2") 4 | format("woff2"); 5 | font-weight: 400; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | 10 | @font-face { 11 | font-family: "Monaspace"; 12 | src: url("https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.101/fonts/webfonts/MonaspaceNeon-Italic.woff2") 13 | format("woff2"); 14 | font-weight: 400; 15 | font-style: italic; 16 | font-display: swap; 17 | } 18 | 19 | @font-face { 20 | font-family: "Monaspace"; 21 | src: url("https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.101/fonts/webfonts/MonaspaceNeon-Bold.woff2") 22 | format("woff2"); 23 | font-weight: 700; 24 | font-style: normal; 25 | font-display: swap; 26 | } 27 | 28 | @font-face { 29 | font-family: "Monaspace"; 30 | src: url("https://cdn.jsdelivr.net/gh/githubnext/monaspace@v1.101/fonts/webfonts/MonaspaceNeon-BoldItalic.woff2") 31 | format("woff2"); 32 | font-weight: 700; 33 | font-style: italic; 34 | font-display: swap; 35 | } 36 | 37 | @font-face { 38 | font-family: "Symbols"; 39 | src: url("../assets/fonts/SymbolsNerdFont.ttf") format("truetype"); 40 | font-display: swap; 41 | } 42 | 43 | :root { 44 | --sl-font-mono: "Monaspace", "Symbols"; 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/examples/detail_view.py: -------------------------------------------------------------------------------- 1 | from examples.bench import typ_bench 2 | from examples.utils.fs import fs, mkbigfile 3 | from examples.utils.main import write_out, copy_write_conf 4 | 5 | 6 | def detail_view(): 7 | with typ_bench() as bench: 8 | write_out("--det=all", bench=bench, dest_name="all") 9 | write_out("--det=user", "--det=group", bench=bench, dest_name="sel") 10 | write_out("--det=std", bench=bench, dest_name="std") 11 | write_out("--det=none", bench=bench, dest_name="none") 12 | copy_write_conf(bench) 13 | write_out("--det=all", bench=bench, dest_name="confd") 14 | 15 | 16 | def header(): 17 | with typ_bench() as bench: 18 | write_out("--det=std", bench=bench, dest_name="on") 19 | write_out("--det=std", "--header=false", bench=bench, dest_name="off") 20 | 21 | 22 | def units(): 23 | with fs( 24 | ( 25 | "units", 26 | [ 27 | (name, lambda p: mkbigfile(p, size=1024**idx)) 28 | for (idx, name) in enumerate("abc") 29 | ], 30 | ) 31 | ) as bench: 32 | for unit in ["binary", "decimal", "none"]: 33 | write_out("--det=size", f"--unit={unit}", bench=bench, dest_name=unit) 34 | 35 | 36 | if __name__ == "__main__": 37 | detail_view() 38 | header() 39 | units() 40 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/suffixes.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Suffixes 3 | description: 4 | pls shows symbolic suffixes after node names to help identify the node type 5 | when it is not a regular file. 6 | --- 7 | 8 | import { Content as SuffixesOn } from "@/examples/suffixes/on.mdx"; 9 | import { Content as SuffixesOff } from "@/examples/suffixes/off.mdx"; 10 | import { Content as SuffixesConf } from "@/examples/suffixes/conf.mdx"; 11 | import { Content as SuffixesConfd } from "@/examples/suffixes/confd.mdx"; 12 | 13 | shows suffixes for many common file types. This is usually helpful to 14 | identify file types by just looking at the name. The suffixes are generally 15 | dimmed so as to not appear like they're actually a part of the file name. 16 | 17 | ## Arguments 18 | 19 | `--suffix`/`-S` can be used to turn suffixes on or off. Suffixes are shown by 20 | default because of their utility. 21 | 22 | ### Examples 23 | 24 | ```bash 25 | pls # or --suffix=true or -S=true 26 | ``` 27 | 28 | 29 | 30 | ```bash 31 | pls --suffix=false # or -S=false 32 | ``` 33 | 34 | 35 | 36 | ## Configuration 37 | 38 | Using the configuration system, you can add suffixes for more file types, in 39 | addition to the default set included with , and change the existing 40 | suffixes to your liking. 41 | 42 | ### Examples 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/utils/urls.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for working with URLs. 2 | //! 3 | //! The public interface of the module consists of one function: 4 | //! 5 | //! * [`get_osc`] 6 | 7 | use std::fmt::Display; 8 | 9 | /// Get the OSC-8 escape sequence for the given URL. 10 | /// 11 | /// Many terminal emulators support OSC-8, which allows terminals to 12 | /// render hyperlinks that have a display text that points to a URL 13 | /// which is not displayed. 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `url` - the URL to generate the escape sequence for 18 | /// * `text` - the text to display for the hyperlink 19 | pub fn get_osc(url: S, text: Option) -> String 20 | where 21 | S: AsRef + Display, 22 | { 23 | let text = text.as_ref().unwrap_or(&url); 24 | format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\") 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::get_osc; 30 | 31 | macro_rules! make_test { 32 | ( $($name:ident: $url:expr, $text:expr => $expected:expr,)* ) => { 33 | $( 34 | #[test] 35 | fn $name() { 36 | assert_eq!(get_osc($url, $text), $expected); 37 | } 38 | )* 39 | }; 40 | } 41 | 42 | make_test!( 43 | test_url_and_test: "https://example.com", Some("Example") => "\x1b]8;;https://example.com\x1b\\Example\x1b]8;;\x1b\\", 44 | test_url_only: "https://example.com", None => "\x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\", 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/ext/ctime.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a trait [`Ctime`], that can be used to extend 2 | //! `Metadata` with a method `c_time` that provides the `st_ctime` of a node 3 | //! with an API that matches the other timestamp fields. 4 | 5 | use std::fs::Metadata; 6 | use std::io::Result as IoResult; 7 | #[cfg(unix)] 8 | use std::os::unix::fs::MetadataExt; 9 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 10 | 11 | // ===== 12 | // Trait 13 | // ===== 14 | 15 | /// This trait provides a method `ctime` that provides the `st_ctime` of a node. 16 | /// What this field represents depends on the operating system. 17 | /// 18 | /// > On some systems (like Unix) is the time of the last metadata change, and, 19 | /// > on others (like Windows), is the creation time. 20 | /// > 21 | /// > — [Python documentation](https://docs.python.org/3/library/stat.html#stat.ST_CTIME) 22 | pub trait Ctime { 23 | /// Compute the `st_ctime` of the node. 24 | /// 25 | /// This function matches the signature of other timestamp fields: 26 | /// 27 | /// * [`accessed`](Metadata::accessed) 28 | /// * [`created`](Metadata::created) 29 | /// * [`modified`](Metadata::modified) 30 | fn c_time(&self) -> IoResult; 31 | } 32 | 33 | // =============== 34 | // Implementations 35 | // =============== 36 | 37 | impl Ctime for Metadata { 38 | fn c_time(&self) -> IoResult { 39 | let sec = self.ctime(); 40 | let nanosec = self.ctime_nsec(); 41 | let ctime = UNIX_EPOCH + Duration::new(sec as u64, nanosec as u32); 42 | Ok(ctime) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/name_filter.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Name filter 3 | description: 4 | pls provides a way to filter the contents by regex matching on names, 5 | providing a way to exclude, or only include, certain names. 6 | --- 7 | 8 | import { Content as NameFilterOn } from "@/examples/name_filter/on.mdx"; 9 | import { Content as NameFilterOff } from "@/examples/name_filter/off.mdx"; 10 | import { Content as NameFilterDis } from "@/examples/name_filter/dis.mdx"; 11 | 12 | allows the user to filter the contents by regex matching on names. Name 13 | matching can be used to exclude the names that match a certain pattern or only 14 | include the names that match a certain pattern. Both can be used in tandem to 15 | perform very powerful filtering. 16 | 17 | ## Arguments 18 | 19 | `--exclude`/`-e` can be used to remove the files that match the given pattern. 20 | `--only`/`-o` can be used to remove the files that do not match the given 21 | pattern. 22 | 23 | :::caution 24 | 25 | Specifying both `--only`/`-o` and `--exclude`/`-e` will limit to only 26 | showing files that simultaneously match both rules. 27 | 28 | ::: 29 | 30 | :::caution 31 | 32 | Name filtering is not applicable when listing a specific node. 33 | 34 | ::: 35 | 36 | ## Examples 37 | 38 | ```bash 39 | pls # or --only='.*' --exclude='$^' 40 | ``` 41 | 42 | 43 | 44 | ```bash 45 | pls --only='(a|c)' --exclude='\.jpe?g$' 46 | ``` 47 | 48 | 49 | 50 | ```bash 51 | pls --exclude='\.jpe?g$' a.jpg # `--exclude` has no effect 52 | ``` 53 | 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pls 2 | 3 | GPL-3.0 4 | Platforms 5 | CI status 6 | Documentation 7 | Release 8 | 9 | [`pls`](https://pls.cli.rs/) is a prettier and powerful `ls(1)` for the pros. 10 | The "p" stands for 11 | 12 | - **pretty**: `pls` output is cleaner, friendlier and more colorful. 13 | - **powerful**: `pls` provides more features than the competition. 14 | - **performant**: `pls` is speedy and performant (written in Rust). 15 | - **practical**: `pls` has sensible defaults and an effortless interface. 16 | - **petite**: `pls` is a small, single-file, binary executable. 17 | - **pliable**: `pls` can be extensively tweaked by power users and pros. 18 | - **personable**: `pls` prioritises consumption by humans over scripts. 19 | 20 | Pick whichever adjective helps you remember the command name. 21 | 22 | For more information, see the [documentation](https://pls.cli.rs/). 23 | -------------------------------------------------------------------------------- /src/traits/sym.rs: -------------------------------------------------------------------------------- 1 | use crate::enums::{SymTarget, Typ}; 2 | use crate::exc::Exc; 3 | use crate::models::Node; 4 | use std::fs; 5 | 6 | pub trait Sym { 7 | fn target(&self) -> Option; 8 | } 9 | 10 | impl Sym for Node<'_> { 11 | /// Get the target destination of the node. 12 | /// 13 | /// If the node is not a symlink, the target is `None`. If the node is a 14 | /// symlink, the target is a variant of [`SymTarget`], wrapped in `Some`. 15 | fn target(&self) -> Option { 16 | if self.typ != Typ::Symlink { 17 | return None; 18 | } 19 | 20 | let target_path = match fs::read_link(&self.path) { 21 | Ok(path) => path, 22 | Err(err) => return Some(SymTarget::Error(Exc::Io(err))), 23 | }; 24 | 25 | // Normalise the symlink path. This process handles symlink that use a 26 | // relative path as target. 27 | let abs_target_path = if target_path.is_absolute() { 28 | target_path.clone() 29 | } else if let Some(parent) = self.path.parent() { 30 | parent.join(&target_path) 31 | } else { 32 | self.path.join(&target_path) 33 | }; 34 | 35 | let target = match abs_target_path.try_exists() { 36 | Err(err) => match err.raw_os_error() { 37 | // 62: 'Too many levels of symbolic links' 38 | // 40: 'Symbolic link loop' 39 | Some(62) | Some(40) => SymTarget::Cyclic(target_path), 40 | _ => SymTarget::Error(Exc::Io(err)), 41 | }, 42 | Ok(true) => SymTarget::Ok(Box::new( 43 | Node::new(&abs_target_path).symlink(target_path.to_string_lossy().to_string()), 44 | )), 45 | Ok(false) => SymTarget::Broken(target_path), 46 | }; 47 | Some(target) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/units.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Units 3 | description: 4 | pls can show the sizes of files in three unit systems, binary (powers of 2), 5 | decimal (powers of 10), and none. 6 | --- 7 | 8 | import { Content as UnitsBinary } from "@/examples/units/binary.mdx"; 9 | import { Content as UnitsDecimal } from "@/examples/units/decimal.mdx"; 10 | import { Content as UnitsNone } from "@/examples/units/none.mdx"; 11 | 12 | can show the sizes of files in three unit systems. 13 | 14 | - binary, which uses higher powers of 2 such as 210 as kibi (Ki), 15 | 220 as mibi (Mi) etc. 16 | - decimal, which users higher powers of 10 such as 103 as kilo (k), 17 | 106 as mega (M) etc. 18 | - none, which lists the exact number of bytes (B) as is. 19 | 20 | ## Arguments 21 | 22 | `--unit`/`-u` can be used to set the unit system to use. uses the binary 23 | unit system by default. 24 | 25 | :::caution 26 | 27 | This flag has no effect unless the [detail view](/features/detail_view/) 28 | contains the `size` field with `--det=size`/`-D=size`. 29 | 30 | ::: 31 | 32 | ### Examples 33 | 34 | ```bash 35 | pls --det=size # or --unit=binary or --u=binary 36 | ``` 37 | 38 | 39 | 40 | ```bash 41 | pls --det=size --unit=decimal # or --u=decimal 42 | ``` 43 | 44 | 45 | 46 | ```bash 47 | pls --det=size --unit=none # or --u=binary 48 | ``` 49 | 50 | 51 | 52 | ## Configuration 53 | 54 | Using the configuration system, you can modify the appearance of size magnitude, 55 | prefix and base unit. 56 | 57 | See the [detail view configuration](/features/detail_view#configuration) for an 58 | example. 59 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/symlinks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Symlinks 3 | description: 4 | pls can trace symlinks to their targets and identify broken or circular 5 | symlinks in the process, even expanding chains of symlinks. 6 | --- 7 | 8 | import { Content as SymlinkOn } from "@/examples/symlinks/on.mdx"; 9 | import { Content as SymlinkOff } from "@/examples/symlinks/off.mdx"; 10 | import { Content as SymlinkConf } from "@/examples/symlinks/conf.mdx"; 11 | import { Content as SymlinkConfd } from "@/examples/symlinks/confd.mdx"; 12 | 13 | can trace symlinks to their targets, and in the process identify broken 14 | or circular symlinks. will also expand chains of symlinks. 15 | 16 | ## Arguments 17 | 18 | `--sym`/`-l` can be used to turn symlink tracing on or off. It is turned on by 19 | default but disabled in grid view due to space constraints. 20 | 21 | :::caution 22 | 23 | Note that symlink tracing (which can take large amounts of horizontal space) is 24 | incompatible with the [grid view](/features/grid_view/). In case of conflict, 25 | the grid view will be used and symlink tracing will be turned off. 26 | 27 | Otherwise, you may disable the grid view using `--grid=false`/`-g=false`. 28 | 29 | ::: 30 | 31 | ### Examples 32 | 33 | ```bash 34 | pls # or --sym=true or -l=true 35 | ``` 36 | 37 | 38 | 39 | ```bash 40 | pls --sym=false # or -l=false 41 | ``` 42 | 43 | 44 | 45 | ## Configuration 46 | 47 | Using the configuration system, you can customise the symlink styles and arrows. 48 | Note that for valid symlinks, the style is only applied to the arrow and not the 49 | target. 50 | 51 | ### Examples 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/contribute.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contribute 3 | description: 4 | pls is a free and open-source project that needs your contributions in code, 5 | docs, sponsorships and evangelism. 6 | --- 7 | 8 | accepts contributions in many forms, ranging from direct code 9 | contributions to sponsorships. 10 | 11 | - **Reporting bugs** 12 | 13 | Reporting bugs helps make more stable and reliable. If you find a bug, 14 | or if something breaks, please 15 | [open an issue](https://github.com/pls-rs/pls/issues/). 16 | 17 | - **Suggesting features** 18 | 19 | aims to be a capable alternative to `ls(1)`. If a feature you need is 20 | absent from , please [open an 21 | issue](https://github.com/pls-rs/pls/issues/). 22 | 23 | - **Submitting code** 24 | 25 | We welcome your code contributions towards bug fixes, new features and other 26 | enhancements at our 27 | [GitHub repository](https://github.com/pls-rs/pls/blob/main/src/). 28 | 29 | - **Writing docs** 30 | 31 | If you can improve this documentation, please do so. The docs are 32 | [co-located with the code](https://github.com/pls-rs/pls/blob/main/docs/) 33 | inside the `docs/` directory. 34 | 35 | - **Starring the repo** 36 | 37 | Be sure to star the [GitHub repo](https://github.com/pls-rs/pls/) as a gesture 38 | of appreciation if is an improvement to your workflow. 39 | 40 | - **Spreading the word** 41 | 42 | If you like , you should tell your friends, post about it to social 43 | media and convince others to try it. 44 | 45 | - **Sponsoring me** 46 | 47 | You can [sponsor me](https://github.com/sponsors/dhruvkb), or another 48 | contributor, to contribute to in any of the ways above. 49 | -------------------------------------------------------------------------------- /examples/src/examples/paths.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from examples.utils.fs import fs 4 | from examples.utils.main import write_out, copy_write_conf 5 | 6 | PROJECT_ROOT = Path(__file__).parents[3] 7 | 8 | 9 | def files_and_dirs(): 10 | write_out( 11 | "README.md", 12 | "Cargo.toml", 13 | "Cargo.lock", 14 | "src", 15 | "docs", 16 | bench=PROJECT_ROOT, 17 | include_bench=False, 18 | dest_name="files_and_dirs", 19 | ) 20 | 21 | 22 | def file_group(): 23 | with fs(("file_group", ["a", ("subdir", ["a"])])) as bench: 24 | copy_write_conf(bench, "outer") 25 | copy_write_conf(bench / "subdir", "inner") 26 | write_out( 27 | "a", 28 | "./../file_group/./subdir/a", 29 | "--det=std", 30 | cwd=bench, 31 | include_bench=False, 32 | bench=bench, 33 | dest_name="file_group", 34 | ) 35 | 36 | 37 | def symlinks(): 38 | with fs( 39 | ( 40 | "symlinks", 41 | [ 42 | ("dir", ["README.md", "LICENSE"]), 43 | ("sym", lambda p: p.symlink_to("./dir")), 44 | ], 45 | ) 46 | ) as bench: 47 | write_out( 48 | "sym", 49 | cwd=bench, 50 | include_bench=False, 51 | bench=bench, 52 | dest_name="symlinks", 53 | ) 54 | write_out( 55 | "./dir", 56 | cwd=bench, 57 | include_bench=False, 58 | bench=bench, 59 | dest_name="destination", 60 | ) 61 | 62 | 63 | if __name__ == "__main__": 64 | files_and_dirs() 65 | file_group() 66 | symlinks() 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug to make pls more stable and rock solid. 4 | labels: "goal:fix" 5 | title: "" 6 | --- 7 | 8 | ## Description 9 | 10 | 14 | 15 | ## Expectations 16 | 17 | 21 | 22 | ## Reproduction 23 | 24 | 28 | 29 | 1. 30 | 2. 31 | 3. 32 | 4. See error. 33 | 34 | ## Screenshots 35 | 36 | 40 | 41 | ## Environment 42 | 43 | 47 | 48 | **`pls` specifications:** 49 | 50 | - Version: 51 | 52 | Device specifications: 53 | 54 | - Operating system: 55 | - Version: 56 | - Other info: 57 | 58 | ## Additional information 59 | 60 | 64 | 65 | 69 | -------------------------------------------------------------------------------- /examples/src/examples/filtering.py: -------------------------------------------------------------------------------- 1 | from examples.bench import typ_bench 2 | from examples.utils.main import write_out, copy_write_conf 3 | from examples.utils.fs import fs 4 | 5 | 6 | def type_filter(): 7 | with typ_bench() as bench: 8 | write_out(bench=bench, dest_name="off") 9 | write_out("--typ=dir", "--typ=symlink", bench=bench, dest_name="on") 10 | write_out("--typ=dir", "fifo", cwd=bench, include_bench=False, dest_name="dis") 11 | 12 | 13 | def name_filter(): 14 | with fs( 15 | ( 16 | "name_filter", 17 | ["a.rs", "b.rs", "c.rs", "a.jpg", "b.jpg", "c.jpeg"], 18 | ) 19 | ) as bench: 20 | write_out("--only=(a|c)", r"--exclude=\.jpe?g", bench=bench, dest_name="on") 21 | write_out(bench=bench, dest_name="off") 22 | write_out( 23 | r"--exclude=\.jpe?g", 24 | "a.jpg", 25 | cwd=bench, 26 | include_bench=False, 27 | dest_name="dis", 28 | ) 29 | 30 | 31 | def importance(): 32 | with fs( 33 | ( 34 | "importance", 35 | [ 36 | (".git", []), # importance -2 37 | (".github", []), # importance -1 38 | ("dir", []), # importance 0 39 | "file", # importance 0 40 | "src", # importance 1 41 | "README.md", # importance 2 42 | ], 43 | ) 44 | ) as bench: 45 | for imp in range(-2, 3): 46 | write_out( 47 | f"--imp={imp}", bench=bench, dest_name=f"imp_{imp}".replace("-", "m") 48 | ) 49 | write_out("--imp=2", "file", cwd=bench, include_bench=False, dest_name="dis") 50 | copy_write_conf(bench) 51 | write_out(bench=bench, dest_name="confd") 52 | 53 | 54 | if __name__ == "__main__": 55 | name_filter() 56 | type_filter() 57 | importance() 58 | -------------------------------------------------------------------------------- /docs/src/components/DocBlock.astro: -------------------------------------------------------------------------------- 1 | --- 2 | let { title, fqTitle, required, type, subfieldsType } = Astro.props; 3 | --- 4 | 5 |

6 |
7 | 8 | 9 | 10 | {title} 11 | 12 | 13 | 14 | {required && *} 15 | {type} 16 |
17 | 18 | 19 | 20 | { 21 | Astro.slots.has("subfields") && ( 22 |
23 | 24 | Properties of {subfieldsType || type} 25 | 26 |
27 | 28 |
29 |
30 | ) 31 | } 32 | 33 | { 34 | Astro.slots.has("examples") && ( 35 |
36 | 37 | Examples of {title} 38 | 39 |
40 | 41 |
42 |
43 | ) 44 | } 45 |
46 | 47 | 57 | 58 | 86 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/icons.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Icons 3 | description: 4 | pls shows pretty Nerd Fonts icons prior to node names to help visually 5 | identify the nodes and their types. 6 | --- 7 | 8 | import { Content as IconsOn } from "@/examples/icons/on.mdx"; 9 | import { Content as IconsOff } from "@/examples/icons/off.mdx"; 10 | import { Content as IconsConf } from "@/examples/icons/conf.mdx"; 11 | import { Content as IconsConfd } from "@/examples/icons/confd.mdx"; 12 | 13 | shows pretty and helpful icons next to files by default. These icons can 14 | help when visually searching for a specific type of file. 15 | 16 | ## Arguments 17 | 18 | `--icon`/`-i` can be used to turn icons on or off. Icons are shown by default 19 | because they're so pretty and helpful. 20 | 21 | ### Examples 22 | 23 | ```bash 24 | pls # or --icon=true or -i=true 25 | ``` 26 | 27 | 28 | 29 | ```bash 30 | pls --icon=false # or -i=false 31 | ``` 32 | 33 | 34 | 35 | ## Configuration 36 | 37 | Using the configuration system, you can add more icons, in addition to the 38 | default set included with , and change the default glyphs of built-in 39 | icons. 40 | 41 | Icons are defined in two steps. 42 | 43 | 1. Map an icon name to a glyph. 44 | 45 | If you map a built-in name, you can change default glyphs shown by 46 | for that node. You can use the 47 | [Nerd Fonts reference](https://www.nerdfonts.com/cheat-sheet) to find your 48 | preferred icons. 49 | 50 | 2. Associate file name patterns to icon names. 51 | 52 | Refer to the [specs guide](/guides/specs/) to learn how to do that. 53 | 54 | shows a default icon for some file types (like directory and symlink) 55 | but comes with dormant mappings for every file type. See the example below for 56 | how to enable the default icon or use a custom one. 57 | 58 | ### Examples 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/args/input.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Conf, ConfMan}; 2 | use crate::enums::Typ; 3 | use crate::exc::Exc; 4 | use crate::ext::Abs; 5 | use log::debug; 6 | use std::path::{Path, PathBuf}; 7 | 8 | // ====== 9 | // Models 10 | // ====== 11 | 12 | /// Represents one path entered in the CLI. 13 | /// 14 | /// The path entered in the CLI can be a file or a directory. The path may be a 15 | /// symlink, which should not be resolved, and treated as a file, even if it 16 | /// points to a directory. 17 | pub struct Input { 18 | /// the path as entered in the CLI 19 | pub path: PathBuf, 20 | 21 | /// the absolute version of the path; 22 | /// This version prefixes the CWD if necessary and resolves `.` and `..` 23 | pub abs: PathBuf, 24 | pub typ: Typ, 25 | 26 | /// the config associated with this path 27 | pub conf: Conf, 28 | } 29 | 30 | // =============== 31 | // Implementations 32 | // =============== 33 | 34 | impl std::fmt::Debug for Input { 35 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 36 | f.debug_struct("Input") 37 | .field("path", &self.path) 38 | .field("abs", &self.abs) 39 | .field("typ", &self.typ) 40 | .finish() 41 | } 42 | } 43 | 44 | impl Input { 45 | pub fn new(path: &Path, conf_man: &ConfMan) -> Result { 46 | let path_buf = path.to_path_buf(); 47 | let abs = path.abs(); 48 | 49 | let typ = path.try_into()?; 50 | 51 | let mut conf = conf_man.get(Some(&path))?; 52 | debug!("{path:?} {:?}", conf.specs); 53 | conf.app_const.massage_imps(); 54 | 55 | Ok(Self { 56 | path: path_buf, 57 | abs, 58 | typ, 59 | conf, 60 | }) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use crate::enums::Typ; 67 | use std::path::PathBuf; 68 | 69 | #[test] 70 | fn test_relative() { 71 | let path = PathBuf::from("README.md"); 72 | let typ: Typ = path.as_path().try_into().unwrap_or(Typ::Unknown); 73 | 74 | assert_eq!(typ, Typ::File); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/src/assets/logo_dark.svg: -------------------------------------------------------------------------------- 1 | 5 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /docs/src/assets/logo_light.svg: -------------------------------------------------------------------------------- 1 | 5 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /examples/src/examples/confs/detail_view.yml: -------------------------------------------------------------------------------- 1 | # .pls.yml 2 | entry_const: 3 | dev_style: magenta 4 | ino_style: magenta 5 | nlink_styles: 6 | file_sing: red bold 7 | file_plur: red bold 8 | dir_sing: red bold 9 | dir_plur: red bold 10 | typ: 11 | dir: 12 | ch: d 13 | symlink: 14 | ch: l 15 | fifo: 16 | ch: | 17 | socket: 18 | ch: s 19 | char_device: 20 | ch: b 21 | block_device: 22 | ch: c 23 | file: 24 | ch: "." 25 | oct_styles: 26 | special: magenta 27 | user: magenta 28 | group: magenta 29 | other: magenta 30 | user_styles: 31 | curr: bold yellow 32 | other: "" 33 | group_styles: 34 | curr: bold yellow 35 | other: "" 36 | size_styles: 37 | mag: green bold 38 | prefix: green 39 | base: hidden 40 | blocks_style: cyan 41 | timestamp_formats: 42 | btime: [day] [month repr:short] [hour repr:24]:[minute] 43 | ctime: [day] [month repr:short] [hour repr:24]:[minute] 44 | mtime: [day] [month repr:short] [hour repr:24]:[minute] 45 | atime: [day] [month repr:short] [hour repr:24]:[minute] 46 | app_const: 47 | table: 48 | header_style: clear 49 | column_names: 50 | dev: Device 51 | ino: inode 52 | nlink: Links 53 | perm: Permissions 54 | oct: Octal 55 | user: User 56 | uid: UID 57 | group: Group 58 | gid: GID 59 | size: Size 60 | blocks: Blocks 61 | btime: Date Created 62 | ctime: Date Changed 63 | mtime: Date Modified 64 | atime: Date Accessed 65 | git: Git 66 | name: Name 67 | -------------------------------------------------------------------------------- /docs/src/components/Stars.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "@astrojs/starlight/components"; 3 | 4 | const { owner, repo } = Astro.props; 5 | --- 6 | 7 | 10 | 11 | 14 | many 15 | 16 | 17 | 18 | 35 | 36 | 72 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/grid_view.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Grid view 3 | pls: 4 | pls can display node names in two dimensions by showing multiple nodes per row 5 | reducing vertical space when the grid view has been activated. 6 | --- 7 | 8 | import { Content as GridViewOff } from "@/examples/grid_view/off.mdx"; 9 | import { Content as GridViewOn } from "@/examples/grid_view/on.mdx"; 10 | 11 | can reduce the amount of vertical space taken by the output, thus 12 | reducing the amount of strolling required, by placing the node names in a grid 13 | instead of a column. 14 | 15 | ## Arguments 16 | 17 | `--grid`/`-g` can be used to turn the grid mode on or off. Grid mode is turned 18 | off by default because it reduces the amount of info shown per file. 19 | 20 | The direction of the grid can be changed with the 21 | [`--down`/`-D` flag](/features/direction/). 22 | 23 | :::caution 24 | 25 | Note that the grid view is incompatible with the 26 | [detail view](/features/detail_view/). In case of conflict, the detail view will 27 | be used and the grid view will be turned off. 28 | 29 | Otherwise, you may disable the detail view using `--det=none`/`-d=none`. 30 | 31 | ::: 32 | 33 | :::caution 34 | 35 | Note that the grid view is also incompatible with 36 | [symlink tracing](/features/symlink/) (which can take large amounts of 37 | horizontal space). In case of conflict, the grid view will be used and symlink 38 | tracing will be turned off. 39 | 40 | ::: 41 | 42 | :::caution 43 | 44 | Note that the grid view is also incompatible with 45 | [collapsing](/features/collapse/) (which leads to nodes that expand to multiple 46 | lines). In case of conflict, the grid view will be used and collapsing will be 47 | turned off. 48 | 49 | ::: 50 | 51 | All these incompatibilities are the reason why the grid view is not the default 52 | view. 53 | 54 | ### Examples 55 | 56 | ```bash 57 | pls # or --grid=false or -g=false 58 | ``` 59 | 60 | 61 | 62 | ```bash 63 | pls --grid=true # or -g=true 64 | ``` 65 | 66 | 67 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := false 2 | 3 | # Show all available recipes 4 | # Show all available recipes, also recurses inside nested justfiles. 5 | @_default: 6 | just --list --unsorted 7 | printf "\nExamples:\n" 8 | printf "=========\n" 9 | just examples/ 10 | printf "\nDocs:\n" 11 | printf "=====\n" 12 | just docs/ 13 | 14 | ######### 15 | # Setup # 16 | ######### 17 | 18 | # Install dependencies for sub-projects. 19 | install: 20 | # Cargo does not need an install step. 21 | just docs/install 22 | just examples/install 23 | 24 | # Download pre-commits and install Git hooks. 25 | pre-commit version="3.8.0": 26 | curl \ 27 | --output pre-commit.pyz \ 28 | --location \ 29 | "https://github.com/pre-commit/pre-commit/releases/download/v{{ version }}/pre-commit-{{ version }}.pyz" 30 | python3 pre-commit.pyz install 31 | 32 | # Run pre-commit to lint and format files. 33 | lint hook="" *files="": 34 | python3 pre-commit.pyz run {{ hook }} {{ if files == "" { "--all-files" } else { "--files" } }} {{ files }} 35 | 36 | ########### 37 | # Recipes # 38 | ########### 39 | 40 | # Run the program. 41 | run *args: 42 | cargo run -- {{ args }} 43 | 44 | # Run the program with debug logging. 45 | debug *args: 46 | env RUST_LOG=debug just run {{ args }} 47 | 48 | # Run tests. 49 | test *args: 50 | cargo test {{ args }} 51 | 52 | ########### 53 | # Release # 54 | ########### 55 | 56 | # Build the release binary. 57 | release: 58 | cargo build --release 59 | 60 | # Install `cross`, if it does not already exist. 61 | get-cross: 62 | [ -x "$(command -v cross)" ] || cargo install cross 63 | 64 | # Build a release binary for the given target with `cross`. 65 | cross target: 66 | cross build --release --verbose --target {{ target }} 67 | 68 | ########### 69 | # Aliases # 70 | ########### 71 | 72 | alias i := install 73 | alias p := pre-commit 74 | alias l := lint 75 | 76 | alias r := run 77 | alias d := debug 78 | alias t := test 79 | 80 | alias R := release 81 | -------------------------------------------------------------------------------- /src/models/pls.rs: -------------------------------------------------------------------------------- 1 | use crate::args::{Group, Input}; 2 | use crate::config::{Args, ConfMan}; 3 | use crate::fmt::render; 4 | use crate::models::{OwnerMan, Window}; 5 | 6 | /// Represents the entire application state. 7 | /// 8 | /// This struct also holds various globals that are used across the 9 | /// application. 10 | #[derive(Default)] 11 | pub struct Pls { 12 | /// configuration manager for `.pls.yml` files 13 | pub conf_man: ConfMan, 14 | /// command-line arguments 15 | pub args: Args, 16 | /// whether the terminal supports Kitty's terminal graphics protocol 17 | pub supports_gfx: bool, 18 | /// the width and height of a terminal cell in pixels 19 | pub window: Option, 20 | } 21 | 22 | impl Pls { 23 | /// Handle the `pls` command and its subcommands. 24 | /// 25 | /// This is the entrypoint of the application that takes over the 26 | /// control from `main`. 27 | pub fn cmd(&self) { 28 | // TODO: Handle subcommands. 29 | self.run(); 30 | } 31 | 32 | /// Run `pls`. 33 | /// 34 | /// This is the entrypoint of the `Pls` class, and once control is passed 35 | /// to it from `main`, it handles everything. 36 | /// 37 | /// The primary function of this method is to organise the input list of 38 | /// paths into groups and then delegate to each group the job of listing 39 | /// their entries and rendering the layout. 40 | fn run(&self) { 41 | let inputs: Vec<_> = self 42 | .args 43 | .paths 44 | .iter() 45 | .filter_map(|path| { 46 | let input = Input::new(path, &self.conf_man); 47 | match input { 48 | Ok(input) => Some(input), 49 | Err(exc) => { 50 | let loc = render(format!("{}", path.display())); 51 | println!("{loc}:"); 52 | println!("\t{exc}"); 53 | None 54 | } 55 | } 56 | }) 57 | .collect(); 58 | 59 | let show_title = self.args.paths.len() > 1; 60 | let groups = Group::partition(inputs, &self.conf_man); 61 | 62 | groups 63 | .iter() 64 | .map(|group| group.render(show_title, &mut OwnerMan::default())) 65 | .filter_map(|res| res.err()) 66 | .for_each(|res| println!("{res}")); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/actions/setup-env/action.yml: -------------------------------------------------------------------------------- 1 | name: pls/setup-env 2 | description: Setup environment for CI + CD jobs 3 | 4 | inputs: 5 | rust_cache_key: 6 | description: The key to use to identify the Rust toolchain cache 7 | default: cargo 8 | rust_components: 9 | description: dtolnay/rust-toolchain#components 10 | required: false 11 | rust_target: 12 | description: dtolnay/rust-toolchain#target 13 | required: false 14 | 15 | runs: 16 | using: composite 17 | steps: 18 | - name: Setup `just` 19 | uses: taiki-e/install-action@v2 20 | with: 21 | tool: just 22 | 23 | - name: Install Rust toolchain 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | components: ${{ inputs.rust_components }} 27 | target: ${{ inputs.rust_target }} 28 | 29 | - uses: actions/cache@v4 30 | with: 31 | path: | 32 | ~/.cargo/bin/ 33 | ~/.cargo/registry/index/ 34 | ~/.cargo/registry/cache/ 35 | ~/.cargo/git/db/ 36 | target/ 37 | key: 38 | ${{ runner.os }}-${{ inputs.rust_cache_key }}-${{ 39 | hashFiles('**/Cargo.lock') }} 40 | 41 | - name: Enable Corepack 42 | shell: bash 43 | run: corepack enable pnpm 44 | 45 | - name: Setup Node.js 46 | uses: actions/setup-node@v4 47 | with: 48 | cache: pnpm 49 | node-version-file: .nvmrc 50 | env: 51 | COREPACK_INTEGRITY_KEYS: '{"npm":[{"expires":"2025-01-29T00:00:00.000Z","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","key":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="},{"expires":null,"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","keytype":"ecdsa-sha2-nistp256","scheme":"ecdsa-sha2-nistp256","key":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g=="}]}' 52 | 53 | - name: Install Node.js dependencies 54 | shell: bash 55 | run: pnpm install 56 | -------------------------------------------------------------------------------- /examples/src/examples/bench.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains reusable benches that are used by multiple example 3 | generation files. 4 | """ 5 | 6 | import os 7 | import uuid 8 | 9 | from examples.utils.fs import mksock, mkbigfile, fs 10 | 11 | 12 | def typ_bench(name: str = None): 13 | if name is None: 14 | name = uuid.uuid4().hex 15 | 16 | return fs( 17 | ( 18 | name, 19 | [ 20 | ("dir", []), 21 | ("sym", lambda p: p.symlink_to("./dir")), 22 | ("fifo", lambda p: os.mkfifo(p)), 23 | ("socket", lambda p: mksock(p)), 24 | ("char_dev", lambda p: p.symlink_to("/dev/null")), 25 | ("block_dev", lambda p: p.symlink_to("/dev/disk0")), 26 | ("file", lambda p: mkbigfile(p, size=1024**2)), 27 | ], 28 | ) 29 | ) 30 | 31 | 32 | def grid_bench(name: str = None): 33 | if name is None: 34 | name = uuid.uuid4().hex 35 | 36 | return fs( 37 | ( 38 | name, 39 | [ 40 | "file_abcd", 41 | "file_efgh", 42 | "file_ijkl", 43 | (".file_mnop", lambda p: p.symlink_to("file_abcd")), 44 | ], 45 | ) 46 | ) 47 | 48 | 49 | def ts_bench(name: str = None): 50 | if name is None: 51 | name = uuid.uuid4().hex 52 | 53 | return fs( 54 | ( 55 | name, 56 | [ 57 | ( 58 | "src", 59 | [ 60 | "index.ts", 61 | "index.js", 62 | "lib.ts", 63 | "lib.js", 64 | "no_parent.js", 65 | "no_child.ts", 66 | ], 67 | ), 68 | "package.json", 69 | "pnpm-lock.yaml", 70 | ".gitignore", 71 | ".prettierignore", 72 | "prettier.config.js", 73 | "tsconfig.json", 74 | "README.md", 75 | "justfile", 76 | ], 77 | ) 78 | ) 79 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pls" 3 | description = "pls is a prettier and powerful ls(1) for the pros" 4 | version = "0.0.1-beta.9" 5 | 6 | authors = ["Dhruv Bhanushali "] 7 | 8 | license = "GPL-3.0-or-later" 9 | 10 | homepage = "https://pls.cli.rs/" 11 | repository = "https://github.com/pls-rs/pls" 12 | documentation = "https://pls.cli.rs/" 13 | readme = "README.md" 14 | exclude = [".github/", "/readme_assets", "/justfile", "/.gitignore"] 15 | 16 | keywords = ["cli", "terminal", "posix", "ls"] 17 | categories = ["command-line-utilities"] 18 | 19 | edition = "2021" 20 | rust-version = "1.80.0" 21 | 22 | [[bin]] 23 | name = "pls" 24 | 25 | [dependencies] 26 | base64 = "0.22.1" 27 | clap = { version = "4.3.11", features = ["derive", "wrap_help"] } 28 | colored = "2.0.0" 29 | crossterm = { version = "0.28.1", default-features = false } 30 | env_logger = { version = "0.11.5", default-features = false } 31 | figment = { version = "0.10.10", features = ["yaml", "test"] } 32 | git2 = { version = "0.19.0", default-features = false } 33 | home = "0.5.5" 34 | libc = "0.2.158" 35 | log = { version = "0.4.19", features = ["release_max_level_off"] } 36 | number_prefix = "0.4.0" 37 | path-clean = "1.0.1" 38 | regex = { version = "1.8.4", default-features = false, features = ["std", "perf"] } 39 | resvg = { version = "0.43.0", default-features = false } 40 | serde = { version = "1.0.164", features = ["derive"] } 41 | serde_regex = "1.1.0" 42 | shellexpand = { version = "3.1.0", default-features = false, features = ["base-0"] } 43 | time = { version = "0.3.22", default-features = false, features = ["std", "alloc", "local-offset", "formatting"] } 44 | unicode-segmentation = "1.10.1" 45 | uzers = { version = "0.12.1", default-features = false, features = ["cache"] } 46 | xterm-query = "0.5.2" 47 | 48 | [profile.release] 49 | # Reference: https://github.com/johnthagen/min-sized-rust 50 | codegen-units = 1 51 | panic = "abort" 52 | lto = true 53 | strip = true 54 | 55 | [package.metadata.release] 56 | sign-commit = true 57 | sign-tag = true 58 | publish = false 59 | push = false 60 | pre-release-commit-message = "Release {{version}}" 61 | tag-message = "Release {{crate_name}} version {{version}}" 62 | -------------------------------------------------------------------------------- /src/traits/name.rs: -------------------------------------------------------------------------------- 1 | use crate::models::Node; 2 | use std::path::PathBuf; 3 | 4 | pub trait Name { 5 | fn ext(&self) -> String; 6 | fn stem(&self) -> String; 7 | fn cname(&self) -> String; 8 | 9 | fn aligned_name(&self) -> String; 10 | } 11 | 12 | impl Name for Node<'_> { 13 | // =========== 14 | // Sort fields 15 | // =========== 16 | 17 | /// Get the extension for a node. 18 | /// 19 | /// Returns a blank string if the node does not have an extension. 20 | fn ext(&self) -> String { 21 | self.path 22 | .extension() 23 | .unwrap_or_default() 24 | .to_string_lossy() 25 | .to_string() 26 | } 27 | 28 | /// Get the name for the node, without the extension, if any. 29 | /// 30 | /// Returns the full name if the node does not have an extension. 31 | fn stem(&self) -> String { 32 | self.path 33 | .file_stem() 34 | .unwrap_or_default() 35 | .to_string_lossy() 36 | .to_string() 37 | } 38 | 39 | /// Get the canonical name for the node. 40 | /// 41 | /// The canonical name is the name of the node, stripped of leading symbols 42 | /// and normalised to lowercase. 43 | fn cname(&self) -> String { 44 | self.name 45 | .to_lowercase() 46 | .trim_start_matches(|c: char| !c.is_alphanumeric()) 47 | .to_string() 48 | } 49 | 50 | // =============== 51 | // Name components 52 | // =============== 53 | 54 | /// Get the name of the node when aligning for leading dots. 55 | /// 56 | /// If the node name starts with a dot, the dot is dimmed. If not, the name 57 | /// is left-padded with a space to line up the alphabetic characters. 58 | fn aligned_name(&self) -> String { 59 | let path = PathBuf::from(&self.display_name); 60 | if let Some(name) = path.file_name() { 61 | let name = name.to_string_lossy(); 62 | 63 | // 'clear' ensures that the dot and padding spaces are not formatted. 64 | let aligned_name = if name.starts_with('.') { 65 | format!(".{}", name.strip_prefix('.').unwrap()) 66 | } else { 67 | format!(" {}", name) 68 | }; 69 | 70 | if let Some(parent) = path.parent() { 71 | return parent.join(aligned_name).to_string_lossy().to_string(); 72 | } 73 | } 74 | self.display_name.clone() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/models/spec.rs: -------------------------------------------------------------------------------- 1 | use crate::enums::Collapse; 2 | use regex::bytes::{Regex, RegexBuilder}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Represents the specification for identifying and styling a node. 6 | /// 7 | /// Specs are the ideological core of `pls` and the key differentiating factor 8 | /// from other tools. 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub struct Spec { 11 | /// a regex pattern to match against the node's name 12 | #[serde(with = "serde_regex")] 13 | pub pattern: Regex, 14 | /// names of the icon to use for the node 15 | pub icons: Option>, 16 | /// styles to apply to the node name and icon 17 | pub style: Option, 18 | /// the importance level of the node 19 | pub importance: Option, 20 | /// the rule for determining the parent node, if any, for this node 21 | pub collapse: Option, 22 | } 23 | 24 | impl Spec { 25 | /// Create a basic `Spec` instance with only a pattern and an icon. 26 | /// 27 | /// `Spec` follows a builder pattern, so you can chain the following method 28 | /// to define the remaining fields. 29 | /// 30 | /// - [`importance`](Spec::importance) 31 | /// - [`style`](Spec::style) 32 | /// - [`collapse`](Spec::collapse) 33 | pub fn new(pattern: &str, icon: &str) -> Self { 34 | Self { 35 | pattern: RegexBuilder::new(pattern).unicode(false).build().unwrap(), 36 | icons: Some(vec![String::from(icon)]), 37 | style: None, 38 | importance: None, 39 | collapse: None, 40 | } 41 | } 42 | 43 | /// Consume the current `Spec` instance and return a new one with the 44 | /// specified importance level. 45 | pub fn importance(self, importance: i8) -> Self { 46 | Self { 47 | importance: Some(importance), 48 | ..self 49 | } 50 | } 51 | 52 | /// Consume the current `Spec` instance and return a new one with the 53 | /// specified style directives. 54 | pub fn style(self, style: &str) -> Self { 55 | Self { 56 | style: Some(String::from(style)), 57 | ..self 58 | } 59 | } 60 | 61 | /// Consume the current `Spec` instance and return a new one with the 62 | /// specified collapse definition. 63 | pub fn collapse(self, collapse: Collapse) -> Self { 64 | Self { 65 | collapse: Some(collapse), 66 | ..self 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/collapse.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collapse 3 | description: 4 | pls can reduce clutter in your file listing by collapsing derivative nodes 5 | into their principal nodes. 6 | --- 7 | 8 | import { Content as CollapseOn } from "@/examples/collapse/on.mdx"; 9 | import { Content as CollapseOff } from "@/examples/collapse/off.mdx"; 10 | import { Content as CollapseConf } from "@/examples/collapse/conf.mdx"; 11 | import { Content as CollapseConfd } from "@/examples/collapse/confd.mdx"; 12 | 13 | In software development it is a common occurrence to have files that are 14 | generated from other files. 15 | 16 | - Lockfiles are generated from package manifests. 17 | - Source maps are generated from source code. 18 | - Classes are generated by compilers parsing the source code. 19 | - Code files are generated by transpilers converting code in another language. 20 | 21 | has the novel ability for you to define the collapsing rules for these 22 | derived files so that they are both listed together with their source, and also 23 | depicted in a way that emphasises their dependency relationship. 24 | 25 | can also nest collapsed nodes into other collapsed nodes, forming a full 26 | tree of collapses. 27 | 28 | ## Arguments 29 | 30 | `--collapse`/`-c` can be used to turn collapsing on or off. enables this 31 | collapsing behaviour by default. 32 | 33 | :::caution 34 | 35 | Note that collapsing (which leads to nodes that expand to multiple lines) is 36 | incompatible with the [grid view](/features/grid_view/). In case of conflict, 37 | the grid view will be used and collapsing will be turned off. 38 | 39 | Otherwise, you may disable the grid view using `--grid=false`/`-g=false`. 40 | 41 | ::: 42 | 43 | ### Examples 44 | 45 | ```bash 46 | pls # or --collapse=true or -c=true 47 | ``` 48 | 49 | 50 | 51 | ```bash 52 | pls --collapse=false # or -c=false 53 | ``` 54 | 55 | 56 | 57 | ## Configuration 58 | 59 | Using the configuration system, you can both change the appearance of the tree 60 | and also define additional rules for collapsing in addition to the built-in 61 | ones. 62 | 63 | ### Examples 64 | 65 | Here is an example showing nested collapses. It uses simple ASCII characters to 66 | render colorful collapse trees. 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/src/examples/utils/main.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import shutil 3 | from pathlib import Path 4 | 5 | from examples.utils.pls import run_pls 6 | from examples.utils.io import write_content 7 | 8 | 9 | CONFS = Path(__file__).parents[1] / "confs" 10 | 11 | 12 | def write_out( 13 | *args: str, 14 | bench: Path | None = None, 15 | include_bench: bool = True, 16 | dest_name: str, 17 | **kwargs, 18 | ): 19 | """ 20 | Run ``pls`` with the given arguments and write the output to an MDX file. 21 | 22 | :param args: the arguments to pass to ``pls`` (except the workbench path) 23 | :param bench: the path to the workbench 24 | :param include_bench: whether to include the workbench path in the arguments 25 | :param dest_name: the name of the output MDX file 26 | :param kwargs: addition keyword arguments to pass to ``run_pls`` 27 | """ 28 | 29 | func_name = _caller() 30 | 31 | args = list(args) 32 | if bench and include_bench: 33 | args.append(str(bench.absolute())) 34 | content = run_pls(args, **kwargs) 35 | 36 | out_file = f"{func_name}/{dest_name}.mdx" 37 | print(f"Writing MDX to '{out_file}'.") 38 | write_content(out_file, content) 39 | 40 | 41 | def copy_write_conf(path: Path, name: str | None = None): 42 | """ 43 | Move the config file from the ``confs`` directory into the workbench and 44 | write a copy as MDX. 45 | 46 | This function also exports the config as an MDX file in the ``examples`` 47 | directory of the docs. 48 | 49 | :param path: the path to the workbench 50 | :param name: the name of the source and destination config file 51 | """ 52 | 53 | func_name = _caller() 54 | 55 | src = CONFS / f"{name or func_name}.yml" 56 | dest = path / ".pls.yml" 57 | print(f"Copying '{src}' to '{dest}'.") 58 | shutil.copy(src, dest) 59 | 60 | out_file = f"{func_name}/{name or 'conf'}.mdx" 61 | print(f"MDX destination is '{out_file}'.") 62 | write_content(out_file, f"```yml\n{src.read_text()}```") 63 | 64 | 65 | def _caller() -> str: 66 | """ 67 | Get the name of the file two levels above the current frame. This function 68 | only returns the name, stripping away the path and the extension. 69 | 70 | :return: the name of the file 71 | """ 72 | 73 | prev_frame = inspect.currentframe().f_back.f_back 74 | func_name = inspect.getframeinfo(prev_frame).function 75 | return func_name 76 | -------------------------------------------------------------------------------- /src/enums/sym.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Conf; 2 | use crate::exc::Exc; 3 | use crate::models::Node; 4 | use serde::{Deserialize, Serialize}; 5 | use std::path::PathBuf; 6 | 7 | /// This enum contains the four states a symlink can be in, out of which one is 8 | /// fine and the rest are problematic. 9 | /// 10 | /// This enum is a unitary enum intended only for use as a `HashMap` key when 11 | /// defining the constants in the config. 12 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 13 | #[serde(rename_all = "snake_case")] 14 | pub enum SymState { 15 | Ok, 16 | Broken, 17 | Cyclic, 18 | Error, 19 | } 20 | 21 | impl From<&SymTarget<'_>> for SymState { 22 | fn from(value: &SymTarget) -> Self { 23 | match value { 24 | SymTarget::Ok(_) => SymState::Ok, 25 | SymTarget::Broken(_) => SymState::Broken, 26 | SymTarget::Cyclic(_) => SymState::Cyclic, 27 | SymTarget::Error(_) => SymState::Error, 28 | } 29 | } 30 | } 31 | 32 | /// This enum is an extension of [`SymState`] that, in addition to the four 33 | /// states of a symlink, also contains additional data relevant to the state. 34 | /// 35 | /// * `Ok` contains a [`Node`] instance wrapping the target path. 36 | /// * `Broken` and `Cyclic` contain the target path as a [`PathBuf`] instance. 37 | /// * `Error` contains the raised [`std::io::Error`] instance. 38 | pub enum SymTarget<'node> { 39 | Ok(Box>), // Valid targets should print like `Node`s. 40 | Broken(PathBuf), // Invalid targets should be kept as-is. 41 | Cyclic(PathBuf), // Target is self, so there is nothing to print. 42 | Error(Exc), // Target cannot be determined. 43 | } 44 | 45 | impl SymTarget<'_> { 46 | /// Print the symlink target. 47 | pub fn print(&self, conf: &Conf) -> String { 48 | let state = self.into(); 49 | let sym_conf = conf.entry_const.symlink.get(&state).unwrap(); 50 | let directives = &sym_conf.style; 51 | let ref_directives = &sym_conf.ref_style; 52 | let sep = &sym_conf.sep; 53 | 54 | match self { 55 | SymTarget::Ok(node) => { 56 | let path = node.display_name(conf, &conf.app_const, &conf.entry_const, &[]); 57 | format!(" <{directives}>{sep} <{ref_directives}>{path}") 58 | } 59 | SymTarget::Broken(path) | SymTarget::Cyclic(path) => { 60 | let path = path.to_string_lossy().to_string(); 61 | format!(" <{directives}>{sep} <{ref_directives}>{path}") 62 | } 63 | SymTarget::Error(exc) => { 64 | format!(" <{directives}>{sep} {exc}",) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/paths.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Paths 3 | description: 4 | pls organises paths supplied as CLI arguments into groups of files and 5 | individual directories. 6 | --- 7 | 8 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 9 | 10 | import { Content as FilesAndDirs } from "@/examples/files_and_dirs/files_and_dirs.mdx"; 11 | import { Content as OuterConf } from "@/examples/file_group/outer.mdx"; 12 | import { Content as InnerConf } from "@/examples/file_group/inner.mdx"; 13 | import { Content as FileGroup } from "@/examples/file_group/file_group.mdx"; 14 | import { Content as Symlinks } from "@/examples/symlinks/symlinks.mdx"; 15 | import { Content as Destination } from "@/examples/symlinks/destination.mdx"; 16 | 17 | First organises paths supplied as CLI arguments into groups of solo 18 | files and individual directories. Then it prints each group one by one. 19 | 20 | ```bash 21 | pls README.md Cargo.toml Cargo.lock src docs 22 | ``` 23 | 24 | 25 | 26 | ## Solo files 27 | 28 | The solo files group consists of all files supplied individually. These files 29 | are collected into one group. Each of the files in this group comes with its own 30 | separate [configuration](/reference/conf/) derived from `.pls.yml` files. 31 | 32 | The special thing here is that the group also has its own configuration 33 | determined from the common ancestor of all these files. This group-level 34 | configuration sets top-level options such as table headings, box-drawing 35 | characters and importance scales. 36 | 37 | Consider the following filesystem tree: 38 | 39 | ``` 40 | file_group 41 | ├── .pls.yml 42 | ├── a 43 | └── subdir 44 | ├── .pls.yml 45 | └── b 46 | ``` 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ```bash 58 | pls a ./../file_group/./subdir/a 59 | ``` 60 | 61 | 62 | 63 | Note how both files retain their individual configurations for the row, but the 64 | table settings come from the outer `.pls.yml` file. Also note that the file 65 | names are shown exactly as they were passed on the command line. 66 | 67 | ## Symlinks 68 | 69 | By default, does not follow symlinks in the arguments provided to it. So 70 | a symlink to a directory will be treated as a file input and will not list the 71 | contents of the target directory. 72 | 73 | ```bash 74 | pls sym 75 | ``` 76 | 77 | 78 | 79 | ```bash 80 | pls ./dir 81 | ``` 82 | 83 | 84 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/type_filter.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Type filter 3 | description: 4 | pls provides a way to filter the contents by node type which, combined with 5 | the name filter, makes it easier to find what you are searching for. 6 | --- 7 | 8 | import { Content as TypeFilterOff } from "@/examples/type_filter/off.mdx"; 9 | import { Content as TypeFilterOn } from "@/examples/type_filter/on.mdx"; 10 | import { Content as TypeFilterDis } from "@/examples/type_filter/dis.mdx"; 11 | 12 | allows the user to only list specific node types in the output. 13 | 14 | ## Arguments 15 | 16 | `--typ`/`-t` can be used to select the node types the user wants to see. The 17 | flag can be specified multiple times to enable multiple file types. Each time it 18 | can take one of these values. 19 | 20 | | Value | Description | 21 | | ------------ | ----------------------------- | 22 | | dir | regular folder | 23 | | symlink | symbolic link | 24 | | fifo | named pipe | 25 | | socket | file-based socket | 26 | | block_device | block special device file | 27 | | char_device | character special device file | 28 | | file | regular file | 29 | | none | **shorthand:** no node types | 30 | | all | **shorthand:** all node types | 31 | 32 | :::caution 33 | 34 | Type filtering is not applicable when listing a specific node. 35 | 36 | ::: 37 | 38 | ### Mechanism 39 | 40 | When parsing the `--typ`/`-t` flag, values are read from the CLI, in order, and 41 | added to a vector of node types till we encounter a shorthand value. If the 42 | shorthand value is `none`, the vector is cleared. If the shorthand value is 43 | `all`, the vector is filled with all the node types. 44 | 45 | For example, consider the invocation below. 46 | 47 | ```bash 48 | pls --typ=symlink --typ=all --typ=none --typ=dir 49 | ``` 50 | 51 | - `symlink` is added to the list. 52 | - All the file types are added to the list, including the already existing 53 | `symlink`. 54 | - All entries so far are dropped when `none` is encountered. 55 | - `dir` is added to the list. 56 | 57 | The final set contains only `dir` and so will only list the directories. 58 | 59 | ### Examples 60 | 61 | ```bash 62 | pls # or --typ=all or --t=all 63 | ``` 64 | 65 | 66 | 67 | ```bash 68 | pls --typ=dir --typ=symlink # or -t=dir -t=symlink 69 | ``` 70 | 71 | 72 | 73 | ```bash 74 | pls --typ=dir --typ=symlink fifo # `--typ` has no effect 75 | ``` 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/importance.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Importance 3 | description: 4 | pls uses an importance-based system to emphasize, de-emphasize or hide nodes 5 | instead of naïvely checking if the node name has a leading dot. 6 | --- 7 | 8 | import { Content as ImportanceM2 } from "@/examples/importance/imp_m2.mdx"; 9 | import { Content as ImportanceM1 } from "@/examples/importance/imp_m1.mdx"; 10 | import { Content as Importance0 } from "@/examples/importance/imp_0.mdx"; 11 | import { Content as Importance1 } from "@/examples/importance/imp_1.mdx"; 12 | import { Content as Importance2 } from "@/examples/importance/imp_2.mdx"; 13 | import { Content as ImportanceDis } from "@/examples/importance/dis.mdx"; 14 | import { Content as ImportanceConf } from "@/examples/importance/conf.mdx"; 15 | import { Content as ImportanceConfd } from "@/examples/importance/confd.mdx"; 16 | 17 | uses an importance system to both hide certain unimportant nodes as well 18 | as emphasize certain important ones. 19 | 20 | Each node has a default importance of -1 if it starts with a leading dot, and 0 21 | otherwise. This importance can be overridden using [specs](/guides/specs/). 22 | 23 | An importance scale maps importance levels to styling attributes. Any files with 24 | importance below the scale are hidden. 25 | 26 | ## Arguments 27 | 28 | `--imp`/`-I` can be used to set the baseline importance level. This affects the 29 | relative importance of all nodes. The default is 0. Setting a higher number will 30 | reduce the importance level of nodes and hide more of them. 31 | 32 | At `--imp=-2`, a node with importance -2 behaves as it has an importance of 0, a 33 | node with importance -1 behaves as it has an importance of 1, and so on. 34 | 35 | :::caution 36 | 37 | Filtering by importance is not applicable when listing a specific node. 38 | 39 | ::: 40 | 41 | ### Examples 42 | 43 | ```bash 44 | pls --imp=-2 # or -I=-2 45 | ``` 46 | 47 | 48 | 49 | ```bash 50 | pls --imp=-1 # or -I=-1 51 | ``` 52 | 53 | 54 | 55 | ```bash 56 | pls # or --imp=0 or -I=0 57 | ``` 58 | 59 | 60 | 61 | ```bash 62 | pls --imp=1 # or -I=1 63 | ``` 64 | 65 | 66 | 67 | ```bash 68 | pls --imp=2 # or -I=2 69 | ``` 70 | 71 | 72 | 73 | ```bash 74 | pls --imp=2 file # `--imp` has no effect 75 | ``` 76 | 77 | 78 | 79 | ## Configuration 80 | 81 | Using the configuration system, you can extend the importance scale to higher or 82 | lower levels and change the default styles applicable at each level. 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/traits/imp.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{AppConst, Conf}; 2 | use crate::models::Node; 3 | use crate::PLS; 4 | use log::debug; 5 | 6 | pub trait Imp { 7 | fn default_imp(&self) -> i8; 8 | fn imp_val(&self) -> i8; 9 | 10 | fn is_visible(&self, conf: &Conf) -> bool; 11 | 12 | fn directives(&self, app_const: &AppConst) -> Option; 13 | } 14 | 15 | impl Imp for Node<'_> { 16 | /// Get the implicit relative importance of the node. 17 | /// 18 | /// This is the importance associated with a node if it has not been set by 19 | /// any matching spec. By default we assume nodes with a leading dot to be 20 | /// less important, as they are normally hidden by the `ls(1)` command. 21 | fn default_imp(&self) -> i8 { 22 | if self.name.starts_with('.') { 23 | -1 24 | } else { 25 | 0 26 | } 27 | } 28 | 29 | /// Get the relative importance of the node. 30 | /// 31 | /// This iterates through the specs in reverse, finding the first available 32 | /// importance or falling back the the [default](Imp::default_imp). Then it 33 | /// subtracts the baseline level from the CLI args. 34 | fn imp_val(&self) -> i8 { 35 | self.specs 36 | .iter() 37 | .rev() 38 | .find_map(|spec| spec.importance) 39 | .unwrap_or(self.default_imp()) 40 | - PLS.args.imp 41 | } 42 | 43 | /// Determine whether the node should be displayed in the list. 44 | /// 45 | /// Elements below the lowest-defined relative-importance are hidden. 46 | fn is_visible(&self, conf: &Conf) -> bool { 47 | debug!("Checking visibility of \"{self}\" based on importance."); 48 | let rel_imp = self.imp_val(); 49 | let min_val = conf.app_const.min_imp(); 50 | 51 | let is_visible = rel_imp >= min_val; 52 | if !is_visible { 53 | debug!("\"{self}\" with relative importance {rel_imp} (min: {min_val}) is hidden.") 54 | } 55 | is_visible 56 | } 57 | 58 | // ========== 59 | // Directives 60 | // ========== 61 | 62 | /// Get the directives associated with the node's relative importance. 63 | /// 64 | /// The directives are read from the configuration with any missing values 65 | /// having no directives set for them. 66 | /// 67 | /// If the node's importance is above the maximum defined, it will be set to 68 | /// the maximum. If it is below the minimum defined, it will already be 69 | /// hidden by [`is_visible`](Imp::is_visible). 70 | fn directives(&self, app_const: &AppConst) -> Option { 71 | let mut rel_imp = self.imp_val(); 72 | let max_val = app_const.max_imp(); 73 | let min_val = app_const.min_imp(); 74 | debug!("\"{self}\" has relative importance {rel_imp} (min: {min_val}, max: {max_val})"); 75 | 76 | rel_imp = rel_imp.clamp(min_val, max_val); 77 | app_const.imp_map.get(&rel_imp).cloned() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/enums/perm.rs: -------------------------------------------------------------------------------- 1 | use crate::config::EntryConst; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// This enum contains different groups of permissions defined on nodes, in a 5 | /// UNIX-like operating system, as they would appear in the octal notation. Each 6 | /// variant of this enum corresponds to one digit of the mode in octal notation. 7 | /// 8 | /// Note that while the values of `Special` cause changes to user, group and 9 | /// other permissions, they are all stored in fourth digit of the octal number. 10 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 11 | #[serde(rename_all = "snake_case")] 12 | pub enum Oct { 13 | Special, 14 | User, 15 | Group, 16 | Other, 17 | } 18 | 19 | /// This enum contains different types of permissions defined on nodes, in a 20 | /// UNIX-like operating system, as they would appear in the symbolic notation. 21 | /// 22 | /// Note that in a symbolic triplet, `Execute` and `Special` are both expressed 23 | /// combined in the third character. 24 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 25 | #[serde(rename_all = "snake_case")] 26 | pub enum Sym { 27 | None, // no permissions 28 | Read, 29 | Write, 30 | Execute, 31 | /// setuid, setgid or sticky bit 32 | Special, 33 | } 34 | 35 | impl Sym { 36 | /// Get the symbolic character associated with a permission. 37 | /// 38 | /// This does not support [`Sym::Special`] because that character can vary 39 | /// based on other factors, use [`Sym::special_ch`]. 40 | /// 41 | /// This function returns a marked-up string. 42 | pub fn ch(&self, entry_const: &EntryConst) -> String { 43 | let ch = match self { 44 | Sym::None => '-', 45 | Sym::Read => 'r', 46 | Sym::Write => 'w', 47 | Sym::Execute => 'x', 48 | // Special maps to 4 characters: 's', 't', 'S' or 'T'. 49 | Sym::Special => panic!("Use `Perm::special_ch` instead."), 50 | }; 51 | let directives = entry_const.perm_styles.get(self).unwrap(); 52 | format!("<{directives}>{ch}") 53 | } 54 | 55 | /// Get the symbolic character associated with a special permission. 56 | /// 57 | /// This function is the equivalent of [`Sym::ch`] that specifically 58 | /// handles [`Sym::Special`]. 59 | /// 60 | /// This function returns a marked-up string. 61 | pub fn special_ch(&self, oct: Oct, execute: bool, entry_const: &EntryConst) -> String { 62 | if self != &Sym::Special { 63 | panic!("Use `Perm::ch` instead.") 64 | } 65 | 66 | let ch = match (oct, execute) { 67 | (Oct::Other, false) => 'T', 68 | (Oct::Other, true) => 't', 69 | (_, false) => 'S', 70 | (_, true) => 's', 71 | }; 72 | let directives = entry_const.perm_styles.get(self).unwrap(); 73 | format!("<{directives}>{ch}") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/src/content/docs/about/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: 4 | pls is a prettier and powerful ls(1) for the pros. It is a modern alternative 5 | to ls(1), which has been around for over half a century. 6 | --- 7 | 8 | import { Card, CardGrid } from "@astrojs/starlight/components"; 9 | 10 | is a prettier and powerful `ls(1)` for the pros. It is a modern 11 | alternative to `ls(1)`, which has been around for over half a century. 12 | 13 | For developers who spend a lot of time in the terminal, is a 14 | game-changer. It brings a touch of joy to an essential and routine task you 15 | perform hundreds of times a day. By utilising the full capabilities of a 16 | terminal, can alleviate cognitive strain, making both tasks of exploring 17 | your directories, and searching for specific files, effortless. 18 | 19 | ## Features 20 | 21 | is a prettier and powerful `ls(1)` for the pros. The "p" stands for: 22 | 23 | 24 | 27 | output is cleaner, friendlier and more colorful. Who doesn't like a 28 | little color in their terminal? 29 | 30 | 33 | providers more features than the competition. It uses a cascading 34 | config system with specs. 35 | 36 | 39 | is speedy and performant (written in Rust). It continues to be fast 40 | even with all features enabled. 41 | 42 | 45 | has sensible defaults and an effortless interface. The CLI is 46 | fluent, intuitive and memorable. 47 | 48 | 51 | is a small, single-file, binary executable. It supports both Mac and 52 | Linux. 53 | 54 | 57 | can be extensively tweaked by power users and pros. Personalise it 58 | exactly how you like it. 59 | 60 | 63 | prioritises consumption by humans over scripts. The output is pretty 64 | and readable, by default. 65 | 66 | 69 | can render high-quality SVG images right in the terminal as file and 70 | directory icons. 71 | 72 | 73 | 74 | You can refer to [our comparison](/about/comparison/) of to other 75 | `ls(1)` alternatives, notably exa/eza. 76 | 77 | ## More info 78 | 79 | For more information, take a look at the [FAQs](/about/faq/). If your question 80 | isn't answered there, feel free to start a 81 | [GitHub discussion](https://github.com/pls-rs/pls/discussions). 82 | -------------------------------------------------------------------------------- /src/output/table.rs: -------------------------------------------------------------------------------- 1 | use crate::config::AppConst; 2 | use crate::enums::DetailField; 3 | use crate::fmt::len; 4 | use crate::PLS; 5 | use std::collections::HashMap; 6 | use std::iter::once; 7 | 8 | /// The detailed renders node names, and optionally, chosen node metadata in 9 | /// a tabular layout with one row per node. 10 | /// 11 | /// The detailed view is one of two views supported by `pls`, the other being 12 | /// the [grid view](crate::output::Grid). 13 | #[derive(Default)] 14 | pub struct Table { 15 | pub entries: Vec>, 16 | pub is_solo: bool, 17 | } 18 | 19 | impl Table { 20 | /// Create a new instance of `Table`, taking ownership of the given entries. 21 | pub fn new(entries: Vec>, is_solo: bool) -> Self { 22 | Self { entries, is_solo } 23 | } 24 | 25 | /// Render the table to STDOUT. 26 | pub fn render(&self, app_const: &AppConst) { 27 | let max_widths = self.max_widths(app_const); 28 | 29 | let iter_basis: Vec<_> = PLS 30 | .args 31 | .details 32 | .iter() 33 | .enumerate() 34 | .map(|(idx, det)| { 35 | let mut cell = det.cell(); 36 | if idx == PLS.args.details.len() - 1 { 37 | cell.padding = (0, 0); // Remove right padding from the last column. 38 | } 39 | (max_widths[idx], det, cell) 40 | }) 41 | .collect(); 42 | 43 | if PLS.args.header { 44 | for (width, det, cell) in &iter_basis { 45 | let name = det.name(app_const); 46 | let directives = app_const.table.header_style.clone(); 47 | print!("{}", &cell.print(name, width, Some(directives))); 48 | } 49 | println!(); 50 | } 51 | 52 | for entry in &self.entries { 53 | for (width, det, cell) in &iter_basis { 54 | print!("{}", &cell.print(entry.get(det).unwrap(), width, None)); 55 | } 56 | println!(); 57 | } 58 | } 59 | 60 | /// Get mapping of detail field to the maximum width of the cells in that 61 | /// column. 62 | fn max_widths(&self, app_const: &AppConst) -> Vec> { 63 | PLS.args 64 | .details 65 | .iter() 66 | .enumerate() 67 | .map(|(det_idx, det)| { 68 | if det_idx == PLS.args.details.len() - 1 { 69 | return None; 70 | } 71 | let end_lim = if self.entries.is_empty() { 72 | // If there are no entries, the limit must be zero. 73 | 0 74 | } else if !self.is_solo && det.uniformly_wide() { 75 | // For uniform columns, only compare the header and row #1. 76 | 1 77 | } else { 78 | // For non-uniform columns, compare the header and every row. 79 | // This is much slower as makes two passes over every cell. 80 | self.entries.len() 81 | }; 82 | self.entries[0..end_lim] 83 | .iter() 84 | .filter_map(|entry| entry.get(det).map(len)) 85 | .chain(once(if PLS.args.header { 86 | len(det.name(app_const)) 87 | } else { 88 | 0 89 | })) 90 | .max() 91 | }) 92 | .collect() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/gfx/svg.rs: -------------------------------------------------------------------------------- 1 | use crate::exc::Exc; 2 | use log::debug; 3 | use resvg::tiny_skia::{Pixmap, Transform}; 4 | use resvg::usvg::{Options, Tree}; 5 | use std::env; 6 | use std::fs::{read_to_string, File}; 7 | use std::io::{Read, Result as IoResult, Write}; 8 | use std::path::Path; 9 | 10 | /// Get the RGBA data for a given SVG file at a given size. 11 | /// 12 | /// This function can retrieve the RGBA data from the cache, if present, and 13 | /// also compute and cache it, if not present. Caching is only enabled if the 14 | /// `PLS_CACHE` environment variable is set. 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `id` - the unique ID of the image 19 | /// * `path` - the path to the SVG file 20 | /// * `size` - the size at which to render the icon 21 | pub fn get_rgba(id: u32, path: &Path, size: u8) -> Option> { 22 | let cache_file = env::var("PLS_CACHE") 23 | .ok() 24 | .map(|cache| Path::new(&cache).join("icons").join(id.to_string())); 25 | 26 | if let Some(cache_file) = &cache_file { 27 | if let Some(rgba_data) = load_from_cache(cache_file) { 28 | return Some(rgba_data); 29 | } 30 | } 31 | 32 | let rgba_data = match compute_rgba(path, size) { 33 | Ok(rgba_data) => Some(rgba_data), 34 | Err(exc) => { 35 | debug!("{}", exc); 36 | None 37 | } 38 | }; 39 | 40 | if let Some(cache_file) = &cache_file { 41 | if let Some(rgba_data) = &rgba_data { 42 | save_to_cache(cache_file, rgba_data).expect("E"); 43 | } 44 | } 45 | 46 | rgba_data 47 | } 48 | 49 | /// Compute the RGBA data for a given SVG file at a given size. 50 | fn compute_rgba(path: &Path, size: u8) -> Result, Exc> { 51 | // Read SVG file 52 | let svg_data = read_to_string(path).map_err(Exc::Io)?; 53 | 54 | // Create a default options struct with the target dimensions 55 | let opt = Options::default(); 56 | let rtree = Tree::from_str(&svg_data, &opt).map_err(Exc::Svg)?; 57 | 58 | // Create a pixmap with the desired dimensions 59 | let mut pixmap = 60 | Pixmap::new(size.into(), size.into()).ok_or(Exc::Other(String::from("Pixmap was None")))?; 61 | 62 | // Render the SVG tree into the pixmap 63 | resvg::render( 64 | &rtree, 65 | Transform::from_scale( 66 | size as f32 / rtree.size().width(), 67 | size as f32 / rtree.size().height(), 68 | ), 69 | &mut pixmap.as_mut(), 70 | ); 71 | 72 | // Get the RGBA data 73 | let rgba_data = pixmap.data().to_vec(); 74 | Ok(rgba_data) 75 | } 76 | 77 | /// Load the RGBA data from the cache, if present. 78 | fn load_from_cache(cache_file: &Path) -> Option> { 79 | if cache_file.exists() { 80 | let mut file = File::open(cache_file).expect("A"); 81 | let mut buffer = Vec::new(); 82 | file.read_to_end(&mut buffer).ok()?; 83 | Some(buffer) 84 | } else { 85 | None 86 | } 87 | } 88 | 89 | /// Save the RGBA data to the cache, creating the necessary directories. 90 | fn save_to_cache(cache_file: &Path, rgba_data: &[u8]) -> IoResult<()> { 91 | std::fs::create_dir_all(cache_file.parent().unwrap())?; 92 | let mut file = File::create(cache_file)?; 93 | file.write_all(rgba_data)?; 94 | Ok(()) 95 | } 96 | -------------------------------------------------------------------------------- /src/args/files_group.rs: -------------------------------------------------------------------------------- 1 | use crate::args::input::Input; 2 | use crate::config::{Conf, ConfMan}; 3 | use crate::enums::DetailField; 4 | use crate::models::{Node, OwnerMan}; 5 | use crate::utils::paths::common_ancestor; 6 | use log::debug; 7 | use std::collections::HashMap; 8 | use std::path::PathBuf; 9 | 10 | // ====== 11 | // Models 12 | // ====== 13 | 14 | /// Represents a group that renders the given collection of individual files. 15 | /// 16 | /// A group of files will use the UI configuration of their common ancestor 17 | /// while still using their individual configurations for their entry in the 18 | /// layout. 19 | pub struct FilesGroup { 20 | pub inputs: Vec, 21 | 22 | pub common_ancestor: Option, 23 | pub parent_conf: Conf, 24 | } 25 | 26 | // =============== 27 | // Implementations 28 | // =============== 29 | 30 | impl FilesGroup { 31 | // =========== 32 | // Constructor 33 | // =========== 34 | 35 | pub fn new(inputs: Vec, conf_man: &ConfMan) -> Self { 36 | let abs: Vec<_> = inputs.iter().map(|input| input.abs.as_path()).collect(); 37 | let common_ancestor = common_ancestor(&abs); 38 | let mut conf = conf_man.get(common_ancestor.as_ref()).unwrap_or_default(); 39 | conf.app_const.massage_imps(); 40 | 41 | Self { 42 | inputs, 43 | common_ancestor, 44 | parent_conf: conf, 45 | } 46 | } 47 | 48 | // ====== 49 | // Public 50 | // ====== 51 | 52 | /// Convert this list of files into entries for the output layout. 53 | /// 54 | /// Since individual nodes are not nested, the function uses each node's 55 | /// [`Node::row`] instead of the flattened output of each node's 56 | /// [`Node::entries`]. 57 | pub fn entries(&self, owner_man: &mut OwnerMan) -> Vec> { 58 | self.nodes() 59 | .iter() 60 | .map(|(node, conf)| { 61 | node.row( 62 | owner_man, 63 | conf, 64 | &self.parent_conf.app_const, 65 | &conf.entry_const, 66 | &[], 67 | ) 68 | }) 69 | .collect() 70 | } 71 | 72 | // ======= 73 | // Private 74 | // ======= 75 | 76 | /// Get a list of nodes from the individual files in this group. 77 | /// 78 | /// Unlike [`DirGroup`](crate::args::dir_group::DirGroup), this function 79 | /// does not filter out nodes based on their visibility. This is because the 80 | /// files in this group have been explicitly provided by the user and should 81 | /// be rendered regardless of their visibility. 82 | fn nodes(&self) -> Vec<(Node, &Conf)> { 83 | self.inputs 84 | .iter() 85 | .map(|input| { 86 | let display_name = input.path.to_string_lossy().to_string(); 87 | let mut node = Node::new(&input.path).solo_file(display_name); 88 | debug!("Currently {} specs", input.conf.specs.len()); 89 | node.match_specs(&input.conf.specs); 90 | (node, &input.conf) 91 | }) 92 | .collect() 93 | } 94 | } 95 | 96 | impl std::fmt::Debug for FilesGroup { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | f.debug_struct("FilesGroup") 99 | .field("inputs", &self.inputs) 100 | .field("common_ancestor", &self.common_ancestor) 101 | .finish() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This repository contains three projects in three languages! 2 | # 3 | # - the main `pls` codebase (Rust) in the root 4 | # - the example generation codebase (Python) in `examples/` 5 | # - the documentation (JavaScript) in `docs/` 6 | 7 | default_install_hook_types: 8 | - pre-commit 9 | - pre-push 10 | 11 | repos: 12 | # Generic 13 | # ======= 14 | 15 | - repo: https://github.com/pre-commit/pre-commit-hooks 16 | rev: v5.0.0 17 | hooks: 18 | - id: check-case-conflict 19 | - id: check-docstring-first 20 | - id: check-executables-have-shebangs 21 | - id: check-json 22 | - id: check-shebang-scripts-are-executable 23 | - id: check-symlinks 24 | - id: check-toml 25 | - id: check-yaml 26 | - id: end-of-file-fixer 27 | - id: fix-encoding-pragma 28 | args: 29 | - --remove 30 | - id: forbid-submodules 31 | - id: mixed-line-ending 32 | args: 33 | - --fix=auto 34 | - id: trailing-whitespace 35 | args: 36 | - --markdown-linebreak-ext=md 37 | 38 | # Rust 39 | # ==== 40 | 41 | - repo: local 42 | hooks: 43 | - id: fmt 44 | name: fmt 45 | language: system 46 | types: 47 | - file 48 | - rust 49 | entry: cargo fmt 50 | pass_filenames: false 51 | 52 | - id: clippy 53 | name: clippy 54 | language: system 55 | types: 56 | - file 57 | - rust 58 | # `-D warnings` ensures that the job fails when encountering warnings. 59 | entry: cargo clippy --all-targets --all-features -- -D warnings 60 | pass_filenames: false 61 | 62 | - repo: local 63 | hooks: 64 | - id: unit 65 | name: unit 66 | language: system 67 | types: 68 | - file 69 | - rust 70 | entry: cargo test 71 | pass_filenames: false 72 | stages: 73 | - pre-push 74 | 75 | # Python 76 | # ====== 77 | 78 | - repo: https://github.com/astral-sh/ruff-pre-commit 79 | rev: v0.9.1 80 | hooks: 81 | - id: ruff # replaces Flake8, isort, pydocstyle, pyupgrade 82 | args: 83 | - --fix 84 | - --exit-non-zero-on-fix 85 | - id: ruff-format # replaces Black 86 | 87 | # JavaScript 88 | # ========== 89 | 90 | - repo: local 91 | hooks: 92 | - id: prettier 93 | name: prettier 94 | "types": [text] 95 | language: system 96 | pass_filenames: true 97 | # Set log-level to error to prevent prettier printing every single file it checks 98 | # when running pre-commit with --all-files 99 | entry: pnpm exec prettier --write --ignore-unknown --log-level error 100 | 101 | - id: eslint 102 | name: eslint 103 | files: (frontend|automations|packages/js).*?\.(js|ts|mjs|vue|json5?)$ 104 | "types": [file] # ESLint only accepts [javascript] by default. 105 | language: system 106 | pass_filenames: false 107 | entry: pnpm exec eslint --max-warnings=0 --no-warn-ignored --fix 108 | -------------------------------------------------------------------------------- /src/args/group.rs: -------------------------------------------------------------------------------- 1 | use crate::args::dir_group::DirGroup; 2 | use crate::args::files_group::FilesGroup; 3 | use crate::args::input::Input; 4 | use crate::config::{Conf, ConfMan}; 5 | use crate::enums::{DetailField, Typ}; 6 | use crate::exc::Exc; 7 | use crate::fmt::render; 8 | use crate::models::OwnerMan; 9 | use crate::output::{Grid, Table}; 10 | use crate::PLS; 11 | use std::collections::HashMap; 12 | 13 | // ====== 14 | // Models 15 | // ====== 16 | 17 | /// Represents a set, possibly singleton, of paths entered in the CLI. 18 | /// 19 | /// Each group generates one UI block, table or grid, in the final output. This 20 | /// is done so that individual files provided as arguments can be displayed 21 | /// more compactly as a collection. 22 | #[derive(Debug)] 23 | pub enum Group { 24 | /// represents one directory path entered on the CLI 25 | Dir(DirGroup), 26 | /// represents all individual file paths entered on the CLI 27 | Files(FilesGroup), 28 | } 29 | 30 | // =============== 31 | // Implementations 32 | // =============== 33 | 34 | impl Group { 35 | /// Partition the given inputs into groups. 36 | /// 37 | /// Each directory becomes its own group, denoted by [`DirGroup`], while 38 | /// all files are collected into a single group denoted by [`FilesGroup`]. 39 | /// This separation is an implementation detail. 40 | pub fn partition(inputs: Vec, conf_man: &ConfMan) -> Vec { 41 | let mut groups = vec![]; 42 | let mut files = vec![]; 43 | for input in inputs { 44 | if input.typ == Typ::Dir { 45 | groups.push(Self::Dir(DirGroup::new(input))); 46 | } else { 47 | files.push(input); 48 | } 49 | } 50 | if !files.is_empty() { 51 | groups.insert(0, Self::Files(FilesGroup::new(files, conf_man))); 52 | } 53 | groups 54 | } 55 | 56 | pub fn render(&self, show_title: bool, owner_man: &mut OwnerMan) -> Result<(), Exc> { 57 | if show_title { 58 | if let Self::Dir(group) = self { 59 | println!( 60 | "\n{}", 61 | render(format!("{}:", group.input.path.display())) 62 | ); 63 | } 64 | } 65 | 66 | let entries = self.entries(owner_man)?; 67 | 68 | if PLS.args.grid { 69 | let grid = Grid::new(entries); 70 | grid.render(&self.conf().app_const); 71 | } else { 72 | let table = Table::new(entries, matches!(self, Self::Files(_))); 73 | table.render(&self.conf().app_const); 74 | } 75 | 76 | Ok(()) 77 | } 78 | 79 | /// Get the config for this group. 80 | /// 81 | /// For a directory, the config file inside the directory is used. For a 82 | /// group of files, the config file in the common ancestor directory is 83 | /// used. 84 | fn conf(&self) -> &Conf { 85 | match self { 86 | Self::Dir(group) => &group.input.conf, 87 | Self::Files(group) => &group.parent_conf, 88 | } 89 | } 90 | 91 | /// Convert this group into a vector of entries that can be passed into the 92 | /// layout to be rendered. 93 | pub fn entries( 94 | &self, 95 | owner_man: &mut OwnerMan, 96 | ) -> Result>, Exc> { 97 | match self { 98 | Self::Dir(group) => group.entries(owner_man), 99 | Self::Files(group) => Ok(group.entries(owner_man)), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/src/content/docs/about/faq.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ 3 | description: 4 | The arrival of a new alternative to ls(1) is bound to raise questions. Here 5 | are the answers to questions raised (or anticipated to be raised) about pls. 6 | --- 7 | 8 | Here are some questions you might have about , and the answers to those 9 | questions. 10 | 11 | --- 12 | 13 | **Why the name ?** 14 | 15 | The name is a play on the `ls(1)` command. I picked it because it was 16 | short, [memorable](/about/intro/#features) and only one keypress away from 17 | `ls(1)`. If you prefer a different name you can always alias it. 18 | 19 | ```bash 20 | alias rls="pls" 21 | ``` 22 | 23 | --- 24 | 25 | **Does support Windows?** 26 | 27 | No. , being a tool for pros, favours operating systems that are popular 28 | with those users, which Windows is not. This may change in the future if there 29 | is [considerable demand](https://github.com/pls-rs/pls/issues/80) and there are 30 | open-source contributions towards that goal. 31 | 32 | --- 33 | 34 | **Is a replacement for `ls(1)`?** 35 | 36 | No. is an alternative, not a replacement, for `ls(1)`. It some more 37 | features, prints prettier output and offers a lot of customisation, which make 38 | it ideal for human usage, but for scripts, `ls(1)` is still a better choice 39 | because it is tried, tested and trusted, not to mention ubiquitous. 40 | 41 | --- 42 | 43 | **Why build an `ls(1)` alternative?** 44 | 45 | IDEs and code editors use helpful UI patterns like icons and colors to 46 | disambiguate files and provide more information about them like their file type 47 | and VCS status. brings these features to the terminal. 48 | 49 | --- 50 | 51 | **Why build another `ls(1)` alternative?** 52 | 53 | None of the existing `ls(1)` alternatives have features that make pro workflows 54 | easier or more pleasant. is the first `ls(1)` alternative that focuses 55 | on the niche demographic of pros who will appreciate a powerful feature set and 56 | deep customisation. 57 | 58 | --- 59 | 60 | **Why Rust and not _<language>_?** 61 | 62 | Rust is a good choice for CLI utilities because it enables them to be very 63 | performant. The pros don't want to see lag in a core part of your workflow. 64 | 65 | Before I learned Rust, was written in Python (which was another reason 66 | it's called ). It seemed like a good fit at the time because it was 67 | decently fast and easy to develop and distribute, but at a certain point Python 68 | started becoming a speed bottleneck. The point being, give me a good reason, and 69 | I'll rewrite it. 70 | 71 | --- 72 | 73 | **Is better than _<alternative>_?** 74 | 75 | makes no claim of being better than any other tool, although we do try! 76 | Our claim is that is a better fit for developers and pros because it has 77 | some powerful, and thus complex, features that not everyone will use. If another 78 | tool has a feature you miss, feel free to open an issue or better yet, a pull 79 | request! See [how compares](/about/comparison/) to other `ls(1)` 80 | alternatives. 81 | 82 | --- 83 | 84 | **Is free?** 85 | 86 | is free in both senses of the word. It does not cost anything to 87 | download and install and the source code is freely available to read, modify and 88 | distribute. is licensed under [version 3, or later, of the GNU 89 | GPL](https://www.gnu.org/licenses/gpl-3.0.en.html). 90 | -------------------------------------------------------------------------------- /examples/src/examples/presentation.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from examples.bench import typ_bench 4 | from examples.utils.fs import fs, mksock 5 | from examples.utils.main import write_out, copy_write_conf 6 | 7 | 8 | def suffixes(): 9 | with typ_bench() as bench: 10 | write_out(bench=bench, dest_name="on") 11 | write_out("--suffix=false", bench=bench, dest_name="off") 12 | copy_write_conf(bench) 13 | write_out(bench=bench, dest_name="confd") 14 | 15 | 16 | def icons(): 17 | with fs( 18 | ( 19 | "icons", 20 | [ 21 | ("dir", []), 22 | ("sym", lambda p: p.symlink_to("./dir")), 23 | ".pls.yml", 24 | ".gitignore", 25 | "README.md", 26 | ("fifo", lambda p: os.mkfifo(p)), 27 | ("socket", lambda p: mksock(p)), 28 | ], 29 | ) 30 | ) as bench: 31 | write_out(bench=bench, dest_name="on") 32 | write_out("--icon=false", bench=bench, dest_name="off") 33 | copy_write_conf(bench) 34 | write_out(bench=bench, dest_name="confd") 35 | 36 | 37 | def symlinks(): 38 | with fs( 39 | ( 40 | "symlink", 41 | [ 42 | ("dir", []), 43 | ("a", lambda p: p.symlink_to("a")), 44 | ("b", lambda p: p.symlink_to("c")), 45 | ("c", lambda p: p.symlink_to("b")), 46 | ("d", lambda p: p.symlink_to("nonexistent")), 47 | ("e", lambda p: p.symlink_to("dir")), 48 | ("f", lambda p: p.symlink_to("dir")), 49 | ("g", lambda p: p.symlink_to("f")), 50 | ], 51 | ) 52 | ) as bench: 53 | # Make the symlink unreadable. 54 | try: 55 | os.chmod(bench / "e", 000, follow_symlinks=False) 56 | except NotImplementedError: 57 | pass 58 | 59 | write_out(bench=bench, dest_name="on") 60 | write_out("--sym=false", bench=bench, dest_name="off") 61 | copy_write_conf(bench) 62 | write_out(bench=bench, dest_name="confd") 63 | 64 | # Re-allow deletion during cleanup. 65 | try: 66 | os.chmod(bench / "e", 777, follow_symlinks=False) 67 | except NotImplementedError: 68 | pass 69 | 70 | 71 | def collapse(): 72 | with fs( 73 | ( 74 | "collapse", 75 | [ 76 | "Cargo.toml", 77 | "Cargo.lock", 78 | ], 79 | ) 80 | ) as bench: 81 | write_out(bench=bench, dest_name="on") 82 | write_out("--collapse=false", bench=bench, dest_name="off") 83 | 84 | with fs( 85 | ( 86 | "collapse", 87 | ["a", "b", "c", ".d", "e"], 88 | ) 89 | ) as bench: 90 | copy_write_conf(bench) 91 | write_out(bench=bench, dest_name="confd") 92 | 93 | 94 | def alignment(): 95 | with fs( 96 | ( 97 | "alignment", 98 | [ 99 | ".pls.yml", 100 | ".gitignore", 101 | "README.md", 102 | "LICENSE", 103 | ], 104 | ) 105 | ) as bench: 106 | write_out(bench=bench, dest_name="on") 107 | write_out("--align=false", bench=bench, dest_name="off") 108 | 109 | 110 | if __name__ == "__main__": 111 | suffixes() 112 | icons() 113 | symlinks() 114 | collapse() 115 | alignment() 116 | -------------------------------------------------------------------------------- /src/utils/paths.rs: -------------------------------------------------------------------------------- 1 | //! This module contains code for working with paths. 2 | //! 3 | //! It deals with paths as abstract strings, without interacting with the 4 | //! underlying file system to check if these paths have any real file at the 5 | //! location they reference. 6 | //! 7 | //! The public interface of the module consists of one function: 8 | //! 9 | //! * [`common_ancestor`] 10 | 11 | use path_clean::PathClean; 12 | use std::path::{Path, PathBuf}; 13 | 14 | /// Get the common ancestor of the given paths. 15 | /// 16 | /// This function normalises the given paths by resolving `..` to the parent 17 | /// directory and dropping any `.` in the path. 18 | /// 19 | /// Note that normalisation of '..' is incorrect for symlinks because the parent 20 | /// of a symlink is not the path component before it. 21 | /// 22 | /// # Arguments 23 | /// 24 | /// * `paths` - the paths for which to find the common ancestor 25 | pub fn common_ancestor(paths: &[&Path]) -> Option { 26 | if paths.is_empty() { 27 | return None; 28 | } 29 | 30 | let mut paths = paths.iter().map(|path| path.clean()); 31 | 32 | let mut common = paths.next().unwrap().to_path_buf(); 33 | for path in paths { 34 | common = common_ancestor_two(&common, &path)?; 35 | } 36 | Some(common) 37 | } 38 | 39 | // ======= 40 | // Private 41 | // ======= 42 | 43 | /// Get the common ancestor of two given paths. 44 | /// 45 | /// This function does not handle relative paths and does not resolve symbols 46 | /// like `.` and `..`. 47 | /// 48 | /// # Arguments 49 | /// 50 | /// * `one` - the first path 51 | /// * `two` - the second path 52 | fn common_ancestor_two(one: &Path, two: &Path) -> Option { 53 | let mut one = one.to_path_buf(); 54 | loop { 55 | if two.starts_with(&one) { 56 | return Some(one); 57 | } 58 | if !one.pop() || one.as_os_str().is_empty() { 59 | break; 60 | } 61 | } 62 | None 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::common_ancestor; 68 | use std::path::{Path, PathBuf}; 69 | 70 | macro_rules! make_common_ancestor_test { 71 | ( $($name:ident: $paths:expr => $parent:expr,)* ) => { 72 | $( 73 | #[test] 74 | fn $name() { 75 | let paths: Vec<&str> = $paths; 76 | let path_bufs: Vec<_> = paths.iter().map(Path::new).collect(); 77 | let parent = common_ancestor(&path_bufs); 78 | 79 | let expected = ($parent as Option<&str>).map(PathBuf::from); 80 | assert_eq!(parent, expected); 81 | } 82 | )* 83 | }; 84 | } 85 | 86 | make_common_ancestor_test!( 87 | test_zero_paths: vec![] => None, 88 | test_one_path: vec!["/a"] => Some("/a"), 89 | test_two_paths: vec!["/a/b", "/a/c"] => Some("/a"), 90 | test_three_paths: vec!["/a/b", "/a/c", "/a/d"] => Some("/a"), 91 | 92 | test_no_common_parent: vec!["a/b", "c/d"] => None, 93 | test_no_common_till_root: vec!["/a/b", "/c/d"] => Some("/"), 94 | 95 | test_variable_length: vec!["/a", "/a/b", "/a/b/c"] => Some("/a"), 96 | 97 | test_trailing_slash_unequal: vec!["/a/b", "/a/c/"] => Some("/a"), 98 | test_trailing_slash_equal: vec!["/a/b", "/a/b/"] => Some("/a/b"), 99 | 100 | test_relative: vec!["/a/b/c", "/a/b/../b/./c"] => Some("/a/b/c"), 101 | test_relative_end: vec!["/a/b/c", "/a/b/c/../."] => Some("/a/b"), 102 | test_relative_extra: vec!["/a/b", "/a/../../../a/b"] => Some("/a/b"), 103 | 104 | test_partial_match: vec!["/a/bat", "/a/ball"] => Some("/a"), 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /src/config/man.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Conf; 2 | use crate::exc::Exc; 3 | use figment::providers::{Data, Format, Serialized, Yaml}; 4 | use figment::Figment; 5 | use git2::Repository; 6 | use log::{debug, info}; 7 | use std::env; 8 | use std::path::Path; 9 | 10 | /// Manages the configuration system of the application. This manager provides 11 | /// `Conf` instances tailored to each path, while caching the base configuration 12 | /// for performance. 13 | pub struct ConfMan { 14 | /// the base configuration, i.e. the serialized output of [`Conf::default`] 15 | pub base: Figment, 16 | } 17 | 18 | impl Default for ConfMan { 19 | /// This includes config files from the one of the following locations: 20 | /// 21 | /// * the file referenced in the `PLS_CONFIG` environment variable 22 | /// * `.pls.yml` in the user's home directory 23 | fn default() -> Self { 24 | info!("Preparing base configuration."); 25 | 26 | let mut base = Figment::from(Serialized::defaults(Conf::default())); 27 | if let Ok(config_path) = env::var("PLS_CONFIG") { 28 | base = base.admerge(Yaml::file(config_path)); 29 | } else if let Some(home_yaml) = home::home_dir().and_then(Self::conf_at) { 30 | base = base.admerge(home_yaml); 31 | } 32 | 33 | info!("Base configuration prepared."); 34 | Self { base } 35 | } 36 | } 37 | 38 | impl ConfMan { 39 | /// Look for a config file in the given directory and return its contents. 40 | /// 41 | /// This function will return `None` if no config file is found inside the 42 | /// given directory. 43 | fn conf_at

(dir: P) -> Option> 44 | where 45 | P: AsRef, 46 | { 47 | let conf_file = dir.as_ref().join(".pls.yml"); 48 | conf_file.exists().then(|| { 49 | debug!("Found config file {conf_file:?}."); 50 | Yaml::file(conf_file) 51 | }) 52 | } 53 | 54 | /// Collects all the relevant `.pls.yml` config files into a vector. 55 | /// 56 | /// This includes config files from the following locations: 57 | /// 58 | /// * all parent directories up to the Git root, if Git-tracked 59 | /// * the given path, if a directory, or it's parent 60 | /// 61 | /// # Arguments 62 | /// 63 | /// * `path` - the path to scan for config files 64 | fn yaml_contents(path: &Path) -> Vec> { 65 | // the given path, if a directory, or it's parent; Note that symlinks 66 | // are treated as files in this situation. 67 | let mut curr = if !path.is_symlink() && path.is_dir() { 68 | path.to_path_buf() 69 | } else { 70 | match path.parent() { 71 | Some(par) => par.to_path_buf(), 72 | None => return vec![], 73 | } 74 | }; 75 | 76 | let mut paths = vec![curr.clone()]; 77 | 78 | let repo_root = Repository::discover(path) 79 | .ok() 80 | .and_then(|repo| repo.workdir().map(Path::to_path_buf)); 81 | if let Some(repo_root) = repo_root { 82 | while curr.pop() { 83 | paths.push(curr.clone()); 84 | if curr == repo_root { 85 | break; 86 | } 87 | } 88 | } 89 | debug!("Checking for configs in {paths:?}."); 90 | 91 | paths.iter().rev().filter_map(Self::conf_at).collect() 92 | } 93 | 94 | /// Get a `Conf` instance for the given path. 95 | /// 96 | /// This merges the path-specific config files with the base and returns the 97 | /// resulting [`Conf`] instance, If there is an error parsing the config 98 | /// files, an [`Exc`] instance will be returned. 99 | pub fn get

(&self, path: Option

) -> Result 100 | where 101 | P: AsRef, 102 | { 103 | let mut fig = self.base.clone(); 104 | 105 | if let Some(path) = path { 106 | for file in Self::yaml_contents(path.as_ref()) { 107 | fig = fig.admerge(file); 108 | } 109 | } 110 | 111 | fig.extract().map_err(Exc::Conf) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/specs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Specs 3 | descriptions: 4 | Specs are the backbone of pls, enabling it to match individual nodes to style 5 | rules. This makes pls more powerful than tools that match extensions or types. 6 | --- 7 | 8 | import { Content as SpecsDef } from "@/examples/specs/def.mdx"; 9 | import { Content as SpecsConf } from "@/examples/specs/conf.mdx"; 10 | import { Content as SpecsConfd } from "@/examples/specs/confd.mdx"; 11 | import { Content as SpecsConfdSrc } from "@/examples/specs/confd_src.mdx"; 12 | 13 | At its core, the output of is a list of nodes. Each node in the list has 14 | a specific color, style and importance, all of which collectively determine how 15 | it is rendered and displayed. 16 | 17 | uses specs to match each node to its visual properties, using a powerful 18 | regex matching system, which allows nodes to be matched by their name, extension 19 | or even fragments of both. 20 | 21 | ## Spec 22 | 23 | A spec consists of four components, of which only the first is mandatory. 24 | 25 | - **pattern:** 26 | [`regex::bytes::Regex`](https://docs.rs/regex/latest/regex/bytes/struct.Regex.html) 27 | 28 | This is a regex pattern that will be compared against the node name. If the 29 | node matches this regex, this spec will be associated with the node. 30 | 31 | - **icons:** `String[]` 32 | 33 | This is a list of names, and not the actual glyphs, of the 34 | [icon](/features/icons) to use for nodes matching this spec. When specs 35 | cascade, the last defined icon definition is used. If no specs match a node, 36 | the default icon for the node type is used. 37 | 38 | One spec may define multiple icons. This is useful to specify an SVG icon with 39 | a Nerd Font fallback. When a terminal doesn't support SVGs, the SVG icons are 40 | skipped over and the Nerd Font fallback is rendered instead. 41 | 42 | - **style:** `String` 43 | 44 | This is a [markup](/guides/markup) directive that must be applied to the nodes 45 | that match this spec. When specs cascade, their directives are combined with 46 | latter rules overriding previous ones if they conflict. 47 | 48 | - **importance:** `i8` 49 | 50 | This defines the [importance level](/features/importance/) of the file and 51 | overrides the default value (determined by the leading dot). 52 | 53 | - **collapse:** [`Collapse`](#collapse) 54 | 55 | ### Collapse 56 | 57 | The goal of collapsing is to indicate that a node is a derivative of another 58 | adjacent node. Collapsing can be performed in two ways. 59 | 60 | - **name** 61 | 62 | In this case, a node with a specific name is collapsed into another with a 63 | specific name. For example, `Cargo.lock` collapses into `Cargo.toml`. 64 | 65 | - **ext** 66 | 67 | In this case, every node with a specific extension is collapsed into another 68 | node with the same stem but different extension. For example, `index.js` and 69 | `lib.js` collapse into `index.ts` and `lib.ts` respectively. 70 | 71 | ## Configuration 72 | 73 | Specs are configured in the [`.pls.yml` file](/reference/conf/). 74 | 75 | ### Examples 76 | 77 | Consider a typical JavaScript project. When listing it with the output 78 | is very plain. That's because ships with a very lean config out of the 79 | box. 80 | 81 | ```bash 82 | pls 83 | ``` 84 | 85 | 86 | 87 | But we can make some things better here. 88 | 89 | - Use better icons for `package.json` and `pnpm-lock.yaml`. 90 | - Make `pnpm-lock.yaml` collapse into `package.json`. 91 | - Add icons for files related to Prettier. 92 | - Add icons and color for TypeScript and JavaScript files. 93 | - Make JavaScript files collapse into their corresponding TypeScript files. 94 | - Emphasise `README.md` and `justfile` as they are starting files. 95 | 96 | Let's add a spec file to it. 97 | 98 | 99 | 100 | 101 | 102 | ```bash 103 | pls src/ 104 | ``` 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/enums/unit_sys.rs: -------------------------------------------------------------------------------- 1 | use crate::config::EntryConst; 2 | use clap::ValueEnum; 3 | use number_prefix::NumberPrefix; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// This enum contains different unit systems to express large numbers, 7 | /// specifically node sizes. 8 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum UnitSys { 11 | Binary, // higher units based on powers of 2^10 12 | Decimal, // higher units based on powers of 10^3 13 | None, // no higher units 14 | } 15 | 16 | impl UnitSys { 17 | /// Split a natural number into a fractional magnitude and a unit prefix. 18 | /// This method should not be invoked on enum variant `UnitSys::None`. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `size` - the natural number to split into magnitude and unit 23 | /// 24 | /// # Returns 25 | /// 26 | /// * the length of the prefix 27 | /// * the fractional magnitude 28 | /// * the prefix of the unit 29 | fn convert(&self, size: u64) -> (usize, f64, &'static str) { 30 | let size = size as f64; 31 | let (len, prefixed) = match self { 32 | UnitSys::Binary => (2, NumberPrefix::binary(size)), 33 | UnitSys::Decimal => (1, NumberPrefix::decimal(size)), 34 | _ => panic!("UnitSys::None cannot be converted."), 35 | }; 36 | let (mag, prefix) = match prefixed { 37 | NumberPrefix::Standalone(mag) => (mag, ""), 38 | NumberPrefix::Prefixed(prefix, mag) => (mag, prefix.symbol()), 39 | }; 40 | (len, mag, prefix) 41 | } 42 | 43 | /// Convert the given number of bytes to a size string that uses the 44 | /// preferred unit system. 45 | /// 46 | /// This function returns a marked-up string. 47 | pub fn size(&self, size: u64, entry_const: &EntryConst) -> String { 48 | let mag_directive = &entry_const.size_styles.mag; 49 | let base_directive = &entry_const.size_styles.base; 50 | 51 | if self == &UnitSys::None { 52 | return format!("<{mag_directive}>{size} <{base_directive}>B"); 53 | } 54 | 55 | let prefix_directive = &entry_const.size_styles.prefix; 56 | 57 | let (width, mag, prefix) = self.convert(size); 58 | format!( 59 | "<{mag_directive}>{mag:.1} \ 60 | <{prefix_directive}>{prefix:>width$}\ 61 | <{base_directive}>B", 62 | width = width 63 | ) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use super::UnitSys; 70 | use crate::config::EntryConst; 71 | 72 | macro_rules! make_test { 73 | ( $($name:ident: $unit:expr, $num:expr => $str:expr,)* ) => { 74 | $( 75 | #[test] 76 | fn $name() { 77 | let entry_const = EntryConst::default(); 78 | let text = $unit.size($num, &entry_const); 79 | assert_eq!(text, $str); 80 | } 81 | )* 82 | }; 83 | } 84 | 85 | make_test!( 86 | none_shows_no_unit_for_base: UnitSys::None, 617 => "617 B", 87 | none_shows_no_unit_for_pow1: UnitSys::None, 1234_u64.pow(1) => "1234 B", 88 | none_shows_no_unit_for_pow2: UnitSys::None, 1234_u64.pow(2) => "1522756 B", 89 | none_shows_no_unit_for_pow3: UnitSys::None, 1234_u64.pow(3) => "1879080904 B", 90 | 91 | binary_shows_no_unit_for_base: UnitSys::Binary, 512 => "512.0 <> B", 92 | binary_shows_ki_unit_for_pow1: UnitSys::Binary, 1024_u64.pow(1) => "1.0 <>KiB", 93 | binary_shows_mi_unit_for_pow2: UnitSys::Binary, 1024_u64.pow(2) => "1.0 <>MiB", 94 | binary_shows_gi_unit_for_pow3: UnitSys::Binary, 1024_u64.pow(3) => "1.0 <>GiB", 95 | 96 | decimal_shows_no_unit_for_base: UnitSys::Decimal, 500 => "500.0 <> B", 97 | decimal_shows_k_unit_for_pow1: UnitSys::Decimal, 1000_u64.pow(1) => "1.0 <>kB", 98 | decimal_shows_m_unit_for_pow2: UnitSys::Decimal, 1000_u64.pow(2) => "1.0 <>MB", 99 | decimal_shows_g_unit_for_pow3: UnitSys::Decimal, 1000_u64.pow(3) => "1.0 <>GB", 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /examples/src/examples/utils/fs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import socket 4 | from collections.abc import Generator 5 | from contextlib import contextmanager 6 | from pathlib import Path 7 | from typing import Callable 8 | 9 | 10 | Creator = Callable[[Path], None] 11 | FsStructure = str | tuple[str, Creator] | tuple[str, list["FsStructure"]] 12 | 13 | 14 | WORKBENCHES = Path("/") / "tmp" / "workbenches" 15 | 16 | 17 | def mkbigfile(path: Path, content: str = "Hello, World!", size: int | None = None): 18 | """ 19 | Create a big file with the given content and size. 20 | 21 | The size is used as an offset to write the data, making a sparse file, that 22 | has the final size equal to the sum of ``size`` and the length of 23 | ``content``. 24 | 25 | This function is designed to mimic the signature of OS functions like 26 | ``os.mkdir`` and ``os.mkfifo``. 27 | 28 | :param path: the path where to create the file 29 | :param content: the content to write in the file 30 | :param size: the base size of the file before including the content length 31 | """ 32 | 33 | with path.open("w") as file: 34 | if size is not None: 35 | file.seek(size) 36 | file.write(content) 37 | 38 | 39 | def mksock(path: Path): 40 | """ 41 | Make a socket file at the given path. 42 | 43 | This function is designed to mimic the signature of OS functions like 44 | ``os.mkdir`` and ``os.mkfifo``. 45 | 46 | :param path: the path where to create the socket file 47 | """ 48 | 49 | sock = socket.socket(socket.AF_UNIX) 50 | sock.bind(str(path.absolute())) 51 | 52 | 53 | @contextmanager 54 | def fs( 55 | structure: FsStructure, 56 | workdir: Path = WORKBENCHES, 57 | ) -> Generator[Path, None, None]: 58 | """ 59 | Given a structure of files and folders, convert them to an actual path on 60 | the file system and yield a reference to the root path of the structure. 61 | Once the context is closed, destroy the nodes from the file system. 62 | 63 | This function must be used as a context manager using ``with``...``as``. 64 | 65 | :param structure: the node hierarchy depicted using lists and tuples 66 | :param workdir: the directory in which all operations are taking place 67 | :yield: the path of the top created node 68 | """ 69 | 70 | path = _create_fs(structure, workdir) 71 | try: 72 | yield path 73 | finally: 74 | _destroy_fs(path) 75 | 76 | 77 | def _create_fs(structure: FsStructure, workdir: Path = WORKBENCHES) -> Path: 78 | """ 79 | Given a structure of files and folders, convert them to an actual path on 80 | the file system. This function recursively invokes itself to create nested 81 | paths. 82 | 83 | :param structure: the structure of the nodes, using lists and tuples 84 | :param workdir: the directory in which all operations are taking place 85 | :return: the path of the top created node 86 | """ 87 | 88 | if not workdir.exists(): 89 | workdir.mkdir(mode=0o755) 90 | 91 | if isinstance(structure, str): 92 | node = workdir.joinpath(structure) 93 | node.touch(mode=0o644) # file 94 | else: # isinstance(structure, tuple): 95 | (name, other) = structure 96 | if callable(other): # `other` is a custom creator function. 97 | node = workdir.joinpath(name) 98 | other(node) # custom creator 99 | else: # isinstance(other, list): # `other` is a list of children. 100 | node = workdir.joinpath(name) 101 | node.mkdir(mode=0o755) # directory 102 | for child in other: 103 | _create_fs(child, node) 104 | return node 105 | 106 | 107 | def _destroy_fs(path: Path): 108 | """ 109 | Destroy the given path, and all its contents if it is a directory. If the 110 | path does not exist in the first place, it does nothing. 111 | 112 | :param path: the path to get rid of 113 | """ 114 | 115 | if not path.exists(): 116 | return 117 | 118 | if path.is_dir(): 119 | shutil.rmtree(path) 120 | else: 121 | os.remove(path) 122 | -------------------------------------------------------------------------------- /src/enums/icon.rs: -------------------------------------------------------------------------------- 1 | use crate::gfx::{compute_hash, get_rgba, render_image, send_image}; 2 | use crate::PLS; 3 | use std::collections::HashMap; 4 | use std::path::PathBuf; 5 | use std::sync::{LazyLock, Mutex}; 6 | 7 | struct ImageData { 8 | /// the ID assigned by the terminal to our image 9 | /// 10 | /// This is different from the hash of the image data. Allowing the 11 | /// terminal to choose an ID prevents new invocations of `pls` from 12 | /// overwriting IDs of images that were displayed by previous 13 | /// invocations. 14 | id: u32, 15 | /// the number of times the image has been displayed 16 | /// 17 | /// This generates new placement IDs for the images. This is 18 | /// required specifically because WezTerm has a bug where not 19 | /// setting unique placement IDs overwrites placements instead of 20 | /// creating new ones. 21 | count: u8, 22 | } 23 | 24 | static IMAGE_DATA: LazyLock>> = 25 | LazyLock::new(|| Mutex::new(HashMap::new())); 26 | 27 | /// This enum contains the two formats of icons supported by `pls`. 28 | pub enum Icon { 29 | /// a Nerd Font or emoji icon 30 | Text(String), 31 | /// the path to an SVG icon 32 | Image(String), 33 | } 34 | 35 | impl From<&str> for Icon { 36 | fn from(s: &str) -> Self { 37 | if s.ends_with(".svg") { 38 | Icon::Image(s.to_string()) 39 | } else { 40 | Icon::Text(s.to_string()) 41 | } 42 | } 43 | } 44 | 45 | impl Icon { 46 | /// Get the size of the icon in pixels. 47 | /// 48 | /// The icon size is determined by the width of a cell in the terminal 49 | /// multiplied by a scaling factor. 50 | pub fn size() -> u8 { 51 | let scale = std::env::var("PLS_ICON_SCALE") 52 | .ok() 53 | .and_then(|string| string.parse().ok()) 54 | .unwrap_or(1.0f32) 55 | .min(2.0); // We only allocate two cells for an icon. 56 | 57 | (scale * PLS.window.as_ref().unwrap().cell_width() as f32) // Convert to px.s 58 | .round() as u8 59 | } 60 | 61 | /// Get the output of the icon using the appropriate method: 62 | /// 63 | /// * For text icons, it generates the markup string with the 64 | /// directives. 65 | /// * For image icons, it generates the Kitty terminal graphics APC 66 | /// sequence. If that fails, it falls back to a blank text icon. 67 | /// 68 | /// The formatting directives for textual icons are a subset of the 69 | /// formatting directives for text. 70 | /// 71 | /// # Arguments 72 | /// 73 | /// * `directives` - the formatting directives to apply to text 74 | pub fn render(&self, text_directives: &str) -> String { 75 | match self { 76 | Icon::Text(text) => { 77 | // Nerd Font icons look weird with underlines and 78 | // synthesised italics. 79 | let directives = text_directives 80 | .replace("underline", "") 81 | .replace("italic", ""); 82 | // We leave a space after the icon to allow Nerd Font 83 | // icons that are slightly bigger than one cell to be 84 | // displayed correctly. 85 | format!("<{directives}>{text:<1} ") 86 | } 87 | 88 | Icon::Image(path) => { 89 | let default = String::from(" "); 90 | 91 | // SVG icons support expanding environment variables in 92 | // the path for theming purposes. 93 | let path = match shellexpand::env(path) { 94 | Ok(path) => path, 95 | Err(_) => return default, 96 | }; 97 | 98 | let size = Icon::size(); 99 | let hash = compute_hash(&PathBuf::from(path.as_ref()), size); 100 | 101 | let mut image_data_store = IMAGE_DATA.lock().unwrap(); 102 | let data = image_data_store 103 | .entry(hash) 104 | .or_insert_with(|| ImageData { count: 0, id: 0 }); 105 | 106 | data.count += 1; 107 | if data.count == 1 { 108 | // If the image is appearing for the first time in 109 | // this session, we send it to the terminal and get 110 | // an ID assigned to it. 111 | match get_rgba(hash, &PathBuf::from(path.as_ref()), size) { 112 | Some(rgba_data) => { 113 | data.id = send_image(hash, size, &rgba_data).unwrap(); 114 | } 115 | None => return default, 116 | } 117 | } 118 | render_image(data.id, size, data.count) 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/output/grid.rs: -------------------------------------------------------------------------------- 1 | use crate::config::AppConst; 2 | use crate::enums::DetailField; 3 | use crate::fmt::len; 4 | use crate::gfx::strip_image; 5 | use crate::output::Cell; 6 | use crate::PLS; 7 | use std::collections::HashMap; 8 | use std::fmt::Alignment; 9 | 10 | /// The grid view renders the node names in a two dimensional layout to minimise 11 | /// scrolling. It does not support rendering of node metadata. 12 | /// 13 | /// The grid view is one of two views supported by `pls`, the other being the 14 | /// [detailed view](crate::output::Table). 15 | /// 16 | /// The grid view tries to render all elements in as few lines as possible. Once 17 | /// the number of lines has been minimised, it minimises the column count by 18 | /// making each column take the maximum number of rows. 19 | pub struct Grid { 20 | pub entries: Vec, 21 | } 22 | 23 | impl Grid { 24 | /// Create a new instance of `Grid`, taking ownership of the given entries. 25 | pub fn new(entries: Vec>) -> Self { 26 | Self { 27 | entries: entries 28 | .into_iter() 29 | .map(|mut entry| entry.remove(&DetailField::Name).unwrap_or_default()) 30 | .collect(), 31 | } 32 | } 33 | 34 | /// Render the grid to STDOUT. 35 | pub fn render(&self, _app_const: &AppConst) { 36 | let mut max_width = self.entries.iter().map(strip_image).map(len).max(); 37 | let max_cols = self.columns(max_width); 38 | 39 | let entry_len = self.entries.len(); 40 | if entry_len == 0 { 41 | // Nothing to render, so we exit. 42 | return; 43 | } 44 | 45 | let rows = (entry_len as f64 / max_cols as f64).ceil() as usize; 46 | let cols = (entry_len as f64 / rows as f64).ceil() as usize; 47 | 48 | if cols == 1 { 49 | // If there is only one column, we don't need to equalise width. 50 | max_width = None; 51 | } 52 | 53 | if cols > 1 && PLS.args.down { 54 | self.print(&self.down(rows), cols, max_width); 55 | } else { 56 | self.print(&self.entries, cols, max_width); 57 | }; 58 | } 59 | 60 | /// Print the entries to the screen. 61 | /// 62 | /// This prints the entries in the specified number of columns, each cell 63 | /// padded to span the given max-width. 64 | fn print(&self, entries: &[S], cols: usize, max_width: Option) 65 | where 66 | S: AsRef, 67 | { 68 | let entry_len = self.entries.len(); 69 | 70 | let cell = Cell::new(Alignment::Left, (0, 2)); 71 | let end_cell = Cell::new(Alignment::Left, (0, 0)); 72 | for (idx, text) in entries.iter().enumerate() { 73 | if idx % cols == cols - 1 || idx == entry_len - 1 { 74 | println!("{}", &end_cell.print(text, &max_width, None)); 75 | } else { 76 | print!("{}", &cell.print(text, &max_width, None)); 77 | } 78 | } 79 | } 80 | 81 | /// Shuffle the entries to enable printing down instead of across. 82 | /// 83 | /// Since terminals can only print row-by-row, we split the entries into 84 | /// columns and then pick one cell per column, going in cycles till all 85 | /// cells are exhausted. 86 | fn down(&self, rows: usize) -> Vec<&String> { 87 | let chunks: Vec<_> = self.entries.chunks(rows).collect(); 88 | (0..rows) 89 | .flat_map(|row_idx| chunks.iter().filter_map(move |chunk| chunk.get(row_idx))) 90 | .collect() 91 | } 92 | 93 | /// Get the number of columns that can be accommodated on the screen. 94 | /// 95 | /// If the terminal width cannot be determined, such as when piping to a 96 | /// file, the output will be laid out in a single column. 97 | fn columns(&self, max_width: Option) -> u16 { 98 | match (Self::term_width(), max_width) { 99 | (Some(term_width), Some(item_width)) => { 100 | let cols = (term_width + 2) / (item_width as u16 + 2); 101 | cols.max(1) 102 | } 103 | _ => 1, 104 | } 105 | } 106 | 107 | /// Get the terminal width. 108 | /// 109 | /// The terminal width is determined from two sources: 110 | /// 111 | /// * the `PLS_COLUMNS` environment variable, if it is set 112 | /// * the result of an ioctl call, if it succeeds 113 | fn term_width() -> Option { 114 | std::env::var("PLS_COLUMNS") // development hack 115 | .ok() 116 | .and_then(|width_str| width_str.parse::().ok()) 117 | .or_else(|| PLS.window.as_ref().map(|win| win.ws_col)) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/output/cell.rs: -------------------------------------------------------------------------------- 1 | use crate::fmt::{len, render}; 2 | use crate::gfx::strip_image; 3 | use std::fmt::Alignment; 4 | 5 | /// Represents one cell in the rendered output. 6 | /// 7 | /// It provides a convenient way to render text with alignment and padding. 8 | /// 9 | /// `Cell` instances are reusable, meaning that once you have a cell for a 10 | /// particular column, you can use it to render any number all rows for that 11 | /// column. This is facilitated by all variable fields being passed as arguments 12 | /// to the [`print`](Cell::print) method and not saved in the struct itself. 13 | pub struct Cell { 14 | pub alignment: Alignment, 15 | pub padding: (usize, usize), 16 | } 17 | 18 | impl Default for Cell { 19 | fn default() -> Self { 20 | Self { 21 | alignment: Alignment::Left, 22 | padding: (0, 1), 23 | } 24 | } 25 | } 26 | 27 | impl Cell { 28 | /// Create a `Cell` instance with the given alignment and padding. 29 | pub fn new(alignment: Alignment, padding: (usize, usize)) -> Self { 30 | Self { alignment, padding } 31 | } 32 | 33 | /// Return the content of the cell, padded to the given width and aligned 34 | /// as per the cell's alignment directive. 35 | /// 36 | /// This function calls render to ensure that markup in the cell contents 37 | /// is rendered into ANSI escape sequences. 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `text` - the text to print in the cell 42 | /// * `width` - the width that the cell should span 43 | /// * `directives` - styles to apply to the entire cell, including padding 44 | pub fn print(&self, text: S, width: &Option, directives: Option) -> String 45 | where 46 | S: AsRef, 47 | { 48 | let text = text.as_ref(); 49 | let text_len = len(strip_image(text)); // This `len` can understand markup. 50 | 51 | let (left, right): (usize, usize) = match width { 52 | Some(width) if *width > text_len => { 53 | let pad = width - text_len; 54 | match self.alignment { 55 | Alignment::Left => (0, pad), 56 | Alignment::Center => (pad / 2, pad - (pad / 2)), 57 | Alignment::Right => (pad, 0), 58 | } 59 | } 60 | _ => (0, 0), 61 | }; 62 | let (left, right) = ( 63 | " ".repeat(left + self.padding.0), 64 | " ".repeat(right + self.padding.1), 65 | ); 66 | 67 | let mut content = format!("{left}{text}{right}"); 68 | 69 | if let Some(directives) = directives { 70 | content.insert_str(0, "<>"); 71 | content.insert_str(1, &directives); 72 | content.push_str(""); 73 | } 74 | 75 | render(content) 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::Cell; 82 | use crate::fmt::render; 83 | use std::fmt::Alignment; 84 | 85 | macro_rules! make_padding_test { 86 | ( $($name:ident: $text:expr, $left:expr, $right:expr => $expected:expr,)* ) => { 87 | $( 88 | #[test] 89 | fn $name() { 90 | let cell = Cell { padding: ($left, $right), ..Cell::default() }; 91 | assert_eq!(cell.print($text, &None, None), $expected); 92 | } 93 | )* 94 | }; 95 | } 96 | 97 | make_padding_test!( 98 | test_left_only_padding: "A", 1, 0 => " A", 99 | test_right_only_padding: "A", 0, 1 => "A ", 100 | test_left_and_right_padding: "A", 1, 1 => " A ", 101 | ); 102 | 103 | macro_rules! make_print_test { 104 | ( $($name:ident: $text:expr, $alignment:expr, $width:expr => $expected:expr,)* ) => { 105 | $( 106 | #[test] 107 | fn $name() { 108 | colored::control::set_override(true); // needed when running tests in CLion 109 | let cell = Cell{ alignment: $alignment, ..Cell::default() }; 110 | assert_eq!(cell.print($text, &$width, None), $expected); 111 | } 112 | )* 113 | }; 114 | } 115 | 116 | make_print_test!( 117 | test_simple_left: "A", Alignment::Left, Some(5) => "A ", 118 | test_simple_right: "A", Alignment::Right, Some(5) => " A ", 119 | test_simple_center: "A", Alignment::Center, Some(5) => " A ", 120 | test_unbalanced_center: "A", Alignment::Center, Some(6) => " A ", 121 | 122 | test_renders_markup: "A", Alignment::Center, None => render("A "), 123 | 124 | test_excludes_markup_from_len: "A", Alignment::Center, Some(5) => render(" A "), 125 | 126 | test_handles_missing_width: "A", Alignment::Center, None => "A ", 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/sorting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sorting 3 | description: 4 | pls offers an unmatched 18 bases for sorting nodes and 2 directions for each 5 | basis, of which you can choose multiple to apply in sequence. 6 | --- 7 | 8 | import { Content as SortingDef } from "@/examples/sorting/def.mdx"; 9 | import { Content as SortingCust } from "@/examples/sorting/cust.mdx"; 10 | 11 | offers the ability to sort the output in your preferred order by 12 | choosing as many as you prefer from 18 bases × 2 directions per base. 13 | 14 | ## Arguments 15 | 16 | `--sort`/`-s` can be used to select the sort bases. The flag can be specified 17 | multiple times to sort by multiple bases. Each time it can take one of these 18 | values. All values except `none` can optionally suffixed with an underscore `_` 19 | to reverse their direction. 20 | 21 | | Name | Description | 22 | | ------ | ----------------------------------------------------------------- | 23 | | dev | device ID | 24 | | ino | inode number | 25 | | nlink | number of hard links | 26 | | typ | node type | 27 | | cat | node category (directory or file) | 28 | | user | user name | 29 | | uid | user ID | 30 | | group | group name | 31 | | gid | group ID | 32 | | size | storage space | 33 | | blocks | number of blocks | 34 | | btime | created at; "b" for birth | 35 | | ctime | changed at; originally meant "created at" | 36 | | mtime | modified at | 37 | | atime | accessed at | 38 | | name | node name | 39 | | cname | canonical name (name in lower case with leading symbols stripped) | 40 | | ext | file extension | 41 | | none | **shorthand:** no sorting | 42 | 43 | By default, sorts file by `cat` and `cname`, which means 44 | 45 | - directories are listed before files (`cat`) 46 | - nodes are sorted by their canonical names (`cname`) 47 | 48 | When sorting by multiple sort bases, the first listed basis is the primary sort 49 | basis, the second is the tie-breaker for the first, the third is the tie-breaker 50 | for the second and so on. 51 | 52 | :::note 53 | 54 | The canonical name of a file is obtained by stripping leading symbols from the 55 | name and then converting it to lowercase. 56 | 57 | So the canonical name of the `.DS_Store` file would be 'ds_store'. This name is 58 | helpful when sorting because it allows for dotfiles and case-mixed files to be 59 | sorted along with the other files, instead of being separated out. 60 | 61 | ::: 62 | 63 | ### Mechanism 64 | 65 | When parsing the `--sort`/`-s` flag, values are read from the CLI, in order, and 66 | added to a vector of sort bases till we encounter the shorthand value `none`, 67 | which clears the vector. 68 | 69 | For example, consider the invocation below. 70 | 71 | ```bash 72 | pls --sort=cat --sort=cname --sort=none --sort=mtime_ 73 | ``` 74 | 75 | - `cat` is added to the list. 76 | - `cname` is added to the list. 77 | - All entries so far are dropped when `none` is encountered. 78 | - `mtime_` is added to the list. 79 | 80 | ### Examples 81 | 82 | ```bash 83 | pls # or --sort=cat --sort=cname or -s=cat -s=cname 84 | ``` 85 | 86 | 87 | 88 | ```bash 89 | pls --det=ino --det=typ --det=size --sort=cat_ --sort=size_ --sort=ino 90 | ``` 91 | 92 | 93 | 94 | Here the `--sort`/`-s` arguments have this effect: 95 | 96 | - `cat_` sorts directories before files. 97 | - `size_` sorts nodes by size in descending order. 98 | - `ino` sorts nodes by inode number in ascending order. 99 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI + CD 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | # Make sure CI fails on all warnings, including Clippy lints 11 | env: 12 | RUSTFLAGS: "-Dwarnings" 13 | 14 | jobs: 15 | lint: # Also checks formatting. 16 | name: Run lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup env 23 | uses: ./.github/actions/setup-env 24 | with: 25 | rust_cache_key: cargo 26 | rust_components: clippy, rustfmt 27 | 28 | - uses: actions/cache@v4 29 | with: 30 | path: | 31 | ~/.cache/pre-commit 32 | key: 33 | ${{ runner.os }}-pre-commit-${{ 34 | hashFiles('**/.pre-commit-config.yaml') }} 35 | 36 | - name: Run lint 37 | run: | 38 | just pre-commit 39 | just lint 40 | 41 | unit: 42 | name: Run unit test 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | os: 47 | - ubuntu-latest 48 | - macos-latest 49 | runs-on: ${{ matrix.os }} 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v4 53 | 54 | - name: Setup env 55 | uses: ./.github/actions/setup-env 56 | with: 57 | rust_cache_key: cargo 58 | 59 | - name: Run unit test 60 | run: just test 61 | 62 | build: 63 | name: Build release 64 | needs: 65 | - lint 66 | - unit 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | build: 71 | - linux 72 | - macos 73 | include: 74 | - build: linux 75 | os: ubuntu-latest 76 | target: x86_64-unknown-linux-musl 77 | - build: macos 78 | os: macos-latest 79 | target: x86_64-apple-darwin 80 | runs-on: ${{ matrix.os }} 81 | steps: 82 | - name: Checkout repository 83 | uses: actions/checkout@v4 84 | 85 | - name: Setup env 86 | uses: ./.github/actions/setup-env 87 | with: 88 | rust_cache_key: cross 89 | rust_target: ${{ matrix.target }} 90 | 91 | - name: Build release 92 | run: | 93 | just get-cross 94 | just cross ${{ matrix.target }} 95 | 96 | - name: Upload binary artifact 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: pls-${{ matrix.target }} 100 | path: target/${{ matrix.target }}/release/pls 101 | 102 | docs: 103 | name: Build docs 104 | runs-on: ubuntu-latest 105 | needs: 106 | - build 107 | steps: 108 | - name: Checkout repository 109 | uses: actions/checkout@v4 110 | 111 | - name: Setup env 112 | uses: ./.github/actions/setup-env 113 | with: 114 | rust_cache_key: docs 115 | 116 | - name: Download artifact 117 | uses: actions/download-artifact@v4 118 | with: 119 | name: pls-x86_64-unknown-linux-musl 120 | path: /tmp/pls 121 | 122 | - name: Make binary accessible and executable 123 | run: | 124 | chmod +x /tmp/pls/pls 125 | echo "/tmp/pls" >> $GITHUB_PATH 126 | 127 | # This must be a separate step because `$PATH` changes are not reflected 128 | # immediately. 129 | - name: Ensure binary is accessible 130 | run: pls --version 131 | 132 | - name: Setup PDM 133 | uses: pdm-project/setup-pdm@v4 134 | with: 135 | cache: true 136 | python-version-file: examples/pyproject.toml 137 | cache-dependency-path: examples/pdm.lock 138 | 139 | - name: Generate examples 140 | working-directory: examples/ 141 | run: | 142 | just install 143 | just all 144 | 145 | - name: Build docs 146 | working-directory: docs/ 147 | run: | 148 | pnpm build 149 | 150 | - name: Publish docs 151 | if: github.event_name == 'push' 152 | working-directory: docs/dist/ 153 | run: | 154 | git init --initial-branch=gh-pages 155 | git config user.name "Dhruv Bhanushali" 156 | git config user.email "hi@dhruvkb.dev" 157 | git add . 158 | git commit --message "Build documentation" 159 | git remote add origin https://x-access-token:${{ secrets.ACCESS_TOKEN }}@github.com/pls-rs/pls-rs.github.io.git 160 | git push --force origin gh-pages 161 | -------------------------------------------------------------------------------- /src/config/conf.rs: -------------------------------------------------------------------------------- 1 | use crate::config::app_const::AppConst; 2 | use crate::config::entry_const::EntryConst; 3 | use crate::enums::Collapse; 4 | use crate::models::Spec; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::HashMap; 7 | 8 | /// Create a [`HashMap`] from a list of key-value pairs. 9 | macro_rules! map_str_str { 10 | ( $($k:expr => $v:expr,)* ) => { 11 | core::convert::From::from([ 12 | $( (String::from($k), String::from($v)), )* 13 | ]) 14 | }; 15 | } 16 | 17 | /// Represents the complete configuration of `pls`. 18 | /// 19 | /// `pls` comes with a lean configuration out-of-the-box and users are 20 | /// encouraged to add their own configuration using YAML files in the home 21 | /// directory, project Git root and/or working directory. 22 | /// 23 | /// Note that `pls` also accepts CLI arguments, which are not represented here. 24 | /// Refer to [`Args`](crate::config::Args) for those. 25 | #[derive(Serialize, Deserialize)] 26 | pub struct Conf { 27 | /// mapping of icon names to actual glyphs from Nerd Fonts or paths to SVGs 28 | pub icons: HashMap, 29 | /// list of node specs, in ascending order of specificity 30 | pub specs: Vec, 31 | /// constants that determine the appearance and styling of each entry 32 | pub entry_const: EntryConst, 33 | /// constants that determine the appearance and styling of the entire UI 34 | pub app_const: AppConst, 35 | } 36 | 37 | impl Default for Conf { 38 | fn default() -> Self { 39 | Self { 40 | icons: map_str_str!( 41 | // pls 42 | "pls" => "", // nf-oct-primitive_dot 43 | "missing" => "", // nf-cod-error 44 | // Node types 45 | "file" => "", 46 | "dir" => "", // nf-fa-folder 47 | "symlink" => "󰌹", // nf-md-link-variant 48 | "fifo" => "󰟥", // nf-md-pipe 49 | "socket" => "󰟨", // nf-md-power_socket_uk 50 | "char_device" => "", // nf-fa-paragraph 51 | "block_device" => "󰋊", // nf-md-harddisk 52 | // Generic 53 | "audio" => "󰓃", // nf-md-speaker 54 | "book" => "", // nf-fa-book 55 | "broom" => "󰃢", // nf-md-broom 56 | "config" => "", // nf-seti-config 57 | "container" => "", // nf-oct-container 58 | "env" => "", // nf-fae-plant 59 | "image" => "󰋩", // nf-md-image 60 | "json" => "", // nf-seti-json 61 | "law" => "", // nf-oct-law 62 | "lock" => "", // nf-oct-lock 63 | "package" => "", // nf-oct-package 64 | "runner" => "󰜎", // nf-md-run 65 | "shell" => "", // nf-oct-terminal 66 | "source" => "", // nf-oct-file_code 67 | "test" => "󰙨", // nf-md-test_tube 68 | "text" => "", // nf-seti-text 69 | "video" => "󰕧", // nf-md-video 70 | // Brands 71 | "apple" => "", // nf-fa-apple 72 | "git" => "󰊢", // nf-md-git 73 | "github" => "", // nf-oct-mark_github 74 | "markdown" => "", // nf-oct-markdown 75 | "rust" => "", // nf-seti-rust 76 | ), 77 | specs: vec![ 78 | // Extensions 79 | Spec::new(r"\.sh$", "shell"), 80 | Spec::new(r"\.rs$", "rust").style("rgb(247,76,0)"), 81 | Spec::new(r"\.(txt|rtf)$", "text"), 82 | Spec::new(r"\.mdx?$", "markdown"), 83 | Spec::new(r"\.ini$", "config"), 84 | Spec::new(r"\.(json|toml|yml|yaml)$", "json"), 85 | Spec::new(r"\.(jpg|jpeg|png|svg|webp|gif|ico)$", "image"), 86 | Spec::new(r"\.(mov|mp4|mkv|webm|avi|flv)$", "video"), 87 | Spec::new(r"\.(mp3|flac|ogg|wav)$", "audio"), 88 | // Partial names 89 | Spec::new(r"^\.env\b", "env"), 90 | Spec::new(r"^README\b", "book").importance(2), 91 | Spec::new(r"^LICENSE\b", "law"), 92 | Spec::new(r"docker-compose.*\.yml$", "container"), 93 | Spec::new(r"Dockerfile", "container"), 94 | // Exact names 95 | Spec::new(r"^\.DS_Store$", "apple").importance(-2), 96 | Spec::new(r"^\.pls\.yml$", "pls").importance(0), 97 | Spec::new(r"^\.git$", "git").importance(-2), 98 | Spec::new(r"^\.gitignore$", "git"), 99 | Spec::new(r"^\.github$", "github"), 100 | Spec::new(r"^src$", "source").importance(1), 101 | Spec::new(r"^(justfile|Makefile)$", "runner"), 102 | Spec::new(r"^Cargo\.toml$", "package"), 103 | Spec::new(r"^Cargo\.lock$", "lock") 104 | .importance(-1) 105 | .collapse(Collapse::Name(String::from("Cargo.toml"))), 106 | Spec::new(r"^rustfmt.toml$", "broom"), 107 | ], 108 | entry_const: EntryConst::default(), 109 | app_const: AppConst::default(), 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /docs/astro.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import starlight from "@astrojs/starlight"; 3 | import autoImport from "astro-auto-import"; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | devToolbar: { 8 | enabled: false, 9 | }, 10 | markdown: { 11 | smartypants: false, // https://daringfireball.net/projects/smartypants/ 12 | }, 13 | integrations: [ 14 | starlight({ 15 | title: "pls", 16 | favicon: "/favicon.ico", 17 | logo: { 18 | light: "./src/assets/logo_light.svg", 19 | dark: "./src/assets/logo_dark.svg", 20 | }, 21 | expressiveCode: { 22 | themes: ["catppuccin-mocha", "catppuccin-latte"], 23 | }, 24 | social: [ 25 | { 26 | icon: "github", 27 | label: "GitHub", 28 | href: "https://github.com/pls-rs/pls", 29 | }, 30 | ], 31 | sidebar: [ 32 | { 33 | label: "About", 34 | items: [ 35 | { label: "Introduction", link: "/about/intro" }, 36 | { label: "FAQ", link: "/about/faq/" }, 37 | { label: "Comparison", link: "/about/comparison/" }, 38 | ], 39 | }, 40 | { 41 | label: "Guides", 42 | items: [ 43 | { label: "Get started", link: "/guides/get_started/" }, 44 | { label: "Paths", link: "/guides/paths/" }, 45 | { label: "Markup", link: "/guides/markup/" }, 46 | { label: "Specs", link: "/guides/specs/" }, 47 | { label: "Contribute", link: "/guides/contribute/" }, 48 | ], 49 | }, 50 | { 51 | label: "Features", 52 | items: [ 53 | { 54 | label: "Detail view", 55 | items: [ 56 | { label: "View", link: "/features/detail_view/" }, 57 | { label: "Header", link: "/features/header/" }, 58 | { label: "Units", link: "/features/units/" }, 59 | ], 60 | }, 61 | { 62 | label: "Grid view", 63 | items: [ 64 | { label: "View", link: "/features/grid_view/" }, 65 | { label: "Direction", link: "/features/direction/" }, 66 | ], 67 | }, 68 | { 69 | label: "Presentation", 70 | items: [ 71 | { label: "Icons", link: "/features/icons/" }, 72 | { label: "Suffixes", link: "/features/suffixes/" }, 73 | { label: "Symlinks", link: "/features/symlinks/" }, 74 | { label: "Collapse", link: "/features/collapse/" }, 75 | { label: "Alignment", link: "/features/alignment/" }, 76 | ], 77 | }, 78 | { 79 | label: "Filtering", 80 | items: [ 81 | { label: "Name filter", link: "/features/name_filter/" }, 82 | { label: "Type filter", link: "/features/type_filter/" }, 83 | { label: "Importance", link: "/features/importance/" }, 84 | ], 85 | }, 86 | { label: "Sorting", link: "/features/sorting/" }, 87 | { label: "Colors", link: "/features/colors/" }, 88 | { label: "Upcoming", link: "/features/upcoming/" }, 89 | ], 90 | }, 91 | { 92 | label: "Cookbooks", 93 | autogenerate: { 94 | directory: "cookbooks", 95 | }, 96 | }, 97 | { 98 | label: "Reference", 99 | autogenerate: { 100 | directory: "reference", 101 | }, 102 | }, 103 | ], 104 | customCss: [ 105 | "./src/styles/brand.css", 106 | "./src/styles/color.css", 107 | "./src/styles/font.css", 108 | "./src/styles/layout.css", 109 | "./src/styles/terminal.css", 110 | "./src/styles/typography.css", 111 | ], 112 | components: { 113 | Footer: "./src/components/Footer.astro", 114 | SocialIcons: "./src/components/SocialIcons.astro", 115 | }, 116 | editLink: { 117 | baseUrl: "https://github.com/pls-rs/pls/edit/main/docs/", 118 | }, 119 | }), 120 | // This causes a warning but it's harmless. 121 | // Bug: https://github.com/delucis/astro-auto-import/issues/46 122 | autoImport({ 123 | imports: [ 124 | "@/components/Pls.astro", 125 | "@/components/Stars.astro", 126 | "@/components/Version.astro", 127 | "@/components/DocBlock.astro", 128 | ], 129 | }), 130 | ], 131 | }); 132 | -------------------------------------------------------------------------------- /docs/src/content/docs/cookbooks/starters.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Starter configs 3 | description: 4 | The USP of pls is that it uses plaintext YAML configurations that can be 5 | checked into VCS and shared with others. Here are some to get you started. 6 | --- 7 | 8 | uses simple YAML-based [config files](/reference/conf/). This makes it 9 | easy to 10 | 11 | - check these configs into VCS to ensure your teammates can enjoy them 12 | - back them up with the rest of your dotfiles 13 | - share them with others to help them get started 14 | 15 | This page consists of some configs to get you started. If you have a config that 16 | you'd like to share with others, [please share](/guides/contribute/) with us. 17 | 18 | ## Common files 19 | 20 | Since comes with a very minimal configuration out-of-the-box, you can 21 | use this to set up for most common file types. 22 | 23 | ```yaml 24 | icons: 25 | pdf: "" # nf-seti-pdf 26 | image: "" # nf-oct-image 27 | audio: "" # nf-seti-audio 28 | video: "" # nf-oct-video 29 | text: "" # nf-seti-text 30 | table: "󰓫" # nf-md-table 31 | specs: 32 | - pattern: \.pdf$ 33 | icons: 34 | - pdf 35 | - pattern: \.(txt|rtf)$ 36 | icons: 37 | - text 38 | - pattern: \.(csv|tsv)$ 39 | icons: 40 | - table 41 | - pattern: \.(mp3|wav|aac)$ 42 | icons: 43 | - audio 44 | - pattern: \.(png|jpg|bmp|webp)$ 45 | icons: 46 | - image 47 | - pattern: \.(mp4|mov|avi|mkv|webm)$ 48 | icons: 49 | - video 50 | ``` 51 | 52 | ## Office 53 | 54 | If you work with a lot of Microsoft Office apps, you can use this config to have 55 | pretty and consistent icons for all your work files. 56 | 57 | ```yaml 58 | icons: 59 | word: "󱎒" # nf-md-microsoft_word 60 | excel: "󱎏" # nf-md-microsoft_excel 61 | powerpoint: "󱎐" # nf-md-microsoft_powerpoint 62 | specs: 63 | - pattern: \.docx?$ 64 | icons: 65 | - word 66 | - pattern: \.xlsx?$ 67 | icons: 68 | - excel 69 | - pattern: \.pptx?$ 70 | icons: 71 | - powerpoint 72 | ``` 73 | 74 | ## Rust 75 | 76 | being a Rust project, comes with the configuration for Rust projects 77 | baked-in. 78 | 79 | ## Python 80 | 81 | If you build Python projects using Pipenv, Poetry or PDM, this config is for 82 | you. 83 | 84 | ```yaml 85 | icons: 86 | python: "" # nf-seti-python 87 | specs: 88 | - pattern: \.py$ 89 | icons: 90 | - python 91 | style: rgb(255,212,59) 92 | - pattern: requirements.*\.txt$ 93 | icons: 94 | - lock 95 | - pattern: ^(pyproject\.toml|Pipfile)$ 96 | icons: 97 | - package 98 | - pattern: ^(poetry|pdm)\.lock$ 99 | icons: 100 | - lock 101 | importance: -1 102 | collapse: 103 | name: pyproject.toml 104 | - pattern: ^Pipfile.lock$ 105 | icons: 106 | - lock 107 | importance: -1 108 | collapse: 109 | name: Pipfile 110 | ``` 111 | 112 | ## Web development 113 | 114 | If you build web applications without any of the major JavaScript frameworks, 115 | this config is for you. 116 | 117 | ```yaml 118 | icons: 119 | html: "" # nf-seti-html 120 | css: "" # nf-seti-css 121 | sass: "" # nf-seti-sass 122 | less: "" # nf-seti-less 123 | markdown: "" # nf-fa-markdown 124 | specs: 125 | - pattern: \.md$ 126 | icons: 127 | - markdown 128 | - pattern: \.x?html?$ 129 | icons: 130 | - html 131 | style: rgb(255,212,59) 132 | - pattern: \.s[ac]ss$ 133 | icons: 134 | - sass 135 | style: rgb(255,212,59) 136 | - pattern: \.less$ 137 | icons: 138 | - less 139 | style: rgb(255,212,59) 140 | - pattern: \.css$ 141 | icons: 142 | - css 143 | style: rgb(79,192,141) 144 | collapse: 145 | ext: scss 146 | ``` 147 | 148 | ## JavaScript/TypeScript 149 | 150 | If you build JavaScript/TypeScript projects, this config is for you. It supports 151 | npm and pnpm as package managers, and Vue and React frameworks as well. If you 152 | use JavaScript for frontend development, you should also inherit the web dev 153 | config above. 154 | 155 | ```yaml 156 | icons: 157 | javascript: "󰌞" # nf-md-language_javascript 158 | typescript: "󰛦" # nf-md-language_typescript 159 | vue: "" # nf-seti-vue 160 | react: "" # nf-seti-react 161 | specs: 162 | - pattern: \.ts$ 163 | icons: 164 | - typescript 165 | style: rgb(49,120,198) 166 | - pattern: \.(c|m)?js$ 167 | icons: 168 | - javascript 169 | style: rgb(247,223,30) 170 | collapse: 171 | ext: ts 172 | - pattern: \.vue$ 173 | icons: 174 | - vue 175 | style: rgb(79,192,141) 176 | - pattern: \.(j|t)sx$ 177 | icons: 178 | - react 179 | style: rgb(97,218,251) 180 | - pattern: ^package-lock\.json$ 181 | icons: 182 | - package 183 | - pattern: ^(pnpm-lock.yaml|package-lock.json)$ 184 | icons: 185 | - lock 186 | importance: -1 187 | collapse: 188 | name: package.json 189 | ``` 190 | -------------------------------------------------------------------------------- /docs/src/content/docs/guides/markup.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markup 3 | description: 4 | pls uses a novel approach towards styling in the terminal using XML-like tags 5 | to specify colors and formatting. 6 | --- 7 | 8 | The terminal is a unique interface. It is purely text-based yet capable of 9 | displaying colors and formatting, using ANSI escape codes. These codes are 10 | difficult to remember and use, so uses a custom approach. 11 | 12 | ## Markup 13 | 14 | The markup language used by is similar to XML. The directives are 15 | written inside `<` and `>` and wrapped around the text to style. The closing tag 16 | always matches the last opened tag, regardless of the text inside it, so it's 17 | customary to leave it blank. 18 | 19 | You can use a single directive, or a combination of directives, separated by 20 | spaces. 21 | 22 | ``` 23 | bold text bold italic text 24 | ``` 25 | 26 |

 27 |   bold text{" "}
 28 |   
 29 |     bold italic text
 30 |   
 31 | 
32 | 33 | Tags can be nested inside each other and will be joined in order. 34 | 35 | ``` 36 | bold italic text only bold text 37 | ``` 38 | 39 |
 40 |   
 41 |     bold italic text only bold text
 42 |   
 43 | 
44 | 45 | To overwrite all outer tags and start a fresh context, use `clear`. 46 | 47 | ``` 48 | plain text only blue text only bold text 49 | ``` 50 | 51 |
 52 |   plain text
 53 |    only blue text
 54 |    only bold text
 55 | 
56 | 57 | ## Directives 58 | 59 | ### Styles 60 | 61 | Terminals can style text in many ways. allows you to use any permutation 62 | of these styles in your configs. 63 | 64 | - blink 65 | - bold 66 | - dimmed 67 | - hidden 68 | - italic 69 | - reversed 70 | - strikethrough 71 | - underline 72 | 73 | :::caution 74 | 75 | Not all terminals support every style. For example, iTerm2 does not support 76 | `blink`. The only style guaranteed to work is `hidden` because it uses a custom 77 | implementation that drops the hidden text entirely. 78 | 79 | ::: 80 | 81 | You can use a single style directive. 82 | 83 | ``` 84 | bold text 85 | ``` 86 | 87 |
 88 |   bold text
 89 | 
90 | 91 | You can use any combination of style directives. 92 | 93 | ``` 94 | BIUS text 95 | ``` 96 | 97 |
 98 |   
 99 |     
100 |       
101 |         BIUS text
102 |       
103 |     
104 |   
105 | 
106 | 107 | ### Colors 108 | 109 | Color support in terminals can range from 16 named colors to 16 million RGB 110 | colors! allows you to use all the colors supported by your terminal. 111 | 112 | #### Named 113 | 114 | Named colors consist of 8 regular colors and 8 bright colors (one for each of 115 | the regular ones). 116 | 117 | - black 118 | - red 119 | - green 120 | - yellow 121 | - blue 122 | - magenta 123 | - cyan 124 | - white 125 | 126 | To use the named colors in you can use the color name directly as a 127 | directive in the tag. 128 | 129 | ``` 130 | blue text 131 | ``` 132 | 133 |
134 |   blue text
135 | 
136 | 137 | To use the bright variant, you can prefix `bright_` before the color name. 138 | 139 | :::note 140 | 141 | Some themes like [Solarized](https://ethanschoonover.com/solarized/) may opt to 142 | use the bright color space for additional colors not covered in the regular set 143 | (like orange in `bright_red`, violet in `bright_magenta`). 144 | 145 | ::: 146 | 147 | ``` 148 | orange text violet text 149 | ``` 150 | 151 |
152 |   orange text{" "}
153 |   violet text
154 | 
155 | 156 | To use a color as the background, you can prefix `bg:` before the color name. 157 | 158 | ``` 159 | black text white text 160 | ``` 161 | 162 |
163 |   
164 |     black text{" "}
165 |     white text
166 |   
167 | 
168 | 169 | #### True colors 170 | 171 | also supports using RGB colors. These colors can be specified using a 172 | triplet of three `u8` numbers, each between 0 and 255, both inclusive. 173 | 174 | ``` 175 | pure green text 176 | ``` 177 | 178 |
179 |   pure green text
180 | 
181 | 182 | To use a color as the background, you can prefix `bg:` before the color name. 183 | 184 | ``` 185 | black text white text 186 | ``` 187 | 188 |
189 |   
190 |     black text{" "}
191 |     white text
192 |   
193 | 
194 | -------------------------------------------------------------------------------- /docs/src/content/docs/about/comparison.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Comparison 3 | description: 4 | There are many alternatives to ls(1) but pls is unique in its target 5 | demographic of pro users and its focus on customisation. 6 | --- 7 | 8 | has the distinction of being an `ls(1)` replacement specifically 9 | targeted a pro audience. This leads to different motivations, different 10 | decisions, different choices and different defaults. This also gives us the 11 | advantage of being able to provide features that are powerful but complex. 12 | 13 | There are other `ls(1)` alternatives that have been around for longer and have 14 | sizeable user bases. 15 | 16 | | Tool | Language | GitHub stars | 17 | | ---------------------------------------------------- | -------- | --------------------------------------------- | 18 | | [exa](https://github.com/ogham/exa) | Rust | | 19 | | [eza](https://github.com/eza-community/eza) | Rust | | 20 | | [`lsd`](https://github.com/lsd-rs/lsd) | Rust | | 21 | | [`colorls`](https://github.com/athityakumar/colorls) | Ruby | | 22 | 23 | strives to provide all the features expected from these tools, and more. 24 | For an idea, here is a comparison between and the most popular 25 | alternatives, [eza](https://eza.rocks) and its predecessor 26 | [exa](https://the.exa.website). 27 | 28 | ## Pros 29 | 30 | - eza hides files with a leading dot by default, an approach it inherits from 31 | `ls(1)`. dims or hides files based on their 32 | [importance](/features/importance/), making it suitable for modern dev 33 | workflows involving tooling configs. 34 | 35 | - eza supports theming using a global YAML file like . But goes 36 | further with [cascading YAML files](/reference/conf/) to enable per-project 37 | configuration that's also more readable, maintainable and ergonomic. You can 38 | also check these config files into your VCS and share them with your team. 39 | 40 | - eza uses globs which limits the match to simple queries. uses 41 | [specs](/guides/specs/) which match files using the full power of regex and 42 | then provide both styling and [icons](/features/icons/) for their matches. 43 | These specs can also be cascaded. 44 | 45 | - eza's output is very colorful by default whereas employs sparse color 46 | and formatting, to add meaning or context. [markup](/guides/markup/) 47 | syntax is more compact than eza's separate fields for colors and styles. 48 | 49 | - has more [metadata fields](/features/detail_view/) compared to eza. It 50 | also allows the user to view some metadata fields in multiple ways (including 51 | simultaneously), which eza cannot do. 52 | 53 | - eza provides filtering by glob matches and sorting by one field. 54 | provides filtering (by [regex](/features/name_filter/) or 55 | [type](/features/type_filter/)) and [sorting](/features/sorting) (by multiple 56 | selections out of over 18 bases × 2 directions). 57 | 58 | - eza can dim a small list of generated files. has a much more powerful 59 | [collapse](/features/collapse/) feature that can render generated files 60 | differently and can be extended with specs. 61 | 62 | - eza defaults to no icons, grid view, files mixed with folders and sorting by 63 | name. defaults to showing icons, list view, folders listed first and 64 | leading dots aligned and sorting by canonical name. 65 | 66 | - can render high-quality [SVG icons](/features/svg.mdx) from icon packs 67 | like [Catppuccin](https://github.com/catppuccin/vscode-icons) for files and 68 | directories. eza cannot do this. 69 | 70 | - creates a grid of uniform width for each cell, which looks more spaced 71 | out, whereas eza's grid view is more compact by compressing each column into 72 | as small a width as necessary. 73 | 74 | ## Cons 75 | 76 | - eza's interface is much more compatible with `ls(1)` in the base case. A lot 77 | of `ls(1)` options map 1:1 with eza. does not maintain compatibility 78 | with `ls(1)` CLI flags. 79 | 80 | - eza is a mature product with a large contributor base. is a young 81 | project with a [novice 🦀 Rustacean maintainer](https://dhruvkb.dev/). 82 | Contributions, both in [code](https://github.com/pls-rs/pls) and as 83 | [sponsorship](https://github.com/sponsors/dhruvkb), are welcome! 84 | 85 | - The spec system uses might be slow when listing lots of files due to 86 | its Cartesian product complexity. Also, lacks the speed optimisations 87 | that a mature tool like eza has built over time. 88 | 89 | - eza can show if a file has extended attributes. opted not to do this 90 | because it's not a common use case. 91 | 92 | - eza has a long grid view (which is a combination of 's 93 | [detail](/features/detail_view/) and [grid](/features/grid_view/) views) that 94 | [are not yet present](/features/upcoming/) in . 95 | 96 | - eza has a tree mode that [does not yet have](/features/upcoming/). 97 | 98 | - eza supports Windows which [does not yet do](/about/faq/). 99 | --------------------------------------------------------------------------------