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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- 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
|