stupidpythonbrowser 4.7.2__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.
pybrowser/browser.py ADDED
@@ -0,0 +1,3175 @@
1
+ """
2
+ PyBrowser: Native macOS Chromium/Material 3 Web Browser (PyObjC & WebKit).
3
+ Main Browser Module - Part 1: Views, Delegates, Coordinators & Utilities.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import time
10
+ import uuid
11
+ import re
12
+ import ast
13
+ import operator
14
+ import threading
15
+ import ctypes
16
+ import base64
17
+ import urllib.parse
18
+ import urllib.request
19
+ import sqlite3
20
+ import traceback
21
+ import tempfile
22
+ import struct
23
+ import zipfile
24
+ import io
25
+ import html
26
+ from dataclasses import dataclass, asdict
27
+ from ctypes import c_void_p, c_uint32, c_char_p, POINTER, byref, string_at
28
+
29
+ import Cocoa
30
+ import Foundation
31
+ import WebKit
32
+ import objc
33
+
34
+ from pybrowser.settings import (
35
+ SETTINGS, UA_PRESETS, DEFAULT_URL, DATA_DIR, SESSION_FILE, BOOKMARKS_FILE,
36
+ SETTINGS_FILE, HISTORY_FILE, ADBLOCK_CACHE_FILE, DNS_CACHE_FILE, USERSCRIPTS_DIR,
37
+ FILTER_LIST_URL, LOCAL_PROXY_PORT, BASE_ADBLOCK_DOMAINS, M3_COLORS, GROUP_COLORS,
38
+ validate_and_get_download_dir, get_settings_html, sanitize_suggested_filename
39
+ )
40
+ from pybrowser.doh_server import (
41
+ DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, BASE_ADBLOCK_DOMAINS, ADBLOCK_LOCK,
42
+ WK_RULE_LIST, is_domain_blocked, search_blocked_domains, load_dns_cache,
43
+ start_proxy_server, create_nw_proxy_config, load_adblock_filters_from_disk,
44
+ compile_webkit_adblock_rules, update_adblock_filters_from_web
45
+ )
46
+ from pybrowser.incognito import create_incognito_data_store, apply_incognito_preferences
47
+ from pybrowser.terminal import TERMINAL_MANAGER, dispatch_to_main
48
+ from pybrowser.task_manager import ProcessManagerController
49
+
50
+ # ============================================================================
51
+ # UTILITIES, DRAWING & KEYCHAIN
52
+ # ============================================================================
53
+ def hex_to_nscolor(hex_str):
54
+ if not hex_str:
55
+ return Cocoa.NSColor.clearColor()
56
+ if hex_str.startswith("rgba"):
57
+ parts = hex_str[5:-1].split(",")
58
+ return Cocoa.NSColor.colorWithSRGBRed_green_blue_alpha_(
59
+ float(parts[0]) / 255.0, float(parts[1]) / 255.0, float(parts[2]) / 255.0, float(parts[3])
60
+ )
61
+ clean_hex = hex_str.lstrip('#')
62
+ if len(clean_hex) == 6:
63
+ r, g, b = tuple(int(clean_hex[i:i+2], 16) for i in (0, 2, 4))
64
+ return Cocoa.NSColor.colorWithSRGBRed_green_blue_alpha_(r / 255.0, g / 255.0, b / 255.0, 1.0)
65
+ elif len(clean_hex) == 8:
66
+ r, g, b, a = tuple(int(clean_hex[i:i+2], 16) for i in (0, 2, 4, 6))
67
+ return Cocoa.NSColor.colorWithSRGBRed_green_blue_alpha_(r / 255.0, g / 255.0, b / 255.0, a / 255.0)
68
+ return Cocoa.NSColor.labelColor()
69
+
70
+ def get_m3_color(token, view, is_incognito=False):
71
+ if is_incognito and token == "tabstrip-bg":
72
+ token = "tabstrip-incognito"
73
+ elif is_incognito and token == "toolbar-bg":
74
+ token = "toolbar-incognito"
75
+ theme = SETTINGS.settings.get("theme_mode", "system")
76
+ is_dark = is_incognito or (theme == "dark")
77
+ if not is_dark and theme == "system" and view and hasattr(view, "effectiveAppearance") and view.effectiveAppearance():
78
+ try:
79
+ is_dark = "Dark" in str(view.effectiveAppearance().name())
80
+ except Exception:
81
+ pass
82
+ colors = M3_COLORS.get(token, ("#FFFFFF", "#000000"))
83
+ return hex_to_nscolor(colors[1 if is_dark else 0])
84
+
85
+ def atomic_write_json(filepath, data):
86
+ try:
87
+ dir_name = os.path.dirname(filepath) or "."
88
+ fd, temp_path = tempfile.mkstemp(dir=dir_name)
89
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
90
+ json.dump(data, f, indent=2)
91
+ os.replace(temp_path, filepath)
92
+ except Exception as e:
93
+ print(f"[Atomic JSON Write Error] {e}")
94
+
95
+ def download_image_data(img_url, dest_path, webview=None, completion_callback=None):
96
+ if not img_url:
97
+ if completion_callback: completion_callback(False)
98
+ return False
99
+ if img_url.startswith("data:"):
100
+ try:
101
+ raw = base64.b64decode(img_url.split(",", 1)[1])
102
+ with open(dest_path, "wb") as f:
103
+ f.write(raw)
104
+ if completion_callback: completion_callback(True)
105
+ return True
106
+ except Exception:
107
+ if completion_callback: completion_callback(False)
108
+ return False
109
+ if img_url.startswith("blob:"):
110
+ if webview:
111
+ dest_json = json.dumps(dest_path)
112
+ js = f"""(async () => {{
113
+ try {{
114
+ const res = await fetch("{img_url}");
115
+ const blob = await res.blob();
116
+ const reader = new FileReader();
117
+ reader.onloadend = () => {{
118
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'blob_image_data', data: reader.result, path: {dest_json}}});
119
+ }};
120
+ reader.readAsDataURL(blob);
121
+ }} catch(e) {{
122
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'image_download_failed', path: {dest_json}}});
123
+ }}
124
+ }})();"""
125
+ dispatch_to_main(lambda: webview.evaluateJavaScript_completionHandler_(js, None))
126
+ return True
127
+ if completion_callback: completion_callback(False)
128
+ return False
129
+
130
+ def worker():
131
+ success = False
132
+ try:
133
+ referer = webview.URL().absoluteString() if (webview and webview.URL()) else img_url
134
+ req = urllib.request.Request(img_url, headers={
135
+ "User-Agent": SETTINGS.get_user_agent(),
136
+ "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
137
+ "Referer": referer
138
+ })
139
+ with urllib.request.urlopen(req, timeout=15) as resp:
140
+ with open(dest_path, "wb") as f:
141
+ f.write(resp.read())
142
+ success = True
143
+ except Exception:
144
+ if webview:
145
+ dest_json = json.dumps(dest_path)
146
+ js = f"""(async () => {{
147
+ try {{
148
+ const res = await fetch("{img_url}", {{ credentials: 'include' }});
149
+ const blob = await res.blob();
150
+ const reader = new FileReader();
151
+ reader.onloadend = () => {{
152
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'blob_image_data', data: reader.result, path: {dest_json}}});
153
+ }};
154
+ reader.readAsDataURL(blob);
155
+ }} catch(err) {{
156
+ window.webkit.messageHandlers.bridge.postMessage({{cmd: 'image_download_failed', path: {dest_json}}});
157
+ }}
158
+ }})();"""
159
+ dispatch_to_main(lambda: webview.evaluateJavaScript_completionHandler_(js, None))
160
+ return
161
+ if completion_callback:
162
+ dispatch_to_main(lambda: completion_callback(success))
163
+
164
+ threading.Thread(target=worker, daemon=True).start()
165
+ return True
166
+
167
+ def safe_math_eval(expr):
168
+ expr = expr.strip()
169
+ if not expr or not re.search(r'[\+\-\*/%]', expr):
170
+ return None
171
+ expr = re.sub(r'([\d\.]+)\s*%\s*of\s*([\d\.]+)', r'(\1/100)*\2', expr)
172
+ if not re.match(r'^[\d\.\s\+\-\*/\(\)]+$', expr):
173
+ return None
174
+ try:
175
+ node = ast.parse(expr, mode='eval').body
176
+ def _eval(n):
177
+ if isinstance(n, (ast.Constant, ast.Num)):
178
+ return getattr(n, 'value', getattr(n, 'n', None))
179
+ elif isinstance(n, ast.BinOp):
180
+ op = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}[type(n.op)]
181
+ return op(_eval(n.left), _eval(n.right))
182
+ elif isinstance(n, ast.UnaryOp):
183
+ op = {ast.USub: operator.neg, ast.UAdd: operator.pos}[type(n.op)]
184
+ return op(_eval(n.operand))
185
+ raise ValueError()
186
+ res = _eval(node)
187
+ return str(int(res)) if (isinstance(res, float) and res.is_integer()) else str(round(res, 4))
188
+ except Exception:
189
+ return None
190
+
191
+ try:
192
+ sec = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/Security.framework/Security")
193
+ SecKeychainAddGenericPassword = sec.SecKeychainAddGenericPassword
194
+ SecKeychainAddGenericPassword.argtypes = [c_void_p, c_uint32, c_char_p, c_uint32, c_char_p, c_uint32, c_char_p, c_void_p]
195
+ SecKeychainFindGenericPassword = sec.SecKeychainFindGenericPassword
196
+ SecKeychainFindGenericPassword.argtypes = [c_void_p, c_uint32, c_char_p, c_uint32, c_char_p, POINTER(c_uint32), POINTER(c_void_p), POINTER(c_void_p)]
197
+ SecKeychainItemFreeContent = sec.SecKeychainItemFreeContent
198
+ SecKeychainItemDelete = sec.SecKeychainItemDelete
199
+ SecKeychainItemDelete.argtypes = [c_void_p]
200
+ except Exception:
201
+ pass
202
+
203
+ def save_to_keychain(host, username, password):
204
+ try:
205
+ srv, acc, pwd = host.encode('utf-8'), username.encode('utf-8'), password.encode('utf-8')
206
+ length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
207
+ if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
208
+ if data_ptr.value: SecKeychainItemFreeContent(None, data_ptr)
209
+ if item_ref.value: SecKeychainItemDelete(item_ref.value)
210
+ SecKeychainAddGenericPassword(None, len(srv), srv, len(acc), acc, len(pwd), pwd, None)
211
+ except Exception:
212
+ pass
213
+
214
+ def get_from_keychain(host, username):
215
+ try:
216
+ srv, acc = host.encode('utf-8'), username.encode('utf-8')
217
+ length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
218
+ if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
219
+ pwd = string_at(data_ptr.value, length.value).decode('utf-8')
220
+ SecKeychainItemFreeContent(None, data_ptr)
221
+ return pwd
222
+ except Exception:
223
+ pass
224
+ return None
225
+
226
+ # ============================================================================
227
+ # USERSCRIPTS & EXTENSIONS (CRX)
228
+ # ============================================================================
229
+ def parse_userscript_meta(content):
230
+ meta = {"name": "Unnamed Script", "match": [], "run-at": "document-end", "enabled": True}
231
+ in_meta = False
232
+ for line in content.splitlines():
233
+ line = line.strip()
234
+ if line == "// ==UserScript==":
235
+ in_meta = True
236
+ elif line == "// ==/UserScript==":
237
+ break
238
+ elif in_meta and line.startswith("// @"):
239
+ parts = line.split(maxsplit=2)
240
+ if len(parts) >= 3:
241
+ key, val = parts[1][1:], parts[2]
242
+ if key in ("match", "include"):
243
+ meta["match"].append(val)
244
+ else:
245
+ meta[key] = val
246
+ return meta
247
+
248
+ def get_installed_userscripts():
249
+ scripts = []
250
+ if not os.path.exists(USERSCRIPTS_DIR):
251
+ return scripts
252
+ for fname in os.listdir(USERSCRIPTS_DIR):
253
+ if fname.endswith(".user.js") or fname.endswith(".user.js.disabled"):
254
+ real_name = fname[:-9] if fname.endswith(".disabled") else fname
255
+ path = os.path.join(USERSCRIPTS_DIR, fname)
256
+ try:
257
+ with open(path, 'r', encoding='utf-8') as f:
258
+ content = f.read()
259
+ meta = parse_userscript_meta(content)
260
+ meta["file"] = real_name
261
+ meta["actual_path"] = path
262
+ meta["code"] = content
263
+ meta["enabled"] = not fname.endswith(".disabled")
264
+ scripts.append(meta)
265
+ except Exception:
266
+ pass
267
+ return scripts
268
+
269
+ def install_crx(ext_id_or_url):
270
+ try:
271
+ m = re.search(r'([a-z]{32})', ext_id_or_url.lower())
272
+ ext_id = m.group(1) if m else ext_id_or_url.split("=")[-1].split("/")[-1].split("?")[0].strip()
273
+ url = f"https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0&x=id%3D{ext_id}%26installsource%3Dondemand%26uc"
274
+ req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
275
+ with urllib.request.urlopen(req, timeout=12) as resp:
276
+ data = resp.read()
277
+ if len(data) < 16 or data[:4] != b'Cr24':
278
+ return False
279
+ version = struct.unpack("<I", data[4:8])[0]
280
+ if version == 3:
281
+ zip_data = data[12 + struct.unpack("<I", data[8:12])[0]:]
282
+ elif version == 2:
283
+ zip_data = data[16 + struct.unpack("<I", data[8:12])[0] + struct.unpack("<I", data[12:16])[0]:]
284
+ else:
285
+ return False
286
+
287
+ with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
288
+ manifest = json.loads(z.read("manifest.json").decode('utf-8', errors='ignore'))
289
+ name = manifest.get("name", ext_id)
290
+ content_scripts = manifest.get("content_scripts", [])
291
+
292
+ combined_js = f"// ==UserScript==\n// @name {name} (CRX)\n// @match <all_urls>\n// ==/UserScript==\n"
293
+ for cs in content_scripts:
294
+ matches, css_files, js_files = cs.get("matches", []), cs.get("css", []), cs.get("js", [])
295
+ match_regex = " || ".join([f"new RegExp('^' + '{m}'.replace(/\\*/g, '.*').replace(/\\//g, '\\\\/') + '$').test(location.href)" for m in matches]) or "true"
296
+ combined_js += f"if ({match_regex}) {{\n"
297
+ for css in css_files:
298
+ try:
299
+ css_content = z.read(css).decode('utf-8', errors='ignore').replace('`', '\\`').replace('$', '\\$')
300
+ combined_js += f"const st = document.createElement('style'); st.textContent = `{css_content}`; (document.head || document.documentElement).appendChild(st);\n"
301
+ except Exception:
302
+ pass
303
+ for jf in js_files:
304
+ try:
305
+ combined_js += z.read(jf).decode('utf-8', errors='ignore') + "\n"
306
+ except Exception:
307
+ pass
308
+ combined_js += "}\n"
309
+
310
+ with open(os.path.join(USERSCRIPTS_DIR, f"{ext_id}.user.js"), "w", encoding="utf-8") as f:
311
+ f.write(combined_js)
312
+
313
+ dispatch_to_main(reload_all_extensions_views)
314
+ return True
315
+ except Exception as e:
316
+ print(f"[CRX Install Error] {e}")
317
+ return False
318
+
319
+ def reload_all_extensions_views():
320
+ app_delegate = Cocoa.NSApp.delegate()
321
+ if not app_delegate:
322
+ return
323
+ for wc in getattr(app_delegate, "controllers", []):
324
+ for tab in wc.tabs:
325
+ wv = tab.get("webview")
326
+ if wv and wv.URL() and "browser://extensions" in wv.URL().absoluteString():
327
+ wv.reload()
328
+
329
+ # ============================================================================
330
+ # SQLITE HISTORY & FAVICONS
331
+ # ============================================================================
332
+ def init_history_db():
333
+ try:
334
+ with sqlite3.connect(HISTORY_FILE) as conn:
335
+ conn.execute("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
336
+ conn.execute("""
337
+ CREATE TABLE IF NOT EXISTS history (
338
+ id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, title TEXT,
339
+ visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, visit_count INTEGER DEFAULT 1
340
+ );
341
+ """)
342
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_history_time ON history(visit_time DESC);")
343
+ except Exception as e:
344
+ print(f"[History DB Error] {e}")
345
+
346
+ def record_history(url, title):
347
+ if not url or url.startswith("browser://"):
348
+ return
349
+ try:
350
+ with sqlite3.connect(HISTORY_FILE) as conn:
351
+ conn.execute("""
352
+ INSERT INTO history (url, title, visit_time, visit_count) VALUES (?, ?, CURRENT_TIMESTAMP, 1)
353
+ ON CONFLICT(url) DO UPDATE SET title = excluded.title, visit_time = CURRENT_TIMESTAMP, visit_count = visit_count + 1;
354
+ """, (url, title or url))
355
+ except Exception:
356
+ pass
357
+
358
+ def query_history(keyword=None):
359
+ try:
360
+ with sqlite3.connect(HISTORY_FILE) as conn:
361
+ cursor = conn.cursor()
362
+ if keyword:
363
+ cursor.execute(
364
+ "SELECT id, url, title, datetime(visit_time, 'localtime'), visit_count FROM history WHERE url LIKE ? OR title LIKE ? ORDER BY visit_time DESC LIMIT 100",
365
+ (f"%{keyword}%", f"%{keyword}%")
366
+ )
367
+ else:
368
+ cursor.execute("SELECT id, url, title, datetime(visit_time, 'localtime'), visit_count FROM history ORDER BY visit_time DESC LIMIT 100")
369
+ return cursor.fetchall()
370
+ except Exception:
371
+ return []
372
+
373
+ def clear_history():
374
+ try:
375
+ with sqlite3.connect(HISTORY_FILE) as conn:
376
+ conn.execute("DELETE FROM history;")
377
+ except Exception:
378
+ pass
379
+
380
+ class FaviconCache:
381
+ _cache = {}
382
+ _lock = threading.Lock()
383
+
384
+ @classmethod
385
+ def clear(cls):
386
+ with cls._lock:
387
+ cls._cache.clear()
388
+
389
+ @classmethod
390
+ def get_favicon(cls, host, callback):
391
+ if not host or host in ("localhost", "127.0.0.1", "::1"):
392
+ return
393
+ with cls._lock:
394
+ if host in cls._cache:
395
+ callback(cls._cache[host])
396
+ return
397
+ def fetcher():
398
+ try:
399
+ url = f"https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32"
400
+ req = urllib.request.Request(url, headers={"User-Agent": SETTINGS.get_user_agent()})
401
+ with urllib.request.urlopen(req, timeout=2.5) as resp:
402
+ raw = resp.read()
403
+ if raw:
404
+ img = Cocoa.NSImage.alloc().initWithData_(Foundation.NSData.dataWithBytes_length_(raw, len(raw)))
405
+ if img:
406
+ with cls._lock:
407
+ cls._cache[host] = img
408
+ dispatch_to_main(lambda: callback(img))
409
+ except Exception:
410
+ pass
411
+ threading.Thread(target=fetcher, daemon=True).start()
412
+
413
+ # ============================================================================
414
+ # INJECTED SCRIPTS & FULLSCREEN UTILITIES
415
+ # ============================================================================
416
+ AUDIO_MONITOR_JS = """
417
+ document.addEventListener('play', (e) => {
418
+ if (e.target && !e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
419
+ }, true);
420
+ document.addEventListener('pause', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
421
+ document.addEventListener('ended', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
422
+ document.addEventListener('volumechange', (e) => {
423
+ if (e.target && e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
424
+ }, true);
425
+ """
426
+
427
+ IMAGE_CONTEXT_MENU_JS = """
428
+ (function() {
429
+ document.addEventListener('contextmenu', function(e) {
430
+ let el = e.target, imgSrc = null;
431
+ let img = el.closest('img') || (el.tagName === 'IMG' ? el : null);
432
+ if (img) imgSrc = img.currentSrc || img.src;
433
+ else if (el.tagName === 'PICTURE') {
434
+ let i = el.querySelector('img'); if (i) imgSrc = i.currentSrc || i.src;
435
+ } else if (el.tagName === 'CANVAS') {
436
+ try { imgSrc = el.toDataURL('image/png'); } catch(_) {}
437
+ } else {
438
+ let bg = window.getComputedStyle(el).backgroundImage;
439
+ if (bg && bg.startsWith('url(')) imgSrc = bg.slice(4, -1).replace(/["']/g, "");
440
+ }
441
+ if (imgSrc && window.webkit?.messageHandlers?.bridge) {
442
+ window.webkit.messageHandlers.bridge.postMessage({cmd: 'context_target_image', src: imgSrc});
443
+ }
444
+ }, true);
445
+ })();
446
+ """
447
+
448
+ KEYCHAIN_AUTOSAVE_JS = """
449
+ document.addEventListener('submit', (e) => {
450
+ if (!e.target || !e.target.querySelector) return;
451
+ let pw = e.target.querySelector('input[type="password"]');
452
+ let user = e.target.querySelector('input[type="text"], input[type="email"]');
453
+ if (pw && pw.value) {
454
+ window.webkit.messageHandlers.bridge.postMessage({
455
+ cmd: 'prompt_save_password', host: location.hostname,
456
+ username: user ? user.value : 'unknown', password: pw.value
457
+ });
458
+ }
459
+ }, true);
460
+ window.addEventListener('load', () => {
461
+ if (location.hostname && window.webkit?.messageHandlers?.bridge) {
462
+ window.webkit.messageHandlers.bridge.postMessage({cmd: 'check_autofill', host: location.hostname});
463
+ }
464
+ });
465
+ """
466
+
467
+ COSMETIC_ADBLOCK_CSS = """
468
+ .ad, .ads, .advert, .advertising, .ad-banner, .ad-container,
469
+ .ad-slot, .ad-wrapper, .sponsored-post, [id^="google_ads_"],
470
+ [id^="div-gpt-ad"], ytd-ad-slot-renderer, .ytp-ad-module,
471
+ .ytp-ad-overlay-container, ytd-in-feed-ad-layout-renderer,
472
+ ytd-banner-promo-renderer, ytd-promoted-video-renderer,
473
+ ytd-ad-inline-playback-meta-block, #player-ads, #masthead-ad,
474
+ .taboola, .outbrain, .criteo-ad, [class*="sponsored"],
475
+ [data-ad-client], [data-ad-slot] {
476
+ display: none !important; visibility: hidden !important;
477
+ height: 0 !important; min-height: 0 !important; max-height: 0 !important;
478
+ pointer-events: none !important; opacity: 0 !important;
479
+ }
480
+ """
481
+
482
+ YOUTUBE_AD_SHIELD_JS = """
483
+ (function() {
484
+ if (!location.hostname.includes('youtube.com')) return;
485
+ const skipAds = () => {
486
+ const skipBtn = document.querySelector('.ytp-skip-ad-button, .ytp-ad-skip-button, .ytp-ad-skip-button-modern');
487
+ if (skipBtn) skipBtn.click();
488
+ const adShowing = document.querySelector('.ad-showing, .ad-interrupting');
489
+ const video = document.querySelector('video');
490
+ if (adShowing && video && !isNaN(video.duration)) {
491
+ video.muted = true; video.playbackRate = 16.0; video.currentTime = video.duration - 0.1;
492
+ }
493
+ const overlay = document.querySelector('.ytp-ad-overlay-close-button');
494
+ if (overlay) overlay.click();
495
+ const dismissBtn = document.querySelector('ytd-enforcement-message-view-model #dismiss-button');
496
+ if (dismissBtn) dismissBtn.click();
497
+ };
498
+ setInterval(skipAds, 150);
499
+ })();
500
+ """
501
+
502
+ IN_WINDOW_FULLSCREEN_JS = """
503
+ (function() {
504
+ let cur = null, origScrollY = 0;
505
+ function ensureStyle() {
506
+ if (!document.getElementById('__pybrowser_fs_style')) {
507
+ const st = document.createElement('style'); st.id = '__pybrowser_fs_style';
508
+ st.textContent = `.__in_window_fullscreen { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; max-width: 100vw !important; max-height: 100vh !important; margin: 0 !important; padding: 0 !important; border: 0 !important; z-index: 2147483647 !important; background: #000 !important; box-sizing: border-box !important; }`;
509
+ (document.head || document.documentElement).appendChild(st);
510
+ }
511
+ }
512
+ Object.defineProperty(document, 'fullscreenElement', { get: () => cur, configurable: true });
513
+ Object.defineProperty(document, 'webkitFullscreenElement', { get: () => cur, configurable: true });
514
+ Object.defineProperty(document, 'fullscreenEnabled', { get: () => true, configurable: true });
515
+ Object.defineProperty(document, 'webkitFullscreenEnabled', { get: () => true, configurable: true });
516
+
517
+ Element.prototype.requestFullscreen = function() {
518
+ ensureStyle();
519
+ if (cur && cur !== this) cur.classList.remove('__in_window_fullscreen');
520
+ cur = this; origScrollY = window.scrollY || window.pageYOffset || 0;
521
+ this.classList.add('__in_window_fullscreen');
522
+ document.dispatchEvent(new Event('fullscreenchange', { bubbles: true }));
523
+ document.dispatchEvent(new Event('webkitfullscreenchange', { bubbles: true }));
524
+ window.dispatchEvent(new Event('resize'));
525
+ if (window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd: 'html5_fullscreen_enter'});
526
+ return Promise.resolve();
527
+ };
528
+ Element.prototype.webkitRequestFullscreen = Element.prototype.requestFullscreen;
529
+
530
+ document.exitFullscreen = function() {
531
+ if (cur) {
532
+ const el = cur; cur = null;
533
+ el.classList.remove('__in_window_fullscreen');
534
+ document.dispatchEvent(new Event('fullscreenchange', { bubbles: true }));
535
+ document.dispatchEvent(new Event('webkitfullscreenchange', { bubbles: true }));
536
+ window.dispatchEvent(new Event('resize'));
537
+ window.scrollTo({ left: 0, top: origScrollY, behavior: 'instant' });
538
+ if (document.documentElement) document.documentElement.scrollLeft = 0;
539
+ if (document.body) document.body.scrollLeft = 0;
540
+ if (window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd: 'html5_fullscreen_exit'});
541
+ }
542
+ return Promise.resolve();
543
+ };
544
+ document.webkitExitFullscreen = document.exitFullscreen;
545
+ document.addEventListener('keydown', function(e) {
546
+ if ((e.key === 'Escape' || e.keyCode === 27) && cur) { e.preventDefault(); e.stopPropagation(); document.exitFullscreen(); }
547
+ }, true);
548
+ })();
549
+ """
550
+
551
+ REFLOW_RESTORE_JS = """
552
+ (function() {
553
+ if (document.documentElement) { document.documentElement.style.removeProperty('zoom'); document.documentElement.scrollLeft = 0; }
554
+ if (document.body) { document.body.style.removeProperty('zoom'); document.body.scrollLeft = 0; }
555
+ document.querySelectorAll('.__in_window_fullscreen').forEach(el => el.classList.remove('__in_window_fullscreen'));
556
+ if (window.scrollX !== 0) window.scrollTo(0, window.scrollY || 0);
557
+ window.dispatchEvent(new Event('resize'));
558
+ })();
559
+ """
560
+
561
+ ESCAPE_CHECK_JS = """
562
+ (function() {
563
+ if (document.fullscreenElement) { if (document.exitFullscreen) document.exitFullscreen(); return true; }
564
+ const fs = document.querySelector('.__in_window_fullscreen');
565
+ if (fs) { fs.classList.remove('__in_window_fullscreen'); window.dispatchEvent(new Event('resize')); return true; }
566
+ return false;
567
+ })();
568
+ """
569
+
570
+ # ============================================================================
571
+ # TAB GROUPS
572
+ # ============================================================================
573
+ @dataclass
574
+ class TabGroup:
575
+ id: str
576
+ title: str
577
+ color_name: str
578
+ collapsed: bool
579
+
580
+ class TabGroupHeaderView(Cocoa.NSView):
581
+ def initWithFrame_group_controller_(self, frame, group, controller):
582
+ self = objc.super(TabGroupHeaderView, self).initWithFrame_(frame)
583
+ if self:
584
+ self.group, self.controller = group, controller
585
+ return self
586
+
587
+ def drawRect_(self, dirtyRect):
588
+ bounds = self.bounds()
589
+ path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(Cocoa.NSMakeRect(2, 4, bounds.size.width - 4, 20), 4, 4)
590
+ hex_to_nscolor(GROUP_COLORS.get(self.group.color_name, "#5F6368")).setFill()
591
+ path.fill()
592
+ attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.whiteColor()}
593
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(self.group.title, attrs).drawAtPoint_(Cocoa.NSMakePoint(6, 6))
594
+
595
+ def mouseDown_(self, event):
596
+ self.group.collapsed = not self.group.collapsed
597
+ self.controller.relayout_tabs()
598
+
599
+ # ============================================================================
600
+ # TITLE BAR VIEW (DEDICATED TOP BAR FOR DRAGGING, EXPANDING & ZEN TRAFFIC LIGHTS)
601
+ # ============================================================================
602
+ class TitleBarView(Cocoa.NSView):
603
+ """
604
+ Dedicated compact title bar above tabs.
605
+ Hosts macOS traffic lights, provides window dragging, and supports double-click maximize.
606
+ Remains visible in Zen mode to keep traffic light buttons isolated from web content.
607
+ """
608
+ def initWithFrame_controller_(self, frame, controller):
609
+ self = objc.super(TitleBarView, self).initWithFrame_(frame)
610
+ if self:
611
+ self.controller = controller
612
+ return self
613
+
614
+ def drawRect_(self, dirtyRect):
615
+ is_inc = self.controller.is_incognito if self.controller else False
616
+ get_m3_color("tabstrip-bg", self, is_inc).setFill()
617
+ Cocoa.NSRectFill(self.bounds())
618
+
619
+ def mouseDown_(self, event):
620
+ if event.clickCount() == 2 and self.controller:
621
+ action = SETTINGS.settings.get("tab_bar_double_click_action", "maximize")
622
+ win = self.controller.window()
623
+ if win:
624
+ if action == "fullscreen":
625
+ win.toggleFullScreen_(None)
626
+ else:
627
+ win.zoom_(None)
628
+ return
629
+ if self.window():
630
+ self.window().performWindowDragWithEvent_(event)
631
+ else:
632
+ objc.super(TitleBarView, self).mouseDown_(event)
633
+
634
+ # ============================================================================
635
+ # MATERIAL 3 CHROME VIEWS & DELEGATES
636
+ # ============================================================================
637
+ class ChromeTabStripView(Cocoa.NSView):
638
+ def initWithFrame_(self, frame):
639
+ self = objc.super(ChromeTabStripView, self).initWithFrame_(frame)
640
+ if self:
641
+ self.controller = None
642
+ return self
643
+
644
+ def drawRect_(self, dirtyRect):
645
+ is_inc = self.controller.is_incognito if self.controller else False
646
+ get_m3_color("tabstrip-bg", self, is_inc).setFill()
647
+ Cocoa.NSRectFill(self.bounds())
648
+
649
+ def mouseDown_(self, event):
650
+ if event.clickCount() == 2 and self.controller:
651
+ action = SETTINGS.settings.get("tab_bar_double_click_action", "maximize")
652
+ win = self.controller.window()
653
+ if win:
654
+ if action == "fullscreen":
655
+ win.toggleFullScreen_(None)
656
+ else:
657
+ win.zoom_(None)
658
+ return
659
+ if self.window():
660
+ self.window().performWindowDragWithEvent_(event)
661
+ else:
662
+ objc.super(ChromeTabStripView, self).mouseDown_(event)
663
+
664
+ class ChromeTabView(Cocoa.NSView):
665
+ def initWithFrame_controller_index_(self, frame, controller, index):
666
+ self = objc.super(ChromeTabView, self).initWithFrame_(frame)
667
+ if self:
668
+ self.controller = controller
669
+ self.index = index
670
+ self.is_hovered = False
671
+ self.tracking_area = None
672
+ self.close_btn_rect = Cocoa.NSZeroRect
673
+ self.audio_btn_rect = Cocoa.NSZeroRect
674
+ return self
675
+
676
+ def updateTrackingAreas(self):
677
+ if self.tracking_area:
678
+ self.removeTrackingArea_(self.tracking_area)
679
+ opts = Cocoa.NSTrackingMouseEnteredAndExited | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
680
+ self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
681
+ self.addTrackingArea_(self.tracking_area)
682
+ objc.super(ChromeTabView, self).updateTrackingAreas()
683
+
684
+ def mouseEntered_(self, event):
685
+ self.is_hovered = True
686
+ self.setNeedsDisplay_(True)
687
+
688
+ def mouseExited_(self, event):
689
+ self.is_hovered = False
690
+ self.setNeedsDisplay_(True)
691
+
692
+ def drawRect_(self, dirtyRect):
693
+ if not (0 <= self.index < len(self.controller.tabs)):
694
+ return
695
+ tab = self.controller.tabs[self.index]
696
+ is_active = (self.index == self.controller.active_tab_index)
697
+ is_inc = self.controller.is_incognito
698
+ bounds = self.bounds()
699
+
700
+ if is_active:
701
+ path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(bounds, 6, 6)
702
+ get_m3_color("active-tab-bg", self, is_inc).setFill()
703
+ path.fill()
704
+ elif self.is_hovered:
705
+ path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(bounds, 6, 6)
706
+ get_m3_color("inactive-tab-hover", self, is_inc).setFill()
707
+ path.fill()
708
+
709
+ # Separator for inactive tabs
710
+ if not is_active and not self.is_hovered and bounds.size.width > 50:
711
+ sep = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(bounds.size.width - 1, 6, 1, bounds.size.height - 12))
712
+ get_m3_color("tab-divider", self, is_inc).setFill()
713
+ sep.fill()
714
+
715
+ # Favicon
716
+ icon_x = 8.0
717
+ icon_y = (bounds.size.height - 16) / 2.0
718
+ if tab.get("favicon"):
719
+ tab["favicon"].drawInRect_fromRect_operation_fraction_(
720
+ Cocoa.NSMakeRect(icon_x, icon_y, 16, 16), Cocoa.NSZeroRect, Cocoa.NSCompositeSourceOver, 1.0
721
+ )
722
+ else:
723
+ def_icon = "💤" if tab.get("is_sleeping") else "📄"
724
+ attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11)}
725
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(def_icon, attrs).drawAtPoint_(Cocoa.NSMakePoint(icon_x, icon_y - 1))
726
+
727
+ if tab.get("pinned", False):
728
+ return
729
+
730
+ # Audio indicator
731
+ has_audio = tab.get("has_audio", False)
732
+ is_muted = tab.get("is_muted", False)
733
+ right_offset = 24.0
734
+
735
+ if has_audio or is_muted:
736
+ audio_str = "🔇" if is_muted else "🔊"
737
+ self.audio_btn_rect = Cocoa.NSMakeRect(bounds.size.width - 40, (bounds.size.height - 16) / 2.0, 16, 16)
738
+ attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(10)}
739
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(audio_str, attrs).drawAtPoint_(Cocoa.NSMakePoint(self.audio_btn_rect.origin.x, self.audio_btn_rect.origin.y - 1))
740
+ right_offset = 42.0
741
+ else:
742
+ self.audio_btn_rect = Cocoa.NSZeroRect
743
+
744
+ # Close button
745
+ self.close_btn_rect = Cocoa.NSMakeRect(bounds.size.width - 20, (bounds.size.height - 14) / 2.0, 14, 14)
746
+ if self.is_hovered or is_active:
747
+ close_attrs = {
748
+ Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11),
749
+ Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.secondaryLabelColor()
750
+ }
751
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_("✕", close_attrs).drawAtPoint_(Cocoa.NSMakePoint(self.close_btn_rect.origin.x + 2, self.close_btn_rect.origin.y - 1))
752
+
753
+ # Tab title
754
+ title_x = icon_x + 20.0
755
+ max_title_w = max(10.0, bounds.size.width - title_x - right_offset)
756
+ title = tab.get("title", "New Tab")
757
+ text_color = Cocoa.NSColor.labelColor() if is_active else Cocoa.NSColor.secondaryLabelColor()
758
+ title_attrs = {
759
+ Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(12),
760
+ Cocoa.NSForegroundColorAttributeName: text_color
761
+ }
762
+ astr = Cocoa.NSAttributedString.alloc().initWithString_attributes_(title, title_attrs)
763
+ astr.drawInRect_(Cocoa.NSMakeRect(title_x, (bounds.size.height - 16) / 2.0, max_title_w, 16))
764
+
765
+ def mouseDown_(self, event):
766
+ pt = self.convertPoint_fromView_(event.locationInWindow(), None)
767
+ if Cocoa.NSPointInRect(pt, self.close_btn_rect):
768
+ self.controller.close_tab(self.index)
769
+ return
770
+ if self.audio_btn_rect.size.width > 0 and Cocoa.NSPointInRect(pt, self.audio_btn_rect):
771
+ self.controller.toggle_mute(self.index)
772
+ return
773
+ self.controller.switch_to_tab(self.index)
774
+
775
+ def menuForEvent_(self, event):
776
+ menu = Cocoa.NSMenu.alloc().init()
777
+ item_dupe = menu.addItemWithTitle_action_keyEquivalent_("Duplicate Tab", "onContextDuplicate:", "")
778
+ item_dupe.setTarget_(self)
779
+ item_pin = menu.addItemWithTitle_action_keyEquivalent_("Unpin Tab" if self.controller.tabs[self.index].get("pinned") else "Pin Tab", "onContextPin:", "")
780
+ item_pin.setTarget_(self)
781
+ item_mute = menu.addItemWithTitle_action_keyEquivalent_("Unmute Tab" if self.controller.tabs[self.index].get("is_muted") else "Mute Tab", "onContextMute:", "")
782
+ item_mute.setTarget_(self)
783
+ item_grp = menu.addItemWithTitle_action_keyEquivalent_("Add Tab to New Group", "onContextGroup:", "")
784
+ item_grp.setTarget_(self)
785
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
786
+ item_close = menu.addItemWithTitle_action_keyEquivalent_("Close Tab", "onContextClose:", "")
787
+ item_close.setTarget_(self)
788
+ return menu
789
+
790
+ def onContextDuplicate_(self, sender): self.controller.duplicate_tab(self.index)
791
+ def onContextPin_(self, sender): self.controller.toggle_pin_tab(self.index)
792
+ def onContextMute_(self, sender): self.controller.toggle_mute(self.index)
793
+ def onContextGroup_(self, sender): self.controller.create_group_for_tab(self.index)
794
+ def onContextClose_(self, sender): self.controller.close_tab(self.index)
795
+
796
+ class ChromeToolbarView(Cocoa.NSView):
797
+ def initWithFrame_(self, frame):
798
+ self = objc.super(ChromeToolbarView, self).initWithFrame_(frame)
799
+ if self:
800
+ self.controller = None
801
+ return self
802
+
803
+ def drawRect_(self, dirtyRect):
804
+ is_inc = self.controller.is_incognito if self.controller else False
805
+ get_m3_color("toolbar-bg", self, is_inc).setFill()
806
+ Cocoa.NSRectFill(self.bounds())
807
+ sep = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(0, 0, self.bounds().size.width, 1))
808
+ get_m3_color("toolbar-separator", self, is_inc).setFill()
809
+ sep.fill()
810
+
811
+ class BookmarksBarView(Cocoa.NSView):
812
+ def initWithFrame_controller_(self, frame, controller):
813
+ self = objc.super(BookmarksBarView, self).initWithFrame_(frame)
814
+ if self:
815
+ self.controller = controller
816
+ self.reload_bookmarks()
817
+ return self
818
+
819
+ def drawRect_(self, dirtyRect):
820
+ is_inc = self.controller.is_incognito if self.controller else False
821
+ get_m3_color("toolbar-bg", self, is_inc).setFill()
822
+ Cocoa.NSRectFill(self.bounds())
823
+ sep = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(0, 0, self.bounds().size.width, 0.5))
824
+ get_m3_color("toolbar-separator", self, is_inc).setFill()
825
+ sep.fill()
826
+
827
+ def reload_bookmarks(self):
828
+ for sub in list(self.subviews()):
829
+ sub.removeFromSuperview()
830
+ bms = []
831
+ if os.path.exists(BOOKMARKS_FILE):
832
+ try:
833
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
834
+ bms = json.load(f)
835
+ except Exception:
836
+ bms = []
837
+
838
+ x = 8.0
839
+ h = self.bounds().size.height
840
+ for b in bms:
841
+ if not isinstance(b, dict) or not b.get("url"):
842
+ continue
843
+ title = b.get("title", b["url"])[:24]
844
+ # Fixed: Removed hardcoded default 🔖 favicon prefix from bookmarks
845
+ btn_w = max(40, min(140, len(title) * 7.5 + 14))
846
+ btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
847
+ btn.setTitle_(title)
848
+ btn.setBezelStyle_(Cocoa.NSBezelStyleInline)
849
+ btn.setFont_(Cocoa.NSFont.systemFontOfSize_(11))
850
+ btn.setTarget_(self)
851
+ btn.setAction_("onBookmarkClick:")
852
+ btn.setToolTip_(b.get("url", ""))
853
+ self.addSubview_(btn)
854
+ x += btn_w + 4.0
855
+ self.setNeedsDisplay_(True)
856
+
857
+ def onBookmarkClick_(self, sender):
858
+ url = sender.toolTip()
859
+ if url and self.controller:
860
+ wv = self.controller.get_active_webview()
861
+ if wv:
862
+ wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(url)))
863
+
864
+ class DownloadDelegate(Cocoa.NSObject):
865
+ def initWithController_(self, controller):
866
+ self = objc.super(DownloadDelegate, self).init()
867
+ if self:
868
+ self.controller = controller
869
+ self.dest_paths = {}
870
+ return self
871
+
872
+ def download_decideDestinationUsingResponse_suggestedFilename_completionHandler_(self, download, response, filename, handler):
873
+ dest_dir = validate_and_get_download_dir()
874
+ base_name = filename or "download"
875
+ dest_path = os.path.join(dest_dir, base_name)
876
+ base, ext = os.path.splitext(dest_path)
877
+ counter = 1
878
+ while os.path.exists(dest_path):
879
+ dest_path = f"{base} ({counter}){ext}"
880
+ counter += 1
881
+
882
+ self.dest_paths[download] = dest_path
883
+ self.controller.register_download(os.path.basename(dest_path), dest_path, download)
884
+
885
+ if hasattr(download, "progress") and download.progress():
886
+ try:
887
+ download.progress().addObserver_forKeyPath_options_context_(self.controller, "fractionCompleted", Cocoa.NSKeyValueObservingOptionNew, None)
888
+ except Exception:
889
+ pass
890
+
891
+ handler(Foundation.NSURL.fileURLWithPath_(dest_path))
892
+
893
+ def downloadDidFinish_(self, download):
894
+ if hasattr(download, "progress") and download.progress():
895
+ try:
896
+ download.progress().removeObserver_forKeyPath_(self.controller, "fractionCompleted")
897
+ except Exception:
898
+ pass
899
+ path = self.dest_paths.pop(download, None)
900
+ self.controller.finish_download(download, "Completed", dl_path=path)
901
+
902
+ def download_didFailWithError_resumeData_(self, download, error, resumeData):
903
+ if hasattr(download, "progress") and download.progress():
904
+ try:
905
+ download.progress().removeObserver_forKeyPath_(self.controller, "fractionCompleted")
906
+ except Exception:
907
+ pass
908
+ path = self.dest_paths.pop(download, None)
909
+ self.controller.finish_download(download, "Failed", dl_path=path)
910
+
911
+ # ============================================================================
912
+ # SCHEME HANDLER
913
+ # ============================================================================
914
+ class InternalSchemeHandler(Cocoa.NSObject):
915
+ def init(self):
916
+ self = objc.super(InternalSchemeHandler, self).init()
917
+ if self:
918
+ self.active_tasks, self.lock = set(), threading.Lock()
919
+ return self
920
+
921
+ def webView_startURLSchemeTask_(self, webView, task):
922
+ with self.lock:
923
+ self.active_tasks.add(task)
924
+ try:
925
+ url = task.request().URL().absoluteString()
926
+ if "settings" in url:
927
+ with ADBLOCK_LOCK:
928
+ sb = len(STEVENBLACK_DOMAINS)
929
+ base = len(BASE_ADBLOCK_DOMAINS)
930
+ cust = len(SETTINGS.get_custom_adblock_domains())
931
+ act = sb + base + cust
932
+ html_str = get_settings_html(sb, base, cust, act)
933
+ elif "bookmarks" in url:
934
+ html_str = self.get_bookmarks_html()
935
+ elif "downloads" in url:
936
+ html_str = self.get_downloads_html()
937
+ elif "extensions" in url:
938
+ html_str = self.get_extensions_html()
939
+ elif "terminal" in url:
940
+ html_str = self.get_terminal_html()
941
+ elif "processes" in url:
942
+ dispatch_to_main(lambda: Cocoa.NSApp.delegate().process_manager.show())
943
+ html_str = "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:40px;background:#FAF9F6;color:#222;'><h2>Task Manager launched in a separate window.</h2></body></html>"
944
+ else:
945
+ html_str = self.get_history_html()
946
+
947
+ data = html_str.encode('utf-8')
948
+ resp = Foundation.NSURLResponse.alloc().initWithURL_MIMEType_expectedContentLength_textEncodingName_(
949
+ task.request().URL(), "text/html", len(data), "utf-8"
950
+ )
951
+ with self.lock:
952
+ if task not in self.active_tasks:
953
+ return
954
+ task.didReceiveResponse_(resp)
955
+ task.didReceiveData_(Foundation.NSData.dataWithBytes_length_(data, len(data)))
956
+ task.didFinish()
957
+ except Exception:
958
+ try:
959
+ task.didFailWithError_(Foundation.NSError.errorWithDomain_code_userInfo_("PyBrowserError", -1, None))
960
+ except Exception:
961
+ pass
962
+ finally:
963
+ with self.lock:
964
+ self.active_tasks.discard(task)
965
+
966
+ def webView_stopURLSchemeTask_(self, webView, task):
967
+ with self.lock:
968
+ self.active_tasks.discard(task)
969
+
970
+ def get_terminal_html(self):
971
+ # Fullscreen responsive xterm with ResizeObserver to ensure vim/less utilize entire viewport
972
+ bg = "#1e1e2e"
973
+ fg = "#c792ea"
974
+ return f"""<!DOCTYPE html><html><head><meta charset='utf-8'>
975
+ <link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css' />
976
+ <script src='https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js'></script>
977
+ <script src='https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js'></script>
978
+ <style>
979
+ *{{box-sizing:border-box;margin:0;padding:0}}
980
+ html,body{{width:100vw;height:100vh;background:{bg};overflow:hidden;padding:0;margin:0}}
981
+ #t{{width:100%;height:100%;background:{bg};overflow:hidden}}
982
+ .xterm-viewport{{overflow-y:auto !important}}
983
+ </style></head><body><div id='t'></div><script>
984
+ const term = new Terminal({{
985
+ fontFamily: 'Menlo, Monaco, "Courier New", monospace',
986
+ fontSize: 13,
987
+ lineHeight: 1.15,
988
+ cursorBlink: true,
989
+ theme: {{ background: '{bg}', foreground: '{fg}', cursor: '#A8C7FA' }}
990
+ }});
991
+ const fit = new FitAddon.FitAddon();
992
+ term.loadAddon(fit);
993
+ term.open(document.getElementById('t'));
994
+
995
+ const postResize = () => {{
996
+ try {{
997
+ fit.fit();
998
+ if (term.cols > 0 && term.rows > 0 && window.webkit?.messageHandlers?.terminalResize) {{
999
+ window.webkit.messageHandlers.terminalResize.postMessage({{ cols: term.cols, rows: term.rows }});
1000
+ }}
1001
+ }} catch(e) {{}}
1002
+ }};
1003
+
1004
+ const ro = new ResizeObserver(() => {{
1005
+ postResize();
1006
+ }});
1007
+ ro.observe(document.getElementById('t'));
1008
+
1009
+ setTimeout(postResize, 30);
1010
+ setTimeout(postResize, 100);
1011
+ setTimeout(postResize, 350);
1012
+ window.addEventListener('resize', postResize);
1013
+
1014
+ term.onData(d => {{
1015
+ if (window.webkit?.messageHandlers?.terminalInput) {{
1016
+ window.webkit.messageHandlers.terminalInput.postMessage(d);
1017
+ }}
1018
+ }});
1019
+
1020
+ window.writeToTerm = b => {{
1021
+ const s = atob(b), u = new Uint8Array(s.length);
1022
+ for (let i = 0; i < s.length; i++) u[i] = s.charCodeAt(i);
1023
+ term.write(new TextDecoder().decode(u));
1024
+ }};
1025
+ </script></body></html>"""
1026
+
1027
+ def get_bookmarks_html(self):
1028
+ bms = []
1029
+ if os.path.exists(BOOKMARKS_FILE):
1030
+ try:
1031
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
1032
+ bms = json.load(f)
1033
+ except Exception:
1034
+ pass
1035
+ items = []
1036
+ for b in bms:
1037
+ if isinstance(b, dict) and b.get("url"):
1038
+ u = b["url"]
1039
+ t = b.get("title", u)
1040
+ safe_u = json.dumps(u)
1041
+ del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
1042
+ items.append(f"<li><a href='{html.escape(u)}'>{html.escape(t)}</a> <button onclick='{del_call}'>Delete</button></li>")
1043
+ rows = "".join(items)
1044
+ return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title'><input id='u' placeholder='https://...'><button onclick='window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)'>Add</button></div><ul>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
1045
+
1046
+ def get_history_html(self):
1047
+ records = query_history()
1048
+ items = []
1049
+ for r in records:
1050
+ del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
1051
+ items.append(f"<tr><td>{r[3]}</td><td><a href='{html.escape(r[1])}'>{html.escape(r[2])}</a></td><td><button onclick='{del_call}'>✕</button></td></tr>")
1052
+ rows = "".join(items)
1053
+ return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick=\"window.webkit.messageHandlers.bridge.postMessage({{cmd:'clear_browsing_data',targets:['history']}});setTimeout(()=>location.reload(),100)\">Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"
1054
+
1055
+ def get_downloads_html(self):
1056
+ return "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Downloads</h2><div id='dl-list'>No active downloads.</div><script>const dls={};window.updateDownload=(id,name,path,status,progress)=>{dls[id]={name,path,status,progress};render()};function render(){const el=document.getElementById('dl-list');el.innerHTML=Object.values(dls).map(d=>`<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'><b>${d.name}</b> (${d.status}) <button onclick=\"window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:'${d.path}'})\">Show in Finder</button></div>`).join('')};window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_request_sync'});</script></body></html>"
1057
+
1058
+ def get_extensions_html(self):
1059
+ scripts = get_installed_userscripts()
1060
+ items = []
1061
+ for s in scripts:
1062
+ f = s["file"]
1063
+ safe_f = json.dumps(f)
1064
+ checked = "checked" if s["enabled"] else ""
1065
+ toggle_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
1066
+ del_call = f"if(confirm('Delete?')){{window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
1067
+ items.append(
1068
+ f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
1069
+ f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
1070
+ f"<label><input type='checkbox' {checked} onchange='{toggle_call}'> Enabled</label> "
1071
+ f"<button onclick=\"{del_call}\">Delete</button>"
1072
+ f"</div>"
1073
+ )
1074
+ rows = "".join(items)
1075
+ return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='let id=prompt(\"CRX ID or URL:\");if(id)window.webkit.messageHandlers.bridge.postMessage({{cmd:\"install_crx\",id:id}})'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"
1076
+
1077
+ # ============================================================================
1078
+ # WEBKIT SUBCLASS & IMAGE CONTEXT MENU
1079
+ # ============================================================================
1080
+ class BrowserWebView(WebKit.WKWebView):
1081
+ def willOpenMenu_withEvent_(self, menu, event):
1082
+ self.open_image_item = None
1083
+ download_item = None
1084
+
1085
+ for item in menu.itemArray():
1086
+ ident = str(item.identifier()) if item.identifier() else ""
1087
+ title = str(item.title()).lower() if item.title() else ""
1088
+ if "openimageinnewwindow" in ident.lower() or "open image in new" in title:
1089
+ self.open_image_item = item
1090
+ item.setTitle_("Open Image in New Tab")
1091
+ elif "downloadimage" in ident.lower() or "download image" in title:
1092
+ download_item = item
1093
+
1094
+ if self.open_image_item:
1095
+ if download_item:
1096
+ download_item.setTarget_(self)
1097
+ download_item.setAction_("onContextDownloadImage:")
1098
+ else:
1099
+ dl = Cocoa.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Download Image", "onContextDownloadImage:", "")
1100
+ dl.setTarget_(self)
1101
+ idx = menu.indexOfItem_(self.open_image_item)
1102
+ menu.insertItem_atIndex_(dl, idx + 1)
1103
+ download_item = dl
1104
+
1105
+ save_as = Cocoa.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Save Image As…", "onContextSaveImageAs:", "")
1106
+ save_as.setTarget_(self)
1107
+ idx = menu.indexOfItem_(download_item)
1108
+ menu.insertItem_atIndex_(save_as, idx + 1)
1109
+
1110
+ if not any("copy image address" in str(it.title()).lower() for it in menu.itemArray()):
1111
+ copy_addr = Cocoa.NSMenuItem.alloc().initWithTitle_action_keyEquivalent_("Copy Image Address", "onContextCopyImageAddress:", "")
1112
+ copy_addr.setTarget_(self)
1113
+ menu.insertItem_atIndex_(copy_addr, idx + 2)
1114
+ else:
1115
+ controller = getattr(self, "controller", None)
1116
+ target_img_url = getattr(controller, "last_context_image_url", None) if controller else None
1117
+ if target_img_url:
1118
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
1119
+ menu.addItemWithTitle_action_keyEquivalent_("Download Image", "onDirectDownloadImage:", "").setTarget_(self)
1120
+ menu.addItemWithTitle_action_keyEquivalent_("Save Image As…", "onDirectSaveImageAs:", "").setTarget_(self)
1121
+ menu.addItemWithTitle_action_keyEquivalent_("Copy Image Address", "onDirectCopyImageAddress:", "").setTarget_(self)
1122
+
1123
+ def onContextDownloadImage_(self, sender):
1124
+ if self.open_image_item:
1125
+ self.pending_image_action = "download"
1126
+ Cocoa.NSApp.sendAction_to_from_(self.open_image_item.action(), self.open_image_item.target(), self.open_image_item)
1127
+
1128
+ def onContextSaveImageAs_(self, sender):
1129
+ if self.open_image_item:
1130
+ self.pending_image_action = "save_as"
1131
+ Cocoa.NSApp.sendAction_to_from_(self.open_image_item.action(), self.open_image_item.target(), self.open_image_item)
1132
+
1133
+ def onContextCopyImageAddress_(self, sender):
1134
+ if self.open_image_item:
1135
+ self.pending_image_action = "copy_address"
1136
+ Cocoa.NSApp.sendAction_to_from_(self.open_image_item.action(), self.open_image_item.target(), self.open_image_item)
1137
+
1138
+ def onDirectDownloadImage_(self, sender):
1139
+ if hasattr(self, "controller") and self.controller and self.controller.last_context_image_url:
1140
+ self.controller.perform_download_image(self.controller.last_context_image_url)
1141
+
1142
+ def onDirectSaveImageAs_(self, sender):
1143
+ if hasattr(self, "controller") and self.controller and self.controller.last_context_image_url:
1144
+ self.controller.perform_save_image_as(self.controller.last_context_image_url)
1145
+
1146
+ def onDirectCopyImageAddress_(self, sender):
1147
+ if hasattr(self, "controller") and self.controller and self.controller.last_context_image_url:
1148
+ self.controller.perform_copy_image_address(self.controller.last_context_image_url)
1149
+
1150
+ # ============================================================================
1151
+ # OMNIBOX & SUGGESTIONS
1152
+ # ============================================================================
1153
+ class ChromeOmniboxField(Cocoa.NSTextField):
1154
+ def initWithFrame_(self, frame):
1155
+ self = objc.super(ChromeOmniboxField, self).initWithFrame_(frame)
1156
+ if self:
1157
+ self.setFocusRingType_(Cocoa.NSFocusRingTypeNone)
1158
+ self.setBordered_(False)
1159
+ self.setDrawsBackground_(False)
1160
+ self.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
1161
+ self.raw_url = ""
1162
+ return self
1163
+
1164
+ def becomeFirstResponder(self):
1165
+ if self.superview():
1166
+ self.superview().setNeedsDisplay_(True)
1167
+ self.setStringValue_(self.raw_url)
1168
+ self.setTextColor_(Cocoa.NSColor.labelColor())
1169
+ self.performSelector_withObject_afterDelay_("selectText:", self, 0.0)
1170
+ return objc.super(ChromeOmniboxField, self).becomeFirstResponder()
1171
+
1172
+ def resignFirstResponder(self):
1173
+ if self.superview():
1174
+ self.superview().setNeedsDisplay_(True)
1175
+ self.setStringValue_(self.raw_url or "")
1176
+ return objc.super(ChromeOmniboxField, self).resignFirstResponder()
1177
+
1178
+ def set_url(self, url):
1179
+ self.raw_url = url
1180
+ fr = self.window().firstResponder() if self.window() else None
1181
+ if fr != self and fr != self.currentEditor():
1182
+ self.setStringValue_(url or "")
1183
+
1184
+ class OmniboxPopupPanel(Cocoa.NSPanel):
1185
+ def canBecomeKeyWindow(self): return False
1186
+ def hasKeyAppearance(self): return True
1187
+
1188
+ class OmniboxSuggestionView(Cocoa.NSView):
1189
+ def initWithFrame_controller_(self, frame, controller):
1190
+ self = objc.super(OmniboxSuggestionView, self).initWithFrame_(frame)
1191
+ if self:
1192
+ self.controller, self.suggestions, self.selected_index, self.tracking_area = controller, [], -1, None
1193
+ return self
1194
+
1195
+ def isFlipped(self): return True
1196
+ def acceptsFirstMouse_(self, event): return True
1197
+
1198
+ def updateTrackingAreas(self):
1199
+ if self.tracking_area: self.removeTrackingArea_(self.tracking_area)
1200
+ opts = Cocoa.NSTrackingMouseMoved | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
1201
+ self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
1202
+ self.addTrackingArea_(self.tracking_area)
1203
+ objc.super(OmniboxSuggestionView, self).updateTrackingAreas()
1204
+
1205
+ def mouseMoved_(self, event):
1206
+ loc = self.convertPoint_fromView_(event.locationInWindow(), None)
1207
+ idx = int(loc.y // 28)
1208
+ if 0 <= idx < len(self.suggestions) and self.selected_index != idx:
1209
+ self.selected_index = idx; self.setNeedsDisplay_(True)
1210
+
1211
+ def mouseDown_(self, event):
1212
+ loc = self.convertPoint_fromView_(event.locationInWindow(), None)
1213
+ idx = int(loc.y // 28)
1214
+ if 0 <= idx < len(self.suggestions):
1215
+ self.controller.omnibox_panel.select_suggestion(self.suggestions[idx])
1216
+
1217
+ def drawRect_(self, dirtyRect):
1218
+ Cocoa.NSColor.clearColor().setFill()
1219
+ Cocoa.NSRectFill(self.bounds())
1220
+ for i, sugg in enumerate(self.suggestions):
1221
+ rect = Cocoa.NSMakeRect(0, i * 28, self.bounds().size.width, 28)
1222
+ if i == self.selected_index:
1223
+ hex_to_nscolor("#1A73E8").setFill()
1224
+ Cocoa.NSRectFill(rect)
1225
+ text_color = Cocoa.NSColor.whiteColor()
1226
+ else:
1227
+ text_color = Cocoa.NSColor.labelColor()
1228
+
1229
+ icon = "🔍" if sugg["type"] == "search" else ("🕒" if sugg["type"] == "history" else ("🔖" if sugg["type"] == "bookmark" else ("🧮" if sugg["type"] == "math" else "⚡")))
1230
+ attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: text_color}
1231
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(f"{icon} {sugg['text']}", attrs).drawAtPoint_(Cocoa.NSMakePoint(12, rect.origin.y + 6))
1232
+
1233
+ class OmniboxPanelCoordinator:
1234
+ def __init__(self, window_controller):
1235
+ self.wc = window_controller
1236
+ self.suggestion_window, self.suggestion_view = None, None
1237
+ self.current_req_id, self.typed_query = 0, ""
1238
+
1239
+ def is_visible(self):
1240
+ return bool(self.suggestion_window and self.suggestion_window.isVisible())
1241
+
1242
+ def handle_text_change(self, query):
1243
+ if not SETTINGS.settings.get("omnibox_suggestions_enabled", True) or not query or not query.strip():
1244
+ self.hide()
1245
+ return
1246
+
1247
+ query_str = query.strip()
1248
+ self.typed_query = query_str
1249
+ self.current_req_id += 1
1250
+ req_id = self.current_req_id
1251
+
1252
+ results = []
1253
+ for qa in ["@settings", "@history", "@bookmarks", "@extensions", "@downloads", "@terminal", "@processes"]:
1254
+ if qa.startswith(query_str.lower()):
1255
+ results.append({"type": "action", "text": qa, "url": f"browser://{qa[1:]}"})
1256
+
1257
+ math_res = safe_math_eval(query_str)
1258
+ if math_res:
1259
+ results.append({"type": "math", "text": f"= {math_res}", "url": math_res, "is_eval": True})
1260
+
1261
+ try:
1262
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
1263
+ bms = json.load(f)
1264
+ for b in bms:
1265
+ if isinstance(b, dict):
1266
+ t, u = b.get("title", ""), b.get("url", "")
1267
+ if query_str.lower() in t.lower() or query_str.lower() in u.lower():
1268
+ results.append({"type": "bookmark", "text": t or u, "url": u})
1269
+ except Exception:
1270
+ pass
1271
+
1272
+ for h in query_history(query_str)[:4]:
1273
+ results.append({"type": "history", "text": h[2], "url": h[1]})
1274
+
1275
+ def fetch_remote():
1276
+ try:
1277
+ url = f"http://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
1278
+ req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
1279
+ with urllib.request.urlopen(req, timeout=0.8) as resp:
1280
+ data = json.loads(resp.read().decode('utf-8'))
1281
+ remote = [{"type": "search", "text": s, "url": s} for s in data[1][:5]]
1282
+ if req_id == self.current_req_id:
1283
+ dispatch_to_main(lambda: self.show(results + remote))
1284
+ except Exception:
1285
+ if req_id == self.current_req_id:
1286
+ dispatch_to_main(lambda: self.show(results))
1287
+
1288
+ threading.Thread(target=fetch_remote, daemon=True).start()
1289
+
1290
+ def show(self, suggestions):
1291
+ if not suggestions:
1292
+ self.hide()
1293
+ return
1294
+ if not self.suggestion_window:
1295
+ self.suggestion_window = OmniboxPopupPanel.alloc().initWithContentRect_styleMask_backing_defer_(
1296
+ Cocoa.NSMakeRect(0, 0, 100, 100), Cocoa.NSWindowStyleMaskBorderless, Cocoa.NSBackingStoreBuffered, False
1297
+ )
1298
+ self.suggestion_window.setOpaque_(False)
1299
+ self.suggestion_window.setAcceptsMouseMovedEvents_(True)
1300
+ self.suggestion_window.setBackgroundColor_(Cocoa.NSColor.clearColor())
1301
+ self.suggestion_window.setHasShadow_(True)
1302
+
1303
+ vev = Cocoa.NSVisualEffectView.alloc().initWithFrame_(self.suggestion_window.contentView().bounds())
1304
+ vev.setMaterial_(Cocoa.NSVisualEffectMaterialPopover)
1305
+ vev.setBlendingMode_(Cocoa.NSVisualEffectBlendingModeBehindWindow)
1306
+ vev.setState_(Cocoa.NSVisualEffectStateActive)
1307
+ vev.setWantsLayer_(True)
1308
+ vev.layer().setCornerRadius_(8)
1309
+ self.suggestion_window.setContentView_(vev)
1310
+
1311
+ self.suggestion_view = OmniboxSuggestionView.alloc().initWithFrame_controller_(vev.bounds(), self.wc)
1312
+ vev.addSubview_(self.suggestion_view)
1313
+
1314
+ self.suggestion_view.suggestions = suggestions[:8]
1315
+ self.suggestion_view.selected_index = -1
1316
+
1317
+ rect_in_window = self.wc.omnibox_container.convertRect_toView_(self.wc.omnibox_container.bounds(), None)
1318
+ rect = self.wc.window().convertRectToScreen_(rect_in_window)
1319
+ popup_h = len(self.suggestion_view.suggestions) * 28
1320
+
1321
+ self.suggestion_window.setFrame_display_(Cocoa.NSMakeRect(rect.origin.x, rect.origin.y - popup_h - 4, rect.size.width, popup_h), True)
1322
+ self.suggestion_view.setFrame_(self.suggestion_window.contentView().bounds())
1323
+ self.suggestion_view.setNeedsDisplay_(True)
1324
+ if self.suggestion_window.parentWindow() != self.wc.window():
1325
+ self.wc.window().addChildWindow_ordered_(self.suggestion_window, Cocoa.NSWindowAbove)
1326
+ self.suggestion_window.orderFront_(None)
1327
+
1328
+ def hide(self):
1329
+ if self.suggestion_window and self.suggestion_window.isVisible():
1330
+ if self.suggestion_window.parentWindow():
1331
+ self.wc.window().removeChildWindow_(self.suggestion_window)
1332
+ self.suggestion_window.orderOut_(None)
1333
+
1334
+ def select_suggestion(self, sugg):
1335
+ if sugg.get("is_eval"):
1336
+ val = sugg.get("url", "")
1337
+ self.wc.omnibox.setStringValue_(val)
1338
+ pb = Cocoa.NSPasteboard.generalPasteboard()
1339
+ pb.clearContents()
1340
+ pb.setString_forType_(val, Cocoa.NSPasteboardTypeString)
1341
+ self.hide()
1342
+ return
1343
+ target = sugg.get("url") or sugg.get("text")
1344
+ self.wc.omnibox.setStringValue_(target)
1345
+ editor = self.wc.omnibox.currentEditor()
1346
+ if editor:
1347
+ editor.setString_(target)
1348
+ self.hide()
1349
+ self.wc.onOmniboxEnter_(self.wc.omnibox)
1350
+
1351
+ def handle_command_selector(self, commandSelector):
1352
+ sel = str(commandSelector)
1353
+ if self.is_visible():
1354
+ if sel == "moveDown:":
1355
+ self.suggestion_view.selected_index = min(len(self.suggestion_view.suggestions)-1, self.suggestion_view.selected_index + 1)
1356
+ if self.suggestion_view.selected_index >= 0:
1357
+ s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
1358
+ self.wc.omnibox.setStringValue_(s.get("url") or s.get("text"))
1359
+ self.suggestion_view.setNeedsDisplay_(True)
1360
+ return True
1361
+ elif sel == "moveUp:":
1362
+ self.suggestion_view.selected_index = max(-1, self.suggestion_view.selected_index - 1)
1363
+ if self.suggestion_view.selected_index >= 0:
1364
+ s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
1365
+ self.wc.omnibox.setStringValue_(s.get("url") or s.get("text"))
1366
+ else:
1367
+ self.wc.omnibox.setStringValue_(self.typed_query)
1368
+ self.suggestion_view.setNeedsDisplay_(True)
1369
+ return True
1370
+ elif sel == "insertNewline:":
1371
+ if self.suggestion_view.selected_index >= 0:
1372
+ self.select_suggestion(self.suggestion_view.suggestions[self.suggestion_view.selected_index])
1373
+ return True
1374
+ elif sel == "cancelOperation:":
1375
+ self.hide()
1376
+ return True
1377
+ return False
1378
+
1379
+ # ============================================================================
1380
+ # MATERIAL PROGRESS BAR & FIND BAR
1381
+ # ============================================================================
1382
+ class MaterialProgressBar(Cocoa.NSView):
1383
+ def initWithFrame_(self, frame):
1384
+ self = objc.super(MaterialProgressBar, self).initWithFrame_(frame)
1385
+ if self:
1386
+ self.progress = 0.0
1387
+ return self
1388
+
1389
+ def setProgress_(self, val):
1390
+ self.progress = min(1.0, max(0.0, float(val)))
1391
+ self.setNeedsDisplay_(True)
1392
+
1393
+ def drawRect_(self, dirtyRect):
1394
+ if self.progress <= 0.0:
1395
+ return
1396
+ w, h = self.bounds().size.width * self.progress, self.bounds().size.height
1397
+ path = Cocoa.NSBezierPath.bezierPathWithRect_(Cocoa.NSMakeRect(0, 0, w, h))
1398
+ is_dark = False
1399
+ try:
1400
+ if self.effectiveAppearance():
1401
+ is_dark = "Dark" in str(self.effectiveAppearance().name())
1402
+ except Exception:
1403
+ pass
1404
+ hex_to_nscolor("#8AB4F8" if is_dark else "#1A73E8").setFill()
1405
+ path.fill()
1406
+
1407
+ class FindBarView(Cocoa.NSView):
1408
+ def initWithFrame_controller_(self, frame, controller):
1409
+ self = objc.super(FindBarView, self).initWithFrame_(frame)
1410
+ if self:
1411
+ self.controller = controller
1412
+ self.setup_ui()
1413
+ return self
1414
+
1415
+ def drawRect_(self, dirtyRect):
1416
+ path = Cocoa.NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(self.bounds(), 8, 8)
1417
+ get_m3_color("toolbar-bg", self).setFill()
1418
+ path.fill()
1419
+ path.setLineWidth_(1.0)
1420
+ get_m3_color("toolbar-separator", self).setStroke()
1421
+ path.stroke()
1422
+
1423
+ def setup_ui(self):
1424
+ self.search_field = Cocoa.NSSearchField.alloc().initWithFrame_(Cocoa.NSMakeRect(10, 10, 200, 22))
1425
+ self.search_field.setTarget_(self)
1426
+ self.search_field.setAction_("onSearch:")
1427
+ self.search_field.setPlaceholderString_("Find in page…")
1428
+ self.addSubview_(self.search_field)
1429
+ self.close_btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(218, 10, 24, 22))
1430
+ self.close_btn.setTitle_("✕")
1431
+ self.close_btn.setBezelStyle_(Cocoa.NSBezelStyleInline)
1432
+ self.close_btn.setTarget_(self)
1433
+ self.close_btn.setAction_("onClose:")
1434
+ self.addSubview_(self.close_btn)
1435
+
1436
+ def onSearch_(self, sender):
1437
+ query = self.search_field.stringValue()
1438
+ wv = self.controller.get_active_webview()
1439
+ if wv and query.strip():
1440
+ cfg = WebKit.WKFindConfiguration.alloc().init()
1441
+ cfg.setWraps_(True)
1442
+ wv.findString_withConfiguration_completionHandler_(query, cfg, lambda res: None)
1443
+
1444
+ def onClose_(self, sender):
1445
+ self.setHidden_(True)
1446
+ wv = self.controller.get_active_webview()
1447
+ if wv:
1448
+ self.controller.window().makeFirstResponder_(wv)
1449
+
1450
+ # ============================================================================
1451
+ # QUICK SWITCHER & MEDIA CONTROLS (PIP)
1452
+ # ============================================================================
1453
+ class QuickSwitcherListView(Cocoa.NSView):
1454
+ def initWithFrame_controller_(self, frame, controller):
1455
+ self = objc.super(QuickSwitcherListView, self).initWithFrame_(frame)
1456
+ if self:
1457
+ self.controller = controller
1458
+ return self
1459
+
1460
+ def isFlipped(self): return True
1461
+
1462
+ def drawRect_(self, dirtyRect):
1463
+ for i, tab in enumerate(self.controller.filtered_tabs):
1464
+ rect = Cocoa.NSMakeRect(0, i * 36, self.bounds().size.width, 36)
1465
+ if i == self.controller.selected_index:
1466
+ hex_to_nscolor("#1A73E8").setFill()
1467
+ Cocoa.NSRectFill(rect)
1468
+ tc, sub_color = Cocoa.NSColor.whiteColor(), Cocoa.NSColor.whiteColor()
1469
+ else:
1470
+ tc, sub_color = Cocoa.NSColor.labelColor(), Cocoa.NSColor.secondaryLabelColor()
1471
+
1472
+ attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: tc}
1473
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(tab["title"][:60], attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 3))
1474
+ sub_attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: sub_color}
1475
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(tab["url"][:80], sub_attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 19))
1476
+
1477
+ class QuickSwitcherController(Cocoa.NSWindowController):
1478
+ def init(self):
1479
+ w, h = 600, 400
1480
+ window = Cocoa.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
1481
+ Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskBorderless | Cocoa.NSWindowStyleMaskFullSizeContentView, Cocoa.NSBackingStoreBuffered, False
1482
+ )
1483
+ window.setOpaque_(False)
1484
+ window.setBackgroundColor_(Cocoa.NSColor.clearColor())
1485
+ window.setHasShadow_(True)
1486
+ window.center()
1487
+ self = objc.super(QuickSwitcherController, self).initWithWindow_(window)
1488
+ if self:
1489
+ self.setup_ui()
1490
+ return self
1491
+
1492
+ def setup_ui(self):
1493
+ vev = Cocoa.NSVisualEffectView.alloc().initWithFrame_(self.window().contentView().bounds())
1494
+ vev.setMaterial_(Cocoa.NSVisualEffectMaterialPopover)
1495
+ vev.setBlendingMode_(Cocoa.NSVisualEffectBlendingModeBehindWindow)
1496
+ vev.setState_(Cocoa.NSVisualEffectStateActive)
1497
+ vev.setWantsLayer_(True)
1498
+ vev.layer().setCornerRadius_(12)
1499
+ vev.layer().setMasksToBounds_(True)
1500
+ self.window().setContentView_(vev)
1501
+
1502
+ self.search_field = Cocoa.NSTextField.alloc().initWithFrame_(Cocoa.NSMakeRect(20, 345, 560, 35))
1503
+ self.search_field.setFont_(Cocoa.NSFont.systemFontOfSize_(20))
1504
+ self.search_field.setFocusRingType_(Cocoa.NSFocusRingTypeNone)
1505
+ self.search_field.setBordered_(False)
1506
+ self.search_field.setDrawsBackground_(False)
1507
+ self.search_field.setPlaceholderString_("Search Open Tabs…")
1508
+ self.search_field.setDelegate_(self)
1509
+ vev.addSubview_(self.search_field)
1510
+
1511
+ self.list_view = QuickSwitcherListView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(0, 0, 600, 330), self)
1512
+ vev.addSubview_(self.list_view)
1513
+ self.tabs, self.filtered_tabs, self.selected_index = [], [], 0
1514
+
1515
+ def show(self):
1516
+ self.tabs = []
1517
+ app_delegate = Cocoa.NSApp.delegate()
1518
+ if app_delegate and hasattr(app_delegate, "controllers"):
1519
+ for wc in app_delegate.controllers:
1520
+ for t in wc.tabs:
1521
+ self.tabs.append({
1522
+ "id": t.get("id"),
1523
+ "title": t.get("title", "Tab"),
1524
+ "url": t.get("url", ""),
1525
+ "wc": wc
1526
+ })
1527
+ self.filtered_tabs = self.tabs.copy()
1528
+ self.selected_index = 0
1529
+ self.search_field.setStringValue_("")
1530
+ self.list_view.setNeedsDisplay_(True)
1531
+ self.window().makeKeyAndOrderFront_(None)
1532
+ self.window().makeFirstResponder_(self.search_field)
1533
+
1534
+ def controlTextDidChange_(self, notif):
1535
+ if notif.object() == self.search_field:
1536
+ q = self.search_field.stringValue().lower().strip()
1537
+ self.filtered_tabs = self.tabs.copy() if not q else [t for t in self.tabs if q in t["title"].lower() or q in t["url"].lower()]
1538
+ self.selected_index = 0
1539
+ self.list_view.setNeedsDisplay_(True)
1540
+
1541
+ def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
1542
+ sel = str(commandSelector)
1543
+ if sel == "moveDown:":
1544
+ self.selected_index = min(len(self.filtered_tabs) - 1, self.selected_index + 1)
1545
+ self.list_view.setNeedsDisplay_(True)
1546
+ return True
1547
+ elif sel == "moveUp:":
1548
+ self.selected_index = max(0, self.selected_index - 1)
1549
+ self.list_view.setNeedsDisplay_(True)
1550
+ return True
1551
+ elif sel == "insertNewline:":
1552
+ if self.filtered_tabs and self.selected_index >= 0:
1553
+ t = self.filtered_tabs[self.selected_index]
1554
+ # Fixed: Resolve current tab index dynamically by ID to avoid desync/crash
1555
+ cur_idx = t["wc"].get_tab_index_by_id(t["id"])
1556
+ if cur_idx is not None:
1557
+ t["wc"].window().makeKeyAndOrderFront_(None)
1558
+ t["wc"].switch_to_tab(cur_idx)
1559
+ self.window().orderOut_(None)
1560
+ return True
1561
+ elif sel == "cancelOperation:":
1562
+ self.window().orderOut_(None)
1563
+ return True
1564
+ return False
1565
+
1566
+ class PiPPanelController(Cocoa.NSWindowController):
1567
+ def initWithBrowser_(self, browser_controller):
1568
+ w, h = 240, 80
1569
+ window = Cocoa.NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
1570
+ Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskTitled | Cocoa.NSWindowStyleMaskClosable | Cocoa.NSWindowStyleMaskHUDWindow | Cocoa.NSWindowStyleMaskUtilityWindow,
1571
+ Cocoa.NSBackingStoreBuffered, False
1572
+ )
1573
+ window.setTitle_("Media Controls")
1574
+ window.setFloatingPanel_(True)
1575
+ window.setLevel_(Cocoa.NSFloatingWindowLevel)
1576
+ window.setReleasedWhenClosed_(False)
1577
+ self = objc.super(PiPPanelController, self).initWithWindow_(window)
1578
+ if self:
1579
+ self.browser = browser_controller
1580
+ self.setup_ui()
1581
+ return self
1582
+
1583
+ def setup_ui(self):
1584
+ cv = self.window().contentView()
1585
+ def make_btn(title, x, action):
1586
+ btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 20, 44, 34))
1587
+ btn.setTitle_(title)
1588
+ btn.setBezelStyle_(Cocoa.NSBezelStyleRounded)
1589
+ btn.setTarget_(self)
1590
+ btn.setAction_(action)
1591
+ cv.addSubview_(btn)
1592
+ return btn
1593
+ self.btn_back = make_btn("↺10", 20, "onScrubBack:")
1594
+ self.btn_play = make_btn("⏯", 95, "onPlayPause:")
1595
+ self.btn_fwd = make_btn("10↻", 170, "onScrubFwd:")
1596
+
1597
+ def toggle(self):
1598
+ if self.window().isVisible():
1599
+ self.window().orderOut_(None)
1600
+ else:
1601
+ self.window().makeKeyAndOrderFront_(None)
1602
+
1603
+ def exec_js(self, js):
1604
+ # Fixed: Prefer active window controller or stored browser controller
1605
+ app_delegate = Cocoa.NSApp.delegate()
1606
+ wc = None
1607
+ key_win = Cocoa.NSApp.keyWindow()
1608
+ if key_win and isinstance(key_win.delegate(), Cocoa.NSWindowController) and hasattr(key_win.delegate(), "get_active_webview"):
1609
+ wc = key_win.delegate()
1610
+ elif self.browser:
1611
+ wc = self.browser
1612
+ elif app_delegate and getattr(app_delegate, "controllers", None):
1613
+ wc = app_delegate.controllers[0]
1614
+
1615
+ if wc:
1616
+ wv = wc.get_active_webview()
1617
+ if wv:
1618
+ wv.evaluateJavaScript_completionHandler_(js, None)
1619
+
1620
+ def onPlayPause_(self, s): self.exec_js("var v = document.querySelector('video, audio'); if(v) v.paused ? v.play() : v.pause();")
1621
+ def onScrubBack_(self, s): self.exec_js("var v = document.querySelector('video, audio'); if(v) v.currentTime -= 10;")
1622
+ def onScrubFwd_(self, s): self.exec_js("var v = document.querySelector('video, audio'); if(v) v.currentTime += 10;")
1623
+ # ============================================================================
1624
+ # BROWSER WINDOW CONTROLLER
1625
+ # ============================================================================
1626
+ TITLE_BAR_H = 30
1627
+
1628
+ class BrowserWindowController(Cocoa.NSWindowController):
1629
+ def init(self):
1630
+ return self.initWithIncognito_(False)
1631
+
1632
+ def initWithIncognito_(self, is_incognito=False):
1633
+ self = objc.super(BrowserWindowController, self).init()
1634
+ if self:
1635
+ self.is_incognito = is_incognito
1636
+ self.incognito_data_store = create_incognito_data_store() if is_incognito else None
1637
+ self.tabs, self.groups, self.closed_tabs = [], [], []
1638
+ self.active_tab_index, self.downloads = -1, []
1639
+ self.tab_bar_visible, self.is_fullscreen = True, False
1640
+ self.zen_auto_entered = False
1641
+ self.last_context_image_url = None
1642
+ self.download_delegate = DownloadDelegate.alloc().initWithController_(self)
1643
+ self.omnibox_panel = OmniboxPanelCoordinator(self)
1644
+ self.tab_manager = self
1645
+ self.setup_window()
1646
+ return self
1647
+
1648
+ def setup_window(self):
1649
+ w, h = 1200, 800
1650
+ window = Cocoa.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
1651
+ Cocoa.NSMakeRect(100, 100, w, h),
1652
+ Cocoa.NSWindowStyleMaskFullSizeContentView | Cocoa.NSWindowStyleMaskTitled |
1653
+ Cocoa.NSWindowStyleMaskClosable | Cocoa.NSWindowStyleMaskMiniaturizable | Cocoa.NSWindowStyleMaskResizable,
1654
+ Cocoa.NSBackingStoreBuffered, False
1655
+ )
1656
+ window.setTitlebarAppearsTransparent_(True)
1657
+ window.setTitleVisibility_(Cocoa.NSWindowTitleHidden)
1658
+ self.setWindow_(window)
1659
+ window.setCollectionBehavior_(Cocoa.NSWindowCollectionBehaviorFullScreenPrimary)
1660
+ window.setDelegate_(self)
1661
+
1662
+ self.root_view = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, 0, w, h))
1663
+ window.setContentView_(self.root_view)
1664
+ self.setup_ui(w, h)
1665
+
1666
+ if not self.restore_session():
1667
+ self.load_initial_tabs()
1668
+ window.center()
1669
+ wmode = SETTINGS.settings.get("startup_window_mode", "normal")
1670
+ if wmode == "maximized":
1671
+ window.zoom_(None)
1672
+ elif wmode == "fullscreen":
1673
+ window.toggleFullScreen_(None)
1674
+
1675
+ self.sleep_timer = Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(60.0, True, lambda t: self.check_sleeping_tabs())
1676
+ window.makeKeyAndOrderFront_(None)
1677
+
1678
+ def load_initial_tabs(self):
1679
+ behavior = SETTINGS.settings.get("startup_behavior", "new_tab")
1680
+ if behavior == "custom_url":
1681
+ self.add_tab(SETTINGS.settings.get("startup_url", DEFAULT_URL))
1682
+ return
1683
+ self.add_tab(DEFAULT_URL)
1684
+
1685
+ def save_session(self):
1686
+ if self.is_incognito:
1687
+ return
1688
+ data = {
1689
+ "active_index": self.active_tab_index,
1690
+ "tabs": [{"url": t.get("url"), "group_id": t.get("group_id"), "pinned": t.get("pinned", False)} for t in self.tabs if t.get("url") and not t.get("url").startswith("browser://")],
1691
+ "groups": [asdict(g) for g in self.groups]
1692
+ }
1693
+ atomic_write_json(SESSION_FILE, data)
1694
+
1695
+ def restore_session(self):
1696
+ if self.is_incognito or SETTINGS.settings.get("startup_behavior") != "restore":
1697
+ return False
1698
+ if not os.path.exists(SESSION_FILE):
1699
+ return False
1700
+ try:
1701
+ with open(SESSION_FILE, "r", encoding="utf-8") as f:
1702
+ data = json.load(f)
1703
+ if isinstance(data, dict):
1704
+ self.groups = [TabGroup(**g) for g in data.get("groups", [])]
1705
+ tabs_data = data.get("tabs", [])
1706
+ if not tabs_data:
1707
+ return False
1708
+ for t in tabs_data:
1709
+ self.add_tab(t.get("url", DEFAULT_URL), pinned=t.get("pinned", False))
1710
+ if t.get("group_id"):
1711
+ self.tabs[-1]["group_id"] = t.get("group_id")
1712
+ idx = data.get("active_index", 0)
1713
+ self.switch_to_tab(max(0, min(idx, len(self.tabs) - 1)))
1714
+ return True
1715
+ elif isinstance(data, list) and data:
1716
+ for u in data:
1717
+ self.add_tab(u)
1718
+ return True
1719
+ except Exception as e:
1720
+ print(f"[Restore Session Error] {e}")
1721
+ return False
1722
+
1723
+ def setup_ui(self, w, h):
1724
+ cv = self.root_view
1725
+
1726
+ # Title bar view placed at the top (hosts macOS traffic lights, drag, double-click maximize)
1727
+ self.title_bar = TitleBarView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H), self)
1728
+ cv.addSubview_(self.title_bar)
1729
+
1730
+ self.tab_strip = ChromeTabStripView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 34, w, 34))
1731
+ self.tab_strip.controller = self
1732
+ cv.addSubview_(self.tab_strip)
1733
+
1734
+ self.toolbar = ChromeToolbarView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 68, w, 34))
1735
+ self.toolbar.controller = self
1736
+ cv.addSubview_(self.toolbar)
1737
+
1738
+ self.progress_bar = MaterialProgressBar.alloc().initWithFrame_(Cocoa.NSMakeRect(0, 0, w, 2.5))
1739
+ self.progress_bar.setHidden_(True)
1740
+ self.toolbar.addSubview_(self.progress_bar)
1741
+
1742
+ bm_visible = SETTINGS.settings.get("bookmarks_bar_visible", True)
1743
+ self.bookmarks_bar = BookmarksBarView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 92, w, 24), self)
1744
+ self.bookmarks_bar.setHidden_(not bm_visible)
1745
+ cv.addSubview_(self.bookmarks_bar)
1746
+
1747
+ top_bars_h = TITLE_BAR_H + (92 if bm_visible else 68)
1748
+ self.web_container = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, 0, w, h - top_bars_h))
1749
+ cv.addSubview_(self.web_container)
1750
+
1751
+ self.find_bar = FindBarView.alloc().initWithFrame_controller_(Cocoa.NSMakeRect(w - 260, h - 140, 250, 42), self)
1752
+ self.find_bar.setHidden_(True)
1753
+ cv.addSubview_(self.find_bar)
1754
+
1755
+ def btn(title, x, action, tip=""):
1756
+ b = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 4, 26, 26))
1757
+ b.setTitle_(title)
1758
+ if tip:
1759
+ b.setToolTip_(tip)
1760
+ b.setBezelStyle_(Cocoa.NSBezelStyleCircular)
1761
+ b.setTarget_(self)
1762
+ b.setAction_(action)
1763
+ self.toolbar.addSubview_(b)
1764
+ return b
1765
+
1766
+ btn("◀", 8, "onGoBack:")
1767
+ btn("▶", 38, "onGoForward:")
1768
+ btn("↻", 68, "onReload:")
1769
+ self.home_btn = btn("🏠", 98, "onGoHome:", "Home Page")
1770
+ self.home_btn.setHidden_(not SETTINGS.settings.get("show_home_button", False))
1771
+
1772
+ omni_x = 132 if SETTINGS.settings.get("show_home_button", False) else 102
1773
+ # Clean uniform 30px step for toolbar buttons, omnibox expands dynamically
1774
+ self.omnibox_container = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(omni_x, 3, max(180, w - omni_x - 162), 28))
1775
+ self.toolbar.addSubview_(self.omnibox_container)
1776
+
1777
+ self.site_info_btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(4, 4, 20, 20))
1778
+ self.site_info_btn.setTitle_("🎛️")
1779
+ self.site_info_btn.setBordered_(False)
1780
+ self.site_info_btn.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
1781
+ self.site_info_btn.setTarget_(self)
1782
+ self.site_info_btn.setAction_("onSiteInfo:")
1783
+ self.omnibox_container.addSubview_(self.site_info_btn)
1784
+
1785
+ self.omnibox = ChromeOmniboxField.alloc().initWithFrame_(Cocoa.NSMakeRect(28, 6, self.omnibox_container.bounds().size.width - 56, 16))
1786
+ self.omnibox.setTarget_(self)
1787
+ self.omnibox.setAction_("onOmniboxEnter:")
1788
+ self.omnibox.setDelegate_(self)
1789
+ self.omnibox_container.addSubview_(self.omnibox)
1790
+
1791
+ self.star_btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(self.omnibox_container.bounds().size.width - 26, 4, 20, 20))
1792
+ self.star_btn.setTitle_("☆")
1793
+ self.star_btn.setToolTip_("Bookmark (Cmd+D)")
1794
+ self.star_btn.setBordered_(False)
1795
+ self.star_btn.setFont_(Cocoa.NSFont.systemFontOfSize_(13))
1796
+ self.star_btn.setTarget_(self)
1797
+ self.star_btn.setAction_("onBookmarkCurrentPage:")
1798
+ self.star_btn.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
1799
+ self.omnibox_container.addSubview_(self.star_btn)
1800
+
1801
+ # Fixed: Buttons pushed cleanly to the right edge with uniform 30px spacing
1802
+ self.pip_btn = btn("⧉", w - 154, "onTogglePiP:", "Media Controls")
1803
+ self.dl_btn = btn("⬇", w - 124, "onShowDownloads:", "Downloads")
1804
+ self.share_btn = btn("↗", w - 94, "onShareCurrentPage:", "Share Page")
1805
+ self.zen_btn = btn("⛶", w - 64, "onToggleZenMode:", "Zen Mode (Opt+Esc)")
1806
+ self.menu_btn = btn("⋮", w - 34, "onMenu:", "Menu")
1807
+
1808
+ def relayout_views(self):
1809
+ w, h = self.root_view.bounds().size.width, self.root_view.bounds().size.height
1810
+ bm_visible = SETTINGS.settings.get("bookmarks_bar_visible", True)
1811
+ is_vert = SETTINGS.settings.get("vertical_tabs_enabled", False)
1812
+ vert_w = 48 if SETTINGS.settings.get("vertical_tabs_collapsed", False) else 220
1813
+
1814
+ if not self.tab_bar_visible:
1815
+ # Zen Mode: Title bar stays at the top to isolate traffic lights from web content
1816
+ self.title_bar.setHidden_(False)
1817
+ self.title_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H))
1818
+ self.tab_strip.setHidden_(True)
1819
+ self.toolbar.setHidden_(True)
1820
+ self.bookmarks_bar.setHidden_(True)
1821
+ self.web_container.setFrame_(Cocoa.NSMakeRect(0, 0, w, h - TITLE_BAR_H))
1822
+ elif is_vert:
1823
+ self.title_bar.setHidden_(False)
1824
+ self.title_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H))
1825
+ self.tab_strip.setHidden_(False)
1826
+ self.tab_strip.setFrame_(Cocoa.NSMakeRect(0, 0, vert_w, h - TITLE_BAR_H))
1827
+ cx, cw, y = vert_w, w - vert_w, h - TITLE_BAR_H - 34
1828
+ self.toolbar.setHidden_(False)
1829
+ self.toolbar.setFrame_(Cocoa.NSMakeRect(cx, y, cw, 34))
1830
+ if bm_visible:
1831
+ y -= 24
1832
+ self.bookmarks_bar.setHidden_(False)
1833
+ self.bookmarks_bar.setFrame_(Cocoa.NSMakeRect(cx, y, cw, 24))
1834
+ else:
1835
+ self.bookmarks_bar.setHidden_(True)
1836
+ self.web_container.setFrame_(Cocoa.NSMakeRect(cx, 0, cw, y))
1837
+ else:
1838
+ self.title_bar.setHidden_(False)
1839
+ self.title_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H, w, TITLE_BAR_H))
1840
+ self.tab_strip.setHidden_(False)
1841
+ self.tab_strip.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 34, w, 34))
1842
+ self.toolbar.setHidden_(False)
1843
+ self.toolbar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 68, w, 34))
1844
+ if bm_visible:
1845
+ self.bookmarks_bar.setFrame_(Cocoa.NSMakeRect(0, h - TITLE_BAR_H - 92, w, 24))
1846
+ self.bookmarks_bar.setHidden_(not bm_visible)
1847
+ top_h = TITLE_BAR_H + (92 if bm_visible else 68)
1848
+ self.web_container.setFrame_(Cocoa.NSMakeRect(0, 0, w, h - top_h))
1849
+
1850
+ show_home = SETTINGS.settings.get("show_home_button", False)
1851
+ if hasattr(self, 'home_btn'):
1852
+ self.home_btn.setHidden_(not show_home)
1853
+ omni_x = 132 if show_home else 102
1854
+ tb_w = self.toolbar.bounds().size.width
1855
+
1856
+ # Fixed: Expand Omnibox to fill empty space, eliminated gap between zen and menu buttons
1857
+ if hasattr(self, 'omnibox_container'):
1858
+ box_w = max(180, tb_w - omni_x - 162)
1859
+ self.omnibox_container.setFrame_(Cocoa.NSMakeRect(omni_x, 3, box_w, 28))
1860
+ self.omnibox.setFrame_(Cocoa.NSMakeRect(28, 6, box_w - 56, 16))
1861
+ self.star_btn.setFrameOrigin_(Cocoa.NSMakePoint(box_w - 26, 4))
1862
+
1863
+ if hasattr(self, 'pip_btn'): self.pip_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 154, 4))
1864
+ if hasattr(self, 'dl_btn'): self.dl_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 124, 4))
1865
+ if hasattr(self, 'share_btn'): self.share_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 94, 4))
1866
+ if hasattr(self, 'zen_btn'): self.zen_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 64, 4))
1867
+ if hasattr(self, 'menu_btn'): self.menu_btn.setFrameOrigin_(Cocoa.NSMakePoint(tb_w - 34, 4))
1868
+ if hasattr(self, 'progress_bar'): self.progress_bar.setFrame_(Cocoa.NSMakeRect(0, 0, tb_w, 2.5))
1869
+ if hasattr(self, 'find_bar'): self.find_bar.setFrameOrigin_(Cocoa.NSMakePoint(self.root_view.bounds().size.width - 260, self.web_container.bounds().size.height - 50))
1870
+
1871
+ wv = self.get_active_webview()
1872
+ if wv:
1873
+ wv.setFrame_(self.web_container.bounds())
1874
+ self.relayout_tabs()
1875
+
1876
+ def relayout_tabs(self):
1877
+ for sub in list(self.tab_strip.subviews()):
1878
+ sub.removeFromSuperview()
1879
+ w = self.tab_strip.bounds().size.width
1880
+ is_vert = SETTINGS.settings.get("vertical_tabs_enabled", False)
1881
+ # Because traffic lights sit in the title bar above, horizontal tabs start cleanly at x = 8.0
1882
+ x = 8.0
1883
+ tab_h = 28 if not is_vert else 32
1884
+ y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
1885
+
1886
+ vis = [t for t in self.tabs if not t.get("group_id") or not self.get_group(t["group_id"]).collapsed]
1887
+ pinned_cnt = sum(1 for t in vis if t.get("pinned", False))
1888
+ tab_w = max(60, min(240, (w - x - 44 - (pinned_cnt * 36)) / max(1, len(vis) - pinned_cnt))) if not is_vert else w
1889
+
1890
+ if is_vert:
1891
+ tb = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(8, self.tab_strip.bounds().size.height - 32, 24, 24))
1892
+ tb.setTitle_("≡")
1893
+ tb.setBezelStyle_(Cocoa.NSBezelStyleCircular)
1894
+ tb.setTarget_(self)
1895
+ tb.setAction_("onToggleVerticalTabs:")
1896
+ self.tab_strip.addSubview_(tb)
1897
+
1898
+ curr_grp = None
1899
+ for i, tab in enumerate(self.tabs):
1900
+ if tab.get("group_id"):
1901
+ grp = self.get_group(tab["group_id"])
1902
+ if grp and curr_grp != grp.id:
1903
+ curr_grp = grp.id
1904
+ if is_vert:
1905
+ self.tab_strip.addSubview_(TabGroupHeaderView.alloc().initWithFrame_group_controller_(Cocoa.NSMakeRect(0, y, w, 28), grp, self))
1906
+ y -= 30
1907
+ else:
1908
+ self.tab_strip.addSubview_(TabGroupHeaderView.alloc().initWithFrame_group_controller_(Cocoa.NSMakeRect(x, 3, 60, 28), grp, self))
1909
+ x += 64
1910
+ if grp and grp.collapsed:
1911
+ continue
1912
+
1913
+ cur_w = 36 if tab.get("pinned", False) else tab_w
1914
+ if is_vert:
1915
+ tab_v = ChromeTabView.alloc().initWithFrame_controller_index_(Cocoa.NSMakeRect(0, y, w, tab_h), self, i)
1916
+ self.tab_strip.addSubview_(tab_v)
1917
+ y -= (tab_h + 2)
1918
+ else:
1919
+ tab_v = ChromeTabView.alloc().initWithFrame_controller_index_(Cocoa.NSMakeRect(x, 3, cur_w, tab_h), self, i)
1920
+ self.tab_strip.addSubview_(tab_v)
1921
+ x += cur_w + 2
1922
+
1923
+ btn_add = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(8 if is_vert else x + 4, y if is_vert else 4, 26, 26))
1924
+ btn_add.setTitle_("+")
1925
+ btn_add.setBezelStyle_(Cocoa.NSBezelStyleCircular)
1926
+ btn_add.setTarget_(self)
1927
+ btn_add.setAction_("onNewTab:")
1928
+ self.tab_strip.addSubview_(btn_add)
1929
+ self.tab_strip.setNeedsDisplay_(True)
1930
+
1931
+ def onToggleVerticalTabs_(self, sender):
1932
+ SETTINGS.settings["vertical_tabs_collapsed"] = not SETTINGS.settings.get("vertical_tabs_collapsed", False)
1933
+ SETTINGS.save()
1934
+ self.relayout_views()
1935
+
1936
+ def add_tab(self, url, pinned=False):
1937
+ config = WebKit.WKWebViewConfiguration.alloc().init()
1938
+ if self.is_incognito:
1939
+ apply_incognito_preferences(config, shared_data_store=self.incognito_data_store)
1940
+ else:
1941
+ ds = WebKit.WKWebsiteDataStore.defaultDataStore()
1942
+ pcfg = create_nw_proxy_config("127.0.0.1", LOCAL_PROXY_PORT)
1943
+ if pcfg and hasattr(ds, "setProxyConfigurations_"):
1944
+ try:
1945
+ ds.setProxyConfigurations_([pcfg])
1946
+ except Exception:
1947
+ pass
1948
+ config.setWebsiteDataStore_(ds)
1949
+ try:
1950
+ config.preferences().setValue_forKey_(True, "developerExtrasEnabled")
1951
+ config.preferences().setValue_forKey_(True, "fullScreenEnabled")
1952
+ if hasattr(config.preferences(), "setJavaScriptCanOpenWindowsAutomatically_"):
1953
+ config.preferences().setJavaScriptCanOpenWindowsAutomatically_(not SETTINGS.settings.get("block_popups", True))
1954
+ if hasattr(config.preferences(), "setJavaScriptEnabled_"):
1955
+ config.preferences().setJavaScriptEnabled_(SETTINGS.settings.get("js_enabled", True))
1956
+ if hasattr(config.preferences(), "setElementFullscreenEnabled_"):
1957
+ config.preferences().setElementFullscreenEnabled_(True)
1958
+ except Exception:
1959
+ pass
1960
+
1961
+ config.setURLSchemeHandler_forURLScheme_(InternalSchemeHandler.alloc().init(), "browser")
1962
+ uc = WebKit.WKUserContentController.alloc().init()
1963
+ uc.addScriptMessageHandler_name_(self, "bridge")
1964
+ uc.addScriptMessageHandler_name_(self, "audio")
1965
+ uc.addScriptMessageHandler_name_(self, "terminalInput")
1966
+ uc.addScriptMessageHandler_name_(self, "terminalResize")
1967
+
1968
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(AUDIO_MONITOR_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1969
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IMAGE_CONTEXT_MENU_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
1970
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(KEYCHAIN_AUTOSAVE_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1971
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IN_WINDOW_FULLSCREEN_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1972
+
1973
+ if SETTINGS.settings.get("cosmetic_adblock_enabled", True):
1974
+ cosmetic_js = "(function() { const st = document.createElement('style'); st.id = '__pybrowser_cosmetic_adblock'; st.textContent = `" + COSMETIC_ADBLOCK_CSS + "`; (document.head || document.documentElement).appendChild(st); })();"
1975
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(cosmetic_js, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
1976
+
1977
+ if SETTINGS.settings.get("yt_adblock_enabled", True):
1978
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(YOUTUBE_AD_SHIELD_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1979
+
1980
+ for script in get_installed_userscripts():
1981
+ if not script["enabled"]:
1982
+ continue
1983
+ matches = script.get("match", [])
1984
+ conds = []
1985
+ for m in matches:
1986
+ if m == "<all_urls>":
1987
+ conds.append("true")
1988
+ else:
1989
+ escaped_slash = r"\/"
1990
+ reg = m.replace("*", ".*").replace("/", escaped_slash)
1991
+ conds.append(f"new RegExp('^{reg}$').test(location.href)")
1992
+ cond_str = " || ".join(conds) if conds else "true"
1993
+ wrapped = f"if ({cond_str}) {{\n" + script["code"] + "\n}}"
1994
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(wrapped, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1995
+
1996
+ if WK_RULE_LIST and SETTINGS.settings.get("adblock_enabled", True):
1997
+ uc.addContentRuleList_(WK_RULE_LIST)
1998
+ config.setUserContentController_(uc)
1999
+
2000
+ wv = BrowserWebView.alloc().initWithFrame_configuration_(self.web_container.bounds(), config)
2001
+ wv.controller = self
2002
+ wv.setAutoresizingMask_(Cocoa.NSViewWidthSizable | Cocoa.NSViewHeightSizable)
2003
+ wv.setNavigationDelegate_(self)
2004
+ wv.setUIDelegate_(self)
2005
+ wv.setCustomUserAgent_(SETTINGS.get_user_agent())
2006
+ if hasattr(wv, "setInspectable_"):
2007
+ try:
2008
+ wv.setInspectable_(True)
2009
+ except Exception:
2010
+ pass
2011
+
2012
+ initial_zoom = float(SETTINGS.settings.get("default_zoom", 0.8))
2013
+ tab = {
2014
+ "id": str(uuid.uuid4()), "webview": wv, "url": url, "title": "New Tab",
2015
+ "favicon": None, "pinned": pinned, "group_id": None, "zoom": initial_zoom,
2016
+ "is_sleeping": False, "has_audio": False, "is_muted": False, "last_active_timestamp": time.time()
2017
+ }
2018
+ self.apply_zoom_to_webview(wv, tab)
2019
+ for k in ("URL", "title", "estimatedProgress"):
2020
+ try:
2021
+ wv.addObserver_forKeyPath_options_context_(self, k, Cocoa.NSKeyValueObservingOptionNew, None)
2022
+ except Exception:
2023
+ pass
2024
+
2025
+ self.tabs.append(tab)
2026
+ self.relayout_tabs()
2027
+ self.switch_to_tab(len(self.tabs) - 1)
2028
+ req_url = url
2029
+ if SETTINGS.settings.get("https_only", True) and req_url.startswith("http://") and not req_url.startswith("http://localhost") and not req_url.startswith("http://127.0.0.1"):
2030
+ req_url = "https://" + req_url[7:]
2031
+ wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(req_url)))
2032
+ self.save_session()
2033
+
2034
+ def get_tab_index_by_id(self, tab_id):
2035
+ for i, t in enumerate(self.tabs):
2036
+ if t.get("id") == tab_id:
2037
+ return i
2038
+ return None
2039
+
2040
+ def switch_to_tab(self, index):
2041
+ if not (0 <= index < len(self.tabs)):
2042
+ return
2043
+ self.active_tab_index = index
2044
+ self.omnibox_panel.hide()
2045
+
2046
+ tab = self.tabs[index]
2047
+ tab["last_active_timestamp"] = time.time()
2048
+ if tab.get("is_sleeping"):
2049
+ self.wake_tab(index)
2050
+ wv = tab["webview"]
2051
+
2052
+ # Fixed: Keep webviews attached and toggle visibility with setHidden_
2053
+ # This completely preserves DOM focus, text inputs, and render state
2054
+ for i, t in enumerate(self.tabs):
2055
+ twv = t.get("webview")
2056
+ if twv:
2057
+ is_current = (i == index)
2058
+ if twv.superview() != self.web_container and is_current:
2059
+ self.web_container.addSubview_(twv)
2060
+ twv.setHidden_(not is_current)
2061
+
2062
+ wv.setFrame_(self.web_container.bounds())
2063
+ self.omnibox.set_url(tab.get("url", ""))
2064
+ self.apply_zoom_to_webview(wv, tab)
2065
+ self.update_star_status()
2066
+ self.update_progress_for_webview(wv)
2067
+ self.relayout_tabs()
2068
+
2069
+ # Fixed: Restore keyboard focus and text cursor to the active input element automatically
2070
+ self.window().makeFirstResponder_(wv)
2071
+ wv.evaluateJavaScript_completionHandler_(
2072
+ "if (document.activeElement && typeof document.activeElement.focus === 'function') { document.activeElement.focus(); }",
2073
+ None
2074
+ )
2075
+
2076
+ def close_tab(self, index):
2077
+ if 0 <= index < len(self.tabs):
2078
+ tab = self.tabs.pop(index)
2079
+ if tab.get("url") and not tab["url"].startswith("browser://"):
2080
+ self.closed_tabs.append(tab["url"])
2081
+ wv = tab.get("webview")
2082
+ if wv:
2083
+ TERMINAL_MANAGER.close_for_webview(wv)
2084
+ for k in ("URL", "title", "estimatedProgress"):
2085
+ try:
2086
+ wv.removeObserver_forKeyPath_(self, k)
2087
+ except Exception:
2088
+ pass
2089
+ if wv.superview():
2090
+ wv.removeFromSuperview()
2091
+ if not self.tabs:
2092
+ self.window().close()
2093
+ else:
2094
+ if index < self.active_tab_index:
2095
+ self.active_tab_index -= 1
2096
+ self.relayout_tabs()
2097
+ elif index == self.active_tab_index:
2098
+ self.switch_to_tab(max(0, min(index, len(self.tabs) - 1)))
2099
+ else:
2100
+ self.relayout_tabs()
2101
+ self.save_session()
2102
+
2103
+ def reopen_last_closed_tab(self):
2104
+ if self.closed_tabs:
2105
+ self.add_tab(self.closed_tabs.pop())
2106
+
2107
+ def toggle_pin_tab(self, index):
2108
+ if 0 <= index < len(self.tabs):
2109
+ self.tabs[index]["pinned"] = not self.tabs[index].get("pinned", False)
2110
+ self.relayout_tabs()
2111
+ self.save_session()
2112
+
2113
+ def duplicate_tab(self, index):
2114
+ if 0 <= index < len(self.tabs) and self.tabs[index].get("url"):
2115
+ self.add_tab(self.tabs[index]["url"])
2116
+
2117
+ def toggle_mute(self, index):
2118
+ if 0 <= index < len(self.tabs):
2119
+ tab = self.tabs[index]
2120
+ tab["is_muted"] = not tab.get("is_muted", False)
2121
+ wv = tab.get("webview")
2122
+ if wv:
2123
+ if hasattr(wv, "setMediaMuted_"):
2124
+ wv.setMediaMuted_(tab["is_muted"])
2125
+ else:
2126
+ wv.evaluateJavaScript_completionHandler_(f"document.querySelectorAll('audio, video').forEach(el => el.muted = {'true' if tab['is_muted'] else 'false'});", None)
2127
+ self.relayout_tabs()
2128
+
2129
+ def create_group_for_tab(self, index):
2130
+ if 0 <= index < len(self.tabs):
2131
+ gid = str(uuid.uuid4())
2132
+ self.groups.append(TabGroup(gid, "Group", "Blue", False))
2133
+ self.tabs[index]["group_id"] = gid
2134
+ self.relayout_tabs()
2135
+ self.save_session()
2136
+
2137
+ def get_group(self, gid):
2138
+ return next((g for g in self.groups if g.id == gid), None)
2139
+
2140
+ def check_sleeping_tabs(self):
2141
+ now = time.time()
2142
+ for i, tab in enumerate(self.tabs):
2143
+ if i == self.active_tab_index or tab.get("has_audio") or tab.get("is_sleeping"):
2144
+ continue
2145
+ if now - tab.get("last_active_timestamp", now) > 1800:
2146
+ self.sleep_tab(i)
2147
+
2148
+ def sleep_tab(self, index):
2149
+ if not (0 <= index < len(self.tabs)) or index == self.active_tab_index:
2150
+ return
2151
+ tab = self.tabs[index]
2152
+ if tab.get("is_sleeping"):
2153
+ return
2154
+ wv = tab.get("webview")
2155
+ if wv:
2156
+ if wv.URL() and not wv.URL().absoluteString().startswith("about:"):
2157
+ tab["url"] = wv.URL().absoluteString()
2158
+ tab["is_sleeping"] = True
2159
+ wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_("about:blank")))
2160
+ else:
2161
+ tab["is_sleeping"] = True
2162
+ self.relayout_tabs()
2163
+
2164
+ def wake_tab(self, index):
2165
+ if not (0 <= index < len(self.tabs)):
2166
+ return
2167
+ tab = self.tabs[index]
2168
+ if not tab.get("is_sleeping"):
2169
+ return
2170
+ tab["is_sleeping"] = False
2171
+ wv = tab.get("webview")
2172
+ if wv and tab.get("url"):
2173
+ wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(tab["url"])))
2174
+ self.relayout_tabs()
2175
+
2176
+ def is_url_bookmarked(self, url):
2177
+ if not url:
2178
+ return False
2179
+ try:
2180
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
2181
+ bms = json.load(f)
2182
+ if isinstance(bms, list):
2183
+ return any(isinstance(b, dict) and b.get("url") == url for b in bms)
2184
+ except Exception:
2185
+ return False
2186
+ return False
2187
+
2188
+ def update_star_status(self):
2189
+ wv = self.get_active_webview()
2190
+ url = wv.URL().absoluteString() if wv and wv.URL() else ""
2191
+ if hasattr(self, 'star_btn'):
2192
+ self.star_btn.setTitle_("★" if self.is_url_bookmarked(url) else "☆")
2193
+
2194
+ def onBookmarkCurrentPage_(self, sender=None):
2195
+ wv = self.get_active_webview()
2196
+ if not wv or not wv.URL():
2197
+ return
2198
+ url = wv.URL().absoluteString()
2199
+ if not url or url.startswith("browser://"):
2200
+ return
2201
+ title = str(wv.title()) if wv.title() else url
2202
+
2203
+ try:
2204
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
2205
+ bms = json.load(f)
2206
+ if not isinstance(bms, list):
2207
+ bms = []
2208
+ except Exception:
2209
+ bms = []
2210
+
2211
+ if self.is_url_bookmarked(url):
2212
+ bms = [b for b in bms if isinstance(b, dict) and b.get("url") != url]
2213
+ else:
2214
+ bms.append({"title": title, "url": url})
2215
+
2216
+ atomic_write_json(BOOKMARKS_FILE, bms)
2217
+ self.bookmarks_bar.reload_bookmarks()
2218
+ self.update_star_status()
2219
+
2220
+ def perform_download_image(self, img_url):
2221
+ if not SETTINGS.settings.get("downloads_enabled", True) or not img_url:
2222
+ return
2223
+ wv = self.get_active_webview()
2224
+ suggested = sanitize_suggested_filename(img_url)
2225
+ dest_dir = validate_and_get_download_dir()
2226
+ dest_path = os.path.join(dest_dir, suggested)
2227
+ base, ext = os.path.splitext(dest_path)
2228
+ counter = 1
2229
+ while os.path.exists(dest_path):
2230
+ dest_path = f"{base} ({counter}){ext}"
2231
+ counter += 1
2232
+
2233
+ dl_id = self.register_download(os.path.basename(dest_path), dest_path, None)
2234
+ download_image_data(img_url, dest_path, webview=wv, completion_callback=lambda ok: self.finish_download(None, "Completed" if ok else "Failed", dl_id=dl_id))
2235
+
2236
+ def perform_save_image_as(self, img_url):
2237
+ if not SETTINGS.settings.get("downloads_enabled", True) or not img_url:
2238
+ return
2239
+ wv = self.get_active_webview()
2240
+ suggested = sanitize_suggested_filename(img_url)
2241
+ default_dir = validate_and_get_download_dir()
2242
+
2243
+ panel = Cocoa.NSSavePanel.savePanel()
2244
+ panel.setNameFieldStringValue_(suggested)
2245
+ panel.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(default_dir))
2246
+ if panel.runModal() == Cocoa.NSModalResponseOK and panel.URL():
2247
+ dest_path = panel.URL().path()
2248
+ dl_id = self.register_download(os.path.basename(dest_path), dest_path, None)
2249
+ download_image_data(img_url, dest_path, webview=wv, completion_callback=lambda ok: self.finish_download(None, "Completed" if ok else "Failed", dl_id=dl_id))
2250
+
2251
+ def perform_copy_image_address(self, img_url):
2252
+ if not img_url:
2253
+ return
2254
+ pb = Cocoa.NSPasteboard.generalPasteboard()
2255
+ pb.clearContents()
2256
+ pb.setString_forType_(img_url, Cocoa.NSPasteboardTypeString)
2257
+
2258
+ def apply_zoom_to_webview(self, wv, tab=None):
2259
+ if not wv:
2260
+ return
2261
+ target_zoom = tab.get("zoom", float(SETTINGS.settings.get("default_zoom", 0.8))) if tab else float(SETTINGS.settings.get("default_zoom", 0.8))
2262
+ target_zoom = max(0.5, min(2.0, target_zoom))
2263
+ try:
2264
+ if hasattr(wv, "setAllowsMagnification_"): wv.setAllowsMagnification_(False)
2265
+ if hasattr(wv, "setMagnification_"): wv.setMagnification_(1.0)
2266
+ wv.setPageZoom_(target_zoom)
2267
+ except Exception:
2268
+ pass
2269
+
2270
+ def restore_normal_zoom(self):
2271
+ wv = self.get_active_webview()
2272
+ if wv:
2273
+ self.apply_zoom_to_webview(wv)
2274
+ wv.evaluateJavaScript_completionHandler_(REFLOW_RESTORE_JS, None)
2275
+
2276
+ def handle_fullscreen_enter(self, auto_hide_bars=True):
2277
+ if auto_hide_bars:
2278
+ if not self.tab_bar_visible:
2279
+ # Tab was already in Zen mode before entering fullscreen
2280
+ self.zen_auto_entered = False
2281
+ else:
2282
+ # Tab was in normal mode, auto-enter Zen mode for fullscreen
2283
+ self.zen_auto_entered = True
2284
+ self.tab_bar_visible = False
2285
+ self.relayout_views()
2286
+ wv = self.get_active_webview()
2287
+ if wv:
2288
+ try:
2289
+ wv.setFrame_(self.web_container.bounds())
2290
+ wv.evaluateJavaScript_completionHandler_("if (window.setTerminalFullscreen) window.setTerminalFullscreen(true);", None)
2291
+ except Exception:
2292
+ pass
2293
+
2294
+ def handle_fullscreen_exit(self, restore_bars=True):
2295
+ if restore_bars:
2296
+ # Fixed: Only exit Zen mode if it was entered automatically by fullscreen
2297
+ if self.zen_auto_entered:
2298
+ self.tab_bar_visible = True
2299
+ self.zen_auto_entered = False
2300
+ self.relayout_views()
2301
+ # If tab was already in Zen mode before fullscreen, keep Zen mode on!
2302
+ wv = self.get_active_webview()
2303
+ if wv:
2304
+ wv.evaluateJavaScript_completionHandler_("if (window.setTerminalFullscreen) window.setTerminalFullscreen(false);", None)
2305
+ self.restore_normal_zoom()
2306
+
2307
+ def handle_escape_key(self):
2308
+ if self.find_bar and not self.find_bar.isHidden():
2309
+ self.find_bar.setHidden_(True)
2310
+ return
2311
+ wv = self.get_active_webview()
2312
+ if not self.tab_bar_visible:
2313
+ self.handle_fullscreen_exit(restore_bars=True)
2314
+ return
2315
+ if wv:
2316
+ def on_done(res, err):
2317
+ if res or not self.tab_bar_visible:
2318
+ self.handle_fullscreen_exit(restore_bars=True)
2319
+ else:
2320
+ self.restore_normal_zoom()
2321
+ wv.evaluateJavaScript_completionHandler_(ESCAPE_CHECK_JS, on_done)
2322
+ else:
2323
+ if not self.tab_bar_visible:
2324
+ self.handle_fullscreen_exit(restore_bars=True)
2325
+ else:
2326
+ self.restore_normal_zoom()
2327
+
2328
+ def toggle_tab_bar(self):
2329
+ self.zen_auto_entered = False
2330
+ self.tab_bar_visible = not self.tab_bar_visible
2331
+ self.relayout_views()
2332
+ wv = self.get_active_webview()
2333
+ if wv:
2334
+ is_fs = not self.tab_bar_visible
2335
+ wv.evaluateJavaScript_completionHandler_(f"if (window.setTerminalFullscreen) window.setTerminalFullscreen({'true' if is_fs else 'false'});", None)
2336
+
2337
+ def onToggleZenMode_(self, sender=None): self.toggle_tab_bar()
2338
+
2339
+ def onZoomIn_(self, sender=None):
2340
+ if 0 <= self.active_tab_index < len(self.tabs):
2341
+ tab = self.tabs[self.active_tab_index]
2342
+ z = min(2.0, round(tab.get("zoom", 0.8) + 0.1, 2))
2343
+ tab["zoom"] = z
2344
+ self.apply_zoom_to_webview(tab.get("webview"), tab)
2345
+
2346
+ def onZoomOut_(self, sender=None):
2347
+ if 0 <= self.active_tab_index < len(self.tabs):
2348
+ tab = self.tabs[self.active_tab_index]
2349
+ z = max(0.5, round(tab.get("zoom", 0.8) - 0.1, 2))
2350
+ tab["zoom"] = z
2351
+ self.apply_zoom_to_webview(tab.get("webview"), tab)
2352
+
2353
+ def onResetZoom_(self, sender=None):
2354
+ if 0 <= self.active_tab_index < len(self.tabs):
2355
+ tab = self.tabs[self.active_tab_index]
2356
+ tab["zoom"] = float(SETTINGS.settings.get("default_zoom", 0.8))
2357
+ self.apply_zoom_to_webview(tab.get("webview"), tab)
2358
+ self.restore_normal_zoom()
2359
+
2360
+ def onInspectElement_(self, sender=None):
2361
+ wv = self.get_active_webview()
2362
+ if wv:
2363
+ try:
2364
+ Cocoa.NSApp.sendAction_to_from_(Cocoa.NSSelectorFromString("_showInspector:"), wv, None)
2365
+ except Exception:
2366
+ pass
2367
+
2368
+ def onMenu_(self, sender):
2369
+ menu = Cocoa.NSMenu.alloc().init()
2370
+ menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t").setTarget_(self)
2371
+ menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(Cocoa.NSApp.delegate())
2372
+ inc_item = menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
2373
+ inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
2374
+ inc_item.setTarget_(Cocoa.NSApp.delegate())
2375
+ menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "openTerminal:", "T").setTarget_(self)
2376
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2377
+ menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
2378
+ bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
2379
+ menu.addItemWithTitle_action_keyEquivalent_(bm_text, "toggleBookmarksBar:", "b").setTarget_(self)
2380
+ menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "").setTarget_(self)
2381
+ menu.addItemWithTitle_action_keyEquivalent_("Find in Page", "onFindInPage:", "f").setTarget_(self)
2382
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2383
+ menu.addItemWithTitle_action_keyEquivalent_("Settings", "openSettings:", ",").setTarget_(self)
2384
+ menu.addItemWithTitle_action_keyEquivalent_("History", "openHistory:", "y").setTarget_(self)
2385
+ menu.addItemWithTitle_action_keyEquivalent_("Bookmarks", "openBookmarks:", "O").setTarget_(self)
2386
+ menu.addItemWithTitle_action_keyEquivalent_("Downloads", "openDownloads:", "D").setTarget_(self)
2387
+ menu.addItemWithTitle_action_keyEquivalent_("Extensions", "openExtensions:", "e").setTarget_(self)
2388
+ menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(Cocoa.NSApp.delegate())
2389
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2390
+ menu.addItemWithTitle_action_keyEquivalent_("Inspect Element", "onInspectElement:", "i").setTarget_(self)
2391
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2392
+ menu.addItemWithTitle_action_keyEquivalent_("Quit PyBrowser", "terminate:", "q").setTarget_(Cocoa.NSApp)
2393
+ menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
2394
+
2395
+ def toggleBookmarksBar_(self, sender=None):
2396
+ SETTINGS.settings["bookmarks_bar_visible"] = not SETTINGS.settings.get("bookmarks_bar_visible", True)
2397
+ SETTINGS.save()
2398
+ self.relayout_views()
2399
+
2400
+ def openTerminal_(self, s=None): self.add_tab("browser://terminal")
2401
+ def openSettings_(self, s=None): self.add_tab("browser://settings")
2402
+ def openHistory_(self, s=None): self.add_tab("browser://history")
2403
+ def openBookmarks_(self, s=None): self.add_tab("browser://bookmarks")
2404
+ def openDownloads_(self, s=None): self.add_tab("browser://downloads")
2405
+ def openExtensions_(self, s=None): self.add_tab("browser://extensions")
2406
+ def onNewTab_(self, s=None): self.add_tab(DEFAULT_URL)
2407
+ def onCloseTab_(self, s=None): self.close_tab(self.active_tab_index)
2408
+ def onReopenTab_(self, s=None): self.reopen_last_closed_tab()
2409
+
2410
+ def onGoBack_(self, s=None):
2411
+ wv = self.get_active_webview()
2412
+ if wv and wv.canGoBack(): wv.goBack_(s)
2413
+
2414
+ def onGoForward_(self, s=None):
2415
+ wv = self.get_active_webview()
2416
+ if wv and wv.canGoForward(): wv.goForward_(s)
2417
+
2418
+ def onReload_(self, s=None):
2419
+ wv = self.get_active_webview()
2420
+ if wv: wv.reload()
2421
+
2422
+ def onGoHome_(self, s=None):
2423
+ url = SETTINGS.settings.get("home_url", "https://www.google.com")
2424
+ wv = self.get_active_webview()
2425
+ if wv: wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(url)))
2426
+
2427
+ def onFocusOmnibox_(self, s=None): self.window().makeFirstResponder_(self.omnibox)
2428
+ def onFindInPage_(self, s=None):
2429
+ self.find_bar.setHidden_(False)
2430
+ self.window().makeFirstResponder_(self.find_bar.search_field)
2431
+
2432
+ def onTogglePiP_(self, s=None):
2433
+ Cocoa.NSApp.delegate().pip_panel.browser = self
2434
+ Cocoa.NSApp.delegate().pip_panel.toggle()
2435
+ wv = self.get_active_webview()
2436
+ if wv:
2437
+ wv.evaluateJavaScript_completionHandler_("const v = document.querySelector('video'); if (v && v.webkitSetPresentationMode) v.webkitSetPresentationMode(v.webkitPresentationMode === 'picture-in-picture' ? 'inline' : 'picture-in-picture');", None)
2438
+
2439
+ def onShareCurrentPage_(self, sender):
2440
+ wv = self.get_active_webview()
2441
+ if wv and wv.URL():
2442
+ Cocoa.NSSharingServicePicker.alloc().initWithItems_([wv.URL()]).showRelativeToRect_ofView_preferredEdge_(sender.bounds(), sender, Cocoa.NSMinYEdge)
2443
+
2444
+ def onShowDownloads_(self, sender):
2445
+ menu = Cocoa.NSMenu.alloc().init()
2446
+ if not self.downloads:
2447
+ menu.addItemWithTitle_action_keyEquivalent_("No Recent Downloads", "", "").setEnabled_(False)
2448
+ else:
2449
+ for dl in reversed(self.downloads[-10:]):
2450
+ item = menu.addItemWithTitle_action_keyEquivalent_(f"{dl['name']} ({dl['status']})", "revealDownload:", "")
2451
+ item.setRepresentedObject_(dl['path'])
2452
+ item.setTarget_(self)
2453
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2454
+ menu.addItemWithTitle_action_keyEquivalent_("Open Downloads Manager", "openDownloads:", "").setTarget_(self)
2455
+ menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
2456
+
2457
+ def revealDownload_(self, sender):
2458
+ path = sender.representedObject()
2459
+ if path and os.path.exists(path):
2460
+ Cocoa.NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path, None)
2461
+
2462
+ def onSiteInfo_(self, sender):
2463
+ wv = self.get_active_webview()
2464
+ if not wv: return
2465
+ url = wv.URL().absoluteString() if wv.URL() else "browser://newtab"
2466
+ parsed = Foundation.NSURL.URLWithString_(url)
2467
+ menu = Cocoa.NSMenu.alloc().init()
2468
+ menu.addItemWithTitle_action_keyEquivalent_(f"Host: {parsed.host() or 'Internal Page'}", "", "").setEnabled_(False)
2469
+ menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if parsed.scheme() == "https" else "Connection: Insecure / Local", "", "").setEnabled_(False)
2470
+ menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2471
+ menu.addItemWithTitle_action_keyEquivalent_("Copy Current URL", "onCopyUrl:", "").setTarget_(self)
2472
+ menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
2473
+
2474
+ def onCopyUrl_(self, sender):
2475
+ wv = self.get_active_webview()
2476
+ if wv and wv.URL():
2477
+ pb = Cocoa.NSPasteboard.generalPasteboard()
2478
+ pb.clearContents()
2479
+ pb.setString_forType_(wv.URL().absoluteString(), Cocoa.NSPasteboardTypeString)
2480
+
2481
+ def onSwitchTab1_(self, s): self.switch_to_tab(0)
2482
+ def onSwitchTab2_(self, s): self.switch_to_tab(1)
2483
+ def onSwitchTab3_(self, s): self.switch_to_tab(2)
2484
+ def onSwitchTab4_(self, s): self.switch_to_tab(3)
2485
+ def onSwitchTab5_(self, s): self.switch_to_tab(4)
2486
+ def onSwitchTab6_(self, s): self.switch_to_tab(5)
2487
+ def onSwitchTab7_(self, s): self.switch_to_tab(6)
2488
+ def onSwitchTab8_(self, s): self.switch_to_tab(7)
2489
+ def onSwitchTab9_(self, s):
2490
+ if self.tabs: self.switch_to_tab(len(self.tabs) - 1)
2491
+
2492
+ def get_active_webview(self):
2493
+ if 0 <= self.active_tab_index < len(self.tabs):
2494
+ return self.tabs[self.active_tab_index].get("webview")
2495
+ return None
2496
+
2497
+ def onOmniboxEnter_(self, sender):
2498
+ self.omnibox_panel.hide()
2499
+ val = self.omnibox.stringValue().strip()
2500
+ if not val:
2501
+ return
2502
+ if val.startswith("chrome://"): val = "browser://" + val[9:]
2503
+ elif val.startswith("edge://"): val = "browser://" + val[7:]
2504
+ if val.startswith("localhost") or val.startswith("127.0.0.1"): val = "http://" + val
2505
+ elif not (val.startswith("http://") or val.startswith("https://") or val.startswith("browser://")):
2506
+ val = "https://" + val if ("." in val and " " not in val) else SETTINGS.get_search_url(val)
2507
+
2508
+ if SETTINGS.settings.get("search_in_new_tab", False):
2509
+ self.add_tab(val)
2510
+ else:
2511
+ wv = self.get_active_webview()
2512
+ if wv:
2513
+ wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(val)))
2514
+ self.window().makeFirstResponder_(self.web_container)
2515
+
2516
+ def controlTextDidChange_(self, notif):
2517
+ if notif.object() == self.omnibox:
2518
+ self.omnibox_panel.handle_text_change(self.omnibox.stringValue())
2519
+
2520
+ def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
2521
+ return self.omnibox_panel.handle_command_selector(commandSelector)
2522
+
2523
+ def register_download(self, name, path, obj):
2524
+ dl_id = str(uuid.uuid4())
2525
+ self.downloads.append({"id": dl_id, "name": name, "path": path, "status": "Downloading", "progress": 0.0, "obj": obj})
2526
+ self.dl_btn.setTitle_(f"⬇ ({len(self.downloads)})")
2527
+ self.sync_downloads_page()
2528
+ return dl_id
2529
+
2530
+ def finish_download(self, obj, status, dl_id=None, dl_path=None):
2531
+ for d in reversed(self.downloads):
2532
+ if (dl_id and d.get("id") == dl_id) or (dl_path and d.get("path") == dl_path) or (obj and d.get("obj") == obj):
2533
+ d["status"], d["progress"] = status, 1.0
2534
+ break
2535
+ self.sync_downloads_page()
2536
+
2537
+ def sync_downloads_page(self):
2538
+ for t in self.tabs:
2539
+ wv = t.get("webview")
2540
+ if wv and wv.URL() and "browser://downloads" in wv.URL().absoluteString():
2541
+ for d in self.downloads:
2542
+ id_json = json.dumps(d.get('id'))
2543
+ name_json = json.dumps(d.get('name'))
2544
+ path_json = json.dumps(d.get('path'))
2545
+ status_json = json.dumps(d.get('status'))
2546
+ prog = d.get('progress', 0.0)
2547
+ wv.evaluateJavaScript_completionHandler_(f"window.updateDownload({id_json}, {name_json}, {path_json}, {status_json}, {prog});", None)
2548
+
2549
+ def show_password_banner(self, host, username, password):
2550
+ if not host or not password:
2551
+ return
2552
+ if hasattr(self, 'pw_banner') and self.pw_banner and self.pw_banner.superview():
2553
+ return
2554
+ w = self.web_container.bounds().size.width
2555
+ self.pw_banner = Cocoa.NSVisualEffectView.alloc().initWithFrame_(Cocoa.NSMakeRect(0, self.web_container.bounds().size.height - 40, w, 40))
2556
+ self.pw_banner.setMaterial_(Cocoa.NSVisualEffectMaterialPopover)
2557
+ self.pw_banner.setAutoresizingMask_(Cocoa.NSViewWidthSizable | Cocoa.NSViewMinYMargin)
2558
+
2559
+ lbl = Cocoa.NSTextField.labelWithString_(f"Save password for {host} in PyBrowser Keychain?")
2560
+ lbl.setFrameOrigin_(Cocoa.NSMakePoint(20, 11))
2561
+ lbl.setFont_(Cocoa.NSFont.systemFontOfSize_(12))
2562
+ self.pw_banner.addSubview_(lbl)
2563
+
2564
+ btn_save = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(w - 90, 8, 75, 24))
2565
+ btn_save.setTitle_("Save")
2566
+ btn_save.setBezelStyle_(Cocoa.NSBezelStyleRounded)
2567
+ btn_save.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
2568
+ btn_save.setAction_("onSavePassword:")
2569
+ btn_save.setTarget_(self)
2570
+ self.pw_banner.addSubview_(btn_save)
2571
+
2572
+ btn_never = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(w - 175, 8, 75, 24))
2573
+ btn_never.setTitle_("Never")
2574
+ btn_never.setBezelStyle_(Cocoa.NSBezelStyleRounded)
2575
+ btn_never.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
2576
+ btn_never.setAction_("onDismissPassword:")
2577
+ btn_never.setTarget_(self)
2578
+ self.pw_banner.addSubview_(btn_never)
2579
+
2580
+ self.pending_pw = {"host": host, "username": username, "password": password}
2581
+ self.web_container.addSubview_(self.pw_banner)
2582
+
2583
+ def onSavePassword_(self, sender):
2584
+ if hasattr(self, 'pending_pw'):
2585
+ p = self.pending_pw
2586
+ save_to_keychain(p["host"], p["username"], p["password"])
2587
+ SETTINGS.settings.setdefault("saved_logins", {})[p["host"]] = p["username"]
2588
+ SETTINGS.save()
2589
+ self.onDismissPassword_(sender)
2590
+
2591
+ def onDismissPassword_(self, sender):
2592
+ if hasattr(self, 'pw_banner') and self.pw_banner:
2593
+ self.pw_banner.removeFromSuperview()
2594
+ self.pw_banner = None
2595
+
2596
+ def update_progress_for_webview(self, wv):
2597
+ if wv != self.get_active_webview():
2598
+ return
2599
+ if not wv.isLoading():
2600
+ self.progress_bar.setHidden_(True)
2601
+ self.progress_bar.setProgress_(0.0)
2602
+ return
2603
+ p = max(0.1, float(wv.estimatedProgress()))
2604
+ self.progress_bar.setHidden_(False)
2605
+ self.progress_bar.setProgress_(p)
2606
+ if p >= 1.0:
2607
+ Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(0.2, False, lambda t: self.progress_bar.setHidden_(True))
2608
+
2609
+ def userContentController_didReceiveScriptMessage_(self, ucc, message):
2610
+ name = message.name()
2611
+ if name == "terminalInput":
2612
+ TERMINAL_MANAGER.write_active(message.webView(), message.body())
2613
+ return
2614
+ elif name == "terminalResize":
2615
+ b = message.body()
2616
+ TERMINAL_MANAGER.get_or_create(message.webView(), b.get("cols"), b.get("rows"))
2617
+ return
2618
+ elif name == "audio":
2619
+ b = message.body() if isinstance(message.body(), dict) else {}
2620
+ has_a = (b.get("cmd") == "audio_play")
2621
+ for t in self.tabs:
2622
+ if t.get("webview") == message.webView():
2623
+ t["has_audio"] = has_a
2624
+ break
2625
+ self.relayout_tabs()
2626
+ return
2627
+
2628
+ body = message.body()
2629
+ cmd = body.get("cmd") if isinstance(body, dict) else None
2630
+
2631
+ if cmd == "open_url":
2632
+ if body.get("url"):
2633
+ self.add_tab(body.get("url"))
2634
+ elif cmd == "toggle_fullscreen_terminal":
2635
+ self.toggle_tab_bar()
2636
+ elif cmd == "context_target_image":
2637
+ self.last_context_image_url = body.get("src")
2638
+ elif cmd == "blob_image_data":
2639
+ data_url, dest_path = body.get("data"), body.get("path")
2640
+ if data_url and dest_path:
2641
+ try:
2642
+ raw = base64.b64decode(data_url.split(",", 1)[1])
2643
+ with open(dest_path, "wb") as f:
2644
+ f.write(raw)
2645
+ self.finish_download(None, "Completed", dl_path=dest_path)
2646
+ except Exception as e:
2647
+ print(f"[Blob Image Save Error] {e}")
2648
+ self.finish_download(None, "Failed", dl_path=dest_path)
2649
+ elif cmd == "image_download_failed":
2650
+ self.finish_download(None, "Failed", dl_path=body.get("path"))
2651
+ elif cmd == "toggle_script":
2652
+ fname = body.get("file")
2653
+ if fname:
2654
+ p1 = os.path.join(USERSCRIPTS_DIR, fname)
2655
+ p2 = os.path.join(USERSCRIPTS_DIR, fname + ".disabled")
2656
+ if os.path.exists(p2): os.rename(p2, p1)
2657
+ elif os.path.exists(p1): os.rename(p1, p2)
2658
+ elif cmd == "delete_script":
2659
+ fname = body.get("file")
2660
+ if fname:
2661
+ for p in (os.path.join(USERSCRIPTS_DIR, fname), os.path.join(USERSCRIPTS_DIR, fname + ".disabled")):
2662
+ if os.path.exists(p): os.remove(p)
2663
+ elif cmd == "install_crx":
2664
+ cid = body.get("id")
2665
+ if cid:
2666
+ threading.Thread(target=lambda: install_crx(cid), daemon=True).start()
2667
+ elif cmd == "prompt_save_password":
2668
+ self.show_password_banner(body.get("host", ""), body.get("username", ""), body.get("password", ""))
2669
+ elif cmd == "check_autofill":
2670
+ host = body.get("host")
2671
+ saved_user = SETTINGS.settings.get("saved_logins", {}).get(host)
2672
+ if saved_user:
2673
+ pwd = get_from_keychain(host, saved_user)
2674
+ if pwd:
2675
+ wv = self.get_active_webview()
2676
+ if wv:
2677
+ wv.evaluateJavaScript_completionHandler_(f"let pw = document.querySelector('input[type=password]'); let user = document.querySelector('input[type=text], input[type=email]'); if(pw) pw.value = '{pwd}'; if(user) user.value = '{saved_user}';", None)
2678
+ elif cmd == "save_setting":
2679
+ key, val = body.get("key"), body.get("val")
2680
+ if key == "download_path":
2681
+ val = os.path.expanduser(str(val).strip())
2682
+ SETTINGS.settings[key] = val
2683
+ SETTINGS.save()
2684
+ if key in ("bookmarks_bar_visible", "show_home_button", "vertical_tabs_enabled"):
2685
+ self.relayout_views()
2686
+ elif key == "theme_mode":
2687
+ app_delegate = Cocoa.NSApp.delegate()
2688
+ for c in getattr(app_delegate, "controllers", []):
2689
+ c.title_bar.setNeedsDisplay_(True)
2690
+ c.tab_strip.setNeedsDisplay_(True)
2691
+ c.toolbar.setNeedsDisplay_(True)
2692
+ c.bookmarks_bar.setNeedsDisplay_(True)
2693
+ for sub in c.tab_strip.subviews():
2694
+ sub.setNeedsDisplay_(True)
2695
+ elif key in ("default_zoom",):
2696
+ wv = self.get_active_webview()
2697
+ if wv:
2698
+ self.apply_zoom_to_webview(wv)
2699
+ elif key in ("user_agent_mode", "custom_user_agent"):
2700
+ ua = SETTINGS.get_user_agent()
2701
+ for t in self.tabs:
2702
+ if t.get("webview"):
2703
+ t["webview"].setCustomUserAgent_(ua)
2704
+ elif key == "doh_provider":
2705
+ with DNS_LOCK:
2706
+ DNS_CACHE.clear()
2707
+ elif key in ("adblock_enabled", "adblock_stevenblack_enabled", "adblock_base_enabled", "custom_adblock_domains"):
2708
+ compile_webkit_adblock_rules()
2709
+ elif cmd == "choose_download_directory":
2710
+ # Fixed: Asynchronous modal sheet targeting caller webview without blocking WebKit IPC
2711
+ calling_wv = message.webView()
2712
+ panel = Cocoa.NSOpenPanel.openPanel()
2713
+ panel.setCanChooseDirectories_(True)
2714
+ panel.setCanChooseFiles_(False)
2715
+ panel.setAllowsMultipleSelection_(False)
2716
+ panel.setTreatsFilePackagesAsDirectories_(True)
2717
+ panel.setPrompt_("Select Download Location")
2718
+ cur_dir = validate_and_get_download_dir()
2719
+ panel.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(cur_dir))
2720
+
2721
+ def on_panel_done(result):
2722
+ if result == Cocoa.NSModalResponseOK and panel.URLs():
2723
+ chosen_path = panel.URLs()[0].path()
2724
+ if os.access(chosen_path, os.W_OK):
2725
+ SETTINGS.settings["download_path"] = chosen_path
2726
+ SETTINGS.save()
2727
+ if calling_wv:
2728
+ path_json = json.dumps(chosen_path)
2729
+ calling_wv.evaluateJavaScript_completionHandler_(
2730
+ f"if (document.getElementById('dl-path-input')) document.getElementById('dl-path-input').value = {path_json}; if (document.getElementById('dl-path-display')) document.getElementById('dl-path-display').innerText = {path_json}; showToast('Download directory updated');", None
2731
+ )
2732
+ else:
2733
+ if calling_wv:
2734
+ calling_wv.evaluateJavaScript_completionHandler_("showToast('Error: Selected directory is not writable');", None)
2735
+
2736
+ panel.beginSheetModalForWindow_completionHandler_(self.window(), on_panel_done)
2737
+ elif cmd == "search_blocked_domains":
2738
+ matches = search_blocked_domains(body.get("query", ""), limit=100)
2739
+ json_res = json.dumps(matches)
2740
+ wv = self.get_active_webview()
2741
+ if wv:
2742
+ wv.evaluateJavaScript_completionHandler_(f"window.renderBlockedResults({json_res});", None)
2743
+ elif cmd == "update_adblock_filters":
2744
+ def fetch_task():
2745
+ count = update_adblock_filters_from_web()
2746
+ def on_done():
2747
+ wv = self.get_active_webview()
2748
+ if wv:
2749
+ wv.evaluateJavaScript_completionHandler_(f"const b = document.getElementById('adblock-count-badge'); if (b) b.innerText = '{count:,} rules'; const btn = document.getElementById('update-filters-btn'); if (btn) {{ btn.innerText = 'Update Lists Now'; btn.disabled = false; }} if (window.showToast) showToast('Adblock filters updated');", None)
2750
+ dispatch_to_main(on_done)
2751
+ threading.Thread(target=fetch_task, daemon=True).start()
2752
+ elif cmd == "clear_browsing_data":
2753
+ targets = body.get("targets", [])
2754
+ if "history" in targets:
2755
+ clear_history()
2756
+ if "favicons" in targets:
2757
+ FaviconCache.clear()
2758
+ if "dns_cache" in targets:
2759
+ with DNS_LOCK:
2760
+ DNS_CACHE.clear()
2761
+ if "cookies_cache" in targets:
2762
+ ds = WebKit.WKWebsiteDataStore.defaultDataStore()
2763
+ ds.removeDataOfTypes_modifiedSince_completionHandler_(WebKit.WKWebsiteDataStore.allWebsiteDataTypes(), Foundation.NSDate.dateWithTimeIntervalSince1970_(0), lambda: None)
2764
+ elif cmd == "reset_defaults":
2765
+ SETTINGS.reset_defaults()
2766
+ self.relayout_views()
2767
+ for tab in self.tabs:
2768
+ wv = tab.get("webview")
2769
+ if wv:
2770
+ self.apply_zoom_to_webview(wv)
2771
+ wv = self.get_active_webview()
2772
+ if wv:
2773
+ wv.reload()
2774
+ elif cmd == "html5_fullscreen_enter":
2775
+ self.handle_fullscreen_enter(auto_hide_bars=True)
2776
+ elif cmd == "html5_fullscreen_exit":
2777
+ self.handle_fullscreen_exit(restore_bars=True)
2778
+ elif cmd == "dl_request_sync":
2779
+ self.sync_downloads_page()
2780
+ elif cmd == "dl_show":
2781
+ p = body.get("path")
2782
+ if p and os.path.exists(p):
2783
+ Cocoa.NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(p, None)
2784
+ else:
2785
+ dest = validate_and_get_download_dir()
2786
+ if os.path.exists(dest):
2787
+ Cocoa.NSWorkspace.sharedWorkspace().openFile_(dest)
2788
+ elif cmd == "dl_cancel":
2789
+ dl_id = body.get("id")
2790
+ for d in self.downloads:
2791
+ if d["id"] == dl_id and d["status"] == "Downloading":
2792
+ if d.get("obj") and hasattr(d["obj"], "cancel"):
2793
+ d["obj"].cancel()
2794
+ self.finish_download(d.get("obj"), "Canceled", dl_id=dl_id)
2795
+ break
2796
+ elif cmd == "delete_bookmark":
2797
+ target_url = body.get("url")
2798
+ if target_url:
2799
+ try:
2800
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
2801
+ bms = json.load(f)
2802
+ except Exception:
2803
+ bms = []
2804
+ atomic_write_json(BOOKMARKS_FILE, [b for b in bms if isinstance(b, dict) and b.get("url") != target_url])
2805
+ self.bookmarks_bar.reload_bookmarks()
2806
+ self.update_star_status()
2807
+ elif cmd == "add_bookmark":
2808
+ u = body.get("url")
2809
+ t = body.get("title") or u
2810
+ if u:
2811
+ try:
2812
+ with open(BOOKMARKS_FILE, "r", encoding="utf-8") as f:
2813
+ bms = json.load(f)
2814
+ except Exception:
2815
+ bms = []
2816
+ if not any(isinstance(b, dict) and b.get("url") == u for b in bms):
2817
+ bms.append({"title": t, "url": u})
2818
+ atomic_write_json(BOOKMARKS_FILE, bms)
2819
+ self.bookmarks_bar.reload_bookmarks()
2820
+ self.update_star_status()
2821
+ elif cmd == "delete_history_item":
2822
+ item_id = body.get("id")
2823
+ if item_id is not None:
2824
+ try:
2825
+ with sqlite3.connect(HISTORY_FILE) as conn:
2826
+ conn.execute("DELETE FROM history WHERE id = ?;", (item_id,))
2827
+ except Exception:
2828
+ pass
2829
+
2830
+ # ========================================================================
2831
+ # WEBKIT DELEGATES
2832
+ # ========================================================================
2833
+ def webView_createWebViewWithConfiguration_forNavigationAction_windowFeatures_(self, wv, cfg, act, feat):
2834
+ pending_action = getattr(wv, "pending_image_action", None)
2835
+ if pending_action:
2836
+ wv.pending_image_action = None
2837
+ img_url = act.request().URL().absoluteString() if act.request().URL() else None
2838
+ if img_url:
2839
+ if pending_action == "save_as": self.perform_save_image_as(img_url)
2840
+ elif pending_action == "download": self.perform_download_image(img_url)
2841
+ elif pending_action == "copy_address": self.perform_copy_image_address(img_url)
2842
+ return None
2843
+
2844
+ url = act.request().URL().absoluteString() if act.request().URL() else None
2845
+ if url:
2846
+ self.add_tab(url)
2847
+ return None
2848
+
2849
+ def webView_decidePolicyForNavigationAction_decisionHandler_(self, wv, action, handler):
2850
+ mods = action.modifierFlags() if hasattr(action, "modifierFlags") else 0
2851
+ if bool(mods & Cocoa.NSEventModifierFlagCommand) and action.navigationType() == WebKit.WKNavigationTypeLinkActivated:
2852
+ url = action.request().URL().absoluteString() if action.request().URL() else None
2853
+ if url:
2854
+ self.add_tab(url)
2855
+ handler(WebKit.WKNavigationActionPolicyCancel)
2856
+ return
2857
+ handler(WebKit.WKNavigationActionPolicyAllow)
2858
+
2859
+ def webView_decidePolicyForNavigationResponse_decisionHandler_(self, wv, resp, handler):
2860
+ if not resp.canShowMIMEType():
2861
+ handler(WebKit.WKNavigationResponsePolicyDownload if SETTINGS.settings.get("downloads_enabled", True) else WebKit.WKNavigationResponsePolicyCancel)
2862
+ return
2863
+ if isinstance(resp.response(), Foundation.NSHTTPURLResponse):
2864
+ disp = resp.response().allHeaderFields().get("Content-Disposition", "") or resp.response().allHeaderFields().get("content-disposition", "")
2865
+ if "attachment" in str(disp).lower():
2866
+ handler(WebKit.WKNavigationResponsePolicyDownload if SETTINGS.settings.get("downloads_enabled", True) else WebKit.WKNavigationResponsePolicyCancel)
2867
+ return
2868
+ handler(WebKit.WKNavigationResponsePolicyAllow)
2869
+
2870
+ def webView_runOpenPanelWithParameters_initiatedByFrame_completionHandler_(self, wv, params, frame, handler):
2871
+ panel = Cocoa.NSOpenPanel.openPanel()
2872
+ panel.setAllowsMultipleSelection_(params.allowsMultipleSelection())
2873
+ panel.setCanChooseFiles_(True)
2874
+ panel.setCanChooseDirectories_(False)
2875
+ handler(panel.URLs() if panel.runModal() == Cocoa.NSModalResponseOK else None)
2876
+
2877
+ def webView_navigationAction_didBecomeDownload_(self, w, a, d):
2878
+ d.setDelegate_(self.download_delegate)
2879
+
2880
+ def webView_navigationResponse_didBecomeDownload_(self, w, r, d):
2881
+ d.setDelegate_(self.download_delegate)
2882
+
2883
+ def webView_didStartProvisionalNavigation_(self, wv, nav):
2884
+ if wv == self.get_active_webview():
2885
+ self.progress_bar.setHidden_(False)
2886
+ self.progress_bar.setProgress_(0.15)
2887
+ self.apply_zoom_to_webview(wv)
2888
+
2889
+ def webView_didFinishNavigation_(self, wv, nav):
2890
+ tab = self.tabs[self.active_tab_index] if 0 <= self.active_tab_index < len(self.tabs) else None
2891
+ self.apply_zoom_to_webview(wv, tab)
2892
+ self.update_star_status()
2893
+ if wv == self.get_active_webview():
2894
+ self.progress_bar.setProgress_(1.0)
2895
+ Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(0.2, False, lambda t: self.progress_bar.setHidden_(True))
2896
+ if not self.is_incognito and wv.URL() and SETTINGS.settings.get("history_enabled", True):
2897
+ record_history(wv.URL().absoluteString(), wv.title() or wv.URL().absoluteString())
2898
+ self.save_session()
2899
+
2900
+ def webView_didFailProvisionalNavigation_withError_(self, wv, nav, err):
2901
+ if wv == self.get_active_webview():
2902
+ self.progress_bar.setHidden_(True)
2903
+ self.progress_bar.setProgress_(0.0)
2904
+
2905
+ def webView_didFailNavigation_withError_(self, wv, nav, err):
2906
+ if wv == self.get_active_webview():
2907
+ self.progress_bar.setHidden_(True)
2908
+ self.progress_bar.setProgress_(0.0)
2909
+
2910
+ def webViewWebContentProcessDidTerminate_(self, wv):
2911
+ TERMINAL_MANAGER.close_for_webview(wv)
2912
+ wv.reload()
2913
+
2914
+ def observeValueForKeyPath_ofObject_change_context_(self, keyPath, obj, change, context):
2915
+ if keyPath == "URL" and obj.URL():
2916
+ for t in self.tabs:
2917
+ if t.get("webview") == obj:
2918
+ if t.get("is_sleeping"):
2919
+ break
2920
+ t["url"] = obj.URL().absoluteString()
2921
+ if obj.URL().host():
2922
+ FaviconCache.get_favicon(obj.URL().host(), lambda img, tab=t: self.set_favicon(tab, img))
2923
+ break
2924
+ if self.get_active_webview() == obj:
2925
+ tab = self.tabs[self.active_tab_index] if 0 <= self.active_tab_index < len(self.tabs) else {}
2926
+ if not tab.get("is_sleeping"):
2927
+ self.omnibox.set_url(obj.URL().absoluteString())
2928
+ self.update_star_status()
2929
+ elif keyPath == "title" and obj.title():
2930
+ for t in self.tabs:
2931
+ if t.get("webview") == obj:
2932
+ if not t.get("is_sleeping"):
2933
+ t["title"] = str(obj.title())
2934
+ break
2935
+ self.relayout_tabs()
2936
+ elif keyPath == "estimatedProgress":
2937
+ self.update_progress_for_webview(obj)
2938
+ elif keyPath == "fractionCompleted":
2939
+ try:
2940
+ frac = float(obj.fractionCompleted())
2941
+ for d in self.downloads:
2942
+ if d.get("obj") and hasattr(d["obj"], "progress") and d["obj"].progress() == obj:
2943
+ d["progress"] = frac
2944
+ self.sync_downloads_page()
2945
+ break
2946
+ except Exception:
2947
+ pass
2948
+
2949
+ def set_favicon(self, tab, img):
2950
+ tab["favicon"] = img
2951
+ self.relayout_tabs()
2952
+
2953
+ def windowDidEnterFullScreen_(self, notif):
2954
+ self.is_fullscreen = True
2955
+ self.relayout_views()
2956
+ self.restore_normal_zoom()
2957
+
2958
+ def windowDidExitFullScreen_(self, notif):
2959
+ self.is_fullscreen = False
2960
+ self.relayout_views()
2961
+ self.restore_normal_zoom()
2962
+
2963
+ def windowDidResignKey_(self, notif):
2964
+ if hasattr(self, 'omnibox_panel') and self.omnibox_panel.is_visible():
2965
+ self.omnibox_panel.hide()
2966
+
2967
+ def windowDidResize_(self, notif):
2968
+ self.relayout_views()
2969
+ if self.omnibox_panel.is_visible():
2970
+ self.omnibox_panel.hide()
2971
+
2972
+ def windowDidMove_(self, notif):
2973
+ if self.omnibox_panel.is_visible():
2974
+ self.omnibox_panel.hide()
2975
+
2976
+ def windowWillClose_(self, notif):
2977
+ self.save_session()
2978
+ for t in self.tabs:
2979
+ wv = t.get("webview")
2980
+ if wv:
2981
+ TERMINAL_MANAGER.close_for_webview(wv)
2982
+ for k in ("URL", "title", "estimatedProgress"):
2983
+ try:
2984
+ wv.removeObserver_forKeyPath_(self, k)
2985
+ except Exception:
2986
+ pass
2987
+ app_delegate = Cocoa.NSApp.delegate()
2988
+ if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
2989
+ app_delegate.controllers.remove(self)
2990
+
2991
+ # ============================================================================
2992
+ # APP DELEGATE
2993
+ # ============================================================================
2994
+ class AppDelegate(Cocoa.NSObject):
2995
+ def applicationDidFinishLaunching_(self, notif):
2996
+ try:
2997
+ init_history_db()
2998
+ load_adblock_filters_from_disk()
2999
+ compile_webkit_adblock_rules()
3000
+ load_dns_cache()
3001
+ start_proxy_server()
3002
+
3003
+ self.controllers = []
3004
+ self.process_manager = ProcessManagerController.alloc().init()
3005
+ self.quick_switcher = QuickSwitcherController.alloc().init()
3006
+ self.setup_menu()
3007
+
3008
+ main_window = BrowserWindowController.alloc().init()
3009
+ self.controllers.append(main_window)
3010
+ self.pip_panel = PiPPanelController.alloc().initWithBrowser_(main_window)
3011
+ Cocoa.NSApp.activateIgnoringOtherApps_(True)
3012
+
3013
+ def click_monitor(event):
3014
+ for c in self.controllers:
3015
+ if c.omnibox_panel.is_visible():
3016
+ if c.omnibox_panel.suggestion_window and event.window() == c.omnibox_panel.suggestion_window:
3017
+ return event
3018
+ pt = event.locationInWindow()
3019
+ if event.window() == c.window():
3020
+ in_omni = Cocoa.NSPointInRect(pt, c.omnibox_container.convertRect_toView_(c.omnibox_container.bounds(), None))
3021
+ if in_omni:
3022
+ return event
3023
+ c.omnibox_panel.hide()
3024
+ return event
3025
+ Cocoa.NSEvent.addLocalMonitorForEventsMatchingMask_handler_(Cocoa.NSEventMaskLeftMouseDown, click_monitor)
3026
+
3027
+ def key_monitor(event):
3028
+ flags = event.modifierFlags()
3029
+ cmd = bool(flags & Cocoa.NSEventModifierFlagCommand)
3030
+ shift = bool(flags & Cocoa.NSEventModifierFlagShift)
3031
+ opt = bool(flags & Cocoa.NSEventModifierFlagOption)
3032
+ key = event.keyCode()
3033
+ chars = event.charactersIgnoringModifiers().lower() if event.charactersIgnoringModifiers() else ""
3034
+ win = Cocoa.NSApp.keyWindow()
3035
+ wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
3036
+
3037
+ if key == 53: # Escape
3038
+ if shift and not cmd and not opt:
3039
+ self.process_manager.show()
3040
+ return None
3041
+ if opt and wc:
3042
+ wc.toggle_tab_bar()
3043
+ return None
3044
+ if wc:
3045
+ wc.handle_escape_key()
3046
+ return None
3047
+
3048
+ if cmd and shift and not opt:
3049
+ if key == 0 or chars == 'a':
3050
+ self.quick_switcher.show()
3051
+ return None
3052
+
3053
+ return event
3054
+ Cocoa.NSEvent.addLocalMonitorForEventsMatchingMask_handler_(Cocoa.NSEventMaskKeyDown, key_monitor)
3055
+
3056
+ def background_filter_check():
3057
+ if not os.path.exists(ADBLOCK_CACHE_FILE) or (time.time() - os.path.getmtime(ADBLOCK_CACHE_FILE) > 604800):
3058
+ update_adblock_filters_from_web()
3059
+ threading.Thread(target=background_filter_check, daemon=True).start()
3060
+
3061
+ except Exception:
3062
+ traceback.print_exc()
3063
+
3064
+ def applicationShouldHandleReopen_hasVisibleWindows_(self, app, flag):
3065
+ if not flag:
3066
+ self.onNewWindow_(None)
3067
+ return True
3068
+ return True
3069
+
3070
+ def onNewWindow_(self, sender):
3071
+ wc = BrowserWindowController.alloc().initWithIncognito_(False)
3072
+ self.controllers.append(wc)
3073
+
3074
+ def onNewIncognitoWindow_(self, sender):
3075
+ wc = BrowserWindowController.alloc().initWithIncognito_(True)
3076
+ self.controllers.append(wc)
3077
+
3078
+ def onNewTerminal_(self, sender):
3079
+ win = Cocoa.NSApp.keyWindow()
3080
+ wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else (self.controllers[0] if self.controllers else None)
3081
+ if wc:
3082
+ wc.add_tab("browser://terminal")
3083
+
3084
+ def onCloseTab_(self, sender=None):
3085
+ win = Cocoa.NSApp.keyWindow()
3086
+ wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
3087
+ if wc and 0 <= wc.active_tab_index < len(wc.tabs):
3088
+ wc.close_tab(wc.active_tab_index)
3089
+
3090
+ def onReopenTab_(self, sender=None):
3091
+ win = Cocoa.NSApp.keyWindow()
3092
+ wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
3093
+ if wc:
3094
+ wc.reopen_last_closed_tab()
3095
+
3096
+ def showTaskManager_(self, sender):
3097
+ self.process_manager.show()
3098
+
3099
+ def quickSwitcher_(self, sender):
3100
+ self.quick_switcher.show()
3101
+
3102
+ def setup_menu(self):
3103
+ main_menu = Cocoa.NSMenu.alloc().init()
3104
+ app_item = Cocoa.NSMenuItem.alloc().init()
3105
+ app_menu = Cocoa.NSMenu.alloc().init()
3106
+ app_menu.addItemWithTitle_action_keyEquivalent_("Quit PyBrowser", "terminate:", "q")
3107
+ app_item.setSubmenu_(app_menu)
3108
+ main_menu.addItem_(app_item)
3109
+
3110
+ file_item = Cocoa.NSMenuItem.alloc().init()
3111
+ file_menu = Cocoa.NSMenu.alloc().initWithTitle_("File")
3112
+ file_menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t")
3113
+ file_menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "onNewTerminal:", "T").setTarget_(self)
3114
+ file_menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(self)
3115
+ inc_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
3116
+ inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
3117
+ inc_item.setTarget_(self)
3118
+ file_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3119
+ file_menu.addItemWithTitle_action_keyEquivalent_("Close Tab", "onCloseTab:", "w").setTarget_(self)
3120
+ reopen_item = file_menu.addItemWithTitle_action_keyEquivalent_("Reopen Closed Tab", "onReopenTab:", "T")
3121
+ reopen_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
3122
+ reopen_item.setTarget_(self)
3123
+ file_item.setSubmenu_(file_menu)
3124
+ main_menu.addItem_(file_item)
3125
+
3126
+ edit_item = Cocoa.NSMenuItem.alloc().init()
3127
+ edit_menu = Cocoa.NSMenu.alloc().initWithTitle_("Edit")
3128
+ edit_menu.addItemWithTitle_action_keyEquivalent_("Undo", "undo:", "z")
3129
+ redo_item = edit_menu.addItemWithTitle_action_keyEquivalent_("Redo", "redo:", "Z")
3130
+ redo_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
3131
+ edit_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3132
+ edit_menu.addItemWithTitle_action_keyEquivalent_("Cut", "cut:", "x")
3133
+ edit_menu.addItemWithTitle_action_keyEquivalent_("Copy", "copy:", "c")
3134
+ edit_menu.addItemWithTitle_action_keyEquivalent_("Paste", "paste:", "v")
3135
+ edit_menu.addItemWithTitle_action_keyEquivalent_("Select All", "selectAll:", "a")
3136
+ edit_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3137
+ edit_menu.addItemWithTitle_action_keyEquivalent_("Find in Page", "onFindInPage:", "f")
3138
+ edit_item.setSubmenu_(edit_menu)
3139
+ main_menu.addItem_(edit_item)
3140
+
3141
+ view_item = Cocoa.NSMenuItem.alloc().init()
3142
+ view_menu = Cocoa.NSMenu.alloc().initWithTitle_("View")
3143
+ view_menu.addItemWithTitle_action_keyEquivalent_("Reload Page", "onReload:", "r")
3144
+ view_menu.addItemWithTitle_action_keyEquivalent_("Focus Omnibox", "onFocusOmnibox:", "l")
3145
+ view_menu.addItemWithTitle_action_keyEquivalent_("Picture-in-Picture", "onTogglePiP:", "p")
3146
+ view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3147
+ view_menu.addItemWithTitle_action_keyEquivalent_("Zoom In", "onZoomIn:", "+")
3148
+ view_menu.addItemWithTitle_action_keyEquivalent_("Zoom Out", "onZoomOut:", "-")
3149
+ view_menu.addItemWithTitle_action_keyEquivalent_("Actual Size", "onResetZoom:", "0")
3150
+ view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3151
+ inspect_item = view_menu.addItemWithTitle_action_keyEquivalent_("Inspect Element", "onInspectElement:", "i")
3152
+ inspect_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagOption)
3153
+ view_item.setSubmenu_(view_menu)
3154
+ main_menu.addItem_(view_item)
3155
+
3156
+ hist_item = Cocoa.NSMenuItem.alloc().init()
3157
+ hist_menu = Cocoa.NSMenu.alloc().initWithTitle_("History")
3158
+ hist_menu.addItemWithTitle_action_keyEquivalent_("Show Full History", "openHistory:", "y")
3159
+ hist_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3160
+ hist_menu.addItemWithTitle_action_keyEquivalent_("Back", "onGoBack:", "[")
3161
+ hist_menu.addItemWithTitle_action_keyEquivalent_("Forward", "onGoForward:", "]")
3162
+ hist_item.setSubmenu_(hist_menu)
3163
+ main_menu.addItem_(hist_item)
3164
+
3165
+ win_item = Cocoa.NSMenuItem.alloc().init()
3166
+ win_menu = Cocoa.NSMenu.alloc().initWithTitle_("Window")
3167
+ for i in range(1, 10):
3168
+ win_menu.addItemWithTitle_action_keyEquivalent_(f"Tab {i}", f"onSwitchTab{i}:", str(i))
3169
+ win_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3170
+ win_menu.addItemWithTitle_action_keyEquivalent_("Quick Switcher", "quickSwitcher:", "k")
3171
+ win_menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(self)
3172
+ win_item.setSubmenu_(win_menu)
3173
+ main_menu.addItem_(win_item)
3174
+
3175
+ Cocoa.NSApp.setMainMenu_(main_menu)