use super::*;
#[cfg(unix)]
fn format_attributes(metadata: &fs::Metadata, is_dir: bool, is_symlink: bool) -> String {
use std::os::unix::fs::PermissionsExt;
let kind = if is_symlink {
'l'
} else if is_dir {
'd'
} else {
'-'
};
let mut text = String::with_capacity(10);
text.push(kind);
text.push_str(&mode_string(metadata.permissions().mode()));
text
}
#[cfg(unix)]
pub fn mode_string(mode: u32) -> String {
let triplet = |shift: u32, special: u32, on: char| {
let bits = mode >> shift;
let execute = bits & 0o1 != 0;
[
if bits & 0o4 != 0 { 'r' } else { '-' },
if bits & 0o2 != 0 { 'w' } else { '-' },
match (mode & special != 0, execute) {
(true, true) => on,
(true, false) => on.to_ascii_uppercase(),
(false, true) => 'x',
(false, false) => '-',
},
]
};
let mut text = String::with_capacity(9);
text.extend(triplet(6, 0o4000, 's'));
text.extend(triplet(3, 0o2000, 's'));
text.extend(triplet(0, 0o1000, 't'));
text
}
#[cfg(windows)]
fn format_attributes(metadata: &fs::Metadata, _is_dir: bool, is_symlink: bool) -> String {
use std::os::windows::fs::MetadataExt;
const READONLY: u32 = 0x0000_0001;
const HIDDEN: u32 = 0x0000_0002;
const SYSTEM: u32 = 0x0000_0004;
const DIRECTORY: u32 = 0x0000_0010;
const ARCHIVE: u32 = 0x0000_0020;
let flags = metadata.file_attributes();
let flag = |bit: u32, on: char| if flags & bit != 0 { on } else { '-' };
let kind = if is_symlink { 'l' } else { flag(DIRECTORY, 'd') };
[kind, flag(ARCHIVE, 'a'), flag(READONLY, 'r'), flag(HIDDEN, 'h'), flag(SYSTEM, 's')].iter().collect()
}
#[cfg(not(any(unix, windows)))]
fn format_attributes(_metadata: &fs::Metadata, _is_dir: bool, _is_symlink: bool) -> String {
String::new()
}
#[cfg(windows)]
pub fn is_hidden(_name: &str, metadata: Option<&fs::Metadata>) -> bool {
use std::os::windows::fs::MetadataExt;
const HIDDEN: u32 = 0x0000_0002;
metadata.is_some_and(|metadata| metadata.file_attributes() & HIDDEN != 0)
}
#[cfg(not(windows))]
pub fn is_hidden(name: &str, _metadata: Option<&fs::Metadata>) -> bool {
name.starts_with('.')
}
pub fn load_directory_rows(path: &Path, options: &Options) -> Result<Vec<Item>, Error> {
let entries: Vec<_> = read_dir(path)?.filter_map(|entry| entry.ok()).collect();
let has_parent = path.parent().is_some();
let mut children = Vec::with_capacity(entries.len() + usize::from(has_parent));
if has_parent {
children.push(Item {
name_os: OsString::from(".."),
name_full: "..".to_string(),
name: "..".to_string(),
extension: String::new(),
is_dir: true,
is_symlink: false,
size: String::new(),
size_bytes: 0,
modified_at: None,
attributes: String::new(),
});
}
for entry in &entries {
let entry_path = entry.path();
let is_symlink = entry.file_type().map(|file_type| file_type.is_symlink()).unwrap_or(false);
let metadata = if is_symlink { fs::metadata(&entry_path).or_else(|_| entry.metadata()).ok() } else { entry.metadata().ok() };
let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
let name_os = entry.file_name();
let name_full = name_os.to_string_lossy().into_owned();
if !options.show_hidden && is_hidden(&name_full, metadata.as_ref()) {
continue;
}
let name = if is_dir { name_full.clone() } else { entry_path.file_stem().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default() };
let extension = if is_dir { String::new() } else { entry_path.extension().map(|e| e.to_string_lossy().into_owned()).unwrap_or_default() };
let size_bytes = if is_dir { 0 } else { metadata.as_ref().map(|m| m.len()).unwrap_or(0) };
let size = if is_dir { "<DIR>".to_string() } else { format_size(size_bytes) };
let modified_at = metadata.as_ref().and_then(|m| m.modified().ok());
let attributes = metadata.as_ref().map(|metadata| format_attributes(metadata, is_dir, is_symlink)).unwrap_or_default();
children.push(Item {
name_os,
name_full,
name,
extension,
is_dir,
is_symlink,
size,
size_bytes,
modified_at,
attributes,
});
}
let sort_start = usize::from(has_parent);
children[sort_start..].sort_by(|a, b| compare_rows(a, b, options));
Ok(children)
}
fn compare_rows(a: &Item, b: &Item, options: &Options) -> std::cmp::Ordering {
use std::cmp::Ordering;
if options.dirs_first {
match (a.is_dir, b.is_dir) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
_ => {}
}
}
let text = |a: &str, b: &str| if options.case_sensitive { a.cmp(b) } else { a.to_lowercase().cmp(&b.to_lowercase()) };
let name = || text(&a.name_full, &b.name_full);
let order = match options.sort_key {
SortKey::Name => name(),
SortKey::Extension => text(&a.extension, &b.extension).then_with(name),
SortKey::Size => a.size_bytes.cmp(&b.size_bytes).then_with(name),
SortKey::Modified => a.modified_at.cmp(&b.modified_at).then_with(name),
};
if options.sort_descending { order.reverse() } else { order }
}
pub fn git_branch(dir: &Path) -> Option<String> {
for ancestor in dir.ancestors() {
let dot_git = ancestor.join(".git");
let Ok(metadata) = fs::metadata(&dot_git) else {
continue;
};
let git_dir = if metadata.is_dir() {
dot_git
} else {
let text = fs::read_to_string(&dot_git).ok()?;
ancestor.join(text.trim().strip_prefix("gitdir:")?.trim())
};
let head = fs::read_to_string(git_dir.join("HEAD")).ok()?;
let head = head.trim();
return Some(match head.strip_prefix("ref:") {
Some(reference) => {
let reference = reference.trim();
reference.strip_prefix("refs/heads/").unwrap_or(reference).to_string()
}
None => head.chars().take(7).collect(),
});
}
None
}
#[cfg(unix)]
pub fn user_at_host() -> (String, bool) {
let uid = unsafe { libc::geteuid() };
let mut buffer = [0 as libc::c_char; 256];
let host = if unsafe { libc::gethostname(buffer.as_mut_ptr(), buffer.len() - 1) } == 0 {
unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) }.to_string_lossy().into_owned()
} else {
String::new()
};
let host = host.split('.').next().unwrap_or_default().to_string();
(format!("{}@{}", user_name(uid), host), uid == 0)
}
#[cfg(not(unix))]
pub fn user_at_host() -> (String, bool) {
let variable = |name: &str| std::env::var(name).unwrap_or_default();
(format!("{}@{}", variable("USERNAME"), variable("COMPUTERNAME")), false)
}
pub fn calculate_dir_size(path: &Path) -> Result<u64, Error> {
walk_dir_size(path, &AtomicBool::new(false), &mut |_| {}).map(Option::unwrap_or_default)
}
pub fn walk_dir_size(path: &Path, cancel: &AtomicBool, found: &mut dyn FnMut(u64)) -> Result<Option<u64>, Error> {
let mut total = 0u64;
for entry in read_dir(path)? {
if cancel.load(Ordering::Relaxed) {
return Ok(None);
}
let Ok(entry) = entry else { continue };
let Ok(file_type) = entry.file_type() else { continue };
if file_type.is_dir() {
match walk_dir_size(&entry.path(), cancel, found) {
Ok(Some(size)) => total += size,
Ok(None) => return Ok(None),
Err(_) => {}
}
} else if let Ok(metadata) = entry.metadata() {
total += metadata.len();
found(metadata.len());
}
}
Ok(Some(total))
}
pub struct Description {
pub size_bytes: Option<u64>,
pub modified: Option<std::time::SystemTime>,
pub owner: String,
pub attributes: String,
pub link: Option<String>,
}
pub fn describe(path: &Path, is_dir: bool) -> Option<Description> {
let metadata = fs::symlink_metadata(path).ok()?;
let is_symlink = metadata.file_type().is_symlink();
let followed = if is_symlink { fs::metadata(path).ok() } else { None };
let size_bytes = if is_symlink { followed.as_ref().map(|target| target.len()) } else { (!is_dir).then_some(metadata.len()) };
Some(Description {
size_bytes,
modified: metadata.modified().ok(),
owner: owner_of(&metadata),
attributes: format_attributes(&metadata, is_dir, is_symlink),
link: is_symlink.then(|| fs::read_link(path).ok()).flatten().map(|target| target.display().to_string()),
})
}