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,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
|