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,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
|