|
|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
from textual.app import ComposeResult
|
|
|
|
|
from textual.screen import ModalScreen
|
|
|
|
|
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
|
|
|
from textual.widgets import Static, Button, Input, Label, ListView, ListItem, Rule, OptionList, Footer
|
|
|
|
|
from textual.widgets import Static, Button, Input, Label, ListView, ListItem, Rule, OptionList, Footer, Select
|
|
|
|
|
from textual import on
|
|
|
|
|
from textual.message import Message
|
|
|
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
|
@@ -84,6 +84,11 @@ class ConfigModal(ModalScreen):
|
|
|
|
|
content-align: left middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.item-row Button {
|
|
|
|
|
width: 15;
|
|
|
|
|
margin-left: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
margin: 0 1;
|
|
|
|
|
}
|
|
|
|
|
@@ -279,6 +284,7 @@ class ConfigModal(ModalScreen):
|
|
|
|
|
# Determine display props from schema
|
|
|
|
|
label_text = k
|
|
|
|
|
is_secret = False
|
|
|
|
|
choices = None
|
|
|
|
|
schema = provider_schema_map.get(k_upper)
|
|
|
|
|
if schema:
|
|
|
|
|
label_text = schema.get("label") or k
|
|
|
|
|
@@ -286,14 +292,27 @@ class ConfigModal(ModalScreen):
|
|
|
|
|
label_text += " *"
|
|
|
|
|
if schema.get("secret"):
|
|
|
|
|
is_secret = True
|
|
|
|
|
choices = schema.get("choices")
|
|
|
|
|
|
|
|
|
|
container.mount(Label(label_text))
|
|
|
|
|
inp_id = f"item-{idx}"
|
|
|
|
|
self._input_id_map[inp_id] = k
|
|
|
|
|
inp = Input(value=str(v), id=inp_id, classes="config-input")
|
|
|
|
|
if is_secret:
|
|
|
|
|
inp.password = True
|
|
|
|
|
container.mount(inp)
|
|
|
|
|
|
|
|
|
|
if choices:
|
|
|
|
|
# Select takes a list of (label, value) tuples
|
|
|
|
|
select_options = [(str(c), str(c)) for c in choices]
|
|
|
|
|
# If current value not in choices, add it or stay blank
|
|
|
|
|
current_val = str(v)
|
|
|
|
|
if current_val not in [str(c) for c in choices]:
|
|
|
|
|
select_options.insert(0, (current_val, current_val))
|
|
|
|
|
|
|
|
|
|
sel = Select(select_options, value=current_val, id=inp_id)
|
|
|
|
|
container.mount(sel)
|
|
|
|
|
else:
|
|
|
|
|
inp = Input(value=str(v), id=inp_id, classes="config-input")
|
|
|
|
|
if is_secret:
|
|
|
|
|
inp.password = True
|
|
|
|
|
container.mount(inp)
|
|
|
|
|
idx += 1
|
|
|
|
|
|
|
|
|
|
# Add required/optional fields from schema that are missing
|
|
|
|
|
@@ -306,17 +325,24 @@ class ConfigModal(ModalScreen):
|
|
|
|
|
label_text += " *"
|
|
|
|
|
|
|
|
|
|
default_val = str(field_def.get("default") or "")
|
|
|
|
|
inp_id = f"item-{idx}"
|
|
|
|
|
self._input_id_map[inp_id] = key
|
|
|
|
|
inp = Input(value=default_val, id=inp_id, classes="config-input")
|
|
|
|
|
idx += 1
|
|
|
|
|
if field_def.get("secret"):
|
|
|
|
|
inp.password = True
|
|
|
|
|
if field_def.get("placeholder"):
|
|
|
|
|
inp.placeholder = field_def.get("placeholder")
|
|
|
|
|
choices = field_def.get("choices")
|
|
|
|
|
|
|
|
|
|
container.mount(Label(label_text))
|
|
|
|
|
container.mount(inp)
|
|
|
|
|
inp_id = f"item-{idx}"
|
|
|
|
|
self._input_id_map[inp_id] = key
|
|
|
|
|
|
|
|
|
|
if choices:
|
|
|
|
|
select_options = [(str(c), str(c)) for c in choices]
|
|
|
|
|
sel = Select(select_options, value=default_val, id=inp_id)
|
|
|
|
|
container.mount(sel)
|
|
|
|
|
else:
|
|
|
|
|
inp = Input(value=default_val, id=inp_id, classes="config-input")
|
|
|
|
|
if field_def.get("secret"):
|
|
|
|
|
inp.password = True
|
|
|
|
|
if field_def.get("placeholder"):
|
|
|
|
|
inp.placeholder = field_def.get("placeholder")
|
|
|
|
|
container.mount(inp)
|
|
|
|
|
idx += 1
|
|
|
|
|
|
|
|
|
|
# If it's a store, we might have required keys (legacy check fallback)
|
|
|
|
|
if item_type.startswith("store-"):
|
|
|
|
|
@@ -495,20 +521,15 @@ class ConfigModal(ModalScreen):
|
|
|
|
|
self.editing_item_name = ptype
|
|
|
|
|
self.refresh_view()
|
|
|
|
|
|
|
|
|
|
@on(Input.Changed)
|
|
|
|
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
|
|
|
if not event.input.id:
|
|
|
|
|
def _update_config_value(self, widget_id: str, value: Any) -> None:
|
|
|
|
|
if widget_id not in self._input_id_map:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
bid = event.input.id
|
|
|
|
|
if bid not in self._input_id_map:
|
|
|
|
|
return
|
|
|
|
|
key = self._input_id_map[widget_id]
|
|
|
|
|
|
|
|
|
|
key = self._input_id_map[bid]
|
|
|
|
|
|
|
|
|
|
if bid.startswith("global-"):
|
|
|
|
|
self.config_data[key] = event.value
|
|
|
|
|
elif bid.startswith("item-") and self.editing_item_name:
|
|
|
|
|
if widget_id.startswith("global-"):
|
|
|
|
|
self.config_data[key] = value
|
|
|
|
|
elif widget_id.startswith("item-") and self.editing_item_name:
|
|
|
|
|
it = str(self.editing_item_type or "")
|
|
|
|
|
inm = str(self.editing_item_name or "")
|
|
|
|
|
|
|
|
|
|
@@ -521,14 +542,26 @@ class ConfigModal(ModalScreen):
|
|
|
|
|
self.config_data["store"][stype] = {}
|
|
|
|
|
if inm not in self.config_data["store"][stype]:
|
|
|
|
|
self.config_data["store"][stype][inm] = {}
|
|
|
|
|
self.config_data["store"][stype][inm][key] = event.value
|
|
|
|
|
self.config_data["store"][stype][inm][key] = value
|
|
|
|
|
else:
|
|
|
|
|
# Provider or other top-level sections
|
|
|
|
|
if it not in self.config_data:
|
|
|
|
|
self.config_data[it] = {}
|
|
|
|
|
if inm not in self.config_data[it]:
|
|
|
|
|
self.config_data[it][inm] = {}
|
|
|
|
|
self.config_data[it][inm][key] = event.value
|
|
|
|
|
self.config_data[it][inm][key] = value
|
|
|
|
|
|
|
|
|
|
@on(Input.Changed)
|
|
|
|
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
|
|
|
if event.input.id:
|
|
|
|
|
self._update_config_value(event.input.id, event.value)
|
|
|
|
|
|
|
|
|
|
@on(Select.Changed)
|
|
|
|
|
def on_select_changed(self, event: Select.Changed) -> None:
|
|
|
|
|
if event.select.id:
|
|
|
|
|
# Select value can be the 'Select.BLANK' sentinel
|
|
|
|
|
if event.value != Select.BLANK:
|
|
|
|
|
self._update_config_value(event.select.id, event.value)
|
|
|
|
|
|
|
|
|
|
def save_all(self) -> None:
|
|
|
|
|
save_config(self.config_data, config_dir=self.workspace_root)
|
|
|
|
|
|