Generate Hydra Fastfetch emblem and verify packaged ASCII art

This commit is contained in:
Codex
2026-10-13 21:21:56 +03:00
parent be30e9dd49
commit 4eebb54f08
8 changed files with 170 additions and 21 deletions
+30
View File
@@ -0,0 +1,30 @@
# Hydra OS mark
`hydra-mark.png` is the original transparent image generated with the built-in
imagegen tool. The complete prompt and conversion settings are in
[`generation.json`](generation.json). `preview.png` shows the resulting ASCII
glyphs; the shipped asset is `image/rootfs/usr/share/hydra/fastfetch.txt`.
The 44-column mark uses ordinary ASCII characters, cyan Fastfetch color `$1`,
and a white `$2` caption. Pillow is needed only to regenerate the text, not to
run Fastfetch or build an image with the checked-in asset.
From the repository root, with Python and Pillow available:
```sh
python3 dev/render-logo.py
fastfetch --config image/rootfs/etc/fastfetch/config.jsonc \
--logo image/rootfs/usr/share/hydra/fastfetch.txt --pipe false
```
On a Nix development host, the converter can also run in a temporary environment:
```sh
nix-shell -p 'python3.withPackages (p: [p.pillow])' \
--run 'python3 dev/render-logo.py'
```
The converter uses opacity to preserve the single-color silhouette, removes
faint alpha noise, and accounts for terminal character proportions. It preserves
the PNG source. Future ISO builds and runtime update bundles include the new text;
previously built ISOs and installed guests retain their existing logo.
+15
View File
@@ -0,0 +1,15 @@
{
"tool": "built-in image_gen",
"generated_on": "2026-10-13",
"prompt": "Use case: logo-brand.\nAsset type: Hydra OS emblem, to be converted to a small 40–48 column ASCII logo for the Fastfetch Linux terminal system summary.\nPrimary request: Create one original, striking and extremely readable three-headed hydra emblem.\nSubject: exactly three reptilian dragon heads sharing one continuous broad base, with three clearly separated curved necks. A slightly taller center head faces forward, two side heads face outward. Distinct angular snouts, simple horns and large negative-space eyes. Broad separated necks and open gaps must make all three heads unmistakable at tiny sizes.\nStyle/medium: flat graphic silhouette; elegant, minimal, crisp, bold shapes; restrained technical OS identity.\nComposition: one centered balanced compact emblem in a square image, fills most of the canvas with a small even margin; no enclosing badge or border.\nColor: a single bright cyan fill, no shading. Actual transparent background including the gaps and eye cutouts.\nConstraints: clarity when downsampled to approximately 44 columns by 22 text rows. Thick shapes and large negative spaces, very few details. No text, letters, words, watermarks, gradients, glow, shadows, 3D, textures, scales, hairline strokes, or mockup. Only the hydra symbol.",
"source": "hydra-mark.png",
"conversion": {
"script": "../../dev/render-logo.py",
"library": "Pillow",
"verified_version": "12.3.0",
"columns": 44,
"character_aspect": 0.5,
"output": "../../image/rootfs/usr/share/hydra/fastfetch.txt"
},
"preview": "preview.png"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+17 -5
View File
@@ -352,14 +352,25 @@ def terminal_ux_iso():
return bytes(data) return bytes(data)
def verify_terminal_ux_branding(output, errors): def verify_terminal_ux_branding(output, errors, logo):
rendered = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', output) ansi = r'\x1b\[[0-?]*[ -/]*[@-~]'
rendered = re.sub(ansi, '', output)
if errors.strip() or re.search(r'\b(error|failed|unknown module)\b', rendered, re.I): if errors.strip() or re.search(r'\b(error|failed|unknown module)\b', rendered, re.I):
raise SmokeError('Installed default Fastfetch emitted errors: ' + (errors or rendered)[-1500:]) raise SmokeError('Installed default Fastfetch emitted errors: ' + (errors or rendered)[-1500:])
if not re.search(r'\bOS\s*:\s*Hydra OS\b', rendered): if not re.search(r'\bOS\s*:\s*Hydra OS\b', rendered):
raise SmokeError('Installed default Fastfetch did not render the Hydra OS pretty name') raise SmokeError('Installed default Fastfetch did not render the Hydra OS pretty name')
if 'H Y D R A O S' not in rendered or rendered.count('(o >') != 3: expected = [line.rstrip() for line in re.sub(r'\$[1-9]|' + ansi, '', logo).splitlines()
raise SmokeError('Installed default Fastfetch did not render the three-headed Hydra logo') if line.strip()]
caption = 'H Y D R A O S'
if (not any(line.strip() == caption for line in expected)
or not any(line.strip() != caption for line in expected)):
raise SmokeError('Packaged Fastfetch logo must contain art and the Hydra OS caption')
width = max(map(len, expected))
lines = iter(rendered.splitlines())
for row in expected:
if not any(line[:width].rstrip() == row
and (len(line) <= width or line[width].isspace()) for line in lines):
raise SmokeError('Installed default Fastfetch did not render the complete packaged Hydra logo')
return rendered return rendered
@@ -414,7 +425,8 @@ fetch = subprocess.run(['fastfetch', '--pipe', 'false', '--show-errors'],
capture_output=True, text=True, timeout=30) capture_output=True, text=True, timeout=30)
if fetch.returncode: if fetch.returncode:
raise SmokeError('Installed default Fastfetch failed: ' + fetch.stderr[-1500:]) raise SmokeError('Installed default Fastfetch failed: ' + fetch.stderr[-1500:])
fastfetch_output = verify_terminal_ux_branding(fetch.stdout, fetch.stderr) fastfetch_output = verify_terminal_ux_branding(
fetch.stdout, fetch.stderr, Path('/usr/share/hydra/fastfetch.txt').read_text())
config_hash = hashlib.sha256(Path('/etc/fastfetch/config.jsonc').read_bytes()).hexdigest() config_hash = hashlib.sha256(Path('/etc/fastfetch/config.jsonc').read_bytes()).hexdigest()
logo_hash = hashlib.sha256(Path('/usr/share/hydra/fastfetch.txt').read_bytes()).hexdigest() logo_hash = hashlib.sha256(Path('/usr/share/hydra/fastfetch.txt').read_bytes()).hexdigest()
base = Path(tempfile.mkdtemp(prefix='hux-', dir='/tmp')) base = Path(tempfile.mkdtemp(prefix='hux-', dir='/tmp'))
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Convert the generated Hydra silhouette to Fastfetch ASCII using Pillow.
Run from the repository root with Python + Pillow (development only):
python3 dev/render-logo.py
"""
import argparse
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
RAMP = ' .:-=+*#%@' # '$' is reserved for Fastfetch color placeholders.
CAPTION = 'H Y D R A O S'
def render(source, columns):
# This is a single-color mark: opacity, not luminance, defines its shape.
# Threshold near-transparent generation noise before reducing the image.
with Image.open(source) as image:
mask = image.convert('RGBA').getchannel('A').point(
lambda value: 255 if value > 160 else 0)
bounds = mask.getbbox()
if bounds is None:
raise ValueError('The source image has no visible silhouette')
mask = mask.crop(bounds)
# A terminal character is approximately twice as tall as it is wide.
rows = max(1, round(mask.height / mask.width * columns * 0.5))
pixels = mask.resize((columns, rows), Image.Resampling.LANCZOS)
lines = []
for y in range(rows):
line = ''
for x in range(columns):
coverage = pixels.getpixel((x, y)) / 255
level = min(9, max(0, round((coverage - 0.09) / 0.82 * 9)))
line += RAMP[level]
lines.append(line.rstrip())
return '$1' + '\n'.join(lines) + '\n\n$2' + CAPTION.center(columns).rstrip() + '\n'
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path,
default=ROOT / 'assets/branding/hydra-mark.png')
parser.add_argument('--output', type=Path,
default=ROOT / 'image/rootfs/usr/share/hydra/fastfetch.txt')
parser.add_argument('--columns', type=int, default=44)
args = parser.parse_args()
if not len(CAPTION) <= args.columns <= 100:
parser.error('columns must be between 13 and 100')
args.output.write_text(render(args.source, args.columns), encoding='ascii')
print(args.output)
if __name__ == '__main__':
main()
+22 -10
View File
@@ -1,10 +1,22 @@
$1 /\ /\ /\ $1 = =
.-(o > .-(o > .-(o > =@ @=
/ / / / / / :@+ :: +@:
\ \______\ \______\ \ :-*@%*@@*%@#-:
\ \ / . +: -@@@@@@@@@@- :+ .
'---. .---' =*:+#= =@*+@@@@+*@= =#+-*=
\ /\/\ / #@#@@##= .%@=*@@*=@%. =##@@#@#
___\/ \/___ #@@@@@@@@#=. .@@@@@@. .=#@@@@@@@@#
/______________\ .%%=:%@@@@@@@*: +%@@%+ :*@@@@@@@%:=%%.
$2 H Y D R A O S .#@##@@@%*@@@@#: =@@= :#@@@@*%@@@##@#.
@%@%=-%@# .*@@@@. .%==%. .@@@@*. #@%-=%@%@
=%-:-%+ #@@@# .@@@@. #@@@# +%-:=%+
-%- @@@@% =@@@@= %@@@@ -%-
.%@@@@. @@@@@@ .@@@@%.
-@@@@@: *%@@@@%* :@@@@@-
.@@@@@* .-%@@@@%-. *@@@@@.
*@@@@@%:+@@@@@@@@+:%@@@@@*
=#%@@@@@@@@@@@@@@@@@@@@%#=
.*@@@@@@@@@@@@@@@@@@*.
.:-=+*#@@#*+=-:.
$2 H Y D R A O S
+28 -6
View File
@@ -650,17 +650,39 @@ else: sys.exit(1)
with self.assertRaises(MediaError): with self.assertRaises(MediaError):
validate_iso(iso) validate_iso(iso)
def test_terminal_ux_branding_rejects_wrong_os_missing_logo_and_render_errors(self): def test_terminal_ux_branding_verifies_packaged_art_with_colors_and_side_data(self):
self.assertTrue(hasattr(self.mod, 'verify_terminal_ux_branding'), 'Default Fastfetch verification is missing') logo = '$1 @@@ @@@ @@@\n %%%%%\n\n$2 H Y D R A O S\n'
rendered = ' .-(o > .-(o > .-(o >\nH Y D R A O S\nOS: Hydra OS x86_64\n' rendered = (' @@@ @@@ @@@ hydratest@hydra-os\n'
self.mod.verify_terminal_ux_branding('\x1b[36m' + rendered + '\x1b[0m', '') ' %%%%% OS: Hydra OS x86_64\n'
' Kernel: Linux\n'
' H Y D R A O S Uptime: 1 minute\n')
actual = self.mod.verify_terminal_ux_branding(
'\x1b[36m' + rendered + '\x1b[0m', '', logo)
self.assertEqual(actual, rendered)
def test_terminal_ux_branding_rejects_incomplete_or_wrong_art_and_render_errors(self):
logo = '$1 @@@\n %%%\n %%%\n$2 H Y D R A O S\n'
rendered = ' @@@\n %%%\n %%%\n H Y D R A O S\nOS: Hydra OS x86_64\n'
for output, error in ((rendered.replace('OS: Hydra OS', 'OS: Gentoo'), ''), for output, error in ((rendered.replace('OS: Hydra OS', 'OS: Gentoo'), ''),
(rendered.replace('(o >', 'head'), ''), (rendered.replace(' @@@\n', ''), ''),
(rendered.replace(' @@@', ' @@@@'), ''),
(rendered.replace(' @@@', ' @@@ xx'), ''),
(rendered.replace(' %%%\n', '', 1), ''),
(rendered.replace(' @@@\n %%%', ' %%%\n @@@'), ''),
(rendered.replace('H Y D R A O S', 'OTHER OS'), ''),
('H Y D R A O S\nOS: Hydra OS x86_64\n', ''),
(rendered, 'Unknown JSON key: invalid'), (rendered, 'Unknown JSON key: invalid'),
(rendered + 'Memory: Error loading module\n', '')): (rendered + 'Memory: Error loading module\n', '')):
with self.subTest(output=output, error=error): with self.subTest(output=output, error=error):
with self.assertRaises(self.mod.SmokeError): with self.assertRaises(self.mod.SmokeError):
self.mod.verify_terminal_ux_branding(output, error) self.mod.verify_terminal_ux_branding(output, error, logo)
def test_terminal_ux_branding_requires_nonblank_packaged_art_and_caption(self):
rendered = ' @@@\n H Y D R A O S\nOS: Hydra OS x86_64\n'
for logo in ('', ' \n', '$2 H Y D R A O S\n', '$1 @@@\n'):
with self.subTest(logo=logo):
with self.assertRaises(self.mod.SmokeError):
self.mod.verify_terminal_ux_branding(rendered, '', logo)
def test_terminal_ux_snapshot_checks_live_qmp_identity_and_same_process(self): def test_terminal_ux_snapshot_checks_live_qmp_identity_and_same_process(self):
self.assertTrue(hasattr(self.mod, 'terminal_ux_snapshot'), 'Installed UX identity verification is missing') self.assertTrue(hasattr(self.mod, 'terminal_ux_snapshot'), 'Installed UX identity verification is missing')