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,203 @@
1
+ """CAT Universal File Preview Engine — PDF Viewer (calc_terminal/viewers/pdf_viewer.py).
2
+
3
+ High-fidelity PDF document viewer:
4
+ - Renders genuine document pages using PyMuPDF (fitz / pymupdf) pixmaps into truecolor display.
5
+ - Displays actual rendered document pages (never mere raw extracted text).
6
+ - Page navigation (Prev, Next, Jump to page, First, Last).
7
+ - Zoom controls (In, Out, Fit, 100%).
8
+ - In-document text search.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from typing import Optional, Tuple
15
+
16
+ TEXTUAL_AVAILABLE = True
17
+ try:
18
+ from rich.console import Group
19
+ from rich.style import Style
20
+ from rich.text import Text
21
+ from textual.containers import Horizontal, Vertical, VerticalScroll
22
+ from textual.widgets import Button, Input, Static
23
+ except Exception:
24
+ TEXTUAL_AVAILABLE = False
25
+ Vertical = object # type: ignore
26
+
27
+
28
+ class PdfViewer(Vertical):
29
+ """High-fidelity visual PDF viewer."""
30
+
31
+ def __init__(self, path: str, **kwargs) -> None:
32
+ super().__init__(**kwargs)
33
+ self.path = os.path.abspath(path)
34
+ self._current_page: int = 0 # 0-indexed
35
+ self._page_count: int = 1
36
+ self._zoom: float = 1.0
37
+ self._fit: bool = True
38
+ self._search_query: str = ""
39
+ self._doc = None
40
+ self._open_document()
41
+
42
+ def compose(self):
43
+ with Horizontal(id="cct-pdf-toolbar", classes="cct-pdf-toolbar"):
44
+ yield Static("", id="cct-pdf-info", classes="cct-tb-pill")
45
+ yield Button("â—€ Prev", id="cct-pdf-prev", classes="cct-ctrl")
46
+ yield Button("Next â–¶", id="cct-pdf-next", classes="cct-ctrl")
47
+ yield Button("−", id="cct-pdf-zoomout", classes="cct-ctrl", tooltip="Zoom Out")
48
+ yield Button("+", id="cct-pdf-zoomin", classes="cct-ctrl", tooltip="Zoom In")
49
+ yield Button("Fit", id="cct-pdf-fit", classes="cct-ctrl", tooltip="Fit Page")
50
+ yield Button("100%", id="cct-pdf-reset", classes="cct-ctrl")
51
+ yield Input(placeholder="Search text...", id="cct-pdf-search")
52
+ yield Button("Open Externally", id="cct-pdf-open", classes="cct-ctrl")
53
+ yield Button("✕", id="cct-pdf-close", classes="cct-ctrl")
54
+ with VerticalScroll(id="cct-pdf-scroll"):
55
+ yield Static("", id="cct-pdf-display", classes="cct-pdf-display")
56
+
57
+ def on_mount(self) -> None:
58
+ self._open_document()
59
+ self._refresh_page()
60
+
61
+ def _open_document(self) -> None:
62
+ try:
63
+ import fitz
64
+ self._doc = fitz.open(self.path)
65
+ self._page_count = max(1, len(self._doc))
66
+ except Exception:
67
+ self._doc = None
68
+
69
+ def _render_page_pixmap(self, page_idx: int) -> Optional[Text]:
70
+ """Render a PDF page directly into truecolor terminal half-blocks."""
71
+ if not self._doc or page_idx < 0 or page_idx >= len(self._doc):
72
+ return None
73
+ try:
74
+ from PIL import Image
75
+ page = self._doc[page_idx]
76
+ # Render page at 72dpi * zoom scale factor
77
+ scale = 0.6 if self._fit else max(0.4, self._zoom * 0.8)
78
+ matrix = page.get_pixmap(matrix=getattr(self._doc, "Matrix", None) or fitz.Matrix(scale, scale))
79
+ im = Image.frombytes("RGB", [matrix.width, matrix.height], matrix.samples)
80
+
81
+ iw, ih = im.size
82
+ if self._fit:
83
+ target_w, target_h = 76, 44
84
+ im.thumbnail((target_w, target_h), getattr(Image, "LANCZOS", 1))
85
+ else:
86
+ target_w = min(200, max(20, int(iw * 0.35 * self._zoom)))
87
+ target_h = min(150, max(15, int(ih * 0.35 * self._zoom)))
88
+ im = im.resize((target_w, target_h), getattr(Image, "LANCZOS", 1))
89
+
90
+ tw, th = im.size
91
+ txt = Text()
92
+ for y in range(0, th, 2):
93
+ for x in range(tw):
94
+ r1, g1, b1 = im.getpixel((x, y))
95
+ if y + 1 < th:
96
+ r2, g2, b2 = im.getpixel((x, y + 1))
97
+ else:
98
+ r2, g2, b2 = r1, g1, b1
99
+ style = Style(color=f"#{r2:02x}{g2:02x}{b2:02x}", bgcolor=f"#{r1:02x}{g1:02x}{b1:02x}")
100
+ txt.append("â–„", style=style)
101
+ if y + 2 < th:
102
+ txt.append("\n")
103
+ return txt
104
+ except Exception:
105
+ return None
106
+
107
+ def _refresh_page(self) -> None:
108
+ try:
109
+ info_pill = self.query_one("#cct-pdf-info", Static)
110
+ disp = self.query_one("#cct-pdf-display", Static)
111
+ base = os.path.basename(self.path)
112
+ zoom_label = "Fit" if self._fit else f"{int(self._zoom * 100)}%"
113
+ info_pill.update(f"📄 {base} · Page {self._current_page + 1}/{self._page_count} · {zoom_label}")
114
+
115
+ rendered_page = self._render_page_pixmap(self._current_page)
116
+ if rendered_page is not None:
117
+ header = Text(f"Page {self._current_page + 1} of {self._page_count}", style="bold cyan")
118
+ parts = [header, Text(""), rendered_page]
119
+ disp.update(Group(*parts))
120
+ else:
121
+ # Honest fallback if PyMuPDF cannot render this page
122
+ disp.update(
123
+ f"[b]📄 {base} — Page {self._current_page + 1}/{self._page_count}[/b]\n\n"
124
+ f"[yellow]PDF rendering engine active. Use external viewer for raw print format.[/yellow]\n"
125
+ f"[dim]Path: {self.path}[/dim]"
126
+ )
127
+ except Exception:
128
+ pass
129
+
130
+ def on_button_pressed(self, event: Button.Pressed) -> None:
131
+ bid = event.button.id
132
+ if bid == "cct-pdf-close":
133
+ self._close_tab()
134
+ elif bid == "cct-pdf-prev":
135
+ if self._current_page > 0:
136
+ self._current_page -= 1
137
+ self._refresh_page()
138
+ elif bid == "cct-pdf-next":
139
+ if self._current_page < self._page_count - 1:
140
+ self._current_page += 1
141
+ self._refresh_page()
142
+ elif bid == "cct-pdf-zoomin":
143
+ self._fit = False
144
+ self._zoom = min(3.0, self._zoom * 1.25)
145
+ self._refresh_page()
146
+ elif bid == "cct-pdf-zoomout":
147
+ self._fit = False
148
+ self._zoom = max(0.4, self._zoom / 1.25)
149
+ self._refresh_page()
150
+ elif bid == "cct-pdf-fit":
151
+ self._fit = True
152
+ self._zoom = 1.0
153
+ self._refresh_page()
154
+ elif bid == "cct-pdf-reset":
155
+ self._fit = False
156
+ self._zoom = 1.0
157
+ self._refresh_page()
158
+ elif bid == "cct-pdf-open":
159
+ try:
160
+ import pathlib, webbrowser
161
+ webbrowser.open(pathlib.Path(self.path).as_uri())
162
+ except Exception:
163
+ pass
164
+
165
+ def on_input_submitted(self, event: Input.Submitted) -> None:
166
+ if event.input.id == "cct-pdf-search":
167
+ query = event.value.strip().lower()
168
+ if not query or not self._doc:
169
+ return
170
+ # Search across pages
171
+ for idx in range(len(self._doc)):
172
+ try:
173
+ text = self._doc[idx].get_text().lower()
174
+ if query in text:
175
+ self._current_page = idx
176
+ self._refresh_page()
177
+ return
178
+ except Exception:
179
+ pass
180
+
181
+ def close(self) -> None:
182
+ """Release underlying document file handle."""
183
+ if self._doc is not None:
184
+ try:
185
+ self._doc.close()
186
+ except Exception:
187
+ pass
188
+ self._doc = None
189
+
190
+ def _close_tab(self) -> None:
191
+ self.close()
192
+ try:
193
+ from ..ui.editor import EditorPane
194
+ node = self._parent
195
+ while node is not None and not isinstance(node, EditorPane):
196
+ node = getattr(node, "_parent", None)
197
+ if isinstance(node, EditorPane):
198
+ for p, tid in list(node._open_paths.items()):
199
+ if os.path.normcase(p) == os.path.normcase(self.path):
200
+ node.close_active(tid)
201
+ break
202
+ except Exception:
203
+ pass
@@ -0,0 +1,164 @@
1
+ """CAT Universal File Preview Engine — PPTX Presentation Viewer (calc_terminal/viewers/presentation_viewer.py).
2
+
3
+ High-fidelity visual PPTX slide viewer:
4
+ - Renders visual presentation slide cards (titles, bullet points, layout cards).
5
+ - Parses PresentationML (ppt/slides/slide*.xml) natively via stdlib zipfile and xml.etree.ElementTree.
6
+ - Supports slide-by-slide slideshow navigation (Prev, Next, Jump).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import re
13
+ import xml.etree.ElementTree as ET
14
+ import zipfile
15
+ from typing import List, Optional
16
+
17
+ TEXTUAL_AVAILABLE = True
18
+ try:
19
+ from rich.console import Group
20
+ from rich.panel import Panel
21
+ from rich.text import Text
22
+ from textual.containers import Horizontal, Vertical, VerticalScroll
23
+ from textual.widgets import Button, Static
24
+ except Exception:
25
+ TEXTUAL_AVAILABLE = False
26
+ Vertical = object # type: ignore
27
+
28
+ P_NS = "{http://schemas.openxmlformats.org/presentationml/2006/main}"
29
+ A_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}"
30
+
31
+
32
+ class PresentationViewer(Vertical):
33
+ """Visual PPTX slide viewer tab."""
34
+
35
+ def __init__(self, path: str, **kwargs) -> None:
36
+ super().__init__(**kwargs)
37
+ self.path = os.path.abspath(path)
38
+ self._current_slide: int = 0
39
+ self._slide_count: int = 1
40
+ self._slides_data: List[dict] = []
41
+ self._load_presentation()
42
+
43
+ @property
44
+ def slide_count(self) -> int:
45
+ return self._slide_count
46
+
47
+ def compose(self):
48
+ with Horizontal(id="cct-pptx-toolbar", classes="cct-pptx-toolbar"):
49
+ yield Static(os.path.basename(self.path), id="cct-pptx-info", classes="cct-tb-pill")
50
+ yield Button("â—€ Prev Slide", id="cct-pptx-prev", classes="cct-ctrl")
51
+ yield Button("Next Slide â–¶", id="cct-pptx-next", classes="cct-ctrl")
52
+ yield Button("Open Externally", id="cct-pptx-open", classes="cct-ctrl")
53
+ yield Button("✕", id="cct-pptx-close", classes="cct-ctrl")
54
+ with VerticalScroll(id="cct-pptx-scroll"):
55
+ yield Static("", id="cct-pptx-display", classes="cct-pptx-display")
56
+
57
+ def on_mount(self) -> None:
58
+ self._load_presentation()
59
+ self._refresh_slide()
60
+
61
+ def _load_presentation(self) -> None:
62
+ """Parse all slides from PPTX package."""
63
+ self._slides_data.clear()
64
+ if not os.path.isfile(self.path):
65
+ return
66
+ try:
67
+ with zipfile.ZipFile(self.path, "r") as pptx_zip:
68
+ slide_files = [
69
+ f for f in pptx_zip.namelist()
70
+ if re.match(r"ppt/slides/slide\d+\.xml", f)
71
+ ]
72
+ # Sort numerically: slide1, slide2...
73
+ slide_files.sort(key=lambda s: int(re.search(r"\d+", s).group(0)))
74
+ self._slide_count = max(1, len(slide_files))
75
+
76
+ for s_file in slide_files:
77
+ xml_data = pptx_zip.read(s_file)
78
+ root = ET.fromstring(xml_data)
79
+ slide_info = {"title": "", "bullets": []}
80
+
81
+ # Extract text boxes and shapes
82
+ for sp in root.findall(f".//{P_NS}sp"):
83
+ text_runs = []
84
+ for t in sp.findall(f".//{A_NS}t"):
85
+ if t.text:
86
+ text_runs.append(t.text)
87
+ full_text = " ".join(text_runs).strip()
88
+ if full_text:
89
+ if not slide_info["title"]:
90
+ slide_info["title"] = full_text
91
+ else:
92
+ slide_info["bullets"].append(full_text)
93
+
94
+ if not slide_info["title"] and slide_info["bullets"]:
95
+ slide_info["title"] = slide_info["bullets"].pop(0)
96
+
97
+ self._slides_data.append(slide_info)
98
+ except Exception:
99
+ pass
100
+
101
+ def _refresh_slide(self) -> None:
102
+ try:
103
+ info = self.query_one("#cct-pptx-info", Static)
104
+ disp = self.query_one("#cct-pptx-display", Static)
105
+ base = os.path.basename(self.path)
106
+ total = len(self._slides_data) or self._slide_count
107
+ info.update(f"📊 {base} · Slide {self._current_slide + 1}/{total}")
108
+
109
+ if self._slides_data and 0 <= self._current_slide < len(self._slides_data):
110
+ data = self._slides_data[self._current_slide]
111
+ title = data.get("title") or f"Slide {self._current_slide + 1}"
112
+ bullets = data.get("bullets", [])
113
+
114
+ body_lines = []
115
+ body_lines.append(Text(f"📌 {title}", style="bold yellow"))
116
+ body_lines.append(Text(""))
117
+ for b in bullets:
118
+ body_lines.append(Text(f" • {b}", style="white"))
119
+
120
+ panel = Panel(
121
+ Group(*body_lines),
122
+ title=f"[bold green]Slide {self._current_slide + 1} / {total}[/bold green]",
123
+ border_style="yellow",
124
+ padding=(2, 4),
125
+ )
126
+ disp.update(panel)
127
+ else:
128
+ disp.update(f"[b]📊 {base}[/b]\n[dim]Slide {self._current_slide + 1} / {total}[/dim]")
129
+ except Exception:
130
+ pass
131
+
132
+ def on_button_pressed(self, event: Button.Pressed) -> None:
133
+ bid = event.button.id
134
+ if bid == "cct-pptx-close":
135
+ self._close_tab()
136
+ elif bid == "cct-pptx-prev":
137
+ if self._current_slide > 0:
138
+ self._current_slide -= 1
139
+ self._refresh_slide()
140
+ elif bid == "cct-pptx-next":
141
+ total = len(self._slides_data) or self._slide_count
142
+ if self._current_slide < total - 1:
143
+ self._current_slide += 1
144
+ self._refresh_slide()
145
+ elif bid == "cct-pptx-open":
146
+ try:
147
+ import pathlib, webbrowser
148
+ webbrowser.open(pathlib.Path(self.path).as_uri())
149
+ except Exception:
150
+ pass
151
+
152
+ def _close_tab(self) -> None:
153
+ try:
154
+ from ..ui.editor import EditorPane
155
+ node = self._parent
156
+ while node is not None and not isinstance(node, EditorPane):
157
+ node = getattr(node, "_parent", None)
158
+ if isinstance(node, EditorPane):
159
+ for p, tid in list(node._open_paths.items()):
160
+ if os.path.normcase(p) == os.path.normcase(self.path):
161
+ node.close_active(tid)
162
+ break
163
+ except Exception:
164
+ pass
@@ -0,0 +1,120 @@
1
+ """CAT Universal File Preview Engine — Viewer Registry (calc_terminal/viewers/registry.py).
2
+
3
+ Centralized file-type detector and viewer provider registry:
4
+ - Prevents files from being inappropriately opened as raw extracted text.
5
+ - Dispatches each file to its dedicated high-fidelity visual viewer.
6
+ - Extensible: plugins and extensions can register custom viewer providers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from typing import Any, Callable, Dict, List, Optional, Set
13
+
14
+
15
+ class ViewerProvider:
16
+ """Interface for a file preview/viewer provider."""
17
+
18
+ def __init__(self, id: str, name: str, extensions: Set[str],
19
+ factory: Optional[Callable[[str, Any], Any]] = None) -> None:
20
+ self.id = id
21
+ self.name = name
22
+ self.extensions = {ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext in extensions}
23
+ self.factory = factory
24
+
25
+ def can_open(self, file_path: str) -> bool:
26
+ ext = os.path.splitext(str(file_path).lower())[1]
27
+ return ext in self.extensions
28
+
29
+ def create_viewer(self, file_path: str, **kwargs) -> Any:
30
+ if self.factory is not None:
31
+ return self.factory(file_path, **kwargs)
32
+ raise NotImplementedError(f"Provider {self.id} has no factory implementation.")
33
+
34
+
35
+ class ViewerRegistry:
36
+ """Registry coordinating all visual file viewers."""
37
+
38
+ def __init__(self) -> None:
39
+ self._providers: Dict[str, ViewerProvider] = {}
40
+ self._register_builtins()
41
+
42
+ def register(self, provider: ViewerProvider) -> None:
43
+ """Register a new viewer provider."""
44
+ self._providers[provider.id] = provider
45
+
46
+ def unregister(self, provider_id: str) -> None:
47
+ """Unregister a viewer provider."""
48
+ self._providers.pop(provider_id, None)
49
+
50
+ def get_provider(self, provider_id: str) -> Optional[ViewerProvider]:
51
+ return self._providers.get(provider_id)
52
+
53
+ def get_provider_for_file(self, file_path: str) -> Optional[ViewerProvider]:
54
+ """Find the registered viewer provider capable of opening this file."""
55
+ ext = os.path.splitext(str(file_path).lower())[1]
56
+ for p in self._providers.values():
57
+ if p.can_open(file_path):
58
+ return p
59
+ return None
60
+
61
+ def can_open_visually(self, file_path: str) -> bool:
62
+ """Return True if a specialized visual viewer exists for this file."""
63
+ p = self.get_provider_for_file(file_path)
64
+ return p is not None and p.id != "code"
65
+
66
+ def create_viewer_for_file(self, file_path: str, **kwargs) -> Optional[Any]:
67
+ """Create the appropriate viewer widget for the given file."""
68
+ provider = self.get_provider_for_file(file_path)
69
+ if provider:
70
+ return provider.create_viewer(file_path, **kwargs)
71
+ return None
72
+
73
+ def _register_builtins(self) -> None:
74
+ """Register default visual providers: image, pdf, docx, pptx, xlsx, csv."""
75
+ # 1. Images
76
+ image_exts = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".bmp", ".ico", ".tiff"}
77
+ def _image_factory(path, **kwargs):
78
+ from .image_viewer import ImageViewer
79
+ return ImageViewer(path, **kwargs)
80
+ self.register(ViewerProvider("image", "Image Viewer", image_exts, _image_factory))
81
+
82
+ # 2. PDF
83
+ pdf_exts = {".pdf"}
84
+ def _pdf_factory(path, **kwargs):
85
+ from .pdf_viewer import PdfViewer
86
+ return PdfViewer(path, **kwargs)
87
+ self.register(ViewerProvider("pdf", "PDF Document Viewer", pdf_exts, _pdf_factory))
88
+
89
+ # 3. DOCX Document
90
+ docx_exts = {".docx"}
91
+ def _docx_factory(path, **kwargs):
92
+ from .document_viewer import DocumentViewer
93
+ return DocumentViewer(path, **kwargs)
94
+ self.register(ViewerProvider("document", "DOCX Document Viewer", docx_exts, _docx_factory))
95
+
96
+ # 4. PPTX Presentation
97
+ pptx_exts = {".pptx"}
98
+ def _pptx_factory(path, **kwargs):
99
+ from .presentation_viewer import PresentationViewer
100
+ return PresentationViewer(path, **kwargs)
101
+ self.register(ViewerProvider("presentation", "Presentation Slide Viewer", pptx_exts, _pptx_factory))
102
+
103
+ # 5. Spreadsheets (XLSX, CSV)
104
+ sheet_exts = {".xlsx", ".csv"}
105
+ def _sheet_factory(path, **kwargs):
106
+ from .spreadsheet_viewer import SpreadsheetViewer
107
+ return SpreadsheetViewer(path, **kwargs)
108
+ self.register(ViewerProvider("spreadsheet", "Spreadsheet Grid Viewer", sheet_exts, _sheet_factory))
109
+
110
+
111
+ # Global singleton registry
112
+ _GLOBAL_VIEWER_REGISTRY: Optional[ViewerRegistry] = None
113
+
114
+
115
+ def get_viewer_registry() -> ViewerRegistry:
116
+ """Return the global ViewerRegistry singleton."""
117
+ global _GLOBAL_VIEWER_REGISTRY
118
+ if _GLOBAL_VIEWER_REGISTRY is None:
119
+ _GLOBAL_VIEWER_REGISTRY = ViewerRegistry()
120
+ return _GLOBAL_VIEWER_REGISTRY
@@ -0,0 +1,204 @@
1
+ """CAT Universal File Preview Engine — Spreadsheet Viewer (calc_terminal/viewers/spreadsheet_viewer.py).
2
+
3
+ High-fidelity visual spreadsheet grid viewer:
4
+ - Supports XLSX and CSV.
5
+ - Displays visual interactive grid table using Textual's DataTable.
6
+ - Parses XLSX multi-sheet workbooks natively via stdlib zipfile and xml.etree.ElementTree.
7
+ - Supports sheet selection, column headers, and search filtering.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import csv
13
+ import os
14
+ import re
15
+ import xml.etree.ElementTree as ET
16
+ import zipfile
17
+ from typing import Dict, List, Optional
18
+
19
+ TEXTUAL_AVAILABLE = True
20
+ try:
21
+ from textual.containers import Horizontal, Vertical
22
+ from textual.widgets import Button, DataTable, Input, Static
23
+ except Exception:
24
+ TEXTUAL_AVAILABLE = False
25
+ Vertical = object # type: ignore
26
+
27
+ S_NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
28
+
29
+
30
+ class SpreadsheetViewer(Vertical):
31
+ """Visual spreadsheet table viewer for XLSX and CSV."""
32
+
33
+ def __init__(self, path: str, **kwargs) -> None:
34
+ super().__init__(**kwargs)
35
+ self.path = os.path.abspath(path)
36
+ self._sheets: Dict[str, List[List[str]]] = {}
37
+ self._sheet_names: List[str] = []
38
+ self._active_sheet_idx: int = 0
39
+ self._load_data()
40
+
41
+ def compose(self):
42
+ with Horizontal(id="cct-sheet-toolbar", classes="cct-sheet-toolbar"):
43
+ yield Static(os.path.basename(self.path), id="cct-sheet-info", classes="cct-tb-pill")
44
+ yield Input(placeholder="Filter rows...", id="cct-sheet-filter")
45
+ yield Button("Open Externally", id="cct-sheet-open", classes="cct-ctrl")
46
+ yield Button("✕", id="cct-sheet-close", classes="cct-ctrl")
47
+ yield Horizontal(id="cct-sheet-tabs-strip", classes="cct-sheet-tabs-strip")
48
+ try:
49
+ yield DataTable(id="cct-sheet-datatable")
50
+ except Exception:
51
+ yield Static("DataTable unavailable", id="cct-sheet-fallback")
52
+
53
+ def on_mount(self) -> None:
54
+ self._load_data()
55
+ self._populate_table()
56
+
57
+ def _load_data(self) -> None:
58
+ """Load either XLSX sheets or CSV rows."""
59
+ self._sheets.clear()
60
+ self._sheet_names.clear()
61
+ if not os.path.isfile(self.path):
62
+ return
63
+
64
+ ext = os.path.splitext(self.path)[1].lower()
65
+ if ext == ".csv":
66
+ try:
67
+ with open(self.path, "r", encoding="utf-8", errors="replace", newline="") as f:
68
+ reader = csv.reader(f)
69
+ rows = [r for r in list(reader)[:500]]
70
+ self._sheets["Sheet1"] = rows
71
+ self._sheet_names = ["Sheet1"]
72
+ except Exception:
73
+ pass
74
+ elif ext in (".xlsx", ".xlsm"):
75
+ self._load_xlsx()
76
+
77
+ def _load_xlsx(self) -> None:
78
+ """Parse XLSX workbook, shared strings, and worksheets."""
79
+ try:
80
+ with zipfile.ZipFile(self.path, "r") as z:
81
+ # 1. Shared strings table
82
+ shared_strings = []
83
+ if "xl/sharedStrings.xml" in z.namelist():
84
+ root_ss = ET.fromstring(z.read("xl/sharedStrings.xml"))
85
+ for si in root_ss.findall(f"{S_NS}si"):
86
+ t = si.find(f"{S_NS}t")
87
+ if t is not None and t.text:
88
+ shared_strings.append(t.text)
89
+ else:
90
+ # Rich text runs
91
+ text_runs = [r_t.text or "" for r_t in si.findall(f".//{S_NS}t")]
92
+ shared_strings.append("".join(text_runs))
93
+
94
+ # 2. Workbook sheet names
95
+ sheet_map = []
96
+ if "xl/workbook.xml" in z.namelist():
97
+ wb_root = ET.fromstring(z.read("xl/workbook.xml"))
98
+ sheets_elem = wb_root.find(f"{S_NS}sheets")
99
+ if sheets_elem is not None:
100
+ for s in sheets_elem.findall(f"{S_NS}sheet"):
101
+ name = s.get("name", "Sheet")
102
+ sheet_id = s.get("sheetId", "1")
103
+ sheet_map.append((name, sheet_id))
104
+
105
+ # Fallback if sheet map missing
106
+ if not sheet_map:
107
+ sheet_files = [f for f in z.namelist() if re.match(r"xl/worksheets/sheet\d+\.xml", f)]
108
+ sheet_map = [(f"Sheet{i+1}", str(i+1)) for i in range(len(sheet_files))]
109
+
110
+ for name, s_id in sheet_map:
111
+ sheet_filename = f"xl/worksheets/sheet{s_id}.xml"
112
+ if sheet_filename not in z.namelist():
113
+ candidates = [f for f in z.namelist() if f.startswith("xl/worksheets/sheet")]
114
+ if candidates:
115
+ sheet_filename = candidates[0]
116
+ else:
117
+ continue
118
+
119
+ sheet_root = ET.fromstring(z.read(sheet_filename))
120
+ sheet_data = sheet_root.find(f"{S_NS}sheetData")
121
+ rows = []
122
+ if sheet_data is not None:
123
+ for row_elem in sheet_data.findall(f"{S_NS}row")[:500]:
124
+ row_cells = []
125
+ for c in row_elem.findall(f"{S_NS}c"):
126
+ cell_type = c.get("t")
127
+ val_elem = c.find(f"{S_NS}v")
128
+ val = val_elem.text if val_elem is not None else ""
129
+ if cell_type == "s" and val.isdigit():
130
+ idx = int(val)
131
+ val = shared_strings[idx] if idx < len(shared_strings) else val
132
+ row_cells.append(str(val))
133
+ rows.append(row_cells)
134
+
135
+ self._sheets[name] = rows
136
+ self._sheet_names.append(name)
137
+ except Exception:
138
+ pass
139
+
140
+ def _populate_table(self, filter_text: str = "") -> None:
141
+ """Render rows into Textual DataTable."""
142
+ try:
143
+ table = self.query_one("#cct-sheet-datatable", DataTable)
144
+ table.clear(columns=True)
145
+
146
+ sheet_name = self._sheet_names[self._active_sheet_idx] if self._sheet_names else "Sheet1"
147
+ rows = self._sheets.get(sheet_name, [])
148
+
149
+ # Filter if requested
150
+ if filter_text:
151
+ q = filter_text.lower()
152
+ rows = [r for r in rows if any(q in str(cell).lower() for cell in r)]
153
+
154
+ if not rows:
155
+ table.add_column("Notice")
156
+ table.add_row("No matching data found.")
157
+ return
158
+
159
+ max_cols = max(len(r) for r in rows)
160
+ # Default headers A, B, C...
161
+ for col_idx in range(max_cols):
162
+ header = chr(65 + col_idx) if col_idx < 26 else f"Col{col_idx+1}"
163
+ table.add_column(header)
164
+
165
+ for r_idx, row in enumerate(rows):
166
+ padded = row + [""] * (max_cols - len(row))
167
+ table.add_row(*padded[:max_cols], label=str(r_idx + 1))
168
+
169
+ # Update info pill
170
+ total_rows = len(self._sheets.get(sheet_name, []))
171
+ info = self.query_one("#cct-sheet-info", Static)
172
+ base = os.path.basename(self.path)
173
+ info.update(f"📊 {base} · {sheet_name} ({total_rows} rows)")
174
+ except Exception:
175
+ pass
176
+
177
+ def on_input_changed(self, event: Input.Changed) -> None:
178
+ if event.input.id == "cct-sheet-filter":
179
+ self._populate_table(event.value.strip())
180
+
181
+ def on_button_pressed(self, event: Button.Pressed) -> None:
182
+ bid = event.button.id
183
+ if bid == "cct-sheet-close":
184
+ self._close_tab()
185
+ elif bid == "cct-sheet-open":
186
+ try:
187
+ import pathlib, webbrowser
188
+ webbrowser.open(pathlib.Path(self.path).as_uri())
189
+ except Exception:
190
+ pass
191
+
192
+ def _close_tab(self) -> None:
193
+ try:
194
+ from ..ui.editor import EditorPane
195
+ node = self._parent
196
+ while node is not None and not isinstance(node, EditorPane):
197
+ node = getattr(node, "_parent", None)
198
+ if isinstance(node, EditorPane):
199
+ for p, tid in list(node._open_paths.items()):
200
+ if os.path.normcase(p) == os.path.normcase(self.path):
201
+ node.close_active(tid)
202
+ break
203
+ except Exception:
204
+ pass