v0.2.14 : barres verticales des cadres aux couleurs du theme + commande theme dans le shell (theme liste les themes, theme <name> change a chaud, completion et menu synchronises)

This commit is contained in:
2026-08-16 13:40:51 -04:00
parent 4b0f02a7c4
commit 2bbe45e508
5 changed files with 161 additions and 25 deletions
+107 -16
View File
@@ -50,8 +50,9 @@ 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,
/// Active color theme (used by the boxed menu highlight; follows the
/// app theme when it is switched with the REPL 'theme <name>' command).
theme: std::cell::Cell<&'static crate::theme::Theme>,
}
/// One-line descriptions for the Tab completion menu.
@@ -75,6 +76,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("export", "backup config + state"),
("import", "restore a backup"),
("shell", "show or switch the system shell"),
("theme", "show or switch the color theme"),
("ls", "list files and directories (Nushell-style)"),
("dir", "list files and directories (same as ls)"),
("cd", "change directory — Tab completes folders"),
@@ -88,12 +90,17 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
impl AmCompleter {
pub fn new(app: &App) -> Self {
let mut completer =
let completer =
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups);
completer.theme = app.theme();
completer.theme.set(app.theme());
completer
}
/// Point the menu at a new theme (after 'theme <name>' in the REPL).
pub fn set_theme(&self, theme: &'static crate::theme::Theme) {
self.theme.set(theme);
}
pub fn from_catalog(
catalog: &crate::catalog::Catalog,
aliases: &BTreeMap<String, String>,
@@ -126,12 +133,12 @@ impl AmCompleter {
descriptions.insert(a.name.clone(), truncate_desc(&desc));
}
Self {
theme: crate::theme::default_theme(),
theme: std::cell::Cell::new(crate::theme::default_theme()),
commands: vec![
"list", "status", "search", "info", "install", "uninstall", "update",
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "ls", "dir",
"cd", "ps", "where", "get", "help", "version", "exit",
"self-update", "self-uninstall", "export", "import", "shell", "theme",
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
],
config_sub: vec!["show", "path", "edit", "validate", "add"],
shells: vec!["bash", "zsh", "fish", "powershell", "elvish"],
@@ -178,6 +185,12 @@ impl AmCompleter {
"shell" => {
pool = self.gateway_shells.iter().map(|s| s.to_string()).collect();
}
"theme" => {
pool = crate::theme::names()
.iter()
.map(|s| s.to_string())
.collect();
}
"cd" | "chdir" => {
// Complete directories only — dive one level per Tab.
let cwd = current_dir();
@@ -340,18 +353,21 @@ impl Highlighter for AmCompleter {
use std::borrow::Cow;
let _ = completion;
match candidate.chars().next() {
Some('╭') | Some('╰') | Some('├') => Cow::Owned(self.theme.frame(candidate)),
Some('╭') | Some('╰') | Some('├') => Cow::Owned(self.theme.get().frame(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))
Cow::Owned(self.theme.get().hdr(candidate))
} else {
let bar = self.theme.get().frame("│");
Cow::Owned(format!(
"│{}│{}│{}",
self.theme.acc(cells[1]),
"{}{}{}{}{}",
bar,
self.theme.get().acc(cells[1]),
bar,
cells[2],
cells[3]
bar
))
}
} else {
@@ -459,8 +475,6 @@ pub fn banner_box(
) -> String {
let w = BOX_WIDTH;
let mut rows: Vec<String> = Vec::new();
let inner = |content: String| format!("│ {} │", pad_visible(content, w));
let blank = || inner(" ".to_string());
let frame = |s: &str| -> String {
if color {
theme.frame(s)
@@ -468,6 +482,15 @@ pub fn banner_box(
s.to_string()
}
};
let inner = |content: String| {
format!(
"{} {} {}",
frame("│"),
pad_visible(content, w),
frame("│")
)
};
let blank = || inner(" ".to_string());
// Top border with the centered header (title in the theme header color,
// border in the theme frame color).
@@ -574,7 +597,7 @@ pub fn banner_box(
.to_string(),
));
rows.push(inner(
" shell ls · cd · ps · where · get · shell · help · version · exit".to_string(),
" shell ls · cd · ps · where · get · shell · theme · help · version · exit".to_string(),
));
// Stats footer.
@@ -894,6 +917,11 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
let t = Instant::now();
let result = handle_input(app, session, &mut last, &trimmed);
last_duration = Some(t.elapsed());
// 'theme <name>' may have switched the app theme: repoint the
// completion menu at the new palette.
if let Some(h) = rl.helper_mut() {
h.set_theme(app.theme());
}
match result {
Ok(true) => break,
Ok(false) => {}
@@ -958,6 +986,10 @@ fn handle_input(
crate::help::print_version(app);
return Ok(false);
}
"theme" | "/theme" => {
theme_command(app, None);
return Ok(false);
}
_ => {}
}
// Slash commands (Hermes-style).
@@ -968,6 +1000,7 @@ fn handle_input(
};
match cmd {
"shell" => shell_command(session, arg),
"theme" => theme_command(app, arg),
"help" => match arg {
Some(topic) => {
if let Err(e) = crate::help::print_command(app, topic) {
@@ -1040,6 +1073,11 @@ fn handle_line(
crate::help::print_version(app);
return Ok(false);
}
// theme | theme <name>: list the themes or switch the active one.
if cmd == "theme" {
theme_command(app, rest.first().map(|s| s.as_str()));
return Ok(false);
}
// Session directory change: kept in the am process, not a child shell.
if matches!(cmd, "cd" | "chdir") {
@@ -1205,7 +1243,7 @@ fn handle_line(
fn is_shell_command(word: &str) -> bool {
matches!(
word,
"ls" | "dir" | "ps" | "where" | "get" | "cd" | "shell" | "help" | "version" | "exit"
"ls" | "dir" | "ps" | "where" | "get" | "cd" | "shell" | "theme" | "help" | "version" | "exit"
)
}
@@ -1368,6 +1406,59 @@ fn shell_command(session: &mut ShellSession, arg: Option<&str>) {
}
}
/// 'theme' in the REPL: list the themes, or switch with 'theme <name>'.
fn theme_command(app: &App, arg: Option<&str>) {
match arg.map(str::trim).filter(|s| !s.is_empty()) {
None => print_theme_status(app),
Some(name) => match crate::theme::find(name) {
Some(t) => {
app.set_theme(t);
let color = app.color();
println!(
"{} {}",
if color { t.hdr("theme:") } else { "theme:".to_string() },
format!("{} — {}", t.acc(t.name), t.title)
);
}
None => app.log.error(format!(
"unknown theme '{name}' — available: {}",
crate::theme::names().join(", ")
)),
},
}
}
/// List the available themes, highlighting the active one.
fn print_theme_status(app: &App) {
let theme = app.theme();
let color = app.color();
let hdr = |s: &str| -> String {
if color {
theme.hdr(s)
} else {
s.to_string()
}
};
println!("{}", hdr("Theme"));
println!(
" ◉ {} — {}",
theme.acc(theme.name),
theme.title
);
let others: Vec<&str> = crate::theme::THEMES
.iter()
.map(|t| t.name)
.filter(|n| *n != theme.name)
.collect();
println!(" {}", theme.dimmed(&format!("⇄ {}", others.join(" · "))));
println!(
" {}",
theme.dimmed(
"'theme <name>' to switch · '--theme <name>' or settings.theme to change the default"
)
);
}
fn print_shell_status(session: &ShellSession) {
let others: Vec<&str> = session
.available