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,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Gestures — manager.py: persistence + CRUD + event dispatch.
|
|
3
|
+
|
|
4
|
+
Storage: ~/.cct_gestures.json (list of gesture dicts)
|
|
5
|
+
If missing, defaults are seeded from bindings.DEFAULT_GESTURES.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from .bindings import DEFAULT_GESTURES, GESTURE_ACTIONS, GESTURE_TARGETS, GESTURE_TRIGGERS
|
|
17
|
+
|
|
18
|
+
STORE_PATH = os.path.join(os.path.expanduser("~"), ".cct_gestures.json")
|
|
19
|
+
|
|
20
|
+
# In-memory cache
|
|
21
|
+
_cache: Optional[List[Dict]] = None
|
|
22
|
+
_cache_mtime: float = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Gesture(dict):
|
|
26
|
+
"""Typed dict for a gesture — behaves like a plain dict for JSON compat."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load() -> List[Dict]:
|
|
30
|
+
global _cache, _cache_mtime
|
|
31
|
+
try:
|
|
32
|
+
mtime = os.path.getmtime(STORE_PATH) if os.path.exists(STORE_PATH) else 0
|
|
33
|
+
if _cache is not None and mtime == _cache_mtime:
|
|
34
|
+
return list(_cache)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
if not os.path.exists(STORE_PATH):
|
|
38
|
+
data = [dict(g) for g in DEFAULT_GESTURES]
|
|
39
|
+
_save(data)
|
|
40
|
+
_cache = list(data)
|
|
41
|
+
try:
|
|
42
|
+
_cache_mtime = os.path.getmtime(STORE_PATH)
|
|
43
|
+
except Exception:
|
|
44
|
+
_cache_mtime = time.time()
|
|
45
|
+
return list(data)
|
|
46
|
+
try:
|
|
47
|
+
with open(STORE_PATH, "r", encoding="utf-8") as f:
|
|
48
|
+
data = json.load(f)
|
|
49
|
+
if isinstance(data, list):
|
|
50
|
+
_cache = list(data)
|
|
51
|
+
try:
|
|
52
|
+
_cache_mtime = os.path.getmtime(STORE_PATH)
|
|
53
|
+
except Exception:
|
|
54
|
+
_cache_mtime = time.time()
|
|
55
|
+
return list(data)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return [dict(g) for g in DEFAULT_GESTURES]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _save(data: List[Dict]) -> bool:
|
|
62
|
+
global _cache, _cache_mtime
|
|
63
|
+
try:
|
|
64
|
+
with open(STORE_PATH, "w", encoding="utf-8") as f:
|
|
65
|
+
json.dump(data, f, indent=2)
|
|
66
|
+
_cache = list(data)
|
|
67
|
+
try:
|
|
68
|
+
_cache_mtime = os.path.getmtime(STORE_PATH)
|
|
69
|
+
except Exception:
|
|
70
|
+
_cache_mtime = time.time()
|
|
71
|
+
return True
|
|
72
|
+
except Exception:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def list_gestures() -> List[Dict]:
|
|
77
|
+
return _load()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_gesture(gesture_id: str) -> Optional[Dict]:
|
|
81
|
+
for g in _load():
|
|
82
|
+
if g.get("id") == gesture_id:
|
|
83
|
+
return g
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def find_gesture(trigger: str, target: str) -> Optional[Dict]:
|
|
88
|
+
"""Find enabled gesture matching trigger + target."""
|
|
89
|
+
try:
|
|
90
|
+
from .. import extensions as _ext
|
|
91
|
+
if not _ext.is_enabled("gestures"):
|
|
92
|
+
return None
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
for g in _load():
|
|
96
|
+
if g.get("trigger") == trigger and g.get("target") == target and g.get("enabled"):
|
|
97
|
+
return g
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_valid_shortcut(keys: str) -> bool:
|
|
102
|
+
"""Validate shortcut like ctrl+s, shift+enter, ctrl+shift+p, f5, alt+enter."""
|
|
103
|
+
import re
|
|
104
|
+
if not keys or not isinstance(keys, str):
|
|
105
|
+
return False
|
|
106
|
+
k = keys.strip().lower()
|
|
107
|
+
# normalize: allow combos with +, allow single keys f1-f12, letters, numbers, enter/space/tab/esc etc
|
|
108
|
+
pattern = r"^(?:(?:ctrl|shift|alt|cmd|meta)\+)*(?:[a-z0-9]|f(?:[1-9]|1[0-2])|enter|space|tab|esc|escape|backspace|delete|up|down|left|right|home|end|pageup|pagedown|insert)$"
|
|
109
|
+
return bool(re.match(pattern, k))
|
|
110
|
+
|
|
111
|
+
def add_gesture(
|
|
112
|
+
name: str,
|
|
113
|
+
trigger: str,
|
|
114
|
+
target: str,
|
|
115
|
+
action: str,
|
|
116
|
+
description: str = "",
|
|
117
|
+
enabled: bool = True,
|
|
118
|
+
keys: str = "",
|
|
119
|
+
) -> Optional[Dict]:
|
|
120
|
+
if trigger not in GESTURE_TRIGGERS:
|
|
121
|
+
return None
|
|
122
|
+
if target not in GESTURE_TARGETS:
|
|
123
|
+
return None
|
|
124
|
+
if action not in GESTURE_ACTIONS:
|
|
125
|
+
return None
|
|
126
|
+
# shortcut trigger requires a key combo
|
|
127
|
+
if trigger == "shortcut":
|
|
128
|
+
if not keys or not _is_valid_shortcut(keys):
|
|
129
|
+
return None
|
|
130
|
+
# ensure keys unique among enabled shortcuts
|
|
131
|
+
for g in _load():
|
|
132
|
+
if g.get("trigger") == "shortcut" and g.get("keys","").lower() == keys.lower() and g.get("enabled"):
|
|
133
|
+
return None # duplicate
|
|
134
|
+
data = _load()
|
|
135
|
+
gesture = {
|
|
136
|
+
"id": f"gesture_{uuid.uuid4().hex[:8]}",
|
|
137
|
+
"name": name,
|
|
138
|
+
"trigger": trigger,
|
|
139
|
+
"target": target,
|
|
140
|
+
"action": action,
|
|
141
|
+
"enabled": bool(enabled),
|
|
142
|
+
"description": description or f"{trigger} on {target} → {action}",
|
|
143
|
+
"created_at": time.time(),
|
|
144
|
+
}
|
|
145
|
+
if trigger == "shortcut" and keys:
|
|
146
|
+
gesture["keys"] = keys.strip().lower()
|
|
147
|
+
gesture["description"] = f"{keys} ({target}) → {action}"
|
|
148
|
+
data.append(gesture)
|
|
149
|
+
_save(data)
|
|
150
|
+
_publish_gesture_event("gesture_created", gesture)
|
|
151
|
+
return gesture
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def find_shortcut(keys: str) -> Optional[Dict]:
|
|
155
|
+
"""Find enabled shortcut gesture matching keys (case-insensitive)."""
|
|
156
|
+
# Extension guard: Gestures disabled → inactive (spec section 5)
|
|
157
|
+
try:
|
|
158
|
+
from .. import extensions as _ext
|
|
159
|
+
if not _ext.is_enabled("gestures"):
|
|
160
|
+
return None
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
k = (keys or "").strip().lower()
|
|
164
|
+
if not k:
|
|
165
|
+
return None
|
|
166
|
+
for g in _load():
|
|
167
|
+
if g.get("trigger") == "shortcut" and g.get("keys","").lower() == k and g.get("enabled"):
|
|
168
|
+
return g
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def handle_shortcut(keys: str, app=None, context: Optional[Dict] = None) -> bool:
|
|
173
|
+
"""Dispatch a keyboard shortcut."""
|
|
174
|
+
# Extension guard: Gestures disabled → inactive
|
|
175
|
+
try:
|
|
176
|
+
from .. import extensions as _ext
|
|
177
|
+
if not _ext.is_enabled("gestures"):
|
|
178
|
+
return False
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
g = find_shortcut(keys)
|
|
182
|
+
if not g:
|
|
183
|
+
return False
|
|
184
|
+
action = g.get("action")
|
|
185
|
+
if not action:
|
|
186
|
+
return False
|
|
187
|
+
try:
|
|
188
|
+
from .bindings import ACTION_HANDLERS
|
|
189
|
+
handler = ACTION_HANDLERS.get(action)
|
|
190
|
+
if handler is not None:
|
|
191
|
+
return bool(handler(app, context or {}, g))
|
|
192
|
+
return _builtin_handler(action, app, context or {}, g)
|
|
193
|
+
except Exception:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def update_gesture(gesture_id: str, **fields) -> Optional[Dict]:
|
|
198
|
+
data = _load()
|
|
199
|
+
for g in data:
|
|
200
|
+
if g.get("id") == gesture_id:
|
|
201
|
+
for k in ("name", "trigger", "target", "action", "enabled", "description", "keys"):
|
|
202
|
+
if k in fields:
|
|
203
|
+
if k == "trigger" and fields[k] not in GESTURE_TRIGGERS:
|
|
204
|
+
continue
|
|
205
|
+
if k == "target" and fields[k] not in GESTURE_TARGETS:
|
|
206
|
+
continue
|
|
207
|
+
if k == "action" and fields[k] not in GESTURE_ACTIONS:
|
|
208
|
+
continue
|
|
209
|
+
if k == "keys" and fields[k]:
|
|
210
|
+
if not _is_valid_shortcut(str(fields[k])):
|
|
211
|
+
continue
|
|
212
|
+
fields[k] = str(fields[k]).strip().lower()
|
|
213
|
+
g[k] = fields[k]
|
|
214
|
+
_save(data)
|
|
215
|
+
_publish_gesture_event("gesture_updated", g)
|
|
216
|
+
return g
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def delete_gesture(gesture_id: str) -> bool:
|
|
221
|
+
data = _load()
|
|
222
|
+
remaining = [g for g in data if g.get("id") != gesture_id]
|
|
223
|
+
if len(remaining) == len(data):
|
|
224
|
+
return False
|
|
225
|
+
_save(remaining)
|
|
226
|
+
_publish_gesture_event("gesture_deleted", {"id": gesture_id})
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def toggle_gesture(gesture_id: str) -> Optional[Dict]:
|
|
231
|
+
data = _load()
|
|
232
|
+
for g in data:
|
|
233
|
+
if g.get("id") == gesture_id:
|
|
234
|
+
g["enabled"] = not bool(g.get("enabled", True))
|
|
235
|
+
_save(data)
|
|
236
|
+
_publish_gesture_event("gesture_toggled", g)
|
|
237
|
+
return g
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def reset_defaults() -> List[Dict]:
|
|
242
|
+
data = [dict(g) for g in DEFAULT_GESTURES]
|
|
243
|
+
_save(data)
|
|
244
|
+
_publish_gesture_event("gestures_reset", {"count": len(data)})
|
|
245
|
+
return list(data)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _publish_gesture_event(topic: str, detail: Dict):
|
|
249
|
+
try:
|
|
250
|
+
from .. import eventbus
|
|
251
|
+
|
|
252
|
+
eventbus.bus.publish(topic, **detail)
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------- handler dispatch
|
|
258
|
+
|
|
259
|
+
def handle_gesture(trigger: str, target: str, app=None, context: Optional[Dict] = None) -> bool:
|
|
260
|
+
"""
|
|
261
|
+
Dispatch a gesture: look up enabled gesture for (trigger, target),
|
|
262
|
+
then invoke its action handler.
|
|
263
|
+
|
|
264
|
+
Returns True if a handler ran, False if no matching gesture/handler.
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
from .. import extensions as _ext
|
|
268
|
+
if not _ext.is_enabled("gestures"):
|
|
269
|
+
return False
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
gesture = find_gesture(trigger, target)
|
|
273
|
+
if not gesture:
|
|
274
|
+
return False
|
|
275
|
+
action = gesture.get("action")
|
|
276
|
+
if not action:
|
|
277
|
+
return False
|
|
278
|
+
# Try to get handler from bindings
|
|
279
|
+
try:
|
|
280
|
+
from .bindings import ACTION_HANDLERS
|
|
281
|
+
|
|
282
|
+
handler = ACTION_HANDLERS.get(action)
|
|
283
|
+
if handler is not None:
|
|
284
|
+
try:
|
|
285
|
+
return bool(handler(app, context or {}, gesture))
|
|
286
|
+
except Exception:
|
|
287
|
+
return False
|
|
288
|
+
# Built-in fallback handlers (no app dependency)
|
|
289
|
+
return _builtin_handler(action, app, context or {}, gesture)
|
|
290
|
+
except Exception:
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _builtin_handler(action: str, app, context: Dict, gesture: Dict) -> bool:
|
|
295
|
+
"""Fallback handlers when no custom handler registered — covers spec examples."""
|
|
296
|
+
try:
|
|
297
|
+
# --- new shortcut actions ---
|
|
298
|
+
if action == "show_preview":
|
|
299
|
+
# Shift+Enter or toolbar ▷
|
|
300
|
+
if app and hasattr(app, "_workspace_shell") and app._workspace_shell:
|
|
301
|
+
try:
|
|
302
|
+
# find active file or any web file
|
|
303
|
+
editor = app._workspace_shell.editor
|
|
304
|
+
# delegate to EditorPane's preview if possible
|
|
305
|
+
if editor and hasattr(editor, "action_show_preview"):
|
|
306
|
+
editor.action_show_preview()
|
|
307
|
+
return True
|
|
308
|
+
# fallback: post PreviewRequested
|
|
309
|
+
from ..ui.events import PreviewRequested
|
|
310
|
+
# try to get path from context or active editor
|
|
311
|
+
path = (context.get("path") if context else None)
|
|
312
|
+
if not path and editor:
|
|
313
|
+
area = editor.active_text_area() if hasattr(editor, "active_text_area") else None
|
|
314
|
+
path = getattr(area, "path", None) if area else None
|
|
315
|
+
if path:
|
|
316
|
+
app.post_message(PreviewRequested(path))
|
|
317
|
+
return True
|
|
318
|
+
# try workspace root fallback
|
|
319
|
+
app.post_message(PreviewRequested(""))
|
|
320
|
+
return True
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
return False
|
|
324
|
+
if action == "toggle_preview":
|
|
325
|
+
if app and hasattr(app, "action_toggle_preview"):
|
|
326
|
+
app.action_toggle_preview()
|
|
327
|
+
return True
|
|
328
|
+
return False
|
|
329
|
+
if action == "save_file":
|
|
330
|
+
if app and hasattr(app, "action_save_file"):
|
|
331
|
+
app.action_save_file()
|
|
332
|
+
return True
|
|
333
|
+
# fallback via editor
|
|
334
|
+
if app and hasattr(app, "_workspace_shell") and app._workspace_shell:
|
|
335
|
+
try:
|
|
336
|
+
app._workspace_shell.editor.save_active()
|
|
337
|
+
return True
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
return False
|
|
341
|
+
if action == "toggle_sidebar":
|
|
342
|
+
if app and hasattr(app, "action_toggle_sidebar"):
|
|
343
|
+
app.action_toggle_sidebar()
|
|
344
|
+
return True
|
|
345
|
+
return False
|
|
346
|
+
if action == "open_palette":
|
|
347
|
+
if app and hasattr(app, "action_open_palette"):
|
|
348
|
+
app.action_open_palette()
|
|
349
|
+
return True
|
|
350
|
+
return False
|
|
351
|
+
if action == "open_gestures":
|
|
352
|
+
if app:
|
|
353
|
+
try:
|
|
354
|
+
from ..ui.gestures_panel import GesturesPanel
|
|
355
|
+
app.push_screen(GesturesPanel())
|
|
356
|
+
return True
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
return False
|
|
360
|
+
if action == "open_vision":
|
|
361
|
+
if app:
|
|
362
|
+
try:
|
|
363
|
+
from ..ui.vision_panel import VisionPanel
|
|
364
|
+
app.push_screen(VisionPanel())
|
|
365
|
+
return True
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
if action == "open_vscode":
|
|
371
|
+
# Double-click editor → open in VS Code
|
|
372
|
+
path = (context.get("path") or context.get("workspace") or "") if context else ""
|
|
373
|
+
if app is not None and hasattr(app, "_workspace_root") and app._workspace_root:
|
|
374
|
+
path = path or app._workspace_root
|
|
375
|
+
if path:
|
|
376
|
+
import subprocess
|
|
377
|
+
import shutil
|
|
378
|
+
code_bin = shutil.which("code") or shutil.which("code.cmd")
|
|
379
|
+
if code_bin:
|
|
380
|
+
subprocess.Popen([code_bin, path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
381
|
+
if app:
|
|
382
|
+
try:
|
|
383
|
+
app._system_note(f"Opened {path} in VS Code")
|
|
384
|
+
except Exception:
|
|
385
|
+
pass
|
|
386
|
+
return True
|
|
387
|
+
if app:
|
|
388
|
+
try:
|
|
389
|
+
app._system_note("VS Code not found (install 'code' on PATH)")
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
if action == "fullscreen_preview":
|
|
395
|
+
if app and hasattr(app, "action_toggle_right_pane_fullscreen"):
|
|
396
|
+
app.action_toggle_right_pane_fullscreen()
|
|
397
|
+
return True
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
if action == "fullscreen_editor":
|
|
401
|
+
if app and hasattr(app, "action_toggle_right_pane_fullscreen"):
|
|
402
|
+
app.action_toggle_right_pane_fullscreen()
|
|
403
|
+
return True
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
if action == "quick_actions":
|
|
407
|
+
path = context.get("path", "") if context else ""
|
|
408
|
+
if app:
|
|
409
|
+
try:
|
|
410
|
+
from ..ui.context_menu import FileQuickActions
|
|
411
|
+
|
|
412
|
+
# Show quick actions via system note fallback if UI not available
|
|
413
|
+
app._system_note(f"Quick actions for: {path or 'file'}")
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
if action == "reveal_actions":
|
|
419
|
+
path = context.get("path", "") if context else ""
|
|
420
|
+
if app:
|
|
421
|
+
try:
|
|
422
|
+
app._system_note(f"Actions revealed for: {path}")
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
if action == "before_after":
|
|
428
|
+
if app and hasattr(app, "_workspace_shell"):
|
|
429
|
+
try:
|
|
430
|
+
editor = app._workspace_shell.editor
|
|
431
|
+
if editor:
|
|
432
|
+
editor.show_diff()
|
|
433
|
+
return True
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
if action == "edit_message":
|
|
439
|
+
turn_id = context.get("turn_id") if context else None
|
|
440
|
+
if turn_id and app:
|
|
441
|
+
try:
|
|
442
|
+
# Trigger rewrite via MessageContextAction
|
|
443
|
+
from ..ui.events import MessageContextAction
|
|
444
|
+
|
|
445
|
+
app.post_message(MessageContextAction("rewrite", turn_id))
|
|
446
|
+
return True
|
|
447
|
+
except Exception:
|
|
448
|
+
pass
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
if action == "copy_path":
|
|
452
|
+
path = context.get("path", "") if context else ""
|
|
453
|
+
if path:
|
|
454
|
+
try:
|
|
455
|
+
from .. import code_editor
|
|
456
|
+
|
|
457
|
+
code_editor.copy_to_clipboard(path)
|
|
458
|
+
if app:
|
|
459
|
+
app._system_note(f"Copied path: {path}")
|
|
460
|
+
return True
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
return False
|
|
464
|
+
except Exception:
|
|
465
|
+
return False
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# Convenience: register builtins on import
|
|
470
|
+
try:
|
|
471
|
+
from .bindings import ACTION_HANDLERS as _AH
|
|
472
|
+
|
|
473
|
+
for _act in ("open_vscode", "fullscreen_preview", "fullscreen_editor", "quick_actions", "reveal_actions", "before_after", "edit_message", "copy_path", "show_preview", "toggle_preview", "save_file", "toggle_sidebar", "open_palette", "open_gestures", "open_vision"):
|
|
474
|
+
if _act not in _AH:
|
|
475
|
+
_AH[_act] = lambda app, ctx, g, _a=_act: _builtin_handler(_a, app, ctx, g)
|
|
476
|
+
except Exception:
|
|
477
|
+
pass
|