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,491 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Desktop Platform Implementation
|
|
3
|
+
|
|
4
|
+
Native Python implementation for Windows/Linux/macOS.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, BinaryIO, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import tkinter as tk
|
|
20
|
+
from tkinter import filedialog
|
|
21
|
+
HAS_TKINTER = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
HAS_TKINTER = False
|
|
24
|
+
|
|
25
|
+
from . import (
|
|
26
|
+
PlatformBase,
|
|
27
|
+
PlatformInfo,
|
|
28
|
+
PlatformType,
|
|
29
|
+
FileHandle,
|
|
30
|
+
DirectoryHandle,
|
|
31
|
+
CommandResult,
|
|
32
|
+
ProcessHandle,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DesktopFileHandle(FileHandle):
|
|
37
|
+
"""File handle for desktop platform."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, file: BinaryIO, path: str):
|
|
40
|
+
self._file = file
|
|
41
|
+
self._path = path
|
|
42
|
+
|
|
43
|
+
async def read(self, size: int = -1) -> bytes:
|
|
44
|
+
loop = asyncio.get_event_loop()
|
|
45
|
+
return await loop.run_in_executor(None, lambda: self._file.read(size))
|
|
46
|
+
|
|
47
|
+
async def write(self, data: Union[bytes, str]) -> int:
|
|
48
|
+
loop = asyncio.get_event_loop()
|
|
49
|
+
if isinstance(data, str):
|
|
50
|
+
data = data.encode("utf-8")
|
|
51
|
+
return await loop.run_in_executor(None, lambda: self._file.write(data))
|
|
52
|
+
|
|
53
|
+
async def close(self) -> None:
|
|
54
|
+
loop = asyncio.get_event_loop()
|
|
55
|
+
await loop.run_in_executor(None, self._file.close)
|
|
56
|
+
|
|
57
|
+
async def seek(self, offset: int, whence: int = 0) -> int:
|
|
58
|
+
loop = asyncio.get_event_loop()
|
|
59
|
+
return await loop.run_in_executor(None, lambda: self._file.seek(offset, whence))
|
|
60
|
+
|
|
61
|
+
def tell(self) -> int:
|
|
62
|
+
return self._file.tell()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DesktopDirectoryHandle(DirectoryHandle):
|
|
66
|
+
"""Directory handle for desktop platform."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, path: str):
|
|
69
|
+
self._path = Path(path).resolve()
|
|
70
|
+
|
|
71
|
+
async def get_file_handle(self, name: str, create: bool = False) -> FileHandle:
|
|
72
|
+
path = self._path / name
|
|
73
|
+
mode = "w+b" if create else "r+b"
|
|
74
|
+
if not path.exists() and not create:
|
|
75
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
76
|
+
file = await asyncio.get_event_loop().run_in_executor(None, lambda: open(path, mode))
|
|
77
|
+
return DesktopFileHandle(file, str(path))
|
|
78
|
+
|
|
79
|
+
async def get_directory_handle(self, name: str, create: bool = False) -> DirectoryHandle:
|
|
80
|
+
path = self._path / name
|
|
81
|
+
if create:
|
|
82
|
+
await asyncio.get_event_loop().run_in_executor(None, lambda: path.mkdir(parents=True, exist_ok=True))
|
|
83
|
+
elif not path.exists():
|
|
84
|
+
raise FileNotFoundError(f"Directory not found: {path}")
|
|
85
|
+
return DesktopDirectoryHandle(str(path))
|
|
86
|
+
|
|
87
|
+
async def remove_entry(self, name: str, recursive: bool = False) -> None:
|
|
88
|
+
path = self._path / name
|
|
89
|
+
if path.is_dir():
|
|
90
|
+
if recursive:
|
|
91
|
+
await asyncio.get_event_loop().run_in_executor(None, lambda: shutil.rmtree(path))
|
|
92
|
+
else:
|
|
93
|
+
await asyncio.get_event_loop().run_in_executor(None, lambda: path.rmdir())
|
|
94
|
+
else:
|
|
95
|
+
await asyncio.get_event_loop().run_in_executor(None, lambda: path.unlink())
|
|
96
|
+
|
|
97
|
+
async def list_entries(self) -> List[str]:
|
|
98
|
+
loop = asyncio.get_event_loop()
|
|
99
|
+
return await loop.run_in_executor(None, lambda: [p.name for p in self._path.iterdir()])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DesktopProcessHandle(ProcessHandle):
|
|
103
|
+
"""Process handle for desktop platform."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, process: subprocess.Popen):
|
|
106
|
+
self._process = process
|
|
107
|
+
|
|
108
|
+
async def wait(self, timeout: Optional[float] = None) -> CommandResult:
|
|
109
|
+
loop = asyncio.get_event_loop()
|
|
110
|
+
try:
|
|
111
|
+
returncode = await loop.run_in_executor(
|
|
112
|
+
None, lambda: self._process.wait(timeout=timeout)
|
|
113
|
+
)
|
|
114
|
+
stdout, stderr = await loop.run_in_executor(None, self._process.communicate)
|
|
115
|
+
return CommandResult(
|
|
116
|
+
returncode=returncode,
|
|
117
|
+
stdout=stdout.decode("utf-8", errors="replace") if stdout else "",
|
|
118
|
+
stderr=stderr.decode("utf-8", errors="replace") if stderr else "",
|
|
119
|
+
success=returncode == 0,
|
|
120
|
+
)
|
|
121
|
+
except subprocess.TimeoutExpired:
|
|
122
|
+
return CommandResult(
|
|
123
|
+
returncode=-1,
|
|
124
|
+
stdout="",
|
|
125
|
+
stderr="Process timed out",
|
|
126
|
+
success=False,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def terminate(self) -> None:
|
|
130
|
+
loop = asyncio.get_event_loop()
|
|
131
|
+
await loop.run_in_executor(None, self._process.terminate)
|
|
132
|
+
|
|
133
|
+
async def kill(self) -> None:
|
|
134
|
+
loop = asyncio.get_event_loop()
|
|
135
|
+
await loop.run_in_executor(None, self._process.kill)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def pid(self) -> Optional[int]:
|
|
139
|
+
return self._process.pid
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def returncode(self) -> Optional[int]:
|
|
143
|
+
return self._process.returncode
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DesktopPlatform(PlatformBase):
|
|
147
|
+
"""Desktop platform implementation."""
|
|
148
|
+
|
|
149
|
+
def __init__(self):
|
|
150
|
+
self._info = PlatformInfo(
|
|
151
|
+
type=PlatformType.DESKTOP,
|
|
152
|
+
name="CAT Desktop",
|
|
153
|
+
version="0.7.9.0",
|
|
154
|
+
is_mobile=False,
|
|
155
|
+
supports_file_system_access=True,
|
|
156
|
+
supports_notifications=True,
|
|
157
|
+
supports_background_sync=False,
|
|
158
|
+
user_agent="",
|
|
159
|
+
)
|
|
160
|
+
self._tk_root: Optional[tk.Tk] = None
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def info(self) -> PlatformInfo:
|
|
164
|
+
return self._info
|
|
165
|
+
|
|
166
|
+
# --- Filesystem ---
|
|
167
|
+
|
|
168
|
+
async def get_root_directory(self) -> DirectoryHandle:
|
|
169
|
+
# Use current working directory as workspace root
|
|
170
|
+
cwd = os.getcwd()
|
|
171
|
+
return DesktopDirectoryHandle(cwd)
|
|
172
|
+
|
|
173
|
+
async def pick_directory(self, title: str = "Select folder") -> Optional[DirectoryHandle]:
|
|
174
|
+
if not HAS_TKINTER:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
loop = asyncio.get_event_loop()
|
|
178
|
+
|
|
179
|
+
def _pick():
|
|
180
|
+
if self._tk_root is None:
|
|
181
|
+
root = tk.Tk()
|
|
182
|
+
root.withdraw()
|
|
183
|
+
self._tk_root = root
|
|
184
|
+
else:
|
|
185
|
+
root = self._tk_root
|
|
186
|
+
|
|
187
|
+
path = filedialog.askdirectory(title=title, master=root)
|
|
188
|
+
return path if path else None
|
|
189
|
+
|
|
190
|
+
path = await loop.run_in_executor(None, _pick)
|
|
191
|
+
if path:
|
|
192
|
+
return DesktopDirectoryHandle(path)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
async def pick_file(self, title: str = "Select file", accept: Optional[dict] = None) -> Optional[FileHandle]:
|
|
196
|
+
if not HAS_TKINTER:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
loop = asyncio.get_event_loop()
|
|
200
|
+
|
|
201
|
+
def _pick():
|
|
202
|
+
if self._tk_root is None:
|
|
203
|
+
root = tk.Tk()
|
|
204
|
+
root.withdraw()
|
|
205
|
+
self._tk_root = root
|
|
206
|
+
else:
|
|
207
|
+
root = self._tk_root
|
|
208
|
+
|
|
209
|
+
filetypes = []
|
|
210
|
+
if accept:
|
|
211
|
+
for desc, exts in accept.items():
|
|
212
|
+
filetypes.append((desc, " ".join(f"*{ext}" for ext in exts)))
|
|
213
|
+
filetypes.append(("All files", "*.*"))
|
|
214
|
+
|
|
215
|
+
path = filedialog.askopenfilename(title=title, filetypes=filetypes, master=root)
|
|
216
|
+
return path if path else None
|
|
217
|
+
|
|
218
|
+
path = await loop.run_in_executor(None, _pick)
|
|
219
|
+
if path:
|
|
220
|
+
file = await loop.run_in_executor(None, lambda: open(path, "r+b"))
|
|
221
|
+
return DesktopFileHandle(file, path)
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
async def resolve_path(self, path: str) -> str:
|
|
225
|
+
loop = asyncio.get_event_loop()
|
|
226
|
+
return await loop.run_in_executor(None, lambda: str(Path(path).expanduser().resolve()))
|
|
227
|
+
|
|
228
|
+
async def path_exists(self, path: str) -> bool:
|
|
229
|
+
loop = asyncio.get_event_loop()
|
|
230
|
+
return await loop.run_in_executor(None, lambda: Path(path).exists())
|
|
231
|
+
|
|
232
|
+
async def is_directory(self, path: str) -> bool:
|
|
233
|
+
loop = asyncio.get_event_loop()
|
|
234
|
+
return await loop.run_in_executor(None, lambda: Path(path).is_dir())
|
|
235
|
+
|
|
236
|
+
async def is_file(self, path: str) -> bool:
|
|
237
|
+
loop = asyncio.get_event_loop()
|
|
238
|
+
return await loop.run_in_executor(None, lambda: Path(path).is_file())
|
|
239
|
+
|
|
240
|
+
async def list_directory(self, path: str) -> List[str]:
|
|
241
|
+
loop = asyncio.get_event_loop()
|
|
242
|
+
return await loop.run_in_executor(None, lambda: [p.name for p in Path(path).iterdir()])
|
|
243
|
+
|
|
244
|
+
async def read_file(self, path: str, encoding: Optional[str] = None) -> Union[str, bytes]:
|
|
245
|
+
loop = asyncio.get_event_loop()
|
|
246
|
+
if encoding:
|
|
247
|
+
return await loop.run_in_executor(None, lambda: Path(path).read_text(encoding=encoding))
|
|
248
|
+
return await loop.run_in_executor(None, lambda: Path(path).read_bytes())
|
|
249
|
+
|
|
250
|
+
async def write_file(self, path: str, data: Union[str, bytes], encoding: Optional[str] = None) -> int:
|
|
251
|
+
loop = asyncio.get_event_loop()
|
|
252
|
+
p = Path(path)
|
|
253
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
if isinstance(data, str):
|
|
255
|
+
return await loop.run_in_executor(None, lambda: p.write_text(data, encoding=encoding or "utf-8"))
|
|
256
|
+
return await loop.run_in_executor(None, lambda: p.write_bytes(data))
|
|
257
|
+
|
|
258
|
+
async def delete_file(self, path: str) -> None:
|
|
259
|
+
loop = asyncio.get_event_loop()
|
|
260
|
+
await loop.run_in_executor(None, lambda: Path(path).unlink(missing_ok=True))
|
|
261
|
+
|
|
262
|
+
async def delete_directory(self, path: str, recursive: bool = False) -> None:
|
|
263
|
+
loop = asyncio.get_event_loop()
|
|
264
|
+
if recursive:
|
|
265
|
+
await loop.run_in_executor(None, lambda: shutil.rmtree(path, ignore_errors=True))
|
|
266
|
+
else:
|
|
267
|
+
await loop.run_in_executor(None, lambda: Path(path).rmdir())
|
|
268
|
+
|
|
269
|
+
async def create_directory(self, path: str, parents: bool = True) -> None:
|
|
270
|
+
loop = asyncio.get_event_loop()
|
|
271
|
+
await loop.run_in_executor(None, lambda: Path(path).mkdir(parents=parents, exist_ok=True))
|
|
272
|
+
|
|
273
|
+
async def copy_file(self, src: str, dst: str) -> None:
|
|
274
|
+
loop = asyncio.get_event_loop()
|
|
275
|
+
await loop.run_in_executor(None, lambda: shutil.copy2(src, dst))
|
|
276
|
+
|
|
277
|
+
async def move_file(self, src: str, dst: str) -> None:
|
|
278
|
+
loop = asyncio.get_event_loop()
|
|
279
|
+
await loop.run_in_executor(None, lambda: shutil.move(src, dst))
|
|
280
|
+
|
|
281
|
+
async def get_file_size(self, path: str) -> int:
|
|
282
|
+
loop = asyncio.get_event_loop()
|
|
283
|
+
return await loop.run_in_executor(None, lambda: Path(path).stat().st_size)
|
|
284
|
+
|
|
285
|
+
async def get_file_mtime(self, path: str) -> float:
|
|
286
|
+
loop = asyncio.get_event_loop()
|
|
287
|
+
return await loop.run_in_executor(None, lambda: Path(path).stat().st_mtime)
|
|
288
|
+
|
|
289
|
+
# --- Process Execution ---
|
|
290
|
+
|
|
291
|
+
async def run_command(
|
|
292
|
+
self,
|
|
293
|
+
cmd: Union[str, List[str]],
|
|
294
|
+
cwd: Optional[str] = None,
|
|
295
|
+
env: Optional[dict] = None,
|
|
296
|
+
capture_output: bool = True,
|
|
297
|
+
timeout: Optional[float] = None,
|
|
298
|
+
) -> CommandResult:
|
|
299
|
+
loop = asyncio.get_event_loop()
|
|
300
|
+
|
|
301
|
+
def _run():
|
|
302
|
+
if isinstance(cmd, str):
|
|
303
|
+
shell = True
|
|
304
|
+
cmd_str = cmd
|
|
305
|
+
else:
|
|
306
|
+
shell = False
|
|
307
|
+
cmd_str = cmd
|
|
308
|
+
|
|
309
|
+
merged_env = os.environ.copy()
|
|
310
|
+
if env:
|
|
311
|
+
merged_env.update(env)
|
|
312
|
+
|
|
313
|
+
proc = subprocess.Popen(
|
|
314
|
+
cmd_str,
|
|
315
|
+
shell=shell,
|
|
316
|
+
cwd=cwd,
|
|
317
|
+
env=merged_env,
|
|
318
|
+
stdout=subprocess.PIPE if capture_output else None,
|
|
319
|
+
stderr=subprocess.PIPE if capture_output else None,
|
|
320
|
+
stdin=subprocess.DEVNULL,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
stdout, stderr = proc.communicate(timeout=timeout)
|
|
325
|
+
return CommandResult(
|
|
326
|
+
returncode=proc.returncode,
|
|
327
|
+
stdout=stdout.decode("utf-8", errors="replace") if stdout else "",
|
|
328
|
+
stderr=stderr.decode("utf-8", errors="replace") if stderr else "",
|
|
329
|
+
success=proc.returncode == 0,
|
|
330
|
+
)
|
|
331
|
+
except subprocess.TimeoutExpired:
|
|
332
|
+
proc.kill()
|
|
333
|
+
stdout, stderr = proc.communicate()
|
|
334
|
+
return CommandResult(
|
|
335
|
+
returncode=-1,
|
|
336
|
+
stdout=stdout.decode("utf-8", errors="replace") if stdout else "",
|
|
337
|
+
stderr=stderr.decode("utf-8", errors="replace") if stderr else "",
|
|
338
|
+
success=False,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return await loop.run_in_executor(None, _run)
|
|
342
|
+
|
|
343
|
+
async def start_process(
|
|
344
|
+
self,
|
|
345
|
+
cmd: Union[str, List[str]],
|
|
346
|
+
cwd: Optional[str] = None,
|
|
347
|
+
env: Optional[dict] = None,
|
|
348
|
+
) -> ProcessHandle:
|
|
349
|
+
loop = asyncio.get_event_loop()
|
|
350
|
+
|
|
351
|
+
def _start():
|
|
352
|
+
if isinstance(cmd, str):
|
|
353
|
+
shell = True
|
|
354
|
+
cmd_str = cmd
|
|
355
|
+
else:
|
|
356
|
+
shell = False
|
|
357
|
+
cmd_str = cmd
|
|
358
|
+
|
|
359
|
+
merged_env = os.environ.copy()
|
|
360
|
+
if env:
|
|
361
|
+
merged_env.update(env)
|
|
362
|
+
|
|
363
|
+
proc = subprocess.Popen(
|
|
364
|
+
cmd_str,
|
|
365
|
+
shell=shell,
|
|
366
|
+
cwd=cwd,
|
|
367
|
+
env=merged_env,
|
|
368
|
+
stdout=subprocess.PIPE,
|
|
369
|
+
stderr=subprocess.PIPE,
|
|
370
|
+
stdin=subprocess.DEVNULL,
|
|
371
|
+
)
|
|
372
|
+
return proc
|
|
373
|
+
|
|
374
|
+
proc = await loop.run_in_executor(None, _start)
|
|
375
|
+
return DesktopProcessHandle(proc)
|
|
376
|
+
|
|
377
|
+
# --- Clipboard ---
|
|
378
|
+
|
|
379
|
+
async def read_clipboard(self) -> str:
|
|
380
|
+
if not HAS_TKINTER:
|
|
381
|
+
return ""
|
|
382
|
+
|
|
383
|
+
loop = asyncio.get_event_loop()
|
|
384
|
+
|
|
385
|
+
def _read():
|
|
386
|
+
if self._tk_root is None:
|
|
387
|
+
root = tk.Tk()
|
|
388
|
+
root.withdraw()
|
|
389
|
+
self._tk_root = root
|
|
390
|
+
else:
|
|
391
|
+
root = self._tk_root
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
return root.clipboard_get()
|
|
395
|
+
except tk.TclError:
|
|
396
|
+
return ""
|
|
397
|
+
|
|
398
|
+
return await loop.run_in_executor(None, _read)
|
|
399
|
+
|
|
400
|
+
async def write_clipboard(self, text: str) -> None:
|
|
401
|
+
if not HAS_TKINTER:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
loop = asyncio.get_event_loop()
|
|
405
|
+
|
|
406
|
+
def _write():
|
|
407
|
+
if self._tk_root is None:
|
|
408
|
+
root = tk.Tk()
|
|
409
|
+
root.withdraw()
|
|
410
|
+
self._tk_root = root
|
|
411
|
+
else:
|
|
412
|
+
root = self._tk_root
|
|
413
|
+
|
|
414
|
+
root.clipboard_clear()
|
|
415
|
+
root.clipboard_append(text)
|
|
416
|
+
root.update()
|
|
417
|
+
|
|
418
|
+
await loop.run_in_executor(None, _write)
|
|
419
|
+
|
|
420
|
+
# --- Notifications ---
|
|
421
|
+
|
|
422
|
+
async def show_notification(self, title: str, body: str, icon: Optional[str] = None) -> None:
|
|
423
|
+
# Use plyer or platform-specific notification
|
|
424
|
+
try:
|
|
425
|
+
from plyer import notification
|
|
426
|
+
loop = asyncio.get_event_loop()
|
|
427
|
+
await loop.run_in_executor(None, lambda: notification.notify(
|
|
428
|
+
title=title,
|
|
429
|
+
message=body,
|
|
430
|
+
app_name="CAT",
|
|
431
|
+
timeout=5,
|
|
432
|
+
))
|
|
433
|
+
except ImportError:
|
|
434
|
+
# Fallback: print to console
|
|
435
|
+
print(f"[NOTIFICATION] {title}: {body}")
|
|
436
|
+
|
|
437
|
+
async def request_notification_permission(self) -> bool:
|
|
438
|
+
# On desktop, notifications usually work without explicit permission
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
# --- Browser/URL ---
|
|
442
|
+
|
|
443
|
+
async def open_url(self, url: str) -> bool:
|
|
444
|
+
import webbrowser
|
|
445
|
+
loop = asyncio.get_event_loop()
|
|
446
|
+
try:
|
|
447
|
+
return await loop.run_in_executor(None, lambda: webbrowser.open(url))
|
|
448
|
+
except Exception:
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
# --- Keyboard/Input ---
|
|
452
|
+
|
|
453
|
+
async def show_keyboard(self) -> None:
|
|
454
|
+
# No-op on desktop
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
async def hide_keyboard(self) -> None:
|
|
458
|
+
# No-op on desktop
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
# --- Storage ---
|
|
462
|
+
|
|
463
|
+
async def get_storage_estimate(self) -> dict:
|
|
464
|
+
import shutil
|
|
465
|
+
total, used, free = shutil.disk_usage(os.getcwd())
|
|
466
|
+
return {
|
|
467
|
+
"quota": total,
|
|
468
|
+
"usage": used,
|
|
469
|
+
"available": free,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async def request_persistent_storage(self) -> bool:
|
|
473
|
+
# Always persistent on desktop
|
|
474
|
+
return True
|
|
475
|
+
|
|
476
|
+
# --- App Lifecycle ---
|
|
477
|
+
|
|
478
|
+
async def minimize_app(self) -> None:
|
|
479
|
+
# Not easily done cross-platform, no-op
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
async def close_app(self) -> None:
|
|
483
|
+
sys.exit(0)
|
|
484
|
+
|
|
485
|
+
# --- Platform-specific ---
|
|
486
|
+
|
|
487
|
+
def get_env(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
488
|
+
return os.environ.get(key, default)
|
|
489
|
+
|
|
490
|
+
def set_env(self, key: str, value: str) -> None:
|
|
491
|
+
os.environ[key] = value
|