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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,166 @@
1
+ """
2
+ CCT UI — ComposerFooter (the single badge row docked inside StickyComposer:
3
+ `NOTEBOOK · provider/model · Workspace` on the left, live context readout
4
+ center, minimal Permissions/Attach/Send glyphs right — flat dot-separated
5
+ text badges, not a button toolbar, matching the reference terminal-chat
6
+ aesthetic) and StatusLine (the compact bar CCTApp docks at the very
7
+ bottom of the screen). Both render state and dispatch events/bubble
8
+ presses to their parent — neither decides what a click means.
9
+ """
10
+
11
+ if __name__ == "__main__":
12
+ print("This is a library file and is not meant to be run directly.")
13
+ import sys
14
+ sys.exit(1)
15
+
16
+ TEXTUAL_AVAILABLE = True
17
+ try:
18
+ from textual.containers import Horizontal
19
+ from textual.widgets import Static, Button
20
+ from textual.reactive import reactive
21
+ except Exception:
22
+ TEXTUAL_AVAILABLE = False
23
+
24
+ from .events import NotebookChanged, WorkspaceChanged, CommandExecuted
25
+ from .. import ai_modes
26
+
27
+ WORKSPACE_CYCLE = ["Workspace", "Kinetics", "Quantum", "Notebook A"]
28
+
29
+
30
+ if TEXTUAL_AVAILABLE:
31
+
32
+ def _hex_blend(h1, h2, t):
33
+ """Linear interpolate two '#rrggbb' hex colors at t in [0, 1].
34
+ Returns a '#rrggbb' hex string — local helper so the mode badge
35
+ can render its 4-segment gradient bar without reaching into
36
+ theme internals (theme.blend takes RGB tuples, not hex)."""
37
+ a = [int(h1[i:i + 2], 16) for i in (1, 3, 5)]
38
+ b = [int(h2[i:i + 2], 16) for i in (1, 3, 5)]
39
+ return "#" + "".join(f"{int(round(x + (y - x) * t)):02x}"
40
+ for x, y in zip(a, b))
41
+
42
+ class _Badge(Static):
43
+ """One flat, clickable text badge (no border/background chrome) —
44
+ the `Notebook` / `provider model` / `Workspace` pieces of
45
+ `NOTEBOOK \u00b7 openai/gpt-4o-mini \u00b7 Workspace`."""
46
+
47
+ def __init__(self, text, badge_id, on_click=None, classes="cct-badge"):
48
+ super().__init__(text, id=badge_id, classes=classes)
49
+ self._on_click = on_click
50
+
51
+ def on_click(self, event):
52
+ if self._on_click:
53
+ self._on_click()
54
+
55
+ class ComposerFooter(Horizontal):
56
+ """The single badge row along the bottom of the composer card:
57
+ `Notebook \u00b7 model \u00b7 Workspace` (left), live context (center),
58
+ three minimal glyph controls (right). One row, one component.
59
+ """
60
+
61
+ # Holds the AI-mode KEY ("notebook"/"agent"/"build"/"plan" — see
62
+ # ai_modes.py), not a display string. Reactive name kept as
63
+ # `notebook_mode` (rather than renamed to `ai_mode`) so the
64
+ # NotebookChanged event this posts, and ui/app.py's existing
65
+ # on_notebook_changed handler, didn't need touching to grow
66
+ # from a 2-way NOTEBOOK/AGENT toggle to the spec's 4 modes.
67
+ notebook_mode = reactive("notebook")
68
+ context_tokens = reactive(0)
69
+
70
+ def __init__(self, model_label="no model"):
71
+ super().__init__(id="cct-bottom-row")
72
+ self._model_label = model_label
73
+ self._workspace_i = 0
74
+
75
+ def _mode_badge_markup(self, mode_key):
76
+ accent = ai_modes.accent_hex(mode_key)
77
+ m = ai_modes.meta(mode_key)
78
+ # v0.7.8: per-mode gradient applied to the badge — a 4-segment
79
+ # ▮ bar blending the mode's two-stop gradient, then the icon in
80
+ # the start color and the label in the end color.
81
+ try:
82
+ gs, ge = ai_modes.gradient(mode_key)
83
+ bar = "".join(f"[{_hex_blend(gs, ge, t)}]\u25ae[/]"
84
+ for t in (0.0, 0.33, 0.66, 1.0))
85
+ return f"{bar} [{gs} b]{m['icon']}[/] [{ge} b]{m['label']}[/]"
86
+ except Exception:
87
+ return f"[{accent} b]{m['icon']} {m['label']}[/]"
88
+
89
+ def compose(self):
90
+ with Horizontal(id="cct-left-controls"):
91
+ yield _Badge(self._mode_badge_markup(self.notebook_mode),
92
+ "badge-notebook", self._toggle_notebook)
93
+ yield Static("\u00b7", classes="cct-badge-sep")
94
+ yield _Badge(self._model_label, "badge-model", self._open_model)
95
+ yield Static("\u00b7", classes="cct-badge-sep")
96
+ yield _Badge(WORKSPACE_CYCLE[0], "badge-workspace", self._cycle_workspace)
97
+ yield Static(id="cct-footer-spacer-left")
98
+ yield Static(self._status_text(), id="cct-center-status")
99
+ yield Static(id="cct-footer-spacer-right")
100
+ with Horizontal(id="cct-right-controls"):
101
+ yield Button("\U0001f6e1", id="btn-permissions", classes="cct-icon-btn", tooltip="Permissions & Security (🛡)")
102
+ yield Button("\U0001f4ce", id="btn-attach", classes="cct-icon-btn", tooltip="Attach file")
103
+ yield Button("\u27a4", id="btn-send", classes="cct-icon-btn cct-icon-send", tooltip="Send message (Enter)")
104
+
105
+ def _status_text(self):
106
+ return f"Context {self.context_tokens}K"
107
+
108
+ def watch_context_tokens(self, value):
109
+ try:
110
+ self.query_one("#cct-center-status", Static).update(self._status_text())
111
+ except Exception:
112
+ pass
113
+
114
+ def set_model_label(self, label):
115
+ self._model_label = label
116
+ try:
117
+ self.query_one("#badge-model", _Badge).update(label)
118
+ except Exception:
119
+ pass
120
+
121
+ def _toggle_notebook(self):
122
+ """Clicking the badge cycles Notebook -> Agent -> Build ->
123
+ Plan -> Notebook (spec section 3's four AI modes, up from
124
+ the old two-stop NOTEBOOK/AGENT toggle)."""
125
+ self.notebook_mode = ai_modes.next_mode(self.notebook_mode)
126
+ self.query_one("#badge-notebook", _Badge).update(
127
+ self._mode_badge_markup(self.notebook_mode))
128
+ self.post_message(NotebookChanged(self.notebook_mode))
129
+
130
+ def _open_model(self):
131
+ self.post_message(CommandExecuted("/model"))
132
+
133
+ def _cycle_workspace(self):
134
+ self._workspace_i = (self._workspace_i + 1) % len(WORKSPACE_CYCLE)
135
+ name = WORKSPACE_CYCLE[self._workspace_i]
136
+ self.query_one("#badge-workspace", _Badge).update(name)
137
+ self.post_message(WorkspaceChanged(name))
138
+
139
+ class StatusLine(Static):
140
+ """One compact bottom status line for the whole app — no boxes,
141
+ no oversized labels. `get_fields` is a zero-arg callable
142
+ returning [(label, value, tone), ...]; CCTApp supplies it and
143
+ calls `refresh_status()` when something worth showing changes,
144
+ instead of this widget reaching into backend state itself."""
145
+
146
+ def __init__(self, get_fields, id="cct-statusline"):
147
+ super().__init__(id=id)
148
+ self._get_fields = get_fields
149
+
150
+ def on_mount(self):
151
+ self.refresh_status()
152
+
153
+ def refresh_status(self):
154
+ from . import theme_css
155
+ tone_role = {"ready": "success", "warn": "warning", "err": "error", "": "text-muted"}
156
+ parts = []
157
+ for label, value, tone in self._get_fields():
158
+ color = theme_css.current_hex(tone_role.get(tone, "text-muted"))
159
+ text = f"[{color}]{value}[/]" if not label else f"{label} [{color}]{value}[/]"
160
+ parts.append(text)
161
+ self.update(" \u00b7 ".join(parts))
162
+
163
+ else:
164
+ ComposerFooter = None
165
+ StatusLine = None
166
+
@@ -0,0 +1,383 @@
1
+ """
2
+ CAT UI — gestures_panel.py: Gesture Manager (spec section 23).
3
+
4
+ Responsive: uses percentage widths, auto height, scroll, and stacked
5
+ fallbacks for narrow terminals. Buttons use consistent sizing and
6
+ plain-text labels (no emoji-width mismatches). Layout never clips.
7
+ """
8
+
9
+ if __name__ == "__main__":
10
+ print("This is a library file and is not meant to be run directly.")
11
+ import sys
12
+ sys.exit(1)
13
+
14
+ from ..gestures.bindings import GESTURE_TRIGGERS, GESTURE_TARGETS, GESTURE_ACTIONS
15
+ from ..gestures.manager import list_gestures, add_gesture, delete_gesture, toggle_gesture, reset_defaults
16
+
17
+ TEXTUAL_AVAILABLE = True
18
+ try:
19
+ from textual.containers import Vertical, Horizontal, VerticalScroll
20
+ from textual.screen import Screen
21
+ from textual.widgets import Static, Button, Input, Select
22
+ from textual.binding import Binding
23
+ except Exception:
24
+ TEXTUAL_AVAILABLE = False
25
+
26
+
27
+ if TEXTUAL_AVAILABLE:
28
+
29
+ class _GestureRow(Horizontal):
30
+ def __init__(self, gesture):
31
+ super().__init__(classes="cct-gesture-row")
32
+ self.gesture = gesture
33
+
34
+ def compose(self):
35
+ g = self.gesture
36
+ enabled = g.get("enabled", True)
37
+ name = g.get("name","")
38
+ trg = g.get("trigger","")
39
+ tgt = g.get("target","")
40
+ act = g.get("action","")
41
+ keys = g.get("keys","")
42
+ detail = f"{keys} ({tgt}) -> {act}" if trg == "shortcut" and keys else f"{trg} on {tgt} -> {act}"
43
+ # Left: name + detail stacked, so narrow terminals don't clip
44
+ with Vertical(classes="cct-gesture-left"):
45
+ if enabled:
46
+ yield Static(f"[b]{name}[/]", classes="cct-gesture-name")
47
+ else:
48
+ yield Static(f"[dim]{name} (disabled)[/]", classes="cct-gesture-name")
49
+ yield Static(detail, classes="cct-gesture-detail")
50
+ # Right: actions - fixed width group that stays visible
51
+ with Horizontal(classes="cct-gesture-actions"):
52
+ label = "Disable" if enabled else "Enable"
53
+ yield Button(label, id=f"gesture-toggle-{g.get('id')}", classes="cct-btn cct-btn-sm")
54
+ yield Button("Delete", id=f"gesture-del-{g.get('id')}", variant="error", classes="cct-btn cct-btn-sm")
55
+
56
+ def on_button_pressed(self, event):
57
+ event.stop()
58
+ gid = self.gesture.get("id")
59
+ if event.button.id == f"gesture-toggle-{gid}":
60
+ toggle_gesture(gid)
61
+ for scr in self.app.screen_stack:
62
+ if isinstance(scr, GesturesPanel):
63
+ scr.refresh_gestures()
64
+ break
65
+ elif event.button.id == f"gesture-del-{gid}":
66
+ delete_gesture(gid)
67
+ for scr in self.app.screen_stack:
68
+ if isinstance(scr, GesturesPanel):
69
+ scr.refresh_gestures()
70
+ break
71
+
72
+
73
+ class GesturesPanel(Screen):
74
+ """Gesture Manager — responsive CAT-style manager panel."""
75
+
76
+ CSS = """
77
+ GesturesPanel { align: center middle; background: $app-background 60%; }
78
+
79
+ #gestures-box {
80
+ width: 84; height: auto; max-height: 42;
81
+ background: $surface; border: round $border;
82
+ padding: 0; overflow: hidden;
83
+ layout: vertical;
84
+ }
85
+ #gestures-titlebar {
86
+ height: 3; min-height: 3;
87
+ padding: 1 2 0 2; border-bottom: solid $border;
88
+ layout: horizontal;
89
+ }
90
+ #gestures-title { width: 1fr; text-style: bold; }
91
+ #gestures-subtitle { color: $text-faint; height: auto; padding: 0 2 1 2; }
92
+ #gestures-disabled { height: auto; padding: 2 2; align: center middle; }
93
+ .gestures-disabled-msg { color: $warning; text-style: bold; height: auto; text-align: center; padding-bottom: 1; }
94
+
95
+ #gestures-count {
96
+ height: 1; color: $text-faint;
97
+ padding: 0 2; margin-bottom: 1;
98
+ }
99
+
100
+ #gestures-list {
101
+ height: 1fr; min-height: 8; max-height: 20;
102
+ overflow-y: auto; overflow-x: hidden;
103
+ margin: 0 1; padding: 0 0;
104
+ scrollbar-gutter: stable; scrollbar-size: 1 1;
105
+ scrollbar-color: $border $surface;
106
+ scrollbar-color-hover: $accent $surface;
107
+ }
108
+ #gestures-empty {
109
+ color: $text-faint; padding: 2 1; text-align: center; height: auto;
110
+ }
111
+
112
+ .cct-gesture-row {
113
+ height: auto; min-height: 4;
114
+ padding: 1 1;
115
+ border-bottom: solid $border;
116
+ border-left: thick transparent;
117
+ layout: horizontal;
118
+ overflow: hidden;
119
+ }
120
+ .cct-gesture-row:hover {
121
+ border-left: thick $accent;
122
+ background: $surface-alt 40%;
123
+ }
124
+ .cct-gesture-left { width: 1fr; height: auto; min-width: 12; }
125
+ .cct-gesture-name { width: auto; height: 1; }
126
+ .cct-gesture-detail { width: auto; height: 1; color: $text-muted; }
127
+ .cct-gesture-actions {
128
+ width: auto; height: auto; min-width: 20;
129
+ layout: horizontal; align: center middle;
130
+ }
131
+ .cct-gesture-actions Button { margin-left: 1; min-width: 10; height: 3; }
132
+
133
+ /* Compact: stack left+actions vertically on very narrow terminals */
134
+ .cct-compact .cct-gesture-row { layout: vertical; min-height: 6; }
135
+ .cct-compact .cct-gesture-actions { width: 100%; padding-top: 1; }
136
+
137
+ /* Form — responsive: rows become vertical on narrow */
138
+ #gestures-form {
139
+ height: auto; padding: 1 2;
140
+ border-top: solid $border; background: $app-background 35%;
141
+ }
142
+ #gestures-form-title { height: 1; text-style: bold; color: $accent; padding-bottom: 1; }
143
+ #gestures-form-row1 { height: 3; margin-bottom: 1; }
144
+ #gestures-form-row1 Input { width: 1fr; }
145
+ #gestures-form-row2 {
146
+ height: auto; min-height: 3;
147
+ layout: horizontal; overflow-x: auto;
148
+ scrollbar-size: 1 1; scrollbar-gutter: stable;
149
+ }
150
+ #gestures-form-row2 Select { width: 1fr; min-width: 14; margin-right: 1; margin-bottom: 1; }
151
+ #gestures-form-row2 Button { min-width: 12; height: 3; }
152
+
153
+ #gestures-form-row3 {
154
+ height: 3; margin-top: 1; layout: horizontal;
155
+ display: none;
156
+ }
157
+ #gestures-form-row3.show { display: block; }
158
+ #gestures-form-row3 Input { width: 1fr; }
159
+ #gestures-shortcut-hint { color: $text-faint; height: 1; padding: 0 2; display: none; }
160
+ #gestures-shortcut-hint.show { display: block; }
161
+
162
+ .cct-compact #gestures-form-row2 { layout: vertical; }
163
+ .cct-compact #gestures-form-row2 Select { width: 100%; margin-right: 0; }
164
+
165
+ #gestures-actions {
166
+ height: auto; min-height: 3; padding: 1 2;
167
+ layout: horizontal; overflow-x: auto;
168
+ scrollbar-size: 1 1; scrollbar-gutter: stable;
169
+ border-top: solid $border;
170
+ }
171
+ #gestures-actions Button { margin-right: 1; margin-bottom: 1; min-width: 16; }
172
+
173
+ #gestures-hint {
174
+ height: auto; min-height: 2;
175
+ color: $text-faint; padding: 1 2;
176
+ text-align: center;
177
+ border-top: solid $border;
178
+ }
179
+
180
+ .cct-compact #gestures-box { width: 96; height: auto; }
181
+ """
182
+
183
+ BINDINGS = [Binding("escape", "cancel", "Cancel")]
184
+
185
+ def __init__(self):
186
+ super().__init__()
187
+
188
+ def compose(self):
189
+ # Extension guard: if Gestures is disabled/uninstalled, show disabled state
190
+ try:
191
+ from .. import extensions as _ext
192
+ if not _ext.is_enabled("gestures"):
193
+ with Vertical(id="gestures-box"):
194
+ with Horizontal(id="gestures-titlebar"):
195
+ yield Static("Gestures & Shortcuts", id="gestures-title")
196
+ yield Button("✕", id="gestures-close", classes="cct-ctrl")
197
+ yield Static("Gestures is disabled — enable it in Extensions to use gestures and shortcuts.", id="gestures-subtitle")
198
+ with Vertical(id="gestures-disabled"):
199
+ yield Static("🔌 Gestures is currently disabled.", classes="gestures-disabled-msg")
200
+ yield Button("Open Extensions", id="gestures-open-ext", variant="primary")
201
+ yield Button("Close", id="gestures-close2")
202
+ return
203
+ except Exception:
204
+ pass
205
+ with Vertical(id="gestures-box"):
206
+ with Horizontal(id="gestures-titlebar"):
207
+ yield Static("Gestures & Shortcuts", id="gestures-title")
208
+ yield Button("X", id="gestures-close", classes="cct-ctrl")
209
+ # v0.7.9.10 fix: Gestures is independent of MCP — never show
210
+ # "MCP server down" here. This panel manages only gestures/
211
+ # shortcuts (see gestures/manager.py), while MCP servers
212
+ # live exclusively in McpServersPanel (calc_terminal/mcp.py).
213
+ yield Static("Gestures: double-click/long-press/swipe. Shortcuts: custom key combos -> actions. (Independent of MCP servers)", id="gestures-subtitle")
214
+ yield Static("", id="gestures-count")
215
+ with VerticalScroll(id="gestures-list"):
216
+ pass
217
+ with Vertical(id="gestures-form"):
218
+ yield Static("Add new gesture / shortcut", id="gestures-form-title")
219
+ with Horizontal(id="gestures-form-row1"):
220
+ yield Input(placeholder="Name (e.g. Shift+Enter -> Preview)", id="gestures-name")
221
+ with Horizontal(id="gestures-form-row2"):
222
+ yield Select([(t, t) for t in GESTURE_TRIGGERS], prompt="Trigger", id="gestures-trigger", value=GESTURE_TRIGGERS[0])
223
+ yield Select([(t, t) for t in GESTURE_TARGETS], prompt="Target", id="gestures-target", value=GESTURE_TARGETS[0])
224
+ yield Select([(a, a) for a in GESTURE_ACTIONS], prompt="Action", id="gestures-action", value=GESTURE_ACTIONS[0])
225
+ yield Button("Add", id="gestures-add", variant="primary", classes="cct-btn-sm")
226
+ with Horizontal(id="gestures-form-row3"):
227
+ yield Input(placeholder="Shortcut keys (e.g. shift+enter, ctrl+s, ctrl+shift+p)", id="gestures-keys")
228
+ yield Static("Hint: shift+enter, ctrl+s, ctrl+shift+p, f5, alt+enter ...", id="gestures-shortcut-hint")
229
+ with Horizontal(id="gestures-actions"):
230
+ yield Button("Reset Defaults", id="gestures-reset", classes="cct-btn-sm")
231
+ yield Button("Close", id="gestures-close2", variant="primary", classes="cct-btn-sm")
232
+ yield Static("Double-click editor -> VS Code · preview -> fullscreen · shortcut: shift+enter -> preview", id="gestures-hint")
233
+
234
+ def on_mount(self):
235
+ self.call_after_refresh(lambda: self.query_one("#gestures-box").add_class("open") if self.query_one("#gestures-box").has_class("open") is False else None)
236
+ try:
237
+ self.query_one("#gestures-box").add_class("open")
238
+ except Exception:
239
+ pass
240
+ self.call_after_refresh(self.refresh_gestures)
241
+ self._update_shortcut_row()
242
+ try:
243
+ if self.size.width < 76 or self.size.height < 30:
244
+ self.query_one("#gestures-box").add_class("cct-compact")
245
+ except Exception:
246
+ pass
247
+ try:
248
+ self.query_one("#gestures-name", Input).focus()
249
+ except Exception:
250
+ pass
251
+
252
+ def _update_shortcut_row(self):
253
+ try:
254
+ trg = self.query_one("#gestures-trigger", Select).value
255
+ show = (trg == "shortcut")
256
+ row = self.query_one("#gestures-form-row3", Horizontal)
257
+ hint = self.query_one("#gestures-shortcut-hint", Static)
258
+ if show:
259
+ row.add_class("show")
260
+ hint.add_class("show")
261
+ else:
262
+ row.remove_class("show")
263
+ hint.remove_class("show")
264
+ except Exception:
265
+ pass
266
+
267
+ def on_select_changed(self, event):
268
+ # show/hide shortcut keys input when trigger changes
269
+ if getattr(event.select, "id", "") == "gestures-trigger":
270
+ self._update_shortcut_row()
271
+
272
+ def refresh_gestures(self):
273
+ try:
274
+ container = self.query_one("#gestures-list", VerticalScroll)
275
+ count_label = self.query_one("#gestures-count", Static)
276
+ except Exception:
277
+ return
278
+ try:
279
+ container.remove_children()
280
+ except Exception:
281
+ pass
282
+ gestures = list_gestures()
283
+ enabled = sum(1 for g in gestures if g.get("enabled"))
284
+ try:
285
+ count_label.update(f"{len(gestures)} gestures · {enabled} enabled")
286
+ except Exception:
287
+ pass
288
+ if not gestures:
289
+ try:
290
+ container.mount(Static("No gestures yet.\nAdd one below or reset to defaults.", id="gestures-empty"))
291
+ except Exception:
292
+ pass
293
+ return
294
+ for g in gestures:
295
+ try:
296
+ container.mount(_GestureRow(g))
297
+ except Exception:
298
+ pass
299
+
300
+ def on_button_pressed(self, event):
301
+ bid = event.button.id
302
+ if bid == "gestures-open-ext":
303
+ try:
304
+ self.dismiss(None)
305
+ from .extensions_panel import ExtensionsPanel
306
+ self.app.push_screen(ExtensionsPanel())
307
+ except Exception:
308
+ self.dismiss(None)
309
+ return
310
+ if bid in ("gestures-close", "gestures-close2"):
311
+ self.dismiss(None)
312
+ elif bid == "gestures-reset":
313
+ reset_defaults()
314
+ self.refresh_gestures()
315
+ try:
316
+ self.app._system_note("Gestures reset to defaults")
317
+ except Exception:
318
+ pass
319
+ elif bid == "gestures-add":
320
+ try:
321
+ name = self.query_one("#gestures-name", Input).value.strip() or "Custom gesture"
322
+ try:
323
+ trigger = self.query_one("#gestures-trigger", Select).value
324
+ except Exception:
325
+ trigger = None
326
+ try:
327
+ target = self.query_one("#gestures-target", Select).value
328
+ except Exception:
329
+ target = None
330
+ try:
331
+ action = self.query_one("#gestures-action", Select).value
332
+ except Exception:
333
+ action = None
334
+ try:
335
+ keys = self.query_one("#gestures-keys", Input).value.strip().lower() if trigger == "shortcut" else ""
336
+ except Exception:
337
+ keys = ""
338
+ if not trigger or trigger not in GESTURE_TRIGGERS:
339
+ trigger = GESTURE_TRIGGERS[0]
340
+ if not target or target not in GESTURE_TARGETS:
341
+ target = GESTURE_TARGETS[0]
342
+ if not action or action not in GESTURE_ACTIONS:
343
+ action = GESTURE_ACTIONS[0]
344
+ # shortcut needs keys
345
+ if trigger == "shortcut":
346
+ if not keys:
347
+ try:
348
+ self.app._system_note("Enter shortcut keys (e.g. shift+enter, ctrl+s)")
349
+ except Exception:
350
+ pass
351
+ return
352
+ result = add_gesture(name, trigger, target, action, keys=keys)
353
+ if result:
354
+ self.refresh_gestures()
355
+ try:
356
+ self.query_one("#gestures-name", Input).value = ""
357
+ self.query_one("#gestures-keys", Input).value = ""
358
+ except Exception:
359
+ pass
360
+ try:
361
+ self.app._system_note(f"Added {'shortcut' if trigger=='shortcut' else 'gesture'}: {name}")
362
+ except Exception:
363
+ pass
364
+ else:
365
+ try:
366
+ self.app._system_note("Could not add — check values (duplicate shortcut or invalid keys)")
367
+ except Exception:
368
+ pass
369
+ except Exception as e:
370
+ try:
371
+ self.app._system_note(f"Could not add gesture: {e}")
372
+ except Exception:
373
+ pass
374
+
375
+ def action_cancel(self):
376
+ self.dismiss(None)
377
+
378
+ def on_key(self, event):
379
+ if event.key == "escape":
380
+ self.dismiss(None)
381
+
382
+ else:
383
+ GesturesPanel = None
@@ -0,0 +1,100 @@
1
+ """Goodbye screen with ASCII art — shown on sign-out."""
2
+
3
+ if __name__ == "__main__":
4
+ import sys
5
+ sys.exit(1)
6
+
7
+ TEXTUAL_AVAILABLE = True
8
+ try:
9
+ from textual.screen import Screen
10
+ from textual.containers import Vertical
11
+ from textual.widgets import Static, Button
12
+ from textual.binding import Binding
13
+ except Exception:
14
+ TEXTUAL_AVAILABLE = False
15
+
16
+ _GOODBYE_CAT = [
17
+ " /\\_/\\ ",
18
+ " ( o.o ) ",
19
+ " > ^ < ",
20
+ ]
21
+
22
+ _GOODBYE_BLOCK = [
23
+ "░██████╗░░█████╗░░█████╗░██████╗░  ██████╗░██╗░░░██╗███████╗",
24
+ "██╔════╝░██╔══██╗██╔══██╗██╔══██╗  ██╔══██╗╚██╗░██╔╝██╔════╝",
25
+ "██║░░██╗░██║░░██║██║░░██║██║░░██║  ██████╦╝░╚████╔╝░█████╗░░",
26
+ "██║░░╚██╗██║░░██║██║░░██║██║░░██║  ██╔══██╗░░╚██╔╝░░██╔══╝░░",
27
+ "╚██████╔╝╚█████╔╝╚█████╔╝██████╔╝  ██████╦╝░░░██║░░░███████╗",
28
+ "░╚═════╝░░╚════╝░░╚════╝░╚═════╝░  ╚═════╝░░░░╚═╝░░░╚══════╝",
29
+ ]
30
+
31
+ if TEXTUAL_AVAILABLE:
32
+ class GoodbyeScreen(Screen):
33
+ CSS = """
34
+ GoodbyeScreen { align: center middle; background: $app-background; }
35
+ #goodbye-box {
36
+ width: 72; height: auto; max-height: 24;
37
+ background: $surface; border: round $border;
38
+ padding: 1 2; align: center middle;
39
+ }
40
+ #goodbye-cat { color: $accent; height: auto; text-align: center; }
41
+ #goodbye-block { color: $text; height: auto; text-align: center; }
42
+ #goodbye-title { color: $accent; height: auto; text-align: center; padding-top: 1; text-style: bold; }
43
+ #goodbye-msg { color: $text-muted; height: auto; text-align: center; padding-top: 1; }
44
+ #goodbye-hint { color: $text-faint; height: 1; text-align: center; padding-top: 1; }
45
+ """
46
+ BINDINGS = [Binding("escape", "close", "Close"), Binding("enter", "close", "Close"), Binding("q", "close", "Close")]
47
+
48
+ def __init__(self, farewell="Session saved. See you soon! \U0001F43E"):
49
+ super().__init__()
50
+ self._farewell = farewell
51
+
52
+ def compose(self):
53
+ with Vertical(id="goodbye-box"):
54
+ # Try to use art.py if available for richer art
55
+ try:
56
+ from .. import art as _art
57
+ from .. import theme as _theme
58
+ # Pick a nice cat variant
59
+ cat_lines = _GOODBYE_CAT
60
+ except Exception:
61
+ cat_lines = _GOODBYE_CAT
62
+ yield Static("\n".join(cat_lines), id="goodbye-cat")
63
+ yield Static("\n".join(_GOODBYE_BLOCK), id="goodbye-block")
64
+ yield Static("G O O D B Y E", id="goodbye-title")
65
+ yield Static(self._farewell, id="goodbye-msg")
66
+ yield Static("Press Enter, Esc or Q to exit \u00b7 Auto-close in 3s", id="goodbye-hint")
67
+
68
+ def on_mount(self):
69
+ self.set_timer(3.0, self._auto_close)
70
+
71
+ def _auto_close(self):
72
+ try:
73
+ self.dismiss(None)
74
+ self.app.exit()
75
+ except Exception:
76
+ pass
77
+
78
+ def on_key(self, event):
79
+ if event.key in ("escape", "enter", "q"):
80
+ self.dismiss(None)
81
+ try:
82
+ self.app.exit()
83
+ except Exception:
84
+ pass
85
+
86
+ def action_close(self):
87
+ self.dismiss(None)
88
+ try:
89
+ self.app.exit()
90
+ except Exception:
91
+ pass
92
+
93
+ def on_button_pressed(self, event):
94
+ self.dismiss(None)
95
+ try:
96
+ self.app.exit()
97
+ except Exception:
98
+ pass
99
+ else:
100
+ GoodbyeScreen = None