stupidpythonbrowser 1.0.3__tar.gz → 1.0.4__tar.gz
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.
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/PKG-INFO +1 -1
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/__init__.py +1 -1
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/browser.py +271 -77
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/doh_server.py +44 -19
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/incognito.py +1 -1
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/settings.py +11 -6
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/task_manager.py +9 -5
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/terminal.py +8 -6
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pyproject.toml +1 -1
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/PKG-INFO +1 -1
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/LICENSE +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/README.md +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/pybrowser/main.py +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/setup.cfg +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/SOURCES.txt +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/dependency_links.txt +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/entry_points.txt +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/requires.txt +0 -0
- {stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/top_level.txt +0 -0
|
@@ -42,7 +42,8 @@ from pybrowser.doh_server import (
|
|
|
42
42
|
DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, BASE_ADBLOCK_DOMAINS, ADBLOCK_LOCK,
|
|
43
43
|
WK_RULE_LIST, is_domain_blocked, search_blocked_domains, load_dns_cache,
|
|
44
44
|
start_proxy_server, create_nw_proxy_config, load_adblock_filters_from_disk,
|
|
45
|
-
compile_webkit_adblock_rules, update_adblock_filters_from_web
|
|
45
|
+
compile_webkit_adblock_rules, update_adblock_filters_from_web,
|
|
46
|
+
clear_dns_cache_and_disk
|
|
46
47
|
)
|
|
47
48
|
from pybrowser.incognito import create_incognito_data_store, apply_incognito_preferences
|
|
48
49
|
from pybrowser.terminal import TERMINAL_MANAGER, dispatch_to_main
|
|
@@ -113,9 +114,10 @@ def download_image_data(img_url, dest_path, webview=None, completion_callback=No
|
|
|
113
114
|
if img_url.startswith("blob:"):
|
|
114
115
|
if webview:
|
|
115
116
|
dest_json = json.dumps(dest_path)
|
|
117
|
+
img_url_json = json.dumps(img_url)
|
|
116
118
|
js = f"""(async () => {{
|
|
117
119
|
try {{
|
|
118
|
-
const res = await fetch(
|
|
120
|
+
const res = await fetch({img_url_json});
|
|
119
121
|
const blob = await res.blob();
|
|
120
122
|
const reader = new FileReader();
|
|
121
123
|
reader.onloadend = () => {{
|
|
@@ -131,10 +133,18 @@ def download_image_data(img_url, dest_path, webview=None, completion_callback=No
|
|
|
131
133
|
if completion_callback: completion_callback(False)
|
|
132
134
|
return False
|
|
133
135
|
|
|
136
|
+
# Bug 8 Fix: Safely resolve referer on the main thread before starting background thread
|
|
137
|
+
referer = img_url
|
|
138
|
+
if webview and Cocoa.NSThread.isMainThread():
|
|
139
|
+
try:
|
|
140
|
+
if webview.URL():
|
|
141
|
+
referer = webview.URL().absoluteString()
|
|
142
|
+
except Exception:
|
|
143
|
+
referer = img_url
|
|
144
|
+
|
|
134
145
|
def worker():
|
|
135
146
|
success = False
|
|
136
147
|
try:
|
|
137
|
-
referer = webview.URL().absoluteString() if (webview and webview.URL()) else img_url
|
|
138
148
|
req = urllib.request.Request(img_url, headers={
|
|
139
149
|
"User-Agent": SETTINGS.get_user_agent(),
|
|
140
150
|
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
|
@@ -147,9 +157,10 @@ def download_image_data(img_url, dest_path, webview=None, completion_callback=No
|
|
|
147
157
|
except Exception:
|
|
148
158
|
if webview:
|
|
149
159
|
dest_json = json.dumps(dest_path)
|
|
160
|
+
img_url_json = json.dumps(img_url)
|
|
150
161
|
js = f"""(async () => {{
|
|
151
162
|
try {{
|
|
152
|
-
const res = await fetch(
|
|
163
|
+
const res = await fetch({img_url_json}, {{ credentials: 'include' }});
|
|
153
164
|
const blob = await res.blob();
|
|
154
165
|
const reader = new FileReader();
|
|
155
166
|
reader.onloadend = () => {{
|
|
@@ -336,7 +347,9 @@ def reload_all_extensions_views():
|
|
|
336
347
|
def init_history_db():
|
|
337
348
|
try:
|
|
338
349
|
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
339
|
-
|
|
350
|
+
# Bug 5 Fix: Separate into individual statements so synchronous pragma executes
|
|
351
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
352
|
+
conn.execute("PRAGMA synchronous=NORMAL;")
|
|
340
353
|
conn.execute("""
|
|
341
354
|
CREATE TABLE IF NOT EXISTS history (
|
|
342
355
|
id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, title TEXT,
|
|
@@ -472,6 +485,7 @@ document.addEventListener('volumechange', (e) => {
|
|
|
472
485
|
}, true);
|
|
473
486
|
"""
|
|
474
487
|
|
|
488
|
+
# Bug 9 Fix: Always report target image (or null) to clear stale context menu states
|
|
475
489
|
IMAGE_CONTEXT_MENU_JS = """
|
|
476
490
|
(function() {
|
|
477
491
|
document.addEventListener('contextmenu', function(e) {
|
|
@@ -486,30 +500,33 @@ IMAGE_CONTEXT_MENU_JS = """
|
|
|
486
500
|
let bg = window.getComputedStyle(el).backgroundImage;
|
|
487
501
|
if (bg && bg.startsWith('url(')) imgSrc = bg.slice(4, -1).replace(/["']/g, "");
|
|
488
502
|
}
|
|
489
|
-
if (
|
|
503
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
|
|
490
504
|
window.webkit.messageHandlers.bridge.postMessage({cmd: 'context_target_image', src: imgSrc});
|
|
491
505
|
}
|
|
492
506
|
}, true);
|
|
493
507
|
})();
|
|
494
508
|
"""
|
|
495
509
|
|
|
510
|
+
# Bug 18 Fix: Wrap keychain autosave in IIFE to protect scope
|
|
496
511
|
KEYCHAIN_AUTOSAVE_JS = """
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
window.webkit
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
window.webkit
|
|
511
|
-
|
|
512
|
-
}
|
|
512
|
+
(function() {
|
|
513
|
+
document.addEventListener('submit', (e) => {
|
|
514
|
+
if (!e.target || !e.target.querySelector) return;
|
|
515
|
+
let pw = e.target.querySelector('input[type="password"]');
|
|
516
|
+
let user = e.target.querySelector('input[type="text"], input[type="email"]');
|
|
517
|
+
if (pw && pw.value && window.webkit?.messageHandlers?.bridge) {
|
|
518
|
+
window.webkit.messageHandlers.bridge.postMessage({
|
|
519
|
+
cmd: 'prompt_save_password', host: location.hostname,
|
|
520
|
+
username: user ? user.value : 'unknown', password: pw.value
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}, true);
|
|
524
|
+
window.addEventListener('load', () => {
|
|
525
|
+
if (location.hostname && window.webkit?.messageHandlers?.bridge) {
|
|
526
|
+
window.webkit.messageHandlers.bridge.postMessage({cmd: 'check_autofill', host: location.hostname});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
})();
|
|
513
530
|
"""
|
|
514
531
|
|
|
515
532
|
COSMETIC_ADBLOCK_CSS = """
|
|
@@ -638,14 +655,16 @@ class TabGroupHeaderView(Cocoa.NSView):
|
|
|
638
655
|
hex_to_nscolor(GROUP_COLORS.get(self.group.color_name, "#5F6368")).setFill()
|
|
639
656
|
path.fill()
|
|
640
657
|
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.whiteColor()}
|
|
641
|
-
|
|
658
|
+
# Bug 17 Fix: Guard title against None
|
|
659
|
+
title_str = self.group.title or "Group"
|
|
660
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(title_str, attrs).drawAtPoint_(Cocoa.NSMakePoint(6, 6))
|
|
642
661
|
|
|
643
662
|
def mouseDown_(self, event):
|
|
644
663
|
self.group.collapsed = not self.group.collapsed
|
|
645
664
|
self.controller.relayout_tabs()
|
|
646
665
|
|
|
647
666
|
# ============================================================================
|
|
648
|
-
# TITLE BAR VIEW
|
|
667
|
+
# TITLE BAR VIEW
|
|
649
668
|
# ============================================================================
|
|
650
669
|
class TitleBarView(Cocoa.NSView):
|
|
651
670
|
"""
|
|
@@ -719,6 +738,9 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
719
738
|
self.tracking_area = None
|
|
720
739
|
self.close_btn_rect = Cocoa.NSZeroRect
|
|
721
740
|
self.audio_btn_rect = Cocoa.NSZeroRect
|
|
741
|
+
# Bug 2 Fix: Track drag state for interactive tab reordering
|
|
742
|
+
self.drag_start = None
|
|
743
|
+
self.is_dragging = False
|
|
722
744
|
return self
|
|
723
745
|
|
|
724
746
|
def updateTrackingAreas(self):
|
|
@@ -798,10 +820,10 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
798
820
|
}
|
|
799
821
|
Cocoa.NSAttributedString.alloc().initWithString_attributes_("✕", close_attrs).drawAtPoint_(Cocoa.NSMakePoint(self.close_btn_rect.origin.x + 2, self.close_btn_rect.origin.y - 1))
|
|
800
822
|
|
|
801
|
-
# Tab title
|
|
823
|
+
# Tab title (Bug 17 Fix: Ensure title is never None)
|
|
802
824
|
title_x = icon_x + 20.0
|
|
803
825
|
max_title_w = max(10.0, bounds.size.width - title_x - right_offset)
|
|
804
|
-
title = tab.get("title"
|
|
826
|
+
title = tab.get("title") or "New Tab"
|
|
805
827
|
text_color = Cocoa.NSColor.labelColor() if is_active else Cocoa.NSColor.secondaryLabelColor()
|
|
806
828
|
title_attrs = {
|
|
807
829
|
Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(12),
|
|
@@ -819,6 +841,42 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
819
841
|
self.controller.toggle_mute(self.index)
|
|
820
842
|
return
|
|
821
843
|
self.controller.switch_to_tab(self.index)
|
|
844
|
+
# Bug 2 Fix: Initialize drag start location
|
|
845
|
+
self.drag_start = event.locationInWindow()
|
|
846
|
+
self.is_dragging = False
|
|
847
|
+
|
|
848
|
+
def mouseDragged_(self, event):
|
|
849
|
+
# Bug 2 Fix: Drag reordering logic
|
|
850
|
+
if not self.drag_start or not self.controller:
|
|
851
|
+
return
|
|
852
|
+
cur_pt = event.locationInWindow()
|
|
853
|
+
dx = cur_pt.x - self.drag_start.x
|
|
854
|
+
dy = cur_pt.y - self.drag_start.y
|
|
855
|
+
if not self.is_dragging and (abs(dx) > 5 or abs(dy) > 5):
|
|
856
|
+
self.is_dragging = True
|
|
857
|
+
|
|
858
|
+
if self.is_dragging:
|
|
859
|
+
strip = self.controller.tab_strip
|
|
860
|
+
if not strip:
|
|
861
|
+
return
|
|
862
|
+
strip_pt = strip.convertPoint_fromView_(cur_pt, None)
|
|
863
|
+
for sub in strip.subviews():
|
|
864
|
+
if isinstance(sub, ChromeTabView) and sub is not self:
|
|
865
|
+
if Cocoa.NSPointInRect(strip_pt, sub.frame()):
|
|
866
|
+
target_idx = sub.index
|
|
867
|
+
if target_idx != self.index:
|
|
868
|
+
from_idx = self.index
|
|
869
|
+
self.controller.move_tab(from_idx, target_idx)
|
|
870
|
+
self.drag_start = cur_pt
|
|
871
|
+
break
|
|
872
|
+
|
|
873
|
+
def mouseUp_(self, event):
|
|
874
|
+
# Bug 2 Fix: Finalize drag and persist ordering
|
|
875
|
+
if self.is_dragging:
|
|
876
|
+
self.is_dragging = False
|
|
877
|
+
if self.controller:
|
|
878
|
+
self.controller.save_session()
|
|
879
|
+
self.drag_start = None
|
|
822
880
|
|
|
823
881
|
def menuForEvent_(self, event):
|
|
824
882
|
menu = Cocoa.NSMenu.alloc().init()
|
|
@@ -888,7 +946,8 @@ class BookmarksBarView(Cocoa.NSView):
|
|
|
888
946
|
for b in bms:
|
|
889
947
|
if not isinstance(b, dict) or not b.get("url"):
|
|
890
948
|
continue
|
|
891
|
-
|
|
949
|
+
# Bug 16 Fix: Safely handle None bookmark titles
|
|
950
|
+
title = (b.get("title") or b.get("url") or "")[:24]
|
|
892
951
|
btn_w = max(50, min(160, len(title) * 7.5 + 26))
|
|
893
952
|
btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
|
|
894
953
|
btn.setTitle_(title)
|
|
@@ -1112,12 +1171,15 @@ window.writeToTerm = b => {{
|
|
|
1112
1171
|
for b in bms:
|
|
1113
1172
|
if isinstance(b, dict) and b.get("url"):
|
|
1114
1173
|
u = b["url"]
|
|
1115
|
-
|
|
1174
|
+
# Bug 16 Fix: Guard title against None
|
|
1175
|
+
t = b.get("title") or u or ""
|
|
1116
1176
|
safe_u = json.dumps(u)
|
|
1177
|
+
esc_u = html.escape(u)
|
|
1178
|
+
esc_t = html.escape(t)
|
|
1117
1179
|
host = urllib.parse.urlparse(u).netloc
|
|
1118
|
-
fav_tag = f"<img src='https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32' width='16' height='16' style='vertical-align:middle;margin-right:8px;border-radius:3px;' onerror
|
|
1180
|
+
fav_tag = f"<img src='https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32' width='16' height='16' style='vertical-align:middle;margin-right:8px;border-radius:3px;' onerror=\"this.style.display='none'\">" if host else ""
|
|
1119
1181
|
del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
|
|
1120
|
-
items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{
|
|
1182
|
+
items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{esc_u}' style='color:#1a73e8;text-decoration:none;'>{esc_t}</a></span> <button onclick='{del_call}' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
|
|
1121
1183
|
rows = "".join(items)
|
|
1122
1184
|
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' style='padding:4px 8px;margin-right:8px;'><input id='u' placeholder='https://...' style='padding:4px 8px;margin-right:8px;width:260px;'><button onclick='if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)' style='padding:4px 12px;'>Add</button></div><ul style='list-style:none;padding:0;'>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
|
|
1123
1185
|
|
|
@@ -1126,12 +1188,17 @@ window.writeToTerm = b => {{
|
|
|
1126
1188
|
items = []
|
|
1127
1189
|
for r in records:
|
|
1128
1190
|
del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
|
|
1129
|
-
|
|
1191
|
+
# Bug 16 Fix: Guard title against None and avoid backslashes inside f-string expressions
|
|
1192
|
+
title_text = r[2] or r[1] or ""
|
|
1193
|
+
url_str = r[1] or ""
|
|
1194
|
+
esc_url = html.escape(url_str)
|
|
1195
|
+
esc_title = html.escape(title_text)
|
|
1196
|
+
items.append(f"<tr><td>{r[3]}</td><td><a href='{esc_url}'>{esc_title}</a></td><td><button onclick='{del_call}'>✕</button></td></tr>")
|
|
1130
1197
|
rows = "".join(items)
|
|
1131
1198
|
return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick=\"if(window.webkit?.messageHandlers?.bridge) 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>"
|
|
1132
1199
|
|
|
1133
1200
|
def get_downloads_html(self):
|
|
1134
|
-
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=\"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:
|
|
1201
|
+
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=\"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:d.path})\">Show in Finder</button></div>`).join('')};if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_request_sync'});</script></body></html>"
|
|
1135
1202
|
|
|
1136
1203
|
def get_extensions_html(self):
|
|
1137
1204
|
scripts = get_installed_userscripts()
|
|
@@ -1242,22 +1309,22 @@ class ChromeOmniboxField(Cocoa.NSTextField):
|
|
|
1242
1309
|
def becomeFirstResponder(self):
|
|
1243
1310
|
if self.superview():
|
|
1244
1311
|
self.superview().setNeedsDisplay_(True)
|
|
1245
|
-
|
|
1312
|
+
# Bug 1 Fix: Ensure string value is never None and eliminate infinite loop selectText
|
|
1313
|
+
self.setStringValue_(self.raw_url or "")
|
|
1246
1314
|
self.setTextColor_(Cocoa.NSColor.labelColor())
|
|
1247
|
-
self.performSelector_withObject_afterDelay_("selectText:", self, 0.0)
|
|
1248
1315
|
return objc.super(ChromeOmniboxField, self).becomeFirstResponder()
|
|
1249
1316
|
|
|
1250
1317
|
def resignFirstResponder(self):
|
|
1251
1318
|
if self.superview():
|
|
1252
1319
|
self.superview().setNeedsDisplay_(True)
|
|
1253
|
-
|
|
1320
|
+
# Bug 1 Fix: Do not wipe input text on resign so enter and navigation work properly
|
|
1254
1321
|
return objc.super(ChromeOmniboxField, self).resignFirstResponder()
|
|
1255
1322
|
|
|
1256
1323
|
def set_url(self, url):
|
|
1257
|
-
self.raw_url = url
|
|
1324
|
+
self.raw_url = url or ""
|
|
1258
1325
|
fr = self.window().firstResponder() if self.window() else None
|
|
1259
1326
|
if fr != self and fr != self.currentEditor():
|
|
1260
|
-
self.setStringValue_(
|
|
1327
|
+
self.setStringValue_(self.raw_url)
|
|
1261
1328
|
|
|
1262
1329
|
class OmniboxPopupPanel(Cocoa.NSPanel):
|
|
1263
1330
|
def canBecomeKeyWindow(self): return False
|
|
@@ -1278,7 +1345,8 @@ class OmniboxSuggestionView(Cocoa.NSView):
|
|
|
1278
1345
|
opts = Cocoa.NSTrackingMouseMoved | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
|
|
1279
1346
|
self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
|
|
1280
1347
|
self.addTrackingArea_(self.tracking_area)
|
|
1281
|
-
|
|
1348
|
+
# Bug 1 Fix: Use correct class in objc.super to avoid fatal TypeError
|
|
1349
|
+
objc.super(OmniboxSuggestionView, self).updateTrackingAreas()
|
|
1282
1350
|
|
|
1283
1351
|
def mouseMoved_(self, event):
|
|
1284
1352
|
loc = self.convertPoint_fromView_(event.locationInWindow(), None)
|
|
@@ -1306,7 +1374,9 @@ class OmniboxSuggestionView(Cocoa.NSView):
|
|
|
1306
1374
|
|
|
1307
1375
|
icon = "🔍" if sugg["type"] == "search" else ("🕒" if sugg["type"] == "history" else ("🔖" if sugg["type"] == "bookmark" else ("🧮" if sugg["type"] == "math" else "⚡")))
|
|
1308
1376
|
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: text_color}
|
|
1309
|
-
|
|
1377
|
+
# Bug 1 Fix: Ensure suggestion text is never None
|
|
1378
|
+
sugg_text = sugg.get("text") or ""
|
|
1379
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(f"{icon} {sugg_text}", attrs).drawAtPoint_(Cocoa.NSMakePoint(12, rect.origin.y + 6))
|
|
1310
1380
|
|
|
1311
1381
|
class OmniboxPanelCoordinator:
|
|
1312
1382
|
def __init__(self, window_controller):
|
|
@@ -1341,14 +1411,15 @@ class OmniboxPanelCoordinator:
|
|
|
1341
1411
|
bms = json.load(f)
|
|
1342
1412
|
for b in bms:
|
|
1343
1413
|
if isinstance(b, dict):
|
|
1344
|
-
t, u = b.get("title"
|
|
1414
|
+
t, u = b.get("title") or "", b.get("url") or ""
|
|
1345
1415
|
if query_str.lower() in t.lower() or query_str.lower() in u.lower():
|
|
1346
1416
|
results.append({"type": "bookmark", "text": t or u, "url": u})
|
|
1347
1417
|
except Exception:
|
|
1348
1418
|
pass
|
|
1349
1419
|
|
|
1420
|
+
# Bug 1 Fix: Safely fallback title for history autocomplete
|
|
1350
1421
|
for h in query_history(query_str)[:4]:
|
|
1351
|
-
results.append({"type": "history", "text": h[2], "url": h[1]})
|
|
1422
|
+
results.append({"type": "history", "text": h[2] or h[1] or "", "url": h[1]})
|
|
1352
1423
|
|
|
1353
1424
|
def fetch_remote():
|
|
1354
1425
|
try:
|
|
@@ -1419,7 +1490,7 @@ class OmniboxPanelCoordinator:
|
|
|
1419
1490
|
pb.setString_forType_(val, Cocoa.NSPasteboardTypeString)
|
|
1420
1491
|
self.hide()
|
|
1421
1492
|
return
|
|
1422
|
-
target = sugg.get("url") or sugg.get("text")
|
|
1493
|
+
target = sugg.get("url") or sugg.get("text") or ""
|
|
1423
1494
|
self.wc.omnibox.setStringValue_(target)
|
|
1424
1495
|
editor = self.wc.omnibox.currentEditor()
|
|
1425
1496
|
if editor:
|
|
@@ -1434,16 +1505,33 @@ class OmniboxPanelCoordinator:
|
|
|
1434
1505
|
self.suggestion_view.selected_index = min(len(self.suggestion_view.suggestions)-1, self.suggestion_view.selected_index + 1)
|
|
1435
1506
|
if self.suggestion_view.selected_index >= 0:
|
|
1436
1507
|
s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
|
|
1437
|
-
|
|
1508
|
+
val = s.get("url") or s.get("text") or ""
|
|
1509
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1510
|
+
# Bug 10 Fix: Update active field editor
|
|
1511
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1512
|
+
if editor:
|
|
1513
|
+
editor.setString_(val)
|
|
1514
|
+
editor.selectAll_(None)
|
|
1438
1515
|
self.suggestion_view.setNeedsDisplay_(True)
|
|
1439
1516
|
return True
|
|
1440
1517
|
elif sel == "moveUp:":
|
|
1441
1518
|
self.suggestion_view.selected_index = max(-1, self.suggestion_view.selected_index - 1)
|
|
1442
1519
|
if self.suggestion_view.selected_index >= 0:
|
|
1443
1520
|
s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
|
|
1444
|
-
|
|
1521
|
+
val = s.get("url") or s.get("text") or ""
|
|
1522
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1523
|
+
# Bug 10 Fix: Update active field editor
|
|
1524
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1525
|
+
if editor:
|
|
1526
|
+
editor.setString_(val)
|
|
1527
|
+
editor.selectAll_(None)
|
|
1445
1528
|
else:
|
|
1446
|
-
self.
|
|
1529
|
+
val = self.typed_query or ""
|
|
1530
|
+
self.wc.omnibox.setStringValue_(val)
|
|
1531
|
+
editor = self.wc.omnibox.currentEditor()
|
|
1532
|
+
if editor:
|
|
1533
|
+
editor.setString_(val)
|
|
1534
|
+
editor.selectAll_(None)
|
|
1447
1535
|
self.suggestion_view.setNeedsDisplay_(True)
|
|
1448
1536
|
return True
|
|
1449
1537
|
elif sel == "insertNewline:":
|
|
@@ -1522,6 +1610,7 @@ class FindBarView(Cocoa.NSView):
|
|
|
1522
1610
|
|
|
1523
1611
|
def onClose_(self, sender):
|
|
1524
1612
|
self.setHidden_(True)
|
|
1613
|
+
# Bug 22 Fix: Refocus webview when closing find bar
|
|
1525
1614
|
wv = self.controller.get_active_webview()
|
|
1526
1615
|
if wv:
|
|
1527
1616
|
self.controller.window().makeFirstResponder_(wv)
|
|
@@ -1529,6 +1618,11 @@ class FindBarView(Cocoa.NSView):
|
|
|
1529
1618
|
# ============================================================================
|
|
1530
1619
|
# QUICK SWITCHER & MEDIA CONTROLS (PIP)
|
|
1531
1620
|
# ============================================================================
|
|
1621
|
+
# Bug 15 Fix: Dedicated panel subclass allowing keyboard focus on borderless window
|
|
1622
|
+
class QuickSwitcherPanel(Cocoa.NSPanel):
|
|
1623
|
+
def canBecomeKeyWindow(self): return True
|
|
1624
|
+
def canBecomeMainWindow(self): return True
|
|
1625
|
+
|
|
1532
1626
|
class QuickSwitcherListView(Cocoa.NSView):
|
|
1533
1627
|
def initWithFrame_controller_(self, frame, controller):
|
|
1534
1628
|
self = objc.super(QuickSwitcherListView, self).initWithFrame_(frame)
|
|
@@ -1538,6 +1632,19 @@ class QuickSwitcherListView(Cocoa.NSView):
|
|
|
1538
1632
|
|
|
1539
1633
|
def isFlipped(self): return True
|
|
1540
1634
|
|
|
1635
|
+
def mouseDown_(self, event):
|
|
1636
|
+
loc = self.convertPoint_fromView_(event.locationInWindow(), None)
|
|
1637
|
+
idx = int(loc.y // 36)
|
|
1638
|
+
if 0 <= idx < len(self.controller.filtered_tabs):
|
|
1639
|
+
self.controller.selected_index = idx
|
|
1640
|
+
self.setNeedsDisplay_(True)
|
|
1641
|
+
t = self.controller.filtered_tabs[idx]
|
|
1642
|
+
cur_idx = t["wc"].get_tab_index_by_id(t["id"])
|
|
1643
|
+
if cur_idx is not None:
|
|
1644
|
+
t["wc"].window().makeKeyAndOrderFront_(None)
|
|
1645
|
+
t["wc"].switch_to_tab(cur_idx)
|
|
1646
|
+
self.controller.window().orderOut_(None)
|
|
1647
|
+
|
|
1541
1648
|
def drawRect_(self, dirtyRect):
|
|
1542
1649
|
for i, tab in enumerate(self.controller.filtered_tabs):
|
|
1543
1650
|
rect = Cocoa.NSMakeRect(0, i * 36, self.bounds().size.width, 36)
|
|
@@ -1548,15 +1655,19 @@ class QuickSwitcherListView(Cocoa.NSView):
|
|
|
1548
1655
|
else:
|
|
1549
1656
|
tc, sub_color = Cocoa.NSColor.labelColor(), Cocoa.NSColor.secondaryLabelColor()
|
|
1550
1657
|
|
|
1658
|
+
# Bug 16 Fix: Guard title and url against None
|
|
1659
|
+
title_text = (tab.get("title") or "Tab")[:60]
|
|
1660
|
+
url_text = (tab.get("url") or "")[:80]
|
|
1551
1661
|
attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: tc}
|
|
1552
|
-
Cocoa.NSAttributedString.alloc().initWithString_attributes_(
|
|
1662
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(title_text, attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 3))
|
|
1553
1663
|
sub_attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: sub_color}
|
|
1554
|
-
Cocoa.NSAttributedString.alloc().initWithString_attributes_(
|
|
1664
|
+
Cocoa.NSAttributedString.alloc().initWithString_attributes_(url_text, sub_attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 19))
|
|
1555
1665
|
|
|
1556
1666
|
class QuickSwitcherController(Cocoa.NSWindowController):
|
|
1557
1667
|
def init(self):
|
|
1558
1668
|
w, h = 600, 400
|
|
1559
|
-
window
|
|
1669
|
+
# Bug 15 Fix: Use QuickSwitcherPanel so window can receive keyboard events
|
|
1670
|
+
window = QuickSwitcherPanel.alloc().initWithContentRect_styleMask_backing_defer_(
|
|
1560
1671
|
Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskBorderless | Cocoa.NSWindowStyleMaskFullSizeContentView, Cocoa.NSBackingStoreBuffered, False
|
|
1561
1672
|
)
|
|
1562
1673
|
window.setOpaque_(False)
|
|
@@ -1599,8 +1710,8 @@ class QuickSwitcherController(Cocoa.NSWindowController):
|
|
|
1599
1710
|
for t in wc.tabs:
|
|
1600
1711
|
self.tabs.append({
|
|
1601
1712
|
"id": t.get("id"),
|
|
1602
|
-
"title": t.get("title"
|
|
1603
|
-
"url": t.get("url"
|
|
1713
|
+
"title": t.get("title") or "Tab",
|
|
1714
|
+
"url": t.get("url") or "",
|
|
1604
1715
|
"wc": wc
|
|
1605
1716
|
})
|
|
1606
1717
|
self.filtered_tabs = self.tabs.copy()
|
|
@@ -1620,7 +1731,8 @@ class QuickSwitcherController(Cocoa.NSWindowController):
|
|
|
1620
1731
|
def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
|
|
1621
1732
|
sel = str(commandSelector)
|
|
1622
1733
|
if sel == "moveDown:":
|
|
1623
|
-
|
|
1734
|
+
if self.filtered_tabs:
|
|
1735
|
+
self.selected_index = min(len(self.filtered_tabs) - 1, self.selected_index + 1)
|
|
1624
1736
|
self.list_view.setNeedsDisplay_(True)
|
|
1625
1737
|
return True
|
|
1626
1738
|
elif sel == "moveUp:":
|
|
@@ -1652,6 +1764,7 @@ class PiPPanelController(Cocoa.NSWindowController):
|
|
|
1652
1764
|
window.setFloatingPanel_(True)
|
|
1653
1765
|
window.setLevel_(Cocoa.NSFloatingWindowLevel)
|
|
1654
1766
|
window.setReleasedWhenClosed_(False)
|
|
1767
|
+
window.center()
|
|
1655
1768
|
self = objc.super(PiPPanelController, self).initWithWindow_(window)
|
|
1656
1769
|
if self:
|
|
1657
1770
|
self.browser = browser_controller
|
|
@@ -1701,6 +1814,7 @@ class PiPPanelController(Cocoa.NSWindowController):
|
|
|
1701
1814
|
# BROWSER WINDOW CONTROLLER
|
|
1702
1815
|
# ============================================================================
|
|
1703
1816
|
TITLE_BAR_H = 30
|
|
1817
|
+
SCRIPT_HANDLER_NAMES = ("bridge", "audio", "terminalInput", "terminalResize")
|
|
1704
1818
|
|
|
1705
1819
|
class BrowserWindowController(Cocoa.NSWindowController):
|
|
1706
1820
|
def init(self):
|
|
@@ -1947,6 +2061,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
1947
2061
|
wv.setFrame_(self.web_container.bounds())
|
|
1948
2062
|
self.relayout_tabs()
|
|
1949
2063
|
|
|
2064
|
+
def move_tab(self, from_idx, to_idx):
|
|
2065
|
+
"""Bug 2 Fix: Interactive tab reordering."""
|
|
2066
|
+
if from_idx == to_idx or not (0 <= from_idx < len(self.tabs) and 0 <= to_idx < len(self.tabs)):
|
|
2067
|
+
return
|
|
2068
|
+
tab = self.tabs.pop(from_idx)
|
|
2069
|
+
self.tabs.insert(to_idx, tab)
|
|
2070
|
+
self.active_tab_index = to_idx
|
|
2071
|
+
self.relayout_tabs()
|
|
2072
|
+
self.save_session()
|
|
2073
|
+
|
|
1950
2074
|
def relayout_tabs(self):
|
|
1951
2075
|
for sub in list(self.tab_strip.subviews()):
|
|
1952
2076
|
sub.removeFromSuperview()
|
|
@@ -1956,7 +2080,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
1956
2080
|
tab_h = 28 if not is_vert else 32
|
|
1957
2081
|
y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
|
|
1958
2082
|
|
|
1959
|
-
|
|
2083
|
+
# Bug 3 Fix: Safely guard against orphan group IDs
|
|
2084
|
+
vis = [t for t in self.tabs if not t.get("group_id") or not (self.get_group(t["group_id"]) and self.get_group(t["group_id"]).collapsed)]
|
|
1960
2085
|
pinned_cnt = sum(1 for t in vis if t.get("pinned", False))
|
|
1961
2086
|
tab_w = max(60, min(240, (w - x - 44 - (pinned_cnt * 36)) / max(1, len(vis) - pinned_cnt))) if not is_vert else w
|
|
1962
2087
|
|
|
@@ -2033,10 +2158,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2033
2158
|
|
|
2034
2159
|
config.setURLSchemeHandler_forURLScheme_(InternalSchemeHandler.alloc().init(), "browser")
|
|
2035
2160
|
uc = WebKit.WKUserContentController.alloc().init()
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
uc.addScriptMessageHandler_name_(self, "terminalInput")
|
|
2039
|
-
uc.addScriptMessageHandler_name_(self, "terminalResize")
|
|
2161
|
+
for h_name in SCRIPT_HANDLER_NAMES:
|
|
2162
|
+
uc.addScriptMessageHandler_name_(self, h_name)
|
|
2040
2163
|
|
|
2041
2164
|
# Injected scripts: Command+click capture, audio monitoring, context menus, and fullscreen handlers
|
|
2042
2165
|
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(COMMAND_CLICK_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
|
|
@@ -2065,7 +2188,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2065
2188
|
reg = m.replace("*", ".*").replace("/", escaped_slash)
|
|
2066
2189
|
conds.append(f"new RegExp('^{reg}$').test(location.href)")
|
|
2067
2190
|
cond_str = " || ".join(conds) if conds else "true"
|
|
2068
|
-
|
|
2191
|
+
# Bug 26 Fix: Wrap in an IIFE so top-level return statements in scripts execute safely
|
|
2192
|
+
wrapped = f"if ({cond_str}) {{\n(function() {{\n" + script["code"] + "\n}})();\n}}"
|
|
2069
2193
|
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(wrapped, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
2070
2194
|
|
|
2071
2195
|
if WK_RULE_LIST and SETTINGS.settings.get("adblock_enabled", True):
|
|
@@ -2158,6 +2282,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2158
2282
|
wv.removeObserver_forKeyPath_(self, k)
|
|
2159
2283
|
except Exception:
|
|
2160
2284
|
pass
|
|
2285
|
+
# Bug 20 Fix: Remove message handlers to eliminate retain cycles
|
|
2286
|
+
uc = wv.configuration().userContentController()
|
|
2287
|
+
for name in SCRIPT_HANDLER_NAMES:
|
|
2288
|
+
try:
|
|
2289
|
+
uc.removeScriptMessageHandlerForName_(name)
|
|
2290
|
+
except Exception:
|
|
2291
|
+
pass
|
|
2161
2292
|
if wv.superview():
|
|
2162
2293
|
wv.removeFromSuperview()
|
|
2163
2294
|
if not self.tabs:
|
|
@@ -2383,6 +2514,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2383
2514
|
def handle_escape_key(self):
|
|
2384
2515
|
if self.find_bar and not self.find_bar.isHidden():
|
|
2385
2516
|
self.find_bar.setHidden_(True)
|
|
2517
|
+
wv = self.get_active_webview()
|
|
2518
|
+
if wv:
|
|
2519
|
+
self.window().makeFirstResponder_(wv)
|
|
2386
2520
|
return
|
|
2387
2521
|
if self.is_active_tab_terminal():
|
|
2388
2522
|
return
|
|
@@ -2450,7 +2584,11 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2450
2584
|
inc_item = menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
|
|
2451
2585
|
inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
2452
2586
|
inc_item.setTarget_(Cocoa.NSApp.delegate())
|
|
2453
|
-
|
|
2587
|
+
|
|
2588
|
+
term_item = menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "openTerminal:", "t")
|
|
2589
|
+
term_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagOption)
|
|
2590
|
+
term_item.setTarget_(self)
|
|
2591
|
+
|
|
2454
2592
|
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2455
2593
|
menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
|
|
2456
2594
|
bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
|
|
@@ -2544,9 +2682,12 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2544
2682
|
if not wv: return
|
|
2545
2683
|
url = wv.URL().absoluteString() if wv.URL() else "browser://newtab"
|
|
2546
2684
|
parsed = Foundation.NSURL.URLWithString_(url)
|
|
2685
|
+
# Bug 24 Fix: Safely guard against None parsed URL
|
|
2686
|
+
host_str = (parsed.host() or "Internal Page") if parsed else "Internal Page"
|
|
2687
|
+
is_https = (parsed.scheme() == "https") if parsed else False
|
|
2547
2688
|
menu = Cocoa.NSMenu.alloc().init()
|
|
2548
|
-
menu.addItemWithTitle_action_keyEquivalent_(f"Host: {
|
|
2549
|
-
menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if
|
|
2689
|
+
menu.addItemWithTitle_action_keyEquivalent_(f"Host: {host_str}", "", "").setEnabled_(False)
|
|
2690
|
+
menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if is_https else "Connection: Insecure / Local", "", "").setEnabled_(False)
|
|
2550
2691
|
menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
2551
2692
|
menu.addItemWithTitle_action_keyEquivalent_("Copy Current URL", "onCopyUrl:", "").setTarget_(self)
|
|
2552
2693
|
menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
|
|
@@ -2591,7 +2732,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2591
2732
|
wv = self.get_active_webview()
|
|
2592
2733
|
if wv:
|
|
2593
2734
|
wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(val)))
|
|
2594
|
-
|
|
2735
|
+
# Bug 11 Fix: Transfer keyboard focus directly to active webview
|
|
2736
|
+
self.window().makeFirstResponder_(wv)
|
|
2595
2737
|
|
|
2596
2738
|
def controlTextDidChange_(self, notif):
|
|
2597
2739
|
if notif.object() == self.omnibox:
|
|
@@ -2725,6 +2867,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2725
2867
|
elif cmd == "toggle_fullscreen_terminal":
|
|
2726
2868
|
self.toggle_tab_bar()
|
|
2727
2869
|
elif cmd == "context_target_image":
|
|
2870
|
+
# Bug 9 Fix: Store or clear target image URL
|
|
2728
2871
|
self.last_context_image_url = body.get("src")
|
|
2729
2872
|
elif cmd == "blob_image_data":
|
|
2730
2873
|
data_url, dest_path = body.get("data"), body.get("path")
|
|
@@ -2758,14 +2901,23 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2758
2901
|
elif cmd == "prompt_save_password":
|
|
2759
2902
|
self.show_password_banner(body.get("host", ""), body.get("username", ""), body.get("password", ""))
|
|
2760
2903
|
elif cmd == "check_autofill":
|
|
2904
|
+
# Bug 18 & 19 Fix: Apply credentials specifically to requesting webview using safe JSON encoding
|
|
2761
2905
|
host = body.get("host")
|
|
2762
2906
|
saved_user = SETTINGS.settings.get("saved_logins", {}).get(host)
|
|
2763
2907
|
if saved_user:
|
|
2764
2908
|
pwd = get_from_keychain(host, saved_user)
|
|
2765
2909
|
if pwd:
|
|
2766
|
-
|
|
2767
|
-
if
|
|
2768
|
-
|
|
2910
|
+
calling_wv = message.webView()
|
|
2911
|
+
if calling_wv:
|
|
2912
|
+
pwd_json = json.dumps(pwd)
|
|
2913
|
+
user_json = json.dumps(saved_user)
|
|
2914
|
+
js = f"""(function() {{
|
|
2915
|
+
let pw = document.querySelector('input[type=password]');
|
|
2916
|
+
let user = document.querySelector('input[type=text], input[type=email]');
|
|
2917
|
+
if (pw) pw.value = {pwd_json};
|
|
2918
|
+
if (user) user.value = {user_json};
|
|
2919
|
+
}})();"""
|
|
2920
|
+
calling_wv.evaluateJavaScript_completionHandler_(js, None)
|
|
2769
2921
|
elif cmd == "save_setting":
|
|
2770
2922
|
raw_key, raw_val = body.get("key"), body.get("val")
|
|
2771
2923
|
if raw_key is None:
|
|
@@ -2789,12 +2941,15 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2789
2941
|
for sub in c.tab_strip.subviews():
|
|
2790
2942
|
sub.setNeedsDisplay_(True)
|
|
2791
2943
|
elif key in ("default_zoom",):
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
t
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2944
|
+
try:
|
|
2945
|
+
new_zoom = float(val)
|
|
2946
|
+
for t in self.tabs:
|
|
2947
|
+
t["zoom"] = new_zoom
|
|
2948
|
+
twv = t.get("webview")
|
|
2949
|
+
if twv:
|
|
2950
|
+
self.apply_zoom_to_webview(twv, t)
|
|
2951
|
+
except (ValueError, TypeError):
|
|
2952
|
+
pass
|
|
2798
2953
|
elif key in ("user_agent_mode", "custom_user_agent"):
|
|
2799
2954
|
ua = SETTINGS.get_user_agent()
|
|
2800
2955
|
for t in self.tabs:
|
|
@@ -2868,8 +3023,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2868
3023
|
FaviconCache.clear()
|
|
2869
3024
|
self.bookmarks_bar.reload_bookmarks()
|
|
2870
3025
|
if "dns_cache" in targets:
|
|
2871
|
-
|
|
2872
|
-
|
|
3026
|
+
# Bug 12 Fix: Completely wipe in-memory and disk DNS cache
|
|
3027
|
+
clear_dns_cache_and_disk()
|
|
2873
3028
|
if "cookies_cache" in targets:
|
|
2874
3029
|
ds = WebKit.WKWebsiteDataStore.defaultDataStore()
|
|
2875
3030
|
ds.removeDataOfTypes_modifiedSince_completionHandler_(WebKit.WKWebsiteDataStore.allWebsiteDataTypes(), Foundation.NSDate.dateWithTimeIntervalSince1970_(0), lambda: None)
|
|
@@ -3047,6 +3202,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3047
3202
|
if not t.get("is_sleeping"):
|
|
3048
3203
|
t["title"] = str(obj.title())
|
|
3049
3204
|
break
|
|
3205
|
+
# Bug 6 Fix: Update SQLite history with real page title when available
|
|
3206
|
+
if not self.is_incognito and obj.URL() and SETTINGS.settings.get("history_enabled", True):
|
|
3207
|
+
record_history(obj.URL().absoluteString(), str(obj.title()))
|
|
3050
3208
|
self.relayout_tabs()
|
|
3051
3209
|
elif keyPath == "estimatedProgress":
|
|
3052
3210
|
self.update_progress_for_webview(obj)
|
|
@@ -3089,6 +3247,14 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3089
3247
|
self.omnibox_panel.hide()
|
|
3090
3248
|
|
|
3091
3249
|
def windowWillClose_(self, notif):
|
|
3250
|
+
# Bug 4 Fix: Invalidate repeating NSTimer to prevent memory retain cycle
|
|
3251
|
+
if hasattr(self, 'sleep_timer') and self.sleep_timer:
|
|
3252
|
+
self.sleep_timer.invalidate()
|
|
3253
|
+
self.sleep_timer = None
|
|
3254
|
+
|
|
3255
|
+
if hasattr(self, 'omnibox_panel') and self.omnibox_panel.is_visible():
|
|
3256
|
+
self.omnibox_panel.hide()
|
|
3257
|
+
|
|
3092
3258
|
self.save_session()
|
|
3093
3259
|
for t in self.tabs:
|
|
3094
3260
|
wv = t.get("webview")
|
|
@@ -3099,6 +3265,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3099
3265
|
wv.removeObserver_forKeyPath_(self, k)
|
|
3100
3266
|
except Exception:
|
|
3101
3267
|
pass
|
|
3268
|
+
# Bug 20 Fix: Clean up userContentController message handlers
|
|
3269
|
+
uc = wv.configuration().userContentController()
|
|
3270
|
+
for name in SCRIPT_HANDLER_NAMES:
|
|
3271
|
+
try:
|
|
3272
|
+
uc.removeScriptMessageHandlerForName_(name)
|
|
3273
|
+
except Exception:
|
|
3274
|
+
pass
|
|
3102
3275
|
app_delegate = Cocoa.NSApp.delegate()
|
|
3103
3276
|
if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
|
|
3104
3277
|
app_delegate.controllers.remove(self)
|
|
@@ -3171,12 +3344,14 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3171
3344
|
# Allow Escape to pass directly into terminal tabs (vim, nano, fzf, less)
|
|
3172
3345
|
if wc.is_active_tab_terminal():
|
|
3173
3346
|
return event
|
|
3347
|
+
# Bug 22 Fix: Refocus webview when find_bar is closed via Escape
|
|
3174
3348
|
if wc.find_bar and not wc.find_bar.isHidden():
|
|
3175
3349
|
wc.find_bar.setHidden_(True)
|
|
3350
|
+
wv = wc.get_active_webview()
|
|
3351
|
+
if wv:
|
|
3352
|
+
wc.window().makeFirstResponder_(wv)
|
|
3176
3353
|
return None
|
|
3177
|
-
|
|
3178
|
-
wc.toggle_tab_bar()
|
|
3179
|
-
return None
|
|
3354
|
+
# Bug 21 Fix: Delegate Escape to handle_escape_key() so HTML5 fullscreen exit is not bypassed
|
|
3180
3355
|
wc.handle_escape_key()
|
|
3181
3356
|
return event
|
|
3182
3357
|
|
|
@@ -3210,11 +3385,24 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3210
3385
|
wc = BrowserWindowController.alloc().initWithIncognito_(True)
|
|
3211
3386
|
self.controllers.append(wc)
|
|
3212
3387
|
|
|
3388
|
+
# Bug 7 Fix: Allow opening new tabs when all windows are closed
|
|
3389
|
+
def onNewTab_(self, sender=None):
|
|
3390
|
+
win = Cocoa.NSApp.keyWindow()
|
|
3391
|
+
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
|
|
3392
|
+
if wc:
|
|
3393
|
+
wc.onNewTab_(sender)
|
|
3394
|
+
else:
|
|
3395
|
+
self.onNewWindow_(sender)
|
|
3396
|
+
|
|
3213
3397
|
def onNewTerminal_(self, sender):
|
|
3214
3398
|
win = Cocoa.NSApp.keyWindow()
|
|
3215
3399
|
wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else (self.controllers[0] if self.controllers else None)
|
|
3216
3400
|
if wc:
|
|
3217
3401
|
wc.add_tab("browser://terminal")
|
|
3402
|
+
else:
|
|
3403
|
+
self.onNewWindow_(None)
|
|
3404
|
+
if self.controllers:
|
|
3405
|
+
self.controllers[-1].add_tab("browser://terminal")
|
|
3218
3406
|
|
|
3219
3407
|
def onCloseTab_(self, sender=None):
|
|
3220
3408
|
win = Cocoa.NSApp.keyWindow()
|
|
@@ -3245,7 +3433,12 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3245
3433
|
file_item = Cocoa.NSMenuItem.alloc().init()
|
|
3246
3434
|
file_menu = Cocoa.NSMenu.alloc().initWithTitle_("File")
|
|
3247
3435
|
file_menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t")
|
|
3248
|
-
|
|
3436
|
+
|
|
3437
|
+
# Bug 27 Fix: Assign distinct shortcut Cmd+Option+T to Terminal tab so Cmd+Shift+T belongs to Reopen Closed Tab
|
|
3438
|
+
term_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "onNewTerminal:", "t")
|
|
3439
|
+
term_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagOption)
|
|
3440
|
+
term_item.setTarget_(self)
|
|
3441
|
+
|
|
3249
3442
|
file_menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(self)
|
|
3250
3443
|
inc_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
|
|
3251
3444
|
inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
|
|
@@ -3304,7 +3497,8 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3304
3497
|
for i in range(1, 10):
|
|
3305
3498
|
win_menu.addItemWithTitle_action_keyEquivalent_(f"Tab {i}", f"onSwitchTab{i}:", str(i))
|
|
3306
3499
|
win_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
|
|
3307
|
-
win_menu.addItemWithTitle_action_keyEquivalent_("Quick Switcher", "quickSwitcher:", "k")
|
|
3500
|
+
qs_item = win_menu.addItemWithTitle_action_keyEquivalent_("Quick Switcher", "quickSwitcher:", "k")
|
|
3501
|
+
qs_item.setTarget_(self)
|
|
3308
3502
|
win_menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(self)
|
|
3309
3503
|
win_item.setSubmenu_(win_menu)
|
|
3310
3504
|
main_menu.addItem_(win_item)
|
|
@@ -36,6 +36,9 @@ DNS_CACHE = {}
|
|
|
36
36
|
DNS_LOCK = threading.Lock()
|
|
37
37
|
DNS_CACHE_DIRTY = False
|
|
38
38
|
|
|
39
|
+
_PROXY_STARTED = False
|
|
40
|
+
_PROXY_START_LOCK = threading.Lock()
|
|
41
|
+
|
|
39
42
|
def rebuild_active_domains():
|
|
40
43
|
global ACTIVE_BLOCK_DOMAINS
|
|
41
44
|
with ADBLOCK_LOCK:
|
|
@@ -92,9 +95,12 @@ def load_dns_cache():
|
|
|
92
95
|
if os.path.exists(DNS_CACHE_FILE):
|
|
93
96
|
try:
|
|
94
97
|
with open(DNS_CACHE_FILE, 'r', encoding='utf-8') as f:
|
|
95
|
-
|
|
98
|
+
loaded = json.load(f)
|
|
99
|
+
with DNS_LOCK:
|
|
100
|
+
DNS_CACHE = loaded if isinstance(loaded, dict) else {}
|
|
96
101
|
except Exception:
|
|
97
|
-
|
|
102
|
+
with DNS_LOCK:
|
|
103
|
+
DNS_CACHE = {}
|
|
98
104
|
|
|
99
105
|
def flush_dns_cache_to_disk():
|
|
100
106
|
global DNS_CACHE_DIRTY
|
|
@@ -107,6 +113,18 @@ def flush_dns_cache_to_disk():
|
|
|
107
113
|
except Exception:
|
|
108
114
|
pass
|
|
109
115
|
|
|
116
|
+
def clear_dns_cache_and_disk():
|
|
117
|
+
"""Bug 12 Fix: Clears memory cache and wipes disk persistence file completely."""
|
|
118
|
+
global DNS_CACHE, DNS_CACHE_DIRTY
|
|
119
|
+
with DNS_LOCK:
|
|
120
|
+
DNS_CACHE.clear()
|
|
121
|
+
DNS_CACHE_DIRTY = False
|
|
122
|
+
try:
|
|
123
|
+
if os.path.exists(DNS_CACHE_FILE):
|
|
124
|
+
os.remove(DNS_CACHE_FILE)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
110
128
|
def doh_resolve(hostname):
|
|
111
129
|
global DNS_CACHE_DIRTY
|
|
112
130
|
now = time.time()
|
|
@@ -282,24 +300,31 @@ class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
|
|
|
282
300
|
pass
|
|
283
301
|
|
|
284
302
|
def start_proxy_server():
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if hasattr(socket, "SO_REUSEPORT"):
|
|
291
|
-
try:
|
|
292
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
293
|
-
except Exception:
|
|
294
|
-
pass
|
|
295
|
-
super().server_bind()
|
|
303
|
+
# Bug 23 Fix: Prevent multiple TCP server instances & duplicate worker threads
|
|
304
|
+
global _PROXY_STARTED
|
|
305
|
+
with _PROXY_START_LOCK:
|
|
306
|
+
if _PROXY_STARTED:
|
|
307
|
+
return
|
|
296
308
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
309
|
+
class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
310
|
+
allow_reuse_address = True
|
|
311
|
+
daemon_threads = True
|
|
312
|
+
|
|
313
|
+
def server_bind(self):
|
|
314
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
315
|
+
try:
|
|
316
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
super().server_bind()
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
server = ReusableTCPServer(("127.0.0.1", LOCAL_PROXY_PORT), DoHProxyHandler)
|
|
323
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
324
|
+
threading.Thread(target=dns_flush_worker, daemon=True).start()
|
|
325
|
+
_PROXY_STARTED = True
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"[Proxy Warning] Could not start local proxy server: {e}")
|
|
303
328
|
|
|
304
329
|
def dns_flush_worker():
|
|
305
330
|
while True:
|
|
@@ -11,6 +11,7 @@ import re
|
|
|
11
11
|
import html
|
|
12
12
|
import tempfile
|
|
13
13
|
import shutil
|
|
14
|
+
import copy
|
|
14
15
|
|
|
15
16
|
UA_PRESETS = {
|
|
16
17
|
"safari_18": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
|
@@ -188,7 +189,7 @@ class SettingsManager:
|
|
|
188
189
|
"saved_logins": {},
|
|
189
190
|
"history_enabled": True
|
|
190
191
|
}
|
|
191
|
-
self.settings =
|
|
192
|
+
self.settings = copy.deepcopy(self.defaults)
|
|
192
193
|
self.load()
|
|
193
194
|
|
|
194
195
|
def load(self):
|
|
@@ -220,7 +221,8 @@ class SettingsManager:
|
|
|
220
221
|
print(f"[Settings Save Error] {e}")
|
|
221
222
|
|
|
222
223
|
def reset_defaults(self):
|
|
223
|
-
|
|
224
|
+
# Bug 25 Fix: Deep copy to prevent mutation of defaults dictionary
|
|
225
|
+
self.settings = copy.deepcopy(self.defaults)
|
|
224
226
|
self.save()
|
|
225
227
|
|
|
226
228
|
def get_search_url(self, query):
|
|
@@ -696,9 +698,12 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
696
698
|
window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
|
|
697
699
|
}}
|
|
698
700
|
}}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
701
|
+
// Bug 14 Fix: Accept either pre-parsed Array or JSON string safely
|
|
702
|
+
window.renderBlockedResults = function(data) {{
|
|
703
|
+
let items = Array.isArray(data) ? data : [];
|
|
704
|
+
if (!items.length && typeof data === 'string') {{
|
|
705
|
+
try {{ items = JSON.parse(data); }} catch(e) {{ items = []; }}
|
|
706
|
+
}}
|
|
702
707
|
const box = document.getElementById('inspector-results');
|
|
703
708
|
if (!items || items.length === 0) {{
|
|
704
709
|
box.innerHTML = '<span style="color:#D93025;">No matches found. Domain is NOT currently blocked.</span>';
|
|
@@ -706,7 +711,7 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
|
|
|
706
711
|
}}
|
|
707
712
|
let out = '<div style="margin-bottom:6px;font-weight:600;color:var(--primary);">' + items.length + ' Matches Found:</div>';
|
|
708
713
|
items.forEach(it => {{
|
|
709
|
-
out += '<div><strong>[BLOCKED]</strong> ' + it.domain + ' <span style="opacity:0.6;">(' + it.source + ')</span></div>';
|
|
714
|
+
out += '<div><strong>[BLOCKED]</strong> ' + (it.domain || '') + ' <span style="opacity:0.6;">(' + (it.source || '') + ')</span></div>';
|
|
710
715
|
}});
|
|
711
716
|
box.innerHTML = out;
|
|
712
717
|
}};
|
|
@@ -114,11 +114,15 @@ class ProcessManagerController(Cocoa.NSWindowController):
|
|
|
114
114
|
|
|
115
115
|
def onPurgeMemory_(self, sender):
|
|
116
116
|
try:
|
|
117
|
-
from pybrowser.doh_server import
|
|
118
|
-
|
|
119
|
-
DNS_CACHE.clear()
|
|
117
|
+
from pybrowser.doh_server import clear_dns_cache_and_disk
|
|
118
|
+
clear_dns_cache_and_disk()
|
|
120
119
|
except Exception:
|
|
121
|
-
|
|
120
|
+
try:
|
|
121
|
+
from pybrowser.doh_server import DNS_CACHE, DNS_LOCK
|
|
122
|
+
with DNS_LOCK:
|
|
123
|
+
DNS_CACHE.clear()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
122
126
|
|
|
123
127
|
try:
|
|
124
128
|
from pybrowser.browser import FaviconCache
|
|
@@ -363,4 +367,4 @@ class ProcessManagerController(Cocoa.NSWindowController):
|
|
|
363
367
|
session = target[1]
|
|
364
368
|
if session:
|
|
365
369
|
session.close()
|
|
366
|
-
self.refresh_data()
|
|
370
|
+
self.refresh_data()
|
|
@@ -222,11 +222,12 @@ class TerminalSession:
|
|
|
222
222
|
env["CARGO_HTTP_PROXY"] = proxy_url
|
|
223
223
|
|
|
224
224
|
# Inject PyBrowser CLI tools into PATH
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
225
|
+
# $$$$$
|
|
226
|
+
# bin_dir = os.path.join(get_data_dir(), "bin")
|
|
227
|
+
# ensure_doh_cli_helper(bin_dir, port)
|
|
228
|
+
# cur_path = env.get("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
|
|
229
|
+
# if bin_dir not in cur_path:
|
|
230
|
+
# env["PATH"] = f"{bin_dir}:{cur_path}"
|
|
230
231
|
|
|
231
232
|
# PyBrowser session indicators
|
|
232
233
|
env["PYBROWSER_DOH_PROXY"] = proxy_url
|
|
@@ -401,7 +402,8 @@ class TerminalSession:
|
|
|
401
402
|
class TerminalManager:
|
|
402
403
|
def __init__(self):
|
|
403
404
|
self.sessions = {}
|
|
404
|
-
|
|
405
|
+
# Bug 13 Fix: Use RLock to prevent reentrant deadlocks when session.close() calls on_close
|
|
406
|
+
self.lock = threading.RLock()
|
|
405
407
|
|
|
406
408
|
def get_or_create(self, webview, cols=None, rows=None):
|
|
407
409
|
with self.lock:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/requires.txt
RENAMED
|
File without changes
|
{stupidpythonbrowser-1.0.3 → stupidpythonbrowser-1.0.4}/stupidpythonbrowser.egg-info/top_level.txt
RENAMED
|
File without changes
|