From 4baae6bf2afb2c07b617100371c07d8b6dea71a5 Mon Sep 17 00:00:00 2001 From: Omarchybot Date: Mon, 24 Aug 2026 17:58:52 +0200 Subject: [PATCH] Draw text as ASCII art in the Omarchy logo font (#8037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add omarchy-ascii for drawing text in the logo font Renders text as ASCII art in Delta Corps Priest 1, the FIGlet font the Omarchy wordmark itself is drawn in, so branding art can be words rather than a picture. The font is embedded in the script and the layout is done in awk, so the command adds nothing to the default package set. The layout runs on one-byte stand-ins for the five block characters the font draws with. Column arithmetic over the characters themselves counts bytes in one locale and characters in another, and the stand-ins keep length() and substr() counting columns either way. Delta Corps Priest 1 carries letters and spaces only, and every mirror of it ships the same file with the digit and punctuation glyphs empty. Anything else is dropped and named on stderr, and text with nothing drawable at all exits 1 rather than printing silence. 🤖 Generated by Opus 5 in Claude Code. * Correct what the renderer did with input it could not draw The route never ran on piped text. The metadata declared `` as required, so `omarchy ascii` with nothing on the command line resolved to the router's help while `omarchy-ascii` run directly worked, which is why the tests missed it: they all called the binary. The argument is optional now, and a test goes through the route. Text reached awk as a command-line variable, where awk reads backslash escapes of its own, so `omarchy ascii 'A\nB'` drew two blocks instead of naming the backslash as a character the font lacks. A text longer than the argument list could not be passed at all. It arrives as awk's input now, with the font on a descriptor of its own. A line with nothing drawable printed nothing at all, so a blank line between two words closed the gap up rather than keeping it. Every line draws its block now, blank ones included, which is what figlet does with a newline. Placing a glyph scanned and copied the whole width of the art so far, costing the square of the line's length: four thousand characters took forty-six seconds. A row is now held without its trailing blanks, counted separately instead, so a glyph costs its own width and those four thousand characters take a tenth of a second. A skipped control character was named on stderr by writing it out, which sends it to the terminal as a control character; those are named by code now. An unknown option was drawn as art rather than refused, so a mistyped `--width 40` quietly rendered the word "width". figlet.c trims the column of blanks that every row of an `M` shares when it is the first glyph on a line, and figlet.js keeps it. asciiart.eu runs figlet.js, so the rendering follows figlet.js and a test pins that `M`, because the wordmark alone does not catch the difference and the next reader would have no way to tell the choice from an oversight. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Codex XHigh --------- Co-authored-by: David Heinemeier Hansson Co-authored-by: Codex XHigh --- bin/omarchy | 1 + bin/omarchy-ascii | 1217 ++++++++++++++++++++++++++++++++++++ manual/41-branding.md | 10 + test/shell.d/ascii-test.sh | 147 +++++ 4 files changed, 1375 insertions(+) create mode 100755 bin/omarchy-ascii create mode 100755 test/shell.d/ascii-test.sh diff --git a/bin/omarchy b/bin/omarchy index 994172ef..111d214a 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -27,6 +27,7 @@ declare -A BINARY_TO_KEY declare -A GROUP_DESCRIPTIONS GROUP_DESCRIPTIONS[agent]="AI coding agent usage data" +GROUP_DESCRIPTIONS[ascii]="Text drawn as ASCII art" GROUP_DESCRIPTIONS[audio]="Audio input and output controls" GROUP_DESCRIPTIONS[bar]="Omarchy shell bar layout and settings" GROUP_DESCRIPTIONS[battery]="Battery status helpers" diff --git a/bin/omarchy-ascii b/bin/omarchy-ascii new file mode 100755 index 00000000..166ae92b --- /dev/null +++ b/bin/omarchy-ascii @@ -0,0 +1,1217 @@ +#!/bin/bash + +# omarchy:summary=Render text as ASCII art in the font the Omarchy logo is drawn in +# omarchy:args=[text...] +# omarchy:examples=omarchy ascii Omarchy | omarchy ascii "Hello there" + +set -o pipefail + +usage() { + cat <<'EOF' +Usage: omarchy-ascii [text...] + +Renders text as ASCII art in Delta Corps Priest 1, the FIGlet font the Omarchy +logo is drawn in. Reads the text from stdin when given none. + +Options: + --help Show this help + +Delta Corps Priest 1 draws letters and spaces only. Digits and punctuation have +no glyph in it, so they are skipped. +EOF +} + +words=() + +while (( $# > 0 )); do + case "$1" in + --help) + usage + exit 0 + ;; + --) + shift + words+=("$@") + break + ;; + -?*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + *) + words+=("$1") + ;; + esac + shift +done + +if (( ${#words[@]} > 0 )); then + text="${words[*]}" +elif [[ ! -t 0 ]]; then + text=$(cat) +else + usage >&2 + exit 1 +fi + +if [[ -z ${text//[[:space:]]/} ]]; then + echo "Nothing to render" >&2 + exit 1 +fi + +# The text arrives as awk's input, one record per line, and the font on a +# descriptor of its own. Handing the text to awk as a variable instead would +# read backslash escapes in it as awk's own, and would put a long text through +# the argument list, which has a size limit that a piped one does not. +awk ' +function rtrim(s) { + sub(/ +$/, "", s) + return s +} + +function ltrim(s) { + sub(/^ +/, "", s) + return s +} + +# A single-character replacement that never treats its needle as a pattern, +# because the hardblank is "$" in this font and would anchor a regex instead. +function replace(s, from, to, at, result) { + while ((at = index(s, from)) > 0) { + result = result substr(s, 1, at - 1) to + s = substr(s, at + 1) + } + + return result s +} + +function load_font( line, header, position, code, i) { + while ((getline line < FONT) > 0) { + font_line++ + + if (font_line == 1) { + split(line, header, " ") + hardblank = substr(header[1], length(header[1])) + height = header[2] + comment_lines = header[6] + continue + } + + if (font_line <= comment_lines + 1) { + continue + } + + # Glyphs follow the comments in character order from 32, `height` lines + # each, every line closed by the endmark the first one introduces. + position = font_line - comment_lines - 2 + code = 32 + int(position / height) + if (code > 126) { + continue + } + + if (endmark == "") { + endmark = substr(line, length(line)) + } + + while (length(line) > 0 && substr(line, length(line)) == endmark) { + line = substr(line, 1, length(line) - 1) + } + + for (i = 1; i <= blocks; i++) { + gsub(block[i], stand_in[i], line) + } + + # The hardblank becomes a marker here, while the string is one glyph wide, + # so printing a finished row is a single pass rather than one rescan per + # hardblank in it. + line = replace(line, hardblank, hardblank_mark) + + glyph[code, position % height] = line + if (length(line) > width[code]) { + width[code] = length(line) + } + } + + close(FONT) +} + +function drawable(source, chars, total, i, code) { + total = split(source, chars, "") + for (i = 1; i <= total; i++) { + code = code_of[chars[i]] + if (code != "" && width[code] > 0) { + return 1 + } + } + + return 0 +} + +# Lay one glyph beside what is drawn so far, kerned: slid left until the two +# would touch, which is the smallest run of blanks between them across all rows. +function draw(source, chars, total, i, r, ch, code, piece, glyph_width, out, trail, amount, lead, cut, keep, remaining, fragment, body, pad, line, drew) { + total = split(source, chars, "") + drew = 0 + + for (i = 1; i <= total; i++) { + ch = chars[i] + code = code_of[ch] + + if (code == "" || width[code] == 0) { + if (!(ch in skipped)) { + skipped[ch] = 1 + skipped_list[++skipped_total] = (ch in escaped) ? escaped[ch] : ch + } + + continue + } + + glyph_width = width[code] + for (r = 0; r < height; r++) { + piece[r] = sprintf("%-" glyph_width "s", glyph[code, r]) + } + + if (!drew) { + for (r = 0; r < height; r++) { + out[r] = rtrim(piece[r]) + trail[r] = glyph_width - length(out[r]) + } + + drew = 1 + continue + } + + amount = -1 + for (r = 0; r < height; r++) { + lead = glyph_width - length(ltrim(piece[r])) + if (amount < 0 || trail[r] + lead < amount) { + amount = trail[r] + lead + } + } + + # Every row gives up the same number of columns, taken from its own trailing + # blanks first and from the glyph leading ones once those run out. A row is + # held without its trailing blanks, counted in trail[] instead, so placing a + # glyph costs its own width rather than the width of the art so far -- which + # is what keeps a long line from taking time in the square of its length. + for (r = 0; r < height; r++) { + cut = (amount < trail[r]) ? amount : trail[r] + keep = amount - cut + remaining = trail[r] - cut + fragment = substr(piece[r], keep + 1) + body = rtrim(fragment) + + if (body == "") { + trail[r] = remaining + length(fragment) + } else { + pad = (remaining > 0) ? sprintf("%" remaining "s", "") : "" + out[r] = out[r] pad body + trail[r] = length(fragment) - length(body) + } + } + } + + # A line that drew nothing still occupies its block, the way a blank line + # between two words of art does, so the art keeps the shape of the text. + for (r = 0; r < height; r++) { + line = out[r] + for (i = 1; i <= blocks; i++) { + gsub(stand_in[i], block[i], line) + } + + gsub(hardblank_mark, " ", line) + sub(/ +$/, "", line) + print line + } +} + +BEGIN { + FONT = "/dev/fd/3" + + # The font draws with five block characters, and the layout above is column + # arithmetic. Each one becomes a single byte for the duration and turns back + # at print time, so length() and substr() count columns no matter what the + # locale believes a character is. + blocks = split("█ ▀ ▄ ▌ ▐", block, " ") + for (i = 1; i <= blocks; i++) { + stand_in[i] = sprintf("%c", i) + } + + hardblank_mark = sprintf("%c", blocks + 1) + + for (i = 32; i <= 126; i++) { + code_of[sprintf("%c", i)] = i + } + + # Skipped characters are named on stderr, and a control character named there + # would reach the terminal as a control character. Name those by code. + for (i = 1; i < 32; i++) { + escaped[sprintf("%c", i)] = sprintf("\\x%02x", i) + } + + escaped[sprintf("%c", 127)] = "\\x7f" + + load_font() + + if (height == "") { + print "Could not read the embedded font." >"/dev/stderr" + aborted = 1 + exit 1 + } +} + +{ + source[++source_count] = $0 + if (drawable($0)) { + any_drawable = 1 + } +} + +END { + if (aborted) { + exit 1 + } + + if (!any_drawable) { + print "Delta Corps Priest 1 draws letters and spaces only, and that text has neither." >"/dev/stderr" + exit 1 + } + + for (i = 1; i <= source_count; i++) { + draw(source[i]) + } + + if (skipped_total > 0) { + list = skipped_list[1] + for (i = 2; i <= skipped_total; i++) { + list = list " " skipped_list[i] + } + + print "Skipped, no glyph in Delta Corps Priest 1: " list >"/dev/stderr" + } +} +' 3<<'DELTA_CORPS_PRIEST_1' <<<"$text" +flf2a$ 9 8 19 0 3 0 64 0 +Font Author: CoSMiC cHiLD + +FIGFont created by patorjk.com's FIGFont Editor: http://patorjk.com/figfont-editor +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@ +$ $@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ + ▄████████$@ + ███ ███$@ + ███ ███$@ + ███ ███$@ +▀███████████$@ + ███ ███$@ + ███ ███$@ + ███ █▀ $@ + @@ +▀█████████▄ $@ + ███ ███$@ + ███ ███$@ + ▄███▄▄▄██▀ $@ +▀▀███▀▀▀██▄ $@ + ███ ██▄$@ + ███ ███$@ +▄█████████▀ $@ + @@ + ▄████████$@ +███ ███$@ +███ █▀ $@ +███ $@ +███ $@ +███ █▄ $@ +███ ███$@ +████████▀ $@ + @@ +████████▄ $@ +███ ▀███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ▄███$@ +████████▀ $@ + @@ + ▄████████$@ + ███ ███$@ + ███ █▀ $@ + ▄███▄▄▄ $@ +▀▀███▀▀▀ $@ + ███ █▄ $@ + ███ ███$@ + ██████████$@ + @@ + ▄████████$@ + ███ ███$@ + ███ █▀ $@ + ▄███▄▄▄ $@ +▀▀███▀▀▀ $@ + ███ $@ + ███ $@ + ███ $@ + @@ + ▄██████▄ $@ + ███ ███$@ + ███ █▀ $@ + ▄███ $@ +▀▀███ ████▄ $@ + ███ ███$@ + ███ ███$@ + ████████▀ $@ + @@ + ▄█ █▄ $@ + ███ ███ $@ + ███ ███ $@ + ▄███▄▄▄▄███▄▄$@ +▀▀███▀▀▀▀███▀ $@ + ███ ███ $@ + ███ ███ $@ + ███ █▀ $@ + @@ + ▄█ $@ +███ $@ +███▌$@ +███▌$@ +███▌$@ +███ $@ +███ $@ +█▀ $@ + @@ + ▄█$@ + ███$@ + ███$@ + ███$@ + ███$@ + ███$@ + ███$@ +█▄ ▄███$@ +▀▀▀▀▀▀ $@@ + ▄█ ▄█▄$@ + ███ ▄███▀$@ + ███▐██▀ $@ + ▄█████▀ $@ +▀▀█████▄ $@ + ███▐██▄ $@ + ███ ▀███▄$@ + ███ ▀█▀$@ + ▀ $@@ + ▄█ $@ +███ $@ +███ $@ +███ $@ +███ $@ +███ $@ +███▌ ▄$@ +█████▄▄██$@ +▀ $@@ + ▄▄▄▄███▄▄▄▄ $@ + ▄██▀▀▀███▀▀▀██▄$@ + ███ ███ ███$@ + ███ ███ ███$@ + ███ ███ ███$@ + ███ ███ ███$@ + ███ ███ ███$@ + ▀█ ███ █▀ $@ + @@ +███▄▄▄▄ $@ +███▀▀▀██▄$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ + ▀█ █▀ $@ + @@ + ▄██████▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ + ▀██████▀ $@ + @@ + ▄███████▄$@ + ███ ███$@ + ███ ███$@ + ███ ███$@ +▀█████████▀ $@ + ███ $@ + ███ $@ + ▄████▀ $@ + @@ +████████▄ $@ +███ ███ $@ +███ ███ $@ +███ ███ $@ +███ ███ $@ +███ ███ $@ +███ ▀ ███ $@ + ▀██████▀▄█$@ + @@ + ▄████████$@ + ███ ███$@ + ███ ███$@ + ▄███▄▄▄▄██▀$@ +▀▀███▀▀▀▀▀ $@ +▀███████████$@ + ███ ███$@ + ███ ███$@ + ███ ███$@@ + ▄████████$@ + ███ ███$@ + ███ █▀ $@ + ███ $@ +▀███████████$@ + ███$@ + ▄█ ███$@ + ▄████████▀ $@ + @@ + ███ $@ +▀█████████▄$@ + ▀███▀▀██$@ + ███ ▀$@ + ███ $@ + ███ $@ + ███ $@ + ▄████▀ $@ + @@ +███ █▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +████████▀ $@ + @@ + ▄█ █▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ + ▀██████▀ $@ + @@ + ▄█ █▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ▄█▄ ███$@ + ▀███▀███▀ $@ + @@ +▀████ ▐████▀$@ + ███▌ ████▀ $@ + ███ ▐███ $@ + ▀███▄███▀ $@ + ████▀██▄ $@ + ▐███ ▀███ $@ + ▄███ ███▄ $@ +████ ███▄$@ + @@ +▄██ ▄ $@ +███ ██▄$@ +███▄▄▄███$@ +▀▀▀▀▀▀███$@ +▄██ ███$@ +███ ███$@ +███ ███$@ + ▀█████▀ $@ + @@ + ▄███████▄ $@ +██▀ ▄██$@ + ▄███▀$@ + ▀█▀▄███▀▄▄$@ + ▄███▀ ▀$@ +▄███▀ $@ +███▄ ▄█$@ + ▀████████▀$@ + @@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ + ▄████████$@ + ███ ███$@ + ███ ███$@ + ███ ███$@ +▀███████████$@ + ███ ███$@ + ███ ███$@ + ███ █▀ $@ + @@ +▀█████████▄ $@ + ███ ███$@ + ███ ███$@ + ▄███▄▄▄██▀ $@ +▀▀███▀▀▀██▄ $@ + ███ ██▄$@ + ███ ███$@ +▄█████████▀ $@ + @@ + ▄████████$@ +███ ███$@ +███ █▀ $@ +███ $@ +███ $@ +███ █▄ $@ +███ ███$@ +████████▀ $@ + @@ +████████▄ $@ +███ ▀███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ▄███$@ +████████▀ $@ + @@ + ▄████████$@ + ███ ███$@ + ███ █▀ $@ + ▄███▄▄▄ $@ +▀▀███▀▀▀ $@ + ███ █▄ $@ + ███ ███$@ + ██████████$@ + @@ + ▄████████$@ + ███ ███$@ + ███ █▀ $@ + ▄███▄▄▄ $@ +▀▀███▀▀▀ $@ + ███ $@ + ███ $@ + ███ $@ + @@ + ▄██████▄ $@ + ███ ███$@ + ███ █▀ $@ + ▄███ $@ +▀▀███ ████▄ $@ + ███ ███$@ + ███ ███$@ + ████████▀ $@ + @@ + ▄█ █▄ $@ + ███ ███ $@ + ███ ███ $@ + ▄███▄▄▄▄███▄▄$@ +▀▀███▀▀▀▀███▀ $@ + ███ ███ $@ + ███ ███ $@ + ███ █▀ $@ + @@ + ▄█ $@ +███ $@ +███▌$@ +███▌$@ +███▌$@ +███ $@ +███ $@ +█▀ $@ + @@ + ▄█$@ + ███$@ + ███$@ + ███$@ + ███$@ + ███$@ + ███$@ +█▄ ▄███$@ +▀▀▀▀▀▀ $@@ + ▄█ ▄█▄$@ + ███ ▄███▀$@ + ███▐██▀ $@ + ▄█████▀ $@ +▀▀█████▄ $@ + ███▐██▄ $@ + ███ ▀███▄$@ + ███ ▀█▀$@ + ▀ $@@ + ▄█ $@ +███ $@ +███ $@ +███ $@ +███ $@ +███ $@ +███▌ ▄$@ +█████▄▄██$@ +▀ $@@ + ▄▄▄▄███▄▄▄▄ $@ + ▄██▀▀▀███▀▀▀██▄$@ + ███ ███ ███$@ + ███ ███ ███$@ + ███ ███ ███$@ + ███ ███ ███$@ + ███ ███ ███$@ + ▀█ ███ █▀ $@ + @@ +███▄▄▄▄ $@ +███▀▀▀██▄$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ + ▀█ █▀ $@ + @@ + ▄██████▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ + ▀██████▀ $@ + @@ + ▄███████▄$@ + ███ ███$@ + ███ ███$@ + ███ ███$@ +▀█████████▀ $@ + ███ $@ + ███ $@ + ▄████▀ $@ + @@ +████████▄ $@ +███ ███ $@ +███ ███ $@ +███ ███ $@ +███ ███ $@ +███ ███ $@ +███ ▀ ███ $@ + ▀██████▀▄█$@ + @@ + ▄████████$@ + ███ ███$@ + ███ ███$@ + ▄███▄▄▄▄██▀$@ +▀▀███▀▀▀▀▀ $@ +▀███████████$@ + ███ ███$@ + ███ ███$@ + ███ ███$@@ + ▄████████$@ + ███ ███$@ + ███ █▀ $@ + ███ $@ +▀███████████$@ + ███$@ + ▄█ ███$@ + ▄████████▀ $@ + @@ + ███ $@ +▀█████████▄$@ + ▀███▀▀██$@ + ███ ▀$@ + ███ $@ + ███ $@ + ███ $@ + ▄████▀ $@ + @@ +███ █▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +████████▀ $@ + @@ + ▄█ █▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ + ▀██████▀ $@ + @@ + ▄█ █▄ $@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ███$@ +███ ▄█▄ ███$@ + ▀███▀███▀ $@ + @@ +▀████ ▐████▀$@ + ███▌ ████▀ $@ + ███ ▐███ $@ + ▀███▄███▀ $@ + ████▀██▄ $@ + ▐███ ▀███ $@ + ▄███ ███▄ $@ +████ ███▄$@ + @@ +▄██ ▄ $@ +███ ██▄$@ +███▄▄▄███$@ +▀▀▀▀▀▀███$@ +▄██ ███$@ +███ ███$@ +███ ███$@ + ▀█████▀ $@ + @@ + ▄███████▄ $@ +██▀ ▄██$@ + ▄███▀$@ + ▀█▀▄███▀▄▄$@ + ▄███▀ ▀$@ +▄███▀ $@ +███▄ ▄█$@ + ▀████████▀$@ + @@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +@ +@ +@ +@ +@ +@ +@ +@ +@@ +DELTA_CORPS_PRIEST_1 diff --git a/manual/41-branding.md b/manual/41-branding.md index 2cf19001..cdbe828d 100644 --- a/manual/41-branding.md +++ b/manual/41-branding.md @@ -43,3 +43,13 @@ omarchy transcode ascii ~/logo.svg ~/.config/omarchy/branding/screensaver.txt -- ``` It takes `--width` and `--height` in terminal columns and rows, a `--mode` of either `braille` (the default, and much finer) or `block`, a `--threshold` percentage for deciding which pixels count as part of the logo, and `--invert` for when your logo is light on a dark background. If a conversion comes out as a blob, the threshold is usually the knob to turn. + +### Words instead of a logo + +`omarchy ascii` draws text in Delta Corps Priest 1, the FIGlet font the Omarchy wordmark itself is drawn in, so a screensaver can say something rather than show a picture: + +``` +omarchy ascii "Back in five" > ~/.config/omarchy/branding/screensaver.txt +``` + +It takes the text as arguments, or reads it from a pipe when given none. The font carries letters and spaces only — it was drawn without digits or punctuation — so anything else is dropped and named on stderr rather than quietly swallowed. diff --git a/test/shell.d/ascii-test.sh b/test/shell.d/ascii-test.sh new file mode 100755 index 00000000..e30e3ea0 --- /dev/null +++ b/test/shell.d/ascii-test.sh @@ -0,0 +1,147 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +export PATH="$ROOT/bin:$PATH" + +columns() { + awk 'NR == 1 { print length($0) }' +} + +# The wordmark FIGlet itself draws for this font, so a change to the embedded +# font or to the kerning shows up here rather than in someone's terminal. +expected=$( + cat <<'WORDMARK' + ▄██████▄ ▄▄▄▄███▄▄▄▄ ▄████████ ▄████████ ▄████████ ▄█ █▄ ▄██ ▄ +███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███ ███ ███ ███ ██▄ +███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███▄▄▄███ +███ ███ ███ ███ ███ ███ ███ ▄███▄▄▄▄██▀ ███ ▄███▄▄▄▄███▄▄ ▀▀▀▀▀▀███ +███ ███ ███ ███ ███ ▀███████████ ▀▀███▀▀▀▀▀ ███ ▀▀███▀▀▀▀███▀ ▄██ ███ +███ ███ ███ ███ ███ ███ ███ ▀███████████ ███ █▄ ███ ███ ███ ███ +███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ + ▀██████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ████████▀ ███ █▀ ▀█████▀ + ███ ███ +WORDMARK +) + +output=$(omarchy-ascii Omarchy) +[[ $output == "$expected" ]] || fail "the wordmark matches the reference rendering" "expected: +$expected +actual: +$output" +pass "the wordmark matches the reference rendering" + +# figlet.js and figlet.c disagree about a first M: the C one trims the column of +# blanks every row of that glyph shares, the JavaScript one keeps it. asciiart.eu +# runs figlet.js, so this pins the leading blanks rather than leaving the next +# reader to "fix" them. +expected_m=$( + cat <<'LETTER_M' + ▄▄▄▄███▄▄▄▄ + ▄██▀▀▀███▀▀▀██▄ + ███ ███ ███ + ███ ███ ███ + ███ ███ ███ + ███ ███ ███ + ███ ███ ███ + ▀█ ███ █▀ + +LETTER_M +) + +output=$(omarchy-ascii M) +[[ $output == "$expected_m" ]] || fail "a leading M keeps the blanks figlet.js gives it" "expected: +$expected_m +actual: +$output" +pass "a leading M keeps the blanks figlet.js gives it" + +output=$(printf 'Omarchy' | omarchy-ascii) +[[ $output == "$expected" ]] || fail "text can arrive on stdin" +pass "text can arrive on stdin" + +# The route is the way a user reaches this, and it dispatches on whether the +# metadata says an argument is required -- so a piped run has to be exercised +# through `omarchy` itself, not just through the binary. +output=$(printf 'Omarchy' | omarchy ascii) +[[ $output == "$expected" ]] || fail "piped text renders through the omarchy route" "got: +$output" +pass "piped text renders through the omarchy route" + +# The block characters are three bytes each, so the column arithmetic has to +# count columns rather than bytes wherever the locale lands. +output=$(LC_ALL=C omarchy-ascii Omarchy) +[[ $output == "$expected" ]] || fail "a byte-only locale draws the same wordmark" +pass "a byte-only locale draws the same wordmark" + +blanks=$(omarchy-ascii Omarchy | grep -c ' $' || true) +[[ $blanks == "0" ]] || fail "no line is padded with trailing blanks" "$blanks lines end in a blank" +pass "no line is padded with trailing blanks" + +# The space is a glyph of five hardblank columns, so it is worth exactly five +# columns of art -- not merely more than none. +tight=$(omarchy-ascii "Hi" | columns) +spaced=$(omarchy-ascii "H i" | columns) +(( tight == 18 )) || fail "Hi is 18 columns wide" "got $tight" +(( spaced == tight + 5 )) || fail "a space between words is five columns" "expected $((tight + 5)), got $spaced" +pass "a space between words is five columns" + +rows=$(omarchy-ascii Hi | wc -l) +(( rows == 9 )) || fail "a block is nine rows" "got $rows" +pass "a block is nine rows" + +# An empty line is a block of its own in figlet, and dropping it would silently +# close up the gap someone put there on purpose. +rows=$(printf 'A\n\nB\n' | omarchy-ascii | wc -l) +(( rows == 27 )) || fail "an empty line still draws its block" "expected 27 rows, got $rows" +pass "an empty line still draws its block" + +# awk reads escapes in a variable it is given on the command line, so text has +# to reach it as input instead: a backslash is a character the font lacks, not +# the start of an escape. +rows=$(omarchy-ascii 'A\nB' 2>/dev/null | wc -l) +(( rows == 9 )) || fail "a backslash in the text is not an escape" "expected 9 rows, got $rows" +pass "a backslash in the text is not an escape" + +# Delta Corps Priest 1 carries letters and spaces only. Dropping the rest in +# silence would leave a version number looking like a bug in the renderer. +status=0 +warning=$(omarchy-ascii "Omarchy 4.0" 2>&1 >/dev/null) || status=$? +(( status == 0 )) || fail "text with unusable characters still draws" "exited $status" +[[ $warning == *"Skipped"* && $warning == *"4"* && $warning == *"."* && $warning == *"0"* ]] || + fail "skipped characters are named on stderr" "got: $warning" +pass "skipped characters are named on stderr" + +output=$(omarchy-ascii "Omarchy 4.0" 2>/dev/null) +[[ $output == "$expected" ]] || fail "the drawable characters still render" +pass "the drawable characters still render" + +# Naming a skipped control character by writing it out would send it to the +# terminal as a control character. +warning=$(printf 'A\001B\n' | omarchy-ascii 2>&1 >/dev/null) +[[ $warning == *'\x01'* ]] || fail "a skipped control character is named by code" "got: $warning" +[[ $warning != *$'\001'* ]] || fail "a skipped control character is not written out raw" +pass "a skipped control character is named by code" + +status=0 +output=$(omarchy-ascii "4.0" 2>/dev/null) || status=$? +(( status == 1 )) || fail "text the font cannot draw at all fails" "exited $status" +[[ -z $output ]] || fail "text the font cannot draw at all prints nothing" +pass "text the font cannot draw at all fails" + +# A mistyped option would otherwise be drawn as art, silently. +status=0 +output=$(omarchy-ascii --width 40 2>&1 >/dev/null) || status=$? +(( status == 1 )) || fail "an unknown option is refused" "exited $status" +[[ $output == *"Unknown option: --width"* ]] || fail "an unknown option is named" "got: $output" +pass "an unknown option is refused" + +output=$(omarchy-ascii -- Hi | columns) +(( output == 18 )) || fail "text after -- is still text" "got $output columns" +pass "text after -- is still text" + +output=$(omarchy-ascii --help) +[[ $output == *"Usage: omarchy-ascii"* ]] || fail "help renders" +pass "help renders"