v0.2.8 : navigation facon Nushell — commande ls en tableau encadre (type, taille, date), completion Tab des dossiers pour cd (niveau par niveau) et des chemins pour ls, descriptions dans le menu Tab

This commit is contained in:
2026-08-15 23:42:32 -04:00
parent d886de322c
commit 3e1b66809a
6 changed files with 499 additions and 12 deletions
+105 -8
View File
@@ -15,6 +15,7 @@
use crate::app::App;
use crate::cli::{Cli, Command, StartArgs};
use crate::commands;
use crate::nav;
use crate::shell::ShellSession;
use anyhow::{anyhow, Result};
use clap::CommandFactory;
@@ -44,8 +45,39 @@ pub struct AmCompleter {
agents: Vec<String>,
aliases: Vec<String>,
groups: Vec<String>,
/// Short descriptions shown next to candidates in the Tab menu
/// (Nushell-style).
descriptions: BTreeMap<String, String>,
}
/// One-line descriptions for the Tab completion menu.
const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("list", "list installed agents"),
("status", "show agent state"),
("search", "search the catalog"),
("info", "agent details"),
("install", "install an agent"),
("uninstall", "remove an agent"),
("update", "update agents"),
("start", "start an agent"),
("stop", "stop an agent"),
("restart", "restart an agent"),
("run", "run an agent command"),
("doctor", "check the environment"),
("config", "manage the configuration"),
("completion", "generate a completion script"),
("self-update", "update agent-manager"),
("self-uninstall", "remove agent-manager"),
("export", "backup config + state"),
("import", "restore a backup"),
("shell", "show or switch the system shell"),
("ls", "list files and directories (Nushell-style)"),
("cd", "change directory — Tab completes folders"),
("help", "show this help"),
("version", "print the version"),
("exit", "quit the shell"),
];
impl AmCompleter {
pub fn new(app: &App) -> Self {
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups)
@@ -67,12 +99,27 @@ impl AmCompleter {
alias_list.sort();
let mut group_list: Vec<String> = groups.keys().map(|g| format!("group:{g}")).collect();
group_list.sort();
let mut descriptions: BTreeMap<String, String> = BTreeMap::new();
for (cmd, desc) in COMMAND_DESCRIPTIONS {
descriptions.insert((*cmd).to_string(), (*desc).to_string());
}
for a in catalog.agents() {
if a.hidden {
continue;
}
let desc = a
.category
.clone()
.or_else(|| a.description.clone())
.unwrap_or_default();
descriptions.insert(a.name.clone(), truncate_desc(&desc));
}
Self {
commands: vec![
"list", "status", "search", "info", "install", "uninstall", "update",
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "help",
"version", "exit",
"self-update", "self-uninstall", "export", "import", "shell", "ls", "cd",
"help", "version", "exit",
],
config_sub: vec!["show", "path", "edit", "validate", "add"],
shells: vec!["bash", "zsh", "fish", "powershell", "elvish"],
@@ -88,6 +135,7 @@ impl AmCompleter {
agents,
aliases: alias_list,
groups: group_list,
descriptions,
}
}
@@ -118,6 +166,16 @@ impl AmCompleter {
"shell" => {
pool = self.gateway_shells.iter().map(|s| s.to_string()).collect();
}
"cd" | "chdir" => {
// Complete directories only — dive one level per Tab.
let cwd = current_dir();
pool = crate::nav::path_candidates(&cwd, word, true);
}
"ls" => {
// Complete paths (files and directories).
let cwd = current_dir();
pool = crate::nav::path_candidates(&cwd, word, false);
}
"install" | "uninstall" | "update" | "start" | "stop" | "restart" | "run"
| "info" | "status" => {
if word.starts_with("group:") {
@@ -172,9 +230,17 @@ impl Completer for AmCompleter {
let (start, cands) = self.candidates_for(line, pos);
let pairs = cands
.into_iter()
.map(|c| Pair {
display: c.clone(),
replacement: c,
.map(|c| {
// Nushell-style menu: show a description next to the
// candidate when one is known.
let display = match self.descriptions.get(&c) {
Some(d) if !d.is_empty() => format!("{c}\t{d}"),
_ => c.clone(),
};
Pair {
display,
replacement: c,
}
})
.collect();
Ok((start, pairs))
@@ -340,7 +406,7 @@ pub fn banner_box(
" system doctor · config · completion · self-update · self-uninstall · export · import"
.to_string(),
));
rows.push(inner(" shell shell · help · version · exit".to_string()));
rows.push(inner(" shell ls · cd · shell · help · version · exit".to_string()));
// Stats footer.
rows.push(blank());
@@ -536,6 +602,20 @@ fn current_dir_string() -> String {
.unwrap_or_else(|_| "?".to_string())
}
fn current_dir() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
/// Keep a catalog description short enough for the completion menu.
fn truncate_desc(desc: &str) -> String {
let one_line = desc.lines().next().unwrap_or(desc);
let mut out: String = one_line.chars().take(64).collect();
if one_line.chars().count() > 64 {
out.push('…');
}
out
}
// ---------------------------------------------------------------------------
// Interactive loop
// ---------------------------------------------------------------------------
@@ -682,6 +762,11 @@ 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)?;
return Ok(false);
}
// Everything that is not an am command is a system command.
if !is_am_command(cmd) {
run_system(app, session, line)?;
@@ -825,6 +910,16 @@ 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() {
Some(p) => crate::config::expand_path(p),
None => current_dir(),
};
print!("{}", nav::render_ls(&target, app.color())?);
Ok(())
}
/// Run one system command through the active shell (inherited stdio).
fn run_system(app: &App, session: &ShellSession, line: &str) -> Result<()> {
app.log.cmd(&format!("{}: {line}", session.current.name));
@@ -904,12 +999,14 @@ 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!(" cd <dir> change directory — Tab completes folders");
println!(" help | version | exit this help, the version, quit");
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. '!list')");
println!(" 'cd <dir>' changes the session directory (in-process)");
println!(" prefix with '!' to force system execution (e.g. '!ls', '!list')");
println!(" Tab completes commands, agents, flags and paths (cycle with Tab)");
println!();
println!("slash commands: /help · /version · /exit · /shell <name>");
}