cct-cli 0.7.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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