From 7650710295560cb6e0532fe4c837641d23c225c4 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 06:57:49 -0400 Subject: [PATCH] [ADD] tui qemu: free values for vCPU, RAM and disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI already took a typed value -- a letter picks a suggestion, a digit stands for itself. The form, though, locked the custom profile into its three dropdowns. Each now gets a final "free value..." entry revealing an input below it. An invalid entry overwrites nothing: the resource falls back to the catalog value instead of freezing the view. The 3 vCPU preset was missing too, between 2 and 4; it comes from the same constant, so both interfaces gain it together. --- FR --- La CLI acceptait déjà une valeur tapée — une lettre choisit une suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait le profil personnalisé dans ses trois listes déroulantes. Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une saisie sous elle. Une entrée invalide n'écrase rien : la ressource retombe sur celle du catalogue plutôt que de bloquer la vue. Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même constante, donc les deux interfaces l'offrent ensemble. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 86 +++++++++++++++++++++++++++++---- script/todo/todo.py | 2 +- script/todo/todo_i18n.py | 8 +++ 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index e9b929c..c49511a 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -245,6 +245,16 @@ def run_deploy_form(ctx, run_app: bool = True): result = {"spec": None} AUTO = "__auto__" + # Dernier choix de chaque liste de ressources : il ne porte pas de valeur, + # il révèle la saisie libre placée juste dessous. + FREE = "__free__" + RES_FIELDS = { + "vcpus": ("#f_vcpus", "#c_vcpus"), + "ram": ("#f_ram", "#c_ram"), + "disk": ("#f_disk", "#c_disk"), + } + SELECT_TO_FIELD = {sel[1:]: f for f, (sel, _i) in RES_FIELDS.items()} + INPUT_TO_FIELD = {inp[1:]: f for f, (_s, inp) in RES_FIELDS.items()} def entry_label(e): star = " *" if e.get("default") else "" @@ -330,6 +340,7 @@ def run_deploy_form(ctx, run_app: bool = True): .grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; } SelectionList { height: 10; border: solid $panel; } RadioSet { height: auto; layout: horizontal; } + .freeval { display: none; } #reslabel { color: $text-muted; } EditVMScreen { align: center middle; } #editbox { @@ -364,6 +375,7 @@ def run_deploy_form(ctx, run_app: bool = True): self.arch = ctx.get("native") or arches[0] self.profile = "1" self.custom = {} + self._free = {} self.overrides = {} self.vms = [] self.rows = [] @@ -385,27 +397,52 @@ def run_deploy_form(ctx, run_app: bool = True): for label in ("x1", "x2", "x3", "x4"): yield RadioButton(label, value=label == "x1") yield RadioButton(t("custom")) + # Chaque ressource offre ses suggestions plus « libre… », + # qui révèle une saisie. C'est l'équivalent TUI de la règle + # de la CLI : une lettre choisit une suggestion, un chiffre + # vaut pour lui-même. yield Select( - [(str(c), c) for c in ctx["cpu_presets"]], + [(str(c), c) for c in ctx["cpu_presets"]] + + [(t("free value…"), FREE)], prompt=t("vCPU"), id="f_vcpus", disabled=True, ) + yield Input( + placeholder=t("vCPU"), + id="c_vcpus", + classes="freeval", + disabled=True, + ) yield Select( [ (f"{m} ({m // 1024}G)", m) for m in ctx["ram_presets"] - ], + ] + + [(t("free value…"), FREE)], prompt=t("RAM (MB)"), id="f_ram", disabled=True, ) + yield Input( + placeholder=t("RAM (MB)"), + id="c_ram", + classes="freeval", + disabled=True, + ) yield Select( - [(d, d) for d in ctx["disk_presets"]], + [(d, d) for d in ctx["disk_presets"]] + + [(t("free value…"), FREE)], prompt=t("Disk"), id="f_disk", disabled=True, ) + yield Input( + placeholder=t("Disk (e.g. 250G, 1.5T)"), + id="c_disk", + classes="freeval", + disabled=True, + ) yield Static("ERPLibre", classes="grouptitle") yield Checkbox( t("Install ERPLibre"), @@ -588,19 +625,50 @@ def run_deploy_form(ctx, run_app: bool = True): index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) custom = self.profile == "custom" - for wid in ("#f_vcpus", "#f_ram", "#f_disk"): - self.query_one(wid, Select).disabled = not custom + for field, (sel, inp) in RES_FIELDS.items(): + self.query_one(sel, Select).disabled = not custom + free = custom and self._free.get(field) + self._show_free(field, free) self._recompute() + def _show_free(self, field, visible) -> None: + """Montre ou cache la saisie libre d'une ressource.""" + widget = self.query_one(RES_FIELDS[field][1], Input) + widget.display = bool(visible) + widget.disabled = not visible + def on_selection_list_selected_changed(self, event) -> None: self._recompute() def on_select_changed(self, event) -> None: - mapping = {"f_vcpus": "vcpus", "f_ram": "ram", "f_disk": "disk"} - field = mapping.get(event.select.id) + field = SELECT_TO_FIELD.get(event.select.id) + if not field: + return + if event.value is FREE: + # La valeur retenue est celle de la saisie, pas ce choix-ci. + self._free[field] = True + self._show_free(field, True) + self.query_one(RES_FIELDS[field][1], Input).focus() + self._apply_free(field) + elif event.value is not SELECT_NULL: + self._free[field] = False + self._show_free(field, False) + self.custom[field] = event.value + self._recompute() + + def _apply_free(self, field) -> None: + """Relit la saisie libre. Une valeur invalide n'écrase rien : le + profil retombe alors sur celle du catalogue.""" + raw = self.query_one(RES_FIELDS[field][1], Input).value.strip() + if field == "disk": + self.custom[field] = parse_disk(raw) or "" + else: + self.custom[field] = positive_int(raw, 0) + + def on_input_changed(self, event) -> None: + field = INPUT_TO_FIELD.get(event.input.id) if field: - if event.value is not SELECT_NULL: - self.custom[field] = event.value + self._apply_free(field) self._recompute() def on_checkbox_changed(self, event) -> None: diff --git a/script/todo/todo.py b/script/todo/todo.py index 0d9fa62..f55798b 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4507,7 +4507,7 @@ class TODO: # KVM autorise plus de vCPU que de cœurs (surengagement) : on prévient # plutôt que d'écrêter, contrairement au multiplicateur x1..x4 qui, lui, # est un calcul automatique et se borne aux cœurs de l'hôte. - _QEMU_CPU_PRESETS = (1, 2, 4, 6, 8, 16, 24, 32) + _QEMU_CPU_PRESETS = (1, 2, 3, 4, 6, 8, 16, 24, 32) @staticmethod def _plural(word, count): diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index cea0ecd..41dd0ab 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1973,6 +1973,14 @@ TRANSLATIONS = { "fr": "RAM (Mo)", "en": "RAM (MB)", }, + "free value…": { + "fr": "valeur libre…", + "en": "free value…", + }, + "Disk (e.g. 250G, 1.5T)": { + "fr": "Disque (ex. 250G, 1,5T)", + "en": "Disk (e.g. 250G, 1.5T)", + }, "all archs": { "fr": "toutes", "en": "all",