diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs
new file mode 100755
index 0000000..8915dc8
--- /dev/null
+++ b/src/fs/explorer/formatter.rs
@@ -0,0 +1,710 @@
+//! ## Formatter
+//!
+//! `formatter` is the module which provides formatting utilities for `FileExplorer`
+
+/*
+*
+* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
+*
+* This file is part of "TermSCP"
+*
+* TermSCP is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* TermSCP is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with TermSCP. If not, see .
+*
+*/
+
+// Deps
+extern crate bytesize;
+extern crate regex;
+#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+extern crate users;
+// Locals
+use super::FsEntry;
+use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
+// Ext
+use bytesize::ByteSize;
+use regex::Regex;
+#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+use users::{get_group_by_gid, get_user_by_uid};
+// Types
+type FmtCallback = fn(&Formatter, &FsEntry, &str) -> String;
+
+// Keys
+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_PEX: &str = "{PEX}";
+const FMT_KEY_SIZE: &str = "{SIZE}";
+const FMT_KEY_SYMLINK: &str = "{SYMLINK}";
+const FMT_KEY_USER: &str = "{USER}";
+// Default
+const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
+// Regex
+lazy_static! {
+ static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
+}
+
+/// ## CallChainBlock
+///
+/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry.
+/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
+/// a chain of function is made using the Formatters method.
+/// This method provides an extremely fast way to format fs entries
+struct CallChainBlock {
+ func: FmtCallback,
+ next_block: Option>,
+}
+
+impl CallChainBlock {
+ /// ### new
+ ///
+ /// Create a new `CallChainBlock`
+ pub fn new(func: FmtCallback) -> Self {
+ CallChainBlock {
+ func,
+ next_block: None,
+ }
+ }
+
+ /// ### next
+ ///
+ /// Call next callback in the CallChain
+ pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Call func
+ let new_str: String = (self.func)(fmt, fsentry, cur_str);
+ // If next is some, call next, otherwise (END OF CHAIN) return new_str
+ match &self.next_block {
+ Some(block) => block.next(fmt, fsentry, new_str.as_str()),
+ None => new_str,
+ }
+ }
+
+ /// ### push
+ ///
+ /// Push func to the last element in the Call chain
+ pub fn push(&mut self, func: FmtCallback) {
+ // Call recursively until an element with next_block equal to None is found
+ match &mut self.next_block {
+ None => self.next_block = Some(Box::new(CallChainBlock::new(func))),
+ Some(block) => block.push(func),
+ }
+ }
+}
+
+/// ## Formatter
+///
+/// Formatter takes care of formatting FsEntries according to the provided keys.
+/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
+/// at each fmt call.
+pub struct Formatter {
+ fmt_str: String,
+ call_chain: CallChainBlock,
+}
+
+impl Default for Formatter {
+ /// ### default
+ ///
+ /// Instantiates a Formatter with the default fmt syntax
+ fn default() -> Self {
+ Formatter {
+ fmt_str: FMT_DEFAULT_STX.to_string(),
+ call_chain: Self::make_callchain(FMT_DEFAULT_STX),
+ }
+ }
+}
+
+impl Formatter {
+ /// ### new
+ ///
+ /// Instantiates a new `Formatter` with the provided format string
+ pub fn new(fmt_str: &str) -> Self {
+ Formatter {
+ fmt_str: fmt_str.to_string(),
+ call_chain: Self::make_callchain(fmt_str),
+ }
+ }
+
+ /// ### fmt
+ ///
+ /// Format fsentry
+ pub fn fmt(&self, fsentry: &FsEntry) -> String {
+ // Execute callchain blocks
+ self.call_chain.next(self, fsentry, self.fmt_str.as_str())
+ }
+
+ // Fmt methods
+
+ /// ### fmt_atime
+ ///
+ /// Format last access time
+ fn fmt_atime(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get date
+ let datetime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M");
+ // Replace `FMT_KEY_ATIME` with datetime
+ cur_str.replace(FMT_KEY_ATIME, format!("{:17}", datetime).as_str())
+ }
+
+ /// ### fmt_ctime
+ ///
+ /// Format creation time
+ fn fmt_ctime(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get date
+ let datetime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M");
+ // Replace `FMT_KEY_ATIME` with datetime
+ cur_str.replace(FMT_KEY_CTIME, format!("{:17}", datetime).as_str())
+ }
+
+ /// ### fmt_group
+ ///
+ /// Format owner group
+ fn fmt_group(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get username
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ let group: String = match fsentry.get_group() {
+ 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(target_os = "windows")]
+ let group: String = match fsentry.get_group() {
+ Some(gid) => gid.to_string(),
+ None => 0.to_string(),
+ };
+ // Replace `FMT_KEY_GROUP` with size
+ cur_str.replace(FMT_KEY_GROUP, format!("{:12}", group).as_str())
+ }
+
+ /// ### fmt_mtime
+ ///
+ /// Format last change time
+ fn fmt_mtime(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get date
+ let datetime: String = fmt_time(fsentry.get_last_change_time(), "%b %d %Y %H:%M");
+ // Replace `FMT_KEY_MTIME` with datetime
+ cur_str.replace(FMT_KEY_MTIME, format!("{:17}", datetime).as_str())
+ }
+
+ /// ### fmt_name
+ ///
+ /// Format file name
+ fn fmt_name(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get file name (or elide if too long)
+ let name: &str = fsentry.get_name();
+ let last_idx: usize = match fsentry.is_dir() {
+ // NOTE: For directories is 19, since we push '/' to name
+ true => 19,
+ false => 20,
+ };
+ let mut name: String = match name.len() >= 24 {
+ false => name.to_string(),
+ true => format!("{}...", &name[0..last_idx]),
+ };
+ if fsentry.is_dir() {
+ name.push('/');
+ }
+ // Replace `FMT_KEY_NAME` with name
+ cur_str.replace(FMT_KEY_NAME, format!("{:24}", name).as_str())
+ }
+
+ /// ### fmt_pex
+ ///
+ /// Format file permissions
+ fn fmt_pex(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Create mode string
+ let mut pex: String = String::with_capacity(10);
+ let file_type: char = match fsentry.is_symlink() {
+ true => 'l',
+ false => match fsentry.is_dir() {
+ true => 'd',
+ false => '-',
+ },
+ };
+ pex.push(file_type);
+ match fsentry.get_unix_pex() {
+ None => pex.push_str("?????????"),
+ Some((owner, group, others)) => pex.push_str(fmt_pex(owner, group, others).as_str()),
+ }
+ // Replace `FMT_KEY_PEX` with pex
+ cur_str.replace(FMT_KEY_PEX, format!("{:10}", pex).as_str())
+ }
+
+ /// ### fmt_size
+ ///
+ /// Format file size
+ fn fmt_size(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ if fsentry.is_file() {
+ // Get byte size
+ let size: ByteSize = ByteSize(fsentry.get_size() as u64);
+ // Replace `FMT_KEY_SIZE` with size
+ cur_str.replace(FMT_KEY_SIZE, format!("{:10}", size.to_string()).as_str())
+ } else {
+ // No size for directories
+ cur_str.replace(FMT_KEY_SIZE, " ")
+ }
+ }
+
+ /// ### fmt_symlink
+ ///
+ /// Format file symlink (if any)
+ fn fmt_symlink(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get file name (or elide if too long)
+ // Replace `FMT_KEY_NAME` with name
+ match fsentry.is_symlink() {
+ false => cur_str.replace(FMT_KEY_SYMLINK, " "),
+ true => cur_str.replace(
+ FMT_KEY_SYMLINK,
+ format!(
+ "-> {:21}",
+ fmt_path_elide(fsentry.get_realfile().get_abs_path().as_path(), 20)
+ )
+ .as_str(),
+ ),
+ }
+ }
+
+ /// ### fmt_user
+ ///
+ /// Format owner user
+ fn fmt_user(&self, fsentry: &FsEntry, cur_str: &str) -> String {
+ // Get username
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ let username: String = match fsentry.get_user() {
+ 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(target_os = "windows")]
+ let username: String = match fsentry.get_user() {
+ Some(uid) => uid.to_string(),
+ None => 0.to_string(),
+ };
+ // Replace `FMT_KEY_USER` with size
+ cur_str.replace(FMT_KEY_USER, format!("{:12}", username).as_str())
+ }
+
+ /// ### fmt_fallback
+ ///
+ /// Fallback function in case the format key is unknown
+ /// It does nothing, just returns cur_str
+ fn fmt_fallback(&self, _fsentry: &FsEntry, cur_str: &str) -> String {
+ cur_str.to_string()
+ }
+
+ // Static
+
+ /// ### make_callchain
+ ///
+ /// Make a callchain starting from the fmt str
+ fn make_callchain(fmt_str: &str) -> CallChainBlock {
+ // Init chain block
+ let mut callchain: Option = None;
+ // Match fmt str against regex
+ for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
+ // Match the match (I guess...)
+ let callback: FmtCallback = match ®ex_match[0] {
+ 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_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,
+ };
+ // Create a callchain or push new element to its back
+ match callchain.as_mut() {
+ None => callchain = Some(CallChainBlock::new(callback)),
+ Some(chain_block) => chain_block.push(callback),
+ }
+ }
+ // Finalize and return
+ match callchain {
+ Some(callchain) => callchain,
+ None => CallChainBlock::new(Self::fmt_fallback),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ use crate::fs::{FsDirectory, FsFile};
+ use std::path::PathBuf;
+ use std::time::SystemTime;
+
+ #[test]
+ fn test_fs_explorer_formatter_callchain() {
+ // Make a dummy formatter
+ let dummy_formatter: Formatter = Formatter::new("");
+ // Make a dummy entry
+ let t_now: SystemTime = SystemTime::now();
+ let dummy_entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("bar.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t_now,
+ last_access_time: t_now,
+ creation_time: t_now,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: Some(0), // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: Some((6, 4, 4)), // UNIX only
+ });
+ let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt);
+ assert!(callchain.next_block.is_none());
+ // Execute
+ assert_eq!(
+ callchain.next(&dummy_formatter, &dummy_entry, ""),
+ String::from("A")
+ );
+ // Push 4 new blocks
+ callchain.push(dummy_fmt);
+ callchain.push(dummy_fmt);
+ callchain.push(dummy_fmt);
+ callchain.push(dummy_fmt);
+ // Verify
+ assert_eq!(
+ callchain.next(&dummy_formatter, &dummy_entry, ""),
+ String::from("AAAAA")
+ );
+ }
+
+ #[test]
+ fn test_fs_explorer_formatter_format_files() {
+ // Make default
+ let formatter: Formatter = Formatter::default();
+ // Experiments :D
+ let t: SystemTime = SystemTime::now();
+ let entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("bar.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: Some(0), // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: Some((6, 4, 4)), // UNIX only
+ });
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "bar.txt -rw-r--r-- root 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ #[cfg(target_os = "windows")]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "bar.txt -rw-r--r-- 0 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ // Elide name
+ let entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("piroparoporoperoperupupu.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: Some(0), // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: Some((6, 4, 4)), // UNIX only
+ });
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "piroparoporoperoperu... -rw-r--r-- root 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ #[cfg(target_os = "windows")]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "piroparoporoperoperu... -rw-r--r-- 0 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ // No pex
+ let entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("bar.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: Some(0), // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: None, // UNIX only
+ });
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "bar.txt -????????? root 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ #[cfg(target_os = "windows")]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "bar.txt -????????? 0 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ // No user
+ let entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("bar.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: None, // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: None, // UNIX only
+ });
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "bar.txt -????????? 0 8.2 KB {}",
+ fmt_time(t, "%b %d %Y %H:%M")
+ )
+ );
+ #[cfg(target_os = "windows")]
+ 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() {
+ // Make default
+ let formatter: Formatter = Formatter::default();
+ // Experiments :D
+ let t_now: SystemTime = SystemTime::now();
+ let entry: FsEntry = FsEntry::Directory(FsDirectory {
+ name: String::from("projects"),
+ abs_path: PathBuf::from("/home/cvisintin/projects"),
+ last_change_time: t_now,
+ last_access_time: t_now,
+ creation_time: t_now,
+ readonly: false,
+ symlink: None, // UNIX only
+ user: Some(0), // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: Some((7, 5, 5)), // UNIX only
+ });
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "projects/ drwxr-xr-x root {}",
+ fmt_time(t_now, "%b %d %Y %H:%M")
+ )
+ );
+ #[cfg(target_os = "windows")]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "projects/ drwxr-xr-x 0 {}",
+ fmt_time(t_now, "%b %d %Y %H:%M")
+ )
+ );
+ // No pex, no user
+ let entry: FsEntry = FsEntry::Directory(FsDirectory {
+ name: String::from("projects"),
+ abs_path: PathBuf::from("/home/cvisintin/projects"),
+ last_change_time: t_now,
+ last_access_time: t_now,
+ creation_time: t_now,
+ readonly: false,
+ symlink: None, // UNIX only
+ user: None, // UNIX only
+ group: Some(0), // UNIX only
+ unix_pex: None, // UNIX only
+ });
+ #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "projects/ d????????? 0 {}",
+ fmt_time(t_now, "%b %d %Y %H:%M")
+ )
+ );
+ #[cfg(target_os = "windows")]
+ assert_eq!(
+ formatter.fmt(&entry),
+ format!(
+ "projects/ d????????? 0 {}",
+ fmt_time(t_now, "%b %d %Y %H:%M")
+ )
+ );
+ }
+
+ #[test]
+ fn test_fs_explorer_formatter_all_together_now() {
+ let formatter: Formatter =
+ Formatter::new("{NAME} {SYMLINK} {GROUP} {USER} {PEX} {SIZE} {ATIME} {CTIME} {MTIME}");
+ // Directory (with symlink)
+ let t: SystemTime = SystemTime::now();
+ let pointer: FsEntry = FsEntry::File(FsFile {
+ name: String::from("project.info"),
+ abs_path: PathBuf::from("/project.info"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: None, // UNIX only
+ group: None, // UNIX only
+ unix_pex: None, // UNIX only
+ });
+ let entry: FsEntry = FsEntry::Directory(FsDirectory {
+ name: String::from("projects"),
+ abs_path: PathBuf::from("/home/cvisintin/project"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ readonly: false,
+ symlink: Some(Box::new(pointer)), // UNIX only
+ user: None, // UNIX only
+ group: None, // UNIX only
+ unix_pex: Some((7, 5, 5)), // UNIX only
+ });
+ assert_eq!(formatter.fmt(&entry), format!(
+ "projects/ -> /project.info 0 0 lrwxr-xr-x {} {} {}",
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ ));
+ // Directory without symlink
+ let entry: FsEntry = FsEntry::Directory(FsDirectory {
+ name: String::from("projects"),
+ abs_path: PathBuf::from("/home/cvisintin/project"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ readonly: false,
+ symlink: None, // UNIX only
+ user: None, // UNIX only
+ group: None, // UNIX only
+ unix_pex: Some((7, 5, 5)), // UNIX only
+ });
+ assert_eq!(formatter.fmt(&entry), format!(
+ "projects/ 0 0 drwxr-xr-x {} {} {}",
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ ));
+ // File with symlink
+ let pointer: FsEntry = FsEntry::File(FsFile {
+ name: String::from("project.info"),
+ abs_path: PathBuf::from("/project.info"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: None, // UNIX only
+ group: None, // UNIX only
+ unix_pex: None, // UNIX only
+ });
+ let entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("bar.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: Some(Box::new(pointer)), // UNIX only
+ user: None, // UNIX only
+ group: None, // UNIX only
+ unix_pex: Some((6, 4, 4)), // UNIX only
+ });
+ assert_eq!(formatter.fmt(&entry), format!(
+ "bar.txt -> /project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ ));
+ // File without symlink
+ let entry: FsEntry = FsEntry::File(FsFile {
+ name: String::from("bar.txt"),
+ abs_path: PathBuf::from("/bar.txt"),
+ last_change_time: t,
+ last_access_time: t,
+ creation_time: t,
+ size: 8192,
+ readonly: false,
+ ftype: Some(String::from("txt")),
+ symlink: None, // UNIX only
+ user: None, // UNIX only
+ group: None, // UNIX only
+ unix_pex: Some((6, 4, 4)), // UNIX only
+ });
+ assert_eq!(formatter.fmt(&entry), format!(
+ "bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ fmt_time(t, "%b %d %Y %H:%M"),
+ ));
+ }
+
+ /// ### dummy_fmt
+ ///
+ /// Dummy formatter, just yelds an 'A' at the end of the current string
+ fn dummy_fmt(_fmt: &Formatter, _entry: &FsEntry, cur_str: &str) -> String {
+ format!("{}A", cur_str)
+ }
+}
diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs
index ffc8ea3..e58d612 100644
--- a/src/fs/explorer/mod.rs
+++ b/src/fs/explorer/mod.rs
@@ -25,10 +25,12 @@
// Mods
pub(crate) mod builder;
+mod formatter;
// Deps
extern crate bitflags;
// Locals
use super::FsEntry;
+use formatter::Formatter;
// Ext
use std::cmp::Reverse;
use std::collections::VecDeque;