dysk-cli-2.10.0/.cargo_vcs_info.json0000644000000001410000000000100126270ustar { "git": { "sha1": "d9b076871d63dc4c37bec053314aa60cb8e91fbe" }, "path_in_vcs": "cli" }dysk-cli-2.10.0/Cargo.toml0000644000000023300000000000100106270ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "dysk-cli" version = "2.10.0" authors = ["dystroy "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "the dysk cli as a library" readme = false license = "MIT" resolver = "1" [profile.release] strip = true [lib] name = "dysk_cli" path = "src/lib.rs" [dependencies.bet] version = "1.0.4" [dependencies.clap] version = "4.4" features = [ "derive", "cargo", ] [dependencies.clap-help] version = "1.0" [dependencies.file-size] version = "1.0.3" [dependencies.lfs-core] version = "0.12" [dependencies.serde] version = "1.0" [dependencies.serde_json] version = "1.0" [dependencies.termimad] version = "0.30" dysk-cli-2.10.0/Cargo.toml.orig000064400000000000000000000007601046102023000143150ustar 00000000000000[package] name = "dysk-cli" version = "2.10.0" authors = ["dystroy "] edition = "2021" license = "MIT" description = "the dysk cli as a library" rust-version = "1.70" resolver = "1" [dependencies] bet = "1.0.4" clap = { version = "4.4", features = ["derive", "cargo"] } clap-help = "1.0" file-size = "1.0.3" lfs-core = "0.12" serde = "1.0" serde_json = "1.0" termimad = "0.30" [profile.release] strip = true [patch.crates-io] # termimad = { path = "../../termimad" } dysk-cli-2.10.0/src/args.rs000064400000000000000000000051541046102023000135210ustar 00000000000000use { crate::{ cols::Cols, filter::Filter, units::Units, sorting::Sorting, }, clap::{Parser, ValueEnum}, termimad::crossterm::tty::IsTty, std::path::PathBuf, }; /// List your filesystems. /// /// Documentation at https://dystroy.org/dysk #[derive(Debug, Parser)] #[command(author, about, name = "dysk", disable_version_flag = true, version, disable_help_flag = true)] pub struct Args { /// print help information #[arg(long)] pub help: bool, /// print the version #[arg(long)] pub version: bool, /// show all mount points #[arg(short, long)] pub all: bool, /// whether to have styles and colors #[arg(long, default_value="auto", value_name = "color")] pub color: TriBool, /// use only ASCII characters for table rendering #[arg(long)] pub ascii: bool, /// fetch stats of remote volumes #[arg(long, default_value="auto", value_name = "choice")] pub remote_stats: TriBool, /// list the column names which can be used in -s, -f, or -c #[arg(long)] pub list_cols: bool, /// columns, eg `-c +inodes` or `-c id+dev+default` #[arg(short, long, default_value = "fs+type+disk+used+use+free+size+mp", value_name = "columns")] pub cols: Cols, /// filter, eg `-f '(size<35G | remote=false) & type=xfs'` #[arg(short, long, value_name = "expr")] pub filter: Option, /// sort, eg `inodes`, `type-desc`, or `size-asc` #[arg(short, long, default_value = "size", value_name = "sort")] pub sort: Sorting, /// units: `SI` (SI norm), `binary` (1024 based), or `bytes` (raw number) #[arg(short, long, default_value = "SI", value_name = "unit")] pub units: Units, /// output as JSON #[arg(short, long)] pub json: bool, /// output as CSV #[arg(long)] pub csv: bool, /// CSV separator #[arg(long, default_value = ",", value_name = "sep")] pub csv_separator: char, /// if provided, only the device holding this path will be shown pub path: Option, } /// This is an Option but I didn't find any way to configure /// clap to parse an Option as I want #[derive(ValueEnum)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TriBool { Auto, Yes, No, } impl TriBool { pub fn unwrap_or_else(self, f: F) -> bool where F: FnOnce() -> bool { match self { Self::Auto => f(), Self::Yes => true, Self::No => false, } } } impl Args { pub fn color(&self) -> bool { self.color.unwrap_or_else(|| std::io::stdout().is_tty()) } } dysk-cli-2.10.0/src/col.rs000064400000000000000000000303161046102023000133400ustar 00000000000000use { crate::order::Order, lfs_core::Mount, std::{ cmp::Ordering, fmt, str::FromStr, }, termimad::minimad::Alignment, }; macro_rules! col_enum { (@just_variant $variant:ident $discarded:ident) => { Col::$variant }; ($($variant:ident $name:literal $($alias:literal)* : $title:literal $($def:ident)*,)*) => { /// A column of the lfs table. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Col { $($variant,)* } pub static ALL_COLS: &[Col] = &[ $(Col::$variant,)* ]; pub static DEFAULT_COLS: &[Col] = &[ $( $(col_enum!(@just_variant $variant $def),)* )* ]; impl FromStr for Col { type Err = ParseColError; fn from_str(s: &str) -> Result { match s { $( $name => Ok(Self::$variant), $( $alias => Ok(Self::$variant), )* )* _ => Err(ParseColError::new(s)), } } } impl fmt::Display for Col { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { $( Self::$variant => write!(f, "{}", self.title()), )* } } } impl Col { pub fn name(self) -> &'static str { match self { $( Self::$variant => $name, )* } } pub fn title(self) -> &'static str { match self { $( Self::$variant => $title, )* } } pub fn aliases(self) -> &'static [&'static str] { match self { $( Self::$variant => &[$($alias,)*], )* } } pub fn is_default(self) -> bool { DEFAULT_COLS.contains(&self) } } }; } // definition of all columns and their names // in the --cols definition col_enum!( // syntax: // Variant name [aliases]: title [default] Id "id": "id", Dev "dev" "device" "device_id": "dev", Filesystem "fs" "filesystem": "filesystem" default, Label "label": "label", Type "type": "type" default, Remote "remote" "rem": "remote", Disk "disk" "dsk": "disk" default, Used "used": "used" default, Use "use": "use" default, UsePercent "use_percent": "use%", Free "free": "free" default, FreePercent "free_percent": "free%", Size "size": "size" default, InodesUsed "inodes_used" "iused": "used inodes", InodesUse "inodes" "ino" "inodes_use" "iuse": "inodes", InodesUsePercent "inodes_use_percent" "iuse_percent": "inodes%", InodesFree "inodes_free" "ifree": "free inodes", InodesCount "inodes_total" "inodes_count" "itotal": "inodes total", MountPoint "mount" "mount_point" "mp": "mount point" default, Uuid "uuid": "UUID", PartUuid "partuuid" "part_uuid": "PARTUUID", ); impl Col { pub fn header_align(self) -> Alignment { match self { Self::Label => Alignment::Left, Self::MountPoint => Alignment::Left, _ => Alignment::Center, } } pub fn content_align(self) -> Alignment { match self { Self::Id => Alignment::Right, Self::Dev => Alignment::Center, Self::Filesystem => Alignment::Left, Self::Label => Alignment::Left, Self::Type => Alignment::Center, Self::Remote => Alignment::Center, Self::Disk => Alignment::Center, Self::Used => Alignment::Right, Self::Use => Alignment::Right, Self::UsePercent => Alignment::Right, Self::Free => Alignment::Right, Self::FreePercent => Alignment::Right, Self::Size => Alignment::Right, Self::InodesUsed => Alignment::Right, Self::InodesUse => Alignment::Right, Self::InodesUsePercent => Alignment::Right, Self::InodesFree => Alignment::Right, Self::InodesCount => Alignment::Right, Self::MountPoint => Alignment::Left, Self::Uuid => Alignment::Left, Self::PartUuid => Alignment::Left, } } pub fn description(self) -> &'static str { match self { Self::Id => "mount point id", Self::Dev => "device id", Self::Filesystem => "filesystem", Self::Label => "volume label", Self::Type => "filesystem type", Self::Remote => "whether it's a remote filesystem", Self::Disk => "storage type", Self::Used => "size used", Self::Use => "usage graphical view", Self::UsePercent => "percentage of blocks used", Self::Free => "free bytes", Self::FreePercent => "percentage of free blocks", Self::Size => "total size", Self::InodesUsed => "number of inodes used", Self::InodesUse => "graphical view of inodes usage", Self::InodesUsePercent => "percentage of inodes used", Self::InodesFree => "number of free inodes", Self::InodesCount => "total count of inodes", Self::MountPoint => "mount point", Self::Uuid => "filesystem UUID", Self::PartUuid => "partition UUID", } } pub fn comparator(self) -> impl for<'a, 'b> FnMut(&'a Mount, &'b Mount) -> Ordering { match self { Self::Id => |a: &Mount, b: &Mount| a.info.id.cmp(&b.info.id), Self::Dev => |a: &Mount, b: &Mount| a.info.dev.cmp(&b.info.dev), Self::Filesystem => |a: &Mount, b: &Mount| a.info.fs.cmp(&b.info.fs), Self::Label => |a: &Mount, b: &Mount| match (&a.fs_label, &b.fs_label) { (Some(a), Some(b)) => a.cmp(b), (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (None, None) => Ordering::Equal, }, Self::Type => |a: &Mount, b: &Mount| a.info.fs_type.cmp(&b.info.fs_type), Self::Remote => |a: &Mount, b: &Mount| a.info.is_remote().cmp(&b.info.is_remote()), Self::Disk => |a: &Mount, b: &Mount| match (&a.disk, &b.disk) { (Some(a), Some(b)) => a.disk_type().to_lowercase().cmp(&b.disk_type().to_lowercase()), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::Used => |a: &Mount, b: &Mount| match (&a.stats(), &b.stats()) { (Some(a), Some(b)) => a.used().cmp(&b.used()), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::Use | Self::UsePercent => |a: &Mount, b: &Mount| match (&a.stats(), &b.stats()) { // the 'use' column shows the percentage of used blocks, so it makes sense // to sort by use_share for it // SAFETY: use_share() doesn't return NaN (Some(a), Some(b)) => a.use_share().partial_cmp(&b.use_share()).unwrap(), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::Free => |a: &Mount, b: &Mount| match (&a.stats(), &b.stats()) { (Some(a), Some(b)) => a.available().cmp(&b.available()), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::FreePercent => |a: &Mount, b: &Mount| match (&a.stats(), &b.stats()) { (Some(a), Some(b)) => b.use_share().partial_cmp(&a.use_share()).unwrap(), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::Size => |a: &Mount, b: &Mount| match (&a.stats(), &b.stats()) { (Some(a), Some(b)) => a.size().cmp(&b.size()), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::InodesUsed => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { (Some(a), Some(b)) => a.used().cmp(&b.used()), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::InodesUsePercent | Self::InodesUse => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { // SAFETY: use_share() doesn't return NaN (Some(a), Some(b)) => a.use_share().partial_cmp(&b.use_share()).unwrap(), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::InodesFree => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { (Some(a), Some(b)) => a.favail.cmp(&b.favail), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::InodesCount => |a: &Mount, b: &Mount| match (&a.inodes(), &b.inodes()) { (Some(a), Some(b)) => a.files.cmp(&b.files), (Some(_), None) => Ordering::Greater, (None, Some(_)) => Ordering::Less, (None, None) => Ordering::Equal, }, Self::MountPoint => |a: &Mount, b: &Mount| a.info.mount_point.cmp(&b.info.mount_point), Self::Uuid => |a: &Mount, b: &Mount| match (&a.uuid, &b.uuid) { (Some(a), Some(b)) => a.cmp(b), (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (None, None) => Ordering::Equal, }, Self::PartUuid => |a: &Mount, b: &Mount| match (&a.part_uuid, &b.part_uuid) { (Some(a), Some(b)) => a.cmp(b), (Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (None, None) => Ordering::Equal, }, } } pub fn default_sort_order(self) -> Order { match self { Self::Id => Order::Asc, Self::Dev => Order::Asc, Self::Filesystem => Order::Asc, Self::Label => Order::Asc, Self::Type => Order::Asc, Self::Remote => Order::Desc, Self::Disk => Order::Asc, Self::Used => Order::Asc, Self::Use => Order::Desc, Self::UsePercent => Order::Asc, Self::Free => Order::Asc, Self::FreePercent => Order::Desc, Self::Size => Order::Desc, Self::InodesUsed => Order::Asc, Self::InodesUse => Order::Asc, Self::InodesUsePercent => Order::Asc, Self::InodesFree => Order::Asc, Self::InodesCount => Order::Asc, Self::MountPoint => Order::Asc, Self::Uuid => Order::Asc, Self::PartUuid => Order::Asc, } } pub fn default_sort_col() -> Self { Self::Size } } #[derive(Debug)] pub struct ParseColError { /// the string which couldn't be parsed pub raw: String, } impl ParseColError { pub fn new>(s: S) -> Self { Self { raw: s.into() } } } impl fmt::Display for ParseColError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{:?} can't be parsed as a column; use 'dysk --list-cols' to see all column names", self.raw, ) } } impl std::error::Error for ParseColError {} dysk-cli-2.10.0/src/col_expr.rs000064400000000000000000000246501046102023000144020ustar 00000000000000use { crate::{ col::*, }, lfs_core::*, std::{ fmt, str::FromStr, }, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColOperator { Lower, LowerOrEqual, Like, Equal, NotEqual, GreaterOrEqual, Greater, } impl ColOperator { pub fn eval(self, a: T, b: T) -> bool { match self { Self::Lower => a < b, Self::LowerOrEqual => a <= b, Self::Equal | Self::Like => a == b, Self::NotEqual => a != b, Self::GreaterOrEqual => a >= b, Self::Greater => a > b, } } pub fn eval_option(self, a: Option, b: T) -> bool { match a { Some(a) => self.eval(a, b), None => false, } } pub fn eval_str(self, a: &str, b: &str) -> bool { match self { Self::Like => a.to_lowercase().contains(&b.to_lowercase()), _ => self.eval(a, b), } } pub fn eval_option_str(self, a: Option<&str>, b: &str) -> bool { match (a, self) { (Some(a), Self::Like) => a.to_lowercase().contains(&b.to_lowercase()), _ => self.eval_option(a, b), } } } /// A leaf in the filter expression tree, an expression which /// may return true or false for any filesystem #[derive(Debug, Clone, PartialEq)] pub struct ColExpr { col: Col, operator: ColOperator, value: String, } impl ColExpr { #[cfg(test)] pub fn new>(col: Col, operator: ColOperator, value: S) -> Self { Self { col, operator, value: value.into(), } } pub fn eval(&self, mount: &Mount) -> Result { Ok(match self.col { Col::Id => self.operator.eval( mount.info.id, self.value.parse::() .map_err(|_| EvalExprError::NotAnId(self.value.to_string()))?, ), Col::Dev => self.operator.eval( mount.info.dev, self.value.parse::() .map_err(|_| EvalExprError::NotADeviceId(self.value.to_string()))?, ), Col::Filesystem => self.operator.eval_str( &mount.info.fs, &self.value, ), Col::Label => self.operator.eval_option_str( mount.fs_label.as_deref(), &self.value, ), Col::Type => self.operator.eval_str( &mount.info.fs_type, &self.value, ), Col::Remote => self.operator.eval( mount.info.is_remote(), parse_bool(&self.value)?, ), Col::Disk => self.operator.eval_option_str( mount.disk.as_ref().map(|d| d.disk_type()), &self.value, ), Col::Used => self.operator.eval_option( mount.stats().as_ref().map(|s| s.used()), parse_integer(&self.value)?, ), Col::Use | Col::UsePercent => self.operator.eval_option( mount.stats().as_ref().map(|s| s.use_share()), parse_float(&self.value)?, ), Col::Free | Col::FreePercent => self.operator.eval_option( mount.stats().as_ref().map(|s| s.available()), parse_integer(&self.value)?, ), Col::Size => self.operator.eval_option( mount.stats().as_ref().map(|s| s.size()), parse_integer(&self.value)?, ), Col::InodesUsed => self.operator.eval_option( mount.inodes().as_ref().map(|i| i.used()), parse_integer(&self.value)?, ), Col::InodesUse | Col::InodesUsePercent => self.operator.eval_option( mount.inodes().as_ref().map(|i| i.use_share()), parse_float(&self.value)?, ), Col::InodesFree => self.operator.eval_option( mount.inodes().as_ref().map(|i| i.favail), parse_integer(&self.value)?, ), Col::InodesCount => self.operator.eval_option( mount.inodes().as_ref().map(|i| i.files), parse_integer(&self.value)?, ), Col::MountPoint => self.operator.eval_str( &mount.info.mount_point.to_string_lossy(), &self.value, ), Col::Uuid => self.operator.eval_option_str( mount.uuid.as_deref(), &self.value, ), Col::PartUuid => self.operator.eval_option_str( mount.part_uuid.as_deref(), &self.value, ), }) } } #[derive(Debug)] pub struct ParseExprError { /// the string which couldn't be parsed pub raw: String, /// why pub message: String, } impl ParseExprError { pub fn new, M: Into>(raw: R, message: M) -> Self { Self { raw: raw.into(), message: message.into(), } } } impl fmt::Display for ParseExprError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{:?} can't be parsed as an expression: {}", self.raw, self.message ) } } impl std::error::Error for ParseExprError {} impl FromStr for ColExpr { type Err = ParseExprError; fn from_str(input: &str) -> Result { let mut chars_indices = input.char_indices(); let mut op_idx = 0; for (idx, c) in &mut chars_indices { if c == '<' || c == '>' || c == '=' { op_idx = idx; break; } } if op_idx == 0 { return Err(ParseExprError::new(input, "Invalid expression; expected ")); } let mut val_idx = op_idx + 1; for (idx, c) in &mut chars_indices { if c != '<' && c != '>' && c != '=' { val_idx = idx; break; } } if val_idx == input.len() { return Err(ParseExprError::new(input, "no value")); } let col = &input[..op_idx]; let col = col.parse() .map_err(|e: ParseColError| ParseExprError::new(input, e.to_string()))?; let operator = match &input[op_idx..val_idx] { "<" => ColOperator::Lower, "<=" => ColOperator::LowerOrEqual, "=" => ColOperator::Like, "==" => ColOperator::Equal, "<>" => ColOperator::NotEqual, ">=" => ColOperator::GreaterOrEqual, ">" => ColOperator::Greater, op => { return Err(ParseExprError::new( input, format!("unknown operator: {:?}", op), )); } }; let value = &input[val_idx..]; let value = value.into(); Ok(Self { col, operator, value }) } } #[test] fn test_col_filter_parsing() { assert_eq!( "remote=false".parse::().unwrap(), ColExpr::new(Col::Remote, ColOperator::Like, "false"), ); assert_eq!( "size<32G".parse::().unwrap(), ColExpr::new(Col::Size, ColOperator::Lower, "32G"), ); } #[derive(Debug, PartialEq)] #[allow(clippy::enum_variant_names)] pub enum EvalExprError { NotANumber(String), NotAnId(String), NotADeviceId(String), NotABool(String), } impl EvalExprError { } impl fmt::Display for EvalExprError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NotANumber(s) => { write!(f, "{:?} can't be evaluated as a number", &s) } Self::NotAnId(s) => { write!(f, "{:?} can't be evaluated as an id", &s) } Self::NotADeviceId(s) => { write!(f, "{:?} can't be evaluated as a device id", &s) } Self::NotABool(s) => { write!(f, "{:?} can't be evaluated as a boolean", &s) } } } } impl std::error::Error for EvalExprError {} fn parse_bool(input: &str) -> Result { let s = input.to_lowercase(); match s.as_ref() { "x" | "t" | "true" | "1" | "y" | "yes" => Ok(true), "f" | "false" | "0" | "n" | "no" => Ok(false), _ => Err(EvalExprError::NotABool(input.to_string())), } } /// Parse numbers like "1234", "32G", "4kB", "54Gib", "1.2M" fn parse_integer(input: &str) -> Result { let s = input.to_lowercase(); let s = s.trim_end_matches('b'); let (s, binary) = match s.strip_suffix('i') { Some(s) => (s, true), None => (s, false), }; let cut = s.find(|c: char| !(c.is_ascii_digit() || c=='.')); let (digits, factor): (&str, u64) = match cut { Some(idx) => ( &s[..idx], match (&s[idx..], binary) { ("k", false) => 1000, ("k", true) => 1024, ("m", false) => 1000*1000, ("m", true) => 1024*1024, ("g", false) => 1000*1000*1000, ("g", true) => 1024*1024*1024, ("t", false) => 1000*1000*1000*1000, ("t", true) => 1024*1024*1024*1024, _ => { // it's not a number return Err(EvalExprError::NotANumber(input.to_string())); } } ), None => (s, 1), }; match digits.parse::() { Ok(n) => Ok((n * factor as f64).ceil() as u64), _ => Err(EvalExprError::NotANumber(input.to_string())), } } #[test] fn test_parse_integer(){ assert_eq!(parse_integer("33"), Ok(33)); assert_eq!(parse_integer("55G"), Ok(55_000_000_000)); assert_eq!(parse_integer("1.23kiB"), Ok(1260)); } /// parse numbers like "0.25", "50%" fn parse_float(input: &str) -> Result { let s = input.to_lowercase(); let (s, percent) = match s.strip_suffix('%') { Some(s) => (s, true), None => (s.as_str(), false), }; let mut n = s.parse::() .map_err(|_| EvalExprError::NotANumber(input.to_string()))?; if percent { n /= 100.0; } Ok(n) } #[test] fn test_parse_float(){ assert_eq!(parse_float("50%").unwrap().to_string(), "0.5".to_string()); } dysk-cli-2.10.0/src/cols.rs000064400000000000000000000214561046102023000135300ustar 00000000000000use { crate::col::*, std::{ str::FromStr, }, }; /// Sequence of columns, ordered #[derive(Debug, Clone, PartialEq)] pub struct Cols(pub Vec); impl Default for Cols { fn default() -> Self { Self(DEFAULT_COLS.to_vec()) } } impl Cols { #[cfg(test)] pub fn new>>(v: V) -> Self { Self(v.into()) } pub fn empty() -> Self { Self(Vec::new()) } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn contains(&self, tbl: Col) -> bool { self.0.contains(&tbl) } pub fn remove(&mut self, removed: Col) { self.0.retain(|&f| f!=removed); } /// Add a col, preventing duplicates /// (may be used when the col is present to reorder) pub fn add(&mut self, added: Col) { self.remove(added); self.0.push(added); } /// Add the columns of the set, except when they're /// already present /// /// This makes it possible to add a set while keeping /// the order of the previous columns, for example /// `dysk -c disk+` pub fn add_set(&mut self, col_set: &[Col]) { if self.0 == ALL_COLS { for &col in col_set { self.add(col); } } else { for &col in col_set { if !self.contains(col) { self.add(col); } } } } pub fn remove_set(&mut self, col_set: &[Col]) { for &col in col_set { self.remove(col); } } pub fn cols(&self) -> &[Col] { &self.0 } } impl FromStr for Cols { type Err = ParseColError; fn from_str(value: &str) -> Result { let value = value.trim(); let mut tokens: Vec = Vec::new(); let mut must_create = true; for c in value.chars() { if c.is_alphabetic() || c == '_' { if must_create { tokens.push(c.into()); must_create = false; } else { let len = tokens.len(); tokens[len-1].push(c); } } else { tokens.push(c.into()); must_create = true; } } let mut cols = if let Some(first_token) = tokens.get(0) { if first_token == "+" || first_token == "-" { // if it starts with an addition or removal, the // default set is implied Cols::default() } else { Cols::empty() } } else { return Ok(Self::default()); }; let mut negative = false; for token in &tokens { match token.as_ref() { "-" => { negative = true; } "+" | "," | " " => {} "all" => { if negative { cols = Cols::empty(); negative = false; } else { // if we add all to something, it means the already // present one are meant to be first for &col in ALL_COLS { if !cols.contains(col) { cols.add(col); } } } } "default" => { if negative { cols.remove_set(DEFAULT_COLS); negative = false; } else { cols.add_set(DEFAULT_COLS); } } _ => { let col: Col = token.parse()?; if negative { cols.remove(col); negative = false; } else { cols.add(col); } } } } match tokens.last().map(|s| s.as_ref()) { Some("-") => { cols.remove_set(DEFAULT_COLS); } Some("+") => { cols.add_set(DEFAULT_COLS); } _ => {} } Ok(cols) } } #[cfg(test)] mod cols_parsing { use { super::*, super::Col::*, }; fn check>>(s: &str, v: V) { println!("cols definition: {s:?}"); let from_str: Cols = s.parse().unwrap(); let from_vec: Cols = Cols::new(v); assert_eq!(from_str, from_vec); } #[test] fn bad_cols(){ assert_eq!( "nothing".parse::().unwrap_err().to_string(), r#""nothing" can't be parsed as a column; use 'dysk --list-cols' to see all column names"#, ); } #[test] fn explicit_cols() { check( "dev", vec![Dev], ); check( "dev,free,used", vec![Dev, Free, Used], ); check( "dev+free + used", vec![Dev, Free, Used], ); check( " dev free used ", vec![Dev, Free, Used], ); check( "all", ALL_COLS, ); } #[test] fn algebraic_cols() { check( "all - dev -inodes + label", vec![Id, Filesystem, Type, Remote, Disk, Used, Use, UsePercent, Free, FreePercent, Size, InodesUsed, InodesUsePercent, InodesFree, InodesCount, MountPoint, Label], ); check( "dev + dev +disk - use + size", vec![Dev, Disk, Size], ); check( "all-default+use", vec![Id, Dev, Label, Remote, UsePercent, FreePercent, InodesUsed, InodesUse, InodesUsePercent, InodesFree, InodesCount, Use], ); check( "all+default", // special: all but default at the end vec![Id, Dev, Label, Remote, UsePercent, FreePercent, InodesUsed, InodesUse, InodesUsePercent, InodesFree, InodesCount, Filesystem, Type, Disk, Used, Use, Free, Size, MountPoint] ); check( "fs dev all", // we want all column but fs and dev at the start vec![Filesystem, Dev, Id, Label, Type, Remote, Disk, Used, Use, UsePercent, Free, FreePercent, Size, InodesUsed, InodesUse, InodesUsePercent, InodesFree, InodesCount, MountPoint], ); check( "fs dev all -id-disk", vec![Filesystem, Dev, Label, Type, Remote, Used, Use, UsePercent, Free, FreePercent, Size, InodesUsed, InodesUse, InodesUsePercent, InodesFree, InodesCount, MountPoint], ); } #[test] fn cols_from_default() { check( "", DEFAULT_COLS, ); check( "-dev", // no impact as dev isn't in defaults DEFAULT_COLS, ); check( "default", DEFAULT_COLS, ); check( "-default", // not really useful vec![], ); check( "default-dev", // no impact as dev isn't in defaults DEFAULT_COLS, ); check( "+dev", vec![Filesystem, Type, Disk, Used, Use, Free, Size, MountPoint, Dev] ); check( "dev+", vec![Dev, Filesystem, Type, Disk, Used, Use, Free, Size, MountPoint] ); check( "all-", vec![Id, Dev, Label, Remote, UsePercent, FreePercent, InodesUsed, InodesUse, InodesUsePercent, InodesFree, InodesCount], ); check( "-size+inodes_free+", vec![Filesystem, Type, Disk, Used, Use, Free, MountPoint, InodesFree, Size] ); check( "+dev-size+inodes_use", vec![Filesystem, Type, Disk, Used, Use, Free, MountPoint, Dev, InodesUse] ); check( "-use-type", vec![Filesystem, Disk, Used, Free, Size, MountPoint] ); check( "default+dev", vec![Filesystem, Type, Disk, Used, Use, Free, Size, MountPoint, Dev] ); check( "default,size+use", // just reordering vec![Filesystem, Type, Disk, Used, Free, MountPoint, Size, Use] ); check( "dev default", vec![Dev, Filesystem, Type, Disk, Used, Use, Free, Size, MountPoint] ); check( "size dev default -disk", vec![Size, Dev, Filesystem, Type, Used, Use, Free, MountPoint] ); check( "default-fs+inodes", vec![Type, Disk, Used, Use, Free, Size, MountPoint, InodesUse] ); check( "+inodes_used+inodes_free", vec![Filesystem, Type, Disk, Used, Use, Free, Size, MountPoint, InodesUsed, InodesFree] ); } } dysk-cli-2.10.0/src/csv.rs000064400000000000000000000075221046102023000133610ustar 00000000000000use { crate::{ Args, col::Col, }, lfs_core::*, std::{ fmt::Display, io::Write, }, }; /// Utility to write in CSV struct Csv { separator: char, w: W, } impl Csv { pub fn new(separator: char, w: W) -> Self { Self { separator, w } } pub fn cell(&mut self, content: D) -> Result<(), std::io::Error> { let s = content.to_string(); let needs_quotes = s.contains(self.separator) || s.contains('"') || s.contains('\n'); if needs_quotes { write!(self.w, "\"")?; for c in s.chars() { if c == '"' { write!(self.w, "\"\"")?; } else { write!(self.w, "{}", c)?; } } write!(self.w, "\"")?; } else { write!(self.w, "{}", s)?; } write!(self.w, "{}", self.separator) } pub fn cell_opt(&mut self, content: Option) -> Result<(), std::io::Error> { if let Some(c) = content { self.cell(c) } else { write!(self.w, "{}", self.separator) } } pub fn end_line(&mut self) -> Result<(), std::io::Error> { writeln!(self.w) } } pub fn print(mounts: &[&Mount], args: &Args) -> Result<(), std::io::Error> { let units = args.units; let mut csv = Csv::new(args.csv_separator, std::io::stdout()); for col in args.cols.cols() { csv.cell(col.title())?; } csv.end_line()?; for mount in mounts { for col in args.cols.cols() { match col { Col::Id => csv.cell(mount.info.id), Col::Dev => csv.cell(format!("{}:{}", mount.info.dev.major, mount.info.dev.minor)), Col::Filesystem => csv.cell(&mount.info.fs), Col::Label => csv.cell_opt(mount.fs_label.as_ref()), Col::Type => csv.cell(&mount.info.fs_type), Col::Remote => csv.cell(if mount.info.is_remote() { "yes" } else { "no" }), Col::Disk => csv.cell_opt(mount.disk.as_ref().map(|d| d.disk_type())), Col::Used => csv.cell_opt(mount.stats().map(|s| units.fmt(s.used()))), Col::Use => csv.cell_opt(mount.stats().map(|s| s.use_share())), Col::UsePercent => csv.cell_opt(mount.stats().map(|s| format!("{:.0}%", 100.0 * s.use_share()))), Col::Free => csv.cell_opt(mount.stats().map(|s| units.fmt(s.available()))), Col::FreePercent => csv.cell_opt(mount.stats().map(|s| format!("{:.0}%", 100.0 * (1.0 - s.use_share())))), Col::Size => csv.cell_opt(mount.stats().map(|s| units.fmt(s.size()))), Col::InodesUsed => csv.cell_opt(mount.inodes().map(|i| i.used())), Col::InodesUse => csv.cell_opt(mount.inodes().map(|i| i.use_share())), Col::InodesUsePercent => csv.cell_opt(mount.inodes().map(|i| format!("{:.0}%", 100.0 * i.use_share()))), Col::InodesFree => csv.cell_opt(mount.inodes().map(|i| i.favail)), Col::InodesCount => csv.cell_opt(mount.inodes().map(|i| i.files)), Col::MountPoint => csv.cell(&mount.info.mount_point.to_string_lossy()), Col::Uuid => csv.cell(mount.uuid.as_ref().map_or("", |v| v)), Col::PartUuid => csv.cell(mount.part_uuid.as_ref().map_or("", |v| v)), }?; } csv.end_line()?; } Ok(()) } #[test] fn test_csv() { use std::io::Cursor; let mut w = Cursor::new(Vec::new()); let mut csv = Csv::new(';', &mut w); csv.cell("1;2;3").unwrap(); csv.cell("\"").unwrap(); csv.cell("").unwrap(); csv.end_line().unwrap(); csv.cell(3).unwrap(); let s = String::from_utf8(w.into_inner()).unwrap(); assert_eq!( s, r#""1;2;3";"""";; 3;"#, ); } dysk-cli-2.10.0/src/filter.rs000064400000000000000000000042431046102023000140500ustar 00000000000000use { crate::{ col_expr::*, }, bet::*, lfs_core::*, std::{ str::FromStr, }, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum BoolOperator { And, Or, Not, } #[derive(Debug, Default, Clone)] pub struct Filter { expr: BeTree, } impl Filter { #[allow(clippy::match_like_matches_macro)] pub fn eval(&self, mount: &Mount) -> Result { self.expr.eval_faillible( // leaf evaluation |col_expr| col_expr.eval(mount), // bool operation |op, a, b| match (op, b) { (BoolOperator::And, Some(b)) => Ok(a & b), (BoolOperator::Or, Some(b)) => Ok(a | b), (BoolOperator::Not, None) => Ok(!a), _ => { unreachable!() } }, // when to short-circuit |op, a| match (op, a) { (BoolOperator::And, false) => true, (BoolOperator::Or, true) => true, _ => false, }, ).map(|b| b.unwrap_or(true)) } pub fn filter<'m>(&self, mounts: &'m[Mount]) -> Result, EvalExprError> { let mut filtered = Vec::new(); for mount in mounts { if self.eval(mount)? { filtered.push(mount); } } Ok(filtered) } } impl FromStr for Filter { type Err = ParseExprError; fn from_str(input: &str) -> Result { // we start by reading the global structure let mut expr: BeTree = BeTree::new(); for c in input.chars() { match c { '&' => expr.push_operator(BoolOperator::And), '|' => expr.push_operator(BoolOperator::Or), '!' => expr.push_operator(BoolOperator::Not), ' ' => {}, '(' => expr.open_par(), ')' => expr.close_par(), _ => expr.mutate_or_create_atom(String::new).push(c), } } // then we parse each leaf let expr = expr.try_map_atoms(|raw| raw.parse())?; Ok(Self { expr }) } } dysk-cli-2.10.0/src/help.rs000064400000000000000000000050431046102023000135120ustar 00000000000000use { crate::{ args::*, }, clap::CommandFactory, }; static INTRO_TEMPLATE: &str = " **dysk** displays filesystem information in a pretty table. Complete documentation at https://dystroy.org/dysk "; static EXAMPLES_TEMPLATE: &str = " **Examples:** ${examples **${example-number})** ${example-title}: `${example-cmd}` ${example-comments} } "; static EXAMPLES: &[Example] = &[ Example::new( "Standard overview of your usual disks", "dysk", "" ), Example::new( "List all filesystems", "dysk -a", "" ), Example::new( "Display inodes information", "dysk -c +inodes", "" ), Example::new( "Add columns of your choice", "dysk -c label+dev+", "You may add columns before, after, or at any other place. \ You can change the column order too. \ See https://dystroy.org/dysk/table#columns\n" ), Example::new( "See the disk of the current directory", "dysk .", "" ), Example::new( "Filter for low space", "dysk -f 'use > 65% | free < 50G'", "" ), Example::new( "Filter to exclude SSD disks", "dysk -f 'disk <> SSD'", "" ), Example::new( "Complex filter", "dysk -f '(type=xfs & remote=no) | size > 5T'", "" ), Example::new( "Export as JSON", "dysk -j", "" ), Example::new( "Sort by free size", "dysk -s free", "Add `-desc` to the column name to sort in reverse." ), ]; pub fn print(ascii: bool) { let mut printer = clap_help::Printer::new(Args::command()) .with("introduction", INTRO_TEMPLATE) .without("author"); printer.template_keys_mut().push("examples"); printer.set_template("examples", EXAMPLES_TEMPLATE); if ascii { printer.skin_mut().limit_to_ascii(); } for (i, example) in EXAMPLES.iter().enumerate() { printer .expander_mut() .sub("examples") .set("example-number", i + 1) .set("example-title", example.title) .set("example-cmd", example.cmd) .set_md("example-comments", example.comments); } printer.print_help(); } struct Example { title: &'static str, cmd: &'static str, comments: &'static str, } impl Example { pub const fn new( title: &'static str, cmd: &'static str, comments: &'static str, ) -> Self { Self { title, cmd, comments } } } dysk-cli-2.10.0/src/json.rs000064400000000000000000000043571046102023000135420ustar 00000000000000use { crate::units::Units, lfs_core::*, serde_json::{json, Value}, }; pub fn output_value(mounts: &[&Mount], units: Units) -> Value { Value::Array( mounts .iter() .map(|mount| { let stats = mount.stats().map(|s| { let inodes = s.inodes.as_ref().map(|inodes| { json!({ "files": inodes.files, "free": inodes.ffree, "avail": inodes.favail, "used-percent": format!("{:.0}%", 100.0*inodes.use_share()), }) }); json!({ "bsize": s.bsize, "blocks": s.blocks, "bfree": s.bfree, "bavail": s.bavail, "size": units.fmt(s.size()), "used": units.fmt(s.used()), "used-percent": format!("{:.0}%", 100.0*s.use_share()), "available": units.fmt(s.available()), "inodes": inodes, }) }); let disk = mount.disk.as_ref().map(|d| { json!({ "type": d.disk_type(), "rotational": d.rotational, "removable": d.removable, "crypted": d.crypted, "ram": d.ram, }) }); json!({ "id": mount.info.id, "dev": { "major": mount.info.dev.major, "minor": mount.info.dev.minor, }, "fs": mount.info.fs, "fs-label": mount.fs_label, "fs-type": mount.info.fs_type, "mount-point": mount.info.mount_point, "disk": disk, "stats": stats, "bound": mount.info.bound, "remote": mount.info.is_remote(), "unreachable": mount.is_unreachable(), }) }) .collect(), ) } dysk-cli-2.10.0/src/lib.rs000064400000000000000000000041611046102023000133300ustar 00000000000000pub mod args; pub mod col; pub mod col_expr; pub mod cols; pub mod csv; pub mod filter; pub mod help; pub mod json; pub mod list_cols; pub mod normal; pub mod order; pub mod sorting; pub mod table; pub mod units; use { crate::{ args::*, normal::*, }, clap::Parser, std::{ fs, os::unix::fs::MetadataExt, }, }; #[allow(clippy::match_like_matches_macro)] pub fn run() { let args = Args::parse(); if args.version { println!("dysk {}", env!("CARGO_PKG_VERSION")); return; } if args.help { help::print(args.ascii); return; } if args.list_cols { list_cols::print(args.color(), args.ascii); return; } let mut options = lfs_core::ReadOptions::default(); options.remote_stats(args.remote_stats.unwrap_or_else(||true)); let mut mounts = match lfs_core::read_mounts(&options) { Ok(mounts) => mounts, Err(e) => { eprintln!("Error reading mounts: {}", e); return; } }; if !args.all { mounts.retain(is_normal); } if let Some(path) = &args.path { let md = match fs::metadata(path) { Ok(md) => md, Err(e) => { eprintln!("Can't read {:?} : {}", path, e); return; } }; let dev = lfs_core::DeviceId::from(md.dev()); mounts.retain(|m| m.info.dev == dev); } args.sort.sort(&mut mounts); let mounts = match args.filter.clone().unwrap_or_default().filter(&mounts) { Ok(mounts) => mounts, Err(e) => { eprintln!("Error in filter evaluation: {}", e); return; } }; if args.csv { csv::print(&mounts, &args).expect("writing csv failed"); return; } if args.json { println!( "{}", serde_json::to_string_pretty(&json::output_value(&mounts, args.units)).unwrap() ); return; } if mounts.is_empty() { println!("no mount to display - try\n dysk -a"); return; } table::print(&mounts, args.color(), &args); } dysk-cli-2.10.0/src/list_cols.rs000064400000000000000000000022421046102023000145530ustar 00000000000000use { crate::col::ALL_COLS, termimad::{ minimad::OwningTemplateExpander, MadSkin, }, }; static MD: &str = r#" The `--cols` launch argument lets you specify the columns of the **dysk** table. You can give the explicit list of all columns: `dysk -c dev+fs` You can add columns to the default ones: `dysk -c +dev+size` Complete syntax at https://dystroy.org/dysk/table |:-:|:-:|:-:|:- |column | aliases | default | content |:-:|:-:|:-:|- ${column |${name}|${aliases}|${default}|${description} } |- "#; /// Print an help text describing columns pub fn print(color: bool, ascii: bool) { let mut expander = OwningTemplateExpander::new(); expander.set_default(""); for &col in ALL_COLS { expander.sub("column") .set("name", col.name()) .set("aliases", col.aliases().join(", ")) .set("default", if col.is_default() { "x" } else { "" }) .set("description", col.description()); } let mut skin = if color { MadSkin::default() } else { MadSkin::no_style() }; if ascii { skin.limit_to_ascii(); } skin.print_owning_expander_md(&expander, MD); } dysk-cli-2.10.0/src/normal.rs000064400000000000000000000010501046102023000140440ustar 00000000000000use lfs_core::Mount; /// Determine whether the mounted filesystem is "normal", which /// means it should be listed in standard pub fn is_normal(m: &Mount) -> bool { ( m.stats().is_some() || m.is_unreachable() ) && ( m.disk.is_some() // by default only fs with disks are shown || m.info.fs_type == "zfs" // unless it's zfs - see https://github.com/Canop/dysk/issues/32 || m.info.is_remote() ) && !m.info.bound // removing bound mounts && m.info.fs_type != "squashfs" // quite ad-hoc... } dysk-cli-2.10.0/src/order.rs000064400000000000000000000017411046102023000136760ustar 00000000000000use { std::{ fmt, str::FromStr, }, }; /// one of the two sorting directions #[derive(Debug, Clone, Copy, PartialEq)] pub enum Order { Asc, Desc, } #[derive(Debug)] pub struct ParseOrderError { /// the string which couldn't be parsed pub raw: String, } impl ParseOrderError { pub fn new>(s: S) -> Self { Self { raw: s.into() } } } impl fmt::Display for ParseOrderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?} can't be parsed as a sort order. Use 'asc' or 'desc' (or nothing)", self.raw) } } impl std::error::Error for ParseOrderError {} impl FromStr for Order { type Err = ParseOrderError; fn from_str(s: &str) -> Result { let s = s.to_lowercase(); match s.as_ref() { "a" | "asc" => Ok(Self::Asc), "d" | "desc" => Ok(Self::Desc), _ => Err(ParseOrderError::new(s)) } } } dysk-cli-2.10.0/src/sorting.rs000064400000000000000000000041461046102023000142520ustar 00000000000000use { crate::{ col::Col, order::Order, }, lfs_core::Mount, std::{ error, fmt, str::FromStr, }, }; /// Sorting directive: the column and the order (asc or desc) #[derive(Debug, Clone, Copy, PartialEq)] pub struct Sorting { col: Col, order: Order, } impl Default for Sorting { fn default() -> Self { let col = Col::default_sort_col(); let order = col.default_sort_order(); Self { col, order } } } impl Sorting { pub fn sort(self, mounts: &mut [Mount]) { let comparator = self.col.comparator(); mounts.sort_by(comparator); if self.order == Order::Desc { mounts.reverse(); } } } #[derive(Debug)] pub struct ParseSortingError { raw: String, reason: String, } impl ParseSortingError { pub fn new, E: ToString>(raw: S, reason: E) -> Self { Self { raw: raw.into(), reason: reason.to_string(), } } } impl fmt::Display for ParseSortingError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?} can't be parsed as a sort expression because {}", self.raw, self.reason) } } impl error::Error for ParseSortingError {} impl FromStr for Sorting { type Err = ParseSortingError; fn from_str(s: &str) -> Result { let cut_idx_len = s .char_indices() .find(|(_idx, c)| c.is_whitespace() || *c == '-') .map(|(idx, c)| (idx, c.len_utf8())); let (s_col, s_order) = match cut_idx_len { Some((idx, len)) => (&s[..idx], Some(&s[idx+len..])), None => (s, None), }; let col: Col = s_col.parse() .map_err(|pce| ParseSortingError::new(s, Box::new(pce)))?; let order = match s_order { Some(s_order) => { s_order.parse() .map_err(|poe| ParseSortingError::new(s, Box::new(poe)))? } None => { col.default_sort_order() } }; Ok(Self { col, order }) } } dysk-cli-2.10.0/src/table.rs000064400000000000000000000123161046102023000136520ustar 00000000000000use { crate::{ Args, col::Col, }, lfs_core::*, termimad::{ crossterm::style::Color::*, minimad::{self, OwningTemplateExpander, TableBuilder}, CompoundStyle, MadSkin, ProgressBar, }, }; // those colors are chosen to be "redish" for used, "greenish" for available // and, most importantly, to work on both white and black backgrounds. If you // find a better combination, please show me. static USED_COLOR: u8 = 209; static AVAI_COLOR: u8 = 65; static SIZE_COLOR: u8 = 172; static BAR_WIDTH: usize = 5; static INODES_BAR_WIDTH: usize = 5; pub fn print(mounts: &[&Mount], color: bool, args: &Args) { if args.cols.is_empty() { return; } let units = args.units; let mut expander = OwningTemplateExpander::new(); expander.set_default(""); for mount in mounts { let sub = expander .sub("rows") .set("id", mount.info.id) .set("dev-major", mount.info.dev.major) .set("dev-minor", mount.info.dev.minor) .set("filesystem", &mount.info.fs) .set("disk", mount.disk.as_ref().map_or("", |d| d.disk_type())) .set("type", &mount.info.fs_type) .set("mount-point", mount.info.mount_point.to_string_lossy()) .set_option("uuid", mount.uuid.as_ref()) .set_option("part_uuid", mount.part_uuid.as_ref()); if let Some(label) = &mount.fs_label { sub.set("label", label); } if mount.info.is_remote() { sub.set("remote", "x"); } if let Some(stats) = mount.stats() { let use_share = stats.use_share(); let free_share = 1.0 - use_share; sub .set("size", units.fmt(stats.size())) .set("used", units.fmt(stats.used())) .set("use-percents", format!("{:.0}%", 100.0 * use_share)) .set_md("bar", progress_bar_md(use_share, BAR_WIDTH, args.ascii)) .set("free", units.fmt(stats.available())) .set("free-percents", format!("{:.0}%", 100.0 * free_share)); if let Some(inodes) = &stats.inodes { let iuse_share = inodes.use_share(); sub .set("inodes", inodes.files) .set("iused", inodes.used()) .set("iuse-percents", format!("{:.0}%", 100.0 * iuse_share)) .set_md("ibar", progress_bar_md(iuse_share, INODES_BAR_WIDTH, args.ascii)) .set("ifree", inodes.favail); } } else if mount.is_unreachable() { sub.set("use-error", "unreachable"); } } let mut skin = if color { make_colored_skin() } else { MadSkin::no_style() }; if args.ascii { skin.limit_to_ascii(); } let mut tbl = TableBuilder::default(); for col in args.cols.cols() { tbl.col( minimad::Col::new( col.title(), match col { Col::Id => "${id}", Col::Dev => "${dev-major}:${dev-minor}", Col::Filesystem => "${filesystem}", Col::Label => "${label}", Col::Disk => "${disk}", Col::Type => "${type}", Col::Remote => "${remote}", Col::Used => "~~${used}~~", Col::Use => "~~${use-percents}~~ ${bar}~~${use-error}~~", Col::UsePercent => "~~${use-percents}~~", Col::Free => "*${free}*", Col::FreePercent => "*${free-percents}*", Col::Size => "**${size}**", Col::InodesFree => "*${ifree}*", Col::InodesUsed => "~~${iused}~~", Col::InodesUse => "~~${iuse-percents}~~ ${ibar}", Col::InodesUsePercent => "~~${iuse-percents}~~", Col::InodesCount => "**${inodes}**", Col::MountPoint => "${mount-point}", Col::Uuid => "${uuid}", Col::PartUuid => "${part_uuid}", } ) .align_content(col.content_align()) .align_header(col.header_align()) ); } skin.print_owning_expander_md(&expander, &tbl); } fn make_colored_skin() -> MadSkin { MadSkin { bold: CompoundStyle::with_fg(AnsiValue(SIZE_COLOR)), // size inline_code: CompoundStyle::with_fgbg(AnsiValue(USED_COLOR), AnsiValue(AVAI_COLOR)), // use bar strikeout: CompoundStyle::with_fg(AnsiValue(USED_COLOR)), // use% italic: CompoundStyle::with_fg(AnsiValue(AVAI_COLOR)), // available ..Default::default() } } fn progress_bar_md( share: f64, bar_width: usize, ascii: bool, ) -> String { if ascii { let count = (share * bar_width as f64).round() as usize; let bar: String = std::iter::repeat('=') .take(count).collect(); let no_bar: String = std::iter::repeat('-') .take(bar_width-count).collect(); format!("~~{}~~*{}*", bar, no_bar) } else { let pb = ProgressBar::new(share as f32, bar_width); format!("`{: Self { Self::Si } } impl FromStr for Units { type Err = String; fn from_str(value: &str) -> Result { match value.to_lowercase().as_ref() { "si" => Ok(Self::Si), "binary" => Ok(Self::Binary), "bytes" => Ok(Self::Bytes), _ => Err(format!("Illegal value: {:?} - valid values are 'SI', 'binary', and 'bytes'", value)), } } } static PREFIXES: &[char] = &['K', 'M', 'G', 'T', 'P']; impl Units { pub fn fmt(self, size: u64) -> String { match self { Self::Si => file_size::fit_4(size), Self::Binary => { if size < 10_000 { size.to_string() } else { let i = size.ilog2() / 10u32; let idx = i as usize - 1; let size = size as f64; if idx >= PREFIXES.len() { "huge".to_string() } else { let v = size / (1024u64.pow(i) as f64); if v >= 10f64 { format!("{:.0}{}i", v.round(), PREFIXES[idx]) } else { format!("{:.1}{}i", v, PREFIXES[idx]) } } } } Self::Bytes => { let mut rev: Vec = Vec::new(); for (i, c) in size.to_string().chars().rev().enumerate() { if i>0 && i%3==0 { rev.push(','); } rev.push(c); } rev.drain(..).rev().collect() } } } } #[test] fn test_fmt_binary() { fn check(v: u64, s: &str) { assert_eq!(&Units::Binary.fmt(v), s); } check(0, "0"); check(1, "1"); check(456, "456"); check(1456, "1456"); check(9_999, "9999"); check(10_000, "9.8Ki"); check(12_345, "12Ki"); check(123_456, "121Ki"); check(1_000_000_000, "954Mi"); check(1_073_741_824, "1.0Gi"); check(1_234_567_890, "1.1Gi"); } #[test] fn test_fmt_bytes() { fn check(v: u64, s: &str) { assert_eq!(&Units::Bytes.fmt(v), s); } check(0, "0"); check(1, "1"); check(456, "456"); check(1456, "1,456"); check(9_999, "9,999"); check(10_000, "10,000"); check(12_345, "12,345"); check(123_456, "123,456"); check(1_234_567, "1,234,567"); check(1_000_000_000, "1,000,000,000"); check(1_234_567_890, "1,234,567,890"); }