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,815 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT UI — ExtensionsPanel (v0.7.9.11 VS Code-style).
|
|
3
|
+
|
|
4
|
+
Full VS Code-inspired marketplace/manager for Gestures / CAT Vision /
|
|
5
|
+
Personalize and future third-party extensions.
|
|
6
|
+
|
|
7
|
+
Features per spec:
|
|
8
|
+
- Search (name, creator, category, keywords)
|
|
9
|
+
- Cards: icon, name, description, creator, verified, size, version,
|
|
10
|
+
category, trusted, stars, Install/Enable/Disable/Uninstall
|
|
11
|
+
- Details page on click
|
|
12
|
+
- States: NOT_INSTALLED, INSTALLING, INSTALLED, ENABLED, DISABLED,
|
|
13
|
+
UNINSTALLING, Error, Update Available
|
|
14
|
+
- Size from filesystem (extensions.get_size)
|
|
15
|
+
- Creator Kazi Zillani ✓ Verified, trusted, future company support
|
|
16
|
+
- Main menu integration via extensions.should_show_in_menu
|
|
17
|
+
- Persistence via extensions registry, error isolation
|
|
18
|
+
|
|
19
|
+
Design: CAT's own design system, not copied Microsoft assets.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
print("This is a library file and is not meant to be run directly.")
|
|
24
|
+
import sys
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
TEXTUAL_AVAILABLE = True
|
|
30
|
+
try:
|
|
31
|
+
from textual.screen import Screen
|
|
32
|
+
from textual.containers import Vertical, Horizontal, VerticalScroll
|
|
33
|
+
from textual.widgets import Static, Button, Input, LoadingIndicator
|
|
34
|
+
from textual.binding import Binding
|
|
35
|
+
except Exception:
|
|
36
|
+
TEXTUAL_AVAILABLE = False
|
|
37
|
+
LoadingIndicator = None
|
|
38
|
+
|
|
39
|
+
if TEXTUAL_AVAILABLE:
|
|
40
|
+
try:
|
|
41
|
+
from .. import extensions as ext_mgr
|
|
42
|
+
except Exception:
|
|
43
|
+
ext_mgr = None
|
|
44
|
+
try:
|
|
45
|
+
from . import theme_css
|
|
46
|
+
except Exception:
|
|
47
|
+
theme_css = None
|
|
48
|
+
|
|
49
|
+
def _hex(role):
|
|
50
|
+
try:
|
|
51
|
+
return theme_css.current_hex(role) if theme_css else "#888"
|
|
52
|
+
except Exception:
|
|
53
|
+
return "#888"
|
|
54
|
+
|
|
55
|
+
class _ExtCard(Horizontal):
|
|
56
|
+
"""VS Code-style card: icon | info | actions"""
|
|
57
|
+
def __init__(self, ext):
|
|
58
|
+
super().__init__(classes="ext-card")
|
|
59
|
+
self.ext = ext
|
|
60
|
+
self.ext_id = ext.get("id") or ext.get("key")
|
|
61
|
+
|
|
62
|
+
def compose(self):
|
|
63
|
+
ext = self.ext
|
|
64
|
+
name = ext.get("display_name") or ext.get("name") or ext.get("id")
|
|
65
|
+
desc = ext.get("description", "")
|
|
66
|
+
icon = ext.get("icon", "▣")
|
|
67
|
+
creator = ext.get("publisher") or ext.get("creator") or "Unknown"
|
|
68
|
+
verified = bool(ext.get("publisher_verified") or ext.get("verified"))
|
|
69
|
+
trusted = bool(ext.get("trusted"))
|
|
70
|
+
version = ext.get("version", "1.0.0")
|
|
71
|
+
category = ext.get("category", "General")
|
|
72
|
+
size = ext.get("size_display") or ext.get("size", "?")
|
|
73
|
+
state = ext.get("state") or ("INSTALLED_ENABLED" if ext.get("enabled") and ext.get("installed") else "NOT_INSTALLED" if not ext.get("installed") else "INSTALLED_DISABLED")
|
|
74
|
+
# fallback state
|
|
75
|
+
if ext.get("installed") and ext.get("enabled"):
|
|
76
|
+
state = "INSTALLED_ENABLED"
|
|
77
|
+
elif ext.get("installed") and not ext.get("enabled"):
|
|
78
|
+
state = "INSTALLED_DISABLED"
|
|
79
|
+
elif not ext.get("installed"):
|
|
80
|
+
state = "NOT_INSTALLED"
|
|
81
|
+
# check busy animation state from parent screen
|
|
82
|
+
busy = None
|
|
83
|
+
try:
|
|
84
|
+
busy = getattr(self.screen, "_busy", {}).get(self.ext_id) # type: ignore
|
|
85
|
+
except Exception:
|
|
86
|
+
busy = None
|
|
87
|
+
if busy:
|
|
88
|
+
# override state for animation — map to proper ING form
|
|
89
|
+
_busy_map = {"install": "INSTALLING", "uninstall": "UNINSTALLING", "enable": "ENABLING", "disable": "DISABLING"}
|
|
90
|
+
state = _busy_map.get(busy, "INSTALLING")
|
|
91
|
+
|
|
92
|
+
# left icon
|
|
93
|
+
with Vertical(classes="ext-card-icon"):
|
|
94
|
+
if state in ("INSTALLING", "UNINSTALLING"):
|
|
95
|
+
if LoadingIndicator:
|
|
96
|
+
yield LoadingIndicator(classes="ext-loading")
|
|
97
|
+
else:
|
|
98
|
+
yield Static("⟳", classes="ext-icon ext-busy")
|
|
99
|
+
else:
|
|
100
|
+
yield Static(icon, classes="ext-icon")
|
|
101
|
+
|
|
102
|
+
# center info
|
|
103
|
+
with Vertical(classes="ext-card-info"):
|
|
104
|
+
# title row: name + verified + trusted
|
|
105
|
+
with Horizontal(classes="ext-title-row"):
|
|
106
|
+
yield Static(name, classes="ext-name")
|
|
107
|
+
if verified:
|
|
108
|
+
yield Static("✓ Verified", classes="ext-verified")
|
|
109
|
+
if trusted:
|
|
110
|
+
yield Static("Trusted", classes="ext-trusted")
|
|
111
|
+
yield Static(desc, classes="ext-desc")
|
|
112
|
+
# meta row: creator, size, version, category
|
|
113
|
+
yield Static(f"{creator} · {size} · v{version} · {category}", classes="ext-meta")
|
|
114
|
+
# status row — with installing animation
|
|
115
|
+
status_map = {
|
|
116
|
+
"NOT_INSTALLED": ("○ Not Installed", "text-faint"),
|
|
117
|
+
"INSTALLED_ENABLED": ("● Enabled", "success"),
|
|
118
|
+
"INSTALLED_DISABLED": ("○ Disabled", "warning"),
|
|
119
|
+
"INSTALLING": ("⟳ Installing…", "warning"),
|
|
120
|
+
"UNINSTALLING": ("⟳ Uninstalling…", "warning"),
|
|
121
|
+
"ENABLING": ("⟳ Enabling…", "warning"),
|
|
122
|
+
"DISABLING": ("⟳ Disabling…", "warning"),
|
|
123
|
+
"ENABLEING": ("⟳ Enabling…", "warning"),
|
|
124
|
+
"DISABLEING": ("⟳ Disabling…", "warning"),
|
|
125
|
+
}
|
|
126
|
+
status_text, tone = status_map.get(state, ("Unknown", "text-faint"))
|
|
127
|
+
yield Static(f"[{_hex(tone)}]{status_text}[/]", classes="ext-status" + (" ext-busy-label" if busy else ""), markup=True)
|
|
128
|
+
|
|
129
|
+
# right actions — show spinner/disabled during busy
|
|
130
|
+
with Vertical(classes="ext-card-actions"):
|
|
131
|
+
if busy:
|
|
132
|
+
if busy == "install":
|
|
133
|
+
yield Button("⟳ Installing…", id=f"ext-busy-{self.ext_id}", variant="primary", classes="cct-btn-sm", disabled=True)
|
|
134
|
+
elif busy == "uninstall":
|
|
135
|
+
yield Button("⟳ Removing…", id=f"ext-busy-{self.ext_id}", variant="error", classes="cct-btn-sm", disabled=True)
|
|
136
|
+
elif busy == "enable":
|
|
137
|
+
yield Button("⟳ Enabling…", id=f"ext-busy-{self.ext_id}", variant="primary", classes="cct-btn-sm", disabled=True)
|
|
138
|
+
elif busy == "disable":
|
|
139
|
+
yield Button("⟳ Disabling…", id=f"ext-busy-{self.ext_id}", classes="cct-btn-sm", disabled=True)
|
|
140
|
+
else:
|
|
141
|
+
yield Button("⟳ Working…", id=f"ext-busy-{self.ext_id}", disabled=True, classes="cct-btn-sm")
|
|
142
|
+
else:
|
|
143
|
+
if state == "NOT_INSTALLED":
|
|
144
|
+
yield Button("Install", id=f"ext-install-{self.ext_id}", variant="primary", classes="cct-btn-sm")
|
|
145
|
+
elif state == "INSTALLED_DISABLED":
|
|
146
|
+
yield Button("Enable", id=f"ext-enable-{self.ext_id}", variant="primary", classes="cct-btn-sm")
|
|
147
|
+
yield Button("Uninstall", id=f"ext-uninstall-{self.ext_id}", variant="error", classes="cct-btn-sm")
|
|
148
|
+
elif state == "INSTALLED_ENABLED":
|
|
149
|
+
yield Button("Disable", id=f"ext-disable-{self.ext_id}", classes="cct-btn-sm")
|
|
150
|
+
yield Button("Uninstall", id=f"ext-uninstall-{self.ext_id}", variant="error", classes="cct-btn-sm")
|
|
151
|
+
else:
|
|
152
|
+
yield Button("Install", id=f"ext-install-{self.ext_id}", variant="primary", classes="cct-btn-sm")
|
|
153
|
+
|
|
154
|
+
def on_click(self, event):
|
|
155
|
+
# clicking the card itself opens details
|
|
156
|
+
# but buttons handle their own clicks, so only handle card background
|
|
157
|
+
if getattr(event, "widget", None) is self:
|
|
158
|
+
try:
|
|
159
|
+
self.screen._open_details(self.ext_id)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
class _DetailsPanel(Screen):
|
|
164
|
+
"""Extension details page (VS Code style)."""
|
|
165
|
+
CSS = """
|
|
166
|
+
_DetailsPanel { align: center middle; background: $surface-darken-2 70%; }
|
|
167
|
+
#det-box { width: 72; max-width: 96%; height: auto; max-height: 46; background: $surface; border: tall $border; padding: 0; layout: vertical; overflow: hidden; }
|
|
168
|
+
#det-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; layout: horizontal; }
|
|
169
|
+
#det-title { width: 1fr; text-style: bold; }
|
|
170
|
+
#det-body { height: 1fr; overflow-y: auto; padding: 1 2; }
|
|
171
|
+
.det-icon { width: 100%; height: 3; text-align: center; text-style: bold; }
|
|
172
|
+
.det-name { width: 100%; height: 1; text-style: bold; text-align: center; }
|
|
173
|
+
.det-creator { width: 100%; height: 1; color: $text-muted; text-align: center; }
|
|
174
|
+
.det-verified { width: 100%; height: 1; color: $success; text-align: center; }
|
|
175
|
+
.det-meta { width: 100%; height: auto; color: $text-faint; text-align: center; padding: 1 0; }
|
|
176
|
+
.det-desc { width: 100%; height: auto; color: $text; padding: 1 0; }
|
|
177
|
+
.det-section { width: 100%; height: 1; color: $text-faint; text-style: bold; padding-top: 1; border-top: solid $border; margin-top: 1; }
|
|
178
|
+
#det-actions { height: auto; padding: 1 2; border-top: solid $border; layout: horizontal; }
|
|
179
|
+
#det-actions Button { margin-right: 1; }
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, ext_id):
|
|
183
|
+
super().__init__()
|
|
184
|
+
self.ext_id = ext_id
|
|
185
|
+
self.ext = ext_mgr.get_metadata(ext_id) if ext_mgr else None
|
|
186
|
+
self._busy = None
|
|
187
|
+
|
|
188
|
+
def compose(self):
|
|
189
|
+
ext = self.ext or {}
|
|
190
|
+
name = ext.get("display_name") or ext.get("name") or self.ext_id
|
|
191
|
+
icon = ext.get("icon", "▣")
|
|
192
|
+
desc = ext.get("description", "")
|
|
193
|
+
creator = ext.get("publisher") or ext.get("creator") or "Unknown"
|
|
194
|
+
verified = bool(ext.get("publisher_verified") or ext.get("verified"))
|
|
195
|
+
trusted = bool(ext.get("trusted"))
|
|
196
|
+
version = ext.get("version", "1.0.0")
|
|
197
|
+
size = ext.get("size_display") or ext.get("size", "?")
|
|
198
|
+
category = ext.get("category", "General")
|
|
199
|
+
state = ext.get("state") or "NOT_INSTALLED"
|
|
200
|
+
# show busy animation if pending
|
|
201
|
+
if self._busy:
|
|
202
|
+
state = {"install":"INSTALLING","uninstall":"UNINSTALLING","enable":"ENABLING","disable":"DISABLING"}.get(self._busy, state)
|
|
203
|
+
with Vertical(id="det-box"):
|
|
204
|
+
with Horizontal(id="det-titlebar"):
|
|
205
|
+
yield Static(f"{icon} {name}", id="det-title")
|
|
206
|
+
yield Button("✕", id="det-close", classes="cct-ctrl")
|
|
207
|
+
with VerticalScroll(id="det-body"):
|
|
208
|
+
if state in ("INSTALLING", "UNINSTALLING") and LoadingIndicator:
|
|
209
|
+
yield LoadingIndicator(classes="det-loading")
|
|
210
|
+
yield Static(icon, classes="det-icon")
|
|
211
|
+
yield Static(name, classes="det-name")
|
|
212
|
+
yield Static(creator, classes="det-creator")
|
|
213
|
+
if verified:
|
|
214
|
+
yield Static("✓ Verified", classes="det-verified")
|
|
215
|
+
else:
|
|
216
|
+
yield Static("Unverified", classes="det-verified")
|
|
217
|
+
if trusted:
|
|
218
|
+
yield Static("Trusted Extension", classes="det-verified")
|
|
219
|
+
if state == "INSTALLING":
|
|
220
|
+
yield Static("⟳ Installing…", classes="det-verified")
|
|
221
|
+
elif state == "UNINSTALLING":
|
|
222
|
+
yield Static("⟳ Uninstalling…", classes="det-verified")
|
|
223
|
+
yield Static(f"{size} · v{version} · {category}", classes="det-meta")
|
|
224
|
+
yield Static(desc, classes="det-desc")
|
|
225
|
+
yield Static("Publisher", classes="det-section")
|
|
226
|
+
pub_type = ext.get("publisher_type", "individual")
|
|
227
|
+
yield Static(f"{creator} ({pub_type}) {'✓ Verified' if verified else ''}", classes="det-desc")
|
|
228
|
+
yield Static("Permissions", classes="det-section")
|
|
229
|
+
yield Static("filesystem, workspace access", classes="det-desc")
|
|
230
|
+
with Horizontal(id="det-actions"):
|
|
231
|
+
if self._busy:
|
|
232
|
+
if self._busy == "install":
|
|
233
|
+
yield Button("⟳ Installing…", id="det-install", variant="primary", disabled=True)
|
|
234
|
+
elif self._busy == "uninstall":
|
|
235
|
+
yield Button("⟳ Removing…", id="det-uninstall", variant="error", disabled=True)
|
|
236
|
+
elif self._busy == "enable":
|
|
237
|
+
yield Button("⟳ Enabling…", id="det-enable", variant="primary", disabled=True)
|
|
238
|
+
elif self._busy == "disable":
|
|
239
|
+
yield Button("⟳ Disabling…", id="det-disable", disabled=True)
|
|
240
|
+
yield Button("Close", id="det-close2", classes="cct-ctrl", disabled=True)
|
|
241
|
+
else:
|
|
242
|
+
if state == "NOT_INSTALLED":
|
|
243
|
+
yield Button("Install", id="det-install", variant="primary")
|
|
244
|
+
elif state == "INSTALLED_DISABLED":
|
|
245
|
+
yield Button("Enable", id="det-enable", variant="primary")
|
|
246
|
+
yield Button("Uninstall", id="det-uninstall", variant="error")
|
|
247
|
+
elif state == "INSTALLED_ENABLED":
|
|
248
|
+
yield Button("Disable", id="det-disable")
|
|
249
|
+
yield Button("Uninstall", id="det-uninstall", variant="error")
|
|
250
|
+
yield Button("Close", id="det-close2", classes="cct-ctrl")
|
|
251
|
+
|
|
252
|
+
def _det_refresh_busy(self):
|
|
253
|
+
try:
|
|
254
|
+
# re-compose body actions to show busy
|
|
255
|
+
box = self.query_one("#det-box")
|
|
256
|
+
# simple refresh by re-mounting actions row
|
|
257
|
+
actions = self.query_one("#det-actions")
|
|
258
|
+
actions.remove_children()
|
|
259
|
+
if self._busy:
|
|
260
|
+
if self._busy == "install":
|
|
261
|
+
actions.mount(Button("⟳ Installing…", id="det-install", variant="primary", disabled=True))
|
|
262
|
+
elif self._busy == "uninstall":
|
|
263
|
+
actions.mount(Button("⟳ Removing…", id="det-uninstall", variant="error", disabled=True))
|
|
264
|
+
elif self._busy == "enable":
|
|
265
|
+
actions.mount(Button("⟳ Enabling…", id="det-enable", variant="primary", disabled=True))
|
|
266
|
+
elif self._busy == "disable":
|
|
267
|
+
actions.mount(Button("⟳ Disabling…", id="det-disable", disabled=True))
|
|
268
|
+
actions.mount(Button("Close", id="det-close2", classes="cct-ctrl", disabled=True))
|
|
269
|
+
else:
|
|
270
|
+
ext = ext_mgr.get_metadata(self.ext_id) if ext_mgr else {}
|
|
271
|
+
state = (ext or {}).get("state") or "NOT_INSTALLED"
|
|
272
|
+
if state == "NOT_INSTALLED":
|
|
273
|
+
actions.mount(Button("Install", id="det-install", variant="primary"))
|
|
274
|
+
elif state == "INSTALLED_DISABLED":
|
|
275
|
+
actions.mount(Button("Enable", id="det-enable", variant="primary"))
|
|
276
|
+
actions.mount(Button("Uninstall", id="det-uninstall", variant="error"))
|
|
277
|
+
elif state == "INSTALLED_ENABLED":
|
|
278
|
+
actions.mount(Button("Disable", id="det-disable"))
|
|
279
|
+
actions.mount(Button("Uninstall", id="det-uninstall", variant="error"))
|
|
280
|
+
actions.mount(Button("Close", id="det-close2", classes="cct-ctrl"))
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
def _finish_det(self, action: str):
|
|
285
|
+
try:
|
|
286
|
+
if action == "install" and ext_mgr:
|
|
287
|
+
ext_mgr.install(self.ext_id)
|
|
288
|
+
self.app._system_note(f"✓ {self.ext_id} installed")
|
|
289
|
+
self.dismiss("installed")
|
|
290
|
+
elif action == "enable" and ext_mgr:
|
|
291
|
+
ext_mgr.enable(self.ext_id)
|
|
292
|
+
self.app._system_note(f"✓ {self.ext_id} enabled")
|
|
293
|
+
self.dismiss("enabled")
|
|
294
|
+
elif action == "disable" and ext_mgr:
|
|
295
|
+
ext_mgr.disable(self.ext_id)
|
|
296
|
+
self.app._system_note(f"✓ {self.ext_id} disabled")
|
|
297
|
+
self.dismiss("disabled")
|
|
298
|
+
elif action == "uninstall" and ext_mgr:
|
|
299
|
+
ext_mgr.uninstall(self.ext_id)
|
|
300
|
+
self.app._system_note(f"✓ {self.ext_id} uninstalled")
|
|
301
|
+
self.dismiss("uninstalled")
|
|
302
|
+
except Exception:
|
|
303
|
+
try:
|
|
304
|
+
self.dismiss(None)
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
def on_button_pressed(self, event):
|
|
309
|
+
bid = event.button.id
|
|
310
|
+
if bid in ("det-close", "det-close2"):
|
|
311
|
+
self.dismiss(None)
|
|
312
|
+
elif bid == "det-install":
|
|
313
|
+
self._busy = "install"
|
|
314
|
+
try:
|
|
315
|
+
event.button.disabled = True
|
|
316
|
+
event.button.label = "⟳ Installing…"
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
self._det_refresh_busy()
|
|
320
|
+
try:
|
|
321
|
+
self.set_timer(0.85, lambda: self._finish_det("install"))
|
|
322
|
+
except Exception:
|
|
323
|
+
self._finish_det("install")
|
|
324
|
+
elif bid == "det-enable":
|
|
325
|
+
self._busy = "enable"
|
|
326
|
+
try:
|
|
327
|
+
event.button.disabled = True
|
|
328
|
+
event.button.label = "⟳ Enabling…"
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
self._det_refresh_busy()
|
|
332
|
+
try:
|
|
333
|
+
self.set_timer(0.6, lambda: self._finish_det("enable"))
|
|
334
|
+
except Exception:
|
|
335
|
+
self._finish_det("enable")
|
|
336
|
+
elif bid == "det-disable":
|
|
337
|
+
self._busy = "disable"
|
|
338
|
+
try:
|
|
339
|
+
event.button.disabled = True
|
|
340
|
+
event.button.label = "⟳ Disabling…"
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
self._det_refresh_busy()
|
|
344
|
+
try:
|
|
345
|
+
self.set_timer(0.6, lambda: self._finish_det("disable"))
|
|
346
|
+
except Exception:
|
|
347
|
+
self._finish_det("disable")
|
|
348
|
+
elif bid == "det-uninstall":
|
|
349
|
+
self._busy = "uninstall"
|
|
350
|
+
try:
|
|
351
|
+
event.button.disabled = True
|
|
352
|
+
event.button.label = "⟳ Removing…"
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
self._det_refresh_busy()
|
|
356
|
+
try:
|
|
357
|
+
self.set_timer(0.85, lambda: self._finish_det("uninstall"))
|
|
358
|
+
except Exception:
|
|
359
|
+
self._finish_det("uninstall")
|
|
360
|
+
|
|
361
|
+
def on_key(self, event):
|
|
362
|
+
if event.key == "escape":
|
|
363
|
+
self.dismiss(None)
|
|
364
|
+
|
|
365
|
+
class _CreateExtensionModal(Screen):
|
|
366
|
+
"""Modal dialog to create and add a custom extension."""
|
|
367
|
+
CSS = """
|
|
368
|
+
_CreateExtensionModal { align: center middle; background: rgba(0, 0, 0, 0.7); }
|
|
369
|
+
#create-ext-box {
|
|
370
|
+
width: 72; max-width: 96%; height: auto; max-height: 44;
|
|
371
|
+
background: $surface; border: tall $border; padding: 0;
|
|
372
|
+
layout: vertical; overflow: hidden;
|
|
373
|
+
}
|
|
374
|
+
#create-ext-titlebar { height: 3; padding: 1 2 0 2; border-bottom: solid $border; layout: horizontal; align: center middle; }
|
|
375
|
+
#create-ext-title { width: 1fr; text-style: bold; color: $text; }
|
|
376
|
+
#create-ext-body { height: auto; padding: 1 2; layout: vertical; }
|
|
377
|
+
.create-field-label { width: 100%; height: 1; color: $text-muted; margin-top: 1; }
|
|
378
|
+
.create-field-label:first-child { margin-top: 0; }
|
|
379
|
+
#create-ext-body Input { width: 100%; height: 3; border: tall $border; background: $surface; }
|
|
380
|
+
#create-ext-body Input:focus { border: tall $accent; }
|
|
381
|
+
#create-ext-row { height: auto; layout: horizontal; }
|
|
382
|
+
#create-ext-row > Vertical { width: 1fr; margin-right: 1; }
|
|
383
|
+
#create-ext-row > Vertical:last-child { margin-right: 0; }
|
|
384
|
+
#create-ext-error { width: 100%; height: 1; color: $error; display: none; margin-top: 1; }
|
|
385
|
+
#create-ext-error.show { display: block; }
|
|
386
|
+
#create-ext-actions { height: 4; min-height: 4; padding: 0 2; border-top: solid $border; background: $surface; layout: horizontal; align: right middle; }
|
|
387
|
+
#create-ext-actions Button { margin-left: 1; min-width: 12; height: 3; }
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def compose(self):
|
|
391
|
+
with Vertical(id="create-ext-box"):
|
|
392
|
+
with Horizontal(id="create-ext-titlebar"):
|
|
393
|
+
yield Static("⚡ Create Custom Extension", id="create-ext-title")
|
|
394
|
+
yield Button("✕", id="create-close", classes="cct-ctrl")
|
|
395
|
+
with Vertical(id="create-ext-body"):
|
|
396
|
+
yield Static("Extension Name *", classes="create-field-label")
|
|
397
|
+
yield Input(placeholder="e.g. Code Formatter Pro", id="input-ext-name")
|
|
398
|
+
with Horizontal(id="create-ext-row"):
|
|
399
|
+
with Vertical():
|
|
400
|
+
yield Static("Extension ID (slug) *", classes="create-field-label")
|
|
401
|
+
yield Input(placeholder="e.g. code_formatter", id="input-ext-id")
|
|
402
|
+
with Vertical():
|
|
403
|
+
yield Static("Icon (emoji/symbol)", classes="create-field-label")
|
|
404
|
+
yield Input(value="⚡", placeholder="⚡", id="input-ext-icon")
|
|
405
|
+
yield Static("Description", classes="create-field-label")
|
|
406
|
+
yield Input(placeholder="e.g. Formats code with custom rules", id="input-ext-desc")
|
|
407
|
+
yield Static("Category", classes="create-field-label")
|
|
408
|
+
yield Input(value="Productivity", placeholder="Category", id="input-ext-cat")
|
|
409
|
+
yield Static("", id="create-ext-error")
|
|
410
|
+
with Horizontal(id="create-ext-actions"):
|
|
411
|
+
yield Button("Cancel", id="create-cancel")
|
|
412
|
+
yield Button("Create & Add", id="create-submit", variant="primary")
|
|
413
|
+
|
|
414
|
+
def on_button_pressed(self, event):
|
|
415
|
+
bid = event.button.id or ""
|
|
416
|
+
if bid in ("create-close", "create-cancel"):
|
|
417
|
+
self.dismiss(None)
|
|
418
|
+
return
|
|
419
|
+
if bid == "create-submit":
|
|
420
|
+
name = self.query_one("#input-ext-name", Input).value.strip()
|
|
421
|
+
raw_id = self.query_one("#input-ext-id", Input).value.strip()
|
|
422
|
+
desc = self.query_one("#input-ext-desc", Input).value.strip()
|
|
423
|
+
icon = self.query_one("#input-ext-icon", Input).value.strip() or "⚡"
|
|
424
|
+
cat = self.query_one("#input-ext-cat", Input).value.strip() or "Productivity"
|
|
425
|
+
err = self.query_one("#create-ext-error", Static)
|
|
426
|
+
|
|
427
|
+
if not name:
|
|
428
|
+
err.update("Please enter an extension name.")
|
|
429
|
+
err.add_class("show")
|
|
430
|
+
return
|
|
431
|
+
ext_id = raw_id.lower().replace("-", "_").replace(" ", "_") if raw_id else name.lower().replace("-", "_").replace(" ", "_")
|
|
432
|
+
import re
|
|
433
|
+
ext_id = re.sub(r"[^a-z0-9_]", "", ext_id)
|
|
434
|
+
if not ext_id:
|
|
435
|
+
err.update("Please enter a valid extension ID.")
|
|
436
|
+
err.add_class("show")
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
if ext_mgr:
|
|
440
|
+
ok, res = ext_mgr.create_extension(
|
|
441
|
+
extension_id=ext_id,
|
|
442
|
+
name=name,
|
|
443
|
+
description=desc,
|
|
444
|
+
icon=icon,
|
|
445
|
+
publisher="You (Custom)",
|
|
446
|
+
category=cat,
|
|
447
|
+
version="1.0.0"
|
|
448
|
+
)
|
|
449
|
+
if ok:
|
|
450
|
+
self.dismiss(("created", ext_id, name))
|
|
451
|
+
else:
|
|
452
|
+
err_msg = res if isinstance(res, str) else (res or {}).get("message", "Failed to create extension.")
|
|
453
|
+
err.update(err_msg)
|
|
454
|
+
err.add_class("show")
|
|
455
|
+
else:
|
|
456
|
+
self.dismiss(None)
|
|
457
|
+
|
|
458
|
+
def on_key(self, event):
|
|
459
|
+
if event.key == "escape":
|
|
460
|
+
self.dismiss(None)
|
|
461
|
+
|
|
462
|
+
class ExtensionsPanel(Screen):
|
|
463
|
+
"""VS Code-style Extensions marketplace — professional, minimal, responsive."""
|
|
464
|
+
|
|
465
|
+
CSS = """
|
|
466
|
+
ExtensionsPanel { align: center middle; background: $surface-darken-2 60%; }
|
|
467
|
+
#ext-box {
|
|
468
|
+
width: 84; max-width: 96%; height: auto; max-height: 75%;
|
|
469
|
+
background: $surface; border: tall $border; padding: 0;
|
|
470
|
+
layout: vertical; overflow: hidden;
|
|
471
|
+
}
|
|
472
|
+
#ext-titlebar { height: 3; min-height: 3; padding: 1 2 0 2; border-bottom: solid $border; layout: horizontal; align: center middle; }
|
|
473
|
+
#ext-title { width: 1fr; text-style: bold; color: $text; }
|
|
474
|
+
#ext-search { height: 3; padding: 0 2; margin: 0; }
|
|
475
|
+
#ext-search Input { width: 1fr; height: 3; border: tall $border; background: $surface; }
|
|
476
|
+
#ext-search Input:focus { border: tall $accent; }
|
|
477
|
+
#ext-subtitle { color: $text-muted; height: 1; padding: 0 2; display: none; }
|
|
478
|
+
#ext-list { height: 1fr; min-height: 4; max-height: 62%; overflow-y: auto; scrollbar-gutter: stable; scrollbar-size: 1 1; scrollbar-color: $border $surface; scrollbar-color-hover: $accent $surface; margin: 0 1; padding: 1 0 0 0; }
|
|
479
|
+
.ext-card { height: auto; min-height: 6; padding: 1 2; border: tall $border; background: $surface; margin: 0 0 1 0; layout: horizontal; overflow: hidden; }
|
|
480
|
+
.ext-card:hover { border: tall $accent; }
|
|
481
|
+
.ext-card.ext-busy { background: $surface; border: tall $accent; }
|
|
482
|
+
.ext-card-icon { width: 6; height: auto; text-align: center; text-style: bold; content-align: center middle; background: $accent 8%; border: tall $border; margin-right: 1; padding: 1 0; }
|
|
483
|
+
.ext-icon { width: 100%; height: 1; text-align: center; content-align: center middle; }
|
|
484
|
+
.ext-loading { width: 6; height: 3; color: $accent; content-align: center middle; }
|
|
485
|
+
.ext-card-info { width: 1fr; height: auto; min-width: 16; }
|
|
486
|
+
.ext-title-row { height: 1; layout: horizontal; }
|
|
487
|
+
.ext-name { width: auto; height: 1; text-style: bold; margin-right: 1; color: $text; }
|
|
488
|
+
.ext-verified { width: auto; height: 1; color: $success; }
|
|
489
|
+
.ext-trusted { width: auto; height: 1; color: $success; margin-left: 1; }
|
|
490
|
+
.ext-desc { width: 100%; height: auto; color: $text-muted; max-height: 2; overflow: hidden; }
|
|
491
|
+
.ext-meta { width: 100%; height: 1; color: $text-muted; }
|
|
492
|
+
.ext-status { width: 100%; height: 1; }
|
|
493
|
+
.ext-busy { color: $accent; }
|
|
494
|
+
.ext-busy-label { color: $warning; text-style: bold; }
|
|
495
|
+
.ext-card-actions { width: auto; min-width: 28; height: auto; layout: horizontal; align: center middle; }
|
|
496
|
+
.ext-card-actions Button { min-width: 12; height: 3; min-height: 3; padding: 0 1; margin-left: 1; }
|
|
497
|
+
.ext-card-actions Button:first-child { margin-left: 0; }
|
|
498
|
+
.ext-card-actions Button:disabled { opacity: 0.7; }
|
|
499
|
+
#ext-actions { height: 4; min-height: 4; padding: 0 2; border-top: solid $border; background: $surface; layout: horizontal; align: center middle; overflow: hidden; }
|
|
500
|
+
#ext-actions-left { width: 1fr; height: auto; layout: horizontal; align: center middle; }
|
|
501
|
+
#ext-actions-left Button { margin-right: 1; min-width: 11; height: 3; min-height: 3; padding: 0 1; }
|
|
502
|
+
#ext-actions Button { min-width: 9; height: 3; min-height: 3; padding: 0 1; }
|
|
503
|
+
#ext-hint { display: none; }
|
|
504
|
+
#ext-error { color: $error; background: $surface; height: 1; padding: 0 2; display: none; }
|
|
505
|
+
#ext-error.show { display: block; }
|
|
506
|
+
.cct-compact #ext-box { width: 96; max-height: 88%; }
|
|
507
|
+
.cct-compact #ext-titlebar { height: 2; min-height: 2; }
|
|
508
|
+
.cct-compact #ext-list { min-height: 6; }
|
|
509
|
+
.cct-compact .ext-card { layout: vertical; min-height: 7; }
|
|
510
|
+
.cct-compact .ext-card-info { width: 100%; }
|
|
511
|
+
.cct-compact .ext-card-actions { width: 100%; layout: horizontal; align: center middle; margin-top: 1; }
|
|
512
|
+
.cct-compact .ext-card-actions Button { width: 1fr; min-width: 10; padding: 0 1; margin-top: 0; margin-right: 1; margin-left: 0; }
|
|
513
|
+
.cct-compact .ext-card-actions Button:last-child { margin-right: 0; }
|
|
514
|
+
.cct-compact #ext-actions { padding: 0 1; }
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
BINDINGS = [Binding("escape", "cancel", "Cancel")]
|
|
518
|
+
|
|
519
|
+
def __init__(self):
|
|
520
|
+
super().__init__()
|
|
521
|
+
self._busy: dict = {}
|
|
522
|
+
self._spinner_index: int = 0
|
|
523
|
+
self._search_query: str = ""
|
|
524
|
+
self._busy_timer = None
|
|
525
|
+
|
|
526
|
+
def compose(self):
|
|
527
|
+
with Vertical(id="ext-box"):
|
|
528
|
+
with Horizontal(id="ext-titlebar"):
|
|
529
|
+
yield Static("▣ Extensions", id="ext-title")
|
|
530
|
+
yield Button("✕", id="ext-close", classes="cct-ctrl")
|
|
531
|
+
with Vertical(id="ext-search"):
|
|
532
|
+
yield Input(placeholder="Search extensions...", id="ext-search-input")
|
|
533
|
+
yield Static("Install to enable. Disable hides from menu.", id="ext-subtitle")
|
|
534
|
+
yield Static("", id="ext-error")
|
|
535
|
+
with VerticalScroll(id="ext-list"):
|
|
536
|
+
for ext in self._filtered():
|
|
537
|
+
yield _ExtCard(ext)
|
|
538
|
+
with Horizontal(id="ext-actions"):
|
|
539
|
+
with Horizontal(id="ext-actions-left"):
|
|
540
|
+
yield Button("+ Create Extension", id="ext-create-btn", classes="cct-btn-sm")
|
|
541
|
+
yield Button("Enable All", id="ext-enable-all", classes="cct-btn-sm")
|
|
542
|
+
yield Button("Disable All", id="ext-disable-all", classes="cct-btn-sm")
|
|
543
|
+
yield Button("Close", id="ext-close2", variant="primary", classes="cct-btn-sm")
|
|
544
|
+
|
|
545
|
+
def on_mount(self):
|
|
546
|
+
try:
|
|
547
|
+
self.query_one("#ext-box").add_class("open")
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
if self.size.width < 86:
|
|
551
|
+
try:
|
|
552
|
+
self.query_one("#ext-box").add_class("cct-compact")
|
|
553
|
+
except Exception:
|
|
554
|
+
pass
|
|
555
|
+
try:
|
|
556
|
+
self.query_one("#ext-search-input", Input).focus()
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
# start spinner tick for install animations (tracked so
|
|
560
|
+
# re-opening the panel cannot stack orphaned tickers).
|
|
561
|
+
try:
|
|
562
|
+
from . import design_system
|
|
563
|
+
design_system.stop_timer(getattr(self, "_spin_timer", None))
|
|
564
|
+
self._spin_timer = self.set_interval(0.12, self._tick_spinner)
|
|
565
|
+
except Exception:
|
|
566
|
+
try:
|
|
567
|
+
self._spin_timer = self.set_interval(0.12, self._tick_spinner)
|
|
568
|
+
except Exception:
|
|
569
|
+
self._spin_timer = None
|
|
570
|
+
|
|
571
|
+
def on_unmount(self):
|
|
572
|
+
try:
|
|
573
|
+
from . import design_system
|
|
574
|
+
design_system.stop_timer(getattr(self, "_spin_timer", None))
|
|
575
|
+
except Exception:
|
|
576
|
+
pass
|
|
577
|
+
self._spin_timer = None
|
|
578
|
+
|
|
579
|
+
def _filtered(self):
|
|
580
|
+
try:
|
|
581
|
+
query = ""
|
|
582
|
+
try:
|
|
583
|
+
query = self.query_one("#ext-search-input", Input).value.strip().lower()
|
|
584
|
+
except Exception:
|
|
585
|
+
query = getattr(self, "_search_query", "") or ""
|
|
586
|
+
all_exts = ext_mgr.list_extensions() if ext_mgr else []
|
|
587
|
+
if not query:
|
|
588
|
+
return all_exts
|
|
589
|
+
out = []
|
|
590
|
+
for ext in all_exts:
|
|
591
|
+
hay = " ".join([
|
|
592
|
+
str(ext.get("name", "")),
|
|
593
|
+
str(ext.get("display_name", "")),
|
|
594
|
+
str(ext.get("publisher", "")),
|
|
595
|
+
str(ext.get("creator", "")),
|
|
596
|
+
str(ext.get("category", "")),
|
|
597
|
+
str(ext.get("description", "")),
|
|
598
|
+
" ".join(ext.get("keywords", [])),
|
|
599
|
+
]).lower()
|
|
600
|
+
if query in hay:
|
|
601
|
+
out.append(ext)
|
|
602
|
+
return out
|
|
603
|
+
except Exception:
|
|
604
|
+
return ext_mgr.list_extensions() if ext_mgr else []
|
|
605
|
+
|
|
606
|
+
def _refresh(self, msg=None):
|
|
607
|
+
# preserve scroll to avoid glitch/jump during install animation
|
|
608
|
+
_saved_y = 0
|
|
609
|
+
try:
|
|
610
|
+
lst = self.query_one("#ext-list", VerticalScroll)
|
|
611
|
+
try:
|
|
612
|
+
_saved_y = lst.scroll_y
|
|
613
|
+
except Exception:
|
|
614
|
+
_saved_y = 0
|
|
615
|
+
lst.remove_children()
|
|
616
|
+
for ext in self._filtered():
|
|
617
|
+
lst.mount(_ExtCard(ext))
|
|
618
|
+
# restore scroll without animation
|
|
619
|
+
try:
|
|
620
|
+
lst.scroll_y = _saved_y
|
|
621
|
+
except Exception:
|
|
622
|
+
pass
|
|
623
|
+
except Exception:
|
|
624
|
+
pass
|
|
625
|
+
if msg:
|
|
626
|
+
try:
|
|
627
|
+
self.app._system_note(msg)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
def _show_error(self, msg=""):
|
|
632
|
+
try:
|
|
633
|
+
lbl = self.query_one("#ext-error", Static)
|
|
634
|
+
if msg:
|
|
635
|
+
lbl.update(msg)
|
|
636
|
+
lbl.add_class("show")
|
|
637
|
+
else:
|
|
638
|
+
lbl.remove_class("show")
|
|
639
|
+
except Exception:
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
def _open_details(self, ext_id):
|
|
643
|
+
try:
|
|
644
|
+
self.app.push_screen(_DetailsPanel(ext_id), lambda res: self._refresh())
|
|
645
|
+
except Exception as e:
|
|
646
|
+
self._show_error(str(e))
|
|
647
|
+
|
|
648
|
+
# ---- install animations (LoadingIndicator handles spin, no full refresh to avoid glitch) ----
|
|
649
|
+
def _tick_spinner(self):
|
|
650
|
+
# kept for compatibility — LoadingIndicator animates automatically, no list refresh to prevent flicker
|
|
651
|
+
if not getattr(self, "_busy", None) or not self._busy:
|
|
652
|
+
return
|
|
653
|
+
self._spinner_index = (self._spinner_index + 1) % 4
|
|
654
|
+
# no _refresh() here — would cause scroll jump/glitch; status is static "Installing…" and LoadingIndicator spins on its own
|
|
655
|
+
|
|
656
|
+
def _start_busy(self, ext_id: str, action: str):
|
|
657
|
+
self._busy[ext_id] = action
|
|
658
|
+
# ensure spinner loop is running
|
|
659
|
+
self._refresh()
|
|
660
|
+
# add busy class to card for CSS highlight
|
|
661
|
+
try:
|
|
662
|
+
for card in self.query("#ext-list _ExtCard"):
|
|
663
|
+
if getattr(card, "ext_id", None) == ext_id:
|
|
664
|
+
card.add_class("ext-busy")
|
|
665
|
+
except Exception:
|
|
666
|
+
pass
|
|
667
|
+
|
|
668
|
+
def _finish_busy(self, ext_id: str):
|
|
669
|
+
try:
|
|
670
|
+
self._busy.pop(ext_id, None)
|
|
671
|
+
except Exception:
|
|
672
|
+
pass
|
|
673
|
+
try:
|
|
674
|
+
self._refresh()
|
|
675
|
+
except Exception:
|
|
676
|
+
pass
|
|
677
|
+
|
|
678
|
+
def _do_install(self, ext_id: str):
|
|
679
|
+
try:
|
|
680
|
+
if ext_mgr:
|
|
681
|
+
ok, msg = ext_mgr.install(ext_id)
|
|
682
|
+
self._finish_busy(ext_id)
|
|
683
|
+
self._refresh(msg)
|
|
684
|
+
if not ok:
|
|
685
|
+
self._show_error(msg)
|
|
686
|
+
else:
|
|
687
|
+
self._show_error("")
|
|
688
|
+
except Exception as e:
|
|
689
|
+
self._finish_busy(ext_id)
|
|
690
|
+
self._show_error(str(e))
|
|
691
|
+
|
|
692
|
+
def _do_uninstall(self, ext_id: str):
|
|
693
|
+
try:
|
|
694
|
+
if ext_mgr:
|
|
695
|
+
ok, msg = ext_mgr.uninstall(ext_id)
|
|
696
|
+
self._finish_busy(ext_id)
|
|
697
|
+
self._refresh(msg)
|
|
698
|
+
if not ok:
|
|
699
|
+
self._show_error(msg)
|
|
700
|
+
except Exception as e:
|
|
701
|
+
self._finish_busy(ext_id)
|
|
702
|
+
self._show_error(str(e))
|
|
703
|
+
|
|
704
|
+
def _do_enable(self, ext_id: str):
|
|
705
|
+
try:
|
|
706
|
+
if ext_mgr:
|
|
707
|
+
ok, msg = ext_mgr.enable(ext_id)
|
|
708
|
+
self._finish_busy(ext_id)
|
|
709
|
+
self._refresh(msg)
|
|
710
|
+
if not ok:
|
|
711
|
+
self._show_error(msg)
|
|
712
|
+
except Exception as e:
|
|
713
|
+
self._finish_busy(ext_id)
|
|
714
|
+
self._show_error(str(e))
|
|
715
|
+
|
|
716
|
+
def _do_disable(self, ext_id: str):
|
|
717
|
+
try:
|
|
718
|
+
if ext_mgr:
|
|
719
|
+
ok, msg = ext_mgr.disable(ext_id)
|
|
720
|
+
self._finish_busy(ext_id)
|
|
721
|
+
self._refresh(msg)
|
|
722
|
+
if not ok:
|
|
723
|
+
self._show_error(msg)
|
|
724
|
+
except Exception as e:
|
|
725
|
+
self._finish_busy(ext_id)
|
|
726
|
+
self._show_error(str(e))
|
|
727
|
+
|
|
728
|
+
def on_input_changed(self, event):
|
|
729
|
+
if getattr(event.input, "id", "") == "ext-search-input":
|
|
730
|
+
self._search_query = event.value
|
|
731
|
+
self._refresh()
|
|
732
|
+
|
|
733
|
+
def on_click(self, event):
|
|
734
|
+
widget = getattr(event, "widget", None)
|
|
735
|
+
if isinstance(widget, _ExtCard):
|
|
736
|
+
self._open_details(widget.ext_id)
|
|
737
|
+
|
|
738
|
+
def on_button_pressed(self, event):
|
|
739
|
+
bid = event.button.id or ""
|
|
740
|
+
if bid in ("ext-close", "ext-close2"):
|
|
741
|
+
self.dismiss(None)
|
|
742
|
+
return
|
|
743
|
+
if bid == "ext-create-btn":
|
|
744
|
+
self.app.push_screen(_CreateExtensionModal(), self._on_created_extension)
|
|
745
|
+
return
|
|
746
|
+
if bid == "ext-enable-all":
|
|
747
|
+
if ext_mgr:
|
|
748
|
+
for ext in ext_mgr.list_extensions():
|
|
749
|
+
if ext.get("installed") and not ext.get("enabled"):
|
|
750
|
+
ext_mgr.enable(ext["id"])
|
|
751
|
+
self._refresh("✓ All extensions enabled")
|
|
752
|
+
return
|
|
753
|
+
if bid == "ext-disable-all":
|
|
754
|
+
if ext_mgr:
|
|
755
|
+
for ext in ext_mgr.list_extensions():
|
|
756
|
+
if ext.get("enabled"):
|
|
757
|
+
ext_mgr.disable(ext["id"])
|
|
758
|
+
self._refresh("✓ All extensions disabled")
|
|
759
|
+
return
|
|
760
|
+
if bid.startswith("ext-install-"):
|
|
761
|
+
key = bid.replace("ext-install-", "")
|
|
762
|
+
if key in self._busy:
|
|
763
|
+
return
|
|
764
|
+
self._start_busy(key, "install")
|
|
765
|
+
try:
|
|
766
|
+
self.set_timer(0.85, lambda: self._do_install(key))
|
|
767
|
+
except Exception:
|
|
768
|
+
self._do_install(key)
|
|
769
|
+
return
|
|
770
|
+
if bid.startswith("ext-enable-"):
|
|
771
|
+
key = bid.replace("ext-enable-", "")
|
|
772
|
+
if key in self._busy:
|
|
773
|
+
return
|
|
774
|
+
self._start_busy(key, "enable")
|
|
775
|
+
try:
|
|
776
|
+
self.set_timer(0.6, lambda: self._do_enable(key))
|
|
777
|
+
except Exception:
|
|
778
|
+
self._do_enable(key)
|
|
779
|
+
return
|
|
780
|
+
if bid.startswith("ext-disable-"):
|
|
781
|
+
key = bid.replace("ext-disable-", "")
|
|
782
|
+
if key in self._busy:
|
|
783
|
+
return
|
|
784
|
+
self._start_busy(key, "disable")
|
|
785
|
+
try:
|
|
786
|
+
self.set_timer(0.6, lambda: self._do_disable(key))
|
|
787
|
+
except Exception:
|
|
788
|
+
self._do_disable(key)
|
|
789
|
+
return
|
|
790
|
+
if bid.startswith("ext-uninstall-"):
|
|
791
|
+
key = bid.replace("ext-uninstall-", "")
|
|
792
|
+
if key in self._busy:
|
|
793
|
+
return
|
|
794
|
+
self._start_busy(key, "uninstall")
|
|
795
|
+
try:
|
|
796
|
+
self.set_timer(0.85, lambda: self._do_uninstall(key))
|
|
797
|
+
except Exception:
|
|
798
|
+
self._do_uninstall(key)
|
|
799
|
+
return
|
|
800
|
+
|
|
801
|
+
def _on_created_extension(self, res):
|
|
802
|
+
if res and isinstance(res, tuple) and res[0] == "created":
|
|
803
|
+
self._refresh(f"✓ Extension '{res[2]}' created & added!")
|
|
804
|
+
|
|
805
|
+
def action_cancel(self):
|
|
806
|
+
self.dismiss(None)
|
|
807
|
+
|
|
808
|
+
def on_key(self, event):
|
|
809
|
+
if event.key == "escape":
|
|
810
|
+
self.dismiss(None)
|
|
811
|
+
|
|
812
|
+
else:
|
|
813
|
+
ExtensionsPanel = None
|
|
814
|
+
_DetailsPanel = None
|
|
815
|
+
_CreateExtensionModal = None
|