j
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from textual import on, work
|
||||
@@ -122,6 +123,20 @@ class ConfigModal(ModalScreen):
|
||||
self._input_id_map = {}
|
||||
self._matrix_status: Optional[Static] = None
|
||||
self._matrix_test_running = False
|
||||
self._editor_snapshot: Optional[Dict[str, Any]] = None
|
||||
|
||||
def _capture_editor_snapshot(self) -> None:
|
||||
self._editor_snapshot = deepcopy(self.config_data)
|
||||
|
||||
def _revert_unsaved_editor_changes(self) -> None:
|
||||
if self._editor_snapshot is not None:
|
||||
self.config_data = deepcopy(self._editor_snapshot)
|
||||
self._editor_snapshot = None
|
||||
|
||||
def _editor_has_changes(self) -> bool:
|
||||
if self._editor_snapshot is None:
|
||||
return True
|
||||
return self.config_data != self._editor_snapshot
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="config-container"):
|
||||
@@ -541,8 +556,10 @@ class ConfigModal(ModalScreen):
|
||||
if not bid: return
|
||||
|
||||
if bid == "cancel-btn":
|
||||
self._revert_unsaved_editor_changes()
|
||||
self.dismiss()
|
||||
elif bid == "back-btn":
|
||||
self._revert_unsaved_editor_changes()
|
||||
self.editing_item_name = None
|
||||
self.editing_item_type = None
|
||||
self.refresh_view()
|
||||
@@ -550,6 +567,9 @@ class ConfigModal(ModalScreen):
|
||||
self._synchronize_inputs_to_config()
|
||||
if not self.validate_current_editor():
|
||||
return
|
||||
if self.editing_item_name and not self._editor_has_changes():
|
||||
self.notify("No changes to save", severity="warning")
|
||||
return
|
||||
try:
|
||||
saved = self.save_all()
|
||||
msg = f"Configuration saved ({saved} entries)"
|
||||
@@ -560,11 +580,13 @@ class ConfigModal(ModalScreen):
|
||||
self.editing_item_name = None
|
||||
self.editing_item_type = None
|
||||
self.refresh_view()
|
||||
self._editor_snapshot = None
|
||||
except Exception as exc:
|
||||
self.notify(f"Save failed: {exc}", severity="error", timeout=10)
|
||||
elif bid in self._button_id_map:
|
||||
action, itype, name = self._button_id_map[bid]
|
||||
if action == "edit":
|
||||
self._capture_editor_snapshot()
|
||||
self.editing_item_type = itype
|
||||
self.editing_item_name = name
|
||||
self.refresh_view()
|
||||
@@ -656,6 +678,8 @@ class ConfigModal(ModalScreen):
|
||||
if not stype:
|
||||
return
|
||||
|
||||
self._capture_editor_snapshot()
|
||||
|
||||
existing_names: set[str] = set()
|
||||
store_block = self.config_data.get("store")
|
||||
if isinstance(store_block, dict):
|
||||
@@ -704,6 +728,7 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
def on_provider_type_selected(self, ptype: str) -> None:
|
||||
if not ptype: return
|
||||
self._capture_editor_snapshot()
|
||||
if "provider" not in self.config_data:
|
||||
self.config_data["provider"] = {}
|
||||
|
||||
@@ -738,51 +763,72 @@ class ConfigModal(ModalScreen):
|
||||
return
|
||||
|
||||
key = self._input_id_map[widget_id]
|
||||
raw_value = value
|
||||
is_blank_string = isinstance(raw_value, str) and not raw_value.strip()
|
||||
|
||||
existing_value: Any = None
|
||||
item_type = str(self.editing_item_type or "")
|
||||
item_name = str(self.editing_item_name or "")
|
||||
|
||||
if widget_id.startswith("global-"):
|
||||
existing_value = self.config_data.get(key)
|
||||
elif widget_id.startswith("item-") and item_name:
|
||||
if item_type.startswith("store-"):
|
||||
stype = item_type.replace("store-", "")
|
||||
store_block = self.config_data.get("store")
|
||||
if isinstance(store_block, dict):
|
||||
type_block = store_block.get(stype)
|
||||
if isinstance(type_block, dict):
|
||||
section = type_block.get(item_name)
|
||||
if isinstance(section, dict):
|
||||
existing_value = section.get(key)
|
||||
else:
|
||||
section_block = self.config_data.get(item_type)
|
||||
if isinstance(section_block, dict):
|
||||
section = section_block.get(item_name)
|
||||
if isinstance(section, dict):
|
||||
existing_value = section.get(key)
|
||||
|
||||
if is_blank_string and existing_value is None:
|
||||
return
|
||||
|
||||
# Try to preserve boolean/integer types
|
||||
processed_value = value
|
||||
if isinstance(value, str):
|
||||
low = value.lower()
|
||||
processed_value = raw_value
|
||||
if isinstance(raw_value, str):
|
||||
low = raw_value.lower()
|
||||
if low == "true":
|
||||
processed_value = True
|
||||
elif low == "false":
|
||||
processed_value = False
|
||||
elif value.isdigit():
|
||||
processed_value = int(value)
|
||||
elif raw_value.isdigit():
|
||||
processed_value = int(raw_value)
|
||||
|
||||
if widget_id.startswith("global-"):
|
||||
self.config_data[key] = processed_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 "")
|
||||
|
||||
# Handle nested store structure
|
||||
if it.startswith("store-"):
|
||||
stype = it.replace("store-", "")
|
||||
elif widget_id.startswith("item-") and item_name:
|
||||
if item_type.startswith("store-"):
|
||||
stype = item_type.replace("store-", "")
|
||||
if "store" not in self.config_data:
|
||||
self.config_data["store"] = {}
|
||||
if stype not in self.config_data["store"]:
|
||||
self.config_data["store"][stype] = {}
|
||||
if inm not in self.config_data["store"][stype]:
|
||||
self.config_data["store"][stype][inm] = {}
|
||||
if item_name not in self.config_data["store"][stype]:
|
||||
self.config_data["store"][stype][item_name] = {}
|
||||
|
||||
# Special case: Renaming the store via the NAME field
|
||||
if key.upper() == "NAME" and processed_value and str(processed_value) != inm:
|
||||
new_inm = str(processed_value)
|
||||
# Move the whole dictionary to the new key
|
||||
self.config_data["store"][stype][new_inm] = self.config_data["store"][stype].pop(inm)
|
||||
# Update editing_item_name so further changes to this screen hit the new key
|
||||
self.editing_item_name = new_inm
|
||||
inm = new_inm
|
||||
if key.upper() == "NAME" and processed_value and str(processed_value) != item_name:
|
||||
new_name = str(processed_value)
|
||||
self.config_data["store"][stype][new_name] = self.config_data["store"][stype].pop(item_name)
|
||||
self.editing_item_name = new_name
|
||||
item_name = new_name
|
||||
|
||||
self.config_data["store"][stype][inm][key] = processed_value
|
||||
self.config_data["store"][stype][item_name][key] = processed_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] = processed_value
|
||||
if item_type not in self.config_data:
|
||||
self.config_data[item_type] = {}
|
||||
if item_name not in self.config_data[item_type]:
|
||||
self.config_data[item_type][item_name] = {}
|
||||
self.config_data[item_type][item_name][key] = processed_value
|
||||
|
||||
def _synchronize_inputs_to_config(self) -> None:
|
||||
"""Capture current input/select values before saving."""
|
||||
@@ -823,7 +869,17 @@ class ConfigModal(ModalScreen):
|
||||
return
|
||||
self._synchronize_inputs_to_config()
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update("Testing Matrix connection…")
|
||||
self._matrix_status.update("Saving configuration before testing…")
|
||||
try:
|
||||
entries = save_config(self.config_data)
|
||||
except Exception as exc:
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(f"Saving configuration failed: {exc}")
|
||||
self._matrix_test_running = False
|
||||
return
|
||||
self.config_data = reload_config()
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(f"Saved configuration ({entries} entries). Testing Matrix connection…")
|
||||
self._matrix_test_running = True
|
||||
self._matrix_test_background()
|
||||
|
||||
@@ -885,8 +941,16 @@ class ConfigModal(ModalScreen):
|
||||
return
|
||||
matrix_block = self.config_data.setdefault("provider", {}).setdefault("matrix", {})
|
||||
matrix_block["rooms"] = ", ".join(cleaned)
|
||||
try:
|
||||
entries = save_config(self.config_data)
|
||||
except Exception as exc:
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(f"Saving default rooms failed: {exc}")
|
||||
return
|
||||
self.config_data = reload_config()
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(f"Saved {len(cleaned)} default room(s).")
|
||||
status = f"Saved {len(cleaned)} default room(s) ({entries} rows persisted)."
|
||||
self._matrix_status.update(status)
|
||||
self.refresh_view()
|
||||
|
||||
@on(Input.Changed)
|
||||
|
||||
@@ -7,6 +7,7 @@ from textual.containers import Container, Horizontal, ScrollableContainer, Verti
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Static, Button, Checkbox
|
||||
from textual import work
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
class MatrixRoomPicker(ModalScreen[List[str]]):
|
||||
@@ -45,30 +46,13 @@ class MatrixRoomPicker(ModalScreen[List[str]]):
|
||||
|
||||
.matrix-room-row {
|
||||
border-bottom: solid $surface;
|
||||
padding: 1 0;
|
||||
padding: 0.5 0;
|
||||
align: left middle;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
.matrix-room-checkbox {
|
||||
width: 3;
|
||||
padding: 0;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
.matrix-room-meta {
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
.matrix-room-name {
|
||||
padding-left: 1;
|
||||
content-align: left middle;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.matrix-room-id {
|
||||
content-align: left middle;
|
||||
text-style: dim;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#matrix-room-actions {
|
||||
@@ -149,17 +133,19 @@ class MatrixRoomPicker(ModalScreen[List[str]]):
|
||||
checkbox = Checkbox(
|
||||
"",
|
||||
id=checkbox_id,
|
||||
classes="matrix-room-checkbox",
|
||||
value=bool(room_id and room_id in self._existing_ids),
|
||||
)
|
||||
self._checkbox_map[checkbox_id] = room_id
|
||||
info = Vertical(classes="matrix-room-meta")
|
||||
info.mount(Static(name or room_id or "Matrix Room", classes="matrix-room-name"))
|
||||
info.mount(Static(room_id or "(no id)", classes="matrix-room-id"))
|
||||
|
||||
label = Text(name or room_id or "Matrix Room")
|
||||
label.stylize("bold")
|
||||
label.append("\n")
|
||||
label.append(room_id or "(no id)", style="dim")
|
||||
|
||||
row = Horizontal(classes="matrix-room-row")
|
||||
self._checklist.mount(row)
|
||||
row.mount(checkbox)
|
||||
row.mount(info)
|
||||
row.mount(Static(label, classes="matrix-room-meta"))
|
||||
self._set_status("Loaded rooms. Select one or more and save.")
|
||||
if self._save_button:
|
||||
self._save_button.disabled = False
|
||||
|
||||
Reference in New Issue
Block a user