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,116 @@
|
|
|
1
|
+
"""PC capacity detection for Ollama model suggestions."""
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
def get_specs():
|
|
7
|
+
"""Return dict with ram_gb, cpu_count, gpu, os, arch."""
|
|
8
|
+
ram_gb = 8
|
|
9
|
+
try:
|
|
10
|
+
import psutil # type: ignore
|
|
11
|
+
ram_gb = round(psutil.virtual_memory().total / (1024**3), 1)
|
|
12
|
+
except Exception:
|
|
13
|
+
# fallback via ctypes on Windows or via /proc on linux
|
|
14
|
+
try:
|
|
15
|
+
if platform.system() == "Windows":
|
|
16
|
+
import ctypes
|
|
17
|
+
class MEMORYSTATUSEX(ctypes.Structure):
|
|
18
|
+
_fields_ = [("dwLength", ctypes.c_ulong),("dwMemoryLoad", ctypes.c_ulong),("ullTotalPhys", ctypes.c_ulonglong),("ullAvailPhys", ctypes.c_ulonglong),("ullTotalPageFile", ctypes.c_ulonglong),("ullAvailPageFile", ctypes.c_ulonglong),("ullTotalVirtual", ctypes.c_ulonglong),("ullAvailVirtual", ctypes.c_ulonglong),("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
|
|
19
|
+
stat = MEMORYSTATUSEX(dwLength=ctypes.sizeof(MEMORYSTATUSEX))
|
|
20
|
+
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
|
|
21
|
+
ram_gb = round(int(stat.ullTotalPhys) / (1024**3), 1)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
cpu = os.cpu_count() or 4
|
|
25
|
+
gpu = "Unknown"
|
|
26
|
+
gpu_vram = 0
|
|
27
|
+
# Try nvidia-smi, torch, or wmic
|
|
28
|
+
try:
|
|
29
|
+
import subprocess as sp
|
|
30
|
+
if shutil.which("nvidia-smi"):
|
|
31
|
+
out = sp.check_output(["nvidia-smi","--query-gpu=name,memory.total","--format=csv,noheader"], timeout=3, text=True)
|
|
32
|
+
if out.strip():
|
|
33
|
+
part = out.splitlines()[0]
|
|
34
|
+
gpu = part.split(",")[0].strip()
|
|
35
|
+
try:
|
|
36
|
+
vram_str = part.split(",")[1].strip().replace(" MiB","")
|
|
37
|
+
gpu_vram = round(int(vram_str)/1024,1)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
elif platform.system() == "Windows":
|
|
41
|
+
try:
|
|
42
|
+
out = sp.check_output("wmic path win32_VideoController get name", shell=True, timeout=3, text=True)
|
|
43
|
+
lines = [l.strip() for l in out.splitlines() if l.strip() and "Name" not in l]
|
|
44
|
+
if lines:
|
|
45
|
+
gpu = lines[0][:40]
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
# torch fallback
|
|
49
|
+
if gpu == "Unknown":
|
|
50
|
+
try:
|
|
51
|
+
import torch # type: ignore
|
|
52
|
+
if torch.cuda.is_available():
|
|
53
|
+
gpu = torch.cuda.get_device_name(0)
|
|
54
|
+
try:
|
|
55
|
+
props = torch.cuda.get_device_properties(0)
|
|
56
|
+
gpu_vram = round(props.total_memory/(1024**3),1)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
disk_free = 0
|
|
64
|
+
try:
|
|
65
|
+
du = shutil.disk_usage(os.path.expanduser("~"))
|
|
66
|
+
disk_free = round(du.free/(1024**3),1)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return {
|
|
70
|
+
"ram_gb": ram_gb,
|
|
71
|
+
"cpu_count": cpu,
|
|
72
|
+
"gpu": gpu,
|
|
73
|
+
"gpu_vram_gb": gpu_vram,
|
|
74
|
+
"disk_free_gb": disk_free,
|
|
75
|
+
"os": platform.system(),
|
|
76
|
+
"arch": platform.machine(),
|
|
77
|
+
"platform": platform.platform(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def suggested_max_params(specs):
|
|
81
|
+
"""Return recommended max model size label and list of suitable models.
|
|
82
|
+
Heuristic: RAM is primary, VRAM bonus if GPU present."""
|
|
83
|
+
ram = specs.get("ram_gb", 8)
|
|
84
|
+
vram = specs.get("gpu_vram_gb", 0)
|
|
85
|
+
effective = max(ram, vram*1.2 if vram else 0) # GPU gives headroom
|
|
86
|
+
# Map to max params
|
|
87
|
+
if effective >= 48:
|
|
88
|
+
tier = "70B+ (you can run 70B-405B with quantization)"
|
|
89
|
+
max_gb = 80
|
|
90
|
+
elif effective >= 24:
|
|
91
|
+
tier = "32B-34B comfortably, 70B quantized"
|
|
92
|
+
max_gb = 35
|
|
93
|
+
elif effective >= 16:
|
|
94
|
+
tier = "13B-14B comfortably, 20B+ quantized"
|
|
95
|
+
max_gb = 16
|
|
96
|
+
elif effective >= 8:
|
|
97
|
+
tier = "7B-8B comfortably"
|
|
98
|
+
max_gb = 9
|
|
99
|
+
else:
|
|
100
|
+
tier = "1B-3B recommended"
|
|
101
|
+
max_gb = 4
|
|
102
|
+
return tier, max_gb
|
|
103
|
+
|
|
104
|
+
def is_suitable(model, max_gb):
|
|
105
|
+
try:
|
|
106
|
+
return float(model.get("size_gb", 99)) <= max_gb * 1.1
|
|
107
|
+
except Exception:
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def recommendation_reason(model, specs, max_gb):
|
|
111
|
+
sz = model.get("size_gb", 0)
|
|
112
|
+
ram = specs.get("ram_gb", 8)
|
|
113
|
+
if sz <= max_gb:
|
|
114
|
+
return "✓ Fits your PC"
|
|
115
|
+
else:
|
|
116
|
+
return f"⚠ Needs ~{sz}GB, you have {ram}GB RAM — may be slow / OOM"
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT Permission Manager — a small, honest permission-state store for the
|
|
3
|
+
primary Textual UI (see calc_terminal/ui/).
|
|
4
|
+
|
|
5
|
+
This does not retroactively sandbox every existing code path in the
|
|
6
|
+
app (sandbox.py / security_scanner.py already do real enforcement for
|
|
7
|
+
/codepad execution). What this module adds is a *single source of
|
|
8
|
+
truth* for the toggles shown in the composer's Permissions panel, plus
|
|
9
|
+
a `request()` helper that produces the data an inline permission card
|
|
10
|
+
needs (action, reason, allow-once vs always-allow vs deny) — mirroring
|
|
11
|
+
the shape code_editor.py's `_scan_and_confirm` already prints, just
|
|
12
|
+
structured so a real widget (not print()) can render it.
|
|
13
|
+
|
|
14
|
+
## Three-mode layer (spec v0.7 section 2)
|
|
15
|
+
|
|
16
|
+
Added on TOP of the existing per-key toggles below, not as a separate
|
|
17
|
+
system — MODES change how `needs_prompt()`/`allowed()` interpret those
|
|
18
|
+
same toggles, they don't duplicate the toggle state:
|
|
19
|
+
|
|
20
|
+
- "ask" (default): every mutating action always prompts,
|
|
21
|
+
regardless of a key's on/off toggle, until the user
|
|
22
|
+
picks "Always Allow This Session" on that specific
|
|
23
|
+
action's card (matches the spec's Mode 1 exactly).
|
|
24
|
+
- "restricted": mutating actions are never silently applied; the
|
|
25
|
+
caller is expected to show a Suggested-Patch-style
|
|
26
|
+
Accept/Reject prompt instead of Allow-Once/Always-
|
|
27
|
+
Allow/Deny (see `restricted_review(key)` below).
|
|
28
|
+
- "full": nothing prompts. Matches the spec's Mode 3 — callers
|
|
29
|
+
should still surface the "visible warning" the spec
|
|
30
|
+
asks for; this module only tracks the state.
|
|
31
|
+
|
|
32
|
+
Honest scope note: today, `execute_python` is the only mutating AI
|
|
33
|
+
action actually wired to a permission gate anywhere in the codebase
|
|
34
|
+
(see ui/app.py's on_message_run_code). The spec's Section 1 ("AI file
|
|
35
|
+
create/edit/rename/delete") describes a whole AI-driven file-editing
|
|
36
|
+
tool CCT doesn't have yet — so "restricted" mode's file-patch-review
|
|
37
|
+
behavior has nowhere to apply until that tool exists. This module
|
|
38
|
+
implements the mode semantics correctly and generally so that gate
|
|
39
|
+
works the moment such a tool is added, rather than only for the one
|
|
40
|
+
action that exists today.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
print("This is a library file and is not meant to be run directly.")
|
|
45
|
+
import sys
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
import itertools
|
|
49
|
+
import time
|
|
50
|
+
|
|
51
|
+
_request_ids = itertools.count(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# (key, label, default_on) — order matches the composer's checklist.
|
|
55
|
+
PERMISSION_DEFS = [
|
|
56
|
+
("read_files", "Read Files", True),
|
|
57
|
+
("write_files", "Write Files", True),
|
|
58
|
+
("delete_files", "Delete Files", False),
|
|
59
|
+
("execute_python", "Execute Python", True),
|
|
60
|
+
("shell_commands", "Shell Commands", True),
|
|
61
|
+
("install_packages", "Install Packages", True),
|
|
62
|
+
("device_control", "Device Control", False),
|
|
63
|
+
("internet", "Internet", False),
|
|
64
|
+
("browser_automation", "Browser Automation", False),
|
|
65
|
+
("formula_library", "Formula Library", True),
|
|
66
|
+
("notebook", "Notebook", True),
|
|
67
|
+
("calculator", "Calculator", True),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Which permission keys represent a MUTATING action (create/write/
|
|
71
|
+
# execute/delete/install-class capability) vs. a read-only one. Modes
|
|
72
|
+
# only change prompting behavior for mutating keys — read_files,
|
|
73
|
+
# formula_library, notebook, and calculator stay governed by their
|
|
74
|
+
# plain toggle in every mode, since "ask every time before reading a
|
|
75
|
+
# file" would make the app unusable and isn't what the spec asks for
|
|
76
|
+
# either (its examples are all write/edit/execute/delete/install/commit).
|
|
77
|
+
MUTATING_KEYS = {
|
|
78
|
+
"write_files",
|
|
79
|
+
"delete_files",
|
|
80
|
+
"execute_python",
|
|
81
|
+
"shell_commands",
|
|
82
|
+
"install_packages",
|
|
83
|
+
"device_control",
|
|
84
|
+
"browser_automation",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
MODES = ("ask", "restricted", "full")
|
|
88
|
+
MODE_LABELS = {
|
|
89
|
+
"ask": "\U0001f512 Ask Every Time",
|
|
90
|
+
"restricted": "\U0001f7e1 Restricted",
|
|
91
|
+
"full": "\U0001f7e2 Full Access",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PermissionManager:
|
|
96
|
+
"""Holds on/off state for each permission and a log of decisions.
|
|
97
|
+
|
|
98
|
+
`always_allow` entries bypass future inline prompts for that key
|
|
99
|
+
until the session ends or the user flips it off again in the panel.
|
|
100
|
+
`always_deny` (v0.7.7) entries work the opposite way — once a user
|
|
101
|
+
picks "Always Deny" on a permission card, that key stops prompting
|
|
102
|
+
and is refused outright for the rest of the session.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self):
|
|
106
|
+
self._state = {key: default for key, _label, default in PERMISSION_DEFS}
|
|
107
|
+
self._always_allow = set()
|
|
108
|
+
self._always_deny = set() # v0.7.7: keys the user permanently (this session) denied
|
|
109
|
+
self.log = [] # list of (timestamp, key, action_text, decision)
|
|
110
|
+
self.mode = "ask" # spec v0.7 default: Ask Every Time
|
|
111
|
+
|
|
112
|
+
def allowed(self, key):
|
|
113
|
+
return bool(self._state.get(key, False))
|
|
114
|
+
|
|
115
|
+
def new_request_id(self):
|
|
116
|
+
"""One id per inline PermissionCard, so PermissionGranted/Denied
|
|
117
|
+
(posted later, after a real UI round trip) can be matched back
|
|
118
|
+
to the specific pending call that's waiting on it."""
|
|
119
|
+
return f"perm-req-{next(_request_ids)}"
|
|
120
|
+
|
|
121
|
+
def toggle(self, key):
|
|
122
|
+
if key in self._state:
|
|
123
|
+
self._state[key] = not self._state[key]
|
|
124
|
+
if not self._state[key]:
|
|
125
|
+
self._always_allow.discard(key)
|
|
126
|
+
else:
|
|
127
|
+
# re-enabling a permission in the panel means the user
|
|
128
|
+
# wants it usable again — clear any always-deny memory
|
|
129
|
+
# for it, same trust-reset semantics as set_mode.
|
|
130
|
+
self._always_deny.discard(key)
|
|
131
|
+
return self._state.get(key, False)
|
|
132
|
+
|
|
133
|
+
def set(self, key, value):
|
|
134
|
+
if key in self._state:
|
|
135
|
+
self._state[key] = bool(value)
|
|
136
|
+
if value:
|
|
137
|
+
self._always_deny.discard(key)
|
|
138
|
+
|
|
139
|
+
def snapshot(self):
|
|
140
|
+
"""Ordered list of (key, label, is_on) for rendering the panel."""
|
|
141
|
+
return [(key, label, self._state.get(key, False)) for key, label, _d in PERMISSION_DEFS]
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------- modes --
|
|
144
|
+
def set_mode(self, mode, persist=True):
|
|
145
|
+
"""Switch the global permission mode. Invalid names are
|
|
146
|
+
rejected (returns False) rather than silently defaulting —
|
|
147
|
+
a typo here should never silently grant broader access than
|
|
148
|
+
intended."""
|
|
149
|
+
if mode not in MODES:
|
|
150
|
+
return False
|
|
151
|
+
self.mode = mode
|
|
152
|
+
if mode == "ask":
|
|
153
|
+
self._always_allow.clear()
|
|
154
|
+
if persist:
|
|
155
|
+
try:
|
|
156
|
+
from . import config as _cfg
|
|
157
|
+
cfg = _cfg.load_config()
|
|
158
|
+
cfg.default_permission_mode = mode
|
|
159
|
+
_cfg.save_config(cfg)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def load_persisted_mode(self):
|
|
165
|
+
try:
|
|
166
|
+
from . import config as _cfg
|
|
167
|
+
cfg = _cfg.load_config()
|
|
168
|
+
m = getattr(cfg, "default_permission_mode", "ask")
|
|
169
|
+
if m in MODES:
|
|
170
|
+
self.mode = m
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def mode_label(self):
|
|
175
|
+
return MODE_LABELS[self.mode]
|
|
176
|
+
|
|
177
|
+
def needs_prompt(self, key):
|
|
178
|
+
"""True if this action should show an inline permission card
|
|
179
|
+
before proceeding.
|
|
180
|
+
|
|
181
|
+
- "full" mode: never prompts (matches spec Mode 3 exactly).
|
|
182
|
+
- "restricted" mode: mutating keys always need review (there's
|
|
183
|
+
no silent path — see restricted_review() for the specific
|
|
184
|
+
Accept/Reject framing instead of Allow/Deny); non-mutating
|
|
185
|
+
keys fall back to plain toggle behavior.
|
|
186
|
+
- "ask" mode: mutating keys always need a prompt UNLESS this
|
|
187
|
+
exact key was already granted "Always Allow This Session"
|
|
188
|
+
earlier in the current mode; non-mutating keys fall back to
|
|
189
|
+
plain toggle behavior.
|
|
190
|
+
|
|
191
|
+
v0.7.7: a key in `always_deny` never prompts — it's refused
|
|
192
|
+
outright (denied without a card) by the decision check.
|
|
193
|
+
"""
|
|
194
|
+
if key in self._always_deny:
|
|
195
|
+
return False
|
|
196
|
+
if self.mode == "full":
|
|
197
|
+
return False
|
|
198
|
+
if key in MUTATING_KEYS:
|
|
199
|
+
if self.mode == "restricted":
|
|
200
|
+
return True
|
|
201
|
+
# mode == "ask"
|
|
202
|
+
return key not in self._always_allow
|
|
203
|
+
return (not self.allowed(key)) and key not in self._always_allow
|
|
204
|
+
|
|
205
|
+
def is_blocked(self, key):
|
|
206
|
+
"""True if action is hard-blocked and must NOT execute.
|
|
207
|
+
- always_deny blocks in any mode.
|
|
208
|
+
- restricted mode blocks all mutating actions (no silent allow).
|
|
209
|
+
- install_flow_state already reports allow=False for restricted, but
|
|
210
|
+
this is the single general check for all mutating keys."""
|
|
211
|
+
if key in self._always_deny:
|
|
212
|
+
return True
|
|
213
|
+
if self.mode == "restricted" and key in MUTATING_KEYS:
|
|
214
|
+
# restricted still prompts, but caller must not auto-allow;
|
|
215
|
+
# hard-block if the prompt is bypassed
|
|
216
|
+
return False # prompt path handles it; not auto-blocked here
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def check(self, key):
|
|
220
|
+
"""Centralized permission evaluation. Returns (allowed, needs_prompt, blocked).
|
|
221
|
+
Callers should use this instead of scattering needs_prompt/allowed checks."""
|
|
222
|
+
if self.refused_by_always_deny(key):
|
|
223
|
+
return (False, False, True)
|
|
224
|
+
if self.mode == "full":
|
|
225
|
+
return (True, False, False)
|
|
226
|
+
if self.mode == "restricted" and key in MUTATING_KEYS:
|
|
227
|
+
return (False, True, False) # must prompt, not auto-allowed
|
|
228
|
+
if key in MUTATING_KEYS:
|
|
229
|
+
if key in self._always_allow:
|
|
230
|
+
return (True, False, False)
|
|
231
|
+
return (False, True, False)
|
|
232
|
+
# non-mutating: governed by toggle
|
|
233
|
+
if self.allowed(key):
|
|
234
|
+
return (True, False, False)
|
|
235
|
+
return (False, False, True) if key in self._always_deny else (False, True, False)
|
|
236
|
+
|
|
237
|
+
def refused_by_always_deny(self, key):
|
|
238
|
+
"""True when this key was permanently (this session) denied and
|
|
239
|
+
should be refused without showing a card at all."""
|
|
240
|
+
return key in self._always_deny
|
|
241
|
+
|
|
242
|
+
def restricted_review(self, key):
|
|
243
|
+
"""True if this key, under the CURRENT mode, should be shown
|
|
244
|
+
as a Suggested-Patch-style Accept/Reject prompt (spec Mode 2)
|
|
245
|
+
rather than the Allow-Once/Always-Allow/Deny card (spec Mode 1).
|
|
246
|
+
Only meaningful when needs_prompt(key) is already True."""
|
|
247
|
+
return self.mode == "restricted" and key in MUTATING_KEYS
|
|
248
|
+
|
|
249
|
+
def decide(self, key, decision, action_text="", reason=""):
|
|
250
|
+
"""Record a decision from an inline permission card.
|
|
251
|
+
|
|
252
|
+
decision is one of 'allow_once', 'always_allow', 'deny',
|
|
253
|
+
'always_deny', 'cancel' (v0.7.7 adds the last two — the spec's
|
|
254
|
+
install dialog offers Allow Once / Always Allow / Deny / Always
|
|
255
|
+
Deny / Cancel).
|
|
256
|
+
Returns True if the action should proceed. In "full" mode this
|
|
257
|
+
is never called by a correctly-behaving caller (needs_prompt
|
|
258
|
+
is already False), but if it IS called, "full" still honors
|
|
259
|
+
the explicit decision rather than silently overriding it.
|
|
260
|
+
"""
|
|
261
|
+
self.log.append((time.time(), key, action_text, decision))
|
|
262
|
+
if decision == "always_allow":
|
|
263
|
+
self._state[key] = True
|
|
264
|
+
self._always_allow.add(key)
|
|
265
|
+
return True
|
|
266
|
+
if decision == "always_deny":
|
|
267
|
+
self._state[key] = False
|
|
268
|
+
self._always_deny.add(key)
|
|
269
|
+
self._always_allow.discard(key)
|
|
270
|
+
return False
|
|
271
|
+
if decision == "allow_once" or decision == "allow":
|
|
272
|
+
# "allow" is the CLI callback's spelling of a one-shot grant
|
|
273
|
+
# (see app.py _cli_perm_callback) — treating it as refusal
|
|
274
|
+
# made "[Enter] Allow" silently DENY the action.
|
|
275
|
+
return True
|
|
276
|
+
return False # deny and cancel both refuse
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ------------------------------------------------------- installs --
|
|
280
|
+
# Package installation is governed by the same three-mode execution
|
|
281
|
+
# policy as every other mutating action:
|
|
282
|
+
#
|
|
283
|
+
# - full access -> executes automatically, no prompt (the install is
|
|
284
|
+
# still logged in manager.log and the package
|
|
285
|
+
# history).
|
|
286
|
+
# - ask every time -> prompt=True; the caller shows its approval card.
|
|
287
|
+
# - restricted -> allow=False; installation is disabled outright
|
|
288
|
+
# and the caller must report a real permission
|
|
289
|
+
# error, never a fake success.
|
|
290
|
+
#
|
|
291
|
+
# The UI flows (calc_terminal/app.py and calc_terminal/ui/app.py) call
|
|
292
|
+
# `install_flow_state()` before starting an install. This module only
|
|
293
|
+
# tracks state; it never shows UI.
|
|
294
|
+
|
|
295
|
+
_SAVED_MODE = None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def install_flow_state():
|
|
299
|
+
"""Returns a dict describing how the install permission flow should
|
|
300
|
+
behave RIGHT NOW under the active mode:
|
|
301
|
+
{"allow": bool, "prompt": bool, "restore_to": None}
|
|
302
|
+
- restricted mode -> allow=False (installation disabled entirely).
|
|
303
|
+
- ask mode -> allow=True, prompt=True.
|
|
304
|
+
- full mode -> allow=True, prompt=False (automatic execution;
|
|
305
|
+
still recorded via decide()/history by callers).
|
|
306
|
+
"""
|
|
307
|
+
global _SAVED_MODE
|
|
308
|
+
if manager.mode == "restricted":
|
|
309
|
+
return {"allow": False, "prompt": False, "restore_to": None}
|
|
310
|
+
if manager.mode == "ask":
|
|
311
|
+
return {"allow": True, "prompt": True, "restore_to": None}
|
|
312
|
+
# full — automatic execution, no narrowing, nothing to restore
|
|
313
|
+
_SAVED_MODE = None
|
|
314
|
+
return {"allow": True, "prompt": False, "restore_to": None}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def restore_mode():
|
|
318
|
+
"""Restores the mode saved by install_flow_state() (spec section 3:
|
|
319
|
+
"After installation, restore the previous permission mode
|
|
320
|
+
automatically"). Safe to call even when nothing was saved."""
|
|
321
|
+
global _SAVED_MODE
|
|
322
|
+
if _SAVED_MODE is not None:
|
|
323
|
+
manager.set_mode(_SAVED_MODE)
|
|
324
|
+
_SAVED_MODE = None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Module-level shared instance — the composer, the permission panel, and
|
|
328
|
+
# any inline request card all read/write through this one object so the
|
|
329
|
+
# checklist state and prompt behaviour stay consistent across a session.
|
|
330
|
+
manager = PermissionManager()
|
|
331
|
+
try:
|
|
332
|
+
manager.load_persisted_mode()
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
calc_terminal/pet.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live mascot pet — Feature request v0.5.6+: "a mascot animated pet
|
|
3
|
+
character who can walk, run, and sleep in real time in the terminal."
|
|
4
|
+
|
|
5
|
+
The app is a blocking-input CLI (it sits on `input()` between turns),
|
|
6
|
+
so there's no free-running redraw thread here — but the pet still
|
|
7
|
+
*feels* alive: every time the prompt box is about to be drawn, it
|
|
8
|
+
looks at how long it's been since the user last did anything and
|
|
9
|
+
picks a state accordingly:
|
|
10
|
+
|
|
11
|
+
* fired off a question quickly (< RUN_THRESHOLD s) -> "run"
|
|
12
|
+
* normal pace -> "walk"
|
|
13
|
+
* left the terminal alone for a while (idle) -> "sleep"
|
|
14
|
+
|
|
15
|
+
Each redraw also advances an internal tick, so across a few turns the
|
|
16
|
+
walk/run cycles actually animate across the screen instead of freezing
|
|
17
|
+
on one frame, and /pet plays a short live loop on demand.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
from . import theme
|
|
24
|
+
|
|
25
|
+
WALK_FRAMES = ["\\o/", " o<", "/o\\", ">o "]
|
|
26
|
+
RUN_FRAMES = ["\\o_/", "_o/-", "-\\o_", "/-o\\"]
|
|
27
|
+
SLEEP_FRAMES = ["(-.-) zzz", "(-.-) zz", "(-.-) z", "(-.-) "]
|
|
28
|
+
|
|
29
|
+
RUN_THRESHOLD = 4 # seconds since last activity -> "run"
|
|
30
|
+
SLEEP_THRESHOLD = 45 # seconds idle -> "sleep"
|
|
31
|
+
|
|
32
|
+
_last_activity = time.time()
|
|
33
|
+
_tick = 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def mark_activity():
|
|
37
|
+
"""Call this once per submitted command/question — resets the idle
|
|
38
|
+
clock so the *next* time the pet is drawn, it reflects how long the
|
|
39
|
+
user took to respond this round."""
|
|
40
|
+
global _last_activity
|
|
41
|
+
_last_activity = time.time()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def current_state():
|
|
45
|
+
elapsed = time.time() - _last_activity
|
|
46
|
+
if elapsed >= SLEEP_THRESHOLD:
|
|
47
|
+
return "sleep"
|
|
48
|
+
if elapsed <= RUN_THRESHOLD:
|
|
49
|
+
return "run"
|
|
50
|
+
return "walk"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _frames_for(state):
|
|
54
|
+
return {"walk": WALK_FRAMES, "run": RUN_FRAMES, "sleep": SLEEP_FRAMES}[state]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _color_for(state):
|
|
58
|
+
return {"walk": theme.CYAN, "run": theme.ORANGE, "sleep": theme.PURPLE}.get(
|
|
59
|
+
state, theme.CYAN)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def frame_line(width=78, state=None):
|
|
63
|
+
"""One line showing the pet in its current (or forced) state at its
|
|
64
|
+
current tick position. Calling this repeatedly advances the walk."""
|
|
65
|
+
global _tick
|
|
66
|
+
_tick += 1
|
|
67
|
+
state = state or current_state()
|
|
68
|
+
frames = _frames_for(state)
|
|
69
|
+
frame = frames[_tick % len(frames)]
|
|
70
|
+
color = _color_for(state)
|
|
71
|
+
|
|
72
|
+
if state == "sleep":
|
|
73
|
+
pos = 2
|
|
74
|
+
else:
|
|
75
|
+
span = max(4, width - len(frame) - 6)
|
|
76
|
+
step = 2 if state == "run" else 1
|
|
77
|
+
pos = (_tick * step) % span
|
|
78
|
+
|
|
79
|
+
return " " * pos + theme.gradient(frame, color, theme.PURPLE, bold=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def print_frame(width=78, state=None):
|
|
83
|
+
if not sys.stdout.isatty():
|
|
84
|
+
return
|
|
85
|
+
print(frame_line(width, state=state))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def demo(width=78, seconds_per_state=1.2):
|
|
89
|
+
"""A short live loop cycling through all three states in place —
|
|
90
|
+
used by /pet so you can actually watch it walk, run, then fall
|
|
91
|
+
asleep without waiting around for real idle time to pass."""
|
|
92
|
+
if not sys.stdout.isatty():
|
|
93
|
+
print(theme.faint(" (mascot needs a real terminal to animate — "
|
|
94
|
+
"showing static frames instead)"))
|
|
95
|
+
for state in ("walk", "run", "sleep"):
|
|
96
|
+
print(theme.dim(f" {state}: ") + frame_line(width, state=state))
|
|
97
|
+
return
|
|
98
|
+
for state, delay in (("walk", 0.05), ("run", 0.03), ("sleep", 0.4)):
|
|
99
|
+
steps = max(6, int(seconds_per_state / delay))
|
|
100
|
+
for _ in range(steps):
|
|
101
|
+
line = frame_line(width, state=state)
|
|
102
|
+
sys.stdout.write("\r" + line + " " * 10)
|
|
103
|
+
sys.stdout.flush()
|
|
104
|
+
time.sleep(delay)
|
|
105
|
+
sys.stdout.write("\r" + " " * (width + 10) + "\r")
|
|
106
|
+
sys.stdout.flush()
|