plugin_config_view.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. # -*- coding: utf-8 -*-
  2. """
  3. Plugin SubView: Konfiguration (F2).
  4. Zeigt und bearbeitet die Konfiguration des ausgewaehlten Plugins.
  5. Aehnlich wie die Server- und Satellite-Konfiguration mit Baumstruktur.
  6. """
  7. from __future__ import annotations
  8. from textual.app import ComposeResult
  9. from textual.containers import Vertical
  10. from textual.widgets import Static, Tree
  11. from textual.widgets.tree import TreeNode
  12. from trixy_core.tui.views.base import MainView
  13. from trixy_core.tui.config.field_registry import (
  14. FieldMeta,
  15. infer_field_meta,
  16. )
  17. from trixy_core.tui.widgets.config_editors import ConfigEditorPanel
  18. class PluginConfigView(MainView):
  19. """F2 im Plugin-Kontext: Plugin-Konfiguration."""
  20. TITLE = "Konfiguration"
  21. VIEW_ID = "plugin_config"
  22. def __init__(self, **kwargs) -> None:
  23. super().__init__(**kwargs)
  24. self._plugin_data: dict = {}
  25. self._config_data: dict = {}
  26. self._modified: dict[str, object] = {}
  27. self._selected_path: str = ""
  28. def set_plugin(self, plugin_data: dict) -> None:
  29. """Setzt die Plugin-Basisdaten."""
  30. self._plugin_data = plugin_data
  31. self._config_data = {}
  32. self._modified = {}
  33. def compose(self) -> ComposeResult:
  34. yield Tree("Plugin-Konfiguration", id="plugin-config-tree")
  35. yield ConfigEditorPanel(id="plugin-config-editor")
  36. yield Static("", id="plugin-config-status")
  37. async def on_activate(self) -> None:
  38. """Laedt die Plugin-Konfiguration."""
  39. await self._load_config()
  40. async def _load_config(self) -> None:
  41. """Laedt die Konfiguration des Plugins."""
  42. if not self.connection or not self._plugin_data:
  43. return
  44. name = self._plugin_data.get("name", "")
  45. if not name:
  46. self._set_status("Kein Plugin ausgewaehlt", error=True)
  47. return
  48. try:
  49. detail = await self.connection.request_plugin_detail(name)
  50. except Exception:
  51. self._set_status("Fehler beim Laden der Konfiguration", error=True)
  52. return
  53. if detail is None or detail.error:
  54. self._set_status(
  55. detail.error if detail else "Keine Antwort", error=True
  56. )
  57. return
  58. self._config_data = detail.config or {}
  59. self._build_tree()
  60. self._set_status("")
  61. def _build_tree(self) -> None:
  62. """Baut den Konfigurationsbaum auf."""
  63. tree = self.query_one("#plugin-config-tree", Tree)
  64. tree.clear()
  65. name = self._plugin_data.get("name", "Plugin")
  66. tree.root.set_label(f"[bold]{name}[/bold] — Konfiguration")
  67. self._add_tree_nodes(tree.root, self._config_data, "")
  68. tree.root.expand()
  69. def _add_tree_nodes(self, parent: TreeNode, data: dict, prefix: str) -> None:
  70. """Fuegt Knoten rekursiv zum Baum hinzu."""
  71. for key, value in sorted(data.items()):
  72. path = f"{prefix}.{key}" if prefix else key
  73. if isinstance(value, dict):
  74. node = parent.add(
  75. f"[bold]{key}[/bold]",
  76. data={"path": path, "is_section": True},
  77. )
  78. self._add_tree_nodes(node, value, path)
  79. node.expand()
  80. else:
  81. if path in self._modified:
  82. display = f"[yellow]{key}[/yellow] = [yellow]{value}[/yellow] *"
  83. else:
  84. display = f"{key} = {value}"
  85. parent.add_leaf(
  86. display,
  87. data={"path": path, "value": value, "is_section": False},
  88. )
  89. async def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
  90. """Wird aufgerufen wenn ein Baum-Knoten selektiert wird."""
  91. node_data = event.node.data
  92. if not node_data or node_data.get("is_section"):
  93. return
  94. path = node_data.get("path", "")
  95. value = node_data.get("value", "")
  96. self._selected_path = path
  97. # FieldMeta per Auto-Inferenz ermitteln
  98. meta = infer_field_meta(value)
  99. # Editor anzeigen
  100. editor = self.query_one("#plugin-config-editor", ConfigEditorPanel)
  101. await editor.show_editor(path, value, meta)
  102. self._set_status(f"Bearbeite: {path}")
  103. async def on_config_editor_panel_submitted(
  104. self, event: ConfigEditorPanel.Submitted
  105. ) -> None:
  106. """Verarbeitet validierte Werte vom Editor-Panel."""
  107. if event.error:
  108. self._set_status(f"Fehler: {event.error}", error=True)
  109. return
  110. if not self.connection or not self._plugin_data:
  111. return
  112. name = self._plugin_data.get("name", "")
  113. if not name:
  114. return
  115. try:
  116. result = await self.connection.write_plugin_config(
  117. plugin_name=name,
  118. key_path=event.path,
  119. value=event.validated_value,
  120. )
  121. if result and result.success:
  122. self._modified[event.path] = event.validated_value
  123. self._set_status("Gespeichert")
  124. await self._load_config()
  125. else:
  126. error = result.error if result else "Unbekannter Fehler"
  127. self._set_status(f"Fehler: {error}", error=True)
  128. except Exception as e:
  129. self._set_status(f"Fehler: {e}", error=True)
  130. def _set_status(self, text: str, error: bool = False) -> None:
  131. """Setzt die Statuszeile."""
  132. status = self.query_one("#plugin-config-status", Static)
  133. if error:
  134. status.update(f"[red]{text}[/red]")
  135. elif text:
  136. status.update(f"[green]{text}[/green]")
  137. else:
  138. status.update("")