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:
+11
-4
@@ -8,6 +8,7 @@ use crate::output::Logger;
|
|||||||
use crate::state::StateStore;
|
use crate::state::StateStore;
|
||||||
use crate::theme::{self, Theme};
|
use crate::theme::{self, Theme};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::cell::Cell;
|
||||||
use std::io::{IsTerminal, Write};
|
use std::io::{IsTerminal, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -20,8 +21,9 @@ pub struct App {
|
|||||||
pub catalog: Catalog,
|
pub catalog: Catalog,
|
||||||
pub state: StateStore,
|
pub state: StateStore,
|
||||||
pub log: Logger,
|
pub log: Logger,
|
||||||
/// Active output color theme.
|
/// Active output color theme (switchable at runtime with the REPL
|
||||||
pub theme: &'static Theme,
|
/// 'theme <name>' command).
|
||||||
|
theme: Cell<&'static Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -90,7 +92,7 @@ impl App {
|
|||||||
catalog,
|
catalog,
|
||||||
state,
|
state,
|
||||||
log,
|
log,
|
||||||
theme,
|
theme: Cell::new(theme),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn theme(&self) -> &'static Theme {
|
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.
|
/// Ask the user for confirmation on stderr. Accepts y/yes/o/oui.
|
||||||
|
|||||||
+26
-2
@@ -508,6 +508,23 @@ pub static HELP_SPECS: &[HelpSpec] = &[
|
|||||||
HelpExample { desc: "Show version and build information.", code: "version" },
|
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 {
|
HelpSpec {
|
||||||
name: "ls",
|
name: "ls",
|
||||||
category: "Shell",
|
category: "Shell",
|
||||||
@@ -751,8 +768,15 @@ fn io_table(input: &str, output: &str, theme: &Theme, color: bool) -> String {
|
|||||||
line
|
line
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let bar = |s: &str| -> String {
|
||||||
|
if color {
|
||||||
|
theme.frame(s)
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
let row = |cells: &[&str], bold: bool| -> String {
|
let row = |cells: &[&str], bold: bool| -> String {
|
||||||
let mut line = String::from("│");
|
let mut line = bar("│");
|
||||||
for (i, c) in cells.iter().enumerate() {
|
for (i, c) in cells.iter().enumerate() {
|
||||||
line.push(' ');
|
line.push(' ');
|
||||||
let text = if bold {
|
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(&text);
|
||||||
line.push_str(&" ".repeat(widths[i] - c.chars().count()));
|
line.push_str(&" ".repeat(widths[i] - c.chars().count()));
|
||||||
line.push(' ');
|
line.push(' ');
|
||||||
line.push('│');
|
line.push_str(&bar("│"));
|
||||||
}
|
}
|
||||||
line
|
line
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-1
@@ -213,6 +213,11 @@ pub fn kv_table(
|
|||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let bar = if color {
|
||||||
|
theme.frame("│")
|
||||||
|
} else {
|
||||||
|
"│".to_string()
|
||||||
|
};
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(&border('╭', '┬', '╮'));
|
out.push_str(&border('╭', '┬', '╮'));
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
@@ -220,7 +225,7 @@ pub fn kv_table(
|
|||||||
let key = paint(k, color, crate::theme::Theme::key);
|
let key = paint(k, color, crate::theme::Theme::key);
|
||||||
let val = paint(v, color, crate::theme::Theme::val);
|
let val = paint(v, color, crate::theme::Theme::val);
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"│ {} │ {} │\n",
|
"{bar} {} {bar} {} {bar}\n",
|
||||||
pad(key.as_str(), key_w),
|
pad(key.as_str(), key_w),
|
||||||
pad(val.as_str(), val_w)
|
pad(val.as_str(), val_w)
|
||||||
));
|
));
|
||||||
|
|||||||
+107
-16
@@ -50,8 +50,9 @@ pub struct AmCompleter {
|
|||||||
/// Short descriptions shown next to candidates in the Tab menu
|
/// Short descriptions shown next to candidates in the Tab menu
|
||||||
/// (Nushell-style).
|
/// (Nushell-style).
|
||||||
descriptions: BTreeMap<String, String>,
|
descriptions: BTreeMap<String, String>,
|
||||||
/// Active color theme (used by the boxed menu highlight).
|
/// Active color theme (used by the boxed menu highlight; follows the
|
||||||
theme: &'static crate::theme::Theme,
|
/// 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.
|
/// One-line descriptions for the Tab completion menu.
|
||||||
@@ -75,6 +76,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
|
|||||||
("export", "backup config + state"),
|
("export", "backup config + state"),
|
||||||
("import", "restore a backup"),
|
("import", "restore a backup"),
|
||||||
("shell", "show or switch the system shell"),
|
("shell", "show or switch the system shell"),
|
||||||
|
("theme", "show or switch the color theme"),
|
||||||
("ls", "list files and directories (Nushell-style)"),
|
("ls", "list files and directories (Nushell-style)"),
|
||||||
("dir", "list files and directories (same as ls)"),
|
("dir", "list files and directories (same as ls)"),
|
||||||
("cd", "change directory — Tab completes folders"),
|
("cd", "change directory — Tab completes folders"),
|
||||||
@@ -88,12 +90,17 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
|
|||||||
|
|
||||||
impl AmCompleter {
|
impl AmCompleter {
|
||||||
pub fn new(app: &App) -> Self {
|
pub fn new(app: &App) -> Self {
|
||||||
let mut completer =
|
let completer =
|
||||||
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups);
|
Self::from_catalog(&app.catalog, &app.config.aliases, &app.config.groups);
|
||||||
completer.theme = app.theme();
|
completer.theme.set(app.theme());
|
||||||
completer
|
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(
|
pub fn from_catalog(
|
||||||
catalog: &crate::catalog::Catalog,
|
catalog: &crate::catalog::Catalog,
|
||||||
aliases: &BTreeMap<String, String>,
|
aliases: &BTreeMap<String, String>,
|
||||||
@@ -126,12 +133,12 @@ impl AmCompleter {
|
|||||||
descriptions.insert(a.name.clone(), truncate_desc(&desc));
|
descriptions.insert(a.name.clone(), truncate_desc(&desc));
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
theme: crate::theme::default_theme(),
|
theme: std::cell::Cell::new(crate::theme::default_theme()),
|
||||||
commands: vec![
|
commands: vec![
|
||||||
"list", "status", "search", "info", "install", "uninstall", "update",
|
"list", "status", "search", "info", "install", "uninstall", "update",
|
||||||
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
||||||
"self-update", "self-uninstall", "export", "import", "shell", "ls", "dir",
|
"self-update", "self-uninstall", "export", "import", "shell", "theme",
|
||||||
"cd", "ps", "where", "get", "help", "version", "exit",
|
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
|
||||||
],
|
],
|
||||||
config_sub: vec!["show", "path", "edit", "validate", "add"],
|
config_sub: vec!["show", "path", "edit", "validate", "add"],
|
||||||
shells: vec!["bash", "zsh", "fish", "powershell", "elvish"],
|
shells: vec!["bash", "zsh", "fish", "powershell", "elvish"],
|
||||||
@@ -178,6 +185,12 @@ impl AmCompleter {
|
|||||||
"shell" => {
|
"shell" => {
|
||||||
pool = self.gateway_shells.iter().map(|s| s.to_string()).collect();
|
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" => {
|
"cd" | "chdir" => {
|
||||||
// Complete directories only — dive one level per Tab.
|
// Complete directories only — dive one level per Tab.
|
||||||
let cwd = current_dir();
|
let cwd = current_dir();
|
||||||
@@ -340,18 +353,21 @@ impl Highlighter for AmCompleter {
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
let _ = completion;
|
let _ = completion;
|
||||||
match candidate.chars().next() {
|
match candidate.chars().next() {
|
||||||
Some('╭') | Some('╰') | Some('├') => Cow::Owned(self.theme.frame(candidate)),
|
Some('╭') | Some('╰') | Some('├') => Cow::Owned(self.theme.get().frame(candidate)),
|
||||||
Some('│') => {
|
Some('│') => {
|
||||||
let cells: Vec<&str> = candidate.split('│').collect();
|
let cells: Vec<&str> = candidate.split('│').collect();
|
||||||
if cells.len() >= 4 {
|
if cells.len() >= 4 {
|
||||||
if cells[1].trim() == "name" && cells[2].trim() == "description" {
|
if cells[1].trim() == "name" && cells[2].trim() == "description" {
|
||||||
Cow::Owned(self.theme.hdr(candidate))
|
Cow::Owned(self.theme.get().hdr(candidate))
|
||||||
} else {
|
} else {
|
||||||
|
let bar = self.theme.get().frame("│");
|
||||||
Cow::Owned(format!(
|
Cow::Owned(format!(
|
||||||
"│{}│{}│{}",
|
"{}{}{}{}{}",
|
||||||
self.theme.acc(cells[1]),
|
bar,
|
||||||
|
self.theme.get().acc(cells[1]),
|
||||||
|
bar,
|
||||||
cells[2],
|
cells[2],
|
||||||
cells[3]
|
bar
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -459,8 +475,6 @@ pub fn banner_box(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let w = BOX_WIDTH;
|
let w = BOX_WIDTH;
|
||||||
let mut rows: Vec<String> = Vec::new();
|
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 {
|
let frame = |s: &str| -> String {
|
||||||
if color {
|
if color {
|
||||||
theme.frame(s)
|
theme.frame(s)
|
||||||
@@ -468,6 +482,15 @@ pub fn banner_box(
|
|||||||
s.to_string()
|
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,
|
// Top border with the centered header (title in the theme header color,
|
||||||
// border in the theme frame color).
|
// border in the theme frame color).
|
||||||
@@ -574,7 +597,7 @@ pub fn banner_box(
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
rows.push(inner(
|
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.
|
// Stats footer.
|
||||||
@@ -894,6 +917,11 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
|
|||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let result = handle_input(app, session, &mut last, &trimmed);
|
let result = handle_input(app, session, &mut last, &trimmed);
|
||||||
last_duration = Some(t.elapsed());
|
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 {
|
match result {
|
||||||
Ok(true) => break,
|
Ok(true) => break,
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
@@ -958,6 +986,10 @@ fn handle_input(
|
|||||||
crate::help::print_version(app);
|
crate::help::print_version(app);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
"theme" | "/theme" => {
|
||||||
|
theme_command(app, None);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// Slash commands (Hermes-style).
|
// Slash commands (Hermes-style).
|
||||||
@@ -968,6 +1000,7 @@ fn handle_input(
|
|||||||
};
|
};
|
||||||
match cmd {
|
match cmd {
|
||||||
"shell" => shell_command(session, arg),
|
"shell" => shell_command(session, arg),
|
||||||
|
"theme" => theme_command(app, arg),
|
||||||
"help" => match arg {
|
"help" => match arg {
|
||||||
Some(topic) => {
|
Some(topic) => {
|
||||||
if let Err(e) = crate::help::print_command(app, topic) {
|
if let Err(e) = crate::help::print_command(app, topic) {
|
||||||
@@ -1040,6 +1073,11 @@ fn handle_line(
|
|||||||
crate::help::print_version(app);
|
crate::help::print_version(app);
|
||||||
return Ok(false);
|
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.
|
// Session directory change: kept in the am process, not a child shell.
|
||||||
if matches!(cmd, "cd" | "chdir") {
|
if matches!(cmd, "cd" | "chdir") {
|
||||||
@@ -1205,7 +1243,7 @@ fn handle_line(
|
|||||||
fn is_shell_command(word: &str) -> bool {
|
fn is_shell_command(word: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
word,
|
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) {
|
fn print_shell_status(session: &ShellSession) {
|
||||||
let others: Vec<&str> = session
|
let others: Vec<&str> = session
|
||||||
.available
|
.available
|
||||||
|
|||||||
+11
-2
@@ -153,8 +153,17 @@ impl DataTable {
|
|||||||
line.push(right);
|
line.push(right);
|
||||||
line
|
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 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()) {
|
for (i, c) in row.iter().enumerate().take(all_widths.len()) {
|
||||||
line.push(' ');
|
line.push(' ');
|
||||||
let cell = fit_cell(c, all_widths[i]);
|
let cell = fit_cell(c, all_widths[i]);
|
||||||
@@ -168,7 +177,7 @@ impl DataTable {
|
|||||||
};
|
};
|
||||||
line.push_str(&rendered);
|
line.push_str(&rendered);
|
||||||
line.push(' ');
|
line.push(' ');
|
||||||
line.push('│');
|
line.push_str(&bar("│"));
|
||||||
}
|
}
|
||||||
line
|
line
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user