cct-cli 0.7.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1229 @@
1
+ """
2
+ CAT Customization — Responsive Professional Redesign (v0.8.3)
3
+ No hover-auto-start: every change is explicit click / Apply / Toggle.
4
+ Card layout, lightweight per-panel editing for performance.
5
+ """
6
+
7
+ if __name__ == "__main__":
8
+ print("This is a library file and is not meant to be run directly.")
9
+ import sys
10
+ sys.exit(1)
11
+
12
+ import time
13
+
14
+ TEXTUAL_AVAILABLE = True
15
+ try:
16
+ from textual.screen import Screen
17
+ from textual.containers import Vertical, Horizontal, ScrollableContainer
18
+ from textual.widgets import Static, Button, Input, Switch, Select
19
+ from textual.binding import Binding
20
+ try:
21
+ from textual.widgets import TextArea
22
+ _HAS_TEXTAREA = True
23
+ except Exception:
24
+ TextArea = None
25
+ _HAS_TEXTAREA = False
26
+ except Exception:
27
+ TEXTUAL_AVAILABLE = False
28
+ _HAS_TEXTAREA = False
29
+
30
+ if TEXTUAL_AVAILABLE:
31
+ try:
32
+ from .. import customization as cust
33
+ from ..customization import COMPONENT_CONSTRAINTS
34
+ except Exception:
35
+ cust = None
36
+ COMPONENT_CONSTRAINTS = {}
37
+ try:
38
+ from . import theme_css
39
+ if hasattr(theme_css, "_patch_rich_divide_line"):
40
+ theme_css._patch_rich_divide_line()
41
+ except Exception:
42
+ theme_css = None
43
+
44
+ def _hex(role):
45
+ try:
46
+ return theme_css.current_hex(role) if theme_css else "#888888"
47
+ except Exception:
48
+ return "#888888"
49
+
50
+ _NAV_SECTIONS = [
51
+ ("OVERVIEW", [("overview", "◎ Overview")]),
52
+ ("LAYOUT", [("layout", "▦ Order"), ("position", "↔ Position"), ("size", "⧉ Size"), ("docking", "⚓ Docking")]),
53
+ ("APPEARANCE", [("appearance", "✎ Appearance"), ("buttons", "⬢ Buttons"), ("icons", "◇ Icons"), ("header", "⬔ Header"), ("menu", "☰ Menu"), ("spacing", "⟷ Spacing")]),
54
+ ("BEHAVIOR", [("visibility", "◉ Visibility")]),
55
+ ("WORKSPACE", [("profiles", "★ Profiles"), ("editor", "⬙ Editor"), ("advanced", "⌘ Advanced"), ("reset", "↺ Reset")]),
56
+ ]
57
+ _HINTS = {
58
+ "overview": "Overview · select panel below then edit in its section",
59
+ "layout": "Order defines visual sequence · use ↑ ↓",
60
+ "position": "Choose panel then click position · explicit Apply",
61
+ "size": "Choose panel then set W/H · Apply",
62
+ "docking": "Docked inline · Floating overlay",
63
+ "appearance": "Appearance overview · jump to details",
64
+ "buttons": "Shape · size · radius · padding",
65
+ "icons": "Size · visibility",
66
+ "header": "Height · logo · title",
67
+ "menu": "Menu order & visibility",
68
+ "spacing": "Padding · margin · borders",
69
+ "visibility": "Toggle any region",
70
+ "profiles": "Save / load layouts",
71
+ "editor": "Visual preview · click to cycle",
72
+ "advanced": "Raw JSON · Save to apply",
73
+ "reset": "Restore defaults",
74
+ }
75
+ _PANEL_IDS = ["sidebar", "chat", "code_editor", "terminal", "file_explorer", "header", "preview", "activity_bar", "secondary_panel", "editor"]
76
+ _POSITION_OPTS = ["left", "right", "center", "top", "bottom", "floating", "docked"]
77
+ _BUTTON_SHAPES = ["rounded", "pill", "square", "minimal", "ghost", "outlined", "filled", "compact", "large"]
78
+ _BUTTON_SIZES = ["small", "medium", "large", "compact"]
79
+ _ICON_SIZES = ["small", "medium", "large"]
80
+
81
+ def _safe_query(widget, selector, typ=None):
82
+ try:
83
+ if typ:
84
+ return widget.query_one(selector, typ)
85
+ return widget.query_one(selector)
86
+ except Exception:
87
+ return None
88
+
89
+ class _Preview(Static):
90
+ allow_select = False
91
+
92
+ def __init__(self, manager, selected="sidebar"):
93
+ super().__init__("", id="cust-preview")
94
+ self._manager = manager
95
+ self._selected = selected or "sidebar"
96
+ self.allow_select = False
97
+
98
+ def on_mount(self):
99
+ self.refresh_preview()
100
+
101
+ def on_mouse_down(self, event):
102
+ try:
103
+ event.stop()
104
+ event.prevent_default()
105
+ except Exception:
106
+ pass
107
+
108
+ def refresh_preview(self):
109
+ try:
110
+ layout = self._manager.get_layout()
111
+ w = 50
112
+ lines = []
113
+ header = layout.get("header", {})
114
+ if header.get("visible", True):
115
+ sel = "● " if self._selected == "header" else " "
116
+ h_h = header.get("height", 3)
117
+ lines.append("┌" + "─" * w + "┐")
118
+ lines.append("│" + f"{sel}HEADER (height: {h_h})".center(w) + "│")
119
+
120
+ panes = []
121
+ seen = set()
122
+ order_list = sorted([v for v in layout.values() if v.get("visible", True)], key=lambda c: c.get("order", 0))
123
+ for p in order_list:
124
+ pid = p["id"]
125
+ if pid in ("header", "terminal"):
126
+ continue
127
+ canon = "code_editor" if pid in ("editor", "code_editor") else ("sidebar" if pid in ("sidebar", "file_explorer") else pid)
128
+ if canon in seen:
129
+ continue
130
+ seen.add(canon)
131
+ panes.append(p)
132
+
133
+ label_map = {"sidebar": "SIDEBAR", "chat": "CHAT", "code_editor": "EDITOR", "file_explorer": "FILES"}
134
+ n = len(panes)
135
+ if n > 0:
136
+ lines.append("├" + "─" * w + "┤")
137
+ col_w = (w - (n - 1)) // n
138
+ rem = (w - (n - 1)) % n
139
+ widths = [col_w + (1 if i < rem else 0) for i in range(n)]
140
+
141
+ row1_parts = []
142
+ row2_parts = []
143
+ for i, p in enumerate(panes):
144
+ pw = widths[i]
145
+ lab = label_map.get(p["id"], p["id"][:8].upper())
146
+ is_sel = (p["id"] == self._selected or ("code_editor" in (p["id"], self._selected) and "editor" in (p["id"], self._selected)) or ("sidebar" in (p["id"], self._selected) and "file_explorer" in (p["id"], self._selected)))
147
+ sel_mark = "● " if is_sel else ""
148
+ pos_str = f"({p.get('position', '?')})"
149
+ row1_parts.append(f"{sel_mark}{lab}".center(pw))
150
+ row2_parts.append(pos_str.center(pw))
151
+
152
+ lines.append("│" + "│".join(row1_parts) + "│")
153
+ lines.append("│" + "│".join(row2_parts) + "│")
154
+
155
+ term = layout.get("terminal", {})
156
+ if term.get("visible", True):
157
+ sel = "● " if self._selected == "terminal" else " "
158
+ lines.append("├" + "─" * w + "┤")
159
+ lines.append("│" + f"{sel}TERMINAL ({term.get('position', 'bottom')})".center(w) + "│")
160
+ lines.append("└" + "─" * w + "┘")
161
+ self.update("\n".join(lines))
162
+ except Exception:
163
+ self.update("Preview unavailable")
164
+
165
+ def on_click(self, event):
166
+ try:
167
+ event.stop()
168
+ event.prevent_default()
169
+ except Exception:
170
+ pass
171
+ try:
172
+ canonical_ids = ["sidebar", "chat", "code_editor", "terminal", "header"]
173
+ cur = self._selected
174
+ if cur in ("editor", "code_editor"):
175
+ cur = "code_editor"
176
+ elif cur in ("file_explorer", "sidebar"):
177
+ cur = "sidebar"
178
+ if cur in canonical_ids:
179
+ idx = canonical_ids.index(cur)
180
+ self._selected = canonical_ids[(idx + 1) % len(canonical_ids)]
181
+ else:
182
+ self._selected = "sidebar"
183
+ self.refresh_preview()
184
+ try:
185
+ if hasattr(self.screen, "_on_preview_select"):
186
+ self.screen._on_preview_select(self._selected)
187
+ except Exception:
188
+ pass
189
+ except Exception:
190
+ pass
191
+
192
+ class CustomizationPanel(Screen):
193
+ allow_select = False
194
+ CSS = """
195
+ CustomizationPanel { align: center middle; background: $app-background 70%; }
196
+ #cust-box { width: 98; max-width: 98%; height: 92%; max-height: 96%; background: $surface; border: round $border; padding: 0; layout: vertical; overflow: hidden; }
197
+ #cust-box Button { min-height: 3; height: 3; min-width: 10; padding: 0 1; }
198
+ #cust-box Button.-style-default, #cust-box Button:ansi.-style-default { min-height: 3; height: 3; min-width: 10; padding: 0 1; }
199
+ #cust-titlebar { height: 3; min-height: 3; padding: 0 2; border-bottom: solid $border; layout: horizontal; align: center middle; }
200
+ #cust-title { width: 1fr; text-style: bold; color: $text; }
201
+ #cust-subtitle { width: auto; color: $text-faint; padding-right: 1; }
202
+ #cust-close { min-width: 3; width: 3; height: 1; min-height: 1; }
203
+ #cust-disabled { height: auto; padding: 2 2; align: center middle; }
204
+ .cust-disabled-msg { color: $warning; text-style: bold; height: auto; text-align: center; }
205
+ .cust-disabled-hint { color: $text-faint; height: auto; text-align: center; padding-top: 1; }
206
+ #cust-disabled-actions { height: auto; padding-top: 1; layout: horizontal; align: center middle; }
207
+ #cust-disabled-actions Button { margin: 0 1; min-width: 10; padding: 0 1; }
208
+ #cust-body { height: 1fr; min-height: 0; layout: horizontal; }
209
+ #cust-sidebar { width: 28; min-width: 24; height: 100%; border-right: solid $border; layout: vertical; background: $surface; }
210
+ #cust-search { height: 3; min-height: 3; padding: 0 1; border-bottom: solid $border; }
211
+ #cust-search Input { width: 1fr; height: 3; background: $app-background; border: round $border; }
212
+ #cust-search Input:focus { border: round $accent; }
213
+ #cust-sidebar-inner { width: 100%; height: 1fr; overflow-y: auto; scrollbar-gutter: stable; }
214
+ .cust-nav-section { color: $text-faint; height: 1; padding: 1 1 0 1; text-style: bold; }
215
+ .cust-nav-btn { width: 100%; height: 1; min-height: 1; padding: 0 1; text-align: left; background: transparent; border: none; color: $text-muted; }
216
+ .cust-nav-btn-sel { background: $accent 14%; color: $accent; text-style: bold; border-left: thick $accent; }
217
+ #cust-content { width: 1fr; height: 1fr; min-height: 10; padding: 1 2; overflow-y: auto; overflow-x: auto; scrollbar-gutter: stable; background: $surface; }
218
+ .cust-card { background: $surface; border: round $border; padding: 1 2; margin-bottom: 1; width: 100%; height: auto; min-height: 6; }
219
+ .cust-card-title { color: $text; text-style: bold; height: 1; }
220
+ .cust-card-desc { color: $text-faint; height: auto; padding-bottom: 1; }
221
+ .cust-label { color: $text-muted; height: 1; min-height: 1; width: auto; min-width: 6; padding-right: 1; }
222
+ .cust-hint { color: $text-faint; height: auto; min-height: 1; padding: 0 0 1 0; }
223
+ .cust-row { height: auto; min-height: 3; layout: horizontal; align: center middle; padding: 0; margin-bottom: 1; width: 100%; overflow-x: auto; }
224
+ .cust-row Button { margin-right: 1; min-width: 10; width: auto; height: 3; min-height: 3; padding: 0 1; }
225
+ .cust-row Input { width: 10; min-width: 6; height: 3; min-height: 3; margin-right: 1; }
226
+ .cust-row Select { width: 18; min-width: 12; height: 3; min-height: 3; margin-right: 1; }
227
+ .cust-field-row { height: auto; min-height: 3; layout: horizontal; align: center middle; padding: 0; margin-bottom: 1; width: 100%; }
228
+ .cust-field-row Input { width: 1fr; min-width: 8; height: 3; min-height: 3; margin-right: 1; }
229
+ .cust-field-row Select { width: 1fr; min-width: 10; height: 3; min-height: 3; margin-right: 1; }
230
+ .cust-field-row Button { height: 3; min-height: 3; min-width: 10; padding: 0 1; }
231
+ .cust-visibility-row { height: auto; min-height: 2; layout: horizontal; align: center middle; border-bottom: solid $border 12%; padding: 1 0; }
232
+ .cust-visibility-label { width: 1fr; color: $text; }
233
+ .cust-drag-row { height: auto; min-height: 3; layout: horizontal; align: center middle; border-bottom: solid $border 60%; padding: 0 1; margin-bottom: 1; }
234
+ .cust-drag-label { width: 1fr; color: $text; }
235
+ #cust-preview { width: 100%; height: auto; padding: 0; background: $surface; color: $text-muted; margin-bottom: 1; }
236
+ #cust-actions { height: 4; min-height: 4; padding: 0 2; border-top: solid $border; layout: horizontal; align: center middle; background: $surface; }
237
+ #cust-actions-left { width: 1fr; layout: horizontal; }
238
+ #cust-actions-left Button { margin-right: 1; min-width: 12; height: 3; }
239
+ #cust-actions-right Button { margin-left: 1; min-width: 10; height: 3; }
240
+ #cust-status { height: 1; color: $text-faint; padding: 0 2; }
241
+ #cust-error { height: auto; color: $error; padding: 1 2; display: none; background: $surface; border: round $error; margin: 0 2; }
242
+ #cust-error.show { display: block; }
243
+ #cust-hint { color: $text-faint; height: 1; padding: 0 2; }
244
+ .cct-compact #cust-body { layout: vertical; }
245
+ .cct-compact #cust-sidebar { width: 100%; height: auto; max-height: 12; border-right: none; border-bottom: solid $border; }
246
+ .cct-compact #cust-content { width: 100%; height: 1fr; min-height: 14; }
247
+ .cct-compact .cust-row { layout: vertical; height: auto; }
248
+ .cct-compact .cust-row Button { width: 100%; margin-right: 0; margin-bottom: 1; }
249
+ .cct-compact .cust-row Input { width: 100%; margin-right: 0; margin-bottom: 1; }
250
+ .cct-compact .cust-row Select { width: 100%; margin-right: 0; margin-bottom: 1; }
251
+ .cct-compact .cust-field-row { layout: vertical; height: auto; }
252
+ .cct-compact .cust-field-row Input { width: 100%; margin-right: 0; margin-bottom: 1; }
253
+ .cct-compact .cust-field-row Select { width: 100%; margin-right: 0; margin-bottom: 1; }
254
+ .cct-compact .cust-field-row Button { width: 100%; margin-right: 0; margin-bottom: 1; }
255
+ .cct-compact .cust-visibility-row { layout: vertical; height: auto; }
256
+ .cct-compact .cust-visibility-row Switch { margin-top: 1; }
257
+ .cct-compact .cust-drag-row { layout: vertical; height: auto; padding: 1 1; }
258
+ .cct-compact .cust-drag-row Button { width: 100%; margin: 1 0 0 0; }
259
+ .cct-compact .cust-drag-label { width: 100%; margin-bottom: 1; }
260
+ .cct-compact #cust-actions { layout: vertical; height: auto; padding: 1 2; }
261
+ .cct-compact #cust-actions-left { width: 100%; layout: vertical; }
262
+ .cct-compact #cust-actions-left Button { width: 100%; margin: 0 0 1 0; }
263
+ .cct-compact #cust-actions-right { width: 100%; layout: vertical; margin-top: 1; }
264
+ .cct-compact #cust-actions-right Button { width: 100%; margin: 0; }
265
+ .cct-compact #cust-preview { height: auto; min-height: 8; }
266
+ """
267
+ BINDINGS = [Binding("escape", "cancel", "Cancel")]
268
+
269
+ def __init__(self):
270
+ super().__init__()
271
+ self._manager = cust.get_manager() if cust else None
272
+ self._selected_panel = "sidebar"
273
+ self._nav_state = "overview"
274
+ self._search_query = ""
275
+ self._last_search_query = ""
276
+ self._status_timer = None
277
+ self._size_panel = "sidebar"
278
+ self._pos_panel = "sidebar"
279
+ self._dock_panel = "sidebar"
280
+
281
+ def compose(self):
282
+ try:
283
+ from .. import extensions as _ext
284
+ if not _ext.is_enabled("customization"):
285
+ with Vertical(id="cust-box"):
286
+ with Horizontal(id="cust-titlebar"):
287
+ yield Static("CAT Customization", id="cust-title")
288
+ yield Button("✕", id="cust-close", classes="cct-ctrl")
289
+ with Vertical(id="cust-disabled"):
290
+ yield Static("Customization is disabled.", classes="cust-disabled-msg")
291
+ yield Static("Enable CAT Customization in Extensions to edit layout & appearance.", classes="cust-disabled-hint")
292
+ with Horizontal(id="cust-disabled-actions"):
293
+ yield Button("Open Extensions", id="cust-open-ext", variant="primary")
294
+ yield Button("Close", id="cust-close2")
295
+ return
296
+ except Exception:
297
+ pass
298
+ with Vertical(id="cust-box"):
299
+ with Horizontal(id="cust-titlebar"):
300
+ yield Static("CAT Customization", id="cust-title")
301
+ yield Static("explicit apply", id="cust-subtitle")
302
+ yield Button("✕", id="cust-close", classes="cct-ctrl")
303
+ yield Static("", id="cust-error")
304
+ with Horizontal(id="cust-body"):
305
+ with Vertical(id="cust-sidebar"):
306
+ with Horizontal(id="cust-search"):
307
+ yield Input(placeholder="Filter…", id="cust-search-input")
308
+ yield ScrollableContainer(id="cust-sidebar-inner")
309
+ with ScrollableContainer(id="cust-content"):
310
+ pass
311
+ yield Static("", id="cust-status")
312
+ with Horizontal(id="cust-actions"):
313
+ with Horizontal(id="cust-actions-left"):
314
+ yield Button("Reset Current", id="cust-reset-current", classes="cct-btn-sm")
315
+ yield Button("Reset All", id="cust-reset-all", variant="error", classes="cct-btn-sm")
316
+ with Horizontal(id="cust-actions-right"):
317
+ yield Button("Close", id="cust-close3", variant="primary", classes="cct-btn-sm")
318
+ yield Static("", id="cust-hint")
319
+
320
+ def on_mount(self):
321
+ if self.size.width < 88 or self.size.height < 28:
322
+ try:
323
+ self.query_one("#cust-box").add_class("cct-compact")
324
+ except Exception:
325
+ pass
326
+ self.call_after_refresh(self._rebuild_nav)
327
+ self.call_after_refresh(self._render_section_view)
328
+ try:
329
+ self.query_one("#cust-search-input", Input).focus()
330
+ except Exception:
331
+ pass
332
+
333
+ def on_resize(self, event):
334
+ try:
335
+ want = self.size.width < 88 or self.size.height < 28
336
+ box = self.query_one("#cust-box")
337
+ if want != box.has_class("cct-compact"):
338
+ box.set_class(want, "cct-compact")
339
+ except Exception:
340
+ pass
341
+
342
+ def _rebuild_nav(self):
343
+ try:
344
+ nav = self.query_one("#cust-sidebar-inner", ScrollableContainer)
345
+ # lightweight: update existing buttons' selected state without full rebuild if possible
346
+ # If nav is empty (first mount), do full rebuild
347
+ if len(list(nav.children)) == 0:
348
+ q = self._search_query.lower().strip()
349
+ for sec, items in _NAV_SECTIONS:
350
+ vis = [(k,l) for k,l in items if not q or q in k.lower() or q in l.lower()]
351
+ if not vis:
352
+ continue
353
+ try:
354
+ nav.mount(Static(sec, classes="cust-nav-section"))
355
+ except Exception:
356
+ pass
357
+ for k,l in vis:
358
+ btn = Button(l, id=f"cust-nav-{k}", classes="cust-nav-btn")
359
+ if k==self._nav_state:
360
+ btn.add_class("cust-nav-btn-sel")
361
+ try:
362
+ nav.mount(btn)
363
+ except Exception:
364
+ pass
365
+ else:
366
+ # update selected state only
367
+ for btn in nav.query(Button):
368
+ if btn.id and btn.id.startswith("cust-nav-"):
369
+ key = btn.id.replace("cust-nav-","")
370
+ btn.set_class(key==self._nav_state, "cust-nav-btn-sel")
371
+ # handle search filtering: only rebuild if search query changed and is active, or was cleared
372
+ cur_q = self._search_query.lower().strip()
373
+ last_q = getattr(self, "_last_search_query", "")
374
+ if cur_q != last_q and (cur_q or last_q):
375
+ self._last_search_query = cur_q
376
+ nav.remove_children()
377
+ for sec, items in _NAV_SECTIONS:
378
+ vis = [(k,l) for k,l in items if not cur_q or cur_q in k.lower() or cur_q in l.lower()]
379
+ if not vis:
380
+ continue
381
+ nav.mount(Static(sec, classes="cust-nav-section"))
382
+ for k,l in vis:
383
+ btn = Button(l, id=f"cust-nav-{k}", classes="cust-nav-btn")
384
+ if k==self._nav_state:
385
+ btn.add_class("cust-nav-btn-sel")
386
+ nav.mount(btn)
387
+ except Exception:
388
+ return
389
+ try:
390
+ self.query_one("#cust-hint", Static).update(_HINTS.get(self._nav_state, "Esc to close"))
391
+ except Exception:
392
+ pass
393
+
394
+ def _set_nav(self, key):
395
+ self._nav_state = key
396
+ self._rebuild_nav()
397
+ self._render_section_view()
398
+
399
+ def _render_section_view(self):
400
+ try:
401
+ c = self.query_one("#cust-content", ScrollableContainer)
402
+ c.remove_children()
403
+ except Exception:
404
+ return
405
+ h = getattr(self, f"_render_{self._nav_state}", None)
406
+ if h:
407
+ try:
408
+ h(c)
409
+ except Exception as e:
410
+ try:
411
+ c.mount(Static(f"Error: {e}", classes="cust-hint"))
412
+ except Exception:
413
+ pass
414
+
415
+ def _show_status(self, msg, style="success"):
416
+ try:
417
+ lbl = self.query_one("#cust-status", Static)
418
+ col = _hex(style) if style in ("success","warning","error") else _hex("success")
419
+ lbl.update(f"[{col}]{msg}[/]")
420
+ except Exception:
421
+ pass
422
+ def _show_error(self, msg):
423
+ try:
424
+ lbl = self.query_one("#cust-error", Static)
425
+ lbl.update(msg)
426
+ lbl.add_class("show")
427
+ self.set_timer(4, lambda: lbl.remove_class("show"))
428
+ except Exception:
429
+ pass
430
+ def _refresh_all(self):
431
+ self.call_after_refresh(self._render_section_view)
432
+ try:
433
+ for p in self.query(_Preview):
434
+ p.refresh_preview()
435
+ except Exception:
436
+ pass
437
+ def _on_preview_select(self, comp_id):
438
+ self._selected_panel = comp_id
439
+ self._pos_panel = comp_id
440
+ self._size_panel = comp_id
441
+ self._dock_panel = comp_id
442
+
443
+ # In-place updates for preview & overview/editor without tearing down the DOM:
444
+ try:
445
+ for hint in self.query(".cust-hint"):
446
+ if self._manager:
447
+ layout = self._manager.get_layout()
448
+ sel = layout.get(comp_id, {})
449
+ if self._nav_state == "editor":
450
+ hint.update(f"Selected: [b]{comp_id}[/]")
451
+ else:
452
+ hint.update(f"Selected: [b]{comp_id}[/] · {sel.get('position','?')} · {'Visible' if sel.get('visible',True) else 'Hidden'} · w:{sel.get('width','auto')} h:{sel.get('height','auto')}")
453
+ except Exception:
454
+ pass
455
+
456
+ try:
457
+ for pid in ("sidebar", "chat", "code_editor", "terminal"):
458
+ b = _safe_query(self, f"#cust-sel-{pid}", Button)
459
+ if b:
460
+ is_sel = (pid == comp_id or (pid == "code_editor" and comp_id == "editor") or (pid == "sidebar" and comp_id == "file_explorer"))
461
+ b.variant = "primary" if is_sel else "default"
462
+ except Exception:
463
+ pass
464
+
465
+ try:
466
+ for p in self.query(_Preview):
467
+ if p._selected != comp_id:
468
+ p._selected = comp_id
469
+ p.refresh_preview()
470
+ except Exception:
471
+ pass
472
+
473
+ if self._nav_state in ("layout", "position", "size"):
474
+ self.call_after_refresh(self._render_section_view)
475
+
476
+ def _live_apply(self):
477
+ # Refresh internal preview
478
+ try:
479
+ for p in self.query(_Preview):
480
+ p.refresh_preview()
481
+ except Exception:
482
+ pass
483
+ # Immediately update live UI underneath (live preview)
484
+ try:
485
+ if hasattr(self.app, "_apply_customization_layout"):
486
+ self.app._apply_customization_layout()
487
+ except Exception:
488
+ pass
489
+
490
+ def _card(self, parent, title, desc=None):
491
+ card = Vertical(classes="cust-card")
492
+ parent.mount(card)
493
+ card.mount(Static(title, classes="cust-card-title"))
494
+ if desc:
495
+ card.mount(Static(desc, classes="cust-card-desc"))
496
+ return card
497
+
498
+ def _render_overview(self, container):
499
+ card = self._card(container, "Overview", "Select a panel then edit in its section · explicit Apply only")
500
+ if self._manager:
501
+ layout = self._manager.get_layout()
502
+ sel = layout.get(self._selected_panel, {})
503
+ card.mount(Static(f"Selected: [b]{self._selected_panel}[/] · {sel.get('position','?')} · {'Visible' if sel.get('visible',True) else 'Hidden'} · w:{sel.get('width','auto')} h:{sel.get('height','auto')}", classes="cust-hint"))
504
+ card.mount(_Preview(self._manager, selected=self._selected_panel))
505
+ row = Horizontal(classes="cust-row")
506
+ card.mount(row)
507
+ for pid, label in [("sidebar", "Sidebar"), ("chat", "Chat"), ("code_editor", "Editor"), ("terminal", "Terminal")]:
508
+ b = Button(label, id=f"cust-sel-{pid}", classes="cct-btn-sm")
509
+ if pid == self._selected_panel or (pid == "code_editor" and self._selected_panel == "editor") or (pid == "sidebar" and self._selected_panel == "file_explorer"):
510
+ b.variant = "primary"
511
+ row.mount(b)
512
+
513
+ def _render_layout(self, container):
514
+ card = self._card(container, "Order", "Use ↑ ↓ to reorder · click to apply")
515
+ if self._manager:
516
+ ordered = self._manager.get_ordered_components()
517
+ seen = set()
518
+ idx = 0
519
+ for c in ordered:
520
+ cid = c["id"]
521
+ canon = "code_editor" if cid in ("editor", "code_editor") else ("sidebar" if cid in ("sidebar", "file_explorer") else cid)
522
+ if canon in seen:
523
+ continue
524
+ seen.add(canon)
525
+ idx += 1
526
+ r = Horizontal(classes="cust-drag-row")
527
+ card.mount(r)
528
+ disp_name = cid.replace("_", " ").title()
529
+ r.mount(Static(f"{idx}. {disp_name} ({c.get('position','?')})", classes="cust-drag-label"))
530
+ r.mount(Button("↑", id=f"cust-order-up-{cid}", classes="cct-btn-sm"))
531
+ r.mount(Button("↓", id=f"cust-order-down-{cid}", classes="cct-btn-sm"))
532
+ r.mount(Button("Hide" if c.get("visible",True) else "Show", id=f"cust-order-vis-{cid}", classes="cct-btn-sm"))
533
+
534
+ def _render_position(self, container):
535
+ card = self._card(container, "Position", "Choose panel then choose position · click to apply (no hover)")
536
+ row = Horizontal(classes="cust-row")
537
+ card.mount(row)
538
+ row.mount(Static("Panel:", classes="cust-label"))
539
+ for pid, label in [("sidebar", "Sidebar"), ("chat", "Chat"), ("code_editor", "Editor"), ("terminal", "Terminal")]:
540
+ b = Button(label, id=f"cust-pospick-{pid}", classes="cct-btn-sm")
541
+ if pid == self._pos_panel or (pid == "code_editor" and self._pos_panel == "editor") or (pid == "sidebar" and self._pos_panel == "file_explorer"):
542
+ b.variant = "primary"
543
+ row.mount(b)
544
+ cur = self._manager.get_layout().get(self._pos_panel, {}).get("position","left") if self._manager else "left"
545
+ card.mount(Static(f"Current: [b]{cur}[/] · Selected: [b]{self._pos_panel}[/]", classes="cust-hint"))
546
+ row2 = Horizontal(classes="cust-row")
547
+ card.mount(row2)
548
+ for pos in _POSITION_OPTS:
549
+ b = Button(pos.title(), id=f"cust-pos-{pos}", classes="cct-btn-sm")
550
+ if pos == cur:
551
+ b.variant = "primary"
552
+ row2.mount(b)
553
+
554
+ # header extra
555
+ card2 = self._card(container, "Header Alignment", "")
556
+ hdr = self._manager.get_layout().get("header", {}) if self._manager else {}
557
+ for key,opts in [("logo_position",["left","center","right"]), ("title_position",["left","center","right"])]:
558
+ cur2 = hdr.get(key,"left")
559
+ card2.mount(Static(key.replace("_"," ").title(), classes="cust-label"))
560
+ r = Horizontal(classes="cust-row")
561
+ card2.mount(r)
562
+ for opt in opts:
563
+ b = Button(opt.title(), id=f"cust-hdr-{key}-{opt}", classes="cct-btn-sm")
564
+ if opt == cur2:
565
+ b.variant = "primary"
566
+ r.mount(b)
567
+
568
+ def _render_size(self, container):
569
+ card = self._card(container, "Size", "Choose panel then set W/H · Apply to save")
570
+ row = Horizontal(classes="cust-row")
571
+ card.mount(row)
572
+ row.mount(Static("Panel:", classes="cust-label"))
573
+ for pid, label in [("sidebar", "Sidebar"), ("chat", "Chat"), ("code_editor", "Editor"), ("terminal", "Terminal")]:
574
+ b = Button(label, id=f"cust-sizepick-{pid}", classes="cct-btn-sm")
575
+ if pid == self._size_panel or (pid == "code_editor" and self._size_panel == "editor") or (pid == "sidebar" and self._size_panel == "file_explorer"):
576
+ b.variant = "primary"
577
+ row.mount(b)
578
+ if self._manager:
579
+ comp = self._manager.get_layout().get(self._size_panel, {})
580
+ constr = COMPONENT_CONSTRAINTS.get(self._size_panel, {})
581
+ card.mount(Static(f"Range {constr.get('min_width',12)}–{constr.get('max_width',80)} cols", classes="cust-hint"))
582
+ row2 = Horizontal(classes="cust-row")
583
+ card.mount(row2)
584
+ row2.mount(Static("W:", classes="cust-label"))
585
+ row2.mount(Input(value=str(comp.get("width") or ""), placeholder="auto", id=f"cust-w-{self._size_panel}"))
586
+ row2.mount(Static("H:", classes="cust-label"))
587
+ row2.mount(Input(value=str(comp.get("height") or ""), placeholder="auto", id=f"cust-h-{self._size_panel}"))
588
+ row2.mount(Button("Apply", id=f"cust-size-apply-{self._size_panel}", variant="primary", classes="cct-btn-sm"))
589
+ quick = Horizontal(classes="cust-row")
590
+ card.mount(quick)
591
+ quick.mount(Static("Quick:", classes="cust-label"))
592
+ for pct in [20,30,40]:
593
+ quick.mount(Button(f"{pct}%", id=f"cust-size-pct-{self._size_panel}-{pct}", classes="cct-btn-sm"))
594
+ quick.mount(Button("Auto", id=f"cust-size-auto-{self._size_panel}", classes="cct-btn-sm"))
595
+
596
+ def _render_docking(self, container):
597
+ card = self._card(container, "Docking", "Explicit click to set dock state")
598
+ row = Horizontal(classes="cust-row")
599
+ card.mount(row)
600
+ row.mount(Static("Panel:", classes="cust-label"))
601
+ for pid, label in [("sidebar", "Sidebar"), ("chat", "Chat"), ("code_editor", "Editor"), ("terminal", "Terminal")]:
602
+ b = Button(label, id=f"cust-dockpick-{pid}", classes="cct-btn-sm")
603
+ if pid == self._dock_panel or (pid == "code_editor" and self._dock_panel == "editor") or (pid == "sidebar" and self._dock_panel == "file_explorer"):
604
+ b.variant = "primary"
605
+ row.mount(b)
606
+ cur = self._manager.get_layout().get(self._dock_panel, {}).get("dock_state","docked") if self._manager else "docked"
607
+ card.mount(Static(f"Current: [b]{cur}[/]", classes="cust-hint"))
608
+ row2 = Horizontal(classes="cust-row")
609
+ card.mount(row2)
610
+ for opt in ["docked","floating"]:
611
+ b = Button(opt.title(), id=f"cust-dock-{self._dock_panel}-{opt}", classes="cct-btn-sm")
612
+ if opt == cur:
613
+ b.variant = "primary"
614
+ row2.mount(b)
615
+
616
+ def _render_appearance(self, container):
617
+ card = self._card(container, "Appearance", "Jump to sections · all edits require click")
618
+ row = Horizontal(classes="cust-row")
619
+ card.mount(row)
620
+ for sec,label in [("buttons","Buttons"),("icons","Icons"),("header","Header"),("menu","Menu"),("spacing","Spacing")]:
621
+ row.mount(Button(label, id=f"cust-jump-{sec}", classes="cct-btn-sm"))
622
+
623
+ def _render_buttons(self, container):
624
+ if not self._manager: return
625
+ btn = self._manager.get_styles().get("buttons", {})
626
+ card = self._card(container, "Buttons", f"Current {btn.get('shape','rounded')} · {btn.get('size','medium')}")
627
+ card.mount(Static("Shape", classes="cust-label"))
628
+ row = Horizontal(classes="cust-row")
629
+ card.mount(row)
630
+ for s in _BUTTON_SHAPES[:5]:
631
+ b = Button(s.title(), id=f"cust-btn-shape-{s}", classes="cct-btn-sm")
632
+ if s == btn.get("shape"): b.variant = "primary"
633
+ row.mount(b)
634
+ row_b = Horizontal(classes="cust-row")
635
+ card.mount(row_b)
636
+ for s in _BUTTON_SHAPES[5:]:
637
+ b = Button(s.title(), id=f"cust-btn-shape-{s}", classes="cct-btn-sm")
638
+ if s == btn.get("shape"): b.variant = "primary"
639
+ row_b.mount(b)
640
+ card.mount(Static("Size", classes="cust-label"))
641
+ row2 = Horizontal(classes="cust-row")
642
+ card.mount(row2)
643
+ for sz in _BUTTON_SIZES:
644
+ b = Button(sz.title(), id=f"cust-btn-size-{sz}", classes="cct-btn-sm")
645
+ if sz == btn.get("size"): b.variant = "primary"
646
+ row2.mount(b)
647
+ row3 = Horizontal(classes="cust-row")
648
+ card.mount(row3)
649
+ row3.mount(Static("Radius:", classes="cust-label")); row3.mount(Input(value=str(btn.get("border_radius",4)), id="cust-btn-radius"))
650
+ row3.mount(Static("Padding:", classes="cust-label")); row3.mount(Input(value=str(btn.get("padding",1)), id="cust-btn-padding"))
651
+ row3.mount(Button("Apply", id="cust-btn-apply", variant="primary", classes="cct-btn-sm"))
652
+
653
+ def _render_icons(self, container):
654
+ if not self._manager: return
655
+ icons = self._manager.get_styles().get("icons", {})
656
+ card = self._card(container, "Icons", f"Current {icons.get('size','medium')}")
657
+ row = Horizontal(classes="cust-row")
658
+ card.mount(row)
659
+ for sz in _ICON_SIZES:
660
+ b = Button(sz.title(), id=f"cust-icon-size-{sz}", classes="cct-btn-sm")
661
+ if sz == icons.get("size"): b.variant = "primary"
662
+ row.mount(b)
663
+ row2 = Horizontal(classes="cust-row")
664
+ card.mount(row2)
665
+ row2.mount(Button("Toggle Visibility", id="cust-icon-vis", classes="cct-btn-sm"))
666
+ row2.mount(Button("Reset", id="cust-icon-reset", classes="cct-btn-sm"))
667
+
668
+ def _render_header(self, container):
669
+ if not self._manager: return
670
+ hdr = self._manager.get_layout().get("header", {})
671
+ card = self._card(container, "Header", f"Height: {hdr.get('height',3)} lines")
672
+ row = Horizontal(classes="cust-row")
673
+ card.mount(row)
674
+ row.mount(Static("Height:", classes="cust-label")); row.mount(Input(value=str(hdr.get("height",3)), id="cust-header-height"))
675
+ row.mount(Switch(value=bool(hdr.get("logo_visible",True)), id="cust-header-logo-vis"))
676
+ row.mount(Static("Logo visible", classes="cust-label"))
677
+ row.mount(Button("Apply", id="cust-header-apply", variant="primary", classes="cct-btn-sm"))
678
+
679
+ def _render_menu(self, container):
680
+ if not self._manager: return
681
+ mm = self._manager.get_main_menu()
682
+ card = self._card(container, "Main Menu", "Order & toggles")
683
+ for idx,item in enumerate(mm.get("order", [])):
684
+ r = Horizontal(classes="cust-drag-row")
685
+ card.mount(r)
686
+ r.mount(Static(f"{idx+1}. {item}", classes="cust-drag-label"))
687
+ r.mount(Button("↑", id=f"cust-menu-up-{item}", classes="cct-btn-sm"))
688
+ r.mount(Button("↓", id=f"cust-menu-down-{item}", classes="cct-btn-sm"))
689
+ r.mount(Switch(value=mm.get("visibility", {}).get(item,True), id=f"cust-mmvis-{item}"))
690
+
691
+ def _render_spacing(self, container):
692
+ if not self._manager: return
693
+ styles = self._manager.get_styles()
694
+ card = self._card(container, "Spacing & Borders", "Padding · margin · borders")
695
+ spacing = styles.get("spacing", {})
696
+ row = Horizontal(classes="cust-row")
697
+ card.mount(row)
698
+ row.mount(Static("Padding:", classes="cust-label")); row.mount(Input(value=str(spacing.get("padding",1)), id="cust-spacing-pad"))
699
+ row.mount(Static("Margin:", classes="cust-label")); row.mount(Input(value=str(spacing.get("margin",0)), id="cust-spacing-margin"))
700
+ row.mount(Static("Gap:", classes="cust-label")); row.mount(Input(value=str(spacing.get("gap",1)), id="cust-spacing-gap"))
701
+ row.mount(Button("Apply Spacing", id="cust-spacing-apply", variant="primary", classes="cct-btn-sm"))
702
+ borders = styles.get("borders", {})
703
+ row2 = Horizontal(classes="cust-row")
704
+ card.mount(row2)
705
+ row2.mount(Static("Radius:", classes="cust-label")); row2.mount(Input(value=str(borders.get("radius",0)), id="cust-border-radius"))
706
+ border_opts = [("Solid", "solid"), ("Round", "round"), ("Heavy", "heavy"), ("None", "none")]
707
+ cur_style = str(borders.get("style", "solid")).lower()
708
+ valid_styles = [opt[1] for opt in border_opts]
709
+ if cur_style not in valid_styles:
710
+ cur_style = "solid"
711
+ row2.mount(Select(border_opts, value=cur_style, id="cust-border-style", allow_blank=False))
712
+ row2.mount(Button("Apply Borders", id="cust-border-apply", variant="primary", classes="cct-btn-sm"))
713
+
714
+ def _render_visibility(self, container):
715
+ card = self._card(container, "Visibility", "Toggle any region · explicit switch")
716
+ if not self._manager: return
717
+ layout = self._manager.get_layout()
718
+ panels_to_show = [
719
+ ("sidebar", "Sidebar"),
720
+ ("chat", "Chat"),
721
+ ("code_editor", "Code Editor"),
722
+ ("terminal", "Terminal"),
723
+ ("header", "Header"),
724
+ ]
725
+ for pid, label in panels_to_show:
726
+ comp = layout.get(pid)
727
+ if not comp: continue
728
+ r = Horizontal(classes="cust-visibility-row")
729
+ card.mount(r)
730
+ pos = comp.get("position", "?")
731
+ r.mount(Static(f"{label} ({pos})", classes="cust-visibility-label"))
732
+ r.mount(Switch(value=bool(comp.get("visible",True)), id=f"cust-vis-{pid}"))
733
+
734
+ def _render_profiles(self, container):
735
+ card = self._card(container, "Profiles", "Save / load layouts")
736
+ if not self._manager: return
737
+ pm = self._manager.profile_manager
738
+ active = self._manager.get_config().get("active_profile")
739
+ for name in pm.list_profiles():
740
+ prof = pm.get_profile(name)
741
+ desc = prof.get("description","") if prof else ""
742
+ mark = "★ " if name == active else " "
743
+ r = Horizontal(classes="cust-drag-row")
744
+ card.mount(r)
745
+ r.mount(Static(f"{mark}{name} — {desc}", classes="cust-drag-label"))
746
+ r.mount(Button("Load", id=f"cust-prof-load-{name}", variant="primary" if name!=active else None, classes="cct-btn-sm"))
747
+ r.mount(Button("Delete", id=f"cust-prof-del-{name}", variant="error", classes="cct-btn-sm"))
748
+ row = Horizontal(classes="cust-field-row")
749
+ card.mount(row)
750
+ row.mount(Input(placeholder="New Profile Name", id="cust-prof-new-name"))
751
+ row.mount(Button("Save", id="cust-prof-save", variant="primary", classes="cct-btn-sm"))
752
+ row2 = Horizontal(classes="cust-field-row")
753
+ card.mount(row2)
754
+ row2.mount(Input(placeholder="Old name", id="cust-prof-rename-old"))
755
+ row2.mount(Input(placeholder="New name", id="cust-prof-rename-new"))
756
+ row2.mount(Button("Rename", id="cust-prof-rename", classes="cct-btn-sm"))
757
+
758
+ def _render_editor(self, container):
759
+ card = self._card(container, "Editor", "Visual preview · click preview to cycle")
760
+ if self._manager:
761
+ card.mount(_Preview(self._manager, selected=self._selected_panel))
762
+ card.mount(Static(f"Selected: [b]{self._selected_panel}[/]", classes="cust-hint"))
763
+ row = Horizontal(classes="cust-row")
764
+ for pos in ["left","right","top","bottom","floating","center"]:
765
+ row.mount(Button(pos.title(), id=f"cust-edit-{pos}", classes="cct-btn-sm"))
766
+ card.mount(row)
767
+ row2 = Horizontal(classes="cust-row")
768
+ card.mount(row2)
769
+ row2.mount(Button("Hide", id="cust-edit-hide", classes="cct-btn-sm"))
770
+ row2.mount(Button("Show", id="cust-edit-show", classes="cct-btn-sm"))
771
+ row2.mount(Button("Docked", id="cust-edit-docked", classes="cct-btn-sm"))
772
+ row2.mount(Button("Floating", id="cust-edit-floating", classes="cct-btn-sm"))
773
+
774
+ def _render_advanced(self, container):
775
+ card = self._card(container, "Advanced JSON", "Edit directly · Validate then Save")
776
+ if not self._manager: return
777
+ import json as _json
778
+ slim = {"layout": self._manager.get_config().get("layout"), "styles": self._manager.get_config().get("styles")}
779
+ txt = _json.dumps(slim, indent=2)
780
+ if _HAS_TEXTAREA:
781
+ ta = TextArea(text=txt[:4000], id="cust-adv-json")
782
+ ta.styles.height = 16; ta.styles.min_height = 8
783
+ card.mount(ta)
784
+ else:
785
+ card.mount(Static(txt[:1500], classes="cust-hint"))
786
+ row = Horizontal(classes="cust-row")
787
+ card.mount(row)
788
+ row.mount(Button("Validate", id="cust-adv-validate", classes="cct-btn-sm"))
789
+ row.mount(Button("Save JSON", id="cust-adv-save", variant="primary", classes="cct-btn-sm"))
790
+
791
+ def _render_reset(self, container):
792
+ card = self._card(container, "Reset", "Restore defaults")
793
+ row = Horizontal(classes="cust-row")
794
+ card.mount(row)
795
+ row.mount(Button("Reset Layout", id="cust-reset-layout", classes="cct-btn-sm"))
796
+ row.mount(Button("Reset Panes", id="cust-reset-panes", classes="cct-btn-sm"))
797
+ row2 = Horizontal(classes="cust-row")
798
+ card.mount(row2)
799
+ row2.mount(Button("Reset Appearance", id="cust-reset-appearance", classes="cct-btn-sm"))
800
+ row2.mount(Button("Reset Buttons", id="cust-reset-buttons", classes="cct-btn-sm"))
801
+ row3 = Horizontal(classes="cust-row")
802
+ card.mount(row3)
803
+ row3.mount(Button("Reset Everything", id="cust-reset-everything", variant="error", classes="cct-btn-sm"))
804
+
805
+ def _jump(self, sec):
806
+ self._nav_state = sec
807
+ self._rebuild_nav()
808
+ self._render_section_view()
809
+
810
+ def on_input_changed(self, event):
811
+ if getattr(event.input, "id", "") == "cust-search-input":
812
+ self._search_query = event.value
813
+ self._rebuild_nav()
814
+ def on_input_submitted(self, event):
815
+ if getattr(event.input, "id", "") == "cust-prof-new-name":
816
+ name = event.value.strip()
817
+ if name:
818
+ ok,msg = self._manager.profile_manager.save_profile(name)
819
+ self._show_status(msg, "success" if ok else "error")
820
+ self._refresh_all()
821
+ event.input.value = ""
822
+ def on_button_pressed(self, event):
823
+ bid = event.button.id or ""
824
+ if bid in ("cust-close","cust-close2","cust-close3"):
825
+ self.dismiss(None); return
826
+ if bid=="cust-open-ext":
827
+ try:
828
+ self.dismiss(None)
829
+ from .extensions_panel import ExtensionsPanel
830
+ self.app.push_screen(ExtensionsPanel())
831
+ except Exception:
832
+ self.dismiss(None)
833
+ return
834
+ if bid.startswith("cust-nav-"):
835
+ self._set_nav(bid.replace("cust-nav-","")); return
836
+ if bid.startswith("cust-jump-"):
837
+ self._jump(bid.replace("cust-jump-","")); return
838
+ if bid.startswith("cust-sel-"):
839
+ self._selected_panel = bid.replace("cust-sel-", "")
840
+ self._pos_panel = self._selected_panel
841
+ self._size_panel = self._selected_panel
842
+ self._dock_panel = self._selected_panel
843
+ self._on_preview_select(self._selected_panel)
844
+ return
845
+ if bid.startswith("cust-order-up-"):
846
+ cid = bid.replace("cust-order-up-","")
847
+ try:
848
+ all_ordered = [c["id"] for c in self._manager.get_ordered_components()]
849
+ seen = set()
850
+ unique_ordered = []
851
+ for k in all_ordered:
852
+ canon = "code_editor" if k in ("editor", "code_editor") else ("sidebar" if k in ("sidebar", "file_explorer") else k)
853
+ if canon not in seen:
854
+ seen.add(canon)
855
+ unique_ordered.append(k)
856
+ idx = unique_ordered.index(cid)
857
+ if idx > 0:
858
+ unique_ordered[idx], unique_ordered[idx-1] = unique_ordered[idx-1], unique_ordered[idx]
859
+ ok, msg = self._manager.set_order(unique_ordered)
860
+ self._show_status(msg, "success" if ok else "error")
861
+ self._live_apply()
862
+ self.call_after_refresh(self._render_section_view)
863
+ except Exception as e:
864
+ self._show_error(str(e))
865
+ return
866
+ if bid.startswith("cust-order-down-"):
867
+ cid = bid.replace("cust-order-down-","")
868
+ try:
869
+ all_ordered = [c["id"] for c in self._manager.get_ordered_components()]
870
+ seen = set()
871
+ unique_ordered = []
872
+ for k in all_ordered:
873
+ canon = "code_editor" if k in ("editor", "code_editor") else ("sidebar" if k in ("sidebar", "file_explorer") else k)
874
+ if canon not in seen:
875
+ seen.add(canon)
876
+ unique_ordered.append(k)
877
+ idx = unique_ordered.index(cid)
878
+ if idx < len(unique_ordered) - 1:
879
+ unique_ordered[idx], unique_ordered[idx+1] = unique_ordered[idx+1], unique_ordered[idx]
880
+ ok, msg = self._manager.set_order(unique_ordered)
881
+ self._show_status(msg, "success" if ok else "error")
882
+ self._live_apply()
883
+ self.call_after_refresh(self._render_section_view)
884
+ except Exception as e:
885
+ self._show_error(str(e))
886
+ return
887
+ if bid.startswith("cust-order-vis-"):
888
+ cid = bid.replace("cust-order-vis-","")
889
+ try:
890
+ comp = self._manager.get_component(cid)
891
+ cur_vis = comp.get("visible", True) if comp else True
892
+ ok, msg = self._manager.set_visibility(cid, not cur_vis)
893
+ self._show_status(msg, "success" if ok else "error")
894
+ self._live_apply()
895
+ self.call_after_refresh(self._render_section_view)
896
+ except Exception as e:
897
+ self._show_error(str(e))
898
+ return
899
+ if bid.startswith("cust-pospick-"):
900
+ pid = bid.replace("cust-pospick-","")
901
+ self._pos_panel = pid
902
+ self._show_status(f"Selected {pid}", "success")
903
+ self.call_after_refresh(self._render_section_view)
904
+ return
905
+ if bid.startswith("cust-pos-"):
906
+ try:
907
+ pos = bid.replace("cust-pos-","")
908
+ pid = self._pos_panel
909
+ if pos in _POSITION_OPTS:
910
+ ok,msg=self._manager.set_position(pid,pos)
911
+ self._show_status(msg, "success" if ok else "error")
912
+ try:
913
+ for p in self.query(_Preview):
914
+ p.refresh_preview()
915
+ except Exception:
916
+ pass
917
+ except Exception as e:
918
+ self._show_error(str(e))
919
+ return
920
+ if bid.startswith("cust-hdr-"):
921
+ try:
922
+ rest=bid.replace("cust-hdr-","")
923
+ for key in ["logo_position","title_position","alignment"]:
924
+ if rest.startswith(key+"-"):
925
+ opt=rest[len(key)+1:]
926
+ ok,msg=self._manager.set_header_config(**{key:opt})
927
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); break
928
+ except Exception as e:
929
+ self._show_error(str(e))
930
+ return
931
+ if bid.startswith("cust-dockpick-"):
932
+ pid = bid.replace("cust-dockpick-","")
933
+ self._dock_panel = pid
934
+ self._show_status(f"Selected {pid}", "success")
935
+ self.call_after_refresh(self._render_section_view)
936
+ return
937
+ if bid.startswith("cust-dock-"):
938
+ try:
939
+ rest=bid.replace("cust-dock-","")
940
+ for pid in _PANEL_IDS+["header"]:
941
+ if rest.startswith(pid+"-"):
942
+ state=rest[len(pid)+1:]
943
+ ok,msg=self._manager.set_dock_state(pid,state)
944
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); break
945
+ except Exception as e:
946
+ self._show_error(str(e))
947
+ return
948
+ if bid.startswith("cust-sizepick-"):
949
+ pid = bid.replace("cust-sizepick-","")
950
+ self._size_panel = pid
951
+ self._show_status(f"Selected {pid}", "success")
952
+ self.call_after_refresh(self._render_section_view)
953
+ return
954
+ if bid.startswith("cust-size-apply-"):
955
+ pid=bid.replace("cust-size-apply-","")
956
+ try:
957
+ w=_safe_query(self, f"#cust-w-{pid}", Input)
958
+ h=_safe_query(self, f"#cust-h-{pid}", Input)
959
+ ok,msg=self._manager.set_size(pid, (w.value.strip() if w and w.value.strip() else None), (h.value.strip() if h and h.value.strip() else None))
960
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
961
+ except Exception as e:
962
+ self._show_error(str(e))
963
+ return
964
+ if bid.startswith("cust-size-pct-"):
965
+ try:
966
+ rest=bid.replace("cust-size-pct-","")
967
+ for pid in _PANEL_IDS:
968
+ if rest.startswith(pid+"-"):
969
+ pct=rest[len(pid)+1:]
970
+ ok,msg=self._manager.set_size(pid, width=int(pct))
971
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); break
972
+ except Exception as e:
973
+ self._show_error(str(e))
974
+ return
975
+ if bid.startswith("cust-size-auto-"):
976
+ pid=bid.replace("cust-size-auto-","")
977
+ ok,msg=self._manager.set_size(pid, width="auto")
978
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
979
+ if bid.startswith("cust-btn-shape-"):
980
+ ok,msg=self._manager.style_manager.set_button_style(shape=bid.replace("cust-btn-shape-",""))
981
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
982
+ if bid.startswith("cust-btn-size-"):
983
+ ok,msg=self._manager.style_manager.set_button_style(size=bid.replace("cust-btn-size-",""))
984
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
985
+ if bid=="cust-btn-apply":
986
+ try:
987
+ kwargs={}
988
+ for fid,key in [("cust-btn-radius","border_radius"),("cust-btn-padding","padding")]:
989
+ w=_safe_query(self, f"#{fid}", Input)
990
+ if w and w.value.strip():
991
+ kwargs[key]=int(w.value.strip())
992
+ if kwargs:
993
+ ok,msg=self._manager.style_manager.set_button_style(**kwargs)
994
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
995
+ except Exception as e:
996
+ self._show_error(str(e))
997
+ return
998
+ if bid.startswith("cust-icon-size-"):
999
+ ok,msg=self._manager.style_manager.set_icon_style(size=bid.replace("cust-icon-size-",""))
1000
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1001
+ if bid=="cust-icon-vis":
1002
+ cur=self._manager.get_styles().get("icons", {}).get("visible",True)
1003
+ ok,msg=self._manager.style_manager.set_icon_style(visible=not cur)
1004
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1005
+ if bid=="cust-icon-reset":
1006
+ ok,msg=self._manager.style_manager.set_icon_style(size="medium", visible=True)
1007
+ self._show_status("Icons reset", "success" if ok else "error"); self._live_apply(); return
1008
+ if bid=="cust-header-apply":
1009
+ try:
1010
+ kwargs={}
1011
+ w=_safe_query(self, "#cust-header-height", Input)
1012
+ if w and w.value.strip(): kwargs["height"]=int(w.value.strip())
1013
+ w=_safe_query(self, "#cust-header-logo-vis", Switch)
1014
+ if w is not None: kwargs["logo_visible"]=bool(w.value)
1015
+ ok,msg=self._manager.set_header_config(**kwargs)
1016
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1017
+ except Exception as e:
1018
+ self._show_error(str(e))
1019
+ return
1020
+ if bid.startswith("cust-menu-up-"):
1021
+ item=bid.replace("cust-menu-up-","")
1022
+ try:
1023
+ mm=self._manager.get_main_menu(); order=list(mm.get("order",[])); idx=order.index(item)
1024
+ if idx>0:
1025
+ order[idx],order[idx-1]=order[idx-1],order[idx]
1026
+ ok,msg=self._manager.set_main_menu_config(order=order)
1027
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1028
+ except Exception as e:
1029
+ self._show_error(str(e))
1030
+ return
1031
+ if bid.startswith("cust-menu-down-"):
1032
+ item=bid.replace("cust-menu-down-","")
1033
+ try:
1034
+ mm=self._manager.get_main_menu(); order=list(mm.get("order",[])); idx=order.index(item)
1035
+ if idx < len(order)-1:
1036
+ order[idx],order[idx+1]=order[idx+1],order[idx]
1037
+ ok,msg=self._manager.set_main_menu_config(order=order)
1038
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1039
+ except Exception as e:
1040
+ self._show_error(str(e))
1041
+ return
1042
+ if bid=="cust-spacing-apply":
1043
+ try:
1044
+ kwargs={}
1045
+ for fid,key in [("cust-spacing-pad","padding"),("cust-spacing-margin","margin"),("cust-spacing-gap","gap")]:
1046
+ w=_safe_query(self, f"#{fid}", Input)
1047
+ if w and w.value.strip():
1048
+ kwargs[key]=int(w.value.strip())
1049
+ if kwargs:
1050
+ ok,msg=self._manager.style_manager.set_spacing(**kwargs)
1051
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1052
+ except Exception as e:
1053
+ self._show_error(str(e))
1054
+ return
1055
+ if bid=="cust-border-apply":
1056
+ try:
1057
+ kwargs={}
1058
+ w=_safe_query(self, "#cust-border-radius", Input)
1059
+ if w and w.value.strip(): kwargs["radius"]=int(w.value.strip())
1060
+ w=_safe_query(self, "#cust-border-style", Select)
1061
+ if w and w.value: kwargs["style"]=w.value
1062
+ ok,msg=self._manager.style_manager.set_borders(**kwargs)
1063
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1064
+ except Exception as e:
1065
+ self._show_error(str(e))
1066
+ return
1067
+ if bid.startswith("cust-prof-load-"):
1068
+ ok,msg=self._manager.profile_manager.load_profile(bid.replace("cust-prof-load-",""))
1069
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1070
+ if bid.startswith("cust-prof-del-"):
1071
+ name=bid.replace("cust-prof-del-","")
1072
+ self.app.push_screen(_ConfirmScreen(f"Delete '{name}'?", f"Delete profile '{name}' permanently."), lambda c: self._on_prof_delete(name,c))
1073
+ return
1074
+ if bid=="cust-prof-save":
1075
+ try:
1076
+ inp=_safe_query(self, "#cust-prof-new-name", Input)
1077
+ name=inp.value.strip() if inp else ""
1078
+ if not name:
1079
+ self._show_error("Enter name"); return
1080
+ ok,msg=self._manager.profile_manager.save_profile(name)
1081
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1082
+ if inp: inp.value=""
1083
+ except Exception as e:
1084
+ self._show_error(str(e))
1085
+ return
1086
+ if bid=="cust-prof-rename":
1087
+ try:
1088
+ o=_safe_query(self, "#cust-prof-rename-old", Input)
1089
+ n=_safe_query(self, "#cust-prof-rename-new", Input)
1090
+ old=o.value.strip() if o else ""; new=n.value.strip() if n else ""
1091
+ if not old or not new:
1092
+ self._show_error("Enter both names"); return
1093
+ ok,msg=self._manager.profile_manager.rename_profile(old,new)
1094
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1095
+ except Exception as e:
1096
+ self._show_error(str(e))
1097
+ return
1098
+ if bid.startswith("cust-edit-"):
1099
+ act=bid.replace("cust-edit-","")
1100
+ pid=self._selected_panel
1101
+ if act in ("left","right","top","bottom","floating","center","docked"):
1102
+ ok,msg=self._manager.set_position(pid,act)
1103
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1104
+ elif act=="hide":
1105
+ ok,msg=self._manager.set_visibility(pid,False)
1106
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1107
+ elif act=="show":
1108
+ ok,msg=self._manager.set_visibility(pid,True)
1109
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1110
+ return
1111
+ if bid=="cust-adv-validate":
1112
+ try:
1113
+ ta=_safe_query(self, "#cust-adv-json", TextArea) if _HAS_TEXTAREA else None
1114
+ if ta:
1115
+ import json as _json
1116
+ _json.loads(ta.text)
1117
+ self._show_status("JSON valid", "success")
1118
+ else:
1119
+ self._show_error("Editor not available")
1120
+ except Exception as e:
1121
+ self._show_error(f"Invalid: {e}")
1122
+ return
1123
+ if bid=="cust-adv-save":
1124
+ try:
1125
+ ta=_safe_query(self, "#cust-adv-json", TextArea) if _HAS_TEXTAREA else None
1126
+ if not ta:
1127
+ self._show_error("Editor not available"); return
1128
+ import json as _json
1129
+ data=_json.loads(ta.text)
1130
+ cfg=self._manager.get_config()
1131
+ if "layout" in data: cfg["layout"].update(data["layout"])
1132
+ if "styles" in data:
1133
+ for k,v in data["styles"].items():
1134
+ if isinstance(v,dict) and k in cfg["styles"]: cfg["styles"][k].update(v)
1135
+ else: cfg["styles"][k]=v
1136
+ ok=self._manager._persist(cfg)
1137
+ self._show_status("Saved" if ok else "Failed", "success" if ok else "error"); self._live_apply()
1138
+ except Exception as e:
1139
+ self._show_error(str(e))
1140
+ return
1141
+ if bid in ("cust-reset-layout","cust-reset-current"):
1142
+ ok,msg=self._manager.reset_current_layout()
1143
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1144
+ if bid=="cust-reset-panes":
1145
+ ok,msg=self._manager.reset_pane_positions()
1146
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1147
+ if bid=="cust-reset-appearance":
1148
+ ok,msg=self._manager.reset_appearance()
1149
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1150
+ if bid=="cust-reset-buttons":
1151
+ ok,msg=self._manager.reset_buttons()
1152
+ self._show_status(msg, "success" if ok else "error"); self._live_apply(); return
1153
+ if bid=="cust-reset-everything" or bid=="cust-reset-all":
1154
+ self.app.push_screen(_ConfirmScreen("Reset Everything?", "Reset ALL customization to defaults? Cannot be undone.", True), self._on_reset_everything)
1155
+ return
1156
+
1157
+ def _on_prof_delete(self, name, confirmed):
1158
+ if not confirmed: return
1159
+ ok,msg=self._manager.profile_manager.delete_profile(name)
1160
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1161
+ def _on_reset_everything(self, confirmed):
1162
+ if not confirmed: return
1163
+ ok,msg=self._manager.reset_everything()
1164
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1165
+
1166
+ def on_select_changed(self, event):
1167
+ sid = getattr(event.select, "id", "") or ""
1168
+ if sid == "cust-pos-select":
1169
+ self._pos_panel = event.value
1170
+ self.call_after_refresh(self._render_section_view)
1171
+ elif sid == "cust-size-select":
1172
+ self._size_panel = event.value
1173
+ self.call_after_refresh(self._render_section_view)
1174
+ elif sid == "cust-dock-select":
1175
+ self._dock_panel = event.value
1176
+ self.call_after_refresh(self._render_section_view)
1177
+ def on_switch_changed(self, event):
1178
+ sid=getattr(event.switch, "id", "") or ""
1179
+ if sid.startswith("cust-vis-"):
1180
+ pid=sid.replace("cust-vis-","")
1181
+ ok,msg=self._manager.set_visibility(pid, bool(event.value))
1182
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1183
+ elif sid.startswith("cust-mmvis-"):
1184
+ item=sid.replace("cust-mmvis-","")
1185
+ try:
1186
+ mm=self._manager.get_main_menu()
1187
+ vis=dict(mm.get("visibility",{}))
1188
+ vis[item]=bool(event.value)
1189
+ ok,msg=self._manager.set_main_menu_config(visibility=vis)
1190
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1191
+ except Exception as e:
1192
+ self._show_error(str(e))
1193
+ elif sid=="cust-header-logo-vis":
1194
+ ok,msg=self._manager.set_header_config(logo_visible=bool(event.value))
1195
+ self._show_status(msg, "success" if ok else "error"); self._live_apply()
1196
+ def action_cancel(self):
1197
+ self.dismiss(None)
1198
+ def on_key(self, event):
1199
+ if event.key=="escape":
1200
+ self.dismiss(None)
1201
+
1202
+ class _ConfirmScreen(Screen):
1203
+ CSS = """
1204
+ _ConfirmScreen { align: center middle; background: $app-background 70%; }
1205
+ #confirm-box { width: 60; max-width: 96%; height: auto; background: $surface; border: round $border; padding: 1 2; }
1206
+ #confirm-title { color: $warning; text-style: bold; height: 1; }
1207
+ #confirm-msg { color: $text; height: auto; padding: 1 0; }
1208
+ #confirm-btns { height: 3; align: center middle; }
1209
+ #confirm-btns Button { margin: 0 1; min-width: 12; }
1210
+ """
1211
+ def __init__(self, title, msg, is_destructive=False):
1212
+ super().__init__()
1213
+ self._title=title; self._msg=msg; self._destructive=is_destructive
1214
+ def compose(self):
1215
+ with Vertical(id="confirm-box"):
1216
+ yield Static(self._title, id="confirm-title")
1217
+ yield Static(self._msg, id="confirm-msg")
1218
+ with Horizontal(id="confirm-btns"):
1219
+ yield Button("Cancel", id="confirm-cancel")
1220
+ yield Button("Confirm", id="confirm-ok", variant="error" if self._destructive else "primary")
1221
+ def on_button_pressed(self, event):
1222
+ self.dismiss(event.button.id=="confirm-ok")
1223
+ def on_key(self, event):
1224
+ if event.key=="escape":
1225
+ self.dismiss(False)
1226
+
1227
+ else:
1228
+ CustomizationPanel=None
1229
+ _ConfirmScreen=None