From cb695fcaa7b550007b19597ed18ec2ba31b25e4a Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Mon, 24 May 2021 10:12:08 +0530 Subject: [PATCH] Add colorful permissions Ref: https://github.com/sayanarijit/xplr/issues/187 --- Cargo.lock | 17 ++++++++-- Cargo.toml | 4 ++- src/app.rs | 13 ++++++-- src/init.lua | 75 +++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 1 + src/lua.rs | 10 +++--- src/permissions.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 30 ++++++++++++++--- 8 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 src/permissions.rs diff --git a/Cargo.lock b/Cargo.lock index 0d54993..7d96c82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ansi-to-tui" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5091b918c2bf8daa9031e1cef129eafc45272352b885f114fb36a9aec512fcf2" +dependencies = [ + "tui", +] + [[package]] name = "anyhow" version = "1.0.40" @@ -410,9 +419,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.92" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "linked-hash-map" @@ -1099,8 +1108,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "xplr" -version = "0.10.2" +version = "0.11.0" dependencies = [ + "ansi-to-tui", "anyhow", "chrono", "criterion", @@ -1109,6 +1119,7 @@ dependencies = [ "humansize", "indexmap", "lazy_static", + "libc", "mime_guess", "mlua", "natord", diff --git a/Cargo.toml b/Cargo.toml index d252920..b61a2f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xplr" -version = "0.10.2" # Update lua.rs +version = "0.11.0" # Update lua.rs authors = ["Arijit Basu "] edition = "2018" description = "A hackable, minimal, fast TUI file explorer" @@ -29,6 +29,8 @@ indexmap = { version = "1.6.2", features = ["serde"] } natord = "1.0.9" humansize = "1.1.0" mlua = { version = "0.5.4", features = ["luajit", "vendored", "serialize", "send"] } +ansi-to-tui = "0.1.9" +libc = "0.2.94" [dev-dependencies] criterion = "0.3" diff --git a/src/app.rs b/src/app.rs index 69a19f7..c2263b7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use crate::config::Mode; use crate::explorer; use crate::input::Key; use crate::lua; +use crate::permissions::Permissions; use crate::ui::Layout; use anyhow::{bail, Result}; use chrono::{DateTime, Local}; @@ -212,6 +213,7 @@ pub struct Node { pub mime_essence: String, pub size: u64, pub human_size: String, + pub permissions: Permissions, pub canonical: Option, pub symlink: Option, } @@ -235,7 +237,7 @@ impl Node { .map(|p| (false, Some(ResolvedNode::from(p)))) .unwrap_or_else(|_| (true, None)); - let (is_symlink, is_dir, is_file, is_readonly, size) = path + let (is_symlink, is_dir, is_file, is_readonly, size, permissions) = path .symlink_metadata() .map(|m| { ( @@ -244,9 +246,10 @@ impl Node { m.is_file(), m.permissions().readonly(), m.len(), + Permissions::from(&m), ) }) - .unwrap_or((false, false, false, false, 0)); + .unwrap_or_else(|_| (false, false, false, false, 0, Permissions::default())); let mime_essence = mime_guess::from_path(&path) .first() @@ -268,6 +271,7 @@ impl Node { mime_essence, size, human_size, + permissions, canonical: maybe_canonical_meta.clone(), symlink: if is_symlink { maybe_canonical_meta @@ -346,6 +350,11 @@ impl Node { pub fn human_size(&self) -> &String { &self.human_size } + + /// Get a reference to the node's permissions. + pub fn permissions(&self) -> &Permissions { + &self.permissions + } } impl Ord for Node { diff --git a/src/init.lua b/src/init.lua index 31e9503..61407d2 100644 --- a/src/init.lua +++ b/src/init.lua @@ -270,7 +270,8 @@ xplr.config.general.table.col_spacing = 1 xplr.config.general.table.col_widths = { { Percentage = 10 }, { Percentage = 50 }, - { Percentage = 20 }, + { Percentage = 10 }, + { Percentage = 10 }, { Percentage = 20 }, } @@ -278,6 +279,7 @@ xplr.config.general.table.col_widths = { xplr.config.general.table.header.cols = { { format = " index", style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } }, { format = "╭──── path", style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } }, + { format = "permissions", style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } }, { format = "size", style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } }, { format = "type", style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } }, } @@ -305,6 +307,10 @@ xplr.config.general.table.row.cols = { format = "builtin.fmt_general_table_row_cols_3", style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } }, + { + format = "builtin.fmt_general_table_row_cols_4", + style = { add_modifiers = nil, bg = nil, fg = nil, sub_modifiers = nil } + }, } xplr.config.general.table.row.height = 0 xplr.config.general.table.row.style.add_modifiers = nil @@ -1999,9 +2005,9 @@ xplr.config.modes.builtin.switch_layout = { ---- Custom xplr.config.modes.custom = {} - -- Function ---- Formaters +------ Index xplr.fn.builtin.fmt_general_table_row_cols_0 = function(m) local r = "" if m.is_before_focus then @@ -2015,11 +2021,12 @@ xplr.fn.builtin.fmt_general_table_row_cols_0 = function(m) return r end +------ Path xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) local r = m.tree .. m.prefix if m.meta.icon == nil then - r = r .. " " + r = r .. " " else r = r .. m.meta.icon .. " " end @@ -2044,13 +2051,70 @@ xplr.fn.builtin.fmt_general_table_row_cols_1 = function(m) r = r .. "/" end end - end return r end +------ Permissions xplr.fn.builtin.fmt_general_table_row_cols_2 = function(m) + + local no_color = os.getenv("NO_COLOR") + + local function green(x) + if no_color == nil then + return "\x1b[32m" .. x .. "\x1b[0m" + else + return x + end + end + + local function yellow(x) + if no_color == nil then + return "\x1b[33m" .. x .. "\x1b[0m" + else + return x + end + end + + + local function red(x) + if no_color == nil then + return "\x1b[31m" .. x .. "\x1b[0m" + else + return x + end + end + + local function bit(x, color, cond) + if cond then + return color(x) + else + return color("-") + end + end + + local r = "" + -- User + r = r .. bit("r", green, m.permissions.user_read) + r = r .. bit("w", yellow, m.permissions.user_write) + r = r .. bit("x", red, m.permissions.user_execute) + + -- Group + r = r .. bit("r", green, m.permissions.group_read) + r = r .. bit("w", yellow, m.permissions.group_write) + r = r .. bit("x", red, m.permissions.group_execute) + + -- Other + r = r .. bit("r", green, m.permissions.other_read) + r = r .. bit("w", yellow, m.permissions.other_write) + r = r .. bit("x", red, m.permissions.other_execute) + + return r +end + +------ Size +xplr.fn.builtin.fmt_general_table_row_cols_3 = function(m) if not m.is_dir then return m.human_size else @@ -2058,7 +2122,8 @@ xplr.fn.builtin.fmt_general_table_row_cols_2 = function(m) end end -xplr.fn.builtin.fmt_general_table_row_cols_3 = function(m) +------ Mime +xplr.fn.builtin.fmt_general_table_row_cols_4 = function(m) if m.is_symlink and not m.is_broken then return m.symlink.mime_essence else diff --git a/src/lib.rs b/src/lib.rs index c0d4b40..bf407fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod event_reader; pub mod explorer; pub mod input; pub mod lua; +pub mod permissions; pub mod pipe_reader; pub mod pwd_watcher; pub mod runner; diff --git a/src/lua.rs b/src/lua.rs index 715d2d6..8936368 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -133,12 +133,10 @@ mod test { #[test] fn test_compatibility() { assert!(check_version(VERSION, "foo path").is_ok()); - assert!(check_version("0.10.0", "foo path").is_ok()); - assert!(check_version("0.10.1", "foo path").is_ok()); - assert!(check_version("0.10.2", "foo path").is_ok()); + assert!(check_version("0.11.0", "foo path").is_ok()); - assert!(check_version("0.10.3", "foo path").is_err()); - assert!(check_version("0.9.1", "foo path").is_err()); - assert!(check_version("1.10.1", "foo path").is_err()); + assert!(check_version("0.11.1", "foo path").is_err()); + assert!(check_version("0.10.0", "foo path").is_err()); + assert!(check_version("1.12.0", "foo path").is_err()); } } diff --git a/src/permissions.rs b/src/permissions.rs new file mode 100644 index 0000000..7f41c6e --- /dev/null +++ b/src/permissions.rs @@ -0,0 +1,81 @@ +// Stolen from https://github.com/Peltoche/lsd/blob/master/src/meta/permissions.rs + +use serde::{Deserialize, Serialize}; +use std::fs::Metadata; + +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Hash, Default)] +pub struct Permissions { + pub user_read: bool, + pub user_write: bool, + pub user_execute: bool, + + pub group_read: bool, + pub group_write: bool, + pub group_execute: bool, + + pub other_read: bool, + pub other_write: bool, + pub other_execute: bool, + + pub sticky: bool, + pub setgid: bool, + pub setuid: bool, +} + +impl<'a> From<&'a Metadata> for Permissions { + #[cfg(unix)] + fn from(meta: &Metadata) -> Self { + use std::os::unix::fs::PermissionsExt; + + let bits = meta.permissions().mode(); + let has_bit = |bit| bits & bit == bit; + + Self { + user_read: has_bit(modes::USER_READ), + user_write: has_bit(modes::USER_WRITE), + user_execute: has_bit(modes::USER_EXECUTE), + + group_read: has_bit(modes::GROUP_READ), + group_write: has_bit(modes::GROUP_WRITE), + group_execute: has_bit(modes::GROUP_EXECUTE), + + other_read: has_bit(modes::OTHER_READ), + other_write: has_bit(modes::OTHER_WRITE), + other_execute: has_bit(modes::OTHER_EXECUTE), + + sticky: has_bit(modes::STICKY), + setgid: has_bit(modes::SETGID), + setuid: has_bit(modes::SETUID), + } + } + + #[cfg(windows)] + fn from(_: &Metadata) -> Self { + panic!("Cannot get permissions from metadata on Windows") + } +} + +// More readable aliases for the permission bits exposed by libc. +#[allow(trivial_numeric_casts)] +#[cfg(unix)] +mod modes { + pub type Mode = u32; + // The `libc::mode_t` type’s actual type varies, but the value returned + // from `metadata.permissions().mode()` is always `u32`. + + pub const USER_READ: Mode = libc::S_IRUSR as Mode; + pub const USER_WRITE: Mode = libc::S_IWUSR as Mode; + pub const USER_EXECUTE: Mode = libc::S_IXUSR as Mode; + + pub const GROUP_READ: Mode = libc::S_IRGRP as Mode; + pub const GROUP_WRITE: Mode = libc::S_IWGRP as Mode; + pub const GROUP_EXECUTE: Mode = libc::S_IXGRP as Mode; + + pub const OTHER_READ: Mode = libc::S_IROTH as Mode; + pub const OTHER_WRITE: Mode = libc::S_IWOTH as Mode; + pub const OTHER_EXECUTE: Mode = libc::S_IXOTH as Mode; + + pub const STICKY: Mode = libc::S_ISVTX as Mode; + pub const SETGID: Mode = libc::S_ISGID as Mode; + pub const SETUID: Mode = libc::S_ISUID as Mode; +} diff --git a/src/ui.rs b/src/ui.rs index b5a254d..87c0293 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,6 +3,9 @@ use crate::app::HelpMenuLine; use crate::app::{Node, ResolvedNode}; use crate::config::PanelUiConfig; use crate::lua; +use crate::permissions::Permissions; +use ansi_to_tui::ansi_to_text; +use anyhow::Result; use indexmap::IndexSet; use lazy_static::lazy_static; use mlua::Lua; @@ -15,7 +18,7 @@ use tui::backend::Backend; use tui::layout::Rect; use tui::layout::{Constraint as TuiConstraint, Direction, Layout as TuiLayout}; use tui::style::{Color, Modifier as TuiModifier, Style as TuiStyle}; -use tui::text::{Span, Spans}; +use tui::text::{Span, Spans, Text}; use tui::widgets::{Block, Borders as TuiBorders, Cell, List, ListItem, Paragraph, Row, Table}; use tui::Frame; @@ -319,7 +322,7 @@ impl From for ResolvedNodeUiMetadata { } #[derive(Debug, Clone, Serialize, Deserialize)] -struct NodeUiMetadata { +pub struct NodeUiMetadata { // From Node pub parent: String, pub relative_path: String, @@ -333,6 +336,7 @@ struct NodeUiMetadata { pub mime_essence: String, pub size: u64, pub human_size: String, + pub permissions: Permissions, pub canonical: Option, pub symlink: Option, @@ -378,6 +382,7 @@ impl NodeUiMetadata { mime_essence: node.mime_essence().clone(), size: node.size(), human_size: node.human_size().clone(), + permissions: node.permissions().to_owned(), canonical: node.canonical().to_owned().map(|s| s.into()), symlink: node.symlink().to_owned().map(|s| s.into()), index, @@ -541,10 +546,27 @@ fn draw_table( .iter() .filter_map(|c| { c.format().as_ref().map(|f| { - lua::call(lua, f, &v).unwrap_or_else(|e| e.to_string()) + let out: Result = lua::call(lua, f, &v); + match out { + Ok(o) => { + let text = + ansi_to_text(o.bytes()).unwrap_or_else(|e| { + Text::raw(format!("{:?}", e)) + }); + + // TODO: Track https://github.com/uttarayan21/ansi-to-tui/issues/2 + // And https://github.com/fdehau/tui-rs/issues/304 + if text.lines.is_empty() { + Text::raw(o.to_string()) + } else { + text + } + } + Err(e) => Text::raw(e.to_string()), + } }) }) - .collect::>() + .collect::>() }) .unwrap_or_default() .iter()