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