jj
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
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, Select
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from textual import on, work
|
||||
from typing import Any
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Static, Button, Input, Label, ListView, ListItem, Rule, Select
|
||||
|
||||
from SYS.config import load_config, save_config, reload_config, global_config
|
||||
from SYS.logger import log
|
||||
from Store.registry import _discover_store_classes, _required_keys_for
|
||||
from ProviderCore.registry import list_providers
|
||||
from TUI.modalscreen.matrix_room_picker import MatrixRoomPicker
|
||||
from TUI.modalscreen.selection_modal import SelectionModal
|
||||
|
||||
class ConfigModal(ModalScreen):
|
||||
@@ -117,6 +120,8 @@ class ConfigModal(ModalScreen):
|
||||
self.editing_item_name = None
|
||||
self._button_id_map = {}
|
||||
self._input_id_map = {}
|
||||
self._matrix_status: Optional[Static] = None
|
||||
self._matrix_test_running = False
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="config-container"):
|
||||
@@ -491,13 +496,28 @@ class ConfigModal(ModalScreen):
|
||||
inp_id = f"item-{idx}"
|
||||
self._input_id_map[inp_id] = rk
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
row.mount(Input(value="", id=inp_id, classes="config-input"))
|
||||
row.mount(Button("Paste", id=f"paste-{inp_id}", classes="paste-btn"))
|
||||
container.mount(row)
|
||||
idx += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if (
|
||||
item_type == "provider"
|
||||
and isinstance(item_name, str)
|
||||
and item_name.strip().lower() == "matrix"
|
||||
):
|
||||
container.mount(Rule())
|
||||
container.mount(Label("Matrix helpers", classes="config-label"))
|
||||
status = Static("Set homeserver + token, then test before saving", id="matrix-status")
|
||||
container.mount(status)
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
row.mount(Button("Test connection", id="matrix-test-btn"))
|
||||
row.mount(Button("Choose default rooms", variant="primary", id="matrix-rooms-btn"))
|
||||
self._matrix_status = status
|
||||
|
||||
def create_field(self, name: str, value: Any, id: str) -> Vertical:
|
||||
# This method is now unused - we mount labels and inputs directly
|
||||
v = Vertical(classes="config-field")
|
||||
@@ -594,6 +614,10 @@ class ConfigModal(ModalScreen):
|
||||
except Exception:
|
||||
pass
|
||||
self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected)
|
||||
elif bid == "matrix-test-btn":
|
||||
self._request_matrix_test()
|
||||
elif bid == "matrix-rooms-btn":
|
||||
self._open_matrix_room_picker()
|
||||
elif bid.startswith("paste-"):
|
||||
# Programmatic paste button
|
||||
target_id = bid.replace("paste-", "")
|
||||
@@ -777,6 +801,94 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
self._update_config_value(widget_id, value)
|
||||
|
||||
def _get_matrix_provider_block(self) -> Dict[str, Any]:
|
||||
providers = self.config_data.get("provider")
|
||||
if not isinstance(providers, dict):
|
||||
return {}
|
||||
block = providers.get("matrix")
|
||||
return block if isinstance(block, dict) else {}
|
||||
|
||||
def _parse_matrix_rooms_value(self) -> List[str]:
|
||||
block = self._get_matrix_provider_block()
|
||||
raw = block.get("rooms")
|
||||
if isinstance(raw, (list, tuple, set)):
|
||||
return [str(item).strip() for item in raw if str(item).strip()]
|
||||
text = str(raw or "").strip()
|
||||
if not text:
|
||||
return []
|
||||
return [segment for segment in re.split(r"[\s,]+", text) if segment]
|
||||
|
||||
def _request_matrix_test(self) -> None:
|
||||
if self._matrix_test_running:
|
||||
return
|
||||
self._synchronize_inputs_to_config()
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update("Testing Matrix connection…")
|
||||
self._matrix_test_running = True
|
||||
self._matrix_test_background()
|
||||
|
||||
@work(thread=True)
|
||||
def _matrix_test_background(self) -> None:
|
||||
try:
|
||||
from Provider.matrix import Matrix
|
||||
|
||||
provider = Matrix(self.config_data)
|
||||
rooms = provider.list_rooms()
|
||||
self.app.call_from_thread(self._matrix_test_result, True, rooms, None)
|
||||
except Exception as exc:
|
||||
self.app.call_from_thread(self._matrix_test_result, False, [], str(exc))
|
||||
|
||||
def _matrix_test_result(
|
||||
self,
|
||||
success: bool,
|
||||
rooms: List[Dict[str, Any]],
|
||||
error: Optional[str],
|
||||
) -> None:
|
||||
self._matrix_test_running = False
|
||||
if success:
|
||||
msg = f"Matrix test succeeded ({len(rooms)} room(s))."
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(msg)
|
||||
self._open_matrix_room_picker(prefetched_rooms=rooms)
|
||||
else:
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(f"Matrix test failed: {error}")
|
||||
|
||||
def _open_matrix_room_picker(
|
||||
self,
|
||||
*,
|
||||
prefetched_rooms: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> None:
|
||||
existing = self._parse_matrix_rooms_value()
|
||||
self.app.push_screen(
|
||||
MatrixRoomPicker(
|
||||
self.config_data,
|
||||
existing=existing,
|
||||
rooms=prefetched_rooms,
|
||||
),
|
||||
callback=self.on_matrix_rooms_selected,
|
||||
)
|
||||
|
||||
def on_matrix_rooms_selected(self, result: Any = None) -> None:
|
||||
if not isinstance(result, list):
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update("Room selection cancelled.")
|
||||
return
|
||||
cleaned: List[str] = []
|
||||
for item in result:
|
||||
candidate = str(item or "").strip()
|
||||
if candidate and candidate not in cleaned:
|
||||
cleaned.append(candidate)
|
||||
if not cleaned:
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update("No default rooms were saved.")
|
||||
return
|
||||
matrix_block = self.config_data.setdefault("provider", {}).setdefault("matrix", {})
|
||||
matrix_block["rooms"] = ", ".join(cleaned)
|
||||
if self._matrix_status:
|
||||
self._matrix_status.update(f"Saved {len(cleaned)} default room(s).")
|
||||
self.refresh_view()
|
||||
|
||||
@on(Input.Changed)
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
if event.input.id:
|
||||
|
||||
197
TUI/modalscreen/matrix_room_picker.py
Normal file
197
TUI/modalscreen/matrix_room_picker.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Static, Button, Checkbox
|
||||
from textual import work
|
||||
|
||||
|
||||
class MatrixRoomPicker(ModalScreen[List[str]]):
|
||||
"""Modal that lists Matrix rooms and returns selected defaults."""
|
||||
|
||||
CSS = """
|
||||
MatrixRoomPicker {
|
||||
align: center middle;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
#matrix-room-picker {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
background: $panel;
|
||||
border: thick $primary;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
#matrix-room-picker-hint {
|
||||
text-style: italic;
|
||||
color: $text-muted;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#matrix-room-scroll {
|
||||
height: 1fr;
|
||||
border: solid $surface;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#matrix-room-checklist {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.matrix-room-row {
|
||||
border-bottom: solid $surface;
|
||||
padding: 1 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 {
|
||||
content-align: left middle;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.matrix-room-id {
|
||||
content-align: left middle;
|
||||
text-style: dim;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#matrix-room-actions {
|
||||
height: 3;
|
||||
align: right middle;
|
||||
}
|
||||
|
||||
#matrix-room-status {
|
||||
height: 3;
|
||||
text-style: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Dict[str, Any],
|
||||
*,
|
||||
existing: Optional[List[str]] = None,
|
||||
rooms: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self._prefetched_rooms = rooms
|
||||
self._existing_ids = {str(r).strip() for r in (existing or []) if str(r).strip()}
|
||||
self._checkbox_map: Dict[str, str] = {}
|
||||
self._rooms: List[Dict[str, Any]] = []
|
||||
self._status_widget: Optional[Static] = None
|
||||
self._checklist: Optional[Vertical] = None
|
||||
self._save_button: Optional[Button] = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="matrix-room-picker"):
|
||||
yield Static("Matrix Default Rooms", classes="section-title")
|
||||
yield Static(
|
||||
"Choose rooms to keep in the sharing defaults and autocomplete.",
|
||||
id="matrix-room-picker-hint",
|
||||
)
|
||||
with ScrollableContainer(id="matrix-room-scroll"):
|
||||
yield Vertical(id="matrix-room-checklist")
|
||||
with Horizontal(id="matrix-room-actions"):
|
||||
yield Button("Cancel", variant="error", id="matrix-room-cancel")
|
||||
yield Button("Save defaults", variant="success", id="matrix-room-save")
|
||||
yield Static("Loading rooms...", id="matrix-room-status")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._status_widget = self.query_one("#matrix-room-status", Static)
|
||||
self._checklist = self.query_one("#matrix-room-checklist", Vertical)
|
||||
self._save_button = self.query_one("#matrix-room-save", Button)
|
||||
if self._save_button:
|
||||
self._save_button.disabled = True
|
||||
if self._prefetched_rooms is not None:
|
||||
self._apply_room_results(self._prefetched_rooms, None)
|
||||
else:
|
||||
if self._status_widget:
|
||||
self._set_status("Loading rooms...")
|
||||
self._load_rooms_background()
|
||||
|
||||
def _set_status(self, text: str) -> None:
|
||||
if self._status_widget:
|
||||
self._status_widget.update(text)
|
||||
|
||||
def _render_rooms(self, rooms: List[Dict[str, Any]]) -> None:
|
||||
if not self._checklist:
|
||||
return
|
||||
for child in list(self._checklist.children):
|
||||
child.remove()
|
||||
self._checkbox_map.clear()
|
||||
self._rooms = rooms
|
||||
if not rooms:
|
||||
self._set_status("Matrix returned no rooms.")
|
||||
if self._save_button:
|
||||
self._save_button.disabled = True
|
||||
return
|
||||
for idx, room in enumerate(rooms):
|
||||
room_id = str(room.get("room_id") or "").strip()
|
||||
name = str(room.get("name") or "").strip()
|
||||
checkbox_id = f"matrix-room-{idx}"
|
||||
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"))
|
||||
row = Horizontal(classes="matrix-room-row")
|
||||
self._checklist.mount(row)
|
||||
row.mount(checkbox)
|
||||
row.mount(info)
|
||||
self._set_status("Loaded rooms. Select one or more and save.")
|
||||
if self._save_button:
|
||||
self._save_button.disabled = False
|
||||
|
||||
@work(thread=True)
|
||||
def _load_rooms_background(self) -> None:
|
||||
try:
|
||||
from Provider.matrix import Matrix
|
||||
provider = Matrix(self.config)
|
||||
rooms = provider.list_rooms()
|
||||
self.app.call_from_thread(self._apply_room_results, rooms, None)
|
||||
except Exception as exc:
|
||||
self.app.call_from_thread(self._apply_room_results, [], str(exc))
|
||||
|
||||
def _apply_room_results(self, rooms: List[Dict[str, Any]], error: Optional[str]) -> None:
|
||||
if error:
|
||||
self._set_status(f"Failed to load Matrix rooms: {error}")
|
||||
if self._save_button:
|
||||
self._save_button.disabled = True
|
||||
return
|
||||
self._render_rooms(rooms)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "matrix-room-cancel":
|
||||
self.dismiss([])
|
||||
elif event.button.id == "matrix-room-save":
|
||||
selected: List[str] = []
|
||||
for checkbox_id, room_id in self._checkbox_map.items():
|
||||
try:
|
||||
cb = self.query_one(f"#{checkbox_id}", Checkbox)
|
||||
if cb.value and room_id:
|
||||
selected.append(room_id)
|
||||
except Exception:
|
||||
pass
|
||||
self.dismiss(selected)
|
||||
Reference in New Issue
Block a user