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,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — ComposerFooter (the single badge row docked inside StickyComposer:
|
|
3
|
+
`NOTEBOOK · provider/model · Workspace` on the left, live context readout
|
|
4
|
+
center, minimal Permissions/Attach/Send glyphs right — flat dot-separated
|
|
5
|
+
text badges, not a button toolbar, matching the reference terminal-chat
|
|
6
|
+
aesthetic) and StatusLine (the compact bar CCTApp docks at the very
|
|
7
|
+
bottom of the screen). Both render state and dispatch events/bubble
|
|
8
|
+
presses to their parent — neither decides what a click means.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
print("This is a library file and is not meant to be run directly.")
|
|
13
|
+
import sys
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
|
|
16
|
+
TEXTUAL_AVAILABLE = True
|
|
17
|
+
try:
|
|
18
|
+
from textual.containers import Horizontal
|
|
19
|
+
from textual.widgets import Static, Button
|
|
20
|
+
from textual.reactive import reactive
|
|
21
|
+
except Exception:
|
|
22
|
+
TEXTUAL_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
from .events import NotebookChanged, WorkspaceChanged, CommandExecuted
|
|
25
|
+
from .. import ai_modes
|
|
26
|
+
|
|
27
|
+
WORKSPACE_CYCLE = ["Workspace", "Kinetics", "Quantum", "Notebook A"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if TEXTUAL_AVAILABLE:
|
|
31
|
+
|
|
32
|
+
def _hex_blend(h1, h2, t):
|
|
33
|
+
"""Linear interpolate two '#rrggbb' hex colors at t in [0, 1].
|
|
34
|
+
Returns a '#rrggbb' hex string — local helper so the mode badge
|
|
35
|
+
can render its 4-segment gradient bar without reaching into
|
|
36
|
+
theme internals (theme.blend takes RGB tuples, not hex)."""
|
|
37
|
+
a = [int(h1[i:i + 2], 16) for i in (1, 3, 5)]
|
|
38
|
+
b = [int(h2[i:i + 2], 16) for i in (1, 3, 5)]
|
|
39
|
+
return "#" + "".join(f"{int(round(x + (y - x) * t)):02x}"
|
|
40
|
+
for x, y in zip(a, b))
|
|
41
|
+
|
|
42
|
+
class _Badge(Static):
|
|
43
|
+
"""One flat, clickable text badge (no border/background chrome) —
|
|
44
|
+
the `Notebook` / `provider model` / `Workspace` pieces of
|
|
45
|
+
`NOTEBOOK \u00b7 openai/gpt-4o-mini \u00b7 Workspace`."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, text, badge_id, on_click=None, classes="cct-badge"):
|
|
48
|
+
super().__init__(text, id=badge_id, classes=classes)
|
|
49
|
+
self._on_click = on_click
|
|
50
|
+
|
|
51
|
+
def on_click(self, event):
|
|
52
|
+
if self._on_click:
|
|
53
|
+
self._on_click()
|
|
54
|
+
|
|
55
|
+
class ComposerFooter(Horizontal):
|
|
56
|
+
"""The single badge row along the bottom of the composer card:
|
|
57
|
+
`Notebook \u00b7 model \u00b7 Workspace` (left), live context (center),
|
|
58
|
+
three minimal glyph controls (right). One row, one component.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Holds the AI-mode KEY ("notebook"/"agent"/"build"/"plan" — see
|
|
62
|
+
# ai_modes.py), not a display string. Reactive name kept as
|
|
63
|
+
# `notebook_mode` (rather than renamed to `ai_mode`) so the
|
|
64
|
+
# NotebookChanged event this posts, and ui/app.py's existing
|
|
65
|
+
# on_notebook_changed handler, didn't need touching to grow
|
|
66
|
+
# from a 2-way NOTEBOOK/AGENT toggle to the spec's 4 modes.
|
|
67
|
+
notebook_mode = reactive("notebook")
|
|
68
|
+
context_tokens = reactive(0)
|
|
69
|
+
|
|
70
|
+
def __init__(self, model_label="no model"):
|
|
71
|
+
super().__init__(id="cct-bottom-row")
|
|
72
|
+
self._model_label = model_label
|
|
73
|
+
self._workspace_i = 0
|
|
74
|
+
|
|
75
|
+
def _mode_badge_markup(self, mode_key):
|
|
76
|
+
accent = ai_modes.accent_hex(mode_key)
|
|
77
|
+
m = ai_modes.meta(mode_key)
|
|
78
|
+
# v0.7.8: per-mode gradient applied to the badge — a 4-segment
|
|
79
|
+
# ▮ bar blending the mode's two-stop gradient, then the icon in
|
|
80
|
+
# the start color and the label in the end color.
|
|
81
|
+
try:
|
|
82
|
+
gs, ge = ai_modes.gradient(mode_key)
|
|
83
|
+
bar = "".join(f"[{_hex_blend(gs, ge, t)}]\u25ae[/]"
|
|
84
|
+
for t in (0.0, 0.33, 0.66, 1.0))
|
|
85
|
+
return f"{bar} [{gs} b]{m['icon']}[/] [{ge} b]{m['label']}[/]"
|
|
86
|
+
except Exception:
|
|
87
|
+
return f"[{accent} b]{m['icon']} {m['label']}[/]"
|
|
88
|
+
|
|
89
|
+
def compose(self):
|
|
90
|
+
with Horizontal(id="cct-left-controls"):
|
|
91
|
+
yield _Badge(self._mode_badge_markup(self.notebook_mode),
|
|
92
|
+
"badge-notebook", self._toggle_notebook)
|
|
93
|
+
yield Static("\u00b7", classes="cct-badge-sep")
|
|
94
|
+
yield _Badge(self._model_label, "badge-model", self._open_model)
|
|
95
|
+
yield Static("\u00b7", classes="cct-badge-sep")
|
|
96
|
+
yield _Badge(WORKSPACE_CYCLE[0], "badge-workspace", self._cycle_workspace)
|
|
97
|
+
yield Static(id="cct-footer-spacer-left")
|
|
98
|
+
yield Static(self._status_text(), id="cct-center-status")
|
|
99
|
+
yield Static(id="cct-footer-spacer-right")
|
|
100
|
+
with Horizontal(id="cct-right-controls"):
|
|
101
|
+
yield Button("\U0001f6e1", id="btn-permissions", classes="cct-icon-btn", tooltip="Permissions & Security (🛡)")
|
|
102
|
+
yield Button("\U0001f4ce", id="btn-attach", classes="cct-icon-btn", tooltip="Attach file")
|
|
103
|
+
yield Button("\u27a4", id="btn-send", classes="cct-icon-btn cct-icon-send", tooltip="Send message (Enter)")
|
|
104
|
+
|
|
105
|
+
def _status_text(self):
|
|
106
|
+
return f"Context {self.context_tokens}K"
|
|
107
|
+
|
|
108
|
+
def watch_context_tokens(self, value):
|
|
109
|
+
try:
|
|
110
|
+
self.query_one("#cct-center-status", Static).update(self._status_text())
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def set_model_label(self, label):
|
|
115
|
+
self._model_label = label
|
|
116
|
+
try:
|
|
117
|
+
self.query_one("#badge-model", _Badge).update(label)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def _toggle_notebook(self):
|
|
122
|
+
"""Clicking the badge cycles Notebook -> Agent -> Build ->
|
|
123
|
+
Plan -> Notebook (spec section 3's four AI modes, up from
|
|
124
|
+
the old two-stop NOTEBOOK/AGENT toggle)."""
|
|
125
|
+
self.notebook_mode = ai_modes.next_mode(self.notebook_mode)
|
|
126
|
+
self.query_one("#badge-notebook", _Badge).update(
|
|
127
|
+
self._mode_badge_markup(self.notebook_mode))
|
|
128
|
+
self.post_message(NotebookChanged(self.notebook_mode))
|
|
129
|
+
|
|
130
|
+
def _open_model(self):
|
|
131
|
+
self.post_message(CommandExecuted("/model"))
|
|
132
|
+
|
|
133
|
+
def _cycle_workspace(self):
|
|
134
|
+
self._workspace_i = (self._workspace_i + 1) % len(WORKSPACE_CYCLE)
|
|
135
|
+
name = WORKSPACE_CYCLE[self._workspace_i]
|
|
136
|
+
self.query_one("#badge-workspace", _Badge).update(name)
|
|
137
|
+
self.post_message(WorkspaceChanged(name))
|
|
138
|
+
|
|
139
|
+
class StatusLine(Static):
|
|
140
|
+
"""One compact bottom status line for the whole app — no boxes,
|
|
141
|
+
no oversized labels. `get_fields` is a zero-arg callable
|
|
142
|
+
returning [(label, value, tone), ...]; CCTApp supplies it and
|
|
143
|
+
calls `refresh_status()` when something worth showing changes,
|
|
144
|
+
instead of this widget reaching into backend state itself."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, get_fields, id="cct-statusline"):
|
|
147
|
+
super().__init__(id=id)
|
|
148
|
+
self._get_fields = get_fields
|
|
149
|
+
|
|
150
|
+
def on_mount(self):
|
|
151
|
+
self.refresh_status()
|
|
152
|
+
|
|
153
|
+
def refresh_status(self):
|
|
154
|
+
from . import theme_css
|
|
155
|
+
tone_role = {"ready": "success", "warn": "warning", "err": "error", "": "text-muted"}
|
|
156
|
+
parts = []
|
|
157
|
+
for label, value, tone in self._get_fields():
|
|
158
|
+
color = theme_css.current_hex(tone_role.get(tone, "text-muted"))
|
|
159
|
+
text = f"[{color}]{value}[/]" if not label else f"{label} [{color}]{value}[/]"
|
|
160
|
+
parts.append(text)
|
|
161
|
+
self.update(" \u00b7 ".join(parts))
|
|
162
|
+
|
|
163
|
+
else:
|
|
164
|
+
ComposerFooter = None
|
|
165
|
+
StatusLine = None
|
|
166
|
+
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT UI — gestures_panel.py: Gesture Manager (spec section 23).
|
|
3
|
+
|
|
4
|
+
Responsive: uses percentage widths, auto height, scroll, and stacked
|
|
5
|
+
fallbacks for narrow terminals. Buttons use consistent sizing and
|
|
6
|
+
plain-text labels (no emoji-width mismatches). Layout never clips.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
print("This is a library file and is not meant to be run directly.")
|
|
11
|
+
import sys
|
|
12
|
+
sys.exit(1)
|
|
13
|
+
|
|
14
|
+
from ..gestures.bindings import GESTURE_TRIGGERS, GESTURE_TARGETS, GESTURE_ACTIONS
|
|
15
|
+
from ..gestures.manager import list_gestures, add_gesture, delete_gesture, toggle_gesture, reset_defaults
|
|
16
|
+
|
|
17
|
+
TEXTUAL_AVAILABLE = True
|
|
18
|
+
try:
|
|
19
|
+
from textual.containers import Vertical, Horizontal, VerticalScroll
|
|
20
|
+
from textual.screen import Screen
|
|
21
|
+
from textual.widgets import Static, Button, Input, Select
|
|
22
|
+
from textual.binding import Binding
|
|
23
|
+
except Exception:
|
|
24
|
+
TEXTUAL_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TEXTUAL_AVAILABLE:
|
|
28
|
+
|
|
29
|
+
class _GestureRow(Horizontal):
|
|
30
|
+
def __init__(self, gesture):
|
|
31
|
+
super().__init__(classes="cct-gesture-row")
|
|
32
|
+
self.gesture = gesture
|
|
33
|
+
|
|
34
|
+
def compose(self):
|
|
35
|
+
g = self.gesture
|
|
36
|
+
enabled = g.get("enabled", True)
|
|
37
|
+
name = g.get("name","")
|
|
38
|
+
trg = g.get("trigger","")
|
|
39
|
+
tgt = g.get("target","")
|
|
40
|
+
act = g.get("action","")
|
|
41
|
+
keys = g.get("keys","")
|
|
42
|
+
detail = f"{keys} ({tgt}) -> {act}" if trg == "shortcut" and keys else f"{trg} on {tgt} -> {act}"
|
|
43
|
+
# Left: name + detail stacked, so narrow terminals don't clip
|
|
44
|
+
with Vertical(classes="cct-gesture-left"):
|
|
45
|
+
if enabled:
|
|
46
|
+
yield Static(f"[b]{name}[/]", classes="cct-gesture-name")
|
|
47
|
+
else:
|
|
48
|
+
yield Static(f"[dim]{name} (disabled)[/]", classes="cct-gesture-name")
|
|
49
|
+
yield Static(detail, classes="cct-gesture-detail")
|
|
50
|
+
# Right: actions - fixed width group that stays visible
|
|
51
|
+
with Horizontal(classes="cct-gesture-actions"):
|
|
52
|
+
label = "Disable" if enabled else "Enable"
|
|
53
|
+
yield Button(label, id=f"gesture-toggle-{g.get('id')}", classes="cct-btn cct-btn-sm")
|
|
54
|
+
yield Button("Delete", id=f"gesture-del-{g.get('id')}", variant="error", classes="cct-btn cct-btn-sm")
|
|
55
|
+
|
|
56
|
+
def on_button_pressed(self, event):
|
|
57
|
+
event.stop()
|
|
58
|
+
gid = self.gesture.get("id")
|
|
59
|
+
if event.button.id == f"gesture-toggle-{gid}":
|
|
60
|
+
toggle_gesture(gid)
|
|
61
|
+
for scr in self.app.screen_stack:
|
|
62
|
+
if isinstance(scr, GesturesPanel):
|
|
63
|
+
scr.refresh_gestures()
|
|
64
|
+
break
|
|
65
|
+
elif event.button.id == f"gesture-del-{gid}":
|
|
66
|
+
delete_gesture(gid)
|
|
67
|
+
for scr in self.app.screen_stack:
|
|
68
|
+
if isinstance(scr, GesturesPanel):
|
|
69
|
+
scr.refresh_gestures()
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GesturesPanel(Screen):
|
|
74
|
+
"""Gesture Manager — responsive CAT-style manager panel."""
|
|
75
|
+
|
|
76
|
+
CSS = """
|
|
77
|
+
GesturesPanel { align: center middle; background: $app-background 60%; }
|
|
78
|
+
|
|
79
|
+
#gestures-box {
|
|
80
|
+
width: 84; height: auto; max-height: 42;
|
|
81
|
+
background: $surface; border: round $border;
|
|
82
|
+
padding: 0; overflow: hidden;
|
|
83
|
+
layout: vertical;
|
|
84
|
+
}
|
|
85
|
+
#gestures-titlebar {
|
|
86
|
+
height: 3; min-height: 3;
|
|
87
|
+
padding: 1 2 0 2; border-bottom: solid $border;
|
|
88
|
+
layout: horizontal;
|
|
89
|
+
}
|
|
90
|
+
#gestures-title { width: 1fr; text-style: bold; }
|
|
91
|
+
#gestures-subtitle { color: $text-faint; height: auto; padding: 0 2 1 2; }
|
|
92
|
+
#gestures-disabled { height: auto; padding: 2 2; align: center middle; }
|
|
93
|
+
.gestures-disabled-msg { color: $warning; text-style: bold; height: auto; text-align: center; padding-bottom: 1; }
|
|
94
|
+
|
|
95
|
+
#gestures-count {
|
|
96
|
+
height: 1; color: $text-faint;
|
|
97
|
+
padding: 0 2; margin-bottom: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#gestures-list {
|
|
101
|
+
height: 1fr; min-height: 8; max-height: 20;
|
|
102
|
+
overflow-y: auto; overflow-x: hidden;
|
|
103
|
+
margin: 0 1; padding: 0 0;
|
|
104
|
+
scrollbar-gutter: stable; scrollbar-size: 1 1;
|
|
105
|
+
scrollbar-color: $border $surface;
|
|
106
|
+
scrollbar-color-hover: $accent $surface;
|
|
107
|
+
}
|
|
108
|
+
#gestures-empty {
|
|
109
|
+
color: $text-faint; padding: 2 1; text-align: center; height: auto;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.cct-gesture-row {
|
|
113
|
+
height: auto; min-height: 4;
|
|
114
|
+
padding: 1 1;
|
|
115
|
+
border-bottom: solid $border;
|
|
116
|
+
border-left: thick transparent;
|
|
117
|
+
layout: horizontal;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
}
|
|
120
|
+
.cct-gesture-row:hover {
|
|
121
|
+
border-left: thick $accent;
|
|
122
|
+
background: $surface-alt 40%;
|
|
123
|
+
}
|
|
124
|
+
.cct-gesture-left { width: 1fr; height: auto; min-width: 12; }
|
|
125
|
+
.cct-gesture-name { width: auto; height: 1; }
|
|
126
|
+
.cct-gesture-detail { width: auto; height: 1; color: $text-muted; }
|
|
127
|
+
.cct-gesture-actions {
|
|
128
|
+
width: auto; height: auto; min-width: 20;
|
|
129
|
+
layout: horizontal; align: center middle;
|
|
130
|
+
}
|
|
131
|
+
.cct-gesture-actions Button { margin-left: 1; min-width: 10; height: 3; }
|
|
132
|
+
|
|
133
|
+
/* Compact: stack left+actions vertically on very narrow terminals */
|
|
134
|
+
.cct-compact .cct-gesture-row { layout: vertical; min-height: 6; }
|
|
135
|
+
.cct-compact .cct-gesture-actions { width: 100%; padding-top: 1; }
|
|
136
|
+
|
|
137
|
+
/* Form — responsive: rows become vertical on narrow */
|
|
138
|
+
#gestures-form {
|
|
139
|
+
height: auto; padding: 1 2;
|
|
140
|
+
border-top: solid $border; background: $app-background 35%;
|
|
141
|
+
}
|
|
142
|
+
#gestures-form-title { height: 1; text-style: bold; color: $accent; padding-bottom: 1; }
|
|
143
|
+
#gestures-form-row1 { height: 3; margin-bottom: 1; }
|
|
144
|
+
#gestures-form-row1 Input { width: 1fr; }
|
|
145
|
+
#gestures-form-row2 {
|
|
146
|
+
height: auto; min-height: 3;
|
|
147
|
+
layout: horizontal; overflow-x: auto;
|
|
148
|
+
scrollbar-size: 1 1; scrollbar-gutter: stable;
|
|
149
|
+
}
|
|
150
|
+
#gestures-form-row2 Select { width: 1fr; min-width: 14; margin-right: 1; margin-bottom: 1; }
|
|
151
|
+
#gestures-form-row2 Button { min-width: 12; height: 3; }
|
|
152
|
+
|
|
153
|
+
#gestures-form-row3 {
|
|
154
|
+
height: 3; margin-top: 1; layout: horizontal;
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
#gestures-form-row3.show { display: block; }
|
|
158
|
+
#gestures-form-row3 Input { width: 1fr; }
|
|
159
|
+
#gestures-shortcut-hint { color: $text-faint; height: 1; padding: 0 2; display: none; }
|
|
160
|
+
#gestures-shortcut-hint.show { display: block; }
|
|
161
|
+
|
|
162
|
+
.cct-compact #gestures-form-row2 { layout: vertical; }
|
|
163
|
+
.cct-compact #gestures-form-row2 Select { width: 100%; margin-right: 0; }
|
|
164
|
+
|
|
165
|
+
#gestures-actions {
|
|
166
|
+
height: auto; min-height: 3; padding: 1 2;
|
|
167
|
+
layout: horizontal; overflow-x: auto;
|
|
168
|
+
scrollbar-size: 1 1; scrollbar-gutter: stable;
|
|
169
|
+
border-top: solid $border;
|
|
170
|
+
}
|
|
171
|
+
#gestures-actions Button { margin-right: 1; margin-bottom: 1; min-width: 16; }
|
|
172
|
+
|
|
173
|
+
#gestures-hint {
|
|
174
|
+
height: auto; min-height: 2;
|
|
175
|
+
color: $text-faint; padding: 1 2;
|
|
176
|
+
text-align: center;
|
|
177
|
+
border-top: solid $border;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.cct-compact #gestures-box { width: 96; height: auto; }
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
BINDINGS = [Binding("escape", "cancel", "Cancel")]
|
|
184
|
+
|
|
185
|
+
def __init__(self):
|
|
186
|
+
super().__init__()
|
|
187
|
+
|
|
188
|
+
def compose(self):
|
|
189
|
+
# Extension guard: if Gestures is disabled/uninstalled, show disabled state
|
|
190
|
+
try:
|
|
191
|
+
from .. import extensions as _ext
|
|
192
|
+
if not _ext.is_enabled("gestures"):
|
|
193
|
+
with Vertical(id="gestures-box"):
|
|
194
|
+
with Horizontal(id="gestures-titlebar"):
|
|
195
|
+
yield Static("Gestures & Shortcuts", id="gestures-title")
|
|
196
|
+
yield Button("✕", id="gestures-close", classes="cct-ctrl")
|
|
197
|
+
yield Static("Gestures is disabled — enable it in Extensions to use gestures and shortcuts.", id="gestures-subtitle")
|
|
198
|
+
with Vertical(id="gestures-disabled"):
|
|
199
|
+
yield Static("🔌 Gestures is currently disabled.", classes="gestures-disabled-msg")
|
|
200
|
+
yield Button("Open Extensions", id="gestures-open-ext", variant="primary")
|
|
201
|
+
yield Button("Close", id="gestures-close2")
|
|
202
|
+
return
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
with Vertical(id="gestures-box"):
|
|
206
|
+
with Horizontal(id="gestures-titlebar"):
|
|
207
|
+
yield Static("Gestures & Shortcuts", id="gestures-title")
|
|
208
|
+
yield Button("X", id="gestures-close", classes="cct-ctrl")
|
|
209
|
+
# v0.7.9.10 fix: Gestures is independent of MCP — never show
|
|
210
|
+
# "MCP server down" here. This panel manages only gestures/
|
|
211
|
+
# shortcuts (see gestures/manager.py), while MCP servers
|
|
212
|
+
# live exclusively in McpServersPanel (calc_terminal/mcp.py).
|
|
213
|
+
yield Static("Gestures: double-click/long-press/swipe. Shortcuts: custom key combos -> actions. (Independent of MCP servers)", id="gestures-subtitle")
|
|
214
|
+
yield Static("", id="gestures-count")
|
|
215
|
+
with VerticalScroll(id="gestures-list"):
|
|
216
|
+
pass
|
|
217
|
+
with Vertical(id="gestures-form"):
|
|
218
|
+
yield Static("Add new gesture / shortcut", id="gestures-form-title")
|
|
219
|
+
with Horizontal(id="gestures-form-row1"):
|
|
220
|
+
yield Input(placeholder="Name (e.g. Shift+Enter -> Preview)", id="gestures-name")
|
|
221
|
+
with Horizontal(id="gestures-form-row2"):
|
|
222
|
+
yield Select([(t, t) for t in GESTURE_TRIGGERS], prompt="Trigger", id="gestures-trigger", value=GESTURE_TRIGGERS[0])
|
|
223
|
+
yield Select([(t, t) for t in GESTURE_TARGETS], prompt="Target", id="gestures-target", value=GESTURE_TARGETS[0])
|
|
224
|
+
yield Select([(a, a) for a in GESTURE_ACTIONS], prompt="Action", id="gestures-action", value=GESTURE_ACTIONS[0])
|
|
225
|
+
yield Button("Add", id="gestures-add", variant="primary", classes="cct-btn-sm")
|
|
226
|
+
with Horizontal(id="gestures-form-row3"):
|
|
227
|
+
yield Input(placeholder="Shortcut keys (e.g. shift+enter, ctrl+s, ctrl+shift+p)", id="gestures-keys")
|
|
228
|
+
yield Static("Hint: shift+enter, ctrl+s, ctrl+shift+p, f5, alt+enter ...", id="gestures-shortcut-hint")
|
|
229
|
+
with Horizontal(id="gestures-actions"):
|
|
230
|
+
yield Button("Reset Defaults", id="gestures-reset", classes="cct-btn-sm")
|
|
231
|
+
yield Button("Close", id="gestures-close2", variant="primary", classes="cct-btn-sm")
|
|
232
|
+
yield Static("Double-click editor -> VS Code · preview -> fullscreen · shortcut: shift+enter -> preview", id="gestures-hint")
|
|
233
|
+
|
|
234
|
+
def on_mount(self):
|
|
235
|
+
self.call_after_refresh(lambda: self.query_one("#gestures-box").add_class("open") if self.query_one("#gestures-box").has_class("open") is False else None)
|
|
236
|
+
try:
|
|
237
|
+
self.query_one("#gestures-box").add_class("open")
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
self.call_after_refresh(self.refresh_gestures)
|
|
241
|
+
self._update_shortcut_row()
|
|
242
|
+
try:
|
|
243
|
+
if self.size.width < 76 or self.size.height < 30:
|
|
244
|
+
self.query_one("#gestures-box").add_class("cct-compact")
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
try:
|
|
248
|
+
self.query_one("#gestures-name", Input).focus()
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
def _update_shortcut_row(self):
|
|
253
|
+
try:
|
|
254
|
+
trg = self.query_one("#gestures-trigger", Select).value
|
|
255
|
+
show = (trg == "shortcut")
|
|
256
|
+
row = self.query_one("#gestures-form-row3", Horizontal)
|
|
257
|
+
hint = self.query_one("#gestures-shortcut-hint", Static)
|
|
258
|
+
if show:
|
|
259
|
+
row.add_class("show")
|
|
260
|
+
hint.add_class("show")
|
|
261
|
+
else:
|
|
262
|
+
row.remove_class("show")
|
|
263
|
+
hint.remove_class("show")
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
def on_select_changed(self, event):
|
|
268
|
+
# show/hide shortcut keys input when trigger changes
|
|
269
|
+
if getattr(event.select, "id", "") == "gestures-trigger":
|
|
270
|
+
self._update_shortcut_row()
|
|
271
|
+
|
|
272
|
+
def refresh_gestures(self):
|
|
273
|
+
try:
|
|
274
|
+
container = self.query_one("#gestures-list", VerticalScroll)
|
|
275
|
+
count_label = self.query_one("#gestures-count", Static)
|
|
276
|
+
except Exception:
|
|
277
|
+
return
|
|
278
|
+
try:
|
|
279
|
+
container.remove_children()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
gestures = list_gestures()
|
|
283
|
+
enabled = sum(1 for g in gestures if g.get("enabled"))
|
|
284
|
+
try:
|
|
285
|
+
count_label.update(f"{len(gestures)} gestures · {enabled} enabled")
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
if not gestures:
|
|
289
|
+
try:
|
|
290
|
+
container.mount(Static("No gestures yet.\nAdd one below or reset to defaults.", id="gestures-empty"))
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
return
|
|
294
|
+
for g in gestures:
|
|
295
|
+
try:
|
|
296
|
+
container.mount(_GestureRow(g))
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
def on_button_pressed(self, event):
|
|
301
|
+
bid = event.button.id
|
|
302
|
+
if bid == "gestures-open-ext":
|
|
303
|
+
try:
|
|
304
|
+
self.dismiss(None)
|
|
305
|
+
from .extensions_panel import ExtensionsPanel
|
|
306
|
+
self.app.push_screen(ExtensionsPanel())
|
|
307
|
+
except Exception:
|
|
308
|
+
self.dismiss(None)
|
|
309
|
+
return
|
|
310
|
+
if bid in ("gestures-close", "gestures-close2"):
|
|
311
|
+
self.dismiss(None)
|
|
312
|
+
elif bid == "gestures-reset":
|
|
313
|
+
reset_defaults()
|
|
314
|
+
self.refresh_gestures()
|
|
315
|
+
try:
|
|
316
|
+
self.app._system_note("Gestures reset to defaults")
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
elif bid == "gestures-add":
|
|
320
|
+
try:
|
|
321
|
+
name = self.query_one("#gestures-name", Input).value.strip() or "Custom gesture"
|
|
322
|
+
try:
|
|
323
|
+
trigger = self.query_one("#gestures-trigger", Select).value
|
|
324
|
+
except Exception:
|
|
325
|
+
trigger = None
|
|
326
|
+
try:
|
|
327
|
+
target = self.query_one("#gestures-target", Select).value
|
|
328
|
+
except Exception:
|
|
329
|
+
target = None
|
|
330
|
+
try:
|
|
331
|
+
action = self.query_one("#gestures-action", Select).value
|
|
332
|
+
except Exception:
|
|
333
|
+
action = None
|
|
334
|
+
try:
|
|
335
|
+
keys = self.query_one("#gestures-keys", Input).value.strip().lower() if trigger == "shortcut" else ""
|
|
336
|
+
except Exception:
|
|
337
|
+
keys = ""
|
|
338
|
+
if not trigger or trigger not in GESTURE_TRIGGERS:
|
|
339
|
+
trigger = GESTURE_TRIGGERS[0]
|
|
340
|
+
if not target or target not in GESTURE_TARGETS:
|
|
341
|
+
target = GESTURE_TARGETS[0]
|
|
342
|
+
if not action or action not in GESTURE_ACTIONS:
|
|
343
|
+
action = GESTURE_ACTIONS[0]
|
|
344
|
+
# shortcut needs keys
|
|
345
|
+
if trigger == "shortcut":
|
|
346
|
+
if not keys:
|
|
347
|
+
try:
|
|
348
|
+
self.app._system_note("Enter shortcut keys (e.g. shift+enter, ctrl+s)")
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
return
|
|
352
|
+
result = add_gesture(name, trigger, target, action, keys=keys)
|
|
353
|
+
if result:
|
|
354
|
+
self.refresh_gestures()
|
|
355
|
+
try:
|
|
356
|
+
self.query_one("#gestures-name", Input).value = ""
|
|
357
|
+
self.query_one("#gestures-keys", Input).value = ""
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
try:
|
|
361
|
+
self.app._system_note(f"Added {'shortcut' if trigger=='shortcut' else 'gesture'}: {name}")
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
else:
|
|
365
|
+
try:
|
|
366
|
+
self.app._system_note("Could not add — check values (duplicate shortcut or invalid keys)")
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
except Exception as e:
|
|
370
|
+
try:
|
|
371
|
+
self.app._system_note(f"Could not add gesture: {e}")
|
|
372
|
+
except Exception:
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
def action_cancel(self):
|
|
376
|
+
self.dismiss(None)
|
|
377
|
+
|
|
378
|
+
def on_key(self, event):
|
|
379
|
+
if event.key == "escape":
|
|
380
|
+
self.dismiss(None)
|
|
381
|
+
|
|
382
|
+
else:
|
|
383
|
+
GesturesPanel = None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Goodbye screen with ASCII art — shown on sign-out."""
|
|
2
|
+
|
|
3
|
+
if __name__ == "__main__":
|
|
4
|
+
import sys
|
|
5
|
+
sys.exit(1)
|
|
6
|
+
|
|
7
|
+
TEXTUAL_AVAILABLE = True
|
|
8
|
+
try:
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual.containers import Vertical
|
|
11
|
+
from textual.widgets import Static, Button
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
except Exception:
|
|
14
|
+
TEXTUAL_AVAILABLE = False
|
|
15
|
+
|
|
16
|
+
_GOODBYE_CAT = [
|
|
17
|
+
" /\\_/\\ ",
|
|
18
|
+
" ( o.o ) ",
|
|
19
|
+
" > ^ < ",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_GOODBYE_BLOCK = [
|
|
23
|
+
"░██████╗░░█████╗░░█████╗░██████╗░ ██████╗░██╗░░░██╗███████╗",
|
|
24
|
+
"██╔════╝░██╔══██╗██╔══██╗██╔══██╗ ██╔══██╗╚██╗░██╔╝██╔════╝",
|
|
25
|
+
"██║░░██╗░██║░░██║██║░░██║██║░░██║ ██████╦╝░╚████╔╝░█████╗░░",
|
|
26
|
+
"██║░░╚██╗██║░░██║██║░░██║██║░░██║ ██╔══██╗░░╚██╔╝░░██╔══╝░░",
|
|
27
|
+
"╚██████╔╝╚█████╔╝╚█████╔╝██████╔╝ ██████╦╝░░░██║░░░███████╗",
|
|
28
|
+
"░╚═════╝░░╚════╝░░╚════╝░╚═════╝░ ╚═════╝░░░░╚═╝░░░╚══════╝",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
if TEXTUAL_AVAILABLE:
|
|
32
|
+
class GoodbyeScreen(Screen):
|
|
33
|
+
CSS = """
|
|
34
|
+
GoodbyeScreen { align: center middle; background: $app-background; }
|
|
35
|
+
#goodbye-box {
|
|
36
|
+
width: 72; height: auto; max-height: 24;
|
|
37
|
+
background: $surface; border: round $border;
|
|
38
|
+
padding: 1 2; align: center middle;
|
|
39
|
+
}
|
|
40
|
+
#goodbye-cat { color: $accent; height: auto; text-align: center; }
|
|
41
|
+
#goodbye-block { color: $text; height: auto; text-align: center; }
|
|
42
|
+
#goodbye-title { color: $accent; height: auto; text-align: center; padding-top: 1; text-style: bold; }
|
|
43
|
+
#goodbye-msg { color: $text-muted; height: auto; text-align: center; padding-top: 1; }
|
|
44
|
+
#goodbye-hint { color: $text-faint; height: 1; text-align: center; padding-top: 1; }
|
|
45
|
+
"""
|
|
46
|
+
BINDINGS = [Binding("escape", "close", "Close"), Binding("enter", "close", "Close"), Binding("q", "close", "Close")]
|
|
47
|
+
|
|
48
|
+
def __init__(self, farewell="Session saved. See you soon! \U0001F43E"):
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._farewell = farewell
|
|
51
|
+
|
|
52
|
+
def compose(self):
|
|
53
|
+
with Vertical(id="goodbye-box"):
|
|
54
|
+
# Try to use art.py if available for richer art
|
|
55
|
+
try:
|
|
56
|
+
from .. import art as _art
|
|
57
|
+
from .. import theme as _theme
|
|
58
|
+
# Pick a nice cat variant
|
|
59
|
+
cat_lines = _GOODBYE_CAT
|
|
60
|
+
except Exception:
|
|
61
|
+
cat_lines = _GOODBYE_CAT
|
|
62
|
+
yield Static("\n".join(cat_lines), id="goodbye-cat")
|
|
63
|
+
yield Static("\n".join(_GOODBYE_BLOCK), id="goodbye-block")
|
|
64
|
+
yield Static("G O O D B Y E", id="goodbye-title")
|
|
65
|
+
yield Static(self._farewell, id="goodbye-msg")
|
|
66
|
+
yield Static("Press Enter, Esc or Q to exit \u00b7 Auto-close in 3s", id="goodbye-hint")
|
|
67
|
+
|
|
68
|
+
def on_mount(self):
|
|
69
|
+
self.set_timer(3.0, self._auto_close)
|
|
70
|
+
|
|
71
|
+
def _auto_close(self):
|
|
72
|
+
try:
|
|
73
|
+
self.dismiss(None)
|
|
74
|
+
self.app.exit()
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def on_key(self, event):
|
|
79
|
+
if event.key in ("escape", "enter", "q"):
|
|
80
|
+
self.dismiss(None)
|
|
81
|
+
try:
|
|
82
|
+
self.app.exit()
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def action_close(self):
|
|
87
|
+
self.dismiss(None)
|
|
88
|
+
try:
|
|
89
|
+
self.app.exit()
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
def on_button_pressed(self, event):
|
|
94
|
+
self.dismiss(None)
|
|
95
|
+
try:
|
|
96
|
+
self.app.exit()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
else:
|
|
100
|
+
GoodbyeScreen = None
|