use std::io::IsTerminal;
use std::path::Path;
use crate::daemon_presence::{self, Mode};
use crate::p10k::wizard::templates::visible_width;
use crate::ported::zsh_h::{DISABLED, PM_UNSET, STAT_NOPRINT};
const LOGO: [(&str, &str); 6] = [
("36", " ███████╗███████╗██╗ ██╗██████╗ ███████╗"),
("36", " ╚══███╔╝██╔════╝██║ ██║██╔══██╗██╔════╝"),
("35", " ███╔╝ ███████╗███████║██████╔╝███████╗"),
("35", " ███╔╝ ╚════██║██╔══██║██╔══██╗╚════██║"),
("31", " ███████╗███████║██║ ██║██║ ██║███████║"),
("31", " ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝"),
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ShellCounts {
pub functions: usize,
pub aliases: usize,
pub parameters: usize,
pub jobs: usize,
}
impl ShellCounts {
pub fn read() -> Self {
let functions = crate::ported::hashtable::shfunctab_lock()
.read()
.map(|tab| {
tab.iter()
.filter(|(_, f)| f.node.flags & DISABLED == 0)
.count()
})
.unwrap_or(0);
let aliases = crate::ported::hashtable::aliastab_lock()
.read()
.map(|tab| {
tab.iter()
.filter(|(_, a)| a.node.flags & DISABLED == 0)
.count()
})
.unwrap_or(0);
let parameters = crate::ported::params::paramtab()
.read()
.map(|tab| {
tab.iter()
.filter(|(_, p)| p.node.flags as u32 & PM_UNSET == 0)
.count()
})
.unwrap_or(0);
let (jobtab, maxjob) = crate::ported::jobs::selectjobtab();
let jobs = (1..=maxjob)
.filter_map(|n| jobtab.get(n))
.filter(|j| j.stat != 0 && !j.procs.is_empty() && j.stat & STAT_NOPRINT == 0)
.count();
Self {
functions,
aliases,
parameters,
jobs,
}
}
}
pub fn print_banner(shell: Option<ShellCounts>) {
let color = colored();
for (code, line) in LOGO {
println!("{}", paint(line, code, color));
}
let summary = format!(
" ZSHRS // v{} // {} builtins // {} extensions ",
env!("CARGO_PKG_VERSION"),
crate::ext_builtins::compat_builtin_names().len(),
crate::ext_builtins::extension_builtin_names().len(),
);
for line in box_lines(&summary, color) {
println!("{line}");
}
let socket = daemon_presence::socket_path();
println!(
"{}",
live_line(socket.as_deref(), daemon_presence::check(), shell, color)
);
}
fn colored() -> bool {
std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none()
}
fn paint(s: &str, code: &str, color: bool) -> String {
if color {
format!("\x1b[{code}m{s}\x1b[0m")
} else {
s.to_string()
}
}
fn live_line(socket: Option<&Path>, mode: Mode, shell: Option<ShellCounts>, color: bool) -> String {
let socket = socket
.map(tilde)
.unwrap_or_else(|| "(no socket path)".to_string());
let state = match mode {
Mode::Present => paint("// up", "32", color),
Mode::Disabled => paint("// daemon disabled", "33", color),
Mode::Absent | Mode::Unknown => paint("// no daemon running", "31", color),
};
let mut line = format!(
"{} {state}",
paint(&format!(" daemon {socket}"), "2", color)
);
if let Some(c) = shell {
for part in [
count(c.functions, "function", "functions"),
count(c.aliases, "alias", "aliases"),
count(c.parameters, "parameter", "parameters"),
count(c.jobs, "job", "jobs"),
] {
line.push_str(" ");
line.push_str(&part);
}
}
line
}
fn count(n: usize, one: &str, many: &str) -> String {
format!("{n} {}", if n == 1 { one } else { many })
}
fn tilde(path: &Path) -> String {
let shown = path.display().to_string();
match std::env::var("HOME") {
Ok(home) if !home.is_empty() && shown.starts_with(&home) => {
format!("~{}", &shown[home.len()..])
}
_ => shown,
}
}
fn box_lines(line: &str, color: bool) -> [String; 3] {
let rule = "─".repeat(visible_width(line));
let border = |s: &str| paint(s, "36", color);
[
border(&format!(" ┌{rule}┐")),
format!("{}{line}{}", border(" │"), border("│")),
border(&format!(" └{rule}┘")),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn banner_box_borders_match_the_content_width() {
for content in [
" ZSHRS // v0.12.61 // 157 builtins // 146 extensions ",
" ZSHRS // v10.100.1000 // 1 builtin // 1 extension ",
"",
] {
for color in [false, true] {
let widths: Vec<usize> = box_lines(content, color)
.iter()
.map(|l| visible_width(l))
.collect();
assert_eq!(
widths[0], widths[1],
"top border and content disagree for {content:?} (color: {color})"
);
assert_eq!(
widths[1], widths[2],
"content and bottom border disagree for {content:?} (color: {color})"
);
}
}
}
#[test]
fn logo_lines_share_one_printed_width() {
let widths: Vec<usize> = LOGO.iter().map(|(_, l)| visible_width(l)).collect();
assert!(
widths.iter().all(|w| *w == widths[0]),
"logo lines are ragged: {widths:?}"
);
}
#[test]
fn live_line_says_why_the_daemon_is_missing_instead_of_claiming_it_is_up() {
let sock = Path::new("/tmp/zshrs-test/daemon.sock");
assert!(live_line(Some(sock), Mode::Absent, None, false).contains("// no daemon running"));
assert!(live_line(Some(sock), Mode::Disabled, None, false).contains("// daemon disabled"));
assert!(live_line(Some(sock), Mode::Present, None, false).contains("// up"));
assert!(live_line(None, Mode::Absent, None, false).contains("(no socket path)"));
}
#[test]
fn live_line_counts_are_plural_correct_and_absent_without_a_shell() {
let one = ShellCounts {
functions: 1,
aliases: 1,
parameters: 1,
jobs: 1,
};
let line = live_line(None, Mode::Present, Some(one), false);
assert!(
line.ends_with("1 function 1 alias 1 parameter 1 job"),
"{line:?}"
);
let line = live_line(None, Mode::Present, Some(ShellCounts::default()), false);
assert!(
line.ends_with("0 functions 0 aliases 0 parameters 0 jobs"),
"{line:?}"
);
assert!(!live_line(None, Mode::Present, None, false).contains("functions"));
}
}