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,887 @@
1
+ """
2
+ CAT Browser — graphical browser shell (replaces CLI browser).
3
+ A real browser UI: tab bar, navigation toolbar, address bar, viewport,
4
+ new-tab page, history/bookmarks/downloads, menu, fullscreen.
5
+
6
+ Uses the existing Chromium engine (browser/engine.py) for real HTML/CSS/JS
7
+ rendering via Playwright. Pages are rendered to screenshots, converted to
8
+ ANSI half-block art, and displayed directly in the terminal.
9
+
10
+ DESIGN: The viewport uses a single Static widget that gets updated with
11
+ the current content (newtab, loading, page screenshot, error). This avoids
12
+ fragile CSS display toggling.
13
+ """
14
+
15
+ if __name__ == "__main__":
16
+ print("This is a library file and is not meant to be run directly.")
17
+ import sys
18
+ sys.exit(1)
19
+
20
+ import os
21
+ import re
22
+ import time
23
+ import threading
24
+ import urllib.parse
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+ TEXTUAL_AVAILABLE = True
29
+ try:
30
+ from textual.app import App, ComposeResult
31
+ from textual.screen import Screen, ModalScreen
32
+ from textual.containers import Horizontal, Vertical, VerticalScroll, Container
33
+ from textual.widgets import Button, Input, Static, Label
34
+ from textual.reactive import reactive
35
+ from textual import work, on
36
+ from textual.message import Message
37
+ from textual.binding import Binding
38
+ from rich.text import Text as RichText
39
+ except Exception:
40
+ TEXTUAL_AVAILABLE = False
41
+ App = object
42
+ Screen = object
43
+
44
+ try:
45
+ from ..browser.browser_state import BrowserState, BrowserTab
46
+ from ..browser.engine import BrowserEngine, PLAYWRIGHT_AVAILABLE
47
+ except Exception:
48
+ BrowserState = None
49
+ BrowserTab = None
50
+ PLAYWRIGHT_AVAILABLE = False
51
+
52
+ try:
53
+ from .. import theme
54
+ except Exception:
55
+ theme = None
56
+
57
+ _BROWSER_STATE = None
58
+ def get_state() -> "BrowserState":
59
+ global _BROWSER_STATE
60
+ if _BROWSER_STATE is None:
61
+ _BROWSER_STATE = BrowserState()
62
+ return _BROWSER_STATE
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # CSS
66
+ # ---------------------------------------------------------------------------
67
+ BROWSER_CSS = """
68
+ #cat-browser {
69
+ width: 100%;
70
+ height: 100%;
71
+ background: $background;
72
+ }
73
+
74
+ /* TAB BAR — compact, minimal */
75
+ #browser-tabbar {
76
+ width: 100%;
77
+ height: 3;
78
+ background: $surface;
79
+ border-bottom: solid $border 50%;
80
+ layout: horizontal;
81
+ padding: 0 0 0 0;
82
+ }
83
+ .browser-tab {
84
+ width: auto;
85
+ min-width: 12;
86
+ max-width: 24;
87
+ height: 3;
88
+ margin: 0;
89
+ padding: 0 1;
90
+ background: transparent;
91
+ color: $text-muted;
92
+ border: none;
93
+ border-bottom: solid $accent 0%;
94
+ content-align: left middle;
95
+ }
96
+ .browser-tab:hover { background: $surface-alt; color: $text; }
97
+ .browser-tab.-active {
98
+ background: $surface-alt;
99
+ color: $text;
100
+ border-bottom: solid $accent 100%;
101
+ text-style: bold;
102
+ }
103
+ .browser-tab.-loading { color: $warning; }
104
+ .browser-tab-favicon { width: 2; content-align: center middle; }
105
+ .browser-tab-title { width: 1fr; padding-left: 0; overflow: hidden; }
106
+ Button.browser-tab-close {
107
+ width: 3; height: 1; min-height: 1; min-width: 3;
108
+ background: transparent; border: none; color: $text-faint;
109
+ content-align: center middle; padding: 0;
110
+ }
111
+ Button.browser-tab-close:hover { color: $error; background: transparent; }
112
+ Button#browser-newtab-btn {
113
+ width: 3; height: 2; min-height: 1; min-width: 3;
114
+ background: transparent; border: none; color: $text-muted;
115
+ content-align: center middle; padding: 0;
116
+ }
117
+ Button#browser-newtab-btn:hover { color: $accent; }
118
+
119
+ /* TOOLBAR — compact single row */
120
+ #browser-toolbar {
121
+ width: 100%;
122
+ height: 3;
123
+ background: $background;
124
+ border-bottom: solid $border 50%;
125
+ layout: horizontal;
126
+ padding: 0 1;
127
+ }
128
+ Button.browser-nav-btn {
129
+ width: 4; height: 1; min-height: 1; min-width: 4;
130
+ background: transparent; border: none; color: $text-muted;
131
+ content-align: center middle; margin-right: 0; padding: 0;
132
+ }
133
+ Button.browser-nav-btn:hover { color: $text; }
134
+ Button.browser-nav-btn:disabled { color: $text-faint; }
135
+ #browser-address-wrap {
136
+ width: 1fr; height: 3; min-height: 3;
137
+ background: $surface; border: round $border;
138
+ layout: horizontal; padding: 0 1;
139
+ }
140
+ #browser-address-wrap:focus-within { border: round $accent; }
141
+ #browser-lock { width: 2; height: 3; content-align: center middle; color: $accent; }
142
+ #browser-lock.-insecure { color: $text-faint; }
143
+ #browser-address {
144
+ width: 1fr; height: 3; min-height: 3;
145
+ background: transparent; border: none; color: $text; padding: 0 1;
146
+ }
147
+ #browser-address:focus { border: none; }
148
+ Button.browser-toolbar-btn {
149
+ width: 4; height: 1; min-height: 1; min-width: 4;
150
+ background: transparent; border: none; color: $text-faint;
151
+ content-align: center middle; margin-left: 0; padding: 0;
152
+ }
153
+ Button.browser-toolbar-btn:hover { color: $text; }
154
+ Button.browser-toolbar-btn.-starred { color: $warning; }
155
+
156
+ /* VIEWPORT — single content area */
157
+ #browser-viewport {
158
+ width: 100%;
159
+ height: 1fr;
160
+ background: $surface;
161
+ overflow-y: auto;
162
+ }
163
+ #browser-content {
164
+ width: 100%;
165
+ height: auto;
166
+ min-height: 10;
167
+ padding: 0;
168
+ background: $surface;
169
+ color: $text;
170
+ }
171
+
172
+ /* MENU */
173
+ #browser-menu {
174
+ width: 24; height: auto; max-height: 70%;
175
+ background: $surface; border: round $border;
176
+ padding: 1 0; dock: right;
177
+ }
178
+ .browser-menu-item {
179
+ width: 100%; height: 1; padding: 0 2;
180
+ background: transparent; border: none; color: $text;
181
+ content-align: left middle;
182
+ }
183
+ .browser-menu-item:hover { background: $surface-alt; color: $accent; }
184
+ .browser-menu-sep { width: 100%; height: 1; color: $border; }
185
+
186
+ /* PANELS (history/bookmarks/etc) */
187
+ .browser-panel {
188
+ width: 100%; height: 100%;
189
+ background: $background; padding: 2;
190
+ }
191
+ .browser-panel-title {
192
+ width: 100%; height: 2;
193
+ color: $text; text-style: bold;
194
+ border-bottom: solid $border; margin-bottom: 1;
195
+ }
196
+ .browser-row {
197
+ width: 100%; height: 1; layout: horizontal;
198
+ padding: 0 1; color: $text;
199
+ }
200
+ .browser-row:hover { background: $surface-alt; }
201
+ .browser-row-favicon { width: 2; content-align: center middle; }
202
+ .browser-row-title { width: 1fr; padding-left: 1; overflow: hidden; }
203
+ .browser-row-url { width: auto; max-width: 30; color: $text-faint; overflow: hidden; }
204
+ .browser-row-time { width: auto; color: $text-faint; padding-left: 1; }
205
+ Button.browser-row-action {
206
+ width: 4; height: 1; min-height: 1; min-width: 4;
207
+ background: transparent; border: none; color: $text-faint; padding: 0;
208
+ }
209
+ Button.browser-row-action:hover { color: $error; }
210
+
211
+ /* Fullscreen */
212
+ #cat-browser.-fullscreen #browser-tabbar { display: none; }
213
+ """
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # NEWTAB HTML (rendered as Rich Text for display in Static widget)
217
+ # ---------------------------------------------------------------------------
218
+ _NEWTAB_HTML = """\
219
+ [bold cyan] CAT Browser[/]
220
+
221
+ [dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]
222
+
223
+ [bold] Type a URL or search query in the address bar above[/]
224
+ [dim]Press[/] [bold cyan]Ctrl+L[/] [dim]to focus the address bar[/]
225
+ [dim]Press[/] [bold cyan]Ctrl+T[/] [dim]for a new tab[/]
226
+ [dim]Press[/] [bold cyan]Ctrl+W[/] [dim]to close the tab[/]
227
+
228
+ [dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]
229
+ """
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # Widgets
233
+ # ---------------------------------------------------------------------------
234
+ if TEXTUAL_AVAILABLE:
235
+
236
+ class TabWidget(Static):
237
+ def __init__(self, tab: BrowserTab, active: bool = False):
238
+ super().__init__("", classes="browser-tab" + (" -active" if active else "") + (" -loading" if tab.loading else ""))
239
+ self.tab = tab
240
+ self.active = active
241
+ self.can_focus = True
242
+
243
+ def compose(self):
244
+ with Horizontal():
245
+ yield Static(self.tab.favicon, classes="browser-tab-favicon")
246
+ yield Static(self.tab.display_title(), classes="browser-tab-title")
247
+ yield Button("×", classes="browser-tab-close", id=f"tab-close-{self.tab.id}")
248
+
249
+ def on_mount(self):
250
+ self.tooltip = self.tab.url
251
+
252
+ def on_click(self, event):
253
+ event.stop()
254
+ self.post_message(BrowserSwitchTab(self.tab.id))
255
+
256
+ def on_button_pressed(self, event):
257
+ event.stop()
258
+ if event.button.id and event.button.id.startswith("tab-close-"):
259
+ tab_id = event.button.id.replace("tab-close-", "")
260
+ self.post_message(BrowserCloseTab(tab_id))
261
+
262
+ class TabBar(Horizontal):
263
+ def __init__(self, **kwargs):
264
+ super().__init__(id="browser-tabbar", **kwargs)
265
+
266
+ def compose(self):
267
+ state = get_state()
268
+ for tab in state.tabs:
269
+ yield TabWidget(tab, active=(tab.id == state.active_tab_id))
270
+ yield Button("+", id="browser-newtab-btn")
271
+
272
+ def refresh_tabs(self):
273
+ for child in list(self.children):
274
+ child.remove()
275
+ state = get_state()
276
+ for tab in state.tabs:
277
+ self.mount(TabWidget(tab, active=(tab.id == state.active_tab_id)))
278
+ self.mount(Button("+", id="browser-newtab-btn"))
279
+
280
+ def on_button_pressed(self, event):
281
+ if event.button.id == "browser-newtab-btn":
282
+ self.post_message(BrowserNewTab())
283
+
284
+ class AddressBar(Horizontal):
285
+ def __init__(self, **kwargs):
286
+ super().__init__(id="browser-toolbar", **kwargs)
287
+
288
+ def compose(self):
289
+ state = get_state()
290
+ tab = state.active_tab
291
+ can_back = tab.history_index > 0 if tab.history else False
292
+ can_fwd = 0 <= tab.history_index < len(tab.history)-1 if tab.history else False
293
+ yield Button("◀", id="browser-back", classes="browser-nav-btn", disabled=not can_back)
294
+ yield Button("▶", id="browser-forward", classes="browser-nav-btn", disabled=not can_fwd)
295
+ yield Button("⟳", id="browser-reload", classes="browser-nav-btn")
296
+ with Horizontal(id="browser-address-wrap"):
297
+ is_secure = tab.url.startswith("https://")
298
+ yield Static("🔒" if is_secure else "○", id="browser-lock", classes="" if is_secure else "-insecure")
299
+ yield Input(value=tab.url if tab.url != "about:home" else "", placeholder="Search or enter address", id="browser-address")
300
+ yield Button("⋮", id="browser-menu-btn", classes="browser-toolbar-btn")
301
+
302
+ def on_button_pressed(self, event):
303
+ bid = event.button.id
304
+ if bid == "browser-back":
305
+ self.post_message(BrowserBack())
306
+ elif bid == "browser-forward":
307
+ self.post_message(BrowserForward())
308
+ elif bid == "browser-reload":
309
+ self.post_message(BrowserReload())
310
+ elif bid == "browser-menu-btn":
311
+ self.post_message(BrowserMenuToggle())
312
+
313
+ def on_input_submitted(self, event):
314
+ if event.input.id == "browser-address":
315
+ self.post_message(BrowserNavigate(event.value))
316
+
317
+ class BrowserViewport(VerticalScroll):
318
+ """Viewport: single content area, updated via show_* methods."""
319
+ def __init__(self, **kwargs):
320
+ super().__init__(id="browser-viewport", **kwargs)
321
+ self._current_mode = "newtab"
322
+
323
+ def compose(self):
324
+ yield Static(RichText.from_markup(_NEWTAB_HTML), id="browser-content")
325
+
326
+ def show_newtab(self):
327
+ self._set_content(RichText.from_markup(_NEWTAB_HTML))
328
+
329
+ def show_loading(self, url: str):
330
+ txt = RichText()
331
+ txt.append(f"\n ⟳ Loading {url[:70]}…\n", style="bold yellow")
332
+ txt.append(" Please wait…\n", style="dim")
333
+ self._set_content(txt)
334
+
335
+ def show_page(self, title: str, url: str, content: str = "", links=None, ansi_image: str = None):
336
+ if url == "about:home":
337
+ self.show_newtab()
338
+ return
339
+ if content.startswith("ERROR:"):
340
+ self.show_error(content[6:])
341
+ return
342
+ txt = RichText()
343
+ if ansi_image:
344
+ try:
345
+ ansi_rt = RichText.from_ansi(ansi_image)
346
+ txt.append_text(ansi_rt)
347
+ except Exception:
348
+ txt.append(ansi_image[:2000])
349
+ else:
350
+ safe_title = title[:80] if title else "Untitled"
351
+ safe_url = url[:80]
352
+ body = content[:2000]
353
+ txt.append(f"{safe_title}\n", style="bold")
354
+ txt.append(f"{safe_url}\n", style="dim")
355
+ txt.append("─" * 40 + "\n", style="dim")
356
+ txt.append(body)
357
+ if links:
358
+ txt.append("\n\n── Links ──\n", style="bold")
359
+ for idx, (href, text) in enumerate(links[:12]):
360
+ t = (text[:40] + "…") if len(text) > 40 else text
361
+ h = (href[:50] + "…") if len(href) > 50 else href
362
+ txt.append(f" [{idx+1}] {t} — {h}\n", style="cyan")
363
+ self._set_content(txt)
364
+ self.scroll_home(animate=False)
365
+
366
+ def show_error(self, msg: str):
367
+ txt = RichText()
368
+ txt.append("\n ⚠ Can't reach this page\n\n", style="bold red")
369
+ txt.append(f" {msg[:200]}\n\n", style="dim")
370
+ txt.append(" [Press Ctrl+R to reload or ⌂ for home]\n", style="cyan")
371
+ self._set_content(txt)
372
+
373
+ def _set_content(self, content):
374
+ """Replace the viewport content widget."""
375
+ try:
376
+ w = self.query_one("#browser-content")
377
+ if isinstance(content, str):
378
+ w.update(RichText.from_markup(content))
379
+ else:
380
+ w.update(content)
381
+ except Exception:
382
+ pass
383
+
384
+ # -------------------------------------------------------------------
385
+ # Messages
386
+ # -------------------------------------------------------------------
387
+ class BrowserNavigate(Message):
388
+ def __init__(self, url: str): super().__init__(); self.url = url
389
+ class BrowserBack(Message): pass
390
+ class BrowserForward(Message): pass
391
+ class BrowserReload(Message): pass
392
+ class BrowserHome(Message): pass
393
+ class BrowserNewTab(Message): pass
394
+ class BrowserCloseTab(Message):
395
+ def __init__(self, tab_id: str): super().__init__(); self.tab_id = tab_id
396
+ class BrowserSwitchTab(Message):
397
+ def __init__(self, tab_id: str): super().__init__(); self.tab_id = tab_id
398
+ class BrowserBookmark(Message): pass
399
+ class BrowserMenuToggle(Message): pass
400
+
401
+ # -------------------------------------------------------------------
402
+ # Browser Shell
403
+ # -------------------------------------------------------------------
404
+ class BrowserShell(Vertical):
405
+ def __init__(self, start_url: str = "about:home", **kwargs):
406
+ super().__init__(id="cat-browser", **kwargs)
407
+ self._start_url = start_url
408
+ self._engine: Optional[BrowserEngine] = None
409
+ self._loading_url: Optional[str] = None
410
+
411
+ def compose(self):
412
+ yield TabBar()
413
+ yield AddressBar()
414
+ yield BrowserViewport()
415
+
416
+ def on_mount(self):
417
+ state = get_state()
418
+ tab = state.active_tab
419
+ if self._start_url and self._start_url != "about:home":
420
+ self.navigate(self._start_url)
421
+ elif tab.url and tab.url != "about:home":
422
+ self.navigate(tab.url)
423
+ else:
424
+ self.query_one(BrowserViewport).show_newtab()
425
+
426
+ def navigate(self, raw_url: str, is_reload: bool = False):
427
+ url = raw_url.strip()
428
+ if not url:
429
+ return
430
+ has_scheme = bool(re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", url))
431
+ is_about = url.lower().startswith("about:")
432
+ is_localhost = "localhost" in url.lower()
433
+
434
+ def is_search(q):
435
+ if has_scheme or is_about or is_localhost:
436
+ return False
437
+ if " " in q.strip():
438
+ return True
439
+ first = q.split()[0] if q else ""
440
+ if "." not in first and "/" not in q:
441
+ if ":" in first:
442
+ return False
443
+ return True
444
+ return False
445
+
446
+ if is_search(url):
447
+ q = urllib.parse.quote_plus(url)
448
+ url = f"https://www.google.com/search?q={q}&hl=en"
449
+ elif not has_scheme and not is_about:
450
+ if "." in url.split("/")[0]:
451
+ url = "https://" + url
452
+
453
+ state = get_state()
454
+ tab = state.active_tab
455
+ should_push = not is_reload and (not tab.history or tab.history[-1] != url)
456
+ if should_push:
457
+ if tab.history_index < len(tab.history)-1:
458
+ tab.history = tab.history[:tab.history_index+1]
459
+ tab.history.append(url)
460
+ tab.history_index = len(tab.history)-1
461
+ tab.url = url
462
+ tab.loading = True
463
+ tab.title = "Loading…"
464
+ if should_push:
465
+ state.update_active(url, "Loading…", loading=True)
466
+ else:
467
+ try:
468
+ tab.favicon = "G" if "google" in url.lower() else ("○" if not url.startswith("https://") else "🔒")
469
+ except Exception:
470
+ pass
471
+ self._refresh_chrome()
472
+ vp = self.query_one(BrowserViewport)
473
+ vp.show_loading(url)
474
+ self._load_url(url)
475
+
476
+ @work(thread=True, exclusive=True, group="browser-load")
477
+ def _load_url(self, url: str):
478
+ content = ""
479
+ title = ""
480
+ ansi_image = None
481
+ links = None
482
+ success = False
483
+ try:
484
+ if url in ("about:home", "about:blank"):
485
+ try:
486
+ self.app.call_from_thread(self._on_load_done, url, "New Tab", "", True, None, None)
487
+ except Exception:
488
+ pass
489
+ return
490
+ if PLAYWRIGHT_AVAILABLE:
491
+ try:
492
+ if self._engine is None:
493
+ from ..browser.engine import BrowserEngine
494
+ eng = BrowserEngine(headless=True)
495
+ eng.start(timeout=10)
496
+ self._engine = eng
497
+ snap = self._engine.navigate(url, timeout=20)
498
+ if snap and snap.ok:
499
+ title = snap.title or url
500
+ lines = []
501
+ if snap.title:
502
+ lines.append(snap.title)
503
+ for ln in snap.outline_lines[:60]:
504
+ lines.append(ln)
505
+ if snap.console_tail:
506
+ lines.append("Console: " + " | ".join(snap.console_tail[:2]))
507
+ content = "\n".join(lines) or "Loaded."
508
+ try:
509
+ import tempfile
510
+ from pathlib import Path as _Path
511
+ from ..cat_browser import render_image_to_ansi
512
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tf:
513
+ tmp = tf.name
514
+ ok = self._engine.screenshot(tmp)
515
+ if ok:
516
+ data = _Path(tmp).read_bytes()
517
+ term_w = 80
518
+ try:
519
+ if hasattr(self, 'size') and self.size.width:
520
+ term_w = max(60, min(120, self.size.width))
521
+ except Exception:
522
+ pass
523
+ ansi_image = render_image_to_ansi(data, term_width=term_w, max_height=28)
524
+ try:
525
+ _Path(tmp).unlink(missing_ok=True)
526
+ except Exception:
527
+ pass
528
+ except Exception:
529
+ ansi_image = None
530
+ success = True
531
+ else:
532
+ content = snap.error if snap else "Navigation failed"
533
+ title = "Error"
534
+ success = False
535
+ except Exception as e:
536
+ content = f"Chromium error: {e}"
537
+ title = "Error"
538
+ success = False
539
+ else:
540
+ content = "Playwright/Chromium not available. Install with: pip install playwright && playwright install chromium"
541
+ title = "Error"
542
+ success = False
543
+ except Exception as e:
544
+ content = f"Load error: {e}"
545
+ title = "Error"
546
+ success = False
547
+ try:
548
+ self.app.call_from_thread(self._on_load_done, url, title, content, success, links, ansi_image)
549
+ except Exception:
550
+ try:
551
+ self.app.call_from_thread(self._on_load_done, url, title, content, success, links, ansi_image)
552
+ except Exception:
553
+ pass
554
+
555
+ def _on_load_done(self, url: str, title: str, content: str, success: bool, links=None, ansi_image=None):
556
+ state = get_state()
557
+ tab = state.active_tab
558
+ tab.loading = False
559
+ tab.title = title[:40] if title else "Untitled"
560
+ tab.url = url
561
+ if success and url not in ("about:home", "about:blank"):
562
+ try:
563
+ if state.history and state.history[-1].url == url:
564
+ state.history[-1].title = tab.title
565
+ except Exception:
566
+ pass
567
+ self._refresh_chrome()
568
+ vp = self.query_one(BrowserViewport)
569
+ if not success:
570
+ vp.show_error(content.replace("ERROR:", ""))
571
+ else:
572
+ vp.show_page(title, url, content, links, ansi_image)
573
+
574
+ def _refresh_chrome(self):
575
+ try:
576
+ self.query_one(TabBar).refresh_tabs()
577
+ except Exception:
578
+ pass
579
+ try:
580
+ toolbar = self.query_one(AddressBar)
581
+ state = get_state()
582
+ tab = state.active_tab
583
+ inp = toolbar.query_one("#browser-address", Input)
584
+ inp.value = tab.url if tab.url != "about:home" else ""
585
+ try:
586
+ back = toolbar.query_one("#browser-back", Button)
587
+ back.disabled = not (tab.history_index > 0)
588
+ fwd = toolbar.query_one("#browser-forward", Button)
589
+ fwd.disabled = not (0 <= tab.history_index < len(tab.history)-1)
590
+ except Exception:
591
+ pass
592
+ except Exception:
593
+ pass
594
+
595
+ def _load_current_tab(self, url: str):
596
+ state = get_state()
597
+ tab = state.active_tab
598
+ tab.url = url
599
+ tab.loading = True
600
+ tab.title = "Loading…"
601
+ self._refresh_chrome()
602
+ self.query_one(BrowserViewport).show_loading(url)
603
+ self._load_url(url)
604
+
605
+ def on_browser_navigate(self, msg: BrowserNavigate):
606
+ self.navigate(msg.url)
607
+
608
+ def on_browser_switch_tab(self, msg: BrowserSwitchTab):
609
+ state = get_state()
610
+ state.switch_tab(msg.tab_id)
611
+ self._refresh_chrome()
612
+ tab = state.active_tab
613
+ vp = self.query_one(BrowserViewport)
614
+ if tab.url == "about:home":
615
+ vp.show_newtab()
616
+ else:
617
+ if not tab.title or tab.title == "Loading…":
618
+ self._load_current_tab(tab.url)
619
+ else:
620
+ vp.show_page(tab.title, tab.url, f"Switched to {tab.title}")
621
+
622
+ def on_browser_back(self, msg: BrowserBack):
623
+ state = get_state()
624
+ url = state.go_back()
625
+ if url:
626
+ self._load_current_tab(url)
627
+
628
+ def on_browser_forward(self, msg: BrowserForward):
629
+ state = get_state()
630
+ url = state.go_forward()
631
+ if url:
632
+ self._load_current_tab(url)
633
+
634
+ def on_browser_reload(self, msg: BrowserReload):
635
+ state = get_state()
636
+ url = state.active_tab.url
637
+ if url and url != "about:home":
638
+ self._load_current_tab(url)
639
+
640
+ def on_browser_new_tab(self, msg: BrowserNewTab):
641
+ state = get_state()
642
+ state.new_tab()
643
+ self._refresh_chrome()
644
+ self.query_one(BrowserViewport).show_newtab()
645
+ try:
646
+ self.query_one("#browser-address", Input).focus()
647
+ except Exception:
648
+ pass
649
+
650
+ def on_browser_close_tab(self, msg: BrowserCloseTab):
651
+ state = get_state()
652
+ state.close_tab(msg.tab_id)
653
+ self._refresh_chrome()
654
+ tab = state.active_tab
655
+ if tab.url == "about:home":
656
+ self.query_one(BrowserViewport).show_newtab()
657
+ else:
658
+ self._load_current_tab(tab.url)
659
+
660
+ def on_browser_bookmark(self, msg: BrowserBookmark):
661
+ state = get_state()
662
+ tab = state.active_tab
663
+ if state.is_bookmarked(tab.url):
664
+ bm = next((b for b in state.bookmarks if b.url == tab.url), None)
665
+ if bm:
666
+ state.remove_bookmark(bm.id)
667
+ else:
668
+ state.add_bookmark(tab.title, tab.url)
669
+ self._refresh_chrome()
670
+
671
+ def on_browser_menu_toggle(self, msg: BrowserMenuToggle):
672
+ try:
673
+ self.app.push_screen(BrowserMenu())
674
+ except Exception:
675
+ pass
676
+
677
+ def on_button_pressed(self, event):
678
+ bid = event.button.id or ""
679
+ if bid == "browser-error-reload":
680
+ self.on_browser_reload(BrowserReload())
681
+ elif bid == "browser-error-home":
682
+ self.on_browser_home(BrowserHome())
683
+
684
+ class BrowserMenu(ModalScreen):
685
+ def compose(self):
686
+ with Vertical(id="browser-menu"):
687
+ for label, action in [
688
+ ("New Tab", "newtab"),
689
+ ("History", "history"),
690
+ ("Bookmarks", "bookmarks"),
691
+ ("Downloads", "downloads"),
692
+ ("Sign Out", "signout"),
693
+ ("Developer Tools", "devtools"),
694
+ ]:
695
+ yield Button(label, id=f"menu-{action}", classes="browser-menu-item")
696
+ yield Static("─", classes="browser-menu-sep")
697
+ yield Button("Close", id="menu-close", classes="browser-menu-item")
698
+
699
+ def on_button_pressed(self, event):
700
+ bid = event.button.id or ""
701
+ act = bid.replace("menu-", "")
702
+ self.dismiss(None)
703
+ if act == "newtab":
704
+ try:
705
+ self.app.query_one(BrowserShell).on_browser_new_tab(BrowserNewTab())
706
+ except Exception:
707
+ pass
708
+ elif act == "history":
709
+ self.app.push_screen(HistoryScreen())
710
+ elif act == "bookmarks":
711
+ self.app.push_screen(BookmarksScreen())
712
+ elif act == "signout":
713
+ try:
714
+ from ..fomoji_auth import logout
715
+ logout()
716
+ except Exception:
717
+ pass
718
+
719
+ class HistoryScreen(ModalScreen):
720
+ def compose(self):
721
+ state = get_state()
722
+ groups = state.history_grouped()
723
+ with Vertical(classes="browser-panel"):
724
+ yield Static("History", classes="browser-panel-title")
725
+ with VerticalScroll():
726
+ for gname in ("Today", "Yesterday", "Earlier"):
727
+ entries = groups.get(gname, [])
728
+ if not entries:
729
+ continue
730
+ yield Static(gname, style="bold dim")
731
+ for h in entries[:40]:
732
+ with Horizontal(classes="browser-row"):
733
+ yield Static(h.favicon, classes="browser-row-favicon")
734
+ yield Static(h.title[:36], classes="browser-row-title")
735
+ yield Static(h.url[:28], classes="browser-row-url")
736
+ yield Static(time.strftime("%H:%M", time.localtime(h.timestamp)), classes="browser-row-time")
737
+ yield Button("Close", id="history-close")
738
+
739
+ def on_button_pressed(self, event):
740
+ if event.button.id == "history-close":
741
+ self.dismiss(None)
742
+
743
+ class BookmarksScreen(ModalScreen):
744
+ def compose(self):
745
+ state = get_state()
746
+ with Vertical(classes="browser-panel"):
747
+ yield Static("Bookmarks", classes="browser-panel-title")
748
+ with VerticalScroll():
749
+ if not state.bookmarks:
750
+ yield Static("No bookmarks yet.", style="dim")
751
+ for bm in state.bookmarks:
752
+ with Horizontal(classes="browser-row"):
753
+ yield Static(bm.favicon, classes="browser-row-favicon")
754
+ yield Static(bm.title[:36], classes="browser-row-title")
755
+ yield Static(bm.url[:36], classes="browser-row-url")
756
+ yield Button("Open", classes="browser-row-action", id=f"bm-open-{bm.id}")
757
+ yield Button("×", classes="browser-row-action", id=f"bm-del-{bm.id}")
758
+ yield Button("Close", id="bookmarks-close")
759
+
760
+ def on_button_pressed(self, event):
761
+ bid = event.button.id or ""
762
+ if bid == "bookmarks-close":
763
+ self.dismiss(None)
764
+ elif bid.startswith("bm-open-"):
765
+ bm_id = bid.replace("bm-open-", "")
766
+ state = get_state()
767
+ bm = next((b for b in state.bookmarks if b.id == bm_id), None)
768
+ if bm:
769
+ self.dismiss(None)
770
+ try:
771
+ self.app.query_one(BrowserShell).navigate(bm.url)
772
+ except Exception:
773
+ pass
774
+ elif bid.startswith("bm-del-"):
775
+ bm_id = bid.replace("bm-del-", "")
776
+ get_state().remove_bookmark(bm_id)
777
+ self.dismiss(None)
778
+ self.app.push_screen(BookmarksScreen())
779
+
780
+ class DownloadsScreen(ModalScreen):
781
+ def compose(self):
782
+ state = get_state()
783
+ with Vertical(classes="browser-panel"):
784
+ yield Static("Downloads", classes="browser-panel-title")
785
+ with VerticalScroll():
786
+ if not state.downloads:
787
+ yield Static("No downloads yet.", style="dim")
788
+ for d in state.downloads:
789
+ with Horizontal(classes="browser-row"):
790
+ yield Static("⬇", classes="browser-row-favicon")
791
+ yield Static(d.filename[:36], classes="browser-row-title")
792
+ yield Static(f"{d.progress}%", classes="browser-row-time")
793
+ yield Static(d.status, classes="browser-row-url")
794
+ yield Button("Close", id="downloads-close")
795
+
796
+ def on_button_pressed(self, event):
797
+ if event.button.id == "downloads-close":
798
+ self.dismiss(None)
799
+
800
+ class DevToolsScreen(ModalScreen):
801
+ def compose(self):
802
+ with Vertical(classes="browser-panel"):
803
+ yield Static("Developer Tools", classes="browser-panel-title")
804
+ with VerticalScroll():
805
+ yield Static("Console", style="bold dim")
806
+ yield Static("No errors.", style="dim")
807
+ yield Static("Network", style="bold dim")
808
+ yield Static("Requests appear here.", style="dim")
809
+ yield Button("Close", id="devtools-close")
810
+
811
+ def on_button_pressed(self, event):
812
+ if event.button.id == "devtools-close":
813
+ self.dismiss(None)
814
+
815
+ class BrowserScreen(Screen):
816
+ """Full-screen browser that takes over CAT workspace."""
817
+ BINDINGS = [
818
+ Binding("ctrl+l", "focus_address", "Focus address"),
819
+ Binding("ctrl+t", "new_tab", "New tab"),
820
+ Binding("ctrl+w", "close_tab", "Close tab"),
821
+ Binding("ctrl+r", "reload", "Reload"),
822
+ Binding("ctrl+shift+t", "reopen_tab", "Reopen"),
823
+ Binding("alt+left", "back", "Back"),
824
+ Binding("alt+right", "forward", "Forward"),
825
+ Binding("f11", "fullscreen", "Fullscreen"),
826
+ Binding("escape", "close_browser", "Close browser"),
827
+ ]
828
+
829
+ def __init__(self, start_url: str = "about:home"):
830
+ super().__init__()
831
+ self._start_url = start_url
832
+
833
+ def on_mount(self):
834
+ try:
835
+ from ..host.launcher import launch_cat_host, can_launch_host
836
+ if can_launch_host():
837
+ launch_cat_host(start_browser_url=self._start_url or "about:home", start_mode="browser", block=False)
838
+ except Exception:
839
+ pass
840
+ self.dismiss(None)
841
+
842
+ def action_focus_address(self):
843
+ pass
844
+
845
+ def action_new_tab(self):
846
+ pass
847
+
848
+ def action_close_tab(self):
849
+ pass
850
+
851
+ def action_reload(self):
852
+ pass
853
+
854
+ def action_reopen_tab(self):
855
+ pass
856
+
857
+ def action_back(self):
858
+ pass
859
+
860
+ def action_forward(self):
861
+ pass
862
+
863
+ def action_fullscreen(self):
864
+ pass
865
+
866
+ def action_close_browser(self):
867
+ self.dismiss(None)
868
+
869
+ else:
870
+ BrowserShell = None
871
+ BrowserScreen = None
872
+ get_state = lambda: None
873
+ BROWSER_CSS = ""
874
+
875
+ # ---------------------------------------------------------------------------
876
+ # Standalone launcher
877
+ # ---------------------------------------------------------------------------
878
+ def launch_browser_app(start_url: str = "about:home") -> int:
879
+ """Launch CAT Browser app (real Chromium / QWebEngineView)."""
880
+ try:
881
+ from ..host.launcher import launch_cat_host, can_launch_host
882
+ if can_launch_host():
883
+ return launch_cat_host(start_browser_url=start_url, start_mode="browser", block=True)
884
+ except Exception:
885
+ pass
886
+ print("CAT Browser requires PySide6. Install with: pip install PySide6")
887
+ return 1