- 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:
Executable
+15
@@ -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
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
Executable
+101
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user