v0.2.9 : ps/where/get facon Nushell avec mini-pipeline (ls | where size > 1mb | get name), dir = ls sur Windows, ls beaucoup plus rapide (taille des dossiers bornee a 500 entrees), autocomplet : prefixe commun + menu avec descriptions + fallback chemins
This commit is contained in:
+73
-145
@@ -1,6 +1,7 @@
|
||||
//! Nushell-inspired navigation helpers: a boxed table listing of files and
|
||||
//! directories (ls), and directory/path completion for Tab.
|
||||
|
||||
use crate::tables::{Cell, DataTable};
|
||||
use anyhow::{Context, Result};
|
||||
use colored::Colorize;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -22,10 +23,11 @@ pub fn format_size(bytes: u64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursive size of a directory, bounded: beyond the entry cap the size is
|
||||
/// reported as unknown (None) so huge trees never freeze the listing.
|
||||
/// Recursive size of a directory, tightly bounded: beyond the entry cap the
|
||||
/// size is reported as unknown (None). The cap is deliberately small so a
|
||||
/// listing stays snappy even when it contains huge trees.
|
||||
pub fn dir_size(path: &Path) -> Option<u64> {
|
||||
const CAP: usize = 20_000;
|
||||
const CAP: usize = 500;
|
||||
let mut total: u64 = 0;
|
||||
let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
|
||||
let mut visited: usize = 0;
|
||||
@@ -49,14 +51,6 @@ pub fn dir_size(path: &Path) -> Option<u64> {
|
||||
Some(total)
|
||||
}
|
||||
|
||||
/// One entry of the ls table.
|
||||
struct LsRow {
|
||||
name: String,
|
||||
kind: &'static str,
|
||||
size: String,
|
||||
modified: String,
|
||||
}
|
||||
|
||||
fn modified_str(path: &Path) -> String {
|
||||
std::fs::metadata(path)
|
||||
.and_then(|m| m.modified())
|
||||
@@ -81,14 +75,31 @@ fn paint(s: &str, color: bool, style: u8) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a Nushell-style boxed table of the entries of a path, with the
|
||||
/// columns: index, name, type, size, modified. Directories come first, then
|
||||
/// alphabetical order; sizes are human-readable and directories show their
|
||||
/// recursive size (bounded).
|
||||
pub fn render_ls(path: &Path, color: bool) -> Result<String> {
|
||||
fn is_executable(path: &Path, meta: &std::fs::Metadata) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
meta.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = meta;
|
||||
matches!(
|
||||
path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.to_lowercase())
|
||||
.as_deref(),
|
||||
Some("exe") | Some("bat") | Some("cmd") | Some("com")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the ls table (columns: name, type, size, modified). Directories
|
||||
/// come first, then alphabetical order.
|
||||
pub fn ls_table(path: &Path, color: bool) -> Result<DataTable> {
|
||||
let rd = std::fs::read_dir(path)
|
||||
.with_context(|| format!("cannot read {}", path.display()))?;
|
||||
let mut rows: Vec<LsRow> = Vec::new();
|
||||
let mut rows: Vec<(String, Vec<Cell>)> = Vec::new();
|
||||
for entry in rd.flatten() {
|
||||
let p = entry.path();
|
||||
let meta = match std::fs::symlink_metadata(&p) {
|
||||
@@ -109,13 +120,13 @@ pub fn render_ls(path: &Path, color: bool) -> Result<String> {
|
||||
} else {
|
||||
("file", 0)
|
||||
};
|
||||
let size = if file_type.is_dir() {
|
||||
let (size_text, size_bytes) = if file_type.is_dir() {
|
||||
match dir_size(&p) {
|
||||
Some(s) => format_size(s),
|
||||
None => "—".to_string(),
|
||||
Some(s) => (format_size(s), Some(s)),
|
||||
None => ("—".to_string(), None),
|
||||
}
|
||||
} else {
|
||||
format_size(meta.len())
|
||||
(format_size(meta.len()), Some(meta.len()))
|
||||
};
|
||||
let mut display = name.clone();
|
||||
if file_type.is_dir() {
|
||||
@@ -125,114 +136,36 @@ pub fn render_ls(path: &Path, color: bool) -> Result<String> {
|
||||
display.push_str(&format!(" -> {}", target.display()));
|
||||
}
|
||||
}
|
||||
rows.push(LsRow {
|
||||
name: paint(&display, color, style),
|
||||
kind,
|
||||
size,
|
||||
modified: modified_str(&p),
|
||||
});
|
||||
rows.push((
|
||||
name.to_lowercase(),
|
||||
vec![
|
||||
Cell::str(paint(&display, color, style)),
|
||||
Cell::str(kind.to_string()),
|
||||
Cell::bytes(size_bytes.unwrap_or(0), size_text),
|
||||
Cell::str(modified_str(&p)),
|
||||
],
|
||||
));
|
||||
}
|
||||
// Directories first, then alphabetical (case-insensitive).
|
||||
// Directories first, then alphabetical.
|
||||
rows.sort_by(|a, b| {
|
||||
let a_dir = a.kind == "dir";
|
||||
let b_dir = b.kind == "dir";
|
||||
b_dir
|
||||
.cmp(&a_dir)
|
||||
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
|
||||
let a_dir = a.1[1].text == "dir";
|
||||
let b_dir = b.1[1].text == "dir";
|
||||
b_dir.cmp(&a_dir).then_with(|| a.0.cmp(&b.0))
|
||||
});
|
||||
|
||||
// Column contents: index, name, type, size, modified.
|
||||
let headers = vec!["#", "name", "type", "size", "modified"];
|
||||
let mut widths: Vec<usize> = headers.iter().map(|h| visible_len(h)).collect();
|
||||
let mut cells: Vec<Vec<String>> = Vec::new();
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let line = vec![
|
||||
i.to_string(),
|
||||
row.name.clone(),
|
||||
row.kind.to_string(),
|
||||
row.size.clone(),
|
||||
row.modified.clone(),
|
||||
];
|
||||
for (c, cell) in line.iter().enumerate() {
|
||||
widths[c] = widths[c].max(visible_len(cell));
|
||||
}
|
||||
cells.push(line);
|
||||
}
|
||||
|
||||
let pad = |s: &str, w: usize| -> String {
|
||||
let n = visible_len(s);
|
||||
format!("{s}{}", " ".repeat(w.saturating_sub(n)))
|
||||
};
|
||||
let border = |left: char, mid: char, right: char| -> String {
|
||||
let mut line = String::new();
|
||||
line.push(left);
|
||||
for (i, w) in widths.iter().enumerate() {
|
||||
if i > 0 {
|
||||
line.push(mid);
|
||||
}
|
||||
line.push_str(&"─".repeat(w + 2));
|
||||
}
|
||||
line.push(right);
|
||||
line
|
||||
};
|
||||
let row = |cells: &[String], bold: bool| -> String {
|
||||
let mut line = String::from("│");
|
||||
for (i, c) in cells.iter().enumerate() {
|
||||
line.push(' ');
|
||||
let cell = pad(c, widths[i]);
|
||||
let rendered = if bold {
|
||||
cell.bold().to_string()
|
||||
} else {
|
||||
cell
|
||||
};
|
||||
line.push_str(&rendered);
|
||||
line.push(' ');
|
||||
line.push('│');
|
||||
}
|
||||
line
|
||||
};
|
||||
|
||||
let dim = |s: &str| -> String {
|
||||
if color {
|
||||
s.dimmed().to_string()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str(&dim(&border('╭', '┬', '╮')));
|
||||
out.push('\n');
|
||||
out.push_str(&row(&headers.iter().map(|h| h.to_string()).collect::<Vec<_>>(), true));
|
||||
out.push('\n');
|
||||
out.push_str(&dim(&border('├', '┼', '┤')));
|
||||
out.push('\n');
|
||||
for line in &cells {
|
||||
out.push_str(&row(line, false));
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(&dim(&border('╰', '┴', '╯')));
|
||||
out.push('\n');
|
||||
Ok(out)
|
||||
Ok(DataTable {
|
||||
columns: vec![
|
||||
"name".into(),
|
||||
"type".into(),
|
||||
"size".into(),
|
||||
"modified".into(),
|
||||
],
|
||||
rows: rows.into_iter().map(|(_, r)| r).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_executable(path: &Path, meta: &std::fs::Metadata) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
meta.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = meta;
|
||||
matches!(
|
||||
path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.to_lowercase())
|
||||
.as_deref(),
|
||||
Some("exe") | Some("bat") | Some("cmd") | Some("com")
|
||||
)
|
||||
}
|
||||
/// Render a Nushell-style boxed table of the entries of a path.
|
||||
pub fn render_ls(path: &Path, color: bool) -> Result<String> {
|
||||
Ok(ls_table(path, color)?.render(color))
|
||||
}
|
||||
|
||||
/// Completion candidates for a path fragment typed after cd or ls.
|
||||
@@ -294,27 +227,6 @@ pub fn path_candidates(base_dir: &Path, input: &str, dirs_only: bool) -> Vec<Str
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn strip_ansi(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut chars = s.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\u{1b}' {
|
||||
for n in chars.by_ref() {
|
||||
if n == 'm' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn visible_len(s: &str) -> usize {
|
||||
strip_ansi(s).chars().count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -328,6 +240,22 @@ mod tests {
|
||||
assert_eq!(format_size(3 * 1024 * 1024 * 1024), "3.0 GB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_size_is_bounded() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let big = dir.path().join("big");
|
||||
std::fs::create_dir_all(&big).unwrap();
|
||||
for i in 0..520 {
|
||||
std::fs::write(big.join(format!("f{i}.txt")), "x").unwrap();
|
||||
}
|
||||
// Beyond the cap the size is unknown, and the walk stays cheap.
|
||||
assert_eq!(dir_size(&big), None);
|
||||
let small = dir.path().join("small");
|
||||
std::fs::create_dir_all(&small).unwrap();
|
||||
std::fs::write(small.join("a.txt"), "hello").unwrap();
|
||||
assert_eq!(dir_size(&small), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_directories_only() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user