summaryrefslogtreecommitdiffstats
path: root/src/modes/display/fileinfo.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/modes/display/fileinfo.rs')
-rw-r--r--src/modes/display/fileinfo.rs451
1 files changed, 451 insertions, 0 deletions
diff --git a/src/modes/display/fileinfo.rs b/src/modes/display/fileinfo.rs
new file mode 100644
index 0000000..cfb65d7
--- /dev/null
+++ b/src/modes/display/fileinfo.rs
@@ -0,0 +1,451 @@
+use std::borrow::Borrow;
+use std::fs::{symlink_metadata, DirEntry, Metadata};
+use std::os::unix::fs::{FileTypeExt, MetadataExt};
+use std::path;
+
+use anyhow::{Context, Result};
+use chrono::offset::Local;
+use chrono::DateTime;
+use tuikit::prelude::{Attr, Color, Effect};
+
+use crate::common::PERMISSIONS_STR;
+use crate::config::extension_color;
+use crate::config::COLORS;
+use crate::modes::Users;
+use crate::modes::MAX_MODE;
+use crate::modes::{human_size, read_symlink_dest};
+
+type Valid = bool;
+
+/// Different kind of files
+#[derive(Debug, Clone, Copy)]
+pub enum FileKind<Valid> {
+ /// Classic files.
+ NormalFile,
+ /// Folder
+ Directory,
+ /// Block devices like /sda1
+ BlockDevice,
+ /// Char devices like /dev/null
+ CharDevice,
+ /// Named pipes
+ Fifo,
+ /// File socket
+ Socket,
+ /// symlink
+ SymbolicLink(Valid),
+}
+
+impl FileKind<Valid> {
+ /// Returns a new `FileKind` depending on metadata.
+ /// Only linux files have some of those metadata
+ /// since we rely on `std::fs::MetadataExt`.
+ pub fn new(meta: &Metadata, filepath: &path::Path) -> Self {
+ if meta.file_type().is_dir() {
+ Self::Directory
+ } else if meta.file_type().is_block_device() {
+ Self::BlockDevice
+ } else if meta.file_type().is_socket() {
+ Self::Socket
+ } else if meta.file_type().is_char_device() {
+ Self::CharDevice
+ } else if meta.file_type().is_fifo() {
+ Self::Fifo
+ } else if meta.file_type().is_symlink() {
+ let valid = is_valid_symlink(filepath);
+ Self::SymbolicLink(valid)
+ } else {
+ Self::NormalFile
+ }
+ }
+ /// Returns the expected first symbol from `ln -l` line.
+ /// d for directory, s for socket, . for file, c for char device,
+ /// b for block, l for links.
+ fn dir_symbol(&self) -> char {
+ match self {
+ Self::Fifo => 'p',
+ Self::Socket => 's',
+ Self::Directory => 'd',
+ Self::NormalFile => '.',
+ Self::CharDevice => 'c',
+ Self::BlockDevice => 'b',
+ Self::SymbolicLink(_) => 'l',
+ }
+ }
+
+ fn sortable_char(&self) -> char {
+ match self {
+ Self::Directory => 'a',
+ Self::NormalFile => 'b',
+ Self::SymbolicLink(_) => 'c',
+ Self::BlockDevice => 'd',
+ Self::CharDevice => 'e',
+ Self::Socket => 'f',
+ Self::Fifo => 'g',
+ }
+ }
+}
+
+/// Different kind of display for the size column.
+/// ls -lh display a human readable size for normal files,
+/// nothing should be displayed for a directory,
+/// Major & Minor driver versions are used for CharDevice & BlockDevice
+#[derive(Clone, Debug)]
+pub enum SizeColumn {
+ /// Used for normal files. It's the size in bytes.
+ Size(u64),
+ /// Used for directories, nothing is displayed
+ None,
+ /// Use for CharDevice and BlockDevice.
+ /// It's the major & minor driver versions.
+ MajorMinor((u8, u8)),
+}
+
+impl std::fmt::Display for SizeColumn {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Size(bytes) => write!(f, " {hs}", hs = human_size(*bytes)),
+ Self::None => write!(f, " - "),
+ Self::MajorMinor((major, minor)) => write!(f, "{major:>3},{minor:<3}"),
+ }
+ }
+}
+
+impl SizeColumn {
+ fn new(size: u64, metadata: &Metadata, file_kind: &FileKind<Valid>) -> Self {
+ match file_kind {
+ FileKind::Directory => Self::None,
+ FileKind::CharDevice | FileKind::BlockDevice => Self::MajorMinor(major_minor(metadata)),
+ _ => Self::Size(size),
+ }
+ }
+}
+
+/// Infos about a file
+/// We read and keep tracks every displayable information about
+/// a file.
+#[derive(Clone, Debug)]
+pub struct FileInfo {
+ /// Full path of the file
+ pub path: std::rc::Rc<path::Path>,
+ /// Filename
+ pub filename: std::rc::Rc<str>,
+ /// File size as a `String`, already human formated.
+ /// For char devices and block devices we display major & minor like ls.
+ pub size_column: SizeColumn,
+ /// True size of a file, not formated
+ pub true_size: u64,
+ /// Owner name of the file.
+ pub owner: std::rc::Rc<str>,
+ /// Group name of the file.
+ pub group: std::rc::Rc<str>,
+ /// System time of last modification
+ pub system_time: std::rc::Rc<str>,
+ /// Is this file currently selected ?
+ pub is_selected: bool,
+ /// What kind of file is this ?
+ pub file_kind: FileKind<Valid>,
+ /// Extension of the file. `""` for a directory.
+ pub extension: std::rc::Rc<str>,
+ /// A formated filename where the "kind" of file
+ /// (directory, char device, block devive, fifo, socket, normal)
+ /// is prepend to the name, allowing a "sort by kind" method.
+ pub kind_format: std::rc::Rc<str>,
+}
+
+impl FileInfo {
+ pub fn new(path: &path::Path, users: &Users) -> Result<Self> {
+ let filename = extract_filename(path)?;
+ let metadata = symlink_metadata(path)?;
+ let path = std::rc::Rc::from(path);
+ let owner = extract_owner(&metadata, users);
+ let group = extract_group(&metadata, users);
+ let system_time = extract_datetime(&metadata)?;
+ let is_selected = false;
+ let true_size = extract_file_size(&metadata);
+ let file_kind = FileKind::new(&metadata, &path);
+ let size_column = SizeColumn::new(true_size, &metadata, &file_kind);
+ let extension = extract_extension(&path).into();
+ let kind_format = filekind_and_filename(&filename, &file_kind);
+
+ Ok(FileInfo {
+ path,
+ filename,
+ size_column,
+ true_size,
+ owner,
+ group,
+ system_time,
+ is_selected,
+ file_kind,
+ extension,
+ kind_format,
+ })
+ }
+
+ /// Reads every information about a file from its metadata and returs
+ /// a new `FileInfo` object if we can create one.
+ pub fn from_direntry(direntry: &DirEntry, users: &Users) -> Result<FileInfo> {
+ Self::new(&direntry.path(), users)
+ }
+
+ /// Creates a fileinfo from a path and a filename.
+ /// The filename is used when we create the fileinfo for "." and ".." in every folder.
+ pub fn from_path_with_name(path: &path::Path, filename: &str, users: &Users) -> Result<Self> {
+ let mut file_info = Self::new(path, users)?;
+ file_info.filename = std::rc::Rc::from(filename);
+ file_info.kind_format = filekind_and_filename(filename, &file_info.file_kind);
+ Ok(file_info)
+ }
+
+ fn metadata(&self) -> Result<std::fs::Metadata> {
+ Ok(symlink_metadata(&self.path)?)
+ }
+
+ /// String representation of file permissions
+ pub fn permissions(&self) -> Result<std::rc::Rc<str>> {
+ Ok(extract_permissions_string(&self.metadata()?))
+ }
+
+ /// Format the file line.
+ /// Since files can have different owners in the same directory, we need to
+ /// know the maximum size of owner column for formatting purpose.
+ pub fn format(&self, owner_col_width: usize, group_col_width: usize) -> Result<String> {
+ let mut repr = self.format_base(owner_col_width, group_col_width)?;
+ repr.push(' ');
+ repr.push_str(&self.filename);
+ if let FileKind::SymbolicLink(_) = self.file_kind {
+ match read_symlink_dest(&self.path) {
+ Some(dest) => repr.push_str(&format!(" -> {dest}")),
+ None => repr.push_str(" broken link"),
+ }
+ }
+ Ok(repr)
+ }
+
+ fn format_base(&self, owner_col_width: usize, group_col_width: usize) -> Result<String> {
+ let owner = format!("{owner:.owner_col_width$}", owner = self.owner,);
+ let group = format!("{group:.group_col_width$}", group = self.group,);
+ let repr = format!(
+ "{dir_symbol}{permissions} {file_size} {owner:<owner_col_width$} {group:<group_col_width$} {system_time}",
+ dir_symbol = self.dir_symbol(),
+ permissions = self.permissions()?,
+ file_size = self.size_column,
+ system_time = self.system_time,
+ );
+ Ok(repr)
+ }
+
+ /// Format the metadata line, without the filename.
+ /// Owned & Group have fixed width of 6.
+ pub fn format_no_filename(&self) -> Result<String> {
+ self.format_base(6, 6)
+ }
+
+ pub fn dir_symbol(&self) -> char {
+ self.file_kind.dir_symbol()
+ }
+
+ pub fn format_simple(&self) -> Result<String> {
+ let s: &str = self.filename.borrow();
+ Ok(s.to_string())
+ }
+
+ /// Select the file.
+ pub fn select(&mut self) {
+ self.is_selected = true;
+ }
+
+ /// Unselect the file.
+ pub fn unselect(&mut self) {
+ self.is_selected = false;
+ }
+
+ /// True iff the file is hidden (aka starts with a '.').
+ pub fn is_hidden(&self) -> bool {
+ self.filename.starts_with('.')
+ }
+
+ pub fn is_dir(&self) -> bool {
+ self.path.is_dir()
+ }
+
+ /// Formated proper name.
+ /// "/ " for `.`
+ pub fn filename_without_dot_dotdot(&self) -> String {
+ match self.filename.as_ref() {
+ "." => "/ ".to_owned(),
+ ".." => {
+ let name = if let Ok(name) = extract_filename(&self.path) {
+ name
+ } else {
+ std::rc::Rc::from("")
+ };
+ format!("/{name} ")
+ }
+ _ => format!("/{name} ", name = self.filename),
+ }
+ }
+}
+
+fn fileinfo_color(fileinfo: &FileInfo) -> Color {
+ match fileinfo.file_kind {
+ FileKind::Directory => COLORS.directory,
+ FileKind::BlockDevice => COLORS.block,
+ FileKind::CharDevice => COLORS.char,
+ FileKind::Fifo => COLORS.fifo,
+ FileKind::Socket => COLORS.socket,
+ FileKind::SymbolicLink(true) => COLORS.symlink,
+ FileKind::SymbolicLink(false) => COLORS.broken,
+ _ => extension_color(&fileinfo.extension),
+ }
+}
+
+/// Holds a `tuikit::attr::Color` and a `tuikit::attr::Effect`
+/// Both are used to print the file.
+/// When printing we still need to know if the file is flagged,
+/// which may change the `tuikit::attr::Effect`.
+#[derive(Clone, Debug)]
+pub struct ColorEffect {
+ color: Color,
+ pub effect: Effect,
+}
+
+impl ColorEffect {
+ /// Calculates a color and an effect from `fm::file_info::FileInfo`.
+ /// Used in `Display::Directory` mode where selection is stored in fileinfo itself.
+ #[inline]
+ pub fn directory(fileinfo: &FileInfo) -> ColorEffect {
+ let color = fileinfo_color(fileinfo);
+
+ let effect = if fileinfo.is_selected {
+ Effect::REVERSE
+ } else {
+ Effect::empty()
+ };
+
+ Self { color, effect }
+ }
+
+ /// Calculates a color and an effect from `crate::app::file_info` and a flag.
+ /// The "selected file" is stored in the node itself, we only need that boolean attribute.
+ #[inline]
+ pub fn node(fileinfo: &FileInfo, is_selected: bool) -> Self {
+ let color = fileinfo_color(fileinfo);
+ let effect = if is_selected {
+ Effect::REVERSE
+ } else {
+ Effect::empty()
+ };
+
+ Self { color, effect }
+ }
+
+ /// Makes a new `tuikit::attr::Attr` where `bg` is default.
+ pub fn attr(&self) -> Attr {
+ Attr {
+ fg: self.color,
+ bg: Color::default(),
+ effect: self.effect,
+ }
+ }
+}
+
+/// Associates a filetype to `tuikit::prelude::Attr` : fg color, bg color and
+/// effect.
+/// Selected file is reversed.
+pub fn fileinfo_attr(fileinfo: &FileInfo) -> Attr {
+ ColorEffect::directory(fileinfo).attr()
+}
+
+/// True if the file isn't hidden.
+pub fn is_not_hidden(entry: &DirEntry) -> Result<bool> {
+ let is_hidden = !entry
+ .file_name()
+ .to_str()
+ .context("Couldn't read filename")?
+ .starts_with('.');
+ Ok(is_hidden)
+}
+
+fn extract_filename(path: &path::Path) -> Result<std::rc::Rc<str>> {
+ let s = path
+ .file_name()
+ .unwrap_or_default()
+ .to_str()
+ .context(format!("Couldn't read filename of {p}", p = path.display()))?;
+ Ok(std::rc::Rc::from(s))
+}
+
+/// Returns the modified time.
+fn extract_datetime(metadata: &Metadata) -> Result<std::rc::Rc<str>> {
+ let datetime: DateTime<Local> = metadata.modified()?.into();
+ Ok(std::rc::Rc::from(
+ format!("{}", datetime.format("%Y/%m/%d %T")).as_str(),
+ ))
+}
+
+/// Reads the permission and converts them into a string.
+fn extract_permissions_string(metadata: &Metadata) -> std::rc::Rc<str> {
+ let mode = (metadata.mode() & MAX_MODE) as usize;
+ let s_o = convert_octal_mode(mode >> 6);
+ let s_g = convert_octal_mode((mode >> 3) & 7);
+ let s_a = convert_octal_mode(mode & 7);
+ std::rc::Rc::from(format!("{s_o}{s_a}{s_g}").as_str())
+}
+
+/// Convert an integer like `Oo7` into its string representation like `"rwx"`
+pub fn convert_octal_mode(mode: usize) -> &'static str {
+ PERMISSIONS_STR[mode]
+}
+
+/// Reads the owner name and returns it as a string.
+/// If it's not possible to get the owner name (happens if the owner exists on a remote machine but not on host),
+/// it returns the uid as a `Result<String>`.
+fn extract_owner(metadata: &Metadata, users: &Users) -> std::rc::Rc<str> {
+ match users.get_user_by_uid(metadata.uid()) {
+ Some(name) => std::rc::Rc::from(name.as_str()),
+ None => std::rc::Rc::from(format!("{}", metadata.uid()).as_str()),
+ }
+}
+
+/// Reads the group name and returns it as a string.
+/// If it's not possible to get the group name (happens if the group exists on a remote machine but not on host),
+/// it returns the gid as a `Result<String>`.
+fn extract_group(metadata: &Metadata, users: &Users) -> std::rc::Rc<str> {
+ match users.get_group_by_gid(metadata.gid()) {
+ Some(name) => std::rc::Rc::from(name.as_str()),
+ None => std::rc::Rc::from(format!("{}", metadata.gid()).as_str()),
+ }
+}
+
+/// Returns the file size.
+fn extract_file_size(metadata: &Metadata) -> u64 {
+ metadata.len()
+}
+
+/// Extract the major & minor driver version of a special file.
+/// It's used for CharDevice & BlockDevice
+fn major_minor(metadata: &Metadata) -> (u8, u8) {
+ let device_ids = metadata.rdev().to_be_bytes();
+ let major = device_ids[6];
+ let minor = device_ids[7];
+ (major, minor)
+}
+
+/// Extract the optional extension from a filename.
+/// Returns empty &str aka "" if the file has no extension.
+pub fn extract_extension(path: &path::Path) -> &str {
+ path.extension()
+ .and_then(std::ffi::OsStr::to_str)
+ .unwrap_or_default()
+}
+
+fn filekind_and_filename(filename: &str, file_kind: &FileKind<Valid>) -> std::rc::Rc<str> {
+ std::rc::Rc::from(format!("{c}{filename}", c = file_kind.sortable_char()).as_str())
+}
+
+/// true iff the path is a valid symlink (pointing to an existing file).
+fn is_valid_symlink(path: &path::Path) -> bool {
+ matches!(std::fs::read_link(path), Ok(dest) if dest.exists())
+}