1use std::io::IsTerminal;
18use std::path::Path;
19
20use crate::daemon_presence::{self, Mode};
21use crate::p10k::wizard::templates::visible_width;
22use crate::ported::zsh_h::{DISABLED, PM_UNSET, STAT_NOPRINT};
23
24pub(crate) const LOGO: [(&str, &str); 6] = [
27 ("36", " ███████╗███████╗██╗ ██╗██████╗ ███████╗"),
28 ("36", " ╚══███╔╝██╔════╝██║ ██║██╔══██╗██╔════╝"),
29 ("35", " ███╔╝ ███████╗███████║██████╔╝███████╗"),
30 ("35", " ███╔╝ ╚════██║██╔══██║██╔══██╗╚════██║"),
31 ("31", " ███████╗███████║██║ ██║██║ ██║███████║"),
32 ("31", " ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝"),
33];
34
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38pub struct ShellCounts {
39 pub functions: usize,
40 pub aliases: usize,
41 pub parameters: usize,
42 pub jobs: usize,
43}
44
45impl ShellCounts {
46 pub fn read() -> Self {
51 let functions = crate::ported::hashtable::shfunctab_lock()
52 .read()
53 .map(|tab| {
54 tab.iter()
55 .filter(|(_, f)| f.node.flags & DISABLED == 0)
56 .count()
57 })
58 .unwrap_or(0);
59 let aliases = crate::ported::hashtable::aliastab_lock()
60 .read()
61 .map(|tab| {
62 tab.iter()
63 .filter(|(_, a)| a.node.flags & DISABLED == 0)
64 .count()
65 })
66 .unwrap_or(0);
67 let parameters = crate::ported::params::paramtab()
68 .read()
69 .map(|tab| {
70 tab.iter()
71 .filter(|(_, p)| p.node.flags as u32 & PM_UNSET == 0)
72 .count()
73 })
74 .unwrap_or(0);
75 let (jobtab, maxjob) = crate::ported::jobs::selectjobtab();
76 let jobs = (1..=maxjob)
77 .filter_map(|n| jobtab.get(n))
78 .filter(|j| j.stat != 0 && !j.procs.is_empty() && j.stat & STAT_NOPRINT == 0)
79 .count();
80 Self {
81 functions,
82 aliases,
83 parameters,
84 jobs,
85 }
86 }
87}
88
89pub fn print_banner(shell: Option<ShellCounts>) {
93 let color = colored();
94 for (code, line) in LOGO {
95 println!("{}", paint(line, code, color));
96 }
97
98 let summary = format!(
99 " ZSHRS // v{} // {} builtins // {} extensions ",
100 env!("CARGO_PKG_VERSION"),
101 crate::ext_builtins::compat_builtin_names().len(),
102 crate::ext_builtins::extension_builtin_names().len(),
103 );
104 for line in box_lines(&summary, color) {
105 println!("{line}");
106 }
107
108 let socket = daemon_presence::socket_path();
109 println!(
110 "{}",
111 live_line(socket.as_deref(), daemon_presence::check(), shell, color)
112 );
113}
114
115fn colored() -> bool {
117 std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none()
118}
119
120fn paint(s: &str, code: &str, color: bool) -> String {
122 if color {
123 format!("\x1b[{code}m{s}\x1b[0m")
124 } else {
125 s.to_string()
126 }
127}
128
129fn live_line(socket: Option<&Path>, mode: Mode, shell: Option<ShellCounts>, color: bool) -> String {
132 let socket = socket
133 .map(tilde)
134 .unwrap_or_else(|| "(no socket path)".to_string());
135 let state = match mode {
136 Mode::Present => paint("// up", "32", color),
137 Mode::Disabled => paint("// daemon disabled", "33", color),
138 Mode::Absent | Mode::Unknown => paint("// no daemon running", "31", color),
139 };
140 let mut line = format!(
141 "{} {state}",
142 paint(&format!(" daemon {socket}"), "2", color)
143 );
144 if let Some(c) = shell {
145 for part in [
146 count(c.functions, "function", "functions"),
147 count(c.aliases, "alias", "aliases"),
148 count(c.parameters, "parameter", "parameters"),
149 count(c.jobs, "job", "jobs"),
150 ] {
151 line.push_str(" ");
152 line.push_str(&part);
153 }
154 }
155 line
156}
157
158fn count(n: usize, one: &str, many: &str) -> String {
160 format!("{n} {}", if n == 1 { one } else { many })
161}
162
163fn tilde(path: &Path) -> String {
165 let shown = path.display().to_string();
166 match std::env::var("HOME") {
167 Ok(home) if !home.is_empty() && shown.starts_with(&home) => {
168 format!("~{}", &shown[home.len()..])
169 }
170 _ => shown,
171 }
172}
173
174fn box_lines(line: &str, color: bool) -> [String; 3] {
178 let rule = "─".repeat(visible_width(line));
179 let border = |s: &str| paint(s, "36", color);
180 [
181 border(&format!(" ┌{rule}┐")),
182 format!("{}{line}{}", border(" │"), border("│")),
183 border(&format!(" └{rule}┘")),
184 ]
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn banner_box_borders_match_the_content_width() {
193 for content in [
196 " ZSHRS // v0.12.61 // 157 builtins // 146 extensions ",
197 " ZSHRS // v10.100.1000 // 1 builtin // 1 extension ",
198 "",
199 ] {
200 for color in [false, true] {
201 let widths: Vec<usize> = box_lines(content, color)
202 .iter()
203 .map(|l| visible_width(l))
204 .collect();
205 assert_eq!(
206 widths[0], widths[1],
207 "top border and content disagree for {content:?} (color: {color})"
208 );
209 assert_eq!(
210 widths[1], widths[2],
211 "content and bottom border disagree for {content:?} (color: {color})"
212 );
213 }
214 }
215 }
216
217 #[test]
218 fn logo_lines_share_one_printed_width() {
219 let widths: Vec<usize> = LOGO.iter().map(|(_, l)| visible_width(l)).collect();
220 assert!(
221 widths.iter().all(|w| *w == widths[0]),
222 "logo lines are ragged: {widths:?}"
223 );
224 }
225
226 #[test]
227 fn live_line_says_why_the_daemon_is_missing_instead_of_claiming_it_is_up() {
228 let sock = Path::new("/tmp/zshrs-test/daemon.sock");
229 assert!(live_line(Some(sock), Mode::Absent, None, false).contains("// no daemon running"));
230 assert!(live_line(Some(sock), Mode::Disabled, None, false).contains("// daemon disabled"));
231 assert!(live_line(Some(sock), Mode::Present, None, false).contains("// up"));
232 assert!(live_line(None, Mode::Absent, None, false).contains("(no socket path)"));
233 }
234
235 #[test]
236 fn live_line_counts_are_plural_correct_and_absent_without_a_shell() {
237 let one = ShellCounts {
238 functions: 1,
239 aliases: 1,
240 parameters: 1,
241 jobs: 1,
242 };
243 let line = live_line(None, Mode::Present, Some(one), false);
244 assert!(
245 line.ends_with("1 function 1 alias 1 parameter 1 job"),
246 "{line:?}"
247 );
248 let line = live_line(None, Mode::Present, Some(ShellCounts::default()), false);
249 assert!(
250 line.ends_with("0 functions 0 aliases 0 parameters 0 jobs"),
251 "{line:?}"
252 );
253 assert!(!live_line(None, Mode::Present, None, false).contains("functions"));
254 }
255}