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
+152 -20
View File
@@ -16,6 +16,8 @@ use crate::app::App;
use crate::cli::{Cli, Command, StartArgs};
use crate::commands;
use crate::nav;
use crate::ps;
use crate::tables::DataTable;
use crate::shell::ShellSession;
use anyhow::{anyhow, Result};
use clap::CommandFactory;
@@ -72,7 +74,11 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("import", "restore a backup"),
("shell", "show or switch the system shell"),
("ls", "list files and directories (Nushell-style)"),
("dir", "list files and directories (same as ls)"),
("cd", "change directory — Tab completes folders"),
("ps", "list processes"),
("where", "filter the last table — where size > 1mb"),
("get", "select a column — get name"),
("help", "show this help"),
("version", "print the version"),
("exit", "quit the shell"),
@@ -118,8 +124,8 @@ impl AmCompleter {
commands: vec![
"list", "status", "search", "info", "install", "uninstall", "update",
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "ls", "cd",
"help", "version", "exit",
"self-update", "self-uninstall", "export", "import", "shell", "ls", "dir",
"cd", "ps", "where", "get", "help", "version", "exit",
],
config_sub: vec!["show", "path", "edit", "validate", "add"],
shells: vec!["bash", "zsh", "fish", "powershell", "elvish"],
@@ -171,7 +177,7 @@ impl AmCompleter {
let cwd = current_dir();
pool = crate::nav::path_candidates(&cwd, word, true);
}
"ls" => {
"ls" | "dir" => {
// Complete paths (files and directories).
let cwd = current_dir();
pool = crate::nav::path_candidates(&cwd, word, false);
@@ -194,6 +200,11 @@ impl AmCompleter {
_ => {
if word.starts_with('-') {
pool = self.options.iter().map(|s| s.to_string()).collect();
} else {
// Nushell fallback: complete file paths for external
// (system) commands when nothing else applies.
let cwd = current_dir();
pool = crate::nav::path_candidates(&cwd, word, false);
}
}
}
@@ -406,7 +417,9 @@ pub fn banner_box(
" system doctor · config · completion · self-update · self-uninstall · export · import"
.to_string(),
));
rows.push(inner(" shell ls · cd · shell · help · version · exit".to_string()));
rows.push(inner(
" shell ls · cd · ps · where · get · shell · help · version · exit".to_string(),
));
// Stats footer.
rows.push(blank());
@@ -648,11 +661,17 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
let mut rl: rustyline::Editor<AmCompleter, rustyline::history::FileHistory> =
rustyline::Editor::new()?;
rl.set_helper(Some(AmCompleter::new(app)));
// Nushell-style completion: a single match is inserted immediately;
// otherwise Tab inserts the longest common prefix and the menu lists the
// candidates with their descriptions.
use rustyline::config::{CompletionType, Configurer};
let _ = rl.set_completion_type(CompletionType::List);
let history_path = app.paths.state_file.with_file_name("history.txt");
if rl.load_history(&history_path).is_err() {
// First run: no history yet.
}
let started = Instant::now();
let mut last: Option<DataTable> = None;
loop {
print_status(app, session, started);
match rl.readline("❯ ") {
@@ -662,7 +681,7 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
continue;
}
let _ = rl.add_history_entry(trimmed.as_str());
match handle_input(app, session, &trimmed) {
match handle_input(app, session, &mut last, &trimmed) {
Ok(true) => break,
Ok(false) => {}
Err(e) => app.log.error(&format!("{e:#}")),
@@ -683,6 +702,7 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
/// Plain fallback when no terminal/line editor is available.
fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
let started = Instant::now();
let mut last: Option<DataTable> = None;
loop {
print_status(app, session, started);
eprint!("❯ ");
@@ -695,7 +715,7 @@ fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
if line.is_empty() {
continue;
}
match handle_input(app, session, &line) {
match handle_input(app, session, &mut last, &line) {
Ok(true) => break,
Ok(false) => {}
Err(e) => app.log.error(&format!("{e:#}")),
@@ -705,7 +725,12 @@ fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
}
/// Handle one line of input. Returns true when the session should exit.
fn handle_input(app: &App, session: &mut ShellSession, line: &str) -> Result<bool> {
fn handle_input(
app: &App,
session: &mut ShellSession,
last: &mut Option<DataTable>,
line: &str,
) -> Result<bool> {
match line {
"exit" | "quit" | "q" | "/exit" | "/quit" => return Ok(true),
"help" | "?" | "/help" => {
@@ -732,12 +757,21 @@ fn handle_input(app: &App, session: &mut ShellSession, line: &str) -> Result<boo
}
return Ok(false);
}
handle_line(app, session, line)
handle_line(app, session, last, line)
}
/// Handle one line of input. Returns true when the session should exit
/// (only after a successful self-uninstall: the binary is gone).
fn handle_line(app: &App, session: &mut ShellSession, line: &str) -> Result<bool> {
fn handle_line(
app: &App,
session: &mut ShellSession,
last: &mut Option<DataTable>,
line: &str,
) -> Result<bool> {
// Nushell-style pipelines: ls | where ... | get ...
if line.contains('|') {
return eval_pipeline(app, last, line);
}
// '!' forces system execution even for words that look like am commands.
if let Some(rest) = line.strip_prefix('!') {
let rest = rest.trim();
@@ -762,9 +796,38 @@ fn handle_line(app: &App, session: &mut ShellSession, line: &str) -> Result<bool
shell_command(session, rest.first().map(|s| s.as_str()));
return Ok(false);
}
// Nushell-style directory listing (the system ls stays available as !ls).
if cmd == "ls" {
ls_command(app, rest)?;
// Nushell-style directory listing (the system ls stays available as !ls,
// and dir maps to the same table on Windows).
if cmd == "ls" || (cmd == "dir" && cfg!(windows)) {
let table = nav::ls_table(&ls_target(rest)?, app.color())?;
show_table(app, last, table);
return Ok(false);
}
if cmd == "ps" {
show_table(app, last, ps::ps_table());
return Ok(false);
}
if cmd == "where" {
let base = last
.clone()
.ok_or_else(|| anyhow!("'where' needs a table — run ls or ps first (e.g. ls | where size > 1mb)"))?;
if rest.len() < 3 {
anyhow::bail!("usage: where <column> <op> <value>");
}
let filtered =
crate::tables::filter_where(&base, &rest[0], &rest[1], &rest[2..].join(" "))?;
show_table(app, last, filtered);
return Ok(false);
}
if cmd == "get" {
let base = last
.clone()
.ok_or_else(|| anyhow!("'get' needs a table — run ls or ps first (e.g. ls | get name)"))?;
let field = rest
.first()
.ok_or_else(|| anyhow!("usage: get <column>"))?;
let column = crate::tables::get_column(&base, field)?;
show_table(app, last, column);
return Ok(false);
}
// Everything that is not an am command is a system command.
@@ -910,14 +973,76 @@ fn is_am_command(word: &str) -> bool {
)
}
/// Nushell-style listing of the current (or given) directory.
fn ls_command(app: &App, rest: &[String]) -> Result<()> {
let target = match rest.first() {
/// Resolve the path argument of ls/dir (defaults to the current directory).
fn ls_target(rest: &[String]) -> Result<std::path::PathBuf> {
Ok(match rest.first() {
Some(p) => crate::config::expand_path(p),
None => current_dir(),
};
print!("{}", nav::render_ls(&target, app.color())?);
Ok(())
})
}
/// Print a table and remember it as the last result (for where/get).
fn show_table(app: &App, last: &mut Option<DataTable>, table: DataTable) {
print!("{}", table.render(app.color()));
*last = Some(table);
}
/// Evaluate a Nushell-style pipeline: ls | ps | where | get, in order.
fn eval_pipeline(app: &App, last: &mut Option<DataTable>, line: &str) -> Result<bool> {
let parts: Vec<&str> = line
.split('|')
.map(str::trim)
.filter(|p| !p.is_empty())
.collect();
let mut table: Option<DataTable> = None;
for part in &parts {
let tokens =
shell_words::split(part).map_err(|e| anyhow!("cannot parse command: {e}"))?;
if tokens.is_empty() {
continue;
}
let cmd = tokens[0].as_str();
let rest = &tokens[1..];
match cmd {
"ls" => {
table = Some(nav::ls_table(&ls_target(rest)?, app.color())?);
}
"dir" if cfg!(windows) => {
table = Some(nav::ls_table(&ls_target(rest)?, app.color())?);
}
"ps" => {
table = Some(ps::ps_table());
}
"where" => {
let base = table
.as_ref()
.ok_or_else(|| anyhow!("'where' needs a table before it"))?;
if rest.len() < 3 {
anyhow::bail!("usage: where <column> <op> <value>");
}
table = Some(crate::tables::filter_where(
base,
&rest[0],
&rest[1],
&rest[2..].join(" "),
)?);
}
"get" => {
let base = table
.as_ref()
.ok_or_else(|| anyhow!("'get' needs a table before it"))?;
let field = rest.first().ok_or_else(|| anyhow!("usage: get <column>"))?;
table = Some(crate::tables::get_column(base, field)?);
}
other => anyhow::bail!(
"'{other}' cannot be used in a pipeline — use ls, ps, where or get"
),
}
}
if let Some(t) = table {
show_table(app, last, t);
}
Ok(false)
}
/// Run one system command through the active shell (inherited stdio).
@@ -999,14 +1124,21 @@ fn print_help() {
println!(" export [--output FILE] backup config + state");
println!(" import <file> restore a backup");
println!(" shell [list|<name>] show or switch the system shell");
println!(" ls [path] list files and directories (Nushell-style table)");
println!(" ls [path] · dir [path] list files and directories (Nushell-style table)");
println!(" ps list processes");
println!(" where <col> <op> <val> filter the last table (ls | where size > 1mb)");
println!(" get <col> select a column (ls | get name)");
println!(" cd <dir> change directory — Tab completes folders");
println!(" help | version | exit this help, the version, quit");
println!();
println!("pipelines:");
println!(" ls | where size > 1mb | get name");
println!(" ps | where name =~ am");
println!();
println!("system gateway:");
println!(" any other input runs as a system command in the active shell");
println!(" prefix with '!' to force system execution (e.g. '!ls', '!list')");
println!(" Tab completes commands, agents, flags and paths (cycle with Tab)");
println!(" Tab inserts the common prefix and lists candidates with descriptions");
println!();
println!("slash commands: /help · /version · /exit · /shell <name>");
}