Skip to content

Commit eabbe2b

Browse files
authored
feat(cli): print the logo in block characters as the startup banner (#631)
## Motivation The startup banner becomes the ethlambda logo, drawn with block characters from the code page 437 set (`█ ▀ ▄ ░`): ``` █ ▄ ▀▀ █ ▄█▀▀▀▄▄ ▄ ▄ ▄▄ ▀▀ ▄ ░░░██░░░░░▀▄░░░ ▀ █ ░ █░░ ▄ ░░ ░░░▀▀▀░░░ ░░ █ ▄ ░░░ ▄██░ ▀ ░▄█▀░░█▀▀▀█░░▀█▄░ █ ▄ ░▄▄▄ █▀▀ ▀▀ ░░█▀▄▄░░▄▄▄░░▄▄▀█░░ ▀ ▀ ▀▀█ █░░ ▄ ▄▄ ░░ █░░█▄▀░▄░▀▄█░░█ ░░ ▄▄ ░░█ █░░░ ░▀▀░░ █▄░░█▄▀█▀▄█░░▄█ ░░▀▀█ ░░░█ ░██▄░░░░ ▄▄▄░ ▀█▄▄█▄░▄█▄▄█▀ ▄▄▄ ░░░░▄██░ ▀█░▄▄▀▀░░░░░ ▄░▀▀░░░▀▀░▄ ░░░░░▀▀ ▄▄█▀ ██▀░░░░░▄░░░█ ░░▀▀▀░░ █░░░░░░░░ ░▀█ ▄██░░░░▄▄ █░░░░█▄ ▄ ▄ ▄█░░░ ███░ ░░▀█▄ █▀░░░▄█▀ █░░▄░░▀█▄▄▄█▀░░░░░ ░░ █▄░░░░▀ ▀░░░██ ▄█▀░░▄ ▄ ░▄░░░▀▄ ██░░░░ ░░░▀█▄ █▀░░▄█▀░▄▀ ░▄█ ░▄░░▀ ▄ ▀░░░░ ░██▄░ █░░▄█ ░█▄ ██▄ ██░░ ██░░░░ ▀▄ ░░█░ ░░▀█▄ ░█░ ░█ █▀░░ ██░░ ▀▄░▄█▀ ▄ ░░██ █░ ▄▀▀█░░ ▀█▄░░ ▄ ▀▀ ▀▀█▀ █ ▄▄█▀ ▄▀▄█ █▄ ▄ ▀███▀ ▀ ▄ ▀ ▀▀ ▄ ▀▀ ▀▀▄ ▀▀ ░ ▀ ▀ █▀▀▀▀█▀▀▀█▀██▀█▀█████▀▀██▀███▀█▀▀▀██▀▀▀███▀▀██ █ ▀▀███ ██ ▀▀ █ ████ ▀▀ █ ▄▀▄ █ ▀▀▄█ ██ █ ▀▀ █ █ ▀▀▀██ ██ ██ █ ▀▀▀█ ██ █ ███ █ ▀▀▄█ ▀▀▄█ ██ █ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ``` Those characters only render on a terminal that decodes UTF-8, and a process cannot ask its terminal which encoding it uses. So the banner follows the locale the terminal advertises, and anything that is not a UTF-8 locale keeps printing the current ASCII wordmark. ## Changes - `bin/ethlambda/assets/ethlambda_banner.txt`: the new logo. - `bin/ethlambda/assets/ethlambda_banner_ascii.txt`: the previous banner, moved out of `main.rs` unchanged. - `bin/ethlambda/src/banner.rs`: embeds both with `include_str!`, and `for_locale()` picks one. - The first non-empty of `LC_ALL`, `LC_CTYPE` and `LANG` decides, which is POSIX precedence. It counts as UTF-8 when it contains `utf-8` or `utf8`, in any case. With none of them set the locale is `C`, so ASCII. - Values are read with `env::var_os`: a set variable whose bytes are not valid Unicode still takes precedence, where `env::var` would skip it and let a lower-ranked variable decide. - `bin/ethlambda/src/main.rs`: drops the `ASCII_ART` constant and prints `banner::for_locale()`. ## Behavior - On a non-UTF-8 locale the output is byte for byte what it was: a blank line, the wordmark, a blank line. - The Docker image does not set `LANG`, so containers keep printing the ASCII banner unless run with `-e LANG=C.UTF-8`. - The choice is made by the process writing the output. `docker logs` or a log file read somewhere else shows whatever the writer's locale picked. ## Tests - 8 unit tests in `banner::tests`: variable precedence, empty values, non-UTF-8 encodings, a non-Unicode `LC_ALL`, and that the fallback file is pure ASCII. They pass the variables in through a closure, so they never touch the process environment. - Ran the binary with `LANG=en_US.UTF-8`, `LANG=C` and no `LANG`: the block logo, then the ASCII wordmark both times. ## ✅ Verification Checklist - [x] `cargo fmt --all -- --check`: clean - [x] `cargo clippy -p ethlambda --profile release-fast --all-targets -- -D warnings`: clean. Scoped to this crate, since the change touches nothing else - [x] `cargo test -p ethlambda --profile release-fast --bins banner`: 8 passing. The full `make test` was not run
1 parent ebbbde7 commit eabbe2b

4 files changed

Lines changed: 143 additions & 9 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
█ ▄
2+
▀▀ █ ▄█▀▀▀▄▄ ▄ ▄
3+
▄▄ ▀▀ ▄ ░░░██░░░░░▀▄░░░ ▀ █ ░
4+
█░░ ▄ ░░ ░░░▀▀▀░░░ ░░ █ ▄ ░░░
5+
▄██░ ▀ ░▄█▀░░█▀▀▀█░░▀█▄░ █ ▄ ░▄▄▄
6+
█▀▀ ▀▀ ░░█▀▄▄░░▄▄▄░░▄▄▀█░░ ▀ ▀ ▀▀█
7+
█░░ ▄ ▄▄ ░░ █░░█▄▀░▄░▀▄█░░█ ░░ ▄▄ ░░█
8+
█░░░ ░▀▀░░ █▄░░█▄▀█▀▄█░░▄█ ░░▀▀█ ░░░█
9+
░██▄░░░░ ▄▄▄░ ▀█▄▄█▄░▄█▄▄█▀ ▄▄▄ ░░░░▄██░
10+
▀█░▄▄▀▀░░░░░ ▄░▀▀░░░▀▀░▄ ░░░░░▀▀ ▄▄█▀
11+
██▀░░░░░▄░░░█ ░░▀▀▀░░ █░░░░░░░░ ░▀█
12+
▄██░░░░▄▄ █░░░░█▄ ▄ ▄ ▄█░░░ ███░ ░░▀█▄
13+
█▀░░░▄█▀ █░░▄░░▀█▄▄▄█▀░░░░░ ░░ █▄░░░░▀
14+
▀░░░██ ▄█▀░░▄ ▄ ░▄░░░▀▄ ██░░░░
15+
░░░▀█▄ █▀░░▄█▀░▄▀ ░▄█ ░▄░░▀ ▄ ▀░░░░
16+
░██▄░ █░░▄█ ░█▄ ██▄ ██░░ ██░░░░
17+
▀▄ ░░█░ ░░▀█▄ ░█░ ░█ █▀░░ ██░░
18+
▀▄░▄█▀ ▄ ░░██ █░ ▄▀▀█░░ ▀█▄░░ ▄
19+
▀▀ ▀▀█▀ █ ▄▄█▀ ▄▀▄█ █▄ ▄ ▀███▀
20+
▀ ▄ ▀ ▀▀ ▄ ▀▀ ▀▀▄ ▀▀ ░ ▀ ▀
21+
█▀▀▀▀█▀▀▀█▀██▀█▀█████▀▀██▀███▀█▀▀▀██▀▀▀███▀▀██
22+
█ ▀▀███ ██ ▀▀ █ ████ ▀▀ █ ▄▀▄ █ ▀▀▄█ ██ █ ▀▀ █
23+
█ ▀▀▀██ ██ ██ █ ▀▀▀█ ██ █ ███ █ ▀▀▄█ ▀▀▄█ ██ █
24+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
_ _ _ _ _
2+
___| |_| |__ | | __ _ _ __ ___ | |__ __| | __ _
3+
/ _ \ __| '_ \| |/ _` | '_ ` _ \| '_ \ / _` |/ _` |
4+
| __/ |_| | | | | (_| | | | | | | |_) | (_| | (_| |
5+
\___|\__|_| |_|_|\__,_|_| |_| |_|_.__/ \__,_|\__,_|

‎bin/ethlambda/src/banner.rs‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! The startup banner, in one of two encodings.
2+
3+
use std::ffi::OsString;
4+
5+
/// The logo, drawn with block characters from the code page 437 set.
6+
const UTF8: &str = include_str!("../assets/ethlambda_banner.txt");
7+
8+
/// The wordmark in plain ASCII, for terminals that cannot decode `UTF8`.
9+
const ASCII: &str = include_str!("../assets/ethlambda_banner_ascii.txt");
10+
11+
/// The banner this process's terminal can display.
12+
///
13+
/// A terminal's encoding is not something a process can query, so this reads
14+
/// the locale, which is how terminals and their shells advertise it.
15+
pub fn for_locale() -> &'static str {
16+
if locale_is_utf8(|name| std::env::var_os(name)) {
17+
UTF8
18+
} else {
19+
ASCII
20+
}
21+
}
22+
23+
/// Whether the locale's character encoding is UTF-8.
24+
///
25+
/// Follows POSIX precedence: the first of `LC_ALL`, `LC_CTYPE` and `LANG` that
26+
/// is set and non-empty decides. With none of them set the locale is `C`, whose
27+
/// encoding is ASCII.
28+
///
29+
/// Values are read as `OsString` because a variable holding bytes that are not
30+
/// valid Unicode is still set: `env::var` would report it as absent, handing the
31+
/// decision to a variable POSIX ranks below it.
32+
fn locale_is_utf8(var: impl Fn(&str) -> Option<OsString>) -> bool {
33+
["LC_ALL", "LC_CTYPE", "LANG"]
34+
.into_iter()
35+
.find_map(|name| var(name).filter(|value| !value.is_empty()))
36+
.is_some_and(|locale| {
37+
let locale = locale.to_string_lossy().to_ascii_lowercase();
38+
locale.contains("utf-8") || locale.contains("utf8")
39+
})
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use super::*;
45+
46+
fn env<'a>(vars: &'a [(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + 'a {
47+
|name| {
48+
vars.iter()
49+
.find(|(key, _)| *key == name)
50+
.map(|(_, value)| OsString::from(value))
51+
}
52+
}
53+
54+
#[test]
55+
fn a_utf8_lang_selects_utf8() {
56+
assert!(locale_is_utf8(env(&[("LANG", "en_US.UTF-8")])));
57+
assert!(locale_is_utf8(env(&[("LANG", "C.utf8")])));
58+
}
59+
60+
#[test]
61+
fn no_locale_is_ascii() {
62+
assert!(!locale_is_utf8(env(&[])));
63+
}
64+
65+
#[test]
66+
fn a_non_utf8_encoding_is_ascii() {
67+
assert!(!locale_is_utf8(env(&[("LANG", "en_US.ISO-8859-1")])));
68+
assert!(!locale_is_utf8(env(&[("LANG", "C")])));
69+
}
70+
71+
#[test]
72+
fn lc_all_overrides_everything() {
73+
let vars = [
74+
("LC_ALL", "C"),
75+
("LC_CTYPE", "en_US.UTF-8"),
76+
("LANG", "en_US.UTF-8"),
77+
];
78+
assert!(!locale_is_utf8(env(&vars)));
79+
}
80+
81+
#[test]
82+
fn lc_ctype_overrides_lang() {
83+
let utf8_ctype = [("LC_CTYPE", "en_US.UTF-8"), ("LANG", "C")];
84+
assert!(locale_is_utf8(env(&utf8_ctype)));
85+
let c_ctype = [("LC_CTYPE", "C"), ("LANG", "en_US.UTF-8")];
86+
assert!(!locale_is_utf8(env(&c_ctype)));
87+
}
88+
89+
#[test]
90+
fn an_empty_variable_counts_as_unset() {
91+
let vars = [("LC_ALL", ""), ("LANG", "en_US.UTF-8")];
92+
assert!(locale_is_utf8(env(&vars)));
93+
}
94+
95+
#[cfg(unix)]
96+
#[test]
97+
fn a_non_unicode_value_still_takes_precedence() {
98+
use std::os::unix::ffi::OsStrExt;
99+
100+
let var = |name: &str| match name {
101+
"LC_ALL" => Some(std::ffi::OsStr::from_bytes(b"\xff").to_os_string()),
102+
"LANG" => Some(OsString::from("en_US.UTF-8")),
103+
_ => None,
104+
};
105+
assert!(!locale_is_utf8(var));
106+
}
107+
108+
#[test]
109+
fn the_fallback_is_ascii() {
110+
assert!(ASCII.is_ascii());
111+
}
112+
}

‎bin/ethlambda/src/main.rs‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod banner;
12
mod benchmark;
23
mod checkpoint_sync;
34
mod cli;
@@ -62,14 +63,6 @@ use ethlambda_storage::{
6263
MAX_RESUMABLE_DB_STATE_AGE, StorageBackend, Store, backend::RocksDBBackend,
6364
};
6465

65-
const ASCII_ART: &str = r#"
66-
_ _ _ _ _
67-
___| |_| |__ | | __ _ _ __ ___ | |__ __| | __ _
68-
/ _ \ __| '_ \| |/ _` | '_ ` _ \| '_ \ / _` |/ _` |
69-
| __/ |_| | | | | (_| | | | | | | |_) | (_| | (_| |
70-
\___|\__|_| |_|_|\__,_|_| |_| |_|_.__/ \__,_|\__,_|
71-
"#;
72-
7366
fn main() -> eyre::Result<()> {
7467
match command::parse() {
7568
Command::Node(options) => {
@@ -165,7 +158,7 @@ async fn run_node(options: NodeOptions) -> eyre::Result<()> {
165158
version: version::CLIENT_VERSION,
166159
};
167160

168-
println!("{ASCII_ART}");
161+
println!("\n{}", banner::for_locale());
169162

170163
info!(version = version::CLIENT_VERSION, "Starting ethlambda");
171164

0 commit comments

Comments
 (0)