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:
+152
-20
@@ -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>");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user