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,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Input Architecture — pointer.py
|
|
3
|
+
Touch hit-target expansion and pointer adaptation.
|
|
4
|
+
|
|
5
|
+
Expands practical touch target zones for:
|
|
6
|
+
- 1-column pane resizers (ExplorerResizeHandle, RightPaneResizeHandle, ComposerResizeHandle)
|
|
7
|
+
- Compact toolbar glyphs (▷ Run, ⿻ Fullscreen, ⏻ Stop, ✕ Close)
|
|
8
|
+
Without changing their sleek visual appearance.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Tuple
|
|
14
|
+
from textual.widget import Widget
|
|
15
|
+
from textual.app import App
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
RESIZER_TOUCH_PADDING = 2 # Extra cells on each side for touch dragging
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TouchHitZone:
|
|
22
|
+
"""Provides hit zone expansion for narrow or small interactive controls."""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def find_nearby_resizer(cls, app: App, screen_x: int, screen_y: int, tolerance: int = RESIZER_TOUCH_PADDING) -> Optional[Widget]:
|
|
26
|
+
"""
|
|
27
|
+
Check if a touch coordinate landed within tolerance cells of any resizer handle.
|
|
28
|
+
Allows touching near a 1-column splitter to drag it naturally.
|
|
29
|
+
Guarantees that interactive inputs, buttons, and composer elements are never hijacked.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
screen = getattr(app, "screen", None)
|
|
33
|
+
if screen is None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
# 1. Guard against redirecting clicks on interactive controls
|
|
37
|
+
widget_at = None
|
|
38
|
+
if hasattr(screen, "get_widget_at"):
|
|
39
|
+
try:
|
|
40
|
+
widget_at, _ = screen.get_widget_at(screen_x, screen_y)
|
|
41
|
+
except Exception:
|
|
42
|
+
widget_at = None
|
|
43
|
+
|
|
44
|
+
if widget_at is not None:
|
|
45
|
+
# Direct click on a resizer itself is allowed
|
|
46
|
+
if getattr(widget_at, "id", None) in ("cct-explorer-resizer", "cct-rightpane-resizer", "cct-composer-resizer"):
|
|
47
|
+
return widget_at
|
|
48
|
+
|
|
49
|
+
# Never hijack clicks intended for text inputs, buttons, switches, or trees
|
|
50
|
+
from textual.widgets import Input, TextArea, Button, OptionList, Select, Switch, Tree
|
|
51
|
+
if isinstance(widget_at, (Input, TextArea, Button, OptionList, Select, Switch, Tree)):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Never hijack clicks inside the composer card (editor stack, prompt row, input, footer, etc.)
|
|
55
|
+
ancestors = [widget_at] + list(getattr(widget_at, "ancestors", []))
|
|
56
|
+
for a in ancestors:
|
|
57
|
+
aid = getattr(a, "id", None)
|
|
58
|
+
if aid in ("cct-composer", "cct-editor-stack", "cct-prompt-row", "cct-input"):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# 2. Resizers to inspect with directional bounds
|
|
62
|
+
for resizer_id in ("cct-explorer-resizer", "cct-rightpane-resizer", "cct-composer-resizer"):
|
|
63
|
+
resizers = screen.query(f"#{resizer_id}")
|
|
64
|
+
for resizer in resizers:
|
|
65
|
+
if not resizer.is_attached or not resizer.visible:
|
|
66
|
+
continue
|
|
67
|
+
region = resizer.region
|
|
68
|
+
|
|
69
|
+
if resizer_id == "cct-composer-resizer":
|
|
70
|
+
# Horizontal splitter above composer:
|
|
71
|
+
# Only expand UPWARD into conversation history (range: region.y - tolerance to region.y + region.height)
|
|
72
|
+
# NEVER expand downward into the composer card (y >= region.y + region.height)
|
|
73
|
+
expanded_x = range(region.x, region.x + region.width)
|
|
74
|
+
expanded_y = range(region.y - tolerance, region.y + region.height)
|
|
75
|
+
else:
|
|
76
|
+
# Vertical splitters (explorer, rightpane):
|
|
77
|
+
# Expand horizontally in X, keep within Y bounds
|
|
78
|
+
expanded_x = range(region.x - tolerance, region.x + region.width + tolerance)
|
|
79
|
+
expanded_y = range(region.y, region.y + region.height)
|
|
80
|
+
|
|
81
|
+
if screen_x in expanded_x and screen_y in expanded_y:
|
|
82
|
+
return resizer
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def find_nearby_toolbar_control(cls, app: App, screen_x: int, screen_y: int, tolerance: int = 1) -> Optional[Widget]:
|
|
89
|
+
"""
|
|
90
|
+
Check if a touch coordinate landed near compact controls (▷, ⿻, ⏻, ✕).
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
screen = getattr(app, "screen", None)
|
|
94
|
+
if screen is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
control_ids = (
|
|
98
|
+
"cct-tb-run", "cct-tb-split", "cct-tb-close", "cct-tb-preview",
|
|
99
|
+
"cct-bv-split", "cct-bv-close", "cct-bv-fullscreen", "cct-bv-reload",
|
|
100
|
+
"btn-send", "btn-attach", "btn-permissions"
|
|
101
|
+
)
|
|
102
|
+
for cid in control_ids:
|
|
103
|
+
matches = screen.query(f"#{cid}")
|
|
104
|
+
for ctrl in matches:
|
|
105
|
+
if not ctrl.is_attached or not ctrl.visible:
|
|
106
|
+
continue
|
|
107
|
+
region = ctrl.region
|
|
108
|
+
expanded_x = range(region.x - tolerance, region.x + region.width + tolerance)
|
|
109
|
+
expanded_y = range(region.y - tolerance, region.y + region.height + tolerance)
|
|
110
|
+
if screen_x in expanded_x and screen_y in expanded_y:
|
|
111
|
+
return ctrl
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
return None
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT Input Architecture — touch.py
|
|
3
|
+
High-fidelity touch interaction handling for terminal applications.
|
|
4
|
+
|
|
5
|
+
Implements:
|
|
6
|
+
1. TouchTapRecognizer:
|
|
7
|
+
- Fuzzy coordinate matching (2-cell tolerance) solving Textual's click-drop bug.
|
|
8
|
+
- Double-tap detection with relaxed offset tolerance.
|
|
9
|
+
- Long-press recognition for touch context actions.
|
|
10
|
+
2. TouchScrollHandler:
|
|
11
|
+
- Natural finger drag-to-scroll across all containers (chat, file tree, editor, modals, settings, model list).
|
|
12
|
+
- Click suppression during drag-scrolling so lifting a finger never causes accidental clicks.
|
|
13
|
+
- Kinetic flick/momentum scrolling with smooth deceleration.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
import math
|
|
20
|
+
from typing import Optional, Tuple, Any, Callable
|
|
21
|
+
from textual import events
|
|
22
|
+
from textual.widget import Widget
|
|
23
|
+
from textual.containers import ScrollableContainer, VerticalScroll
|
|
24
|
+
from textual.scroll_view import ScrollView
|
|
25
|
+
|
|
26
|
+
# Tolerances & Thresholds
|
|
27
|
+
TAP_MAX_DISTANCE = 2.0 # Manhattan distance in cells (finger jitter allowance)
|
|
28
|
+
TAP_MAX_DURATION = 0.45 # Max seconds between down and up for a single tap
|
|
29
|
+
DOUBLE_TAP_MAX_DELAY = 0.40 # Max seconds between taps for double-tap
|
|
30
|
+
DOUBLE_TAP_MAX_DISTANCE = 3.0 # Max distance between consecutive taps
|
|
31
|
+
LONG_PRESS_DURATION = 0.55 # Seconds required to trigger long-press
|
|
32
|
+
SCROLL_DRAG_THRESHOLD = 2.5 # Movement cells required to switch into scroll mode
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TouchTapRecognizer:
|
|
36
|
+
"""Detects clean taps, double-taps, and long-presses despite finger jitter."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, on_tap: Optional[Callable[[Widget, events.MouseUp, int], None]] = None,
|
|
39
|
+
on_long_press: Optional[Callable[[Widget, Tuple[int, int]], None]] = None):
|
|
40
|
+
self.on_tap = on_tap
|
|
41
|
+
self.on_long_press = on_long_press
|
|
42
|
+
|
|
43
|
+
self._active = False
|
|
44
|
+
self._down_time = 0.0
|
|
45
|
+
self._down_pos: Tuple[int, int] = (0, 0)
|
|
46
|
+
self._down_screen_pos: Tuple[int, int] = (0, 0)
|
|
47
|
+
self._down_widget: Optional[Widget] = None
|
|
48
|
+
|
|
49
|
+
# Double-click / chained tap state
|
|
50
|
+
self._last_tap_time = 0.0
|
|
51
|
+
self._last_tap_pos: Tuple[int, int] = (0, 0)
|
|
52
|
+
self._last_tap_widget: Optional[Widget] = None
|
|
53
|
+
self._chain = 1
|
|
54
|
+
|
|
55
|
+
# Long-press timer tracking
|
|
56
|
+
self._long_press_triggered = False
|
|
57
|
+
|
|
58
|
+
def on_mouse_down(self, widget: Optional[Widget], event: events.MouseDown) -> None:
|
|
59
|
+
"""Handle finger contact."""
|
|
60
|
+
if getattr(event, "button", 1) != 1:
|
|
61
|
+
return
|
|
62
|
+
now = time.monotonic()
|
|
63
|
+
self._active = True
|
|
64
|
+
self._down_time = now
|
|
65
|
+
self._down_pos = (event.x, event.y)
|
|
66
|
+
self._down_screen_pos = (getattr(event, "screen_x", event.x), getattr(event, "screen_y", event.y))
|
|
67
|
+
self._down_widget = widget
|
|
68
|
+
self._long_press_triggered = False
|
|
69
|
+
|
|
70
|
+
def on_mouse_move(self, event: events.MouseMove) -> bool:
|
|
71
|
+
"""Check if movement exceeds tap jitter threshold."""
|
|
72
|
+
if not self._active:
|
|
73
|
+
return False
|
|
74
|
+
cur_screen = (getattr(event, "screen_x", event.x), getattr(event, "screen_y", event.y))
|
|
75
|
+
dx = abs(cur_screen[0] - self._down_screen_pos[0])
|
|
76
|
+
dy = abs(cur_screen[1] - self._down_screen_pos[1])
|
|
77
|
+
if dx + dy > TAP_MAX_DISTANCE:
|
|
78
|
+
# Finger moved beyond tap threshold — no longer a tap
|
|
79
|
+
self._active = False
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def check_long_press(self) -> bool:
|
|
84
|
+
"""Poll or timer check for long-press trigger."""
|
|
85
|
+
if not self._active or self._long_press_triggered or self._down_widget is None:
|
|
86
|
+
return False
|
|
87
|
+
now = time.monotonic()
|
|
88
|
+
if now - self._down_time >= LONG_PRESS_DURATION:
|
|
89
|
+
self._long_press_triggered = True
|
|
90
|
+
self._active = False # Suppress normal tap on release
|
|
91
|
+
if self.on_long_press and self._down_widget is not None:
|
|
92
|
+
try:
|
|
93
|
+
self.on_long_press(self._down_widget, self._down_screen_pos)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def on_mouse_up(self, up_widget: Optional[Widget], event: events.MouseUp) -> Tuple[bool, int, Optional[Widget]]:
|
|
100
|
+
"""
|
|
101
|
+
Handle finger lift.
|
|
102
|
+
Returns: (is_tap, click_chain, resolved_target_widget)
|
|
103
|
+
"""
|
|
104
|
+
if not self._active or self._long_press_triggered:
|
|
105
|
+
self._active = False
|
|
106
|
+
return False, 1, None
|
|
107
|
+
|
|
108
|
+
self._active = False
|
|
109
|
+
now = time.monotonic()
|
|
110
|
+
duration = now - self._down_time
|
|
111
|
+
|
|
112
|
+
if duration > TAP_MAX_DURATION:
|
|
113
|
+
return False, 1, None
|
|
114
|
+
|
|
115
|
+
cur_screen = (getattr(event, "screen_x", event.x), getattr(event, "screen_y", event.y))
|
|
116
|
+
dx = abs(cur_screen[0] - self._down_screen_pos[0])
|
|
117
|
+
dy = abs(cur_screen[1] - self._down_screen_pos[1])
|
|
118
|
+
|
|
119
|
+
if dx + dy > TAP_MAX_DISTANCE:
|
|
120
|
+
return False, 1, None
|
|
121
|
+
|
|
122
|
+
# Resolve best target widget:
|
|
123
|
+
# If up_widget is down_widget, exact match.
|
|
124
|
+
# Otherwise, check if down_widget is an interactive widget or descendant/ancestor
|
|
125
|
+
target = self._resolve_target(self._down_widget, up_widget)
|
|
126
|
+
if target is None:
|
|
127
|
+
return False, 1, None
|
|
128
|
+
|
|
129
|
+
# Determine chain (double-tap)
|
|
130
|
+
dist_from_last = (abs(cur_screen[0] - self._last_tap_pos[0]) +
|
|
131
|
+
abs(cur_screen[1] - self._last_tap_pos[1]))
|
|
132
|
+
if (now - self._last_tap_time <= DOUBLE_TAP_MAX_DELAY and
|
|
133
|
+
dist_from_last <= DOUBLE_TAP_MAX_DISTANCE and
|
|
134
|
+
(self._last_tap_widget is target or self._is_related(self._last_tap_widget, target))):
|
|
135
|
+
self._chain += 1
|
|
136
|
+
else:
|
|
137
|
+
self._chain = 1
|
|
138
|
+
|
|
139
|
+
self._last_tap_time = now
|
|
140
|
+
self._last_tap_pos = cur_screen
|
|
141
|
+
self._last_tap_widget = target
|
|
142
|
+
|
|
143
|
+
if self.on_tap:
|
|
144
|
+
try:
|
|
145
|
+
self.on_tap(target, event, self._chain)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
return True, self._chain, target
|
|
150
|
+
|
|
151
|
+
def cancel(self) -> None:
|
|
152
|
+
"""Cancel tap recognition (e.g. when drag-to-scroll takes over)."""
|
|
153
|
+
self._active = False
|
|
154
|
+
self._long_press_triggered = False
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _is_related(w1: Optional[Widget], w2: Optional[Widget]) -> bool:
|
|
158
|
+
if w1 is None or w2 is None:
|
|
159
|
+
return False
|
|
160
|
+
if w1 is w2:
|
|
161
|
+
return True
|
|
162
|
+
# Check parent hierarchy
|
|
163
|
+
cur = w1
|
|
164
|
+
while cur is not None:
|
|
165
|
+
if cur is w2:
|
|
166
|
+
return True
|
|
167
|
+
cur = getattr(cur, "parent", None)
|
|
168
|
+
cur = w2
|
|
169
|
+
while cur is not None:
|
|
170
|
+
if cur is w1:
|
|
171
|
+
return True
|
|
172
|
+
cur = getattr(cur, "parent", None)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def _resolve_target(cls, down_w: Optional[Widget], up_w: Optional[Widget]) -> Optional[Widget]:
|
|
177
|
+
if down_w is None:
|
|
178
|
+
return up_w
|
|
179
|
+
if up_w is None:
|
|
180
|
+
return down_w
|
|
181
|
+
if down_w is up_w:
|
|
182
|
+
return down_w
|
|
183
|
+
|
|
184
|
+
# If one is ancestor of the other, favor the interactive child or button
|
|
185
|
+
from textual.widgets import Button, Tab, Tree
|
|
186
|
+
|
|
187
|
+
interactive_types = (Button, Tab)
|
|
188
|
+
|
|
189
|
+
if isinstance(down_w, interactive_types):
|
|
190
|
+
return down_w
|
|
191
|
+
if isinstance(up_w, interactive_types):
|
|
192
|
+
return up_w
|
|
193
|
+
|
|
194
|
+
# Check if down_widget is inside a Button or interactive control
|
|
195
|
+
cur = down_w
|
|
196
|
+
while cur is not None:
|
|
197
|
+
if isinstance(cur, interactive_types) or getattr(cur, "can_focus", False):
|
|
198
|
+
return cur
|
|
199
|
+
cur = getattr(cur, "parent", None)
|
|
200
|
+
|
|
201
|
+
cur = up_w
|
|
202
|
+
while cur is not None:
|
|
203
|
+
if isinstance(cur, interactive_types) or getattr(cur, "can_focus", False):
|
|
204
|
+
return cur
|
|
205
|
+
cur = getattr(cur, "parent", None)
|
|
206
|
+
|
|
207
|
+
# If they share a direct parent container, down_w is what the user touched first
|
|
208
|
+
return down_w
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TouchScrollHandler:
|
|
212
|
+
"""Manages finger drag-to-scroll with kinetic momentum across any scroll container."""
|
|
213
|
+
|
|
214
|
+
def __init__(self):
|
|
215
|
+
self._is_scrolling = False
|
|
216
|
+
self._container: Optional[Widget] = None
|
|
217
|
+
self._down_screen_y = 0.0
|
|
218
|
+
self._down_screen_x = 0.0
|
|
219
|
+
self._last_move_y = 0.0
|
|
220
|
+
self._last_move_time = 0.0
|
|
221
|
+
self._initial_scroll_y = 0.0
|
|
222
|
+
self._velocity_y = 0.0 # cells per second
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def is_scrolling(self) -> bool:
|
|
226
|
+
return self._is_scrolling
|
|
227
|
+
|
|
228
|
+
def on_mouse_down(self, target_widget: Optional[Widget], event: events.MouseDown) -> bool:
|
|
229
|
+
"""Start tracking potential touch drag."""
|
|
230
|
+
if getattr(event, "button", 1) != 1:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
self._container = self._find_scroll_container(target_widget)
|
|
234
|
+
self._is_scrolling = False
|
|
235
|
+
self._down_screen_y = float(getattr(event, "screen_y", event.y))
|
|
236
|
+
self._down_screen_x = float(getattr(event, "screen_x", event.x))
|
|
237
|
+
self._last_move_y = self._down_screen_y
|
|
238
|
+
self._last_move_time = time.monotonic()
|
|
239
|
+
self._velocity_y = 0.0
|
|
240
|
+
|
|
241
|
+
if self._container is not None:
|
|
242
|
+
self._initial_scroll_y = float(getattr(self._container, "scroll_y", 0.0))
|
|
243
|
+
return True
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
def on_mouse_move(self, event: events.MouseMove) -> bool:
|
|
247
|
+
"""Handle finger movement and perform drag scrolling."""
|
|
248
|
+
if self._container is None:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
now = time.monotonic()
|
|
252
|
+
cur_y = float(getattr(event, "screen_y", event.y))
|
|
253
|
+
dt = max(0.001, now - self._last_move_time)
|
|
254
|
+
|
|
255
|
+
dy_total = cur_y - self._down_screen_y
|
|
256
|
+
step_dy = cur_y - self._last_move_y
|
|
257
|
+
|
|
258
|
+
if not self._is_scrolling:
|
|
259
|
+
# Check threshold to enter drag-scroll mode
|
|
260
|
+
if abs(dy_total) >= SCROLL_DRAG_THRESHOLD:
|
|
261
|
+
self._is_scrolling = True
|
|
262
|
+
|
|
263
|
+
if self._is_scrolling:
|
|
264
|
+
# Invert delta: dragging finger UP scrolls content DOWN
|
|
265
|
+
scroll_delta = -step_dy
|
|
266
|
+
self._apply_scroll(self._container, scroll_delta)
|
|
267
|
+
|
|
268
|
+
# Update velocity moving average
|
|
269
|
+
instant_velocity = -step_dy / dt
|
|
270
|
+
self._velocity_y = 0.6 * self._velocity_y + 0.4 * instant_velocity
|
|
271
|
+
|
|
272
|
+
self._last_move_y = cur_y
|
|
273
|
+
self._last_move_time = now
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def on_mouse_up(self, event: events.MouseUp) -> bool:
|
|
279
|
+
"""Finish drag-scroll and trigger flick momentum if applicable."""
|
|
280
|
+
was_scrolling = self._is_scrolling
|
|
281
|
+
container = self._container
|
|
282
|
+
|
|
283
|
+
self._is_scrolling = False
|
|
284
|
+
self._container = None
|
|
285
|
+
|
|
286
|
+
if was_scrolling and container is not None:
|
|
287
|
+
# Apply momentum if released with velocity (> 8 cells/sec)
|
|
288
|
+
if abs(self._velocity_y) > 8.0:
|
|
289
|
+
self._apply_momentum(container, self._velocity_y)
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def _find_scroll_container(cls, start_widget: Optional[Widget]) -> Optional[Widget]:
|
|
296
|
+
"""Find the nearest scrollable ancestor with actual overflowing content."""
|
|
297
|
+
if start_widget is None:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
# Interactive controls must never trigger drag-scrolling
|
|
301
|
+
from textual.widgets import Button, Input, TextArea, Tab, Select, OptionList, Checkbox, RadioSet, Switch
|
|
302
|
+
interactive_types = (Button, Input, TextArea, Tab, Select, OptionList, Checkbox, RadioSet, Switch)
|
|
303
|
+
|
|
304
|
+
# Check if the touched widget or its immediate ancestors are interactive
|
|
305
|
+
check_node = start_widget
|
|
306
|
+
for _ in range(3):
|
|
307
|
+
if check_node is None:
|
|
308
|
+
break
|
|
309
|
+
if isinstance(check_node, (ScrollableContainer, VerticalScroll, ScrollView)):
|
|
310
|
+
break
|
|
311
|
+
if isinstance(check_node, interactive_types) or getattr(check_node, "can_focus", False):
|
|
312
|
+
return None
|
|
313
|
+
check_node = getattr(check_node, "parent", None)
|
|
314
|
+
|
|
315
|
+
cur = start_widget
|
|
316
|
+
while cur is not None:
|
|
317
|
+
# Check for scrollable containers that actually have scrollable content
|
|
318
|
+
if isinstance(cur, (ScrollableContainer, VerticalScroll, ScrollView)):
|
|
319
|
+
if getattr(cur, "max_scroll_y", 0) > 0:
|
|
320
|
+
return cur
|
|
321
|
+
cur = getattr(cur, "parent", None)
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def _apply_scroll(cls, container: Widget, delta_y: float) -> None:
|
|
326
|
+
"""Apply vertical scroll offset."""
|
|
327
|
+
try:
|
|
328
|
+
if hasattr(container, "scroll_relative"):
|
|
329
|
+
container.scroll_relative(y=delta_y, animate=False)
|
|
330
|
+
elif hasattr(container, "scroll_to"):
|
|
331
|
+
new_y = max(0.0, getattr(container, "scroll_y", 0.0) + delta_y)
|
|
332
|
+
container.scroll_to(y=new_y, animate=False)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def _apply_momentum(cls, container: Widget, initial_velocity: float) -> None:
|
|
338
|
+
"""Apply smooth kinetic flick decay."""
|
|
339
|
+
try:
|
|
340
|
+
# Cap momentum travel to prevent runaway scrolls
|
|
341
|
+
travel = math.copysign(min(18.0, abs(initial_velocity) * 0.18), initial_velocity)
|
|
342
|
+
if hasattr(container, "scroll_relative"):
|
|
343
|
+
container.scroll_relative(y=travel, animate=True, duration=0.22, easing="out_cubic")
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
calc_terminal/keys.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform single-keypress reader, used to drive shortcut keys
|
|
3
|
+
inside the live simulations (atom viewer, orbital viewer, graphs)
|
|
4
|
+
without needing to press Enter.
|
|
5
|
+
|
|
6
|
+
Falls back gracefully to blocking input() on platforms/terminals where
|
|
7
|
+
raw mode isn't available (e.g. some CI pipes) so the app never crashes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import select
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def stdin_is_interactive():
|
|
15
|
+
"""True if stdin is a real attached terminal that can deliver raw
|
|
16
|
+
keypresses. False for pipes, redirected files, and other
|
|
17
|
+
non-interactive contexts (CI runners, containers without -it,
|
|
18
|
+
scripted input, etc).
|
|
19
|
+
|
|
20
|
+
This matters because select.select() reports a pipe at EOF as
|
|
21
|
+
'readable' forever, and termios/msvcrt both fail silently on a
|
|
22
|
+
non-tty fd. Without this check, a live view driven by
|
|
23
|
+
key_available()/read_key() spins in a tight busy loop forever,
|
|
24
|
+
burning CPU and never exiting, since it can never actually detect a
|
|
25
|
+
'q' keypress that will never arrive."""
|
|
26
|
+
try:
|
|
27
|
+
return sys.stdin.isatty()
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def key_available():
|
|
33
|
+
"""Non-blocking check: is a key waiting in stdin? Unix only; on
|
|
34
|
+
platforms without select-on-stdin support this always returns False
|
|
35
|
+
(the caller should just keep animating on a timer). Always False
|
|
36
|
+
when stdin isn't a real interactive terminal — see
|
|
37
|
+
stdin_is_interactive() for why."""
|
|
38
|
+
if not stdin_is_interactive():
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
r, _, _ = select.select([sys.stdin], [], [], 0)
|
|
42
|
+
return bool(r)
|
|
43
|
+
except Exception:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_key():
|
|
48
|
+
"""Read exactly one keypress, no Enter required. Returns '' if no
|
|
49
|
+
raw-mode backend is available."""
|
|
50
|
+
try:
|
|
51
|
+
import termios
|
|
52
|
+
import tty
|
|
53
|
+
fd = sys.stdin.fileno()
|
|
54
|
+
old = termios.tcgetattr(fd)
|
|
55
|
+
try:
|
|
56
|
+
tty.setcbreak(fd)
|
|
57
|
+
ch = sys.stdin.read(1)
|
|
58
|
+
if ch == '\x1b': # potential escape sequence
|
|
59
|
+
# peek if more chars are coming (non-blocking)
|
|
60
|
+
r, _, _ = select.select([sys.stdin], [], [], 0.01)
|
|
61
|
+
if r:
|
|
62
|
+
ch += sys.stdin.read(2) # read [A, [B, etc.
|
|
63
|
+
finally:
|
|
64
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
65
|
+
return ch
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
try:
|
|
69
|
+
import msvcrt
|
|
70
|
+
if msvcrt.kbhit():
|
|
71
|
+
return msvcrt.getch().decode(errors="ignore")
|
|
72
|
+
return ""
|
|
73
|
+
except Exception:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
SHORTCUTS_HELP = [
|
|
78
|
+
("q", "quit the live view / return to menu"),
|
|
79
|
+
("space", "pause / resume animation"),
|
|
80
|
+
("+ / -", "speed up / slow down"),
|
|
81
|
+
("n / p", "next / previous element or state"),
|
|
82
|
+
("r", "reset / randomize"),
|
|
83
|
+
("s", "save a snapshot (2D/3D image export)"),
|
|
84
|
+
]
|