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>
155 lines
7.1 KiB
Python
155 lines
7.1 KiB
Python
"""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())
|