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:
2026-08-16 09:11:09 -04:00
parent 3e1b66809a
commit 57234b7a38
8 changed files with 784 additions and 167 deletions
+73 -145
View File
@@ -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();