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,640 @@
1
+ """
2
+ Code Pad + Text Editor for Chemistry Calc Terminal (CCT) [BETA].
3
+
4
+ Feature 1 — a real in-terminal code pad: write code line-by-line, get CCT AI
5
+ to generate/insert code for you, run Python snippets, save/load files — all
6
+ with its own fixed GitHub-Light syntax highlighting (independent of the
7
+ app's own dark/light theme, since that's what was asked for).
8
+
9
+ Feature 2 — the same engine in "text" mode powers /edit, a plain-text
10
+ in-terminal editor (notes, todo lists, snippets) with find/replace.
11
+
12
+ Feature 6 — /import (and this module's `:import`) reads local text files or
13
+ images and hands them to aicore so the AI/agent can actually use their
14
+ content, not just their filename.
15
+
16
+ Design note: CCT has no curses/full-screen UI (it's a plain print()+input()
17
+ REPL, like the rest of the app), so this is a *line editor*: you type one
18
+ line at a time, and everything starting with ':' is a command (save, run,
19
+ ai, list, etc.) rather than a line of content. This matches how every other
20
+ part of CCT already works (e.g. /solve, /settings) — no new UI paradigm,
21
+ no new dependency.
22
+ """
23
+
24
+ if __name__ == '__main__':
25
+ print("This is a library file and is not meant to be run directly.")
26
+ print("Please run 'python main.py' or 'python model.py' from the project root directory.")
27
+ import sys
28
+ sys.exit(1)
29
+
30
+ import os
31
+ import re
32
+ import sys
33
+ import time
34
+ import subprocess
35
+
36
+ from . import theme
37
+ from . import aicore
38
+ from . import sound
39
+ from . import security_scanner
40
+ from . import sandbox
41
+
42
+ DEFAULT_SAVE_DIR = os.path.join(os.path.expanduser("~"), "cct_files")
43
+
44
+ # --------------------------------------------------------- copy / clipboard --
45
+ # Feature request: "we can also copy the code" — works two ways:
46
+ # 1. Inside the code pad itself: `:copy` copies the whole buffer.
47
+ # 2. From an AI chat reply that contains a fenced code block: `/copycode`
48
+ # copies the most recently *shown* block (the conversation renderer / fallback_cli records it here
49
+ # every time it renders one, so this module stays the single source
50
+ # of truth for "what code did we last show the user").
51
+ _LAST_BLOCKS = []
52
+
53
+
54
+ def remember_block(code, lang=""):
55
+ """Called every time a fenced code block is rendered in
56
+ an AI reply, so /copycode always has something sensible to copy."""
57
+ _LAST_BLOCKS.append({"code": code, "lang": lang})
58
+ del _LAST_BLOCKS[:-10] # keep a short history, most-recent last
59
+
60
+
61
+ def get_last_block():
62
+ return _LAST_BLOCKS[-1] if _LAST_BLOCKS else None
63
+
64
+
65
+ def copy_to_clipboard(text):
66
+ """Best-effort clipboard copy with no required third-party dependency.
67
+ Tries pyperclip first (if installed), then the native OS clipboard
68
+ command for the current platform. Returns (ok: bool, detail: str) —
69
+ `detail` is either the method that worked or why it failed, so the
70
+ caller can show a useful message either way."""
71
+ try:
72
+ import pyperclip
73
+ pyperclip.copy(text)
74
+ return True, "pyperclip"
75
+ except Exception:
76
+ pass
77
+
78
+ try:
79
+ if sys.platform.startswith("win"):
80
+ p = subprocess.Popen(["clip"], stdin=subprocess.PIPE, shell=True)
81
+ p.communicate(input=text.encode("utf-16-le"))
82
+ if p.returncode == 0:
83
+ return True, "clip"
84
+ elif sys.platform == "darwin":
85
+ p = subprocess.Popen(["pbcopy"], stdin=subprocess.PIPE)
86
+ p.communicate(input=text.encode("utf-8"))
87
+ if p.returncode == 0:
88
+ return True, "pbcopy"
89
+ else:
90
+ for cmd in (["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"], ["wl-copy"]):
91
+ try:
92
+ p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
93
+ p.communicate(input=text.encode("utf-8"))
94
+ if p.returncode == 0:
95
+ return True, cmd[0]
96
+ except FileNotFoundError:
97
+ continue
98
+ except Exception as e:
99
+ return False, str(e)
100
+ return False, "no clipboard tool available (install 'pyperclip', or xclip/xsel/wl-copy on Linux)"
101
+
102
+ # ------------------------------------------------------------------ syntax --
103
+ # Fixed GitHub-Light-style Python highlighting for the code pad, regardless
104
+ # of the app's own current theme (dark or light) — the code pad always
105
+ # renders code in these colors, only the surrounding chrome (panels, hints)
106
+ # follows the app theme.
107
+ _PALETTE = theme.SYNTAX_GITHUB_LIGHT
108
+ _KEYWORDS = {
109
+ "def", "class", "return", "if", "elif", "else", "for", "while", "in", "is",
110
+ "not", "and", "or", "import", "from", "as", "try", "except", "finally",
111
+ "with", "pass", "break", "continue", "lambda", "yield", "global", "nonlocal",
112
+ "raise", "assert", "del", "async", "await", "True", "False", "None",
113
+ }
114
+ _BUILTINS = {
115
+ "print", "len", "range", "str", "int", "float", "list", "dict", "set",
116
+ "tuple", "open", "input", "type", "isinstance", "enumerate", "zip", "map",
117
+ "filter", "sorted", "sum", "min", "max", "abs", "round", "super", "self",
118
+ }
119
+ _TOKEN_RE = re.compile(r"""
120
+ (?P<comment>\#.*$)
121
+ | (?P<string>(\"\"\".*?\"\"\"|'''.*?'''|"[^"\n]*"|'[^'\n]*'))
122
+ | (?P<number>\b\d+\.?\d*\b)
123
+ | (?P<name>[A-Za-z_][A-Za-z0-9_]*)
124
+ | (?P<op>[\+\-\*/%=<>!&|^~:,\.\(\)\[\]\{\}])
125
+ """, re.VERBOSE)
126
+
127
+
128
+ def highlight_python(line):
129
+ """Return `line` with GitHub-Light-styled ANSI color codes applied,
130
+ token by token. Best-effort regex highlighting — good enough for a
131
+ terminal code pad, not a full parser."""
132
+ out = []
133
+ pos = 0
134
+ for m in _TOKEN_RE.finditer(line):
135
+ if m.start() > pos:
136
+ out.append(line[pos:m.start()])
137
+ kind = m.lastgroup
138
+ text = m.group()
139
+ if kind == "comment":
140
+ out.append(theme.fg(text, _PALETTE["comment"]))
141
+ elif kind == "string":
142
+ out.append(theme.fg(text, _PALETTE["string"]))
143
+ elif kind == "number":
144
+ out.append(theme.fg(text, _PALETTE["number"]))
145
+ elif kind == "name":
146
+ if text in _KEYWORDS:
147
+ out.append(theme.fg(text, _PALETTE["keyword"], bold=True))
148
+ elif text in _BUILTINS:
149
+ out.append(theme.fg(text, _PALETTE["builtin"]))
150
+ else:
151
+ out.append(theme.fg(text, _PALETTE["plain"]))
152
+ elif kind == "op":
153
+ out.append(theme.fg(text, _PALETTE["operator"]))
154
+ else:
155
+ out.append(text)
156
+ pos = m.end()
157
+ out.append(line[pos:])
158
+ return "".join(out)
159
+
160
+
161
+ def highlight_text(line):
162
+ """Plain-text mode gets no syntax coloring — just the normal app text
163
+ color, so /edit reads like a clean notepad rather than looking like
164
+ mis-highlighted code."""
165
+ return theme.text(line)
166
+
167
+
168
+ # --------------------------------------------------------------- rendering --
169
+ def _print_buffer(buffer, mode):
170
+ highlighter = highlight_python if mode == "code" else highlight_text
171
+ if not buffer:
172
+ print(theme.faint(" (empty — start typing lines, or use :ai <prompt>)"))
173
+ return
174
+ gutter_w = len(str(len(buffer)))
175
+ for i, line in enumerate(buffer, 1):
176
+ num = theme.faint(str(i).rjust(gutter_w) + " │ ")
177
+ print(f" {num}{highlighter(line)}")
178
+
179
+
180
+ def _header(mode, filename, dirty):
181
+ kind = "CODE PAD" if mode == "code" else "TEXT EDITOR"
182
+ title_line = theme.badge("BETA", theme.BG_WARN) + " " + theme.purple(f"▓ {kind} ▓", bold=True)
183
+ fname = filename or "(untitled)"
184
+ dirty_flag = theme.orange(" ●unsaved") if dirty else ""
185
+ lines = [
186
+ title_line,
187
+ theme.dim(f"File: {fname}{dirty_flag}"),
188
+ ]
189
+ if mode == "code":
190
+ lines.append(theme.dim("Syntax: GitHub Light │ :run executes Python │ :ai <prompt> asks CAT AI to write code"))
191
+ else:
192
+ lines.append(theme.dim(":find <text> │ :replace <old> <new> │ :ai <prompt> asks CAT AI to write/edit text"))
193
+ lines.append(theme.faint(":save [path] :load <path> :import <path> :copy :list :clear :undo :help :back"))
194
+ return theme.panel(lines, title="codepad" if mode == "code" else "textedit",
195
+ color=theme.PURPLE, width=_width())
196
+
197
+
198
+ def _width():
199
+ try:
200
+ from .engine import WIDTH
201
+ return WIDTH
202
+ except Exception:
203
+ return 78
204
+
205
+
206
+ def _print_help(mode):
207
+ rows = [
208
+ (":run", "Execute the buffer as a Python script (code mode only, sandboxed)"),
209
+ (":scan", "Static security scan of the buffer (code mode only)"),
210
+ (":watch <path>", "Live-watch a file on disk, redrawing on change"),
211
+ (":ai <prompt>", "Ask CAT AI to write code/text and append it to the buffer"),
212
+ (":save [path]", "Save the buffer to a file (defaults to ~/cct_files/)"),
213
+ (":load <path>", "Replace the buffer with a file's contents"),
214
+ (":import <path>", "Import a text file (content) or image (sent to AI vision)"),
215
+ (":list", "Reprint the whole buffer with line numbers"),
216
+ (":copy", "Copy the whole buffer to the clipboard (or save as fallback)"),
217
+ (":clear", "Erase the whole buffer (asks to confirm)"),
218
+ (":undo", "Undo the last change to the buffer"),
219
+ (":del <n>", "Delete line n"),
220
+ (":find <text>", "Text mode: list every line containing <text>"),
221
+ (":replace <old> <new>", "Text mode: replace first match on each line"),
222
+ (":theme light|dark", "Switch the whole app's theme"),
223
+ (":back / :exit", "Leave the editor and return home"),
224
+ ]
225
+ lines = [theme.cyan("COMMANDS", bold=True), ""]
226
+ for cmd, desc in rows:
227
+ lines.append(f" {theme.text(cmd, bold=True):<28} {theme.dim(desc)}")
228
+ print()
229
+ print(theme.panel(lines, title="help", color=theme.CYAN, width=_width()))
230
+
231
+
232
+ def _save_buffer(buffer, path):
233
+ if not path:
234
+ os.makedirs(DEFAULT_SAVE_DIR, exist_ok=True)
235
+ ext = ".py" if _looks_like_code(buffer) else ".txt"
236
+ path = os.path.join(DEFAULT_SAVE_DIR, f"untitled_{int(time.time())}{ext}")
237
+ path = os.path.expanduser(path)
238
+ try:
239
+ with open(path, "w", encoding="utf-8") as f:
240
+ f.write("\n".join(buffer) + ("\n" if buffer else ""))
241
+ return True, path
242
+ except Exception as e:
243
+ return False, str(e)
244
+
245
+
246
+ def _looks_like_code(buffer):
247
+ text = "\n".join(buffer)
248
+ return bool(re.search(r"^\s*(def |class |import |from |print\()", text, re.M))
249
+
250
+
251
+ def _load_buffer(path):
252
+ path = os.path.expanduser(path)
253
+ content, truncated = aicore.read_text_file_for_context(path)
254
+ if content is None:
255
+ return None, truncated
256
+ lines = content.split("\n")
257
+ if lines and lines[-1] == "":
258
+ lines.pop()
259
+ return lines, truncated
260
+
261
+
262
+ def _scan_and_confirm(code):
263
+ """Runs the real static scanner and, for anything above LOW risk,
264
+ shows a permission-card-style prompt before execution — same shape
265
+ as the app's other permission cards (action / reason / risk /
266
+ allow-once / deny), just rendered inline in the code pad."""
267
+ findings = security_scanner.scan_code(code)
268
+ risk = security_scanner.risk_level(findings)
269
+ if risk in ("none", "low"):
270
+ return True
271
+ lines = [
272
+ theme.orange("ACTION", bold=True) + " Execute this code in the sandbox",
273
+ theme.orange("RISK", bold=True) + f" {risk.upper()}",
274
+ theme.orange("REASON", bold=True) + " " + security_scanner.summarize(findings),
275
+ ]
276
+ for f in findings[:6]:
277
+ lines.append(" " + theme.dim(repr(f)))
278
+ lines.append("")
279
+ lines.append(theme.dim(" [Enter] Allow once [n] Deny"))
280
+ print()
281
+ print(theme.panel(lines, title="permission", color=theme.ORANGE, width=_width()))
282
+ choice = input(theme.dim(" \u25b8 ")).strip().lower()
283
+ return choice != "n"
284
+
285
+
286
+ def _run_python(buffer):
287
+ code = "\n".join(buffer)
288
+ if not code.strip():
289
+ print(theme.orange(" Nothing to run — the buffer is empty."))
290
+ return
291
+ if not _scan_and_confirm(code):
292
+ print(theme.dim(" Cancelled."))
293
+ return
294
+ label = "sandboxed" if sandbox.sandbox_available() else "timeout-only (no rlimit sandbox on this OS)"
295
+ print(theme.dim(f" Running ({label})..."))
296
+ result = sandbox.run_sandboxed(code, timeout=15)
297
+ if result["stdout"]:
298
+ print(theme.panel([theme.text(l) for l in result["stdout"].rstrip("\n").split("\n")] or [""],
299
+ title="stdout", color=theme.GREEN, width=_width()))
300
+ if result["timed_out"]:
301
+ sound.play("error")
302
+ print(theme.red(" ✗ Timed out after 15s (possible infinite loop)."))
303
+ elif result["killed_by_limit"]:
304
+ sound.play("error")
305
+ print(theme.red(" ✗ Killed by sandbox resource limit (CPU/memory)."))
306
+ elif result["returncode"] != 0:
307
+ sound.play("error")
308
+ err_lines = (result["stderr"] or "Unknown error").rstrip("\n").split("\n")
309
+ print(theme.panel([theme.red(l) for l in err_lines], title="error",
310
+ color=theme.RED, width=_width()))
311
+ else:
312
+ sound.play("success")
313
+ if not result["stdout"]:
314
+ print(theme.green(" ✓ Ran with no output."))
315
+
316
+
317
+ def _scan_buffer(buffer):
318
+ code = "\n".join(buffer)
319
+ if not code.strip():
320
+ print(theme.orange(" Nothing to scan — the buffer is empty."))
321
+ return
322
+ findings = security_scanner.scan_code(code)
323
+ risk = security_scanner.risk_level(findings)
324
+ color = {"high": theme.RED, "medium": theme.ORANGE, "low": theme.CYAN, "none": theme.GREEN}[risk]
325
+ lines = [theme.fg(f"RISK: {risk.upper()}", color, bold=True), ""]
326
+ if findings:
327
+ for f in findings:
328
+ lines.append(" " + theme.dim(repr(f)))
329
+ else:
330
+ lines.append(theme.green(" No risky patterns detected."))
331
+ print()
332
+ print(theme.panel(lines, title="security scan", color=color, width=_width()))
333
+
334
+
335
+ def _watch_file(path):
336
+ """Polls a file's mtime and reprints it whenever it changes on disk,
337
+ until the user hits Enter — a real filesystem watch (not a fake
338
+ 'watching...' spinner), implemented via polling since that's the
339
+ only portable option without an extra dependency. Guarded by
340
+ stdin_is_interactive() so a piped/non-tty session (where Enter can
341
+ never arrive) doesn't spin forever — same precedent as the
342
+ /sim3d and /atomsim keypress-loop fix."""
343
+ from . import keys
344
+ path = os.path.expanduser(path)
345
+ if not os.path.isfile(path):
346
+ print(theme.red(f" ✗ No such file: {path}"))
347
+ return
348
+ if not keys.stdin_is_interactive():
349
+ print(theme.orange(" :watch needs a real attached terminal (not piped input) — showing the file once instead."))
350
+ with open(path, encoding="utf-8", errors="replace") as f:
351
+ print(theme.panel([theme.text(l) for l in f.read().splitlines()], title=path, color=theme.CYAN, width=_width()))
352
+ return
353
+
354
+ print(theme.dim(f" Watching {path} — press Enter to stop."))
355
+ last_mtime = None
356
+ try:
357
+ while True:
358
+ if keys.key_available():
359
+ keys.read_key()
360
+ break
361
+ time.sleep(0.4)
362
+ try:
363
+ mtime = os.path.getmtime(path)
364
+ except OSError:
365
+ continue
366
+ if mtime != last_mtime:
367
+ last_mtime = mtime
368
+ with open(path, encoding="utf-8", errors="replace") as f:
369
+ content = f.read().splitlines()
370
+ theme.clear_screen()
371
+ print(theme.panel([theme.text(l) for l in content], title=f"{path} (live)", color=theme.CYAN, width=_width()))
372
+ print(theme.dim(" Press Enter to stop watching."))
373
+ except (KeyboardInterrupt, EOFError):
374
+ pass
375
+ print(theme.dim(" Stopped watching."))
376
+
377
+
378
+ def _ask_ai_for_content(prompt, mode, buffer):
379
+ config = aicore.load_config()
380
+ if not config.get("provider"):
381
+ print(theme.orange(" No AI provider configured yet — run /ai or /model first."))
382
+ return None
383
+ context = ""
384
+ if buffer:
385
+ context = "Current buffer so far:\n```\n" + "\n".join(buffer) + "\n```\n\n"
386
+ if mode == "code":
387
+ sys_prompt = ("You are a precise coding assistant embedded in a terminal code pad. "
388
+ "Reply with ONLY the code — no markdown fences, no explanation, no "
389
+ "commentary — ready to append directly to the buffer as-is.")
390
+ else:
391
+ sys_prompt = ("You are a writing assistant embedded in a terminal text editor. "
392
+ "Reply with ONLY the plain text to insert — no markdown, no commentary.")
393
+ print(theme.dim(" CAT AI is generating..."))
394
+ t0 = time.time()
395
+ reply = aicore.query_ai(context + "Request: " + prompt, system_prompt=sys_prompt)
396
+ dur = time.time() - t0
397
+ reply = re.sub(r"^```[a-zA-Z]*\n?|```$", "", reply.strip(), flags=re.M).strip()
398
+ print(theme.faint(f" ({dur:.1f}s, ~{aicore.estimate_tokens(reply)} tok generated)"))
399
+ return reply.split("\n") if reply else None
400
+
401
+
402
+ def run_editor(app, mode="code"):
403
+ """Entry point for /codepad (mode='code') and /edit (mode='text')."""
404
+ buffer = []
405
+ history = [] # undo stack (previous buffer states)
406
+ filename = None
407
+ dirty = False
408
+
409
+ def push_undo():
410
+ history.append(list(buffer))
411
+ if len(history) > 30:
412
+ history.pop(0)
413
+
414
+ theme.clear_screen()
415
+ print()
416
+ print(_header(mode, filename, dirty))
417
+ print()
418
+
419
+ label = "code" if mode == "code" else "text"
420
+ while True:
421
+ try:
422
+ raw = input(theme.dim(f" {label} ▸ "))
423
+ except (EOFError, KeyboardInterrupt):
424
+ print()
425
+ break
426
+
427
+ stripped = raw.strip()
428
+ low = stripped.lower()
429
+
430
+ if not stripped:
431
+ continue
432
+
433
+ if low in (":back", ":exit", ":q"):
434
+ if dirty:
435
+ confirm = input(theme.orange(" Unsaved changes — leave anyway? (y/N) ▸ ")).strip().lower()
436
+ if confirm != "y":
437
+ continue
438
+ break
439
+
440
+ if low == ":help":
441
+ _print_help(mode)
442
+ continue
443
+
444
+ if low == ":list":
445
+ print()
446
+ _print_buffer(buffer, mode)
447
+ print()
448
+ continue
449
+
450
+ if low == ":copy":
451
+ if not buffer:
452
+ print(theme.faint(" Nothing to copy — the buffer is empty."))
453
+ continue
454
+ ok, detail = copy_to_clipboard("\n".join(buffer))
455
+ if ok:
456
+ print(theme.green(f" ✓ Copied {len(buffer)} line(s) to the clipboard (via {detail})."))
457
+ else:
458
+ saved_ok, saved_path = _save_buffer(buffer, None)
459
+ if saved_ok:
460
+ print(theme.orange(f" Clipboard unavailable ({detail}).") + " " +
461
+ theme.green(f"Saved instead to {saved_path} — open it to copy."))
462
+ else:
463
+ print(theme.red(f" ✗ Clipboard unavailable ({detail}), and saving a fallback also failed."))
464
+ continue
465
+
466
+ if low == ":clear":
467
+ confirm = input(theme.orange(" Erase the whole buffer? (y/N) ▸ ")).strip().lower()
468
+ if confirm == "y":
469
+ push_undo()
470
+ buffer = []
471
+ dirty = True
472
+ print(theme.green(" Buffer cleared."))
473
+ continue
474
+
475
+ if low == ":undo":
476
+ if history:
477
+ buffer = history.pop()
478
+ dirty = True
479
+ print(theme.green(" Undone."))
480
+ _print_buffer(buffer, mode)
481
+ else:
482
+ print(theme.faint(" Nothing to undo."))
483
+ continue
484
+
485
+ if low.startswith(":del "):
486
+ arg = stripped.split(" ", 1)[1].strip()
487
+ if arg.isdigit() and 1 <= int(arg) <= len(buffer):
488
+ push_undo()
489
+ removed = buffer.pop(int(arg) - 1)
490
+ dirty = True
491
+ print(theme.green(f" Deleted line {arg}: ") + theme.faint(removed[:60]))
492
+ else:
493
+ print(theme.red(f" No such line '{arg}'."))
494
+ continue
495
+
496
+ if low.startswith(":theme"):
497
+ arg = stripped.split(" ", 1)[1].strip() if " " in stripped else ""
498
+ new_name = theme.set_theme(arg or ("textual-light" if not theme.is_light() else "tokyo-night"))
499
+ print(theme.green(f" Theme switched to {new_name}."))
500
+ print()
501
+ print(_header(mode, filename, dirty))
502
+ continue
503
+
504
+ if low == ":run":
505
+ if mode != "code":
506
+ print(theme.orange(" :run only works in the code pad (/codepad), not the text editor."))
507
+ continue
508
+ _run_python(buffer)
509
+ continue
510
+
511
+ if low == ":scan":
512
+ if mode != "code":
513
+ print(theme.orange(" :scan only works in the code pad (/codepad), not the text editor."))
514
+ continue
515
+ _scan_buffer(buffer)
516
+ continue
517
+
518
+ if low.startswith(":watch "):
519
+ _watch_file(stripped.split(" ", 1)[1].strip())
520
+ continue
521
+
522
+ if low.startswith(":ai"):
523
+ prompt = stripped[3:].strip()
524
+ if not prompt:
525
+ print(theme.orange(" Usage: :ai <what to generate>, e.g. :ai a function that reverses a string"))
526
+ continue
527
+ generated = _ask_ai_for_content(prompt, mode, buffer)
528
+ if generated:
529
+ push_undo()
530
+ buffer.extend(generated)
531
+ dirty = True
532
+ print()
533
+ _print_buffer(buffer, mode)
534
+ continue
535
+
536
+ if low.startswith(":save"):
537
+ arg = stripped.split(" ", 1)[1].strip() if " " in stripped else ""
538
+ ok, result = _save_buffer(buffer, arg or filename)
539
+ if ok:
540
+ filename = result
541
+ dirty = False
542
+ print(theme.green(f" ✓ Saved to {result}"))
543
+ else:
544
+ print(theme.red(f" ✗ Could not save: {result}"))
545
+ continue
546
+
547
+ if low.startswith(":load "):
548
+ path = stripped.split(" ", 1)[1].strip()
549
+ loaded, truncated = _load_buffer(path)
550
+ if loaded is None:
551
+ print(theme.red(f" ✗ Could not read '{path}'."))
552
+ else:
553
+ push_undo()
554
+ buffer = loaded
555
+ filename = os.path.expanduser(path)
556
+ dirty = False
557
+ mode = "code" if path.lower().endswith((".py", ".pyw")) else mode
558
+ if truncated:
559
+ print(theme.orange(f" Loaded (truncated to {aicore.TEXT_FILE_MAX_CHARS} chars)."))
560
+ else:
561
+ print(theme.green(f" ✓ Loaded {path}"))
562
+ _print_buffer(buffer, mode)
563
+ continue
564
+
565
+ if low.startswith(":import "):
566
+ path = stripped.split(" ", 1)[1].strip()
567
+ _handle_import(path, mode, buffer, push_undo)
568
+ continue
569
+
570
+ if mode == "text" and low.startswith(":find "):
571
+ needle = stripped.split(" ", 1)[1]
572
+ hits = [(i, l) for i, l in enumerate(buffer, 1) if needle.lower() in l.lower()]
573
+ if hits:
574
+ for i, l in hits:
575
+ print(f" {theme.cyan(str(i) + ':')} {theme.text(l)}")
576
+ else:
577
+ print(theme.faint(f" No matches for '{needle}'."))
578
+ continue
579
+
580
+ if mode == "text" and low.startswith(":replace "):
581
+ rest = stripped.split(" ", 2)
582
+ if len(rest) < 3:
583
+ print(theme.orange(" Usage: :replace <old> <new>"))
584
+ continue
585
+ old, new = rest[1], rest[2]
586
+ push_undo()
587
+ n = 0
588
+ for i, l in enumerate(buffer):
589
+ if old in l:
590
+ buffer[i] = l.replace(old, new, 1)
591
+ n += 1
592
+ dirty = dirty or n > 0
593
+ print(theme.green(f" Replaced in {n} line(s)."))
594
+ continue
595
+
596
+ # Anything else typed is just a new line of content.
597
+ push_undo()
598
+ buffer.append(raw)
599
+ dirty = True
600
+ line_no = len(buffer)
601
+ highlighter = highlight_python if mode == "code" else highlight_text
602
+ print(theme.faint(f" {line_no} │ ") + highlighter(raw))
603
+
604
+ app.print_home()
605
+
606
+
607
+ def _handle_import(path, mode, buffer, push_undo):
608
+ path = os.path.expanduser(path.strip())
609
+ if not os.path.exists(path):
610
+ print(theme.red(f" ✗ File not found: {path}"))
611
+ return
612
+ if aicore.is_image_file(path):
613
+ config = aicore.load_config()
614
+ if not config.get("provider"):
615
+ print(theme.orange(" Image detected, but no AI is configured to analyze it — run /ai or /model first."))
616
+ return
617
+ print(theme.dim(f" Sending image to {config.get('provider')} for analysis..."))
618
+ result = aicore.query_ai_with_image(
619
+ "Describe this image in detail, and if it contains code, chemistry, "
620
+ "math, or a diagram, extract/transcribe the relevant content precisely.",
621
+ path)
622
+ print()
623
+ print(theme.panel([theme.text(l) for l in result.split("\n")], title="image analysis",
624
+ color=theme.PURPLE, width=_width()))
625
+ push_undo()
626
+ buffer.append(f"# Imported image: {path}")
627
+ for l in result.split("\n"):
628
+ buffer.append(("# " if mode == "code" else "") + l)
629
+ return
630
+ content, truncated = aicore.read_text_file_for_context(path)
631
+ if content is None:
632
+ print(theme.red(f" ✗ Could not read '{path}' as text."))
633
+ return
634
+ push_undo()
635
+ buffer.extend(content.split("\n"))
636
+ if truncated:
637
+ print(theme.orange(f" ✓ Imported {path} (truncated to {aicore.TEXT_FILE_MAX_CHARS} chars)."))
638
+ else:
639
+ print(theme.green(f" ✓ Imported {path}"))
640
+ _print_buffer(buffer, mode)