#33 m6/owner-logo

mergedpassopened
details
branches
master ← m6/owner-logo
updated
id
5519019f-99b8-4923-b089-234385a7c909

Patch

src/logo.rs+119 −7
diff --git a/src/logo.rs b/src/logo.rsindex f580b2b..f71358c 100644--- 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
diff --git a/src/main.rs b/src/main.rsindex bcacf00..cffc87e 100644--- 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
diff --git a/static/ascii.txt b/static/ascii.txtindex 1e39422..020ad10 100644--- 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
diff --git a/static/logo-ansi.txt b/static/logo-ansi.txtnew file mode 100644index 0000000..992e446--- /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
diff --git a/tests/logo.rs b/tests/logo.rsindex 08884b5..21c3518 100644--- 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-- 2.55.0

Comments (0)

No comments yet.

Reviews (0)

No reviews yet.

all pulls