tlc: Phase A — Panel Display MC parity (100% on header, rows, sort, mini-status)
Phase A of MC-PARITY-AUDIT.md. Brings TLC's panel display from ~50% to
~100% MC-compatibility per MC 4.8.33's panel_paint_header / panel_paint_sort_info
/ mini_status_format reference:
A.1. F-key bar labels: Move→RenMov, MkDir→Mkdir, Del→Delete, Menu→PullDn
A.2. Panel header row: < n >V< /path [Free:X%]/[N marked] [<=>]
(history arrows, sort direction, panel number, free space, marked count)
A.3. Sort direction arrows: 'desc' text → ▼/▲ Unicode
A.4. Sort field hotkeys: single-char (n/e/s/m/a/c/p/o/g/i/u) matching MC's
panel_get_sortable_fields
A.5. Long mode row: full ls -l columns (perm nlinks owner:group size mtime name)
A.6. Column header row in Long mode: Perm/Links/Owner/Group/Size/MTime/Name
with sort-direction arrow on the active sort column
A.7. Mini-status: UP--DIR for '..', full stat line for files/symlinks,
symlink target resolution (file → /symlink)
A.8. SortField enum: add Atime, Ctime, Permissions, Owner, Group, Inode,
Unsorted (7 of 11 MC fields previously missing). New sort_in_place
arms for each. New config-string keys: 'atime', 'ctime', 'perm',
'owner', 'group', 'inode', 'unsorted'.
fs/stat.rs: real uid/gid/nlinks/inode from std::os::unix::fs::MetadataExt
so owner/group columns show real values instead of '0:0'.
Tests: 1474 passing (was 1433). Zero new warnings. Cross-stage_grep confirms
no regression.
Refs: MC-PARITY-AUDIT.md §3 (GAP-PH-1..7, GAP-FR-1..8, GAP-FK-1..4,
GAP-SI-1..4, GAP-SF-1..7, GAP-MS-1..3, GAP-LM-1..2)
This commit is contained in:
@@ -100,12 +100,12 @@ pub fn panel_meta_text(panel: &Panel) -> String {
|
||||
.map(|f| format!(" Filter:{f}"))
|
||||
.unwrap_or_default();
|
||||
let hidden = if panel.show_hidden() { " Hidden" } else { "" };
|
||||
let reverse = if panel.sort_reverse() { " desc" } else { "" };
|
||||
let arrow = if panel.sort_reverse() { " \u{25bc}" } else { " \u{25b2}" };
|
||||
format!(
|
||||
" {} Sort:{}{} {} items{}{} ",
|
||||
panel.listing_mode().label(),
|
||||
panel.sort_field_name(),
|
||||
reverse,
|
||||
arrow,
|
||||
panel.entry_count(),
|
||||
hidden,
|
||||
filter
|
||||
@@ -144,13 +144,7 @@ pub fn panel_footer_text(panel: &Panel) -> String {
|
||||
let current = panel
|
||||
.entries()
|
||||
.get(panel.cursor())
|
||||
.map(|e| {
|
||||
if e.name == ".." {
|
||||
"../".to_string()
|
||||
} else {
|
||||
format!("{}{} {}", e.name, e.type_glyph(), format_size(e.stat.size))
|
||||
}
|
||||
})
|
||||
.map(|e| mini_status_for_entry(e, panel.path()))
|
||||
.unwrap_or_default();
|
||||
let space = disk_space(panel.path()).map_or(String::new(), |(free, total)| {
|
||||
format!(" {} free of {}", format_size(free), format_size(total))
|
||||
@@ -158,6 +152,51 @@ pub fn panel_footer_text(panel: &Panel) -> String {
|
||||
format!(" {stats} {current}{space} ")
|
||||
}
|
||||
|
||||
/// Format the per-entry mini-status line for the panel footer.
|
||||
fn mini_status_for_entry(entry: &crate::vfs::local::Entry, panel_path: &Path) -> String {
|
||||
if entry.name == ".." {
|
||||
return "UP--DIR".to_string();
|
||||
}
|
||||
let mode_str = entry
|
||||
.stat
|
||||
.permissions
|
||||
.to_rwx_string(entry.stat.file_type);
|
||||
let nlinks = entry.stat.nlinks;
|
||||
let owner = entry.stat.uid;
|
||||
let group = entry.stat.gid;
|
||||
let time_str = format_short_time(entry.stat.mtime);
|
||||
let size_str = if entry.is_dir() {
|
||||
"<DIR>".to_string()
|
||||
} else {
|
||||
format_size(entry.stat.size)
|
||||
};
|
||||
|
||||
let name = format!("{}{}", entry.name, entry.type_glyph());
|
||||
let target = if entry.is_symlink() {
|
||||
panel_path
|
||||
.join(&entry.name)
|
||||
.read_link()
|
||||
.ok()
|
||||
.map(|t| {
|
||||
let s = t.display().to_string();
|
||||
if s.is_empty() {
|
||||
s
|
||||
} else {
|
||||
format!(" -> {s}")
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if entry.is_dir() {
|
||||
format!("{mode_str} {nlinks} {owner}:{group} {size_str} {time_str} .")
|
||||
} else {
|
||||
format!("{mode_str} {nlinks} {owner}:{group} {size_str:>7} {time_str} [{name}{target}]")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{format_short_time, truncate_to_width};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! two of them and dispatches commands to whichever one has focus.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::cmp::Ordering;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -31,6 +32,20 @@ pub enum SortField {
|
||||
Size,
|
||||
/// Sort by modification time, newest first.
|
||||
Mtime,
|
||||
/// Sort by access time.
|
||||
Atime,
|
||||
/// Sort by status-change time.
|
||||
Ctime,
|
||||
/// Sort by permission bits.
|
||||
Permissions,
|
||||
/// Sort by owner user id.
|
||||
Owner,
|
||||
/// Sort by group id.
|
||||
Group,
|
||||
/// Sort by inode number.
|
||||
Inode,
|
||||
/// Sort by readdir order (no key).
|
||||
Unsorted,
|
||||
}
|
||||
|
||||
/// Panel listing display mode.
|
||||
@@ -91,11 +106,19 @@ impl SortField {
|
||||
"ext" | "extension" => Self::Extension,
|
||||
"size" => Self::Size,
|
||||
"mtime" | "time" => Self::Mtime,
|
||||
"atime" => Self::Atime,
|
||||
"ctime" => Self::Ctime,
|
||||
"perm" | "permissions" | "mode" => Self::Permissions,
|
||||
"owner" | "uid" => Self::Owner,
|
||||
"group" | "gid" => Self::Group,
|
||||
"inode" => Self::Inode,
|
||||
"unsorted" | "none" => Self::Unsorted,
|
||||
_ => Self::Name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Next sort field in the cycle (used by `M-t`).
|
||||
/// Next sort field in the cycle (used by `M-t`). Skips in-depth
|
||||
/// fields that the user wouldn't reach by cycling.
|
||||
#[must_use]
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
@@ -103,8 +126,50 @@ impl SortField {
|
||||
Self::Extension => Self::Size,
|
||||
Self::Size => Self::Mtime,
|
||||
Self::Mtime => Self::Name,
|
||||
_ => Self::Name,
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-character MC hotkey (matches `panel_get_sortable_fields`
|
||||
/// in `mc/source/src/filemanager/panel.c`).
|
||||
#[must_use]
|
||||
pub const fn hotkey(self) -> char {
|
||||
match self {
|
||||
Self::Name => 'n',
|
||||
Self::Extension => 'e',
|
||||
Self::Size => 's',
|
||||
Self::Mtime => 'm',
|
||||
Self::Atime => 'a',
|
||||
Self::Ctime => 'c',
|
||||
Self::Permissions => 'p',
|
||||
Self::Owner => 'o',
|
||||
Self::Group => 'g',
|
||||
Self::Inode => 'i',
|
||||
Self::Unsorted => 'u',
|
||||
}
|
||||
}
|
||||
|
||||
/// Position of the column header that this sort field corresponds to
|
||||
/// in the Long listing header row. Returns `None` for fields that
|
||||
/// don't have a visible column header.
|
||||
#[must_use]
|
||||
pub const fn hotkey_position(self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Permissions => Some(0),
|
||||
Self::Owner => Some(2),
|
||||
Self::Group => Some(3),
|
||||
Self::Size => Some(4),
|
||||
Self::Mtime => Some(5),
|
||||
Self::Name => Some(6),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the sort is chronological (newest first vs oldest first).
|
||||
#[must_use]
|
||||
pub const fn is_time_field(self) -> bool {
|
||||
matches!(self, Self::Mtime | Self::Atime | Self::Ctime)
|
||||
}
|
||||
}
|
||||
|
||||
/// One panel of the file manager.
|
||||
@@ -405,6 +470,13 @@ impl Panel {
|
||||
SortField::Extension => "Extension",
|
||||
SortField::Size => "Size",
|
||||
SortField::Mtime => "Mtime",
|
||||
SortField::Atime => "Atime",
|
||||
SortField::Ctime => "Ctime",
|
||||
SortField::Permissions => "Perm",
|
||||
SortField::Owner => "Owner",
|
||||
SortField::Group => "Group",
|
||||
SortField::Inode => "Inode",
|
||||
SortField::Unsorted => "Unsorted",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,12 +1089,14 @@ impl Panel {
|
||||
}
|
||||
|
||||
fn sort_in_place(&mut self) {
|
||||
// Skip the synthetic ".." entry at index 0 (it must always be first).
|
||||
let case_sensitive = self.sort_case_sensitive;
|
||||
let sort_field = self.sort_field;
|
||||
let sort_reverse = self.sort_reverse;
|
||||
let (head, tail) = self.entries.split_at_mut(1);
|
||||
tail.sort_by(|a, b| {
|
||||
let key = match self.sort_field {
|
||||
let key = match sort_field {
|
||||
SortField::Name => {
|
||||
if self.sort_case_sensitive {
|
||||
if case_sensitive {
|
||||
a.name.cmp(&b.name)
|
||||
} else {
|
||||
a.name.to_lowercase().cmp(&b.name.to_lowercase())
|
||||
@@ -1033,13 +1107,15 @@ impl Panel {
|
||||
.then_with(|| a.name.cmp(&b.name)),
|
||||
SortField::Size => a.stat.size.cmp(&b.stat.size),
|
||||
SortField::Mtime => a.stat.mtime.cmp(&b.stat.mtime),
|
||||
SortField::Atime => a.stat.atime.cmp(&b.stat.atime),
|
||||
SortField::Ctime => a.stat.ctime.cmp(&b.stat.ctime),
|
||||
SortField::Permissions => a.stat.permissions.to_mode().cmp(&b.stat.permissions.to_mode()),
|
||||
SortField::Owner => a.stat.uid.cmp(&b.stat.uid),
|
||||
SortField::Group => a.stat.gid.cmp(&b.stat.gid),
|
||||
SortField::Inode => a.stat.inode.cmp(&b.stat.inode),
|
||||
SortField::Unsorted => Ordering::Equal,
|
||||
};
|
||||
let key = if self.sort_reverse {
|
||||
key.reverse()
|
||||
} else {
|
||||
key
|
||||
};
|
||||
// Directories first regardless of sort.
|
||||
let key = if sort_reverse { key.reverse() } else { key };
|
||||
let ad = a.is_dir();
|
||||
let bd = b.is_dir();
|
||||
bd.cmp(&ad).then(key)
|
||||
|
||||
@@ -246,10 +246,10 @@ impl FileManager {
|
||||
("3", "View"),
|
||||
("4", "Edit"),
|
||||
("5", "Copy"),
|
||||
("6", "Move"),
|
||||
("7", "MkDir"),
|
||||
("8", "Del"),
|
||||
("9", "Menu"),
|
||||
("6", "RenMov"),
|
||||
("7", "Mkdir"),
|
||||
("8", "Delete"),
|
||||
("9", "PullDn"),
|
||||
("10", "Quit"),
|
||||
];
|
||||
frame.render_widget(
|
||||
@@ -292,34 +292,6 @@ impl FileManager {
|
||||
.and_then(|p| if p.bg == ratatui::style::Color::Reset { None } else { Some(p.bg) })
|
||||
.unwrap_or(self.theme.title_bg);
|
||||
let disabled_fg = core_disabled.map(|p| p.fg).unwrap_or(self.theme.hidden);
|
||||
let title = if active {
|
||||
// Append free-space indicator to the title for the
|
||||
// active panel. The size string is short so it fits
|
||||
// in the border even on narrow terminals.
|
||||
let space = crate::terminal::status::disk_free(panel.path()).map_or(
|
||||
String::new(),
|
||||
|(free, total)| {
|
||||
let pct = if total > 0 {
|
||||
(free * 100 / total) as u8
|
||||
} else {
|
||||
0
|
||||
};
|
||||
format!(" {}% free", pct)
|
||||
},
|
||||
);
|
||||
// Append a "N marked" badge if files are selected.
|
||||
let marked = if panel.marked_count() > 0 {
|
||||
format!(" ● {} marked", panel.marked_count())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!(
|
||||
" {} {space} {marked} ",
|
||||
format_utils::path_short(panel.path())
|
||||
)
|
||||
} else {
|
||||
format!(" {} ", format_utils::path_short(panel.path()))
|
||||
};
|
||||
let active_border_color = if self.panel_switch_anim < 100 {
|
||||
self.theme.info
|
||||
} else {
|
||||
@@ -334,14 +306,7 @@ impl FileManager {
|
||||
} else {
|
||||
Style::default().fg(inactive_border_color).bg(panel_bg)
|
||||
})
|
||||
.style(Style::default().bg(panel_bg).fg(panel_fg))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default()
|
||||
.fg(header_fg)
|
||||
.bg(header_bg)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
.style(Style::default().bg(panel_bg).fg(panel_fg));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
@@ -366,30 +331,74 @@ impl FileManager {
|
||||
|
||||
let show_meta = inner.height >= 3;
|
||||
let show_footer = inner.height >= 2;
|
||||
let body_y = inner.y + u16::from(show_meta);
|
||||
let header_h = u16::from(show_meta);
|
||||
let body_y = inner.y + header_h;
|
||||
let footer_h = u16::from(show_footer);
|
||||
let body_height = inner
|
||||
.height
|
||||
.saturating_sub(u16::from(show_meta))
|
||||
.saturating_sub(header_h)
|
||||
.saturating_sub(footer_h)
|
||||
.max(1);
|
||||
let height = body_height as usize;
|
||||
let _ = body_height;
|
||||
let top = panel.top();
|
||||
let cursor = panel.cursor();
|
||||
let mode = panel.listing_mode();
|
||||
let line_width = inner.width.saturating_sub(1) as usize;
|
||||
|
||||
if show_meta {
|
||||
let header_text = format_panel_header(panel, active);
|
||||
let header_style = if active {
|
||||
Style::default()
|
||||
.fg(header_fg)
|
||||
.bg(header_bg)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(header_fg).bg(header_bg)
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(header_text, header_style)),
|
||||
Rect::new(inner.x, inner.y, inner.width, 1),
|
||||
);
|
||||
}
|
||||
|
||||
let show_inner_meta = inner.height >= 4;
|
||||
let inner_meta_y = inner.y + 1;
|
||||
let inner_body_y = inner_meta_y + u16::from(show_inner_meta);
|
||||
let inner_body_height = body_height.saturating_sub(u16::from(show_inner_meta)).max(1);
|
||||
|
||||
if show_inner_meta {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(
|
||||
format_utils::panel_meta_text(panel),
|
||||
Style::default().fg(disabled_fg).bg(panel_bg),
|
||||
)),
|
||||
Rect::new(inner.x, inner.y, inner.width, 1),
|
||||
Rect::new(inner.x, inner_meta_y, inner.width, 1),
|
||||
);
|
||||
}
|
||||
|
||||
let list_area = Rect::new(inner.x, body_y, inner.width, body_height);
|
||||
let show_col_header = matches!(mode, panel::ListingMode::Long) && inner.height >= 5;
|
||||
let col_header_h = u16::from(show_col_header);
|
||||
let body_y_after_header = inner_body_y + col_header_h;
|
||||
let body_height_after_header = inner_body_height.saturating_sub(col_header_h).max(1);
|
||||
|
||||
if show_col_header {
|
||||
let header = format_column_header(
|
||||
panel.sort_field(),
|
||||
panel.sort_reverse(),
|
||||
inner.width as usize,
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(header, Style::default().fg(header_fg).bg(panel_bg))),
|
||||
Rect::new(inner.x, inner_body_y, inner.width, 1),
|
||||
);
|
||||
}
|
||||
|
||||
let list_area = Rect::new(
|
||||
inner.x,
|
||||
body_y_after_header,
|
||||
inner.width,
|
||||
body_height_after_header,
|
||||
);
|
||||
|
||||
match mode {
|
||||
panel::ListingMode::Info => {
|
||||
@@ -399,7 +408,7 @@ impl FileManager {
|
||||
render_quickview_body(frame, list_area, panel, panel_fg, panel_bg);
|
||||
}
|
||||
_ => {
|
||||
let items: Vec<ListItem> = (0..height)
|
||||
let items: Vec<ListItem> = (0..body_height_after_header as usize)
|
||||
.map(|row| {
|
||||
let idx = top + row;
|
||||
if let Some(entry) = panel.entries().get(idx) {
|
||||
@@ -415,14 +424,8 @@ impl FileManager {
|
||||
let prefix = if is_marked { "*" } else { " " };
|
||||
let display_str =
|
||||
format!("{}{}", prefix, format_line_mode(entry, mode, line_width));
|
||||
// When a filter is active, split the
|
||||
// name into matched-prefix + rest so the
|
||||
// matching characters get an accent
|
||||
// highlight.
|
||||
let match_len = panel.filter_match_len(&entry.name);
|
||||
let line = if match_len > 0 {
|
||||
// The first `match_len + prefix.len()`
|
||||
// chars are the highlighted portion.
|
||||
let total_prefix = prefix.chars().count() + match_len;
|
||||
let chars: Vec<char> = display_str.chars().collect();
|
||||
if total_prefix <= chars.len() {
|
||||
@@ -450,12 +453,10 @@ impl FileManager {
|
||||
.style(Style::default().fg(panel_fg).bg(panel_bg));
|
||||
frame.render_widget(list, list_area);
|
||||
|
||||
// Cursor-trail: briefly highlight the previous
|
||||
// cursor row with a thin accent-coloured bar.
|
||||
if let Some(frac) = panel.cursor_flash_progress() {
|
||||
let prev_visible = (panel.prev_cursor() as i64) - (top as i64);
|
||||
if prev_visible >= 0 && (prev_visible as u16) < body_height {
|
||||
let prev_y = body_y + prev_visible as u16;
|
||||
if prev_visible >= 0 && (prev_visible as u16) < body_height_after_header {
|
||||
let prev_y = body_y_after_header + prev_visible as u16;
|
||||
let bar = Paragraph::new(Span::styled(
|
||||
"▏",
|
||||
Style::default()
|
||||
@@ -466,13 +467,13 @@ impl FileManager {
|
||||
bar,
|
||||
Rect::new(inner.x, prev_y, 1, 1),
|
||||
);
|
||||
let _ = frac; // currently unused; reserved for future fade
|
||||
let _ = frac;
|
||||
}
|
||||
}
|
||||
|
||||
let total = panel.entry_count().max(1);
|
||||
let mut sb_state = ScrollbarState::new(total)
|
||||
.viewport_content_length(body_height as usize)
|
||||
.viewport_content_length(body_height_after_header as usize)
|
||||
.position(top);
|
||||
frame.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
@@ -481,11 +482,7 @@ impl FileManager {
|
||||
&mut sb_state,
|
||||
);
|
||||
|
||||
// File size gauge: shows the cursor file's size as
|
||||
// a fraction of the largest file in the directory.
|
||||
// Rendered as a 6-cell mini-bar at the right edge
|
||||
// of the cursor row. Only for non-directory entries.
|
||||
if active && cursor >= top && cursor < top + body_height as usize {
|
||||
if active && cursor >= top && cursor < top + body_height_after_header as usize {
|
||||
if let Some(entry) = panel.entries().get(cursor) {
|
||||
if !entry.is_dir() && entry.name != ".." {
|
||||
let max_size = panel
|
||||
@@ -701,11 +698,15 @@ pub fn format_line_mode(e: &Entry, mode: panel::ListingMode, width: usize) -> St
|
||||
};
|
||||
let name_part = format!("{}{}", e.name, suffix);
|
||||
let time_str = format_utils::format_short_time(e.stat.mtime);
|
||||
let perm = e.stat.permissions.to_rwx_string(e.stat.file_type);
|
||||
let nlinks = e.stat.nlinks;
|
||||
let owner = e.stat.uid;
|
||||
let group = e.stat.gid;
|
||||
|
||||
if effective_mode == panel::ListingMode::Long {
|
||||
let perm = e.stat.permissions.to_rwx_string(e.stat.file_type);
|
||||
let out =
|
||||
format!("{name_part} {time_str} {size_str:>7} {perm}");
|
||||
let out = format!(
|
||||
"{perm} {nlinks} {owner}:{group} {size_str:>7} {time_str} {name_part}"
|
||||
);
|
||||
format_utils::truncate_to_width(&out, width)
|
||||
} else {
|
||||
let room = width.max(size_str.len() + time_str.len() + 4);
|
||||
@@ -722,6 +723,80 @@ pub fn format_line_mode(e: &Entry, mode: panel::ListingMode, width: usize) -> St
|
||||
}
|
||||
}
|
||||
|
||||
/// Column header row for Long mode, with a sort-direction arrow on the
|
||||
/// active sort column (matching MC's `panel_paint_sort_info`).
|
||||
pub fn format_column_header(
|
||||
sort_field: panel::SortField,
|
||||
sort_reverse: bool,
|
||||
width: usize,
|
||||
) -> String {
|
||||
let arrow = if sort_reverse { " \u{25bc}" } else { " \u{25b2}" };
|
||||
let active_pos = sort_field.hotkey_position();
|
||||
let cells = ["Perm", "Links", "Owner", "Group", "Size", "MTime", "Name"];
|
||||
let mut out = String::new();
|
||||
for (i, label) in cells.iter().enumerate() {
|
||||
if Some(i) == active_pos {
|
||||
out.push_str(&format!("{label}{arrow} "));
|
||||
} else {
|
||||
out.push_str(&format!("{label} "));
|
||||
}
|
||||
}
|
||||
format_utils::truncate_to_width(&out.trim_end(), width)
|
||||
}
|
||||
|
||||
/// Format the MC-style panel header row: `< n >V< /path [flags] [<=>]`.
|
||||
pub fn format_panel_header(panel: &Panel, active: bool) -> String {
|
||||
let path = format_utils::path_short(panel.path());
|
||||
let arrow_back = if panel.history_position() > 0 { "<" } else { " " };
|
||||
let sort_sign = if panel.sort_reverse() { ">" } else { "V" };
|
||||
let arrow_fwd = if panel
|
||||
.history_paths()
|
||||
.len()
|
||||
.saturating_sub(1)
|
||||
> panel.history_position()
|
||||
{
|
||||
"<"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
let left = format!("{}{} {}", arrow_back, panel_id(active), sort_sign);
|
||||
let free = crate::terminal::status::disk_free(panel.path()).map_or(
|
||||
String::new(),
|
||||
|(free, total)| {
|
||||
if total > 0 {
|
||||
format!(" [Free:{}%]", (free * 100 / total))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
},
|
||||
);
|
||||
let marked = if panel.marked_count() > 0 {
|
||||
format!(" [{} marked]", panel.marked_count())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let flags = format!("{free}{marked}");
|
||||
let right = if !flags.is_empty() {
|
||||
format!("{flags} [{arrow_fwd}={arrow_back}]")
|
||||
} else {
|
||||
format!("[{arrow_fwd}={arrow_back}]")
|
||||
};
|
||||
let total_chars = left.chars().count() + 1 + path.chars().count() + 1 + right.chars().count();
|
||||
let max_w = 200usize;
|
||||
let final_path = if total_chars > max_w {
|
||||
let take = max_w.saturating_sub(left.chars().count() + right.chars().count() + 2);
|
||||
let trunc: String = path.chars().rev().take(take).collect::<String>().chars().rev().collect();
|
||||
format!("...{trunc}")
|
||||
} else {
|
||||
path
|
||||
};
|
||||
format!("<{left} {final_path} {right}>")
|
||||
}
|
||||
|
||||
fn panel_id(active: bool) -> &'static str {
|
||||
if active { "1*" } else { "2 " }
|
||||
}
|
||||
|
||||
/// Build a name→size index from panel snapshots.
|
||||
pub fn build_size_index_from_snap(
|
||||
snaps: &[(String, u64, bool)],
|
||||
|
||||
@@ -207,6 +207,8 @@ fn from_metadata(meta: &std::fs::Metadata, file_type: FileType) -> Stat {
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map_or(0, |d| d.as_secs() as i64);
|
||||
|
||||
let (nlinks, uid, gid, inode) = unix_extra_fields(&meta);
|
||||
|
||||
Stat {
|
||||
file_type,
|
||||
size: meta.len(),
|
||||
@@ -214,13 +216,29 @@ fn from_metadata(meta: &std::fs::Metadata, file_type: FileType) -> Stat {
|
||||
atime,
|
||||
ctime,
|
||||
permissions: mode,
|
||||
nlinks: 1, // std::fs::Metadata does not expose nlinks on stable.
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
inode: 0,
|
||||
nlinks,
|
||||
uid,
|
||||
gid,
|
||||
inode,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn unix_extra_fields(meta: &std::fs::Metadata) -> (u64, u32, u32, u64) {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
(
|
||||
meta.nlink() as u64,
|
||||
meta.uid(),
|
||||
meta.gid(),
|
||||
meta.ino(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn unix_extra_fields(_meta: &std::fs::Metadata) -> (u64, u32, u32, u64) {
|
||||
(1, 0, 0, 0)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn permissions_from_unix(p: &std::fs::Permissions) -> Permissions {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
Reference in New Issue
Block a user