diff --git a/TUI/modalscreen/config_modal.py b/TUI/modalscreen/config_modal.py index b3ceaa7..0053a31 100644 --- a/TUI/modalscreen/config_modal.py +++ b/TUI/modalscreen/config_modal.py @@ -11,6 +11,8 @@ import json from pathlib import Path from SYS.config import load_config, save_config from Store.registry import _discover_store_classes, _required_keys_for +from ProviderCore.registry import list_providers +from TUI.modalscreen.selection_modal import SelectionModal class ConfigModal(ModalScreen): """A modal for editing the configuration.""" @@ -113,11 +115,13 @@ class ConfigModal(ModalScreen): with Horizontal(id="config-actions"): yield Button("Save", variant="success", id="save-btn") yield Button("Add Store", variant="primary", id="add-store-btn") + yield Button("Add Provider", variant="primary", id="add-provider-btn") yield Button("Back", id="back-btn") yield Button("Close", variant="error", id="cancel-btn") def on_mount(self) -> None: self.query_one("#add-store-btn", Button).display = False + self.query_one("#add-provider-btn", Button).display = False self.query_one("#back-btn", Button).display = False self.refresh_view() @@ -131,7 +135,9 @@ class ConfigModal(ModalScreen): # Update visibility of buttons try: self.query_one("#add-store-btn", Button).display = (self.current_category == "stores" and self.editing_item_name is None) + self.query_one("#add-provider-btn", Button).display = (self.current_category == "providers" and self.editing_item_name is None) self.query_one("#back-btn", Button).display = (self.editing_item_name is not None) + self.query_one("#save-btn", Button).display = (self.editing_item_name is not None or self.current_category == "globals") except Exception: pass @@ -184,6 +190,7 @@ class ConfigModal(ModalScreen): row = Horizontal( Static(name, classes="item-label"), Button("Edit", id=f"edit-provider-{name}"), + Button("Delete", variant="error", id=f"del-provider-{name}"), classes="item-row" ) container.mount(row) @@ -226,6 +233,20 @@ class ConfigModal(ModalScreen): if rk.upper() not in existing_keys_upper: container.mount(Label(rk)) container.mount(Input(value="", id=f"item-{rk}", classes="config-input")) + + # If it's a provider, we might have required keys + if item_type == "provider": + from ProviderCore.registry import get_provider_class + try: + pcls = get_provider_class(item_name) + if pcls: + required_keys = pcls.required_config_keys() + for rk in required_keys: + if rk.upper() not in existing_keys_upper: + container.mount(Label(rk)) + container.mount(Input(value="", id=f"item-{rk}", classes="config-input")) + except Exception: + pass def create_field(self, name: str, value: Any, id: str) -> Vertical: # This method is now unused - we mount labels and inputs directly @@ -256,6 +277,8 @@ class ConfigModal(ModalScreen): self.editing_item_type = None self.refresh_view() elif bid == "save-btn": + if not self.validate_current_editor(): + return self.save_all() self.notify("Configuration saved!") # Return to the main list view within the current category @@ -274,16 +297,11 @@ class ConfigModal(ModalScreen): self.editing_item_type = "provider" self.refresh_view() elif bid == "add-store-btn": - # Create a simple dialog or default folder store - new_name = "new_folder" - if "store" not in self.config_data: - self.config_data["store"] = {} - if "folder" not in self.config_data["store"]: - self.config_data["store"]["folder"] = {} - self.config_data["store"]["folder"][new_name] = {"NAME": new_name, "path": ""} - self.editing_item_type = "store-folder" - self.editing_item_name = new_name - self.refresh_view() + options = list(_discover_store_classes().keys()) + self.app.push_screen(SelectionModal("Select Store Type", options), callback=self.on_store_type_selected) + elif bid == "add-provider-btn": + options = list(list_providers().keys()) + self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected) elif bid.startswith("del-store-"): parts = bid.replace("del-store-", "").split("-", 1) if len(parts) == 2: @@ -292,6 +310,57 @@ class ConfigModal(ModalScreen): if name in self.config_data["store"][stype]: del self.config_data["store"][stype][name] self.refresh_view() + elif bid.startswith("del-provider-"): + name = bid.replace("del-provider-", "") + if "provider" in self.config_data and name in self.config_data["provider"]: + del self.config_data["provider"][name] + self.refresh_view() + + def on_store_type_selected(self, stype: str) -> None: + if not stype: return + new_name = f"new_{stype}" + if "store" not in self.config_data: + self.config_data["store"] = {} + if stype not in self.config_data["store"]: + self.config_data["store"][stype] = {} + + # Default config for the new store + new_config = {"NAME": new_name} + classes = _discover_store_classes() + if stype in classes: + required = _required_keys_for(classes[stype]) + for rk in required: + if rk.upper() != "NAME": + new_config[rk] = "" + + self.config_data["store"][stype][new_name] = new_config + self.editing_item_type = f"store-{stype}" + self.editing_item_name = new_name + self.refresh_view() + + def on_provider_type_selected(self, ptype: str) -> None: + if not ptype: return + if "provider" not in self.config_data: + self.config_data["provider"] = {} + + # For providers, they are usually top-level entries in 'provider' dict + if ptype not in self.config_data["provider"]: + # Get required keys if possible + from ProviderCore.registry import get_provider_class + try: + pcls = get_provider_class(ptype) + new_config = {} + if pcls: + required = pcls.required_config_keys() + for rk in required: + new_config[rk] = "" + self.config_data["provider"][ptype] = new_config + except Exception: + self.config_data["provider"][ptype] = {} + + self.editing_item_type = "provider" + self.editing_item_name = ptype + self.refresh_view() @on(Input.Changed) def on_input_changed(self, event: Input.Changed) -> None: @@ -325,3 +394,46 @@ class ConfigModal(ModalScreen): def save_all(self) -> None: save_config(self.config_data, config_dir=self.workspace_root) + + def validate_current_editor(self) -> bool: + """Ensure all required fields for the current item are filled.""" + if not self.editing_item_name: + return True + + item_type = str(self.editing_item_type or "") + item_name = str(self.editing_item_name or "") + + required_keys = [] + section = {} + + if item_type.startswith("store-"): + stype = item_type.replace("store-", "") + classes = _discover_store_classes() + if stype in classes: + required_keys = _required_keys_for(classes[stype]) + section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {}) + elif item_type == "provider": + from ProviderCore.registry import get_provider_class + try: + pcls = get_provider_class(item_name) + if pcls: + required_keys = pcls.required_config_keys() + except Exception: + pass + section = self.config_data.get("provider", {}).get(item_name, {}) + + # Check required keys + for rk in required_keys: + # Case-insensitive lookup for the required key in the current section + val = None + rk_upper = rk.upper() + for k, v in section.items(): + if k.upper() == rk_upper: + val = v + break + + if not val or not str(val).strip(): + self.notify(f"Required field '{rk}' cannot be blank", severity="error") + return False + + return True diff --git a/TUI/modalscreen/selection_modal.py b/TUI/modalscreen/selection_modal.py new file mode 100644 index 0000000..f982b1d --- /dev/null +++ b/TUI/modalscreen/selection_modal.py @@ -0,0 +1,65 @@ +from textual.app import ComposeResult +from textual.screen import ModalScreen +from textual.containers import Container, ScrollableContainer +from textual.widgets import Static, Button, Label +from typing import List, Callable + +class SelectionModal(ModalScreen[str]): + """A modal for selecting a type from a list of strings.""" + + CSS = """ + SelectionModal { + align: center middle; + background: rgba(0, 0, 0, 0.5); + } + + #selection-container { + width: 40%; + height: 60%; + background: $panel; + border: thick $primary; + padding: 1; + } + + .selection-title { + background: $accent; + color: $text; + padding: 0 1; + margin-bottom: 1; + text-align: center; + text-style: bold; + height: 3; + content-align: center middle; + } + + .selection-button { + width: 100%; + margin-bottom: 1; + } + + #selection-cancel { + width: 100%; + margin-top: 1; + background: $error; + } + """ + + def __init__(self, title: str, options: List[str]) -> None: + super().__init__() + self.selection_title = title + self.options = sorted(options) + + def compose(self) -> ComposeResult: + with Container(id="selection-container"): + yield Static(self.selection_title, classes="selection-title") + with ScrollableContainer(): + for opt in self.options: + yield Button(opt, id=f"opt-{opt}", classes="selection-button") + yield Button("Cancel", id="selection-cancel") + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "selection-cancel": + self.dismiss("") + elif event.button.id and event.button.id.startswith("opt-"): + selection = event.button.id.replace("opt-", "") + self.dismiss(selection)