v0.2.14 : aide Nushell, themes de couleurs et cadres blancs
- Aide facon Nushell : help <cmd>, <cmd> -h/--help, help, help commands, help --find <texte>, help <agent> (fiche info) - sections Usage/Flags/ Command Type/Parameters/Input-output/Examples avec couleurs - Commande version : tableau Nushell (commit, build, rustc, theme...) + --json ; build.rs pour commit/branche git ; -V reste la version courte - Themes : --theme <nom> (default, ocean, sunset, forest, dracula, mono) + settings.theme dans la config ; activation VT Windows pour les couleurs - Cadres blancs Nushell partages : list, search, status, doctor, ls/ps/ where/get et menu d autocompletion Tab encadre (colonnes name/description)
This commit is contained in:
+202
-63
@@ -13,14 +13,13 @@
|
||||
//! available.
|
||||
|
||||
use crate::app::App;
|
||||
use crate::cli::{Cli, Command, StartArgs};
|
||||
use crate::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;
|
||||
use colored::Colorize;
|
||||
use rustyline::completion::{Completer, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
@@ -50,6 +49,8 @@ pub struct AmCompleter {
|
||||
/// Short descriptions shown next to candidates in the Tab menu
|
||||
/// (Nushell-style).
|
||||
descriptions: BTreeMap<String, String>,
|
||||
/// Active color theme (used by the boxed menu highlight).
|
||||
theme: &'static crate::theme::Theme,
|
||||
}
|
||||
|
||||
/// One-line descriptions for the Tab completion menu.
|
||||
@@ -86,7 +87,10 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
|
||||
|
||||
impl AmCompleter {
|
||||
pub fn new(app: &App) -> Self {
|
||||
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups)
|
||||
let mut completer =
|
||||
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups);
|
||||
completer.theme = app.theme();
|
||||
completer
|
||||
}
|
||||
|
||||
pub fn from_catalog(
|
||||
@@ -121,6 +125,7 @@ impl AmCompleter {
|
||||
descriptions.insert(a.name.clone(), truncate_desc(&desc));
|
||||
}
|
||||
Self {
|
||||
theme: crate::theme::default_theme(),
|
||||
commands: vec![
|
||||
"list", "status", "search", "info", "install", "uninstall", "update",
|
||||
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
||||
@@ -239,25 +244,82 @@ impl Completer for AmCompleter {
|
||||
_ctx: &Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Pair>)> {
|
||||
let (start, cands) = self.candidates_for(line, pos);
|
||||
let pairs = cands
|
||||
.into_iter()
|
||||
.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,
|
||||
let pairs = if cands.len() < 2 {
|
||||
// A single match is inserted immediately (no menu).
|
||||
cands
|
||||
.into_iter()
|
||||
.map(|c| Pair {
|
||||
display: c.clone(),
|
||||
replacement: c,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.boxed_pairs(&cands)
|
||||
};
|
||||
Ok((start, pairs))
|
||||
}
|
||||
}
|
||||
|
||||
impl AmCompleter {
|
||||
/// Build the boxed Tab menu: one candidate per box line, so rustyline's
|
||||
/// single-column layout renders the whole menu as one white-framed
|
||||
/// table. Decorative lines (borders, header) replace the typed word
|
||||
/// with the common prefix, so completion behaves exactly as before.
|
||||
fn boxed_pairs(&self, cands: &[String]) -> Vec<Pair> {
|
||||
let rows: Vec<(String, String)> = cands
|
||||
.iter()
|
||||
.map(|c| (c.clone(), self.descriptions.get(c).cloned().unwrap_or_default()))
|
||||
.collect();
|
||||
let has_desc = rows.iter().any(|(_, d)| !d.is_empty());
|
||||
let lines = crate::output::completion_box(&rows);
|
||||
let lcp = common_prefix(cands);
|
||||
let n = cands.len();
|
||||
let data_start = if has_desc { 3 } else { 1 };
|
||||
// Pad every line to the terminal width: rustyline then lays the
|
||||
// menu out in one column, one box line per row.
|
||||
let width = crate::output::terminal_width()
|
||||
.unwrap_or(120)
|
||||
.max(lines.iter().map(|l| l.chars().count()).max().unwrap_or(0));
|
||||
lines
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
let replacement = if i >= data_start && i < data_start + n {
|
||||
cands[i - data_start].clone()
|
||||
} else {
|
||||
lcp.clone()
|
||||
};
|
||||
let display = format!(
|
||||
"{line}{}",
|
||||
" ".repeat(width.saturating_sub(line.chars().count()))
|
||||
);
|
||||
Pair {
|
||||
display,
|
||||
replacement,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Longest common prefix of the candidates (chars, not bytes).
|
||||
fn common_prefix(items: &[String]) -> String {
|
||||
let mut iter = items.iter();
|
||||
let Some(first) = iter.next() else {
|
||||
return String::new();
|
||||
};
|
||||
let mut prefix = first.clone();
|
||||
for s in iter {
|
||||
while !s.starts_with(&prefix) && !prefix.is_empty() {
|
||||
prefix.pop();
|
||||
}
|
||||
if prefix.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
prefix
|
||||
}
|
||||
|
||||
impl Hinter for AmCompleter {
|
||||
type Hint = String;
|
||||
|
||||
@@ -266,7 +328,39 @@ impl Hinter for AmCompleter {
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for AmCompleter {}
|
||||
impl Highlighter for AmCompleter {
|
||||
/// Style the boxed completion menu: borders dim, the header row in the
|
||||
/// theme header style, candidate names in the accent color.
|
||||
fn highlight_candidate<'c>(
|
||||
&self,
|
||||
candidate: &'c str,
|
||||
completion: rustyline::CompletionType,
|
||||
) -> std::borrow::Cow<'c, str> {
|
||||
use std::borrow::Cow;
|
||||
let _ = completion;
|
||||
match candidate.chars().next() {
|
||||
Some('╭') | Some('╰') | Some('├') => Cow::Owned(self.theme.dimmed(candidate)),
|
||||
Some('│') => {
|
||||
let cells: Vec<&str> = candidate.split('│').collect();
|
||||
if cells.len() >= 4 {
|
||||
if cells[1].trim() == "name" && cells[2].trim() == "description" {
|
||||
Cow::Owned(self.theme.hdr(candidate))
|
||||
} else {
|
||||
Cow::Owned(format!(
|
||||
"│{}│{}│{}",
|
||||
self.theme.acc(cells[1]),
|
||||
cells[2],
|
||||
cells[3]
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(candidate)
|
||||
}
|
||||
}
|
||||
_ => Cow::Borrowed(candidate),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Validator for AmCompleter {}
|
||||
impl Helper for AmCompleter {}
|
||||
|
||||
@@ -682,9 +776,7 @@ pub fn run(app: &App) -> Result<i32> {
|
||||
let forced = std::env::var_os("AM_FORCE_REPL").is_some();
|
||||
if !std::io::stdin().is_terminal() && !forced {
|
||||
// Piped stdin: print the help instead of hanging.
|
||||
let mut cmd = Cli::command();
|
||||
cmd.print_help()?;
|
||||
println!();
|
||||
crate::help::print_general(app);
|
||||
return Ok(2);
|
||||
}
|
||||
let mut session = ShellSession::start(&app.config);
|
||||
@@ -786,11 +878,11 @@ fn handle_input(
|
||||
match line {
|
||||
"exit" | "quit" | "q" | "/exit" | "/quit" => return Ok(true),
|
||||
"help" | "?" | "/help" => {
|
||||
print_help();
|
||||
crate::help::print_general(app);
|
||||
return Ok(false);
|
||||
}
|
||||
"version" | "/version" => {
|
||||
println!("am {}", env!("CARGO_PKG_VERSION"));
|
||||
crate::help::print_version(app);
|
||||
return Ok(false);
|
||||
}
|
||||
_ => {}
|
||||
@@ -803,6 +895,14 @@ fn handle_input(
|
||||
};
|
||||
match cmd {
|
||||
"shell" => shell_command(session, arg),
|
||||
"help" => match arg {
|
||||
Some(topic) => {
|
||||
if let Err(e) = crate::help::print_command(app, topic) {
|
||||
app.log.error(format!("{e:#}"));
|
||||
}
|
||||
}
|
||||
None => crate::help::print_general(app),
|
||||
},
|
||||
_ => app
|
||||
.log
|
||||
.error(&format!("unknown command '/{cmd}' — type '/help'")),
|
||||
@@ -839,6 +939,35 @@ fn handle_line(
|
||||
let cmd = tokens[0].as_str();
|
||||
let rest = &tokens[1..];
|
||||
|
||||
// Nushell-style help: 'ls -h', 'ls --help', 'start -h', ... show the
|
||||
// colored command help. System commands keep their own -h handling.
|
||||
if is_am_command(cmd) || is_shell_command(cmd) {
|
||||
let help_flag_pos = rest.iter().position(|t| t == "-h" || t == "--help");
|
||||
let dd_pos = rest.iter().position(|t| t == "--");
|
||||
let help_requested = match help_flag_pos {
|
||||
Some(p) => dd_pos.is_none_or(|d| p < d),
|
||||
None => false,
|
||||
};
|
||||
// 'run <agent> --help' passes --help through to the agent; only
|
||||
// 'run -h' right after the command name is a help request.
|
||||
let passthrough = cmd == "run" && help_flag_pos != Some(0);
|
||||
if help_requested && !passthrough {
|
||||
if let Err(e) = crate::help::print_command(app, cmd) {
|
||||
app.log.error(format!("{e:#}"));
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// help <topic> | help --find <text> | help commands
|
||||
if cmd == "help" {
|
||||
return run_help_command(app, rest);
|
||||
}
|
||||
if cmd == "version" {
|
||||
crate::help::print_version(app);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Session directory change: kept in the am process, not a child shell.
|
||||
if matches!(cmd, "cd" | "chdir") {
|
||||
change_dir(rest)?;
|
||||
@@ -999,6 +1128,14 @@ fn handle_line(
|
||||
Ok(exit_after)
|
||||
}
|
||||
|
||||
/// Whether a word is one of the REPL's shell-level commands (ls, ps, ...).
|
||||
fn is_shell_command(word: &str) -> bool {
|
||||
matches!(
|
||||
word,
|
||||
"ls" | "dir" | "ps" | "where" | "get" | "cd" | "shell" | "help" | "version" | "exit"
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether a word is one of the am commands (everything else is executed by
|
||||
/// the system shell).
|
||||
fn is_am_command(word: &str) -> bool {
|
||||
@@ -1027,7 +1164,14 @@ fn is_am_command(word: &str) -> bool {
|
||||
|
||||
/// 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() {
|
||||
// A leading '--' ends flag parsing, so 'ls -- -h' can list a file
|
||||
// literally named "-h" instead of showing the help.
|
||||
let args: Vec<&String> = if rest.first().map(String::as_str) == Some("--") {
|
||||
rest.iter().skip(1).collect()
|
||||
} else {
|
||||
rest.iter().collect()
|
||||
};
|
||||
Ok(match args.first() {
|
||||
Some(p) => crate::config::expand_path(p),
|
||||
None => current_dir(),
|
||||
})
|
||||
@@ -1037,7 +1181,10 @@ fn ls_target(rest: &[String]) -> Result<std::path::PathBuf> {
|
||||
/// The table is rendered to fit the terminal width so rows never wrap.
|
||||
fn show_table(app: &App, last: &mut Option<DataTable>, table: DataTable) {
|
||||
let width = crate::output::terminal_width().unwrap_or(120);
|
||||
print!("{}", table.render(app.color(), width));
|
||||
print!(
|
||||
"{}",
|
||||
table.render_themed(app.theme(), app.color(), width)
|
||||
);
|
||||
*last = Some(table);
|
||||
}
|
||||
|
||||
@@ -1057,6 +1204,18 @@ fn eval_pipeline(app: &App, last: &mut Option<DataTable>, line: &str) -> Result<
|
||||
}
|
||||
let cmd = tokens[0].as_str();
|
||||
let rest = &tokens[1..];
|
||||
// Nushell-style: 'ls -h' inside a pipeline shows the command help.
|
||||
if rest
|
||||
.iter()
|
||||
.take_while(|t| t.as_str() != "--")
|
||||
.any(|t| t == "-h" || t == "--help")
|
||||
&& matches!(cmd, "ls" | "dir" | "ps" | "where" | "get")
|
||||
{
|
||||
if let Err(e) = crate::help::print_command(app, cmd) {
|
||||
app.log.error(format!("{e:#}"));
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
match cmd {
|
||||
"ls" => {
|
||||
table = Some(nav::ls_table(&ls_target(rest)?, app.color())?);
|
||||
@@ -1157,44 +1316,24 @@ fn print_shell_status(session: &ShellSession) {
|
||||
println!("use 'shell <name>' to switch; set settings.default_shell in the config to make it permanent");
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("commands:");
|
||||
println!(" list [--all] list installed agents (--all: whole catalog)");
|
||||
println!(" status [agent] show agent state");
|
||||
println!(" search <keyword> search the catalog");
|
||||
println!(" info <agent> show agent details");
|
||||
println!(" install <agent> [--method <m>] install an agent (--force to reinstall)");
|
||||
println!(" uninstall <agent> [--purge] remove an agent");
|
||||
println!(" update <agent> | update --all update agents");
|
||||
println!(" start <agent> [--background] start an agent");
|
||||
println!(" stop <agent> [--force] stop an agent");
|
||||
println!(" restart <agent> [--background] restart an agent");
|
||||
println!(" run <agent> [args...] run an agent command directly");
|
||||
println!(" doctor [--fix] check the environment");
|
||||
println!(" config show | path | validate | edit");
|
||||
println!(" completion <shell> generate a completion script");
|
||||
println!(" self-update [--check] update agent-manager");
|
||||
println!(" self-uninstall remove agent-manager and everything it created");
|
||||
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] · 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 inserts the common prefix and lists candidates with descriptions");
|
||||
println!();
|
||||
println!("slash commands: /help · /version · /exit · /shell <name>");
|
||||
/// 'help' in the REPL: the overview, a command/agent topic, the command
|
||||
/// list, or a --find search — the same Nushell-style help as 'am help'.
|
||||
fn run_help_command(app: &App, rest: &[String]) -> Result<bool> {
|
||||
let mut iter = rest.iter();
|
||||
match iter.next().map(|s| s.as_str()) {
|
||||
None => crate::help::print_general(app),
|
||||
Some("--find") => {
|
||||
let text = iter.next().map(|s| s.as_str()).unwrap_or("");
|
||||
crate::help::print_find(app, text);
|
||||
}
|
||||
Some("-h") | Some("--help") => {
|
||||
crate::help::print_command(app, "help")?;
|
||||
}
|
||||
Some(topic) => {
|
||||
crate::help::print_command(app, topic)?;
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user