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
+11 -4
View File
@@ -8,6 +8,7 @@ use crate::output::Logger;
use crate::state::StateStore;
use crate::theme::{self, Theme};
use anyhow::{anyhow, Context, Result};
use std::cell::Cell;
use std::io::{IsTerminal, Write};
use std::path::PathBuf;
@@ -20,8 +21,9 @@ pub struct App {
pub catalog: Catalog,
pub state: StateStore,
pub log: Logger,
/// Active output color theme.
pub theme: &'static Theme,
/// Active output color theme (switchable at runtime with the REPL
/// 'theme <name>' command).
theme: Cell<&'static Theme>,
}
#[derive(Debug, Clone)]
@@ -90,7 +92,7 @@ impl App {
catalog,
state,
log,
theme,
theme: Cell::new(theme),
})
}
@@ -111,7 +113,12 @@ impl App {
}
pub fn theme(&self) -> &'static Theme {
self.theme
self.theme.get()
}
/// Switch the active theme (used by the REPL 'theme <name>' command).
pub fn set_theme(&self, theme: &'static Theme) {
self.theme.set(theme);
}
/// Ask the user for confirmation on stderr. Accepts y/yes/o/oui.
+26 -2
View File
@@ -508,6 +508,23 @@ pub static HELP_SPECS: &[HelpSpec] = &[
HelpExample { desc: "Show version and build information.", code: "version" },
],
},
HelpSpec {
name: "theme",
category: "Shell",
usage: "theme {flags} [name]",
about: "Show the color themes, or switch the active one inside the shell.",
search_terms: &["color", "colors", "palette"],
flags: &[],
subcommands: &[],
parameters: &[
HelpParam { name: "name", typ: "string", desc: "Theme to switch to (default, ocean, sunset, forest, dracula, mono); omit to list them" },
],
io: None,
examples: &[
HelpExample { desc: "List the available themes.", code: "theme" },
HelpExample { desc: "Switch to the ocean theme.", code: "theme ocean" },
],
},
HelpSpec {
name: "ls",
category: "Shell",
@@ -751,8 +768,15 @@ fn io_table(input: &str, output: &str, theme: &Theme, color: bool) -> String {
line
}
};
let bar = |s: &str| -> String {
if color {
theme.frame(s)
} else {
s.to_string()
}
};
let row = |cells: &[&str], bold: bool| -> String {
let mut line = String::from("│");
let mut line = bar("│");
for (i, c) in cells.iter().enumerate() {
line.push(' ');
let text = if bold {
@@ -767,7 +791,7 @@ fn io_table(input: &str, output: &str, theme: &Theme, color: bool) -> String {
line.push_str(&text);
line.push_str(&" ".repeat(widths[i] - c.chars().count()));
line.push(' ');
line.push('│');
line.push_str(&bar("│"));
}
line
};
+6 -1
View File
@@ -213,6 +213,11 @@ pub fn kv_table(
s.to_string()
}
};
let bar = if color {
theme.frame("│")
} else {
"│".to_string()
};
let mut out = String::new();
out.push_str(&border('╭', '┬', '╮'));
out.push('\n');
@@ -220,7 +225,7 @@ pub fn kv_table(
let key = paint(k, color, crate::theme::Theme::key);
let val = paint(v, color, crate::theme::Theme::val);
out.push_str(&format!(
"│ {} │ {} │\n",
"{bar} {} {bar} {} {bar}\n",
pad(key.as_str(), key_w),
pad(val.as_str(), val_w)
));
+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
+11 -2
View File
@@ -153,8 +153,17 @@ impl DataTable {
line.push(right);
line
};
let bar = |s: &str| -> String {
if !color {
return s.to_string();
}
match theme {
Some(t) => t.frame(s),
None => s.to_string(),
}
};
let row_line = |row: &[String], header: bool| -> String {
let mut line = String::from("│");
let mut line = bar("│");
for (i, c) in row.iter().enumerate().take(all_widths.len()) {
line.push(' ');
let cell = fit_cell(c, all_widths[i]);
@@ -168,7 +177,7 @@ impl DataTable {
};
line.push_str(&rendered);
line.push(' ');
line.push('│');
line.push_str(&bar("│"));
}
line
};