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