v0.2.14 : splash screen theme - art ASCII et bordures aux couleurs du theme, section Theme (theme actif + liste des themes disponibles), ligne de statut theme
This commit is contained in:
+149
-48
@@ -19,6 +19,7 @@ use crate::nav;
|
||||
use crate::ps;
|
||||
use crate::tables::DataTable;
|
||||
use crate::shell::ShellSession;
|
||||
use crate::theme::Theme;
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::Colorize;
|
||||
use rustyline::completion::{Completer, Pair};
|
||||
@@ -443,7 +444,8 @@ pub fn banner_art() -> Vec<String> {
|
||||
}
|
||||
|
||||
/// The boxed banner: block art, Shell Gateway section (default shell
|
||||
/// highlighted), command groups and the stats footer.
|
||||
/// highlighted), the active color theme, command groups and the stats
|
||||
/// footer. Every color comes from the active theme.
|
||||
pub fn banner_box(
|
||||
version: &str,
|
||||
agents: usize,
|
||||
@@ -452,6 +454,7 @@ pub fn banner_box(
|
||||
session: &ShellSession,
|
||||
session_id: &str,
|
||||
cwd: &str,
|
||||
theme: &Theme,
|
||||
color: bool,
|
||||
) -> String {
|
||||
let w = BOX_WIDTH;
|
||||
@@ -459,27 +462,41 @@ pub fn banner_box(
|
||||
let inner = |content: String| format!("│ {} │", pad_visible(content, w));
|
||||
let blank = || inner(" ".to_string());
|
||||
|
||||
// Top border with the centered header.
|
||||
let header = format!(" Agent Manager v{version} · shell gateway ");
|
||||
let fill = w + 2 - visible_len(&header);
|
||||
// Top border with the centered header (title in the theme header color).
|
||||
let title = format!(" Agent Manager v{version} · shell gateway ");
|
||||
let title = if color {
|
||||
theme.hdr(&title)
|
||||
} else {
|
||||
title
|
||||
};
|
||||
let fill = w + 2 - visible_len(&title);
|
||||
let left = fill / 2;
|
||||
let right = fill - left;
|
||||
rows.push(dim(
|
||||
&format!("╭{}{}{}╮", "─".repeat(left), header, "─".repeat(right)),
|
||||
&format!("╭{}{}{}╮", "─".repeat(left), title, "─".repeat(right)),
|
||||
theme,
|
||||
color,
|
||||
));
|
||||
|
||||
// Block-art title.
|
||||
// Block-art title, painted in the theme accent color.
|
||||
for art in banner_art() {
|
||||
rows.push(inner(art));
|
||||
rows.push(inner(if color {
|
||||
theme.acc(&art)
|
||||
} else {
|
||||
art
|
||||
}));
|
||||
}
|
||||
|
||||
// Shell gateway: the user's default shell is highlighted at startup.
|
||||
rows.push(blank());
|
||||
rows.push(inner(bold_cyan("Shell Gateway", color)));
|
||||
rows.push(inner(if color {
|
||||
theme.hdr("Shell Gateway")
|
||||
} else {
|
||||
"Shell Gateway".to_string()
|
||||
}));
|
||||
let current = format!(
|
||||
"◉ {} — {}{}",
|
||||
green(session.current.name, color),
|
||||
green(session.current.name, theme, color),
|
||||
session.current.title,
|
||||
if session.is_default() { " (default)" } else { "" }
|
||||
);
|
||||
@@ -495,15 +512,50 @@ pub fn banner_box(
|
||||
} else {
|
||||
others.join(" · ")
|
||||
};
|
||||
rows.push(inner(dim(&format!(" ⇄ {avail}"), color)));
|
||||
rows.push(inner(dim(&format!(" ⇄ {avail}"), theme, color)));
|
||||
rows.push(inner(dim(
|
||||
" system commands run in the active shell — 'shell <name>' switches",
|
||||
theme,
|
||||
color,
|
||||
)));
|
||||
|
||||
// Active color theme: the current one highlighted, every other theme
|
||||
// listed, plus the two ways to switch.
|
||||
rows.push(blank());
|
||||
rows.push(inner(if color {
|
||||
theme.hdr("Theme")
|
||||
} else {
|
||||
"Theme".to_string()
|
||||
}));
|
||||
let current_theme = format!(
|
||||
"◉ {} — {}",
|
||||
green(theme.name, theme, color),
|
||||
theme.title
|
||||
);
|
||||
rows.push(inner(format!(" {current_theme}")));
|
||||
let theme_others: Vec<&str> = crate::theme::THEMES
|
||||
.iter()
|
||||
.map(|t| t.name)
|
||||
.filter(|n| *n != theme.name)
|
||||
.collect();
|
||||
rows.push(inner(dim(
|
||||
&format!(" ⇄ {}", theme_others.join(" · ")),
|
||||
theme,
|
||||
color,
|
||||
)));
|
||||
rows.push(inner(dim(
|
||||
" 'am --theme <name>' to switch · settings.theme in the config",
|
||||
theme,
|
||||
color,
|
||||
)));
|
||||
|
||||
// am command groups.
|
||||
rows.push(blank());
|
||||
rows.push(inner(bold_cyan("Commands", color)));
|
||||
rows.push(inner(if color {
|
||||
theme.hdr("Commands")
|
||||
} else {
|
||||
"Commands".to_string()
|
||||
}));
|
||||
rows.push(inner(" catalog list · status · search · info".to_string()));
|
||||
rows.push(inner(
|
||||
" lifecycle install · uninstall · update · start · stop · restart · run".to_string(),
|
||||
@@ -520,9 +572,11 @@ pub fn banner_box(
|
||||
rows.push(blank());
|
||||
rows.push(inner(dim(
|
||||
&format!(
|
||||
"am v{version} · {agents} agents · {external} external · {installed} managed · shell {} · /help for commands",
|
||||
session.current.name
|
||||
"am v{version} · {agents} agents · {external} external · {installed} managed · shell {} · theme {} · /help for commands",
|
||||
session.current.name,
|
||||
theme.name
|
||||
),
|
||||
theme,
|
||||
color,
|
||||
)));
|
||||
let prefix = format!(" session {session_id} · cwd ");
|
||||
@@ -530,9 +584,10 @@ pub fn banner_box(
|
||||
rows.push(inner(format!("{prefix}{}", shorten_head(cwd, budget))));
|
||||
rows.push(inner(yellow(
|
||||
" ✦ Tip: 'shell <name>' switches the system shell · '!' forces a system command",
|
||||
theme,
|
||||
color,
|
||||
)));
|
||||
rows.push(dim(&format!("╰{}╯", "─".repeat(w + 2)), color));
|
||||
rows.push(dim(&format!("╰{}╯", "─".repeat(w + 2)), theme, color));
|
||||
|
||||
rows.join("\n")
|
||||
}
|
||||
@@ -540,6 +595,7 @@ pub fn banner_box(
|
||||
/// The full startup banner (box + welcome + tip).
|
||||
pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
||||
let color = app.color();
|
||||
let theme = app.theme();
|
||||
let mut text = banner_box(
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
agent_count(app),
|
||||
@@ -548,6 +604,7 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
||||
session,
|
||||
session_id,
|
||||
¤t_dir_string(),
|
||||
theme,
|
||||
color,
|
||||
);
|
||||
text.push('\n');
|
||||
@@ -556,28 +613,30 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
||||
);
|
||||
text.push_str(&format!(
|
||||
"{} {}\n",
|
||||
yellow("✦ Tip:", color),
|
||||
yellow("✦ Tip:", theme, color),
|
||||
dim(
|
||||
&format!(
|
||||
"system commands run in {} — 'shell <name>' switches · '!' forces · 'cd' works in this session",
|
||||
session.current.name
|
||||
),
|
||||
theme,
|
||||
color,
|
||||
),
|
||||
));
|
||||
text
|
||||
}
|
||||
|
||||
/// Color an emoji glyph (style names: green, blue, yellow, cyan).
|
||||
fn emoji(glyph: &str, style: &str, color: bool) -> String {
|
||||
/// Color an emoji glyph with the active theme (style names: green, blue,
|
||||
/// yellow, cyan).
|
||||
fn emoji(glyph: &str, style: &str, theme: &Theme, color: bool) -> String {
|
||||
if !color {
|
||||
return glyph.to_string();
|
||||
}
|
||||
match style {
|
||||
"green" => glyph.green().to_string(),
|
||||
"blue" => glyph.blue().to_string(),
|
||||
"yellow" => glyph.yellow().to_string(),
|
||||
"cyan" => glyph.cyan().to_string(),
|
||||
"green" => theme.acc(glyph),
|
||||
"blue" => theme.inf(glyph),
|
||||
"yellow" => theme.warn(glyph),
|
||||
"cyan" => theme.inf(glyph),
|
||||
_ => glyph.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -588,19 +647,24 @@ fn emoji(glyph: &str, style: &str, color: bool) -> String {
|
||||
/// the first one).
|
||||
fn print_status(app: &App, started: Instant, last: Option<Duration>) {
|
||||
let color = app.color();
|
||||
let sep = dim(" │ ", color);
|
||||
let theme = app.theme();
|
||||
let sep = dim(" │ ", theme, color);
|
||||
let cwd = shorten_head(¤t_dir_string(), 30);
|
||||
let folder = format!("{} {}", emoji("🗂️", "cyan", color), dim(&cwd, color));
|
||||
let folder = format!(
|
||||
"{} {}",
|
||||
emoji("🗂️", "cyan", theme, color),
|
||||
dim(&cwd, theme, color)
|
||||
);
|
||||
let counts = format!(
|
||||
"{} {} {} {} {} {} {} {}",
|
||||
dim(&agent_count(app).to_string(), color),
|
||||
emoji("🤖", "green", color),
|
||||
dim("·", color),
|
||||
dim(&external_count(app).to_string(), color),
|
||||
emoji("🌐", "blue", color),
|
||||
dim("·", color),
|
||||
dim(&installed_count(app).to_string(), color),
|
||||
emoji("📊", "yellow", color),
|
||||
dim(&agent_count(app).to_string(), theme, color),
|
||||
emoji("🤖", "green", theme, color),
|
||||
dim("·", theme, color),
|
||||
dim(&external_count(app).to_string(), theme, color),
|
||||
emoji("🌐", "blue", theme, color),
|
||||
dim("·", theme, color),
|
||||
dim(&installed_count(app).to_string(), theme, color),
|
||||
emoji("📊", "yellow", theme, color),
|
||||
);
|
||||
let last_text = match last {
|
||||
Some(d) if d.as_secs() >= 1 => format_elapsed(d),
|
||||
@@ -608,11 +672,19 @@ fn print_status(app: &App, started: Instant, last: Option<Duration>) {
|
||||
Some(d) => format!("{}µs", d.as_micros()),
|
||||
None => "—".to_string(),
|
||||
};
|
||||
let last_part = format!("{} {}", emoji("⚡", "yellow", color), dim(&last_text, color));
|
||||
let elapsed = dim(&format!("⏲ {}", format_elapsed(started.elapsed())), color);
|
||||
let last_part = format!(
|
||||
"{} {}",
|
||||
emoji("⚡", "yellow", theme, color),
|
||||
dim(&last_text, theme, color)
|
||||
);
|
||||
let elapsed = dim(
|
||||
&format!("⏲ {}", format_elapsed(started.elapsed())),
|
||||
theme,
|
||||
color,
|
||||
);
|
||||
println!(
|
||||
"{}am v{}{sep}{folder}{sep}{counts}{sep}{last_part}{sep}{elapsed}",
|
||||
dim(" ⚙ ", color),
|
||||
dim(" ⚙ ", theme, color),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
);
|
||||
}
|
||||
@@ -651,33 +723,25 @@ fn pad_visible(content: String, width: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn dim(s: &str, color: bool) -> String {
|
||||
fn dim(s: &str, theme: &Theme, color: bool) -> String {
|
||||
if color {
|
||||
s.dimmed().to_string()
|
||||
theme.dimmed(s)
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn bold_cyan(s: &str, color: bool) -> String {
|
||||
fn green(s: &str, theme: &Theme, color: bool) -> String {
|
||||
if color {
|
||||
s.cyan().bold().to_string()
|
||||
theme.acc(s)
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn green(s: &str, color: bool) -> String {
|
||||
fn yellow(s: &str, theme: &Theme, color: bool) -> String {
|
||||
if color {
|
||||
s.green().bold().to_string()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn yellow(s: &str, color: bool) -> String {
|
||||
if color {
|
||||
s.yellow().to_string()
|
||||
theme.warn(s)
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
@@ -1440,6 +1504,7 @@ mod tests {
|
||||
&session,
|
||||
"20260815_123456_abcdef",
|
||||
"/home/alice/dev/agent-manager",
|
||||
crate::theme::default_theme(),
|
||||
false,
|
||||
);
|
||||
let widths: Vec<usize> = text.lines().map(|l| l.chars().count()).collect();
|
||||
@@ -1453,6 +1518,42 @@ mod tests {
|
||||
assert!(text.contains("12 external"));
|
||||
assert!(text.contains("3 managed"));
|
||||
assert!(text.contains("agent-manager"));
|
||||
// The splash screen shows the active theme and every other theme.
|
||||
assert!(text.contains("Theme"));
|
||||
assert!(text.contains("◉ default"));
|
||||
assert!(text.contains("ocean"));
|
||||
assert!(text.contains("sunset"));
|
||||
assert!(text.contains("forest"));
|
||||
assert!(text.contains("dracula"));
|
||||
assert!(text.contains("mono"));
|
||||
assert!(text.contains("theme default"));
|
||||
assert!(text.contains("--theme <name>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_box_is_themed_and_still_aligned() {
|
||||
let session = test_session();
|
||||
let ocean = crate::theme::find("ocean").unwrap();
|
||||
let text = banner_box(
|
||||
"9.9.9",
|
||||
65,
|
||||
3,
|
||||
12,
|
||||
&session,
|
||||
"20260815_123456_abcdef",
|
||||
"/home/alice/dev/agent-manager",
|
||||
ocean,
|
||||
true,
|
||||
);
|
||||
// ANSI color codes are present with the ocean theme...
|
||||
assert!(text.contains("\x1b["), "no ANSI in themed banner:\n{text}");
|
||||
assert!(strip_ansi(&text).contains("◉ ocean"));
|
||||
// ...and the visible box stays aligned (strip the ANSI codes).
|
||||
let widths: Vec<usize> = text
|
||||
.lines()
|
||||
.map(|l| strip_ansi(l).chars().count())
|
||||
.collect();
|
||||
assert!(widths.iter().all(|w| *w == widths[0]), "unaligned box:\n{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user