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,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
calc_terminal/terminal_identity.py
|
|
3
|
+
==================================
|
|
4
|
+
Centralized Terminal & Tab Identity management for CAT CLI.
|
|
5
|
+
|
|
6
|
+
Guarantees that the active terminal tab and window title display:
|
|
7
|
+
🐱 CAT CLI (or CAT CLI)
|
|
8
|
+
across Windows Terminal, Command Prompt (cmd.exe), PowerShell, and POSIX
|
|
9
|
+
terminals, preventing generic host shell names ('Command Prompt',
|
|
10
|
+
'PowerShell', 'cmd.exe', 'pwsh') from leaking or polluting the tab/window.
|
|
11
|
+
|
|
12
|
+
Features:
|
|
13
|
+
- Emits UTF-8 OSC 0 and OSC 2 escape sequences safely without Windows cp1252 crashes.
|
|
14
|
+
- Calls Win32 kernel32.SetConsoleTitleW for conhost / ConPTY consistency.
|
|
15
|
+
- Loads and sets the official CAT logo (.ico) via WM_SETICON on the console HWND.
|
|
16
|
+
- Detects and registers the CAT CLI profile in Windows Terminal settings.json.
|
|
17
|
+
- Runs a lightweight TitleGuard daemon thread to recover if external processes
|
|
18
|
+
(e.g. child cmd.exe / npm / git) overwrite the console title.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import atexit
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from typing import Optional, Tuple
|
|
27
|
+
|
|
28
|
+
BRAND_NAME = "CAT CLI"
|
|
29
|
+
BRAND_EMOJI = "🐱"
|
|
30
|
+
DEFAULT_TITLE = f"{BRAND_EMOJI} {BRAND_NAME}"
|
|
31
|
+
PLAIN_TITLE = BRAND_NAME
|
|
32
|
+
WT_CAT_GUID = "{a3f2b4c1-d8e7-4f9a-8b1c-3d5e7f9a2b4c}"
|
|
33
|
+
|
|
34
|
+
_current_active_title: str = DEFAULT_TITLE
|
|
35
|
+
_title_guard_thread: Optional[threading.Thread] = None
|
|
36
|
+
_title_guard_stop_event = threading.Event()
|
|
37
|
+
_title_guard_lock = threading.Lock()
|
|
38
|
+
_title_saved: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_local_appdata_dir() -> str:
|
|
42
|
+
local_app = os.environ.get("LOCALAPPDATA")
|
|
43
|
+
if local_app:
|
|
44
|
+
base = os.path.join(local_app, "CCT")
|
|
45
|
+
else:
|
|
46
|
+
base = os.path.expanduser("~/.cct")
|
|
47
|
+
try:
|
|
48
|
+
os.makedirs(base, exist_ok=True)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
return base
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_icon_path() -> Optional[str]:
|
|
55
|
+
"""Return the absolute path to the multi-resolution CAT .ico file,
|
|
56
|
+
ensuring it exists in a stable location."""
|
|
57
|
+
# 1. Check local CCT app data
|
|
58
|
+
local_dir = _get_local_appdata_dir()
|
|
59
|
+
local_ico = os.path.join(local_dir, "cat.ico")
|
|
60
|
+
if os.path.isfile(local_ico) and os.path.getsize(local_ico) > 0:
|
|
61
|
+
return os.path.abspath(local_ico)
|
|
62
|
+
|
|
63
|
+
# 2. Check bundled package paths
|
|
64
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
65
|
+
candidates = [
|
|
66
|
+
os.path.join(pkg_dir, "cat.ico"),
|
|
67
|
+
os.path.join(pkg_dir, "web", "static", "icons", "cat.ico"),
|
|
68
|
+
]
|
|
69
|
+
for c in candidates:
|
|
70
|
+
if os.path.isfile(c) and os.path.getsize(c) > 0:
|
|
71
|
+
# Copy to local appdata for persistent access by external apps (Windows Terminal)
|
|
72
|
+
try:
|
|
73
|
+
import shutil
|
|
74
|
+
shutil.copy2(c, local_ico)
|
|
75
|
+
return os.path.abspath(local_ico)
|
|
76
|
+
except Exception:
|
|
77
|
+
return os.path.abspath(c)
|
|
78
|
+
|
|
79
|
+
# 3. Try generating from icon-512.png if PIL is available
|
|
80
|
+
png_source = os.path.join(pkg_dir, "web", "static", "icons", "icon-512.png")
|
|
81
|
+
if os.path.isfile(png_source):
|
|
82
|
+
try:
|
|
83
|
+
from PIL import Image
|
|
84
|
+
img = Image.open(png_source)
|
|
85
|
+
sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
|
|
86
|
+
img.save(local_ico, sizes=sizes)
|
|
87
|
+
return os.path.abspath(local_ico)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def apply_console_icon(ico_path: Optional[str] = None) -> bool:
|
|
95
|
+
"""Set the Windows console window icon via Win32 WM_SETICON."""
|
|
96
|
+
if sys.platform != "win32":
|
|
97
|
+
return False
|
|
98
|
+
try:
|
|
99
|
+
import ctypes
|
|
100
|
+
kernel32 = ctypes.windll.kernel32
|
|
101
|
+
user32 = ctypes.windll.user32
|
|
102
|
+
|
|
103
|
+
hwnd = kernel32.GetConsoleWindow()
|
|
104
|
+
if not hwnd:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
if not ico_path:
|
|
108
|
+
ico_path = get_icon_path()
|
|
109
|
+
if not ico_path or not os.path.isfile(ico_path):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
IMAGE_ICON = 1
|
|
113
|
+
LR_LOADFROMFILE = 0x00000010
|
|
114
|
+
WM_SETICON = 0x0080
|
|
115
|
+
ICON_SMALL = 0
|
|
116
|
+
ICON_BIG = 1
|
|
117
|
+
|
|
118
|
+
# Load small (16x16) and big (32x32) icons
|
|
119
|
+
hicon_small = user32.LoadImageW(None, ico_path, IMAGE_ICON, 16, 16, LR_LOADFROMFILE)
|
|
120
|
+
hicon_big = user32.LoadImageW(None, ico_path, IMAGE_ICON, 32, 32, LR_LOADFROMFILE)
|
|
121
|
+
|
|
122
|
+
success = False
|
|
123
|
+
if hicon_small:
|
|
124
|
+
user32.SendMessageW(hwnd, WM_SETICON, ICON_SMALL, hicon_small)
|
|
125
|
+
success = True
|
|
126
|
+
if hicon_big:
|
|
127
|
+
user32.SendMessageW(hwnd, WM_SETICON, ICON_BIG, hicon_big)
|
|
128
|
+
success = True
|
|
129
|
+
return success
|
|
130
|
+
except Exception:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _safe_write_osc(text: str) -> bool:
|
|
135
|
+
"""Write OSC title escape sequences using raw UTF-8 bytes to prevent
|
|
136
|
+
Windows default cp1252 UnicodeEncodeError crashes."""
|
|
137
|
+
try:
|
|
138
|
+
if os.environ.get("TERM", "") == "dumb":
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
is_tty = False
|
|
142
|
+
try:
|
|
143
|
+
is_tty = bool(getattr(sys.stdout, "isatty", lambda: False)())
|
|
144
|
+
except Exception:
|
|
145
|
+
is_tty = False
|
|
146
|
+
|
|
147
|
+
if not is_tty:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Clean control characters
|
|
151
|
+
clean = "".join(ch for ch in str(text) if ch.isprintable() and ch not in "\x07\x1b")[:100]
|
|
152
|
+
|
|
153
|
+
# Push title once on xterm if supported
|
|
154
|
+
global _title_saved
|
|
155
|
+
push_seq = b"\x1b[22t" if not _title_saved else b""
|
|
156
|
+
if not _title_saved:
|
|
157
|
+
_title_saved = True
|
|
158
|
+
|
|
159
|
+
# OSC 0 (window & icon) and OSC 2 (window title)
|
|
160
|
+
osc_seq = f"\x1b]0;{clean}\x07\x1b]2;{clean}\x07".encode("utf-8")
|
|
161
|
+
|
|
162
|
+
payload = push_seq + osc_seq
|
|
163
|
+
# Prefer writing directly to buffer for UTF-8 reliability
|
|
164
|
+
buf = getattr(sys.stdout, "buffer", None)
|
|
165
|
+
if buf is not None:
|
|
166
|
+
buf.write(payload)
|
|
167
|
+
buf.flush()
|
|
168
|
+
else:
|
|
169
|
+
try:
|
|
170
|
+
sys.stdout.write(f"\x1b]0;{clean}\x07")
|
|
171
|
+
sys.stdout.flush()
|
|
172
|
+
except UnicodeEncodeError:
|
|
173
|
+
fallback = clean.encode("ascii", errors="ignore").decode("ascii").strip()
|
|
174
|
+
sys.stdout.write(f"\x1b]0;{fallback}\x07")
|
|
175
|
+
sys.stdout.flush()
|
|
176
|
+
return True
|
|
177
|
+
except Exception:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def set_terminal_title(title: Optional[str] = None, with_icon: bool = True) -> bool:
|
|
182
|
+
"""Set the terminal tab and window title to '🐱 CAT CLI' (or custom title).
|
|
183
|
+
Centralized function that handles:
|
|
184
|
+
1. OSC 0 and OSC 2 terminal sequences (UTF-8 safe)
|
|
185
|
+
2. Win32 SetConsoleTitleW
|
|
186
|
+
3. Win32 console WM_SETICON (on Windows)
|
|
187
|
+
"""
|
|
188
|
+
global _current_active_title
|
|
189
|
+
|
|
190
|
+
if title is None or not str(title).strip():
|
|
191
|
+
chosen = DEFAULT_TITLE if with_icon else PLAIN_TITLE
|
|
192
|
+
else:
|
|
193
|
+
t = str(title).strip()
|
|
194
|
+
# If the user passed a sub-title like 'Starting' or 'Ready', brand it nicely
|
|
195
|
+
if t.lower() in ("starting", "ready", "running", "exiting"):
|
|
196
|
+
chosen = f"{DEFAULT_TITLE} — {t.capitalize()}"
|
|
197
|
+
elif not (t.startswith(BRAND_EMOJI) or BRAND_NAME in t):
|
|
198
|
+
chosen = f"{DEFAULT_TITLE} — {t}"
|
|
199
|
+
else:
|
|
200
|
+
chosen = t
|
|
201
|
+
|
|
202
|
+
_current_active_title = chosen
|
|
203
|
+
|
|
204
|
+
# 1. OSC sequences (for Windows Terminal, modern conhost, Mintty, VS Code, iTerm, etc.)
|
|
205
|
+
osc_ok = _safe_write_osc(chosen)
|
|
206
|
+
|
|
207
|
+
# 2. Win32 SetConsoleTitleW (for classic conhost, cmd.exe, PowerShell, and ConPTY)
|
|
208
|
+
win32_ok = False
|
|
209
|
+
if sys.platform == "win32":
|
|
210
|
+
try:
|
|
211
|
+
import ctypes
|
|
212
|
+
ctypes.windll.kernel32.SetConsoleTitleW(chosen)
|
|
213
|
+
win32_ok = True
|
|
214
|
+
apply_console_icon()
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
return osc_ok or win32_ok
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def restore_terminal_title() -> bool:
|
|
222
|
+
"""Restore previous terminal title on exit (xterm title stack pop)."""
|
|
223
|
+
global _title_saved
|
|
224
|
+
if not _title_saved:
|
|
225
|
+
return False
|
|
226
|
+
try:
|
|
227
|
+
buf = getattr(sys.stdout, "buffer", None)
|
|
228
|
+
if buf is not None:
|
|
229
|
+
buf.write(b"\x1b[23t")
|
|
230
|
+
buf.flush()
|
|
231
|
+
else:
|
|
232
|
+
sys.stdout.write("\x1b[23t")
|
|
233
|
+
sys.stdout.flush()
|
|
234
|
+
_title_saved = False
|
|
235
|
+
return True
|
|
236
|
+
except Exception:
|
|
237
|
+
_title_saved = False
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_current_console_title() -> Optional[str]:
|
|
242
|
+
"""Retrieve the current console title via Win32 GetConsoleTitleW."""
|
|
243
|
+
if sys.platform != "win32":
|
|
244
|
+
return None
|
|
245
|
+
try:
|
|
246
|
+
import ctypes
|
|
247
|
+
buf = ctypes.create_unicode_buffer(1024)
|
|
248
|
+
length = ctypes.windll.kernel32.GetConsoleTitleW(buf, 1024)
|
|
249
|
+
if length > 0:
|
|
250
|
+
return buf.value
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _is_polluted_title(current: Optional[str]) -> bool:
|
|
257
|
+
"""Check if the title currently contains a generic shell or subprocess name."""
|
|
258
|
+
if not current:
|
|
259
|
+
return True
|
|
260
|
+
s = current.strip().lower()
|
|
261
|
+
# If it contains CAT CLI, it's good
|
|
262
|
+
if "cat cli" in s or "coding agent terminal" in s:
|
|
263
|
+
return False
|
|
264
|
+
# Polluted shell names
|
|
265
|
+
polluters = [
|
|
266
|
+
"command prompt",
|
|
267
|
+
"powershell",
|
|
268
|
+
"cmd.exe",
|
|
269
|
+
"pwsh",
|
|
270
|
+
"windows powershell",
|
|
271
|
+
"npm",
|
|
272
|
+
"node",
|
|
273
|
+
"git",
|
|
274
|
+
"python",
|
|
275
|
+
]
|
|
276
|
+
return any(p in s for p in polluters)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _title_guard_worker(interval: float = 1.0):
|
|
280
|
+
"""Background worker that continuously asserts the CAT CLI title
|
|
281
|
+
if an external subprocess or shell clobbers it."""
|
|
282
|
+
while not _title_guard_stop_event.is_set():
|
|
283
|
+
try:
|
|
284
|
+
if sys.platform == "win32":
|
|
285
|
+
curr = get_current_console_title()
|
|
286
|
+
if _is_polluted_title(curr):
|
|
287
|
+
set_terminal_title(_current_active_title)
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
_title_guard_stop_event.wait(interval)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def start_title_guard(interval: float = 1.0):
|
|
294
|
+
"""Start the background thread that guards against title pollution."""
|
|
295
|
+
global _title_guard_thread
|
|
296
|
+
with _title_guard_lock:
|
|
297
|
+
if _title_guard_thread is not None and _title_guard_thread.is_alive():
|
|
298
|
+
return
|
|
299
|
+
_title_guard_stop_event.clear()
|
|
300
|
+
_title_guard_thread = threading.Thread(
|
|
301
|
+
target=_title_guard_worker,
|
|
302
|
+
args=(interval,),
|
|
303
|
+
name="CAT-TitleGuard",
|
|
304
|
+
daemon=True,
|
|
305
|
+
)
|
|
306
|
+
_title_guard_thread.start()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def stop_title_guard():
|
|
310
|
+
"""Stop the title guard thread."""
|
|
311
|
+
with _title_guard_lock:
|
|
312
|
+
_title_guard_stop_event.set()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Ensure title guard stops cleanly on exit
|
|
316
|
+
atexit.register(stop_title_guard)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def find_windows_terminal_settings() -> Optional[str]:
|
|
320
|
+
"""Locate the active Windows Terminal settings.json file."""
|
|
321
|
+
if sys.platform != "win32":
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
local_app = os.environ.get("LOCALAPPDATA", "")
|
|
325
|
+
candidates = [
|
|
326
|
+
# Windows Terminal (Stable)
|
|
327
|
+
os.path.join(local_app, "Packages", "Microsoft.WindowsTerminal_8wekyb3d8bbwe", "LocalState", "settings.json"),
|
|
328
|
+
# Windows Terminal (Preview)
|
|
329
|
+
os.path.join(local_app, "Packages", "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", "LocalState", "settings.json"),
|
|
330
|
+
# Unpackaged / Scoop / Winget portable
|
|
331
|
+
os.path.join(local_app, "Microsoft", "Windows Terminal", "settings.json"),
|
|
332
|
+
]
|
|
333
|
+
for c in candidates:
|
|
334
|
+
if os.path.isfile(c):
|
|
335
|
+
return c
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def configure_windows_terminal_profile() -> Tuple[bool, str]:
|
|
340
|
+
"""Register or update the dedicated 'CAT CLI' profile in Windows Terminal settings.json.
|
|
341
|
+
Sets:
|
|
342
|
+
- name: "CAT CLI"
|
|
343
|
+
- commandline: "cat"
|
|
344
|
+
- icon: path to cat.ico
|
|
345
|
+
- tabTitle: "CAT CLI"
|
|
346
|
+
- suppressApplicationTitle: false
|
|
347
|
+
"""
|
|
348
|
+
settings_file = find_windows_terminal_settings()
|
|
349
|
+
if not settings_file:
|
|
350
|
+
return False, "Windows Terminal settings.json not found."
|
|
351
|
+
|
|
352
|
+
ico_path = get_icon_path()
|
|
353
|
+
if not ico_path:
|
|
354
|
+
return False, "CAT icon (.ico) not available."
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
import json
|
|
358
|
+
with open(settings_file, "r", encoding="utf-8-sig") as f:
|
|
359
|
+
data = json.load(f)
|
|
360
|
+
|
|
361
|
+
profiles = data.setdefault("profiles", {})
|
|
362
|
+
plist = profiles.setdefault("list", [])
|
|
363
|
+
|
|
364
|
+
# Find existing profile
|
|
365
|
+
target_profile = None
|
|
366
|
+
for p in plist:
|
|
367
|
+
if p.get("guid") == WT_CAT_GUID or p.get("name") == BRAND_NAME:
|
|
368
|
+
target_profile = p
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
if target_profile is None:
|
|
372
|
+
target_profile = {
|
|
373
|
+
"guid": WT_CAT_GUID,
|
|
374
|
+
"name": BRAND_NAME,
|
|
375
|
+
"commandline": "cat",
|
|
376
|
+
"icon": ico_path,
|
|
377
|
+
"tabTitle": BRAND_NAME,
|
|
378
|
+
"suppressApplicationTitle": False,
|
|
379
|
+
}
|
|
380
|
+
plist.append(target_profile)
|
|
381
|
+
action_desc = "Created new 'CAT CLI' profile"
|
|
382
|
+
else:
|
|
383
|
+
target_profile["name"] = BRAND_NAME
|
|
384
|
+
target_profile["icon"] = ico_path
|
|
385
|
+
target_profile["tabTitle"] = BRAND_NAME
|
|
386
|
+
action_desc = "Updated existing 'CAT CLI' profile"
|
|
387
|
+
|
|
388
|
+
# Atomic write back
|
|
389
|
+
tmp_file = settings_file + ".tmp"
|
|
390
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
391
|
+
json.dump(data, f, indent=4)
|
|
392
|
+
os.replace(tmp_file, settings_file)
|
|
393
|
+
return True, f"{action_desc} in Windows Terminal ({settings_file})."
|
|
394
|
+
except Exception as e:
|
|
395
|
+
return False, f"Failed to configure Windows Terminal profile: {e}"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def init_terminal_identity():
|
|
399
|
+
"""High-level initialization called on CAT CLI startup."""
|
|
400
|
+
# 1. Set the initial title immediately
|
|
401
|
+
set_terminal_title()
|
|
402
|
+
|
|
403
|
+
# 2. Configure Windows Terminal profile if running on Windows
|
|
404
|
+
if sys.platform == "win32":
|
|
405
|
+
try:
|
|
406
|
+
configure_windows_terminal_profile()
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
# 3. Start title guard to protect against subprocess/shell pollution
|
|
411
|
+
start_title_guard()
|