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,157 @@
"""Textual pieces shared by the dprompt and dbanner editors."""
import os
import subprocess
import sys
from rich.style import Style
from rich.text import Text
from textual import events
from textual.containers import Horizontal, Vertical
from textual.message import Message
from textual.screen import ModalScreen
from textual.widget import Widget
from textual.widgets import Button, Input, Label, Select
from . import colors
from .i18n import t
NONE = "-" # Select has no None value
def color_options(allow_none=True):
names = {
"black": ("чорний", "black"), "red": ("червоний", "red"), "green": ("зелений", "green"),
"yellow": ("жовтий", "yellow"), "blue": ("синій", "blue"), "magenta": ("пурпуровий", "magenta"),
"cyan": ("бірюзовий", "cyan"), "white": ("білий", "white"),
}
opts = [(t("— типовий —", "— default —"), NONE)] if allow_none else []
for c in colors.NAMES:
base = c[7:] if c.startswith("bright_") else c
label = t(*names[base])
if c.startswith("bright_"):
label = t("яскравий ", "bright ") + label
opts.append((Text.assemble(("██ ", c), label), c))
return opts
def color_value(value):
return None if value in (NONE, Select.NULL, None) else value
def to_select(color):
return color if color in colors.NAMES else NONE
class Swatches(Widget):
"""A row of the 16 colors (and "none") to click; the chosen one is marked."""
DEFAULT_CSS = """
Swatches { height: 2; width: 36; }
"""
class Picked(Message):
def __init__(self, swatches, color):
super().__init__()
self.swatches, self.color = swatches, color
def __init__(self, color=None, allow_none=True, **kw):
super().__init__(**kw)
self.color, self.allow_none = color, allow_none
def _items(self):
return ([None] if self.allow_none else []) + list(colors.NAMES)
def render(self):
text = Text()
for i, c in enumerate(self._items()):
if i == 9:
text.append("\n")
mark = "▣" if c == self.color else "■"
if c is None:
text.append("∅ " if c != self.color else "▣ ", Style(bold=True))
else:
text.append(mark * 2 + " " if c == self.color else "██ ", Style(color=c))
return text
def on_click(self, event: events.Click):
items = self._items()
index = (event.y * 9) + event.x // 3
if 0 <= event.x // 3 < 9 and 0 <= index < len(items):
self.color = items[index]
self.refresh()
self.post_message(self.Picked(self, self.color))
class AskDialog(ModalScreen):
"""A few text fields with OK/Cancel; dismisses with {key: value} or None."""
DEFAULT_CSS = """
AskDialog { align: center middle; }
AskDialog > Vertical { width: 72; height: auto; border: round $accent; background: $surface; padding: 1 2; }
AskDialog Label { margin-top: 1; }
AskDialog Horizontal { height: auto; margin-top: 1; align-horizontal: right; }
AskDialog Button { margin-left: 2; }
"""
BINDINGS = [("escape", "cancel", t("Скасувати", "Cancel"))]
def __init__(self, heading, fields):
"""fields: [(key, label, value or a list of (label, value) choices, current)]"""
super().__init__()
self.heading, self.fields = heading, fields
def compose(self):
with Vertical():
yield Label(Text(self.heading, style="bold"))
for key, label, value, *rest in self.fields:
yield Label(Text(label))
if isinstance(value, list):
yield Select(value, value=rest[0] if rest else value[0][1], allow_blank=False, id="f_" + key)
else:
yield Input(str(value), id="f_" + key)
with Horizontal():
yield Button(t("Скасувати", "Cancel"), id="cancel")
yield Button("OK", variant="primary", id="ok")
def on_mount(self):
inputs = self.query(Input)
if inputs:
inputs.first().focus()
def _result(self):
return {key: self.query_one("#f_" + key).value for key, *_ in self.fields}
def on_button_pressed(self, event: Button.Pressed):
self.dismiss(self._result() if event.button.id == "ok" else None)
def on_input_submitted(self, event: Input.Submitted):
self.dismiss(self._result())
def action_cancel(self):
self.dismiss(None)
def run_as_root(app, args):
"""Runs one of our commands through sudo with the TUI suspended, so sudo can
ask for the password. Returns True on success."""
exe = "/usr/bin/" + args[0]
if os.path.exists(exe):
cmd = [exe] + args[1:]
else: # from the source tree
module = {"dprompt": "das_shell.cli_prompt", "dbanner": "das_shell.cli_banner"}[args[0]]
env = [f"{k}={os.environ[k]}" for k in ("PYTHONPATH", "DAS_SHELL_DATA", "DAS_SHELL_ETC", "DAS_SHELL_CACHE")
if k in os.environ]
cmd = ["env"] + env + [sys.executable, "-m", module] + args[1:]
if os.geteuid() != 0:
cmd = ["sudo"] + cmd
with app.suspend():
print("\n$ " + " ".join(cmd))
code = subprocess.run(cmd).returncode
if code:
input(t("Помилка. Enter — повернутися…", "Failed. Press Enter to go back…"))
return code == 0
def bar(*buttons):
row = Horizontal(*buttons, classes="buttons")
return row