diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 4888cb786..f33e9ede9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target out.md **/*.rs.bk +.direnv/ diff --git a/Cargo.lock b/Cargo.lock index ab12a299c..3410db978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,6 +170,7 @@ dependencies = [ "js-sys", "num-traits", "pure-rust-locales", + "serde", "wasm-bindgen", "windows-targets 0.52.4", ] @@ -752,6 +753,7 @@ dependencies = [ "once_cell", "predicates", "serde", + "serde_json", "serde_yaml", "serial_test", "sys-locale", @@ -1043,6 +1045,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" diff --git a/Cargo.toml b/Cargo.toml index 731a0db8a..cf8200042 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ terminal_size = "0.3" thiserror = "1.0" sys-locale = "0.3" once_cell = "1.17.1" -chrono = { version = "0.4.19", features = ["unstable-locales"] } +chrono = { version = "0.4.19", features = ["serde", "unstable-locales"] } chrono-humanize = "0.2" # incompatible with v0.1.11 unicode-width = "0.1.13" @@ -46,6 +46,7 @@ serde_yaml = "0.9" url = "2.1" vsort = "0.2" xdg = "2.5" +serde_json = "1.0.128" [target."cfg(not(all(windows, target_arch = \"x86\", target_env = \"gnu\")))".dependencies] # if ssl feature is enabled compilation will fail on arm-unknown-linux-gnueabihf and i686-pc-windows-gnu diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..8451a3b3e --- /dev/null +++ b/flake.lock @@ -0,0 +1,57 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 0, + "narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=", + "path": "/nix/store/60sn02zhawl3kwn0r515zff3h6hg6ydz-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..2ee1b6aa0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + inputs = { + utils.url = "github:numtide/flake-utils"; + }; + outputs = { self, nixpkgs, utils }: utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + lsd = pkgs.rustPlatform.buildRustPackage { + pname = "lsd"; + version = "1.1.5"; + + src = ./.; + + cargoHash = "sha256-chryC4YDvd8c7fIiHMWi+g5JYZJqkLPknSCgzYVKucE="; + doCheck = false; + + nativeBuildInputs = [ pkgs.git ]; + + #meta = with pkgs.lib; { + # description = "A fast line-oriented regex search tool, similar to ag and ack"; + # homepage = "https://github.com/BurntSushi/ripgrep"; + # license = licenses.unlicense; + # maintainers = []; + #}; + }; + in { + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + cargo + rustc + rust-analyzer + rustfmt + ]; + }; + packages.default = lsd; + } + ); +} diff --git a/src/app.rs b/src/app.rs index 1be493972..e7bf92460 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,6 +60,10 @@ pub struct Cli { #[arg(long)] pub tree: bool, + /// Print the output as json + #[arg(long)] + pub json: bool, + /// Stop recursing into directories after reaching specified depth #[arg(long, value_name = "NUM")] pub depth: Option, diff --git a/src/core.rs b/src/core.rs index 97472ec40..0aed4c1b4 100644 --- a/src/core.rs +++ b/src/core.rs @@ -72,7 +72,7 @@ impl Core { // or require a raw output (like the `wc` command). if !tty_available { // we should not overwrite the tree layout - if flags.layout != Layout::Tree { + if flags.layout != Layout::Tree && flags.layout != Layout::Json { flags.layout = Layout::OneLine; } @@ -174,22 +174,28 @@ impl Core { } fn display(&self, metas: &[Meta]) { - let output = if self.flags.layout == Layout::Tree { - display::tree( + let output = match self.flags.layout { + Layout::Tree => display::tree( metas, &self.flags, &self.colors, &self.icons, &self.git_theme, - ) - } else { - display::grid( + ), + Layout::Json => display::json( metas, &self.flags, &self.colors, &self.icons, &self.git_theme, - ) + ), + _ => display::grid( + metas, + &self.flags, + &self.colors, + &self.icons, + &self.git_theme, + ), }; print_output!("{}", output); diff --git a/src/display.rs b/src/display.rs index ca037f0b7..be0e4953a 100644 --- a/src/display.rs +++ b/src/display.rs @@ -4,7 +4,9 @@ use crate::flags::{Display, Flags, HyperlinkOption, Layout}; use crate::git_theme::GitTheme; use crate::icon::Icons; use crate::meta::name::DisplayOption; -use crate::meta::{FileType, Meta, OwnerCache}; +use crate::meta::{Date, FileType, Meta, OwnerCache}; +use chrono::{DateTime, Local}; +use serde::Serialize; use std::collections::HashMap; use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use terminal_size::terminal_size; @@ -15,6 +17,95 @@ const LINE: &str = "\u{2502} "; // "│ " const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──" const BLANK: &str = " "; +#[derive(Serialize)] +struct JsonMeta { + name: String, + display: String, + r#type: String, + extension: Option, + dirlike: bool, + content: Option>, + date: Option>, + icon: String, + permissions: String, + size: Option, + user: Option, + group: Option, + path: String, +} + +impl JsonMeta { + fn from_meta(value: &Meta, icons: &Icons, colors: &Colors, flags: &Flags) -> JsonMeta { + let name = &value.name; + let icon = icons.get(&name); + let display = name.render( + colors, + icons, + &DisplayOption::FileName, + flags.hyperlink, + false, + ); + let permissions = value + .permissions_or_attributes + .as_ref() + .unwrap() + .render(colors, flags) + .to_string(); + let size = value.size.as_ref().map(|size| size.get_bytes()); + let user = value.owner.as_ref().map(|owner| { + owner + .render_user(colors, &OwnerCache::default(), flags) + .to_string() + }); + let group = value.owner.as_ref().map(|owner| { + owner + .render_group(colors, &OwnerCache::default(), flags) + .to_string() + }); + let path = value.path.to_str().unwrap().to_string(); + + JsonMeta { + content: value.content.as_ref().map(|content| { + content + .iter() + .map(|meta| JsonMeta::from_meta(meta, icons, colors, flags)) + .collect() + }), + name: name.name.clone(), + display: display.to_string(), + r#type: name.file_type().render(colors).to_string(), + extension: name.extension().map(String::from), + date: value.date.as_ref().and_then(|date| match date { + &Date::Date(date) => Some(date.clone()), + &Date::Invalid => None, + }), + dirlike: value.file_type.is_dirlike(), + permissions, + icon, + size, + user, + group, + path, + } + } +} + +pub fn json( + metas: &[Meta], + flags: &Flags, + colors: &Colors, + icons: &Icons, + _git_theme: &GitTheme, +) -> String { + serde_json::to_string( + &metas + .into_iter() + .map(|meta| JsonMeta::from_meta(meta, icons, colors, flags)) + .collect::>(), + ) + .unwrap() +} + pub fn grid( metas: &[Meta], flags: &Flags, diff --git a/src/flags/layout.rs b/src/flags/layout.rs index d0d44aa94..8b8bdbed8 100644 --- a/src/flags/layout.rs +++ b/src/flags/layout.rs @@ -16,6 +16,7 @@ pub enum Layout { Grid, Tree, OneLine, + Json, } impl Configurable for Layout { @@ -26,7 +27,9 @@ impl Configurable for Layout { /// arguments is greater than 1, this also returns the [OneLine](Layout::OneLine) variant. /// Finally if neither of them is passed, this returns [None]. fn from_cli(cli: &Cli) -> Option { - if cli.tree { + if cli.json { + Some(Self::Json) + } else if cli.tree { Some(Self::Tree) } else if cli.long || cli.oneline || cli.inode || cli.context || cli.blocks.len() > 1 // TODO: handle this differently diff --git a/src/git.rs b/src/git.rs index 207da076d..a38714784 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,8 +1,10 @@ +use serde::Serialize; + use crate::meta::git_file_status::GitFileStatus; use std::path::{Path, PathBuf}; #[allow(dead_code)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize)] pub enum GitStatus { /// No status info #[default] diff --git a/src/meta/access_control.rs b/src/meta/access_control.rs index 493025a7a..e53a3e66f 100644 --- a/src/meta/access_control.rs +++ b/src/meta/access_control.rs @@ -1,7 +1,9 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use std::path::Path; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct AccessControl { has_acl: bool, selinux_context: String, diff --git a/src/meta/date.rs b/src/meta/date.rs index 007023c35..100876567 100644 --- a/src/meta/date.rs +++ b/src/meta/date.rs @@ -3,11 +3,12 @@ use crate::color::{ColoredString, Colors, Elem}; use crate::flags::{DateFlag, Flags}; use chrono::{DateTime, Duration, Local}; use chrono_humanize::HumanTime; +use serde::Serialize; use std::fs::Metadata; use std::panic; use std::time::SystemTime; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub enum Date { Date(DateTime), Invalid, diff --git a/src/meta/filetype.rs b/src/meta/filetype.rs index 07ca8ab2f..dd6b2d396 100644 --- a/src/meta/filetype.rs +++ b/src/meta/filetype.rs @@ -1,7 +1,9 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize)] #[cfg_attr(windows, allow(dead_code))] pub enum FileType { BlockDevice, diff --git a/src/meta/git_file_status.rs b/src/meta/git_file_status.rs index 160517d9b..63a3d9e02 100644 --- a/src/meta/git_file_status.rs +++ b/src/meta/git_file_status.rs @@ -1,8 +1,10 @@ +use serde::Serialize; + use crate::color::{self, ColoredString, Colors}; use crate::git::GitStatus; use crate::git_theme::GitTheme; -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] pub struct GitFileStatus { pub index: GitStatus, pub workdir: GitStatus, diff --git a/src/meta/indicator.rs b/src/meta/indicator.rs index c81758b99..35fa5dc0f 100644 --- a/src/meta/indicator.rs +++ b/src/meta/indicator.rs @@ -1,8 +1,10 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors}; use crate::flags::Flags; use crate::meta::FileType; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Indicator(&'static str); impl From for Indicator { diff --git a/src/meta/inode.rs b/src/meta/inode.rs index c7d2dae47..f4bbf3f8e 100644 --- a/src/meta/inode.rs +++ b/src/meta/inode.rs @@ -1,7 +1,9 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize)] pub struct INode { index: Option, } diff --git a/src/meta/links.rs b/src/meta/links.rs index 6ee86129b..24827cd21 100644 --- a/src/meta/links.rs +++ b/src/meta/links.rs @@ -1,7 +1,9 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use std::fs::Metadata; -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize)] pub struct Links { nlink: Option, } diff --git a/src/meta/mod.rs b/src/meta/mod.rs index e8384fd19..84ce8c4fb 100644 --- a/src/meta/mod.rs +++ b/src/meta/mod.rs @@ -18,6 +18,8 @@ mod windows_attributes; #[cfg(windows)] mod windows_utils; +use serde::Serialize; + pub use self::access_control::AccessControl; pub use self::date::Date; pub use self::filetype::FileType; @@ -41,7 +43,7 @@ use std::path::{Component, Path, PathBuf}; #[cfg(windows)] use self::windows_attributes::get_attributes; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct Meta { pub name: Name, pub path: PathBuf, diff --git a/src/meta/name.rs b/src/meta/name.rs index 788c8907e..11979d642 100644 --- a/src/meta/name.rs +++ b/src/meta/name.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use crate::flags::HyperlinkOption; use crate::icon::Icons; @@ -15,7 +17,7 @@ pub enum DisplayOption<'a> { None, } -#[derive(Clone, Debug, Eq)] +#[derive(Clone, Debug, Eq, Serialize)] pub struct Name { pub name: String, path: PathBuf, diff --git a/src/meta/owner.rs b/src/meta/owner.rs index 132df3bb2..2372539df 100644 --- a/src/meta/owner.rs +++ b/src/meta/owner.rs @@ -1,5 +1,6 @@ use crate::color::{ColoredString, Colors, Elem}; use crate::Flags; +use serde::Serialize; #[cfg(unix)] use std::fs::Metadata; #[cfg(unix)] @@ -14,7 +15,7 @@ pub struct Cache { } #[cfg(unix)] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] pub struct Owner { user: u32, group: u32, diff --git a/src/meta/permissions.rs b/src/meta/permissions.rs index 323675df9..e39ebc09d 100644 --- a/src/meta/permissions.rs +++ b/src/meta/permissions.rs @@ -1,8 +1,10 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use crate::flags::{Flags, PermissionFlag}; use std::fs::Metadata; -#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize)] pub struct Permissions { pub user_read: bool, pub user_write: bool, diff --git a/src/meta/permissions_or_attributes.rs b/src/meta/permissions_or_attributes.rs index d1c7347c2..567d7227a 100644 --- a/src/meta/permissions_or_attributes.rs +++ b/src/meta/permissions_or_attributes.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + #[cfg(windows)] use super::windows_attributes::WindowsAttributes; use crate::{ @@ -7,7 +9,7 @@ use crate::{ use super::Permissions; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum PermissionsOrAttributes { Permissions(Permissions), #[cfg(windows)] diff --git a/src/meta/size.rs b/src/meta/size.rs index 8b66c96e6..bf95068c5 100644 --- a/src/meta/size.rs +++ b/src/meta/size.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use crate::flags::{Flags, SizeFlag}; use std::fs::Metadata; @@ -16,7 +18,7 @@ pub enum Unit { Tera, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct Size { bytes: u64, } diff --git a/src/meta/symlink.rs b/src/meta/symlink.rs index 951ba2493..b36335501 100644 --- a/src/meta/symlink.rs +++ b/src/meta/symlink.rs @@ -1,9 +1,11 @@ +use serde::Serialize; + use crate::color::{ColoredString, Colors, Elem}; use crate::flags::Flags; use std::fs::read_link; use std::path::Path; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct SymLink { target: Option, valid: bool,