mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
number the options of the OCI menus instead of showing internal values
- Dialog menus show their options numbered, as the rest of ProxMenux does, and return the value of the chosen one, so each description has the whole width in every language. - A storage, a bridge, a device node and the main OCI menu keep their tags, and number tags and separators are left as they are. - Test for the numbering, the default option, the kept tags and a cancelled menu.
This commit is contained in:
@@ -29,7 +29,7 @@ from .installer import (
|
||||
run_remote_install,
|
||||
run_remote_rclone_mount,
|
||||
)
|
||||
from .ui import APP_TITLE, DETAIL_WINDOW_WIDTH, UserCancelled, interactive_ui, menu_size
|
||||
from .ui import APP_TITLE, DETAIL_WINDOW_WIDTH, ReadableTags, UserCancelled, interactive_ui, menu_size
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -565,8 +565,8 @@ def interactive(catalog: Catalog) -> int:
|
||||
for number, key in category_by_index.items()]
|
||||
selection = "search"
|
||||
while True:
|
||||
selection = ui.choose(translate("Select a category or search for applications:"), options, selection,
|
||||
size=MENU_SIZE)
|
||||
selection = ui.choose(translate("Select a category or search for applications:"), ReadableTags(options),
|
||||
selection, size=MENU_SIZE)
|
||||
if selection is None:
|
||||
return 0
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
from . import host
|
||||
from .i18n import translate
|
||||
from .ui import UserCancelled
|
||||
from .ui import ReadableTags, UserCancelled
|
||||
|
||||
|
||||
GPU_NODE = re.compile(r'/dev/dri/(?:renderD|card)[0-9]+|/dev/kfd')
|
||||
@@ -22,7 +22,7 @@ def choose_usb_device(ui, title, default=None, attached=()):
|
||||
ui.message(translate('No selectable USB device was found. Type the host node by hand.'))
|
||||
return ui.ask(manual_prompt, default or '/dev/ttyACM0')
|
||||
options.append(('manual', translate('Another node, typed by hand')))
|
||||
selected = ui.choose(title, options, default if default in dict(options) else options[0][0])
|
||||
selected = ui.choose(title, ReadableTags(options), default if default in dict(options) else options[0][0])
|
||||
if selected is None:
|
||||
raise UserCancelled(translate('Device configuration cancelled'))
|
||||
return ui.ask(manual_prompt, default or '/dev/ttyACM0') if selected == 'manual' else selected
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any
|
||||
from . import host
|
||||
from . import network as access
|
||||
from .i18n import source_text, translate
|
||||
from .ui import DialogUI, TerminalUI, UserCancelled
|
||||
from .ui import DialogUI, ReadableTags, TerminalUI, UserCancelled
|
||||
from .custom_mounts import ask_custom_mounts
|
||||
from .extra_devices import device_permissions
|
||||
|
||||
@@ -160,7 +160,7 @@ def ask_storage(ui, text: str, content: str, default: str, mode: str = ADVANCED_
|
||||
options = [(row["storage"], f"{row.get('type', '')} {host.gib(row.get('avail'))} GB {translate('free')}")
|
||||
for row in rows]
|
||||
names = [name for name, _ in options]
|
||||
selected = ui.choose(text, options, default if default in names else names[0])
|
||||
selected = ui.choose(text, ReadableTags(options), default if default in names else names[0])
|
||||
if selected is None:
|
||||
raise UserCancelled(text)
|
||||
return selected
|
||||
@@ -302,7 +302,7 @@ def ask_bridge(ui, text: str, default: str, mode: str = ADVANCED_MODE) -> str:
|
||||
return ui.ask(text, default)
|
||||
options = [(row["iface"], row.get("cidr") or (row.get("comments") or "").strip() or "-") for row in rows]
|
||||
names = [name for name, _ in options]
|
||||
selected = ui.choose(text, options, default if default in names else names[0])
|
||||
selected = ui.choose(text, ReadableTags(options), default if default in names else names[0])
|
||||
if selected is None:
|
||||
raise UserCancelled(text)
|
||||
return selected
|
||||
|
||||
@@ -190,6 +190,11 @@ class TerminalUI:
|
||||
# Width of the windows of the installer: questions, choices and notices. The
|
||||
# final summary and the page of an application, which show tables, are wider.
|
||||
MENU_WIDTH = 72
|
||||
|
||||
|
||||
class ReadableTags(list):
|
||||
"""Menu options whose tags the user reads, as a storage, a bridge or a
|
||||
device node: they are shown as they are, not numbered."""
|
||||
SUMMARY_WIDTH = 78
|
||||
DETAIL_WINDOW_WIDTH = 90
|
||||
|
||||
@@ -279,6 +284,19 @@ class DialogUI:
|
||||
def choose(self, text: str, options: list[tuple[str, str]], default: str | None = None,
|
||||
title: str | None = None, show_tags: bool = True,
|
||||
size: tuple[int, int, int] | None = None, colors: bool = False) -> str | None:
|
||||
# A word is an internal value: the menu numbers the options, as the
|
||||
# rest of ProxMenux does, and returns the value of the chosen one.
|
||||
values: dict[str, str] = {}
|
||||
if not isinstance(options, ReadableTags) and any(tag and not tag.isdigit() for tag, _ in options):
|
||||
numbered = []
|
||||
for tag, label in options:
|
||||
if tag:
|
||||
values[str(len(values) + 1)] = tag
|
||||
if tag == default:
|
||||
default = str(len(values))
|
||||
tag = str(len(values))
|
||||
numbered.append((tag, label))
|
||||
options = numbered
|
||||
if size:
|
||||
widget = ["--colors"] if colors else []
|
||||
widget += ["--default-item", default] if default else []
|
||||
@@ -286,7 +304,8 @@ class DialogUI:
|
||||
for tag, label in options:
|
||||
widget += [tag, label]
|
||||
result = self._run(widget, title)
|
||||
return result.stderr.strip() if result.returncode == 0 else None
|
||||
selected = result.stderr.strip() if result.returncode == 0 else None
|
||||
return values.get(selected, selected) if values and selected else selected
|
||||
terminal = shutil.get_terminal_size((100, 30))
|
||||
tag_width = max((len(tag) for tag, _ in options), default=0) + 2 if show_tags else 0
|
||||
width = min(max(MENU_WIDTH, max((len(label) for _, label in options), default=0) + tag_width + 14,
|
||||
@@ -302,7 +321,8 @@ class DialogUI:
|
||||
for tag, label in options:
|
||||
widget += [tag, label]
|
||||
result = self._run(widget, title)
|
||||
return result.stderr.strip() if result.returncode == 0 else None
|
||||
selected = result.stderr.strip() if result.returncode == 0 else None
|
||||
return values.get(selected, selected) if values and selected else selected
|
||||
|
||||
def detail_menu(self, text: str, options: list[tuple[str, str]], default: str | None = None,
|
||||
title: str | None = None) -> str | None:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""A menu shows numbers, not internal values: the code still receives the
|
||||
value of the option chosen. A tag the user reads, such as a storage, stays."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from proxmenux_oci.ui import DialogUI, ReadableTags
|
||||
|
||||
SOURCES = [("paste", "Paste its Compose file in the terminal"),
|
||||
("file", "Read its Compose file from a file of this host"),
|
||||
("oci-compose-paste", "Paste an oci-compose in the terminal")]
|
||||
|
||||
|
||||
class DialogMenuTagsTests(unittest.TestCase):
|
||||
def run_menu(self, options, answer, **kwargs):
|
||||
seen = {}
|
||||
|
||||
def run(widget, title):
|
||||
seen["widget"] = widget
|
||||
return SimpleNamespace(returncode=0, stderr=answer)
|
||||
|
||||
ui = DialogUI()
|
||||
with patch.object(ui, "_run", side_effect=run):
|
||||
selected = ui.choose("How is the image described?", options, **kwargs)
|
||||
menu = seen["widget"][seen["widget"].index("--menu") + 5:]
|
||||
return selected, menu[0::2], seen["widget"]
|
||||
|
||||
def test_word_tags_are_shown_as_numbers_and_the_value_comes_back(self):
|
||||
selected, tags, _ = self.run_menu(SOURCES, "3", default="file", size=(20, 80, 10))
|
||||
self.assertEqual(tags, ["1", "2", "3"])
|
||||
self.assertEqual(selected, "oci-compose-paste")
|
||||
|
||||
def test_the_default_follows_its_number(self):
|
||||
_, _, widget = self.run_menu(SOURCES, "2", default="file")
|
||||
self.assertEqual(widget[widget.index("--default-item") + 1], "2")
|
||||
|
||||
def test_a_tag_the_user_reads_is_kept(self):
|
||||
storages = [("local-lvm", "lvmthin 669 GB free"), ("local2-lvm", "lvmthin 269 GB free")]
|
||||
selected, tags, _ = self.run_menu(ReadableTags(storages), "local2-lvm")
|
||||
self.assertEqual((tags, selected), (["local-lvm", "local2-lvm"], "local2-lvm"))
|
||||
|
||||
def test_number_tags_and_separators_are_left_as_they_are(self):
|
||||
options = [("101", "Jellyfin"), ("", ""), ("205", "Immich")]
|
||||
selected, tags, _ = self.run_menu(options, "205")
|
||||
self.assertEqual((tags, selected), (["101", "", "205"], "205"))
|
||||
|
||||
def test_cancelling_returns_nothing(self):
|
||||
ui = DialogUI()
|
||||
with patch.object(ui, "_run", return_value=SimpleNamespace(returncode=1, stderr="")):
|
||||
self.assertIsNone(ui.choose("How is the image described?", SOURCES))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user