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
@@ -0,0 +1,154 @@
"""dbanner: the banner shown at SSH login. Without arguments it opens the editor."""
import json
import os
import subprocess
import sys
import time
from . import banner, widgets
from .cli import die, need_root, resolve, run_tui
from .i18n import t
PROG = "dbanner"
SERVICE = "/etc/sv/das-banner"
SERVICE_LINK = "/var/service/das-banner"
def text(design, use_cache, color):
values = widgets.Values(design.get("vars"))
blocks = widgets.blocks(design, values, use_cache=use_cache)
return banner.render(design, values, blocks, color=color)
def wants_login_banner(design):
if os.path.exists(banner.OFF_FILE) or os.path.exists(os.path.expanduser("~/.hushlogin")):
return False
if not sys.stdout.isatty():
return False
return bool(os.environ.get("SSH_TTY")) or bool(design.get("local"))
def has_cached(design):
return any(w.get("enabled") is not False and widgets.mode_of(w) == "cached"
for w in design.get("widgets", []))
def set_service(enabled):
"""Enables the das-banner service (runit) that keeps cached widgets fresh."""
if not os.path.isdir(os.path.dirname(SERVICE_LINK)) or not os.path.isdir(SERVICE):
return False
if enabled and not os.path.lexists(SERVICE_LINK):
os.symlink(SERVICE, SERVICE_LINK)
elif not enabled and os.path.lexists(SERVICE_LINK):
os.remove(SERVICE_LINK)
return True
def apply(design):
design = banner.normalize(design)
banner.save(design, banner.SYSTEM_FILE)
os.chmod(banner.SYSTEM_FILE, 0o644)
cached = has_cached(design)
service = set_service(cached)
if service and cached:
# The service fills the cache in a moment; this makes the first login see it too
subprocess.Popen([sys.executable, os.path.abspath(sys.argv[0]), "refresh"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
return cached and service
USAGE = t("""Використання: dbanner [команда]
(без команди) редактор банера з готовими варіантами
list готові банери, ваші чернетки й малюнки
show [НАЗВА|ФАЙЛ] показати банер (типово — системний) зі свіжими даними
--no-color без кольорів
apply НАЗВА|ФАЙЛ зробити банером при вході по SSH (/etc/das/banner.json)
off | on вимкнути або ввімкнути банер
refresh [--force] оновити віджети з кешем (робить служба das-banner)
Банер показується при вході по SSH; "local": true у файлі — і при вході в консоль.
Окремий користувач вимикає його файлом ~/.hushlogin.""",
"""Usage: dbanner [command]
(no command) banner editor with ready-made designs
list ready-made banners, your drafts and pictures
show [NAME|FILE] show a banner (default: the system one) with fresh data
--no-color without colors
apply NAME|FILE make it the SSH login banner (/etc/das/banner.json)
off | on turn the banner off or on
refresh [--force] update the cached widgets (the das-banner service does it)
The banner is shown at SSH logins; "local": true in the file shows it on the
console too. A user turns it off for themselves with ~/.hushlogin.""")
def main(argv=None):
argv = sys.argv[1:] if argv is None else argv
if not argv:
from . import banner_app
return run_tui(PROG, banner_app.BannerApp)
cmd, args = argv[0], argv[1:]
flags = {a for a in args if a.startswith("--")}
args = [a for a in args if not a.startswith("--")]
try:
if cmd in ("-h", "--help", "help"):
print(USAGE)
elif cmd == "list":
print(t("Готові:", "Ready-made:"))
for name, title in banner.list_presets():
print(f" {name:<14} {title}")
drafts = banner.list_drafts()
if drafts:
print(t("Чернетки:", "Drafts:"))
for name, path in drafts:
print(f" {name:<14} {path}")
print(t("Малюнки (вставляються в редакторі):", "Pictures (inserted in the editor):"))
for name, title, _ in banner.list_art():
print(f" {name:<14} {title}")
elif cmd == "show":
if "--login" in flags:
design = banner.system_design()
if wants_login_banner(design):
sys.stdout.write(text(design, use_cache=True, color=True))
sys.stdout.flush()
# A hung widget thread must not hold the login
os._exit(0)
design = banner.load(args[0]) if args else banner.system_design()
color = "--no-color" not in flags and sys.stdout.isatty() or "--color" in flags
sys.stdout.write(text(design, use_cache=not args, color=color))
elif cmd == "apply":
if not args:
die(PROG, "вкажіть назву або файл", "name a design or a file")
spec = resolve(args[0])
design = banner.load(spec)
need_root(["apply", spec])
cached = apply(design)
print(t("Банер записано в /etc/das/banner.json, його видно при наступному вході по SSH.",
"The banner is written to /etc/das/banner.json; it shows at the next SSH login."))
if cached:
print(t("Віджети з кешем оновлює служба das-banner (увімкнено).",
"Cached widgets are updated by the das-banner service (enabled)."))
elif cmd in ("off", "on"):
need_root([cmd])
if cmd == "off":
open(banner.OFF_FILE, "w").close()
elif os.path.exists(banner.OFF_FILE):
os.remove(banner.OFF_FILE)
print(t("Банер вимкнено." if cmd == "off" else "Банер увімкнено.",
"The banner is off." if cmd == "off" else "The banner is on."))
elif cmd == "refresh":
if "--loop" in flags:
while True:
widgets.refresh(banner.system_design())
time.sleep(60)
widgets.refresh(banner.system_design(), force="--force" in flags)
else:
die(PROG, f"невідома команда: {cmd}\n{USAGE}", f"unknown command: {cmd}\n{USAGE}", 2)
except FileNotFoundError as e:
die(PROG, f"не знайдено: {e.filename or e}", f"not found: {e.filename or e}")
except (ValueError, json.JSONDecodeError) as e:
die(PROG, f"помилка у файлі: {e}", f"bad file: {e}")
except PermissionError as e:
die(PROG, f"немає доступу: {e.filename}", f"permission denied: {e.filename}")
return 0
if __name__ == "__main__":
sys.exit(main())