use std::path::PathBuf;
use std::time::UNIX_EPOCH;
use bytesize::ByteSize;
use lazy_regex::{Lazy, Regex};
use remotefs::File;
use unicode_width::UnicodeWidthStr;
#[cfg(posix)]
use uzers::{get_group_by_gid, get_user_by_uid};
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
use crate::utils::path::diff_paths;
use crate::utils::string::secure_substring;
type FmtCallback = fn(&Formatter, &File, &str, &str, Option<&usize>, Option<&String>) -> String;
const FMT_KEY_ATIME: &str = "ATIME";
const FMT_KEY_CTIME: &str = "CTIME";
const FMT_KEY_GROUP: &str = "GROUP";
const FMT_KEY_MTIME: &str = "MTIME";
const FMT_KEY_NAME: &str = "NAME";
const FMT_KEY_PATH: &str = "PATH";
const FMT_KEY_PEX: &str = "PEX";
const FMT_KEY_SIZE: &str = "SIZE";
const FMT_KEY_SYMLINK: &str = "SYMLINK";
const FMT_KEY_USER: &str = "USER";
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
static FMT_KEY_REGEX: Lazy<Regex> = lazy_regex!(r"\{(.*?)\}");
static FMT_ATTR_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?");
struct CallChainBlock {
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
next_block: Option<Box<CallChainBlock>>,
}
impl CallChainBlock {
pub fn new(
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) -> Self {
CallChainBlock {
func,
prefix,
fmt_len,
fmt_extra,
next_block: None,
}
}
pub fn next(&self, fmt: &Formatter, fsentry: &File, cur_str: &str) -> String {
let new_str: String = (self.func)(
fmt,
fsentry,
cur_str,
self.prefix.as_str(),
self.fmt_len.as_ref(),
self.fmt_extra.as_ref(),
);
match &self.next_block {
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
None => new_str,
}
}
pub fn push(
&mut self,
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) {
match &mut self.next_block {
None => {
self.next_block = Some(Box::new(CallChainBlock::new(
func, prefix, fmt_len, fmt_extra,
)));
}
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
}
}
}
pub struct Formatter {
call_chain: CallChainBlock,
}
impl Default for Formatter {
fn default() -> Self {
Formatter {
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
}
}
}
impl Formatter {
pub fn new(fmt_str: &str) -> Self {
Formatter {
call_chain: Self::make_callchain(fmt_str),
}
}
pub fn fmt(&self, fsentry: &File) -> String {
self.call_chain.next(self, fsentry, "")
}
fn fmt_atime(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
let datetime: String = fmt_time(
fsentry.metadata().accessed.unwrap_or(UNIX_EPOCH),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
fn fmt_ctime(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
let datetime: String = fmt_time(
fsentry.metadata().created.unwrap_or(UNIX_EPOCH),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
fn fmt_group(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
#[cfg(posix)]
let group: String = match fsentry.metadata().gid {
Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => 0.to_string(),
};
#[cfg(win)]
let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(),
None => 0.to_string(),
};
format!(
"{}{}{:0width$}",
cur_str,
prefix,
group,
width = fmt_len.unwrap_or(&12)
)
}
fn fmt_mtime(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
let datetime: String = fmt_time(
fsentry.metadata().modified.unwrap_or(UNIX_EPOCH),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
fn fmt_name(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 24,
};
let name = fsentry.name();
let last_idx: usize = match fsentry.is_dir() {
true => file_len - 2,
false => file_len - 1,
};
let mut name: String = match name.width() >= file_len {
false => name,
true => format!("{}…", secure_substring(&name, 0, last_idx)),
};
if fsentry.is_dir() {
name.push('/');
}
format!("{cur_str}{prefix}{name:0file_len$}")
}
fn fmt_path(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
let p = match fmt_extra {
None => fsentry.path().to_path_buf(),
Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path())
.unwrap_or_else(|| fsentry.path().to_path_buf()),
};
format!(
"{}{}{}",
cur_str,
prefix,
match fmt_len {
None => p.display().to_string(),
Some(len) => fmt_path_elide(p.as_path(), *len),
}
)
}
fn fmt_pex(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
let mut pex: String = String::with_capacity(10);
let file_type: char = match fsentry.metadata().symlink.is_some() {
true => 'l',
false => match fsentry.is_dir() {
true => 'd',
false => '-',
},
};
pex.push(file_type);
match fsentry.metadata().mode {
None => pex.push_str("?????????"),
Some(mode) => pex.push_str(
format!(
"{}{}{}",
fmt_pex(mode.user()),
fmt_pex(mode.group()),
fmt_pex(mode.others())
)
.as_str(),
),
}
format!("{cur_str}{prefix}{pex:10}")
}
fn fmt_size(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
if fsentry.is_file() {
let size: ByteSize = ByteSize(fsentry.metadata().size);
let mut fmt = size.display().si().to_string();
let pad = 10usize.saturating_sub(fmt.len());
for _ in 0..pad {
fmt.push(' ');
}
format!("{cur_str}{prefix}{fmt}")
} else if fsentry.metadata().symlink.is_some() {
match fsentry.metadata().symlink.as_ref() {
Some(symlink) => {
let size = ByteSize(symlink.to_string_lossy().len() as u64);
let mut fmt = size.display().si().to_string();
let pad = 10usize.saturating_sub(fmt.len());
for _ in 0..pad {
fmt.push(' ');
}
format!("{cur_str}{prefix}{fmt}")
}
None => format!("{cur_str}{prefix} "),
}
} else {
format!("{cur_str}{prefix} ")
}
}
fn fmt_symlink(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 21,
};
match fsentry.metadata().symlink.as_deref() {
None => format!("{cur_str}{prefix} "),
Some(p) => format!(
"{}{}-> {:0width$}",
cur_str,
prefix,
fmt_path_elide(p, file_len - 1),
width = file_len
),
}
}
fn fmt_user(
&self,
fsentry: &File,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
#[cfg(posix)]
let username: String = match fsentry.metadata().uid {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => 0.to_string(),
};
#[cfg(win)]
let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(),
None => 0.to_string(),
};
format!("{cur_str}{prefix}{username:12}")
}
fn fmt_fallback(
&self,
_fsentry: &File,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
format!("{cur_str}{prefix}")
}
fn make_callchain(fmt_str: &str) -> CallChainBlock {
let mut callchain: Option<CallChainBlock> = None;
let mut last_index: usize = 0;
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
let Some(full_match) = regex_match.get(0) else {
continue;
};
let index: usize = full_match.start();
let prefix: String = String::from(&fmt_str[last_index..index]);
last_index += prefix.len() + full_match.as_str().len();
match FMT_ATTR_REGEX.captures(®ex_match[1]) {
Some(regex_match) => {
let callback: FmtCallback = match ®ex_match.get(1) {
Some(key) => match key.as_str() {
FMT_KEY_ATIME => Self::fmt_atime,
FMT_KEY_CTIME => Self::fmt_ctime,
FMT_KEY_GROUP => Self::fmt_group,
FMT_KEY_MTIME => Self::fmt_mtime,
FMT_KEY_NAME => Self::fmt_name,
FMT_KEY_PATH => Self::fmt_path,
FMT_KEY_PEX => Self::fmt_pex,
FMT_KEY_SIZE => Self::fmt_size,
FMT_KEY_SYMLINK => Self::fmt_symlink,
FMT_KEY_USER => Self::fmt_user,
_ => Self::fmt_fallback,
},
None => Self::fmt_fallback,
};
let fmt_len: Option<usize> = match ®ex_match.get(3) {
Some(len) => len.as_str().parse::<usize>().ok(),
None => None,
};
let fmt_extra: Option<String> = regex_match
.get(5)
.as_ref()
.map(|extra| extra.as_str().to_string());
match callchain.as_mut() {
None => {
callchain =
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra));
}
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
}
}
None => continue,
}
}
match callchain {
Some(callchain) => callchain,
None => CallChainBlock::new(Self::fmt_fallback, String::new(), None, None),
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::time::SystemTime;
use pretty_assertions::assert_eq;
use remotefs::fs::{File, FileType, Metadata, UnixPex};
use super::*;
#[test]
fn test_fs_explorer_formatter_callchain() {
let dummy_formatter: Formatter = Formatter::new("");
let t: SystemTime = SystemTime::now();
let dummy_entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
assert!(callchain.next_block.is_none());
assert_eq!(callchain.prefix, String::from("h"));
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hA")
);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
String::from("hAhAhAhAhA")
);
}
#[test]
fn test_fs_explorer_formatter_format_files() {
let formatter: Formatter = Formatter::default();
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -rw-r--r-- 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
let entry = File {
path: PathBuf::from("/piroparoporoperoperupupu.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
};
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: None,
},
};
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? root 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
};
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -????????? 0 8.2 kB {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_format_dirs() {
let formatter: Formatter = Formatter::default();
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Directory,
size: 4096,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o755)),
},
};
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x root {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ drwxr-xr-x 0 {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Directory,
size: 4096,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
};
#[cfg(posix)]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(win)]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t, "%b %d %Y %H:%M")
)
);
}
#[test]
fn test_fs_explorer_formatter_all_together_now() {
let formatter: Formatter = Formatter::new(
"{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}",
);
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 4096,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
};
assert_eq!(
formatter.fmt(&entry),
format!(
"projects -> project.info 0 0 lrwxr-xr-x 12 B {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
let entry = File {
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Directory,
size: 4096,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
};
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt -> project.info 0 0 lrw-r--r-- 12 B {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
let entry = File {
path: PathBuf::from("/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
assert_eq!(
formatter.fmt(&entry),
format!(
"bar.txt 0 0 -rw-r--r-- 8.2 kB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
)
);
}
#[test]
#[cfg(posix)]
fn should_fmt_path() {
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!(
formatter.fmt(&entry).as_str(),
"File path: /tmp/a/b/c/bar.txt"
);
let formatter: Formatter = Formatter::new("File path: {PATH:8}");
assert_eq!(
formatter.fmt(&entry).as_str(),
"File path: /tmp/…/c/bar.txt"
);
let formatter: Formatter = Formatter::new("File path: {PATH:128:/tmp/a/b}");
assert_eq!(formatter.fmt(&entry).as_str(), "File path: c/bar.txt");
}
#[test]
#[cfg(posix)]
fn should_fmt_utf8_path() {
let t: SystemTime = SystemTime::now();
let entry = File {
path: PathBuf::from("/tmp/a/b/c/россия"),
metadata: Metadata {
accessed: Some(t),
created: Some(t),
modified: Some(t),
file_type: FileType::Symlink,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
};
let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!(
formatter.fmt(&entry).as_str(),
"File path: /tmp/a/b/c/россия"
);
let formatter: Formatter = Formatter::new("File path: {PATH:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "File path: /tmp/…/c/россия");
}
#[test]
fn should_fmt_short_ascii_name() {
let entry = File {
path: PathBuf::from("/tmp/foo.txt"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "foo.txt ");
}
#[test]
fn should_fmt_exceeding_length_ascii_name() {
let entry = File {
path: PathBuf::from("/tmp/christian-visintin.txt"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "christi…");
}
#[test]
fn should_fmt_short_utf8_name() {
let entry = File {
path: PathBuf::from("/tmp/россия"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "россия ");
}
#[test]
fn should_fmt_long_utf8_name() {
let entry = File {
path: PathBuf::from("/tmp/喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let formatter: Formatter = Formatter::new("{NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "喵喵喵喵喵喵喵…");
}
#[test]
fn should_ignore_unknown_formatter_keys() {
let entry = File {
path: PathBuf::from("/tmp/foo.txt"),
metadata: Metadata {
accessed: None,
created: None,
modified: None,
file_type: FileType::File,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: None,
},
};
let formatter: Formatter = Formatter::new("before {UNKNOWN:12} after {NAME:8}");
assert_eq!(formatter.fmt(&entry).as_str(), "before after foo.txt ");
}
fn dummy_fmt(
_fmt: &Formatter,
_entry: &File,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
format!("{cur_str}{prefix}A")
}
}