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 @@
1
+ """CAT Universal File Preview Engine & Document Viewers."""
@@ -0,0 +1,252 @@
1
+ """CAT Universal File Preview Engine — DOCX Document Viewer (calc_terminal/viewers/document_viewer.py).
2
+
3
+ High-fidelity visual DOCX document renderer:
4
+ - Displays actual visual document structure (headings, styled paragraphs, formatted tables, lists).
5
+ - Parses WordprocessingML (word/document.xml) natively via stdlib zipfile and xml.etree.ElementTree.
6
+ - Supports zoom, page scrolling, and external open.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import xml.etree.ElementTree as ET
13
+ import zipfile
14
+ from typing import List, Optional
15
+
16
+ TEXTUAL_AVAILABLE = True
17
+ try:
18
+ from rich.console import Group
19
+ from rich.panel import Panel
20
+ from rich.table import Table
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
+ W_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
29
+
30
+
31
+ class DocumentViewer(Vertical):
32
+ """Visual DOCX document renderer tab."""
33
+
34
+ def __init__(self, path: str, **kwargs) -> None:
35
+ super().__init__(**kwargs)
36
+ self.path = os.path.abspath(path)
37
+ self._zoom: float = 1.0
38
+ self._elements: List[dict] = []
39
+ self._load_elements()
40
+
41
+ def _load_elements(self) -> List[dict]:
42
+ self._elements = []
43
+ if not os.path.isfile(self.path):
44
+ return self._elements
45
+ try:
46
+ with zipfile.ZipFile(self.path, "r") as docx_zip:
47
+ if "word/document.xml" not in docx_zip.namelist():
48
+ return self._elements
49
+ xml_content = docx_zip.read("word/document.xml")
50
+ root = ET.fromstring(xml_content)
51
+ body = root.find(f"{W_NS}body")
52
+ if body is None:
53
+ return self._elements
54
+ for child in body:
55
+ tag = child.tag
56
+ if tag == f"{W_NS}p":
57
+ style_elem = child.find(f".//{W_NS}pStyle")
58
+ style_val = style_elem.get(f"{W_NS}val", "") if style_elem is not None else ""
59
+ is_heading = style_val.lower().startswith("heading") or "title" in style_val.lower()
60
+ runs = []
61
+ for r in child.findall(f"{W_NS}r"):
62
+ t_elem = r.find(f"{W_NS}t")
63
+ if t_elem is not None and t_elem.text:
64
+ runs.append(t_elem.text)
65
+ full_text = "".join(runs).strip()
66
+ if full_text:
67
+ self._elements.append({
68
+ "type": "heading" if is_heading else "paragraph",
69
+ "style": style_val,
70
+ "text": full_text,
71
+ })
72
+ elif tag == f"{W_NS}tbl":
73
+ table_data = []
74
+ for tr in child.findall(f"{W_NS}tr"):
75
+ row_cells = []
76
+ for tc in tr.findall(f"{W_NS}tc"):
77
+ cell_texts = []
78
+ for cp in tc.findall(f"{W_NS}p"):
79
+ for cr in cp.findall(f"{W_NS}r"):
80
+ ct = cr.find(f"{W_NS}t")
81
+ if ct is not None and ct.text:
82
+ cell_texts.append(ct.text)
83
+ row_cells.append(" ".join(cell_texts).strip())
84
+ if any(row_cells):
85
+ table_data.append(row_cells)
86
+ if table_data:
87
+ self._elements.append({
88
+ "type": "table",
89
+ "rows": table_data,
90
+ })
91
+ except Exception:
92
+ pass
93
+ return self._elements
94
+
95
+ def compose(self):
96
+ with Horizontal(id="cct-docx-toolbar", classes="cct-docx-toolbar"):
97
+ yield Static(os.path.basename(self.path), id="cct-docx-info", classes="cct-tb-pill")
98
+ yield Button("−", id="cct-docx-zoomout", classes="cct-ctrl", tooltip="Zoom Out")
99
+ yield Button("+", id="cct-docx-zoomin", classes="cct-ctrl", tooltip="Zoom In")
100
+ yield Button("100%", id="cct-docx-reset", classes="cct-ctrl")
101
+ yield Button("Open Externally", id="cct-docx-open", classes="cct-ctrl")
102
+ yield Button("✕", id="cct-docx-close", classes="cct-ctrl")
103
+ with VerticalScroll(id="cct-docx-scroll"):
104
+ yield Static("", id="cct-docx-display", classes="cct-docx-display")
105
+
106
+ def on_mount(self) -> None:
107
+ self._render_document()
108
+
109
+ def _render_document(self) -> None:
110
+ try:
111
+ disp = self.query_one("#cct-docx-display", Static)
112
+ if not os.path.isfile(self.path):
113
+ disp.update("[red]File not found.[/red]")
114
+ return
115
+
116
+ elements = []
117
+ with zipfile.ZipFile(self.path, "r") as docx_zip:
118
+ if "word/document.xml" not in docx_zip.namelist():
119
+ disp.update("[red]Invalid DOCX: word/document.xml missing.[/red]")
120
+ return
121
+ xml_content = docx_zip.read("word/document.xml")
122
+ root = ET.fromstring(xml_content)
123
+ body = root.find(f"{W_NS}body")
124
+ if body is None:
125
+ disp.update("[dim]Empty document body.[/dim]")
126
+ return
127
+
128
+ for child in body:
129
+ tag = child.tag
130
+ if tag == f"{W_NS}p":
131
+ # Paragraph or Heading
132
+ p_elem = self._parse_paragraph(child)
133
+ if p_elem:
134
+ elements.append(p_elem)
135
+ elif tag == f"{W_NS}tbl":
136
+ # Table
137
+ tbl_elem = self._parse_table(child)
138
+ if tbl_elem:
139
+ elements.append(tbl_elem)
140
+
141
+ if elements:
142
+ # Wrap in styled document sheet panel
143
+ base = os.path.basename(self.path)
144
+ doc_group = Group(*elements)
145
+ sheet_panel = Panel(
146
+ doc_group,
147
+ title=f"[bold blue]📄 {base}[/bold blue]",
148
+ border_style="cyan",
149
+ padding=(1, 2),
150
+ )
151
+ disp.update(sheet_panel)
152
+ else:
153
+ disp.update("[italic dim]No displayable text or tables in document.[/italic dim]")
154
+ except Exception as e:
155
+ try:
156
+ self.query_one("#cct-docx-display", Static).update(f"[red]Error rendering DOCX: {e}[/red]")
157
+ except Exception:
158
+ pass
159
+
160
+ def _parse_paragraph(self, p_node: ET.Element) -> Optional[Any]:
161
+ """Parse Word paragraph into Rich Text or Heading."""
162
+ # Check for heading style
163
+ p_style = p_node.find(f"{W_NS}pPr/{W_NS}pStyle")
164
+ style_val = p_style.get(f"{W_NS}val", "") if p_style is not None else ""
165
+ is_heading1 = "Heading1" in style_val or "heading 1" in style_val.lower()
166
+ is_heading2 = "Heading2" in style_val or "heading 2" in style_val.lower()
167
+
168
+ full_text = Text()
169
+ for r_node in p_node.findall(f"{W_NS}r"):
170
+ r_pr = r_node.find(f"{W_NS}rPr")
171
+ is_bold = r_pr is not None and r_pr.find(f"{W_NS}b") is not None
172
+ is_italic = r_pr is not None and r_pr.find(f"{W_NS}i") is not None
173
+ t_node = r_node.find(f"{W_NS}t")
174
+ if t_node is not None and t_node.text:
175
+ style = ""
176
+ if is_heading1:
177
+ style = "bold cyan"
178
+ elif is_heading2:
179
+ style = "bold green"
180
+ elif is_bold and is_italic:
181
+ style = "bold italic"
182
+ elif is_bold:
183
+ style = "bold"
184
+ elif is_italic:
185
+ style = "italic"
186
+ full_text.append(t_node.text, style=style)
187
+
188
+ if not full_text.plain.strip():
189
+ return Text("") # blank spacing line
190
+ return full_text
191
+
192
+ def _parse_table(self, tbl_node: ET.Element) -> Optional[Table]:
193
+ """Parse Word table into a formatted Rich Table."""
194
+ table = Table(show_header=True, header_style="bold magenta", border_style="dim")
195
+ rows_data = []
196
+ for tr in tbl_node.findall(f"{W_NS}tr"):
197
+ row = []
198
+ for tc in tr.findall(f"{W_NS}tc"):
199
+ cell_texts = []
200
+ for p in tc.findall(f"{W_NS}p"):
201
+ p_text = "".join(t.text or "" for t in p.findall(f".//{W_NS}t"))
202
+ if p_text:
203
+ cell_texts.append(p_text)
204
+ row.append("\n".join(cell_texts))
205
+ rows_data.append(row)
206
+
207
+ if not rows_data:
208
+ return None
209
+
210
+ max_cols = max(len(r) for r in rows_data)
211
+ for i in range(max_cols):
212
+ header = rows_data[0][i] if len(rows_data[0]) > i and rows_data[0][i] else f"Col {i + 1}"
213
+ table.add_column(header)
214
+
215
+ for row in rows_data[1:]:
216
+ padded = row + [""] * (max_cols - len(row))
217
+ table.add_row(*padded[:max_cols])
218
+ return table
219
+
220
+ def on_button_pressed(self, event: Button.Pressed) -> None:
221
+ bid = event.button.id
222
+ if bid == "cct-docx-close":
223
+ self._close_tab()
224
+ elif bid == "cct-docx-zoomin":
225
+ self._zoom = min(2.5, self._zoom * 1.2)
226
+ self._render_document()
227
+ elif bid == "cct-docx-zoomout":
228
+ self._zoom = max(0.5, self._zoom / 1.2)
229
+ self._render_document()
230
+ elif bid == "cct-docx-reset":
231
+ self._zoom = 1.0
232
+ self._render_document()
233
+ elif bid == "cct-docx-open":
234
+ try:
235
+ import pathlib, webbrowser
236
+ webbrowser.open(pathlib.Path(self.path).as_uri())
237
+ except Exception:
238
+ pass
239
+
240
+ def _close_tab(self) -> None:
241
+ try:
242
+ from ..ui.editor import EditorPane
243
+ node = self._parent
244
+ while node is not None and not isinstance(node, EditorPane):
245
+ node = getattr(node, "_parent", None)
246
+ if isinstance(node, EditorPane):
247
+ for p, tid in list(node._open_paths.items()):
248
+ if os.path.normcase(p) == os.path.normcase(self.path):
249
+ node.close_active(tid)
250
+ break
251
+ except Exception:
252
+ pass
@@ -0,0 +1,241 @@
1
+ """CAT Universal File Preview Engine — Image Viewer (calc_terminal/viewers/image_viewer.py).
2
+
3
+ High-fidelity image viewer:
4
+ - Supports PNG, JPG, JPEG, WEBP, GIF, SVG, BMP, ICO, TIFF.
5
+ - Zoom controls: 25%, 50%, 100%, 200%, fit screen, fit width.
6
+ - 90-degree rotation, pan scrolling, dimensions, file size, transparency visualization.
7
+ - High-quality LANCZOS rendering with terminal truecolor half-blocks.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import re
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, Static
23
+ except Exception:
24
+ TEXTUAL_AVAILABLE = False
25
+ Vertical = object # type: ignore
26
+
27
+
28
+ class ImageViewer(Vertical):
29
+ """High-quality visual image viewer tab."""
30
+
31
+ def __init__(self, path: str, **kwargs) -> None:
32
+ super().__init__(**kwargs)
33
+ self.path = os.path.abspath(path)
34
+ self._zoom: float = 1.0
35
+ self._fit: bool = False
36
+ self._rotation: int = 0 # 0, 90, 180, 270
37
+
38
+ def compose(self):
39
+ with Horizontal(id="cct-image-toolbar", classes="cct-image-toolbar"):
40
+ yield Static("", id="cct-image-info", classes="cct-tb-pill")
41
+ yield Button("−", id="cct-image-zoomout", classes="cct-ctrl", tooltip="Zoom Out")
42
+ yield Button("+", id="cct-image-zoomin", classes="cct-ctrl", tooltip="Zoom In")
43
+ yield Button("25%", id="cct-image-z25", classes="cct-ctrl")
44
+ yield Button("50%", id="cct-image-z50", classes="cct-ctrl")
45
+ yield Button("100%", id="cct-image-reset", classes="cct-ctrl", tooltip="Actual size (100%)")
46
+ yield Button("200%", id="cct-image-z200", classes="cct-ctrl")
47
+ yield Button("Fit", id="cct-image-fit", classes="cct-ctrl", tooltip="Fit to screen")
48
+ yield Button("↻", id="cct-image-rotate", classes="cct-ctrl", tooltip="Rotate 90°")
49
+ yield Button("✕", id="cct-image-close", classes="cct-ctrl")
50
+ with VerticalScroll(id="cct-image-scroll"):
51
+ yield Static("", id="cct-image-display", classes="cct-image-display")
52
+
53
+ def on_mount(self) -> None:
54
+ self._refresh_info()
55
+ self._refresh_display()
56
+
57
+ def _get_info(self) -> Tuple[int, int, str, bool, str]:
58
+ """Return (width, height, size_str, has_alpha, format_name)."""
59
+ try:
60
+ size = os.path.getsize(self.path)
61
+ if size < 1024:
62
+ size_str = f"{size} B"
63
+ elif size < 1024 * 1024:
64
+ size_str = f"{size / 1024:.1f} KB"
65
+ else:
66
+ size_str = f"{size / (1024 * 1024):.1f} MB"
67
+ except Exception:
68
+ size_str = "? B"
69
+
70
+ w, h = 0, 0
71
+ has_alpha = False
72
+ fmt = ""
73
+
74
+ try:
75
+ from PIL import Image
76
+ with Image.open(self.path) as im:
77
+ w, h = im.size
78
+ fmt = im.format or ""
79
+ has_alpha = im.mode in ("RGBA", "LA") or "transparency" in im.info
80
+ except Exception:
81
+ if self.path.lower().endswith(".svg"):
82
+ try:
83
+ with open(self.path, "r", encoding="utf-8", errors="ignore") as f:
84
+ txt = f.read(4096)
85
+ m = re.search(r'width="(\d+)"', txt)
86
+ n = re.search(r'height="(\d+)"', txt)
87
+ if m and n:
88
+ w, h = int(m.group(1)), int(n.group(1))
89
+ fmt = "SVG"
90
+ except Exception:
91
+ pass
92
+ return w, h, size_str, has_alpha, fmt
93
+
94
+ def _refresh_info(self) -> None:
95
+ try:
96
+ w, h, size_str, has_alpha, fmt = self._get_info()
97
+ info = f"{w}×{h} {size_str}" if w and h else size_str
98
+ if fmt:
99
+ info = f"{fmt} · " + info
100
+ if has_alpha:
101
+ info += " · alpha"
102
+ if self._rotation:
103
+ info += f" · {self._rotation}°"
104
+ zoom_str = "Fit" if self._fit else f"{int(self._zoom * 100)}%"
105
+ info += f" · {zoom_str}"
106
+ self.query_one("#cct-image-info", Static).update(info)
107
+ except Exception:
108
+ pass
109
+
110
+ def _render_ascii_preview(self) -> Optional[Text]:
111
+ """Render the image using truecolor half-blocks ('▄') for high vertical density."""
112
+ try:
113
+ from PIL import Image
114
+ with Image.open(self.path) as im:
115
+ if self._rotation:
116
+ im = im.rotate(-self._rotation, expand=True)
117
+
118
+ has_alpha = im.mode in ("RGBA", "LA") or "transparency" in im.info
119
+ if has_alpha:
120
+ # Checkerboard background simulation for transparency
121
+ checker = Image.new("RGBA", im.size, (48, 48, 48, 255))
122
+ im = Image.alpha_composite(checker, im.convert("RGBA")).convert("RGB")
123
+ elif im.mode != "RGB":
124
+ im = im.convert("RGB")
125
+
126
+ iw, ih = im.size
127
+ if iw <= 0 or ih <= 0:
128
+ return None
129
+
130
+ resample = getattr(Image, "LANCZOS", getattr(Image, "BILINEAR", 1))
131
+ if self._fit:
132
+ max_w, max_h = 80, 34
133
+ im.thumbnail((max_w, max_h), resample)
134
+ else:
135
+ target_w = max(1, int(iw * self._zoom * 0.15))
136
+ target_h = max(1, int(ih * self._zoom * 0.15))
137
+ # Prevent runaway memory allocation on 8K images
138
+ target_w = min(target_w, 400)
139
+ target_h = min(target_h, 300)
140
+ im = im.resize((target_w, target_h), resample)
141
+
142
+ tw, th = im.size
143
+ if tw <= 0 or th <= 0:
144
+ return None
145
+
146
+ txt = Text()
147
+ for y in range(0, th, 2):
148
+ for x in range(tw):
149
+ r1, g1, b1 = im.getpixel((x, y))
150
+ if y + 1 < th:
151
+ r2, g2, b2 = im.getpixel((x, y + 1))
152
+ else:
153
+ r2, g2, b2 = r1, g1, b1
154
+ # ▄: top half is background, bottom half is foreground
155
+ style = Style(color=f"#{r2:02x}{g2:02x}{b2:02x}", bgcolor=f"#{r1:02x}{g1:02x}{b1:02x}")
156
+ txt.append("▄", style=style)
157
+ if y + 2 < th:
158
+ txt.append("\n")
159
+ return txt
160
+ except Exception:
161
+ return None
162
+
163
+ def _refresh_display(self) -> None:
164
+ try:
165
+ disp = self.query_one("#cct-image-display", Static)
166
+ w, h, size_str, has_alpha, fmt = self._get_info()
167
+ preview = self._render_ascii_preview()
168
+
169
+ if preview is not None:
170
+ base = os.path.basename(self.path)
171
+ header = Text(f"🖼 {base} ({w}×{h}, {size_str})", style="bold")
172
+ footer = Text(f"Path: {self.path}", style="dim")
173
+ parts = [header, Text(""), preview, Text(""), footer]
174
+ disp.update(Group(*parts))
175
+ else:
176
+ base = os.path.basename(self.path)
177
+ disp.update(
178
+ f"[b]🖼 {base}[/b]\n[dim]{w}×{h} · {size_str}[/dim]\n\n"
179
+ f"[yellow]High-resolution image viewer available via Pillow.[/yellow]\n"
180
+ f"[dim]Path: {self.path}[/dim]"
181
+ )
182
+ except Exception:
183
+ pass
184
+
185
+ def on_button_pressed(self, event: Button.Pressed) -> None:
186
+ bid = event.button.id
187
+ if bid == "cct-image-close":
188
+ self._close_tab()
189
+ elif bid == "cct-image-zoomin":
190
+ self._fit = False
191
+ self._zoom = min(4.0, self._zoom * 1.25)
192
+ self._refresh_info()
193
+ self._refresh_display()
194
+ elif bid == "cct-image-zoomout":
195
+ self._fit = False
196
+ self._zoom = max(0.2, self._zoom / 1.25)
197
+ self._refresh_info()
198
+ self._refresh_display()
199
+ elif bid == "cct-image-z25":
200
+ self._fit = False
201
+ self._zoom = 0.25
202
+ self._refresh_info()
203
+ self._refresh_display()
204
+ elif bid == "cct-image-z50":
205
+ self._fit = False
206
+ self._zoom = 0.50
207
+ self._refresh_info()
208
+ self._refresh_display()
209
+ elif bid == "cct-image-reset":
210
+ self._fit = False
211
+ self._zoom = 1.0
212
+ self._refresh_info()
213
+ self._refresh_display()
214
+ elif bid == "cct-image-z200":
215
+ self._fit = False
216
+ self._zoom = 2.0
217
+ self._refresh_info()
218
+ self._refresh_display()
219
+ elif bid == "cct-image-fit":
220
+ self._fit = True
221
+ self._refresh_info()
222
+ self._refresh_display()
223
+ elif bid == "cct-image-rotate":
224
+ self._rotation = (self._rotation + 90) % 360
225
+ self._refresh_info()
226
+ self._refresh_display()
227
+
228
+ def _close_tab(self) -> None:
229
+ """Find parent EditorPane and close this tab."""
230
+ try:
231
+ from ..ui.editor import EditorPane
232
+ node = self._parent
233
+ while node is not None and not isinstance(node, EditorPane):
234
+ node = getattr(node, "_parent", None)
235
+ if isinstance(node, EditorPane):
236
+ for p, tid in list(node._open_paths.items()):
237
+ if os.path.normcase(p) == os.path.normcase(self.path):
238
+ node.close_active(tid)
239
+ break
240
+ except Exception:
241
+ pass