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::ps;
|
||||||
use crate::tables::DataTable;
|
use crate::tables::DataTable;
|
||||||
use crate::shell::ShellSession;
|
use crate::shell::ShellSession;
|
||||||
|
use crate::theme::Theme;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use rustyline::completion::{Completer, Pair};
|
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
|
/// 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(
|
pub fn banner_box(
|
||||||
version: &str,
|
version: &str,
|
||||||
agents: usize,
|
agents: usize,
|
||||||
@@ -452,6 +454,7 @@ pub fn banner_box(
|
|||||||
session: &ShellSession,
|
session: &ShellSession,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
cwd: &str,
|
cwd: &str,
|
||||||
|
theme: &Theme,
|
||||||
color: bool,
|
color: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let w = BOX_WIDTH;
|
let w = BOX_WIDTH;
|
||||||
@@ -459,27 +462,41 @@ pub fn banner_box(
|
|||||||
let inner = |content: String| format!("│ {} │", pad_visible(content, w));
|
let inner = |content: String| format!("│ {} │", pad_visible(content, w));
|
||||||
let blank = || inner(" ".to_string());
|
let blank = || inner(" ".to_string());
|
||||||
|
|
||||||
// Top border with the centered header.
|
// Top border with the centered header (title in the theme header color).
|
||||||
let header = format!(" Agent Manager v{version} · shell gateway ");
|
let title = format!(" Agent Manager v{version} · shell gateway ");
|
||||||
let fill = w + 2 - visible_len(&header);
|
let title = if color {
|
||||||
|
theme.hdr(&title)
|
||||||
|
} else {
|
||||||
|
title
|
||||||
|
};
|
||||||
|
let fill = w + 2 - visible_len(&title);
|
||||||
let left = fill / 2;
|
let left = fill / 2;
|
||||||
let right = fill - left;
|
let right = fill - left;
|
||||||
rows.push(dim(
|
rows.push(dim(
|
||||||
&format!("╭{}{}{}╮", "─".repeat(left), header, "─".repeat(right)),
|
&format!("╭{}{}{}╮", "─".repeat(left), title, "─".repeat(right)),
|
||||||
|
theme,
|
||||||
color,
|
color,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Block-art title.
|
// Block-art title, painted in the theme accent color.
|
||||||
for art in banner_art() {
|
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.
|
// Shell gateway: the user's default shell is highlighted at startup.
|
||||||
rows.push(blank());
|
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!(
|
let current = format!(
|
||||||
"◉ {} — {}{}",
|
"◉ {} — {}{}",
|
||||||
green(session.current.name, color),
|
green(session.current.name, theme, color),
|
||||||
session.current.title,
|
session.current.title,
|
||||||
if session.is_default() { " (default)" } else { "" }
|
if session.is_default() { " (default)" } else { "" }
|
||||||
);
|
);
|
||||||
@@ -495,15 +512,50 @@ pub fn banner_box(
|
|||||||
} else {
|
} else {
|
||||||
others.join(" · ")
|
others.join(" · ")
|
||||||
};
|
};
|
||||||
rows.push(inner(dim(&format!(" ⇄ {avail}"), color)));
|
rows.push(inner(dim(&format!(" ⇄ {avail}"), theme, color)));
|
||||||
rows.push(inner(dim(
|
rows.push(inner(dim(
|
||||||
" system commands run in the active shell — 'shell <name>' switches",
|
" 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,
|
color,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// am command groups.
|
// am command groups.
|
||||||
rows.push(blank());
|
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(" catalog list · status · search · info".to_string()));
|
||||||
rows.push(inner(
|
rows.push(inner(
|
||||||
" lifecycle install · uninstall · update · start · stop · restart · run".to_string(),
|
" lifecycle install · uninstall · update · start · stop · restart · run".to_string(),
|
||||||
@@ -520,9 +572,11 @@ pub fn banner_box(
|
|||||||
rows.push(blank());
|
rows.push(blank());
|
||||||
rows.push(inner(dim(
|
rows.push(inner(dim(
|
||||||
&format!(
|
&format!(
|
||||||
"am v{version} · {agents} agents · {external} external · {installed} managed · shell {} · /help for commands",
|
"am v{version} · {agents} agents · {external} external · {installed} managed · shell {} · theme {} · /help for commands",
|
||||||
session.current.name
|
session.current.name,
|
||||||
|
theme.name
|
||||||
),
|
),
|
||||||
|
theme,
|
||||||
color,
|
color,
|
||||||
)));
|
)));
|
||||||
let prefix = format!(" session {session_id} · cwd ");
|
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(format!("{prefix}{}", shorten_head(cwd, budget))));
|
||||||
rows.push(inner(yellow(
|
rows.push(inner(yellow(
|
||||||
" ✦ Tip: 'shell <name>' switches the system shell · '!' forces a system command",
|
" ✦ Tip: 'shell <name>' switches the system shell · '!' forces a system command",
|
||||||
|
theme,
|
||||||
color,
|
color,
|
||||||
)));
|
)));
|
||||||
rows.push(dim(&format!("╰{}╯", "─".repeat(w + 2)), color));
|
rows.push(dim(&format!("╰{}╯", "─".repeat(w + 2)), theme, color));
|
||||||
|
|
||||||
rows.join("\n")
|
rows.join("\n")
|
||||||
}
|
}
|
||||||
@@ -540,6 +595,7 @@ pub fn banner_box(
|
|||||||
/// The full startup banner (box + welcome + tip).
|
/// The full startup banner (box + welcome + tip).
|
||||||
pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
||||||
let color = app.color();
|
let color = app.color();
|
||||||
|
let theme = app.theme();
|
||||||
let mut text = banner_box(
|
let mut text = banner_box(
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
agent_count(app),
|
agent_count(app),
|
||||||
@@ -548,6 +604,7 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
|||||||
session,
|
session,
|
||||||
session_id,
|
session_id,
|
||||||
¤t_dir_string(),
|
¤t_dir_string(),
|
||||||
|
theme,
|
||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
text.push('\n');
|
text.push('\n');
|
||||||
@@ -556,28 +613,30 @@ pub fn banner(app: &App, session: &ShellSession, session_id: &str) -> String {
|
|||||||
);
|
);
|
||||||
text.push_str(&format!(
|
text.push_str(&format!(
|
||||||
"{} {}\n",
|
"{} {}\n",
|
||||||
yellow("✦ Tip:", color),
|
yellow("✦ Tip:", theme, color),
|
||||||
dim(
|
dim(
|
||||||
&format!(
|
&format!(
|
||||||
"system commands run in {} — 'shell <name>' switches · '!' forces · 'cd' works in this session",
|
"system commands run in {} — 'shell <name>' switches · '!' forces · 'cd' works in this session",
|
||||||
session.current.name
|
session.current.name
|
||||||
),
|
),
|
||||||
|
theme,
|
||||||
color,
|
color,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Color an emoji glyph (style names: green, blue, yellow, cyan).
|
/// Color an emoji glyph with the active theme (style names: green, blue,
|
||||||
fn emoji(glyph: &str, style: &str, color: bool) -> String {
|
/// yellow, cyan).
|
||||||
|
fn emoji(glyph: &str, style: &str, theme: &Theme, color: bool) -> String {
|
||||||
if !color {
|
if !color {
|
||||||
return glyph.to_string();
|
return glyph.to_string();
|
||||||
}
|
}
|
||||||
match style {
|
match style {
|
||||||
"green" => glyph.green().to_string(),
|
"green" => theme.acc(glyph),
|
||||||
"blue" => glyph.blue().to_string(),
|
"blue" => theme.inf(glyph),
|
||||||
"yellow" => glyph.yellow().to_string(),
|
"yellow" => theme.warn(glyph),
|
||||||
"cyan" => glyph.cyan().to_string(),
|
"cyan" => theme.inf(glyph),
|
||||||
_ => glyph.to_string(),
|
_ => glyph.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,19 +647,24 @@ fn emoji(glyph: &str, style: &str, color: bool) -> String {
|
|||||||
/// the first one).
|
/// the first one).
|
||||||
fn print_status(app: &App, started: Instant, last: Option<Duration>) {
|
fn print_status(app: &App, started: Instant, last: Option<Duration>) {
|
||||||
let color = app.color();
|
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 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!(
|
let counts = format!(
|
||||||
"{} {} {} {} {} {} {} {}",
|
"{} {} {} {} {} {} {} {}",
|
||||||
dim(&agent_count(app).to_string(), color),
|
dim(&agent_count(app).to_string(), theme, color),
|
||||||
emoji("🤖", "green", color),
|
emoji("🤖", "green", theme, color),
|
||||||
dim("·", color),
|
dim("·", theme, color),
|
||||||
dim(&external_count(app).to_string(), color),
|
dim(&external_count(app).to_string(), theme, color),
|
||||||
emoji("🌐", "blue", color),
|
emoji("🌐", "blue", theme, color),
|
||||||
dim("·", color),
|
dim("·", theme, color),
|
||||||
dim(&installed_count(app).to_string(), color),
|
dim(&installed_count(app).to_string(), theme, color),
|
||||||
emoji("📊", "yellow", color),
|
emoji("📊", "yellow", theme, color),
|
||||||
);
|
);
|
||||||
let last_text = match last {
|
let last_text = match last {
|
||||||
Some(d) if d.as_secs() >= 1 => format_elapsed(d),
|
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()),
|
Some(d) => format!("{}µs", d.as_micros()),
|
||||||
None => "—".to_string(),
|
None => "—".to_string(),
|
||||||
};
|
};
|
||||||
let last_part = format!("{} {}", emoji("⚡", "yellow", color), dim(&last_text, color));
|
let last_part = format!(
|
||||||
let elapsed = dim(&format!("⏲ {}", format_elapsed(started.elapsed())), color);
|
"{} {}",
|
||||||
|
emoji("⚡", "yellow", theme, color),
|
||||||
|
dim(&last_text, theme, color)
|
||||||
|
);
|
||||||
|
let elapsed = dim(
|
||||||
|
&format!("⏲ {}", format_elapsed(started.elapsed())),
|
||||||
|
theme,
|
||||||
|
color,
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"{}am v{}{sep}{folder}{sep}{counts}{sep}{last_part}{sep}{elapsed}",
|
"{}am v{}{sep}{folder}{sep}{counts}{sep}{last_part}{sep}{elapsed}",
|
||||||
dim(" ⚙ ", color),
|
dim(" ⚙ ", theme, color),
|
||||||
env!("CARGO_PKG_VERSION"),
|
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 {
|
if color {
|
||||||
s.dimmed().to_string()
|
theme.dimmed(s)
|
||||||
} else {
|
} else {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bold_cyan(s: &str, color: bool) -> String {
|
fn green(s: &str, theme: &Theme, color: bool) -> String {
|
||||||
if color {
|
if color {
|
||||||
s.cyan().bold().to_string()
|
theme.acc(s)
|
||||||
} else {
|
} else {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn green(s: &str, color: bool) -> String {
|
fn yellow(s: &str, theme: &Theme, color: bool) -> String {
|
||||||
if color {
|
if color {
|
||||||
s.green().bold().to_string()
|
theme.warn(s)
|
||||||
} else {
|
|
||||||
s.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn yellow(s: &str, color: bool) -> String {
|
|
||||||
if color {
|
|
||||||
s.yellow().to_string()
|
|
||||||
} else {
|
} else {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
@@ -1440,6 +1504,7 @@ mod tests {
|
|||||||
&session,
|
&session,
|
||||||
"20260815_123456_abcdef",
|
"20260815_123456_abcdef",
|
||||||
"/home/alice/dev/agent-manager",
|
"/home/alice/dev/agent-manager",
|
||||||
|
crate::theme::default_theme(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
let widths: Vec<usize> = text.lines().map(|l| l.chars().count()).collect();
|
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("12 external"));
|
||||||
assert!(text.contains("3 managed"));
|
assert!(text.contains("3 managed"));
|
||||||
assert!(text.contains("agent-manager"));
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user