diff --git a/docs/configuration/joshuto.toml.md b/docs/configuration/joshuto.toml.md index 3c9c65536..e7d70e9be 100644 --- a/docs/configuration/joshuto.toml.md +++ b/docs/configuration/joshuto.toml.md @@ -81,11 +81,13 @@ line_number_style = "none" # Options include # - size # - mtime +# - atime +# - btime # - user # - group # - perm # - none (can't be combined with other options) -# - all (can't be combined with other options) +# - all (same with none, but if there are not enough space to display the whole string, it will be cut off) linemode = "size" # Configurations related to file sorting diff --git a/src/config/clean/app/display/line_mode.rs b/src/config/clean/app/display/line_mode.rs index 4c287bbaf..ed2c81bc4 100644 --- a/src/config/clean/app/display/line_mode.rs +++ b/src/config/clean/app/display/line_mode.rs @@ -1,22 +1,79 @@ -use serde::Deserialize; - use crate::error::{AppError, AppErrorKind, AppResult}; -bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] - #[serde(transparent)] - pub struct LineMode: u8 { - const size = 1 << 0; - const mtime = 1 << 1; - const user = 1 << 2; - const group = 1 << 3; - const perm = 1 << 4; +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LineMode { + pub mode: [LineModeArgs; 8], + pub size: usize, +} + +impl LineMode { + pub const fn all() -> Self { + Self { + mode: [ + LineModeArgs::Size, + LineModeArgs::ModifyTime, + LineModeArgs::AccessTime, + LineModeArgs::BirthTime, + LineModeArgs::User, + LineModeArgs::Group, + LineModeArgs::Permission, + LineModeArgs::Null, + ], + size: 7, + } + } + + pub const fn empty() -> Self { + Self { + mode: [LineModeArgs::Null; 8], + size: 0, + } + } + + pub fn add_mode(&mut self, mode: LineModeArgs) { + if self.mode.contains(&mode) { + return; + } + + self.mode[self.size] = mode; + self.size += 1; + } +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub enum LineModeArgs { + Size, + ModifyTime, + AccessTime, + BirthTime, + User, + Group, + Permission, + #[default] + Null, +} + +impl AsRef for LineModeArgs { + fn as_ref(&self) -> &str { + match self { + LineModeArgs::Size => "size", + LineModeArgs::ModifyTime => "mtime", + LineModeArgs::AccessTime => "atime", + LineModeArgs::BirthTime => "ctime", + LineModeArgs::User => "user", + LineModeArgs::Group => "group", + LineModeArgs::Permission => "perm", + LineModeArgs::Null => unreachable!(), + } } } impl Default for LineMode { fn default() -> Self { - Self::size + let mut mode = [Default::default(); 8]; + mode[0] = LineModeArgs::Size; + + Self { size: 1, mode } } } @@ -26,37 +83,39 @@ impl LineMode { "all" => Ok(LineMode::all()), "none" => Ok(LineMode::empty()), _ => { - let mut flags = name.split('|'); - - let mut linemode = LineMode::empty(); - - flags.try_for_each(|flag| { - match flag.trim() { - "size" => linemode |= LineMode::size, - "mtime" => linemode |= LineMode::mtime, - "user" => linemode |= LineMode::user, - "group" => linemode |= LineMode::group, - "perm" => linemode |= LineMode::perm, - flag => { + let mut line_mode = LineMode::empty(); + + for mode in name.split('|').map(|mode| mode.trim()) { + match mode { + "size" => line_mode.add_mode(LineModeArgs::Size), + "mtime" => line_mode.add_mode(LineModeArgs::ModifyTime), + "atime" => line_mode.add_mode(LineModeArgs::AccessTime), + "btime" => line_mode.add_mode(LineModeArgs::BirthTime), + "user" => line_mode.add_mode(LineModeArgs::User), + "group" => line_mode.add_mode(LineModeArgs::Group), + "perm" => line_mode.add_mode(LineModeArgs::Permission), + e => { return Err(AppError::new( AppErrorKind::InvalidParameters, - format!("Linemode '{}' unknown.", flag), + format!("Linemode '{}' unknown.", e), )) } } + } - Ok(()) - })?; - - Ok(linemode) + Ok(line_mode) } } } pub fn as_string(&self) -> String { - self.iter_names() - .map(|f| f.0) - .collect::>() - .join(" | ") + let modes: Vec<&str> = self + .mode + .iter() + .take(self.size) + .map(AsRef::as_ref) + .collect(); + + modes.join(" | ") } } diff --git a/src/config/raw/app/display/config.rs b/src/config/raw/app/display/config.rs index 2fe10c097..9b0583812 100644 --- a/src/config/raw/app/display/config.rs +++ b/src/config/raw/app/display/config.rs @@ -1,6 +1,6 @@ -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; -use crate::config::clean::app::display::line_mode::LineMode; +use crate::config::clean::app::display::line_mode::{LineMode, LineModeArgs}; use super::sort::SortOptionRaw; @@ -51,7 +51,7 @@ pub struct DisplayOptionRaw { #[serde(default)] pub line_number_style: String, - #[serde(default)] + #[serde(default, deserialize_with = "deserialize_line_mode")] pub linemode: LineMode, } @@ -73,3 +73,27 @@ impl std::default::Default for DisplayOptionRaw { } } } + +fn deserialize_line_mode<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let line_mode_string: String = Deserialize::deserialize(deserializer)?; + + let mut line_mode = LineMode::empty(); + + for mode in line_mode_string.split('|').map(|mode| mode.trim()) { + match mode { + "size" => line_mode.add_mode(LineModeArgs::Size), + "mtime" => line_mode.add_mode(LineModeArgs::ModifyTime), + "atime" => line_mode.add_mode(LineModeArgs::AccessTime), + "btime" => line_mode.add_mode(LineModeArgs::BirthTime), + "user" => line_mode.add_mode(LineModeArgs::User), + "group" => line_mode.add_mode(LineModeArgs::Group), + "perm" => line_mode.add_mode(LineModeArgs::Permission), + e => eprintln!("{e} is an unsupportted line mode, will be ignored"), + } + } + + Ok(line_mode) +} diff --git a/src/fs/metadata.rs b/src/fs/metadata.rs index 132567eca..b1faffb9e 100644 --- a/src/fs/metadata.rs +++ b/src/fs/metadata.rs @@ -24,12 +24,14 @@ pub enum LinkType { #[derive(Clone, Debug)] pub struct JoshutoMetadata { - _len: u64, - _directory_size: Option, - _modified: time::SystemTime, - _permissions: fs::Permissions, - _file_type: FileType, - _link_type: LinkType, + len: u64, + directory_size: Option, + modified: time::SystemTime, + accessed: time::SystemTime, + created: time::SystemTime, + permissions: fs::Permissions, + file_type: FileType, + link_type: LinkType, #[cfg(unix)] pub uid: u32, #[cfg(unix)] @@ -45,21 +47,29 @@ impl JoshutoMetadata { let symlink_metadata = fs::symlink_metadata(path)?; let metadata = fs::metadata(path); - let (_len, _modified, _permissions) = match metadata.as_ref() { - Ok(m) => (m.len(), m.modified()?, m.permissions()), + let (len, modified, accessed, created, permissions) = match metadata.as_ref() { + Ok(m) => ( + m.len(), + m.modified()?, + m.accessed()?, + m.created()?, + m.permissions(), + ), Err(_) => ( symlink_metadata.len(), symlink_metadata.modified()?, + symlink_metadata.accessed()?, + symlink_metadata.created()?, symlink_metadata.permissions(), ), }; - let (_file_type, _directory_size) = match metadata.as_ref() { + let (file_type, directory_size) = match metadata.as_ref() { Ok(m) if m.file_type().is_dir() => (FileType::Directory, None), _ => (FileType::File, None), }; - let _link_type = if symlink_metadata.file_type().is_symlink() { + let link_type = if symlink_metadata.file_type().is_symlink() { let mut link = "".to_string(); if let Ok(path) = fs::read_link(path) { @@ -85,12 +95,14 @@ impl JoshutoMetadata { let mode = symlink_metadata.mode(); Ok(Self { - _len, - _directory_size, - _modified, - _permissions, - _file_type, - _link_type, + len, + directory_size, + modified, + accessed, + created, + permissions, + file_type, + link_type, #[cfg(unix)] uid, #[cfg(unix)] @@ -101,38 +113,46 @@ impl JoshutoMetadata { } pub fn len(&self) -> u64 { - self._len + self.len } pub fn directory_size(&self) -> Option { - self._directory_size + self.directory_size } pub fn update_directory_size(&mut self, size: usize) { - self._directory_size = Some(size); + self.directory_size = Some(size); } pub fn modified(&self) -> time::SystemTime { - self._modified + self.modified + } + + pub fn accessed(&self) -> time::SystemTime { + self.accessed + } + + pub fn created(&self) -> time::SystemTime { + self.created } pub fn permissions_ref(&self) -> &fs::Permissions { - &self._permissions + &self.permissions } pub fn permissions_mut(&mut self) -> &mut fs::Permissions { - &mut self._permissions + &mut self.permissions } pub fn file_type(&self) -> &FileType { - &self._file_type + &self.file_type } pub fn link_type(&self) -> &LinkType { - &self._link_type + &self.link_type } pub fn is_dir(&self) -> bool { - self._file_type == FileType::Directory + self.file_type == FileType::Directory } } diff --git a/src/key_command/impl_comment.rs b/src/key_command/impl_comment.rs index e22618ca3..10c720a58 100644 --- a/src/key_command/impl_comment.rs +++ b/src/key_command/impl_comment.rs @@ -1,7 +1,4 @@ -use crate::{ - config::clean::app::display::{line_mode::LineMode, sort_type::SortType}, - io::FileOperationOptions, -}; +use crate::{config::clean::app::display::sort_type::SortType, io::FileOperationOptions}; use crate::commands::sub_process::SubprocessCallMode; @@ -11,14 +8,7 @@ impl CommandComment for Command { // These comments are displayed at the help page fn comment(&self) -> &'static str { match self { - Self::SetLineMode(linemode) => match *linemode { - LineMode::size => "Show files with size", - LineMode::mtime => "Show files with modified time", - LineMode::user => "Show files with user", - LineMode::group => "Show files with group", - LineMode::perm => "Show files with permission", - _ => "Show files with multi-attribution", - }, + Self::SetLineMode(_) => "Show File's metadata in line", Self::Escape => "Escape from visual mode (cancel)", Self::BulkRename => "Bulk rename", diff --git a/src/ui/widgets/tui_dirlist_detailed.rs b/src/ui/widgets/tui_dirlist_detailed.rs index 0318a1987..e9d34eb86 100644 --- a/src/ui/widgets/tui_dirlist_detailed.rs +++ b/src/ui/widgets/tui_dirlist_detailed.rs @@ -5,12 +5,13 @@ use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use ratatui::widgets::Widget; -use crate::config::clean::app::display::line_mode::LineMode; +use crate::config::clean::app::display::line_mode::{LineMode, LineModeArgs}; use crate::config::clean::app::display::line_number::LineNumberStyle; use crate::config::clean::app::display::tab::TabDisplayOption; use crate::config::clean::app::display::DisplayOption; use crate::config::clean::app::AppConfig; use crate::fs::{FileType, JoshutoDirEntry, JoshutoDirList, LinkType}; +use crate::util::format::time_to_string; use crate::util::string::UnicodeTruncate; use crate::util::style; use crate::util::{format, unix}; @@ -133,6 +134,26 @@ fn get_entry_size_string(entry: &JoshutoDirEntry) -> String { } } +fn display_line_mode(mode: LineMode, entry: &JoshutoDirEntry) -> String { + let metadata = &entry.metadata; + + mode.mode + .iter() + .take(mode.size) + .map(|arg| match arg { + LineModeArgs::Size => get_entry_size_string(entry), + LineModeArgs::ModifyTime => time_to_string(metadata.modified()), + LineModeArgs::AccessTime => time_to_string(metadata.accessed()), + LineModeArgs::BirthTime => time_to_string(metadata.created()), + LineModeArgs::User => unix::uid_to_string(metadata.uid).unwrap_or("unknown".into()), + LineModeArgs::Group => unix::gid_to_string(metadata.gid).unwrap_or("unknown".into()), + LineModeArgs::Permission => unix::mode_to_string(metadata.mode), + LineModeArgs::Null => unreachable!(), + }) + .collect::>() + .join(" ") +} + #[allow(clippy::too_many_arguments)] fn print_entry( config: &AppConfig, @@ -164,22 +185,8 @@ fn print_entry( let label = name.to_string(); let left_label_original = label; - let right_label_original = format!( - " {}{} ", - symlink_string, - linemode - .iter_names() - .map(|f| match f.0 { - "size" => get_entry_size_string(entry), - "mtime" => format::mtime_to_string(entry.metadata.modified()), - "user" => unix::uid_to_string(entry.metadata.uid).unwrap_or("unknown".into()), - "group" => unix::gid_to_string(entry.metadata.gid).unwrap_or("unknown".into()), - "perm" => unix::mode_to_string(entry.metadata.mode), - _ => unreachable!(), - }) - .collect::>() - .join(" ") - ); + let right_label_original = + format!(" {}{} ", symlink_string, display_line_mode(linemode, entry)); // draw prefix first let prefix_width = prefix.width(); @@ -188,11 +195,8 @@ fn print_entry( // factor left_label and right_label let drawing_width = drawing_width - prefix_width; - let (left_label, right_label) = factor_labels_for_entry( - &left_label_original, - right_label_original.as_str(), - drawing_width, - ); + let (left_label, right_label) = + factor_labels_for_entry(left_label_original, right_label_original, drawing_width); // Draw labels buf.set_stringn(x, y, left_label, drawing_width, style); @@ -205,11 +209,11 @@ fn print_entry( ); } -fn factor_labels_for_entry<'a>( - left_label_original: &'a str, - right_label_original: &'a str, +fn factor_labels_for_entry( + left_label_original: String, + right_label_original: String, drawing_width: usize, -) -> (String, &'a str) { +) -> (String, String) { let left_label_original_width = left_label_original.width(); let right_label_original_width = right_label_original.width(); @@ -217,21 +221,25 @@ fn factor_labels_for_entry<'a>( let width_remainder = left_width_remainder - left_label_original_width as i32; if drawing_width == 0 { - ("".to_string(), "") + ("".into(), "".into()) } else if width_remainder >= 0 { (left_label_original.to_string(), right_label_original) } else if left_width_remainder < MIN_LEFT_LABEL_WIDTH { ( if left_label_original.width() as i32 <= left_width_remainder { - trim_file_label(left_label_original, drawing_width) + trim_file_label(&left_label_original, drawing_width) } else { left_label_original.to_string() }, - "", + if let Some(width) = (drawing_width - MIN_LEFT_LABEL_WIDTH as usize).checked_sub(3) { + right_label_original.chars().take(width).collect::() + " .." + } else { + "".into() + }, ) } else { ( - trim_file_label(left_label_original, left_width_remainder as usize), + trim_file_label(&left_label_original, left_width_remainder as usize), right_label_original, ) } @@ -278,8 +286,8 @@ mod test_factor_labels { let left = "foo.ext"; let right = "right"; assert_eq!( - ("".to_string(), ""), - factor_labels_for_entry(left, right, 0) + ("".to_string(), "".to_string()), + factor_labels_for_entry(left.into(), right.into(), 0) ); } @@ -288,8 +296,8 @@ mod test_factor_labels { let left = "foo.ext"; let right = "right"; assert_eq!( - (left.to_string(), right), - factor_labels_for_entry(left, right, 20) + (left.to_string(), right.to_string()), + factor_labels_for_entry(left.into(), right.into(), 20) ); } @@ -298,8 +306,8 @@ mod test_factor_labels { let left = "foo.ext"; let right = "right"; assert_eq!( - (left.to_string(), right), - factor_labels_for_entry(left, right, 12) + (left.to_string(), right.to_string()), + factor_labels_for_entry(left.into(), right.into(), 12) ); } @@ -309,8 +317,8 @@ mod test_factor_labels { let right = "right"; assert!(left.chars().count() as i32 == MIN_LEFT_LABEL_WIDTH); assert_eq!( - ("foobarbazfo.ext".to_string(), ""), - factor_labels_for_entry(left, right, MIN_LEFT_LABEL_WIDTH as usize) + ("foobarbazfo.ext".to_string(), "".to_string()), + factor_labels_for_entry(left.into(), right.into(), MIN_LEFT_LABEL_WIDTH as usize) ); } @@ -320,10 +328,10 @@ mod test_factor_labels { let right = "right"; assert!(left.chars().count() as i32 > MIN_LEFT_LABEL_WIDTH + right.chars().count() as i32); assert_eq!( - ("foobarbazf….ext".to_string(), right), + ("foobarbazf….ext".to_string(), right.to_string()), factor_labels_for_entry( - left, - right, + left.into(), + right.into(), MIN_LEFT_LABEL_WIDTH as usize + right.chars().count() ) ); @@ -337,8 +345,8 @@ mod test_factor_labels { let right = "right"; assert!(left.chars().count() as i32 > MIN_LEFT_LABEL_WIDTH); assert_eq!( - ("foooooobaaaaaaarbaaaa…".to_string(), right), - factor_labels_for_entry(left, right, left.chars().count()) + ("foooooobaaaaaaarbaaaa…".to_string(), right.to_string()), + factor_labels_for_entry(left.into(), right.into(), left.chars().count()) ); } } diff --git a/src/ui/widgets/tui_footer.rs b/src/ui/widgets/tui_footer.rs index 6f186db2d..a36a95928 100644 --- a/src/ui/widgets/tui_footer.rs +++ b/src/ui/widgets/tui_footer.rs @@ -52,7 +52,7 @@ impl<'a> Widget for TuiFooter<'a> { let user_str = unix::uid_to_string(entry.metadata.uid).unwrap_or("unknown".into()); let group_str = unix::gid_to_string(entry.metadata.gid).unwrap_or("unknown".into()); - let mtime_str = format::mtime_to_string(entry.metadata.modified()); + let mtime_str = format::time_to_string(entry.metadata.modified()); let size_str = format::file_size_to_string(entry.metadata.len()); let path = self.dirlist.file_path(); diff --git a/src/util/format.rs b/src/util/format.rs index 3079840f8..68f1273b7 100644 --- a/src/util/format.rs +++ b/src/util/format.rs @@ -20,9 +20,9 @@ pub fn file_size_to_string(file_size: u64) -> String { } } -pub fn mtime_to_string(mtime: time::SystemTime) -> String { - const MTIME_FORMATTING: &str = "%Y-%m-%d %H:%M"; +pub fn time_to_string(time: time::SystemTime) -> String { + const TIME_FORMATTING: &str = "%Y-%m-%d %H:%M"; - let datetime: chrono::DateTime = mtime.into(); - datetime.format(MTIME_FORMATTING).to_string() + let datetime: chrono::DateTime = time.into(); + datetime.format(TIME_FORMATTING).to_string() }