- 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,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
|
||||
Reference in New Issue
Block a user