#33 m6/owner-logo
Patch
src/logo.rs+119 −7--- a/src/logo.rs+++ b/src/logo.rs@@ -1,29 +1,44 @@1-//! hu13 logo + ASCII banner (m6, f0rge issues #15 + #27 + #28).1+//! hu13 logo + ASCII banner (m6, f0rge issues #15 + #26 + #27 + #28).22 //!33 //! The lockup lives hand-written in `static/logo.svg` (diamond-hub mark +44 //! hu13 lettered as <path>, no <text>/fonts; reads at 24px header size)55 //! plus `static/wordmark.svg` (legacy horizontal lockup); the simplified66 //! 16px favicon variant lives in `static/favicon.svg` (mark only, no77 //! letters — small sizes stay legible, full lettering stays in logo.svg);8-//! the text banner lives in `static/ascii.txt`.8+//! the text banner lives in `static/ascii.txt` (owner block-art, 12 lines,9+//! stripped from `static/logo-ansi.txt`).910 //!10-//! [`ASCII_ART`], [`LOGO_SVG`], [`FAVICON_SVG`] and [`WORDMARK_SVG`]11-//! re-expose those static files for later issues (navbar #16, favicon #17,12-//! wiki README #19, agents page) with no build step.11+//! [`ASCII_ART`], [`LOGO_ANSI_RAW`], [`LOGO_SVG`], [`FAVICON_SVG`] and12+//! [`WORDMARK_SVG`] re-expose those static files for later issues (navbar13+//! #16, favicon #17, wiki README #19, agents page) with no build step.1314 //!1415 //! [`ascii_pre`] renders the banner as an escaped `<pre>` block (see15-//! [`crate::layout::esc`]): the art itself is plain `|/-_` plus spaces so16+//! [`crate::layout::esc`]): the art itself is block glyphs plus spaces so1617 //! escaping is a no-op today, but the wrapper keeps the invariant that any1718 //! future art containing `<>&"` stays inert inside HTML.19+//!20+//! `static/logo-ansi.txt` stores the owner's 12-line ANSI art with literal21+//! `[...m` codes and NO raw ESC bytes (repo stays ESC-free); terminal use22+//! goes through [`logo_ansi_for_tty`], which prefixes each SGR code with23+//! ESC at runtime so a tty renders colors while pipes stay plain.1824 1925 use crate::layout::esc;2026 2127 // TODO(m6+): stubs only — navbar (#16), favicon (#17), wiki README (#19)2228 // and the agents page will call these; allow until wired.2329 #[allow(dead_code)]24-/// ASCII banner, byte-for-byte `static/ascii.txt` (5 lines, <=60 cols).30+/// ASCII banner, byte-for-byte `static/ascii.txt` (owner block-art,31+/// 12 lines, 42 cols, block glyphs + spaces).2532 pub const ASCII_ART: &str = include_str!("../static/ascii.txt");2633 34+#[allow(dead_code)]35+/// Owner ANSI block-art, byte-for-byte `static/logo-ansi.txt` (12 lines).36+///37+/// Stored with literal `[...m` SGR codes and NO raw ESC bytes (see raw38+/// file); use [`logo_ansi_for_tty`] for terminal output and39+/// [`strip_ansi_sgr`] for the plain render.40+pub const LOGO_ANSI_RAW: &str = include_str!("../static/logo-ansi.txt");41+2742 #[allow(dead_code)]2843 /// Header lockup, byte-for-byte `static/logo.svg` (mark + hu13 as <path>).2944 pub const LOGO_SVG: &str = include_str!("../static/logo.svg");@@ -50,6 +65,80 @@ pub fn ascii_pre() -> String {5065 )5166 }5267 68+/// Strip SGR color codes from owner ANSI art.69+///70+/// Removes literal `[...m` sequences (`[` + digits/`;` + `m`) plus the same71+/// sequences with a leading ESC byte (`\x1b[...m`, as produced by72+/// [`interpret_ansi_sgr`]), leaving the plain block glyphs. Equivalent to73+/// `sed 's/\[[0-9;]*m//g'` on the ESC-free stored file.74+#[allow(dead_code)]75+#[must_use]76+pub fn strip_ansi_sgr(s: &str) -> String {77+ let bytes = s.as_bytes();78+ let mut out = String::with_capacity(s.len());79+ let mut i = 0;80+ while i < bytes.len() {81+ let mut j = i;82+ // Optional single ESC prefix (interpreted form).83+ if bytes[j] == 0x1b && j + 1 < bytes.len() && bytes[j + 1] == b'[' {84+ j += 1;85+ }86+ if bytes[j] == b'[' {87+ let mut k = j + 1;88+ while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {89+ k += 1;90+ }91+ if k < bytes.len() && bytes[k] == b'm' && k > j + 1 {92+ i = k + 1;93+ continue;94+ }95+ }96+ let ch = s[i..].chars().next().expect("char boundary");97+ out.push(ch);98+ i += ch.len_utf8();99+ }100+ out101+}102+103+/// Prefix each literal SGR code (`[...m`) with ESC for tty output.104+///105+/// The stored file is ESC-free so the repo never holds raw ESC bytes;106+/// this converts at runtime so a terminal renders magenta/cyan/grey.107+/// A `[` not followed by digits/`;` + `m` is left alone.108+#[allow(dead_code)]109+#[must_use]110+pub fn interpret_ansi_sgr(s: &str) -> String {111+ let bytes = s.as_bytes();112+ let mut out = String::with_capacity(s.len() + 64);113+ let mut i = 0;114+ while i < bytes.len() {115+ if bytes[i] == b'[' {116+ let mut k = i + 1;117+ while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {118+ k += 1;119+ }120+ if k < bytes.len() && bytes[k] == b'm' && k > i + 1 {121+ out.push('\u{1b}');122+ let ch = s[i..].chars().next().expect("char boundary");123+ out.push(ch);124+ i += ch.len_utf8();125+ continue;126+ }127+ }128+ let ch = s[i..].chars().next().expect("char boundary");129+ out.push(ch);130+ i += ch.len_utf8();131+ }132+ out133+}134+135+/// Owner ANSI art interpreted for a tty (ESC-prefixed SGR codes).136+#[allow(dead_code)]137+#[must_use]138+pub fn logo_ansi_for_tty() -> String {139+ interpret_ansi_sgr(LOGO_ANSI_RAW)140+}141+53142 #[cfg(test)]54143 mod tests {55144 use super::*;@@ -80,6 +169,29 @@ mod tests {80169 assert!(!pre.contains("<script"), "no script smuggling");81170 }82171 172+ #[test]173+ fn ansi_raw_is_esc_free_with_codes() {174+ // Owner file (#26): literal `[...m` codes, block glyphs, no ESC.175+ assert!(!LOGO_ANSI_RAW.contains('\u{1b}'), "no raw ESC bytes stored");176+ assert!(LOGO_ANSI_RAW.contains("[0;"), "holds SGR codes");177+ assert!(LOGO_ANSI_RAW.contains('█'), "holds block glyphs");178+ assert_eq!(LOGO_ANSI_RAW.lines().count(), 12, "12 lines");179+ }180+181+ #[test]182+ fn strip_yields_ascii() {183+ assert_eq!(strip_ansi_sgr(LOGO_ANSI_RAW), ASCII_ART, "strip == ascii");184+ assert_eq!(strip_ansi_sgr(ASCII_ART), ASCII_ART, "plain is stable");185+ }186+187+ #[test]188+ fn tty_interpretation_holds_esc_and_strips_back() {189+ let tty = logo_ansi_for_tty();190+ assert!(tty.contains('\u{1b}'), "tty form carries ESC");191+ assert!(tty.contains('█'), "glyphs kept");192+ assert_eq!(strip_ansi_sgr(&tty), ASCII_ART, "tty strips to ascii");193+ }194+83195 #[test]84196 fn svg_consts_match_tiny_marks() {85197 for (name, svg) in [
src/main.rs+6 −0--- a/src/main.rs+++ b/src/main.rs@@ -464,6 +464,12 @@ fn real_main() -> i32 {464464 match rest[0].as_str() {465465 "--version" | "-V" | "version" => {466466 println!("hu13 {VERSION}");467+ // Owner logo (#26): on a tty also show the ANSI block-art468+ // interpreted (ESC-prefixed so colors render); pipes stay a469+ // single plain version line. Zero-dep via IsTerminal.470+ if std::io::IsTerminal::is_terminal(&std::io::stdout()) {471+ print!("{}", crate::logo::logo_ansi_for_tty());472+ }467473 0468474 }469475 "--help" | "-h" | "help" => {
static/ascii.txt+12 −5--- a/static/ascii.txt+++ b/static/ascii.txt@@ -1,5 +1,12 @@1-| /| ___2-|___ | | |3-| | | | | |---4-| | | | | | |5-| | |___| __|_ |___|1+ ███ ███ ███ ███ █████ ▄███▄ 2+ ██▓ ░ ▓██ ██▓ ░ ▓██ ▄▄ ▒▓█ ▄▄▀ █▓░ ░▓█ ▀3+ ▓▓░ ▒ ░▓▓ ▓▓░ ▒ ░▓▓ ▒▒ ░▒▓ ▒▒ ▓▓░▌▄▐░▓▓ 4+ ▓▒░ ▀ ░▒▓ ▓▒░ ▀ ░▒▓ ▀▀ ░░▒ ▀▀ ▓▒░ ▀ ░▒▓ 5+ ░░░ ▀ ░░░ ░░▒ ░ ░░░ ░░ ░░░ ░░▄▄▄▄ ▀ ░░░ 6+ ░░▒▀▀▀▒░░ ░▒▓ ▒ ▒░░ ▒▒ ▒░░ ▒▒▓▓▓▓ ░░░░▒ 7+ ░▒▒ ▓ ▒▒░ ▒▓▓ ▓ ▓▒░ ▓▓ ▒▒░ ▓▓▀▀▀▀ ▄ ▒▒▓ 8+ ▒▒▓ ▒ ▓▒▒ ▓▓█ █ ▓▓▒ ██ ▓▒▒ ██ ▓▓▓ ▓ ██▓ 9+ ▓▓█ ▄ █▓▓ ▓█▓ ▄ ▓█▓ ▄▄ █▓▓ ▄▄ █▓▓ ▄ ▓██ 10+ ▓██ ▒ ██▓ ███ ▒ ▓██ ▒▒ ██▓ ▒▒ ██▓ ▀ ███ 11+ ███ ▀ ███ ███▄ ▄███ ▀▀ ███ ▀▀▄ ███ ███ ▄12+ ███ ███ ▀███████▀ ███████ ▀███▀
static/logo-ansi.txt+12 −0--- /dev/null+++ b/static/logo-ansi.txt@@ -0,0 +1,12 @@1+[0;37;40m [0;95;40m███[0;37;40m [0;95;40m███[0;37;40m [0;95;40m███[0;37;40m [0;95;40m███[0;37;40m [0;95;40m█[0;95;45m██[0;95;40m██[0;37;40m [0;95;40m▄███▄[0;37;40m [0m2+[0;37;40m [0;95;40m██[0;95;45m▓[0;37;40m [0;36;40m░[0;37;40m [0;95;45m▓[0;95;40m██[0;37;40m [0;95;40m██[0;95;45m▓[0;37;40m [0;36;40m░[0;37;40m [0;95;45m▓[0;95;40m██[0;37;40m [0;90;40m▄▄[0;37;40m [0;95;45m▒▓█[0;37;40m [0;90;40m▄▄▀[0;37;40m [0;95;40m█[0;95;45m▓░[0;37;40m [0;95;45m░▓[0;95;40m█[0;37;40m [0;90;40m▀[0m3+[0;37;40m [0;95;45m▓▓░[0;37;40m [0;36;40m▒[0;37;40m [0;95;45m░▓▓[0;37;40m [0;95;45m▓▓░[0;37;40m [0;36;40m▒[0;37;40m [0;95;45m░▓▓[0;37;40m [0;36;40m▒▒[0;37;40m [0;95;45m░▒▓[0;37;40m [0;36;40m▒▒[0;37;40m [0;95;45m▓▓░[0;35;40m▌[0;90;40m▄[0;35;40m▐[0;95;45m░▓▓[0;37;40m [0m4+[0;37;40m [0;95;45m▓▒[0;96;46m░[0;37;40m [0;90;40m▀[0;37;40m [0;96;46m░[0;95;45m▒▓[0;37;40m [0;95;45m▓▒[0;96;46m░[0;37;40m [0;90;40m▀[0;37;40m [0;96;46m░[0;95;45m▒▓[0;37;40m [0;90;40m▀▀[0;37;40m [0;96;46m░[0;95;45m░▒[0;37;40m [0;90;40m▀▀[0;37;40m [0;95;45m▓▒░[0;37;40m [0;90;40m▀[0;37;40m [0;96;46m░[0;95;45m▒▓[0;37;40m [0m5+[0;37;40m [0;95;45m░[0;96;46m░░[0;37;40m [0;34;40m▀[0;37;40m [0;96;46m░░[0;95;45m░[0;37;40m [0;95;45m░[0;96;46m░▒[0;37;40m [0;34;40m░[0;37;40m [0;96;46m░░[0;95;45m░[0;37;40m [0;34;40m░░[0;37;40m [0;96;46m░░[0;95;45m░[0;37;40m [0;34;40m░░▄▄▄▄[0;37;40m [0;34;40m▀[0;37;40m [0;96;46m░░[0;95;45m░[0;37;40m [0m6+[0;37;40m [0;96;46m░░▒[0;96;40m▀▀▀[0;96;46m▒░░[0;37;40m [0;96;46m░▒▓[0;37;40m [0;34;40m▒[0;37;40m [0;96;46m▒░░[0;37;40m [0;34;40m▒▒[0;37;40m [0;96;46m▒░░[0;37;40m [0;34;40m▒▒▓▓▓▓[0;37;40m [0;96;46m░░░░▒[0;37;40m [0m7+[0;37;40m [0;96;46m░▒▒[0;37;40m [0;34;40m▓[0;37;40m [0;96;46m▒▒░[0;37;40m [0;96;46m▒▓▓[0;37;40m [0;34;40m▓[0;37;40m [0;96;46m▓▒░[0;37;40m [0;34;40m▓▓[0;37;40m [0;96;46m▒▒░[0;37;40m [0;34;40m▓▓▀▀▀▀[0;37;40m [0;34;40m▄[0;37;40m [0;96;46m▒▒▓[0;37;40m [0m8+[0;37;40m [0;96;46m▒▒▓[0;37;40m [0;34;40m▒[0;37;40m [0;96;46m▓▒▒[0;37;40m [0;96;40m▓[0;96;46m▓[0;96;40m█[0;37;40m [0;34;40m█[0;37;40m [0;96;46m▓▓▒[0;37;40m [0;34;40m██[0;37;40m [0;96;46m▓▒▒[0;37;40m [0;34;40m██[0;37;40m [0;96;46m▓▓▓[0;37;40m [0;34;40m▓[0;37;40m [0;96;40m██▓[0;37;40m [0m9+[0;37;40m [0;96;46m▓▓█[0;37;40m [0;90;40m▄[0;37;40m [0;96;46m█▓▓[0;37;40m [0;96;46m▓█▓[0;37;40m [0;90;40m▄[0;37;40m [0;96;46m▓█▓[0;37;40m [0;90;40m▄▄[0;37;40m [0;96;46m█▓▓[0;37;40m [0;90;40m▄▄[0;37;40m [0;96;40m█[0;96;46m▓▓[0;37;40m [0;90;40m▄[0;37;40m [0;96;46m▓██[0;37;40m [0m10+[0;37;40m [0;96;46m▓██[0;37;40m [0;36;40m▒[0;37;40m [0;96;46m██▓[0;37;40m [0;96;46m███[0;37;40m [0;36;40m▒[0;37;40m [0;96;46m▓[0;96;40m██[0;37;40m [0;36;40m▒▒[0;37;40m [0;96;46m██▓[0;37;40m [0;36;40m▒▒[0;37;40m [0;96;40m██[0;96;46m▓[0;37;40m [0;90;40m▀[0;37;40m [0;96;46m███[0;37;40m [0m11+[0;37;40m [0;96;40m███[0;37;40m [0;90;40m▀[0;37;40m [0;96;40m███[0;37;40m [0;96;40m█[0;96;46m██[0;96;40m▄[0;37;40m [0;96;40m▄[0;96;46m█[0;96;40m██[0;37;40m [0;90;40m▀▀[0;37;40m [0;96;46m███[0;37;40m [0;90;40m▀▀▄[0;37;40m [0;96;40m█[0;96;46m██[0;37;40m [0;96;46m███[0;37;40m [0;90;40m▄[0m12+[0;37;40m [0;96;46m███[0;37;40m [0;96;46m███[0;37;40m [0;96;40m▀██[0;96;46m███[0;96;40m██▀[0;37;40m [0;96;40m███████[0;37;40m [0;96;40m▀[0;96;46m███[0;96;40m▀[0;37;40m [0m
tests/logo.rs+90 −21--- a/tests/logo.rs+++ b/tests/logo.rs@@ -1,7 +1,9 @@1-//! Logo suite (m6, f0rge issues #15 + #27 + #28):1+//! Logo suite (m6, f0rge issues #15 + #26 + #27 + #28):22 //! `static/logo.svg` (lockup with hu13 as <path>) + `static/favicon.svg`33 //! (simplified mark-only 16px variant) exist + valid XML + <2KB total,4-//! wordmark variant, ASCII banner limits, `<pre>` rendering invariant.4+//! wordmark variant, owner block-art banner (`static/ascii.txt` +5+//! `static/logo-ansi.txt`, 12 lines) + purity/strip tests + `<pre>`6+//! rendering invariant.57 //!68 //! File-level suite (hu13 is a binary crate, so `src/logo.rs` unit tests79 //! cover the `ascii_pre()` helper itself; here we assert on disk).@@ -209,34 +211,47 @@ fn wordmark_variant_exists_tiny() {209211 210212 #[test]211213 fn ascii_banner_limits_and_aesthetic() {214+ // Owner block-art (#26): 12-line block glyphs reading "hu13".215+ // Charset is space + block set (█ ▓ ▒ ░ ▄ ▀ ▌ ▐) plus the legacy216+ // line-art chars (| - / _ +) for forward compat — still no letters,217+ // no `<>&"` so `<pre>` stays verbatim via esc().212218 let art = read_static("ascii.txt");213219 assert!(!art.trim().is_empty(), "banner is non-empty");214220 let lines: Vec<&str> = art.lines().collect();215- assert!(216- lines.len() <= 12,217- "banner is {} lines, cap is 12",218- lines.len()219- );220- assert!(lines.len() >= 2, "banner is a banner, not one line");221- let mut saw_pipe = false;222- let mut saw_hyphen = false;223- let mut saw_slash = false;221+ assert_eq!(lines.len(), 12, "owner banner is 12 lines");222+ let mut saw_block = false;224223 for line in &lines {225224 assert!(line.chars().count() <= 60, "banner line too wide: {line:?}");226225 for ch in line.chars() {227- saw_pipe |= ch == '|';228- saw_hyphen |= ch == '-';229- saw_slash |= ch == '/';226+ assert!(!ch.is_ascii_alphabetic(), "no letters, got {ch:?}");230227 assert!(231- matches!(ch, '|' | '-' | '/' | '_' | '+' | ' '),232- "sharp-corner aesthetic only (| - / _ + space), got {ch:?}"228+ !matches!(ch, '<' | '>' | '&' | '"'),229+ "nothing to escape, got {ch:?}"230+ );231+ saw_block |= matches!(ch, '█' | '▓' | '▒' | '░' | '▄' | '▀' | '▌' | '▐');232+ assert!(233+ matches!(234+ ch,235+ ' ' | '█'236+ | '▓'237+ | '▒'238+ | '░'239+ | '▄'240+ | '▀'241+ | '▌'242+ | '▐'243+ | '|'244+ | '-'245+ | '/'246+ | '_'247+ | '+'248+ ),249+ "block set + legacy (| - / _ + space), got {ch:?}"233250 );234251 }235252 }236- assert!(237- saw_pipe && saw_hyphen && saw_slash,238- "uses pipes+hyphens+slashes"239- );253+ assert!(saw_block, "uses block glyphs");254+ assert!(art.contains('█'), "block fill present");240255 }241256 242257 #[test]@@ -250,3 +265,58 @@ fn ascii_renders_inside_pre_unescaped_except_esc() {250265 assert!(pre.contains("<pre>"), "inside <pre>");251266 assert!(pre.contains(&art), "verbatim, unescaped-except-esc()");252267 }268+269+/// Strip literal SGR codes (`[...m`, no ESC byte) the way the rubric270+/// prescribes: `sed 's/\[[0-9;]*m//g'`.271+fn strip_literal_sgr(s: &str) -> String {272+ let bytes = s.as_bytes();273+ let mut out = String::with_capacity(s.len());274+ let mut i = 0;275+ while i < bytes.len() {276+ if bytes[i] == b'[' {277+ let mut k = i + 1;278+ while k < bytes.len() && (bytes[k].is_ascii_digit() || bytes[k] == b';') {279+ k += 1;280+ }281+ if k < bytes.len() && bytes[k] == b'm' && k > i + 1 {282+ i = k + 1;283+ continue;284+ }285+ }286+ let ch = s[i..].chars().next().expect("char boundary");287+ out.push(ch);288+ i += ch.len_utf8();289+ }290+ out291+}292+293+#[test]294+fn ansi_strips_to_ascii_byte_for_byte() {295+ // Owner ANSI original (#26) strips to the plain banner byte-for-byte.296+ let ansi = read_static("logo-ansi.txt");297+ let ascii = read_static("ascii.txt");298+ assert_eq!(strip_literal_sgr(&ansi), ascii, "strip(ansi) == ascii");299+ assert_eq!(ansi.lines().count(), 12, "ansi is 12 lines");300+ assert_eq!(ascii.lines().count(), 12, "ascii is 12 lines");301+}302+303+#[test]304+fn logo_ansi_is_esc_free_with_codes() {305+ // Stored with literal `[...m` codes, block glyphs, NO raw ESC bytes.306+ let ansi = read_static("logo-ansi.txt");307+ assert!(!ansi.contains('\u{1b}'), "no raw ESC bytes stored");308+ assert!(ansi.contains("[0;"), "holds SGR codes");309+ assert!(ansi.contains("[0m"), "holds reset");310+ assert!(ansi.contains('█'), "holds block glyphs");311+}312+313+#[test]314+fn static_holds_no_esc_bytes() {315+ // Success rubric (#26): `grep -r $'\\x1b' static/` is empty.316+ let dir = manifest().join("static");317+ for name in ["logo-ansi.txt", "ascii.txt", "logo.svg", "favicon.svg"] {318+ let bytes =319+ std::fs::read(dir.join(name)).unwrap_or_else(|_| panic!("static/{name} exists"));320+ assert!(!bytes.contains(&0x1b), "static/{name} holds no ESC bytes");321+ }322+}253--
Comments (0)
No comments yet.
Reviews (0)
No reviews yet.