Terminal: Das prompt, dprompt and dbanner (0.9.1)
build / build (push) Successful in 38m26s

- das-shell 0.1.0: the Das two-line bash prompt by default (replaces the
  PS1 of Void's skel .bashrc once), dprompt (Textual prompt builder with
  presets and live preview) and dbanner (SSH login banner editor: canvas
  tools, fields with values, widgets, das-banner service for cached ones,
  Das OS logo and tryzub pictures). Python tests in make test.
- das-server 0.3.0 depends on das-shell; the server installer mentions
  dprompt and dbanner; the desktop image includes it.
- run-qemu.sh forwards the guest's SSH to localhost:2222.
- Hardware fixes: Ventoy menu icon, boot menu titles "Das OS <version>".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
das
2026-10-21 09:17:10 +03:00
co-authored by Claude Opus 5.5
parent 66ea614e0d
commit a2bffc7fe9
72 changed files with 5506 additions and 23 deletions
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# Regenerates the ready-made dbanner pictures from the SVGs in this directory.
# das-logo.svg: das-theme's logo with <use> expanded; trident*.svg: Lesser Coat
# of Arms of Ukraine from Wikimedia Commons (public domain, state symbol).
set -e
cd "$(dirname "$0")/../.."
A=srcpkgs/das-shell/files/share/banner/art
T=tools/art2cells.py
LOGO="#2f6fde=bright_blue #ffd23f=yellow #f5c400=bright_yellow #1f4fa8=blue"
python3 $T tools/art/das-logo.svg 16 16 das-logo-small "Логотип Das OS, малий" "Das OS logo, small" $LOGO --mirror >$A/das-logo-small.json
python3 $T tools/art/das-logo.svg 24 24 das-logo "Логотип Das OS" "Das OS logo" $LOGO --mirror >$A/das-logo.json
python3 $T tools/art/trident.svg 16 22 trident-small "Тризуб, малий" "Tryzub, small" "#ffd500=bright_yellow" --mirror >$A/trident-small.json
python3 $T tools/art/trident.svg 20 28 trident "Тризуб" "Tryzub" "#ffd500=bright_yellow" --mirror >$A/trident.json
python3 $T tools/art/trident-shield.svg 22 30 trident-shield "Малий герб України (тризуб на щиті)" \
"Lesser coat of arms of Ukraine (tryzub on a shield)" "#ffd500=bright_yellow" "#005bbb=blue" --mirror >$A/trident-shield.json
+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="-100 -100 200 200">
<!-- das-logo.svg of das-theme with the <use> copies expanded (ImageMagick MSVG has no <use>) -->
<polygon points="0.00,0.00 19.13,-46.19 0.00,-92.39 -19.13,-46.19" fill="#2f6fde"/>
<polygon points="0.00,-25.40 8.61,-46.19 0.00,-66.98 -8.61,-46.19" fill="#ffd23f"/>
<polygon points="0.00,0.00 46.19,-19.13 65.33,-65.33 19.13,-46.19" fill="#f5c400"/>
<polygon points="17.96,-17.96 38.75,-26.57 47.36,-47.36 26.57,-38.75" fill="#1f4fa8"/>
<polygon points="0.00,0.00 46.19,19.13 92.39,-0.00 46.19,-19.13" fill="#2f6fde"/>
<polygon points="25.40,-0.00 46.19,8.61 66.98,-0.00 46.19,-8.61" fill="#ffd23f"/>
<polygon points="-0.00,0.00 19.13,46.19 65.33,65.33 46.19,19.13" fill="#f5c400"/>
<polygon points="17.96,17.96 26.57,38.75 47.36,47.36 38.75,26.57" fill="#1f4fa8"/>
<polygon points="-0.00,0.00 -19.13,46.19 0.00,92.39 19.13,46.19" fill="#2f6fde"/>
<polygon points="0.00,25.40 -8.61,46.19 0.00,66.98 8.61,46.19" fill="#ffd23f"/>
<polygon points="0.00,-0.00 -46.19,19.13 -65.33,65.33 -19.13,46.19" fill="#f5c400"/>
<polygon points="-17.96,17.96 -38.75,26.57 -47.36,47.36 -26.57,38.75" fill="#1f4fa8"/>
<polygon points="0.00,-0.00 -46.19,-19.13 -92.39,0.00 -46.19,19.13" fill="#2f6fde"/>
<polygon points="-25.40,0.00 -46.19,-8.61 -66.98,0.00 -46.19,8.61" fill="#ffd23f"/>
<polygon points="0.00,0.00 -19.13,-46.19 -65.33,-65.33 -46.19,-19.13" fill="#f5c400"/>
<polygon points="-17.96,-17.96 -26.57,-38.75 -47.36,-47.36 -38.75,-26.57" fill="#1f4fa8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="330" height="461" viewBox="-82.5 0 165 230.5" fill="#ffd500">
<path d="M-81.25 1.25h162.5v172.5a31.25 31.25 0 0 1-18.578029 28.565428L0 228.867475l-62.671971-27.802047A31.25 31.25 0 0 1-81.25 172.5z" fill="#005bbb"/>
<path d="M5.985561 78.82382a104.079383 104.079383 0 0 0 14.053598 56.017033 55 55 0 0 1-13.218774 70.637179A20 20 0 0 0 0 212.5a20 20 0 0 0-6.820384-7.021968 55 55 0 0 1-13.218774-70.637179A104.079383 104.079383 0 0 0-5.98556 78.82382l-1.599642-45.260519A30.103986 30.103986 0 0 1 0 12.5a30.103986 30.103986 0 0 1 7.585202 21.063301zM5 193.624749a45 45 0 0 0 6.395675-53.75496A114.079383 114.079383 0 0 1 0 112.734179a114.079383 114.079383 0 0 1-11.395675 27.13561A45 45 0 0 0-5 193.624749V162.5H5z"/>
<path id="a" d="M27.779818 75.17546A62.64982 62.64982 0 0 1 60 27.5v145H0l-5-10a22.5 22.5 0 0 1 17.560976-21.95122l14.634147-3.292683a10 10 0 1 0-4.427443-19.503751zm5.998315 34.353887a20 20 0 0 1-4.387889 37.482848l-14.634146 3.292683A12.5 12.5 0 0 0 5 162.5h45V48.265462a52.64982 52.64982 0 0 0-12.283879 28.037802zM42 122.5h10v10H42z"/>
<use xlink:href="#a" transform="scale(-1 1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="330" height="461" viewBox="-82.5 0 165 230.5" fill="#ffd500">
<path d="M5.985561 78.82382a104.079383 104.079383 0 0 0 14.053598 56.017033 55 55 0 0 1-13.218774 70.637179A20 20 0 0 0 0 212.5a20 20 0 0 0-6.820384-7.021968 55 55 0 0 1-13.218774-70.637179A104.079383 104.079383 0 0 0-5.98556 78.82382l-1.599642-45.260519A30.103986 30.103986 0 0 1 0 12.5a30.103986 30.103986 0 0 1 7.585202 21.063301zM5 193.624749a45 45 0 0 0 6.395675-53.75496A114.079383 114.079383 0 0 1 0 112.734179a114.079383 114.079383 0 0 1-11.395675 27.13561A45 45 0 0 0-5 193.624749V162.5H5z"/>
<path id="a" d="M27.779818 75.17546A62.64982 62.64982 0 0 1 60 27.5v145H0l-5-10a22.5 22.5 0 0 1 17.560976-21.95122l14.634147-3.292683a10 10 0 1 0-4.427443-19.503751zm5.998315 34.353887a20 20 0 0 1-4.387889 37.482848l-14.634146 3.292683A12.5 12.5 0 0 0 5 162.5h45V48.265462a52.64982 52.64982 0 0 0-12.283879 28.037802zM42 122.5h10v10H42z"/>
<use xlink:href="#a" transform="scale(-1 1)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Turns an SVG into a dbanner picture: half-block cells (▀ ▄ █) in ANSI colors.
tools/art2cells.py SVG WIDTH_PX HEIGHT_PX NAME "UK title" "EN title" \
"#rrggbb=ansi_name" ... [--mirror] > srcpkgs/das-shell/files/share/banner/art/NAME.json
Each output cell holds two square pixels, so HEIGHT_PX must be even. The SVG is
drawn 8 times larger by ImageMagick, and every pixel takes the color that
covers most of its 8x8 block (or stays empty). Colors are matched to the
nearest one given on the command line; --mirror copies the left half to the
right for symmetric pictures. Needs ImageMagick and Pillow; this is a
developer tool, the package ships only the JSON.
"""
import json
import os
import subprocess
import sys
import tempfile
from collections import Counter
from PIL import Image
SCALE = 8
def main():
args = sys.argv[1:]
mirror = "--mirror" in args
if mirror:
args.remove("--mirror")
svg, width, height, name, uk, en, *pairs = args
width, height = int(width), int(height)
palette = []
for pair in pairs:
hexcolor, ansi = pair.split("=")
palette.append((tuple(int(hexcolor[i:i + 2], 16) for i in (1, 3, 5)), ansi))
with tempfile.TemporaryDirectory() as tmp:
png = os.path.join(tmp, "art.png")
subprocess.run(["convert", "-background", "none", "-density", "600", svg,
"-resize", f"{width * SCALE}x{height * SCALE}!", png], check=True)
img = Image.open(png).convert("RGBA")
def nearest(rgb):
return min(palette, key=lambda p: sum((a - b) ** 2 for a, b in zip(p[0], rgb)))[1]
pixels = []
for py in range(height):
row = []
for px in range(width):
votes = Counter()
for y in range(py * SCALE, (py + 1) * SCALE):
for x in range(px * SCALE, (px + 1) * SCALE):
r, g, b, a = img.getpixel((x, y))
votes[nearest((r, g, b)) if a >= 128 else None] += 1
row.append(votes.most_common(1)[0][0])
if mirror:
# The source is symmetric; rounding is not, so copy the left half
for x in range(width // 2):
row[width - 1 - x] = row[x]
pixels.append(row)
styles, keys = {}, "abcdefghijklmnopqrstuvwxyz"
def key(fg, bg):
want = {"fg": fg, "bg": bg, "bold": False}
for k, v in styles.items():
if v == want:
return k
k = keys[len(styles)]
styles[k] = want
return k
lines, smap = [], []
for cy in range(height // 2):
line, keyline = "", ""
for x in range(width):
top, bottom = pixels[2 * cy][x], pixels[2 * cy + 1][x]
if top is None and bottom is None:
ch, k = " ", " "
elif top == bottom:
ch, k = "█", key(top, None)
elif bottom is None:
ch, k = "▀", key(top, None)
elif top is None:
ch, k = "▄", key(bottom, None)
else:
ch, k = "▀", key(top, bottom)
line += ch
keyline += k
lines.append(line.rstrip())
smap.append(keyline[:len(line.rstrip())])
design = {"name": name, "title": {"uk": uk, "en": en}, "styles": styles,
"lines": lines, "style_map": smap}
json.dump(design, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
main()
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Writes the ready-made dbanner designs to srcpkgs/das-shell/files/share/banner/presets.
The designs are drawn with the same Grid that the editor uses, so they stay
editable in dbanner. Run after changing this file or the pictures in
share/banner/art (tools/art/build.sh).
"""
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FILES = os.path.join(ROOT, "srcpkgs", "das-shell", "files")
sys.path.insert(0, os.path.join(FILES, "python"))
from das_shell import banner # noqa: E402
OUT = os.path.join(FILES, "share", "banner", "presets")
def art(name):
return banner.Grid.from_design(banner.load_file(os.path.join(FILES, "share", "banner", "art", name + ".json")))
def field(grid, x, y, width, text, fg=None, bold=False, bg=None, align="left", font=None):
f = {"x": x, "y": y, "width": width, "align": align, "text": text,
"style": grid.style_key(fg, bg, bold)}
if font:
f["font"] = font
return f
def builtin(name, **extra):
return dict({"type": "builtin", "name": name}, **extra)
def save(name, uk, en, grid, fields, widgets, vars_=None, position="below"):
d = {"name": name, "title": {"uk": uk, "en": en}, "local": False, "vars": vars_ or {},
"fields": fields, "widgets_position": position, "widgets": widgets}
d = grid.to_design(d)
banner.save(d, os.path.join(OUT, name + ".json"))
def das_classic():
# The frame of the banner on the author's home server
g = banner.Grid(39, 5)
frame = g.style_key("blue")
g.box(0, 0, 38, 4, "hash", frame)
red = g.style_key("red", None, True)
g.text(7, 2, "Server IP:", red)
g.text(7, 3, "Link:", red)
fields = [
field(g, 2, 1, 35, "-=:dAs:=- {title}", "white", True, align="center"),
field(g, 18, 2, 19, "{ip}", "cyan", True),
field(g, 13, 3, 24, "{url}", "cyan", True),
]
widgets = [builtin("uptime"), builtin("disks"), builtin("memory"), builtin("docker"),
builtin("services"), builtin("updates")]
save("das-classic", "Das класичний: рамка з #, назва, IP, посилання", "Das classic: # frame, name, IP, link",
g, fields, widgets, {"title": "Das OS", "url": "https://das.org.ua/"})
def double():
w = 64
g = banner.Grid(w, 10)
cyan = g.style_key("cyan", None, True)
g.box(0, 0, w - 1, 9, "double", cyan)
g.text(1, 7, "─" * (w - 2), g.style_key("bright_black"))
fields = [
field(g, 2, 1, w - 4, "{hostname}", "bright_white", True, align="center", font="small"),
field(g, 2, 5, w - 4, "{title}", "yellow", True, align="center"),
field(g, 2, 6, w - 4, "{url}", "cyan", align="center"),
field(g, 2, 8, w - 4, "IP {ip} · {os} · {kernel}", "white", align="center"),
]
widgets = [builtin("uptime"), builtin("load"), builtin("memory"), builtin("disks"), builtin("services")]
save("double", "Подвійна рамка з великим ім'ям сервера", "Double frame with the server name in big letters",
g, fields, widgets, {"title": "Das OS Server", "url": ""})
def info_column(g, x, y, width, label_fg, value_fg, rows):
fields = []
for i, (label, value) in enumerate(rows):
if label is None:
fields.append(field(g, x, y + i, width, value, label_fg))
continue
fields.append(field(g, x, y + i, 9, label, label_fg, True))
fields.append(field(g, x + 9, y + i, width - 9, value, value_fg))
return fields
def blocks():
logo = art("das-logo")
g = banner.Grid(76, max(logo.height, 12))
g.stamp(0, 0, logo)
x = logo.width + 4
fields = [field(g, x, 0, 76 - x, "{user}@{hostname}", "bright_cyan", True)]
fields += info_column(g, x, 1, 76 - x, "bright_blue", "white", [
(None, "─" * 30),
("[[ОС|OS]]", "{os}"),
("[[Ядро|Kernel]]", "{kernel}"),
("[[Працює|Uptime]]", "{uptime}"),
("CPU", "{cpu}"),
("[[Пам'ять|Memory]]", "{mem}"),
("IP", "{ips}"),
("[[Вхід|Users]]", "{users}"),
])
fields.append(field(g, x, 10, 76 - x, "{url}", "yellow"))
save("blocks", "Логотип Das OS і відомості про систему", "Das OS logo and system facts",
g, fields, [builtin("disks")], {"url": ""})
def dashboard():
w = 70
g = banner.Grid(w, 3)
g.box(0, 0, w - 1, 2, "rounded", g.style_key("bright_blue"))
fields = [field(g, 2, 1, w - 4, "{hostname} · {ip} · {url}", "bright_white", True, align="center")]
widgets = [builtin(n) for n in ("uptime", "load", "memory", "disks", "network", "docker", "updates",
"weather", "smart", "services", "fail2ban", "snapshots", "lastlog")]
save("dashboard", "Панель: усі віджети під заголовком", "Dashboard: every widget under a header",
g, fields, widgets, {"url": "", "city": "Kyiv"})
def minimal():
g = banner.Grid(70, 1)
fields = [field(g, 0, 0, 70, "{hostname} · {ip} · [[працює|up]] {uptime} · {load}", "bright_cyan", True)]
save("minimal", "Мінімальний: один рядок", "Minimal: one line", g, fields, [])
def ukraine():
shield = art("trident-shield")
g = banner.Grid(80, shield.height)
g.stamp(0, 0, shield)
x = shield.width + 4
blue, yellow = "bright_blue", "bright_yellow"
fields = [
field(g, x, 1, 80 - x, "{hostname}", yellow, True, font="small"),
field(g, x, 6, 80 - x, "[[Слава Україні!|Glory to Ukraine!]]", blue, True),
]
fields += info_column(g, x, 8, 80 - x, blue, yellow, [
("IP", "{ip}"),
("[[Сайт|Site]]", "{url}"),
("[[Працює|Uptime]]", "{uptime}"),
("[[Пам'ять|Memory]]", "{mem}"),
("[[ОС|OS]]", "{os}"),
])
save("ukraine", "Тризуб: малий герб України і дані сервера", "Tryzub: Ukrainian coat of arms and server facts",
g, fields, [builtin("disks"), builtin("services")], {"url": ""})
def tryzub_small():
# A compact one for small screens: the small tryzub, a few lines of text
tz = art("trident-small")
g = banner.Grid(60, tz.height)
g.stamp(0, 0, tz)
x = tz.width + 3
fields = [
field(g, x, 2, 60 - x, "{hostname}", "bright_yellow", True),
field(g, x, 3, 60 - x, "{ip}", "bright_blue", True),
field(g, x, 5, 60 - x, "[[Працює|Uptime]]: {uptime}", "white"),
field(g, x, 6, 60 - x, "{url}", "bright_blue"),
]
save("tryzub", "Малий тризуб з іменем і IP", "Small tryzub with name and IP", g, fields,
[builtin("disks")], {"url": ""})
if __name__ == "__main__":
os.makedirs(OUT, exist_ok=True)
for make in (das_classic, double, blocks, dashboard, minimal, ukraine, tryzub_small):
make()