stupidpythonbrowser 1.0.2__py3-none-any.whl → 1.0.4__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/__init__.py CHANGED
@@ -3,7 +3,7 @@ PyBrowser: Native macOS Chromium/Material 3 Web Browser.
3
3
  Package Root.
4
4
  """
5
5
 
6
- __version__ = "1.0.2"
6
+ __version__ = "1.0.4"
7
7
  __all__ = [
8
8
  "browser",
9
9
  "settings",
pybrowser/browser.py CHANGED
@@ -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("{img_url}");
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("{img_url}", {{ credentials: 'include' }});
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
- conn.execute("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
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,
@@ -455,15 +468,24 @@ COMMAND_CLICK_JS = """
455
468
 
456
469
  AUDIO_MONITOR_JS = """
457
470
  document.addEventListener('play', (e) => {
458
- if (e.target && !e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
471
+ if (e.target && !e.target.muted && window.webkit?.messageHandlers?.audio) {
472
+ window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
473
+ }
474
+ }, true);
475
+ document.addEventListener('pause', () => {
476
+ if (window.webkit?.messageHandlers?.audio) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
477
+ }, true);
478
+ document.addEventListener('ended', () => {
479
+ if (window.webkit?.messageHandlers?.audio) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
459
480
  }, true);
460
- document.addEventListener('pause', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
461
- document.addEventListener('ended', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
462
481
  document.addEventListener('volumechange', (e) => {
463
- if (e.target && e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
482
+ if (e.target && e.target.muted && window.webkit?.messageHandlers?.audio) {
483
+ window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
484
+ }
464
485
  }, true);
465
486
  """
466
487
 
488
+ # Bug 9 Fix: Always report target image (or null) to clear stale context menu states
467
489
  IMAGE_CONTEXT_MENU_JS = """
468
490
  (function() {
469
491
  document.addEventListener('contextmenu', function(e) {
@@ -478,30 +500,33 @@ IMAGE_CONTEXT_MENU_JS = """
478
500
  let bg = window.getComputedStyle(el).backgroundImage;
479
501
  if (bg && bg.startsWith('url(')) imgSrc = bg.slice(4, -1).replace(/["']/g, "");
480
502
  }
481
- if (imgSrc && window.webkit?.messageHandlers?.bridge) {
503
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
482
504
  window.webkit.messageHandlers.bridge.postMessage({cmd: 'context_target_image', src: imgSrc});
483
505
  }
484
506
  }, true);
485
507
  })();
486
508
  """
487
509
 
510
+ # Bug 18 Fix: Wrap keychain autosave in IIFE to protect scope
488
511
  KEYCHAIN_AUTOSAVE_JS = """
489
- document.addEventListener('submit', (e) => {
490
- if (!e.target || !e.target.querySelector) return;
491
- let pw = e.target.querySelector('input[type="password"]');
492
- let user = e.target.querySelector('input[type="text"], input[type="email"]');
493
- if (pw && pw.value) {
494
- window.webkit.messageHandlers.bridge.postMessage({
495
- cmd: 'prompt_save_password', host: location.hostname,
496
- username: user ? user.value : 'unknown', password: pw.value
497
- });
498
- }
499
- }, true);
500
- window.addEventListener('load', () => {
501
- if (location.hostname && window.webkit?.messageHandlers?.bridge) {
502
- window.webkit.messageHandlers.bridge.postMessage({cmd: 'check_autofill', host: location.hostname});
503
- }
504
- });
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
+ })();
505
530
  """
506
531
 
507
532
  COSMETIC_ADBLOCK_CSS = """
@@ -630,14 +655,16 @@ class TabGroupHeaderView(Cocoa.NSView):
630
655
  hex_to_nscolor(GROUP_COLORS.get(self.group.color_name, "#5F6368")).setFill()
631
656
  path.fill()
632
657
  attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: Cocoa.NSColor.whiteColor()}
633
- Cocoa.NSAttributedString.alloc().initWithString_attributes_(self.group.title, attrs).drawAtPoint_(Cocoa.NSMakePoint(6, 6))
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))
634
661
 
635
662
  def mouseDown_(self, event):
636
663
  self.group.collapsed = not self.group.collapsed
637
664
  self.controller.relayout_tabs()
638
665
 
639
666
  # ============================================================================
640
- # TITLE BAR VIEW (DEDICATED TOP BAR FOR DRAGGING, EXPANDING & ZEN TRAFFIC LIGHTS)
667
+ # TITLE BAR VIEW
641
668
  # ============================================================================
642
669
  class TitleBarView(Cocoa.NSView):
643
670
  """
@@ -711,6 +738,9 @@ class ChromeTabView(Cocoa.NSView):
711
738
  self.tracking_area = None
712
739
  self.close_btn_rect = Cocoa.NSZeroRect
713
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
714
744
  return self
715
745
 
716
746
  def updateTrackingAreas(self):
@@ -790,10 +820,10 @@ class ChromeTabView(Cocoa.NSView):
790
820
  }
791
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))
792
822
 
793
- # Tab title
823
+ # Tab title (Bug 17 Fix: Ensure title is never None)
794
824
  title_x = icon_x + 20.0
795
825
  max_title_w = max(10.0, bounds.size.width - title_x - right_offset)
796
- title = tab.get("title", "New Tab")
826
+ title = tab.get("title") or "New Tab"
797
827
  text_color = Cocoa.NSColor.labelColor() if is_active else Cocoa.NSColor.secondaryLabelColor()
798
828
  title_attrs = {
799
829
  Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(12),
@@ -811,6 +841,42 @@ class ChromeTabView(Cocoa.NSView):
811
841
  self.controller.toggle_mute(self.index)
812
842
  return
813
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
814
880
 
815
881
  def menuForEvent_(self, event):
816
882
  menu = Cocoa.NSMenu.alloc().init()
@@ -880,7 +946,8 @@ class BookmarksBarView(Cocoa.NSView):
880
946
  for b in bms:
881
947
  if not isinstance(b, dict) or not b.get("url"):
882
948
  continue
883
- title = b.get("title", b["url"])[:24]
949
+ # Bug 16 Fix: Safely handle None bookmark titles
950
+ title = (b.get("title") or b.get("url") or "")[:24]
884
951
  btn_w = max(50, min(160, len(title) * 7.5 + 26))
885
952
  btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
886
953
  btn.setTitle_(title)
@@ -1104,26 +1171,34 @@ window.writeToTerm = b => {{
1104
1171
  for b in bms:
1105
1172
  if isinstance(b, dict) and b.get("url"):
1106
1173
  u = b["url"]
1107
- t = b.get("title", u)
1174
+ # Bug 16 Fix: Guard title against None
1175
+ t = b.get("title") or u or ""
1108
1176
  safe_u = json.dumps(u)
1177
+ esc_u = html.escape(u)
1178
+ esc_t = html.escape(t)
1109
1179
  host = urllib.parse.urlparse(u).netloc
1110
- 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 ""
1111
- del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
1112
- 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='{html.escape(u)}' style='color:#1a73e8;text-decoration:none;'>{html.escape(t)}</a></span> <button onclick='{del_call}' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
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 ""
1181
+ del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
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>")
1113
1183
  rows = "".join(items)
1114
- 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='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>"
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>"
1115
1185
 
1116
1186
  def get_history_html(self):
1117
1187
  records = query_history()
1118
1188
  items = []
1119
1189
  for r in records:
1120
- del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
1121
- 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>")
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();"
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>")
1122
1197
  rows = "".join(items)
1123
- 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>"
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>"
1124
1199
 
1125
1200
  def get_downloads_html(self):
1126
- 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>"
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>"
1127
1202
 
1128
1203
  def get_extensions_html(self):
1129
1204
  scripts = get_installed_userscripts()
@@ -1132,8 +1207,8 @@ window.writeToTerm = b => {{
1132
1207
  f = s["file"]
1133
1208
  safe_f = json.dumps(f)
1134
1209
  checked = "checked" if s["enabled"] else ""
1135
- toggle_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
1136
- del_call = f"if(confirm('Delete?')){{window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
1210
+ toggle_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
1211
+ del_call = f"if(confirm('Delete?')){{if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
1137
1212
  items.append(
1138
1213
  f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
1139
1214
  f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
@@ -1142,7 +1217,7 @@ window.writeToTerm = b => {{
1142
1217
  f"</div>"
1143
1218
  )
1144
1219
  rows = "".join(items)
1145
- 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>"
1220
+ 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)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>"
1146
1221
 
1147
1222
  # ============================================================================
1148
1223
  # WEBKIT SUBCLASS & IMAGE CONTEXT MENU
@@ -1234,22 +1309,22 @@ class ChromeOmniboxField(Cocoa.NSTextField):
1234
1309
  def becomeFirstResponder(self):
1235
1310
  if self.superview():
1236
1311
  self.superview().setNeedsDisplay_(True)
1237
- self.setStringValue_(self.raw_url)
1312
+ # Bug 1 Fix: Ensure string value is never None and eliminate infinite loop selectText
1313
+ self.setStringValue_(self.raw_url or "")
1238
1314
  self.setTextColor_(Cocoa.NSColor.labelColor())
1239
- self.performSelector_withObject_afterDelay_("selectText:", self, 0.0)
1240
1315
  return objc.super(ChromeOmniboxField, self).becomeFirstResponder()
1241
1316
 
1242
1317
  def resignFirstResponder(self):
1243
1318
  if self.superview():
1244
1319
  self.superview().setNeedsDisplay_(True)
1245
- self.setStringValue_(self.raw_url or "")
1320
+ # Bug 1 Fix: Do not wipe input text on resign so enter and navigation work properly
1246
1321
  return objc.super(ChromeOmniboxField, self).resignFirstResponder()
1247
1322
 
1248
1323
  def set_url(self, url):
1249
- self.raw_url = url
1324
+ self.raw_url = url or ""
1250
1325
  fr = self.window().firstResponder() if self.window() else None
1251
1326
  if fr != self and fr != self.currentEditor():
1252
- self.setStringValue_(url or "")
1327
+ self.setStringValue_(self.raw_url)
1253
1328
 
1254
1329
  class OmniboxPopupPanel(Cocoa.NSPanel):
1255
1330
  def canBecomeKeyWindow(self): return False
@@ -1270,6 +1345,7 @@ class OmniboxSuggestionView(Cocoa.NSView):
1270
1345
  opts = Cocoa.NSTrackingMouseMoved | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
1271
1346
  self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
1272
1347
  self.addTrackingArea_(self.tracking_area)
1348
+ # Bug 1 Fix: Use correct class in objc.super to avoid fatal TypeError
1273
1349
  objc.super(OmniboxSuggestionView, self).updateTrackingAreas()
1274
1350
 
1275
1351
  def mouseMoved_(self, event):
@@ -1298,7 +1374,9 @@ class OmniboxSuggestionView(Cocoa.NSView):
1298
1374
 
1299
1375
  icon = "🔍" if sugg["type"] == "search" else ("🕒" if sugg["type"] == "history" else ("🔖" if sugg["type"] == "bookmark" else ("🧮" if sugg["type"] == "math" else "⚡")))
1300
1376
  attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: text_color}
1301
- Cocoa.NSAttributedString.alloc().initWithString_attributes_(f"{icon} {sugg['text']}", attrs).drawAtPoint_(Cocoa.NSMakePoint(12, rect.origin.y + 6))
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))
1302
1380
 
1303
1381
  class OmniboxPanelCoordinator:
1304
1382
  def __init__(self, window_controller):
@@ -1333,14 +1411,15 @@ class OmniboxPanelCoordinator:
1333
1411
  bms = json.load(f)
1334
1412
  for b in bms:
1335
1413
  if isinstance(b, dict):
1336
- t, u = b.get("title", ""), b.get("url", "")
1414
+ t, u = b.get("title") or "", b.get("url") or ""
1337
1415
  if query_str.lower() in t.lower() or query_str.lower() in u.lower():
1338
1416
  results.append({"type": "bookmark", "text": t or u, "url": u})
1339
1417
  except Exception:
1340
1418
  pass
1341
1419
 
1420
+ # Bug 1 Fix: Safely fallback title for history autocomplete
1342
1421
  for h in query_history(query_str)[:4]:
1343
- 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]})
1344
1423
 
1345
1424
  def fetch_remote():
1346
1425
  try:
@@ -1411,7 +1490,7 @@ class OmniboxPanelCoordinator:
1411
1490
  pb.setString_forType_(val, Cocoa.NSPasteboardTypeString)
1412
1491
  self.hide()
1413
1492
  return
1414
- target = sugg.get("url") or sugg.get("text")
1493
+ target = sugg.get("url") or sugg.get("text") or ""
1415
1494
  self.wc.omnibox.setStringValue_(target)
1416
1495
  editor = self.wc.omnibox.currentEditor()
1417
1496
  if editor:
@@ -1426,16 +1505,33 @@ class OmniboxPanelCoordinator:
1426
1505
  self.suggestion_view.selected_index = min(len(self.suggestion_view.suggestions)-1, self.suggestion_view.selected_index + 1)
1427
1506
  if self.suggestion_view.selected_index >= 0:
1428
1507
  s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
1429
- self.wc.omnibox.setStringValue_(s.get("url") or s.get("text"))
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)
1430
1515
  self.suggestion_view.setNeedsDisplay_(True)
1431
1516
  return True
1432
1517
  elif sel == "moveUp:":
1433
1518
  self.suggestion_view.selected_index = max(-1, self.suggestion_view.selected_index - 1)
1434
1519
  if self.suggestion_view.selected_index >= 0:
1435
1520
  s = self.suggestion_view.suggestions[self.suggestion_view.selected_index]
1436
- self.wc.omnibox.setStringValue_(s.get("url") or s.get("text"))
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)
1437
1528
  else:
1438
- self.wc.omnibox.setStringValue_(self.typed_query)
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)
1439
1535
  self.suggestion_view.setNeedsDisplay_(True)
1440
1536
  return True
1441
1537
  elif sel == "insertNewline:":
@@ -1514,6 +1610,7 @@ class FindBarView(Cocoa.NSView):
1514
1610
 
1515
1611
  def onClose_(self, sender):
1516
1612
  self.setHidden_(True)
1613
+ # Bug 22 Fix: Refocus webview when closing find bar
1517
1614
  wv = self.controller.get_active_webview()
1518
1615
  if wv:
1519
1616
  self.controller.window().makeFirstResponder_(wv)
@@ -1521,6 +1618,11 @@ class FindBarView(Cocoa.NSView):
1521
1618
  # ============================================================================
1522
1619
  # QUICK SWITCHER & MEDIA CONTROLS (PIP)
1523
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
+
1524
1626
  class QuickSwitcherListView(Cocoa.NSView):
1525
1627
  def initWithFrame_controller_(self, frame, controller):
1526
1628
  self = objc.super(QuickSwitcherListView, self).initWithFrame_(frame)
@@ -1530,6 +1632,19 @@ class QuickSwitcherListView(Cocoa.NSView):
1530
1632
 
1531
1633
  def isFlipped(self): return True
1532
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
+
1533
1648
  def drawRect_(self, dirtyRect):
1534
1649
  for i, tab in enumerate(self.controller.filtered_tabs):
1535
1650
  rect = Cocoa.NSMakeRect(0, i * 36, self.bounds().size.width, 36)
@@ -1540,15 +1655,19 @@ class QuickSwitcherListView(Cocoa.NSView):
1540
1655
  else:
1541
1656
  tc, sub_color = Cocoa.NSColor.labelColor(), Cocoa.NSColor.secondaryLabelColor()
1542
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]
1543
1661
  attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.boldSystemFontOfSize_(13), Cocoa.NSForegroundColorAttributeName: tc}
1544
- Cocoa.NSAttributedString.alloc().initWithString_attributes_(tab["title"][:60], attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 3))
1662
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(title_text, attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 3))
1545
1663
  sub_attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: sub_color}
1546
- Cocoa.NSAttributedString.alloc().initWithString_attributes_(tab["url"][:80], sub_attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 19))
1664
+ Cocoa.NSAttributedString.alloc().initWithString_attributes_(url_text, sub_attrs).drawAtPoint_(Cocoa.NSMakePoint(20, rect.origin.y + 19))
1547
1665
 
1548
1666
  class QuickSwitcherController(Cocoa.NSWindowController):
1549
1667
  def init(self):
1550
1668
  w, h = 600, 400
1551
- window = Cocoa.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
1669
+ # Bug 15 Fix: Use QuickSwitcherPanel so window can receive keyboard events
1670
+ window = QuickSwitcherPanel.alloc().initWithContentRect_styleMask_backing_defer_(
1552
1671
  Cocoa.NSMakeRect(0, 0, w, h), Cocoa.NSWindowStyleMaskBorderless | Cocoa.NSWindowStyleMaskFullSizeContentView, Cocoa.NSBackingStoreBuffered, False
1553
1672
  )
1554
1673
  window.setOpaque_(False)
@@ -1591,8 +1710,8 @@ class QuickSwitcherController(Cocoa.NSWindowController):
1591
1710
  for t in wc.tabs:
1592
1711
  self.tabs.append({
1593
1712
  "id": t.get("id"),
1594
- "title": t.get("title", "Tab"),
1595
- "url": t.get("url", ""),
1713
+ "title": t.get("title") or "Tab",
1714
+ "url": t.get("url") or "",
1596
1715
  "wc": wc
1597
1716
  })
1598
1717
  self.filtered_tabs = self.tabs.copy()
@@ -1612,7 +1731,8 @@ class QuickSwitcherController(Cocoa.NSWindowController):
1612
1731
  def control_textView_doCommandBySelector_(self, control, textView, commandSelector):
1613
1732
  sel = str(commandSelector)
1614
1733
  if sel == "moveDown:":
1615
- self.selected_index = min(len(self.filtered_tabs) - 1, self.selected_index + 1)
1734
+ if self.filtered_tabs:
1735
+ self.selected_index = min(len(self.filtered_tabs) - 1, self.selected_index + 1)
1616
1736
  self.list_view.setNeedsDisplay_(True)
1617
1737
  return True
1618
1738
  elif sel == "moveUp:":
@@ -1644,6 +1764,7 @@ class PiPPanelController(Cocoa.NSWindowController):
1644
1764
  window.setFloatingPanel_(True)
1645
1765
  window.setLevel_(Cocoa.NSFloatingWindowLevel)
1646
1766
  window.setReleasedWhenClosed_(False)
1767
+ window.center()
1647
1768
  self = objc.super(PiPPanelController, self).initWithWindow_(window)
1648
1769
  if self:
1649
1770
  self.browser = browser_controller
@@ -1693,6 +1814,7 @@ class PiPPanelController(Cocoa.NSWindowController):
1693
1814
  # BROWSER WINDOW CONTROLLER
1694
1815
  # ============================================================================
1695
1816
  TITLE_BAR_H = 30
1817
+ SCRIPT_HANDLER_NAMES = ("bridge", "audio", "terminalInput", "terminalResize")
1696
1818
 
1697
1819
  class BrowserWindowController(Cocoa.NSWindowController):
1698
1820
  def init(self):
@@ -1939,6 +2061,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
1939
2061
  wv.setFrame_(self.web_container.bounds())
1940
2062
  self.relayout_tabs()
1941
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
+
1942
2074
  def relayout_tabs(self):
1943
2075
  for sub in list(self.tab_strip.subviews()):
1944
2076
  sub.removeFromSuperview()
@@ -1948,7 +2080,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
1948
2080
  tab_h = 28 if not is_vert else 32
1949
2081
  y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
1950
2082
 
1951
- vis = [t for t in self.tabs if not t.get("group_id") or not self.get_group(t["group_id"]).collapsed]
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)]
1952
2085
  pinned_cnt = sum(1 for t in vis if t.get("pinned", False))
1953
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
1954
2087
 
@@ -2025,10 +2158,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
2025
2158
 
2026
2159
  config.setURLSchemeHandler_forURLScheme_(InternalSchemeHandler.alloc().init(), "browser")
2027
2160
  uc = WebKit.WKUserContentController.alloc().init()
2028
- uc.addScriptMessageHandler_name_(self, "bridge")
2029
- uc.addScriptMessageHandler_name_(self, "audio")
2030
- uc.addScriptMessageHandler_name_(self, "terminalInput")
2031
- uc.addScriptMessageHandler_name_(self, "terminalResize")
2161
+ for h_name in SCRIPT_HANDLER_NAMES:
2162
+ uc.addScriptMessageHandler_name_(self, h_name)
2032
2163
 
2033
2164
  # Injected scripts: Command+click capture, audio monitoring, context menus, and fullscreen handlers
2034
2165
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(COMMAND_CLICK_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
@@ -2057,7 +2188,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
2057
2188
  reg = m.replace("*", ".*").replace("/", escaped_slash)
2058
2189
  conds.append(f"new RegExp('^{reg}$').test(location.href)")
2059
2190
  cond_str = " || ".join(conds) if conds else "true"
2060
- wrapped = f"if ({cond_str}) {{\n" + script["code"] + "\n}}"
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}}"
2061
2193
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(wrapped, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
2062
2194
 
2063
2195
  if WK_RULE_LIST and SETTINGS.settings.get("adblock_enabled", True):
@@ -2150,6 +2282,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
2150
2282
  wv.removeObserver_forKeyPath_(self, k)
2151
2283
  except Exception:
2152
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
2153
2292
  if wv.superview():
2154
2293
  wv.removeFromSuperview()
2155
2294
  if not self.tabs:
@@ -2375,6 +2514,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
2375
2514
  def handle_escape_key(self):
2376
2515
  if self.find_bar and not self.find_bar.isHidden():
2377
2516
  self.find_bar.setHidden_(True)
2517
+ wv = self.get_active_webview()
2518
+ if wv:
2519
+ self.window().makeFirstResponder_(wv)
2378
2520
  return
2379
2521
  if self.is_active_tab_terminal():
2380
2522
  return
@@ -2442,7 +2584,11 @@ class BrowserWindowController(Cocoa.NSWindowController):
2442
2584
  inc_item = menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
2443
2585
  inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
2444
2586
  inc_item.setTarget_(Cocoa.NSApp.delegate())
2445
- menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "openTerminal:", "T").setTarget_(self)
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
+
2446
2592
  menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2447
2593
  menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
2448
2594
  bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
@@ -2536,9 +2682,12 @@ class BrowserWindowController(Cocoa.NSWindowController):
2536
2682
  if not wv: return
2537
2683
  url = wv.URL().absoluteString() if wv.URL() else "browser://newtab"
2538
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
2539
2688
  menu = Cocoa.NSMenu.alloc().init()
2540
- menu.addItemWithTitle_action_keyEquivalent_(f"Host: {parsed.host() or 'Internal Page'}", "", "").setEnabled_(False)
2541
- menu.addItemWithTitle_action_keyEquivalent_("Connection: Secure (HTTPS)" if parsed.scheme() == "https" else "Connection: Insecure / Local", "", "").setEnabled_(False)
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)
2542
2691
  menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2543
2692
  menu.addItemWithTitle_action_keyEquivalent_("Copy Current URL", "onCopyUrl:", "").setTarget_(self)
2544
2693
  menu.popUpMenuPositioningItem_atLocation_inView_(None, Cocoa.NSMakePoint(0, 0), sender)
@@ -2583,7 +2732,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
2583
2732
  wv = self.get_active_webview()
2584
2733
  if wv:
2585
2734
  wv.loadRequest_(Foundation.NSURLRequest.requestWithURL_(Foundation.NSURL.URLWithString_(val)))
2586
- self.window().makeFirstResponder_(self.web_container)
2735
+ # Bug 11 Fix: Transfer keyboard focus directly to active webview
2736
+ self.window().makeFirstResponder_(wv)
2587
2737
 
2588
2738
  def controlTextDidChange_(self, notif):
2589
2739
  if notif.object() == self.omnibox:
@@ -2685,10 +2835,14 @@ class BrowserWindowController(Cocoa.NSWindowController):
2685
2835
  return
2686
2836
  elif name == "terminalResize":
2687
2837
  b = message.body()
2688
- TERMINAL_MANAGER.get_or_create(message.webView(), b.get("cols"), b.get("rows"))
2838
+ TERMINAL_MANAGER.get_or_create(
2839
+ message.webView(),
2840
+ b.get("cols") if hasattr(b, "get") else None,
2841
+ b.get("rows") if hasattr(b, "get") else None
2842
+ )
2689
2843
  return
2690
2844
  elif name == "audio":
2691
- b = message.body() if isinstance(message.body(), dict) else {}
2845
+ b = message.body() if hasattr(message.body(), "get") else {}
2692
2846
  has_a = (b.get("cmd") == "audio_play")
2693
2847
  for t in self.tabs:
2694
2848
  if t.get("webview") == message.webView():
@@ -2698,7 +2852,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
2698
2852
  return
2699
2853
 
2700
2854
  body = message.body()
2701
- cmd = body.get("cmd") if isinstance(body, dict) else None
2855
+ cmd = None
2856
+ if hasattr(body, "get"):
2857
+ cmd = body.get("cmd")
2858
+ elif isinstance(body, dict):
2859
+ cmd = body.get("cmd")
2860
+ if cmd is not None:
2861
+ cmd = str(cmd)
2702
2862
 
2703
2863
  if cmd == "open_url":
2704
2864
  target_url = body.get("url")
@@ -2707,6 +2867,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2707
2867
  elif cmd == "toggle_fullscreen_terminal":
2708
2868
  self.toggle_tab_bar()
2709
2869
  elif cmd == "context_target_image":
2870
+ # Bug 9 Fix: Store or clear target image URL
2710
2871
  self.last_context_image_url = body.get("src")
2711
2872
  elif cmd == "blob_image_data":
2712
2873
  data_url, dest_path = body.get("data"), body.get("path")
@@ -2740,17 +2901,29 @@ class BrowserWindowController(Cocoa.NSWindowController):
2740
2901
  elif cmd == "prompt_save_password":
2741
2902
  self.show_password_banner(body.get("host", ""), body.get("username", ""), body.get("password", ""))
2742
2903
  elif cmd == "check_autofill":
2904
+ # Bug 18 & 19 Fix: Apply credentials specifically to requesting webview using safe JSON encoding
2743
2905
  host = body.get("host")
2744
2906
  saved_user = SETTINGS.settings.get("saved_logins", {}).get(host)
2745
2907
  if saved_user:
2746
2908
  pwd = get_from_keychain(host, saved_user)
2747
2909
  if pwd:
2748
- wv = self.get_active_webview()
2749
- if wv:
2750
- 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)
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)
2751
2921
  elif cmd == "save_setting":
2752
- key, val = body.get("key"), body.get("val")
2753
- val = sanitize_for_json(val)
2922
+ raw_key, raw_val = body.get("key"), body.get("val")
2923
+ if raw_key is None:
2924
+ return
2925
+ key = str(raw_key)
2926
+ val = sanitize_for_json(raw_val)
2754
2927
  if key == "download_path":
2755
2928
  val = os.path.expanduser(str(val).strip())
2756
2929
  SETTINGS.settings[key] = val
@@ -2768,12 +2941,15 @@ class BrowserWindowController(Cocoa.NSWindowController):
2768
2941
  for sub in c.tab_strip.subviews():
2769
2942
  sub.setNeedsDisplay_(True)
2770
2943
  elif key in ("default_zoom",):
2771
- new_zoom = float(val)
2772
- for t in self.tabs:
2773
- t["zoom"] = new_zoom
2774
- twv = t.get("webview")
2775
- if twv:
2776
- self.apply_zoom_to_webview(twv, t)
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
2777
2953
  elif key in ("user_agent_mode", "custom_user_agent"):
2778
2954
  ua = SETTINGS.get_user_agent()
2779
2955
  for t in self.tabs:
@@ -2809,7 +2985,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2809
2985
 
2810
2986
  res = panel.runModal()
2811
2987
  if res == Cocoa.NSModalResponseOK and panel.URLs():
2812
- chosen_path = panel.URLs()[0].path()
2988
+ chosen_path = str(panel.URLs()[0].path())
2813
2989
  if os.access(chosen_path, os.W_OK):
2814
2990
  SETTINGS.settings["download_path"] = chosen_path
2815
2991
  SETTINGS.save()
@@ -2839,15 +3015,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
2839
3015
  dispatch_to_main(on_done)
2840
3016
  threading.Thread(target=fetch_task, daemon=True).start()
2841
3017
  elif cmd == "clear_browsing_data":
2842
- targets = body.get("targets", [])
3018
+ raw_targets = body.get("targets", [])
3019
+ targets = list(raw_targets) if hasattr(raw_targets, "__iter__") else []
2843
3020
  if "history" in targets:
2844
3021
  clear_history()
2845
3022
  if "favicons" in targets:
2846
3023
  FaviconCache.clear()
2847
3024
  self.bookmarks_bar.reload_bookmarks()
2848
3025
  if "dns_cache" in targets:
2849
- with DNS_LOCK:
2850
- DNS_CACHE.clear()
3026
+ # Bug 12 Fix: Completely wipe in-memory and disk DNS cache
3027
+ clear_dns_cache_and_disk()
2851
3028
  if "cookies_cache" in targets:
2852
3029
  ds = WebKit.WKWebsiteDataStore.defaultDataStore()
2853
3030
  ds.removeDataOfTypes_modifiedSince_completionHandler_(WebKit.WKWebsiteDataStore.allWebsiteDataTypes(), Foundation.NSDate.dateWithTimeIntervalSince1970_(0), lambda: None)
@@ -3025,6 +3202,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
3025
3202
  if not t.get("is_sleeping"):
3026
3203
  t["title"] = str(obj.title())
3027
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()))
3028
3208
  self.relayout_tabs()
3029
3209
  elif keyPath == "estimatedProgress":
3030
3210
  self.update_progress_for_webview(obj)
@@ -3067,6 +3247,14 @@ class BrowserWindowController(Cocoa.NSWindowController):
3067
3247
  self.omnibox_panel.hide()
3068
3248
 
3069
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
+
3070
3258
  self.save_session()
3071
3259
  for t in self.tabs:
3072
3260
  wv = t.get("webview")
@@ -3077,6 +3265,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
3077
3265
  wv.removeObserver_forKeyPath_(self, k)
3078
3266
  except Exception:
3079
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
3080
3275
  app_delegate = Cocoa.NSApp.delegate()
3081
3276
  if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
3082
3277
  app_delegate.controllers.remove(self)
@@ -3149,12 +3344,14 @@ class AppDelegate(Cocoa.NSObject):
3149
3344
  # Allow Escape to pass directly into terminal tabs (vim, nano, fzf, less)
3150
3345
  if wc.is_active_tab_terminal():
3151
3346
  return event
3347
+ # Bug 22 Fix: Refocus webview when find_bar is closed via Escape
3152
3348
  if wc.find_bar and not wc.find_bar.isHidden():
3153
3349
  wc.find_bar.setHidden_(True)
3350
+ wv = wc.get_active_webview()
3351
+ if wv:
3352
+ wc.window().makeFirstResponder_(wv)
3154
3353
  return None
3155
- if not wc.tab_bar_visible:
3156
- wc.toggle_tab_bar()
3157
- return None
3354
+ # Bug 21 Fix: Delegate Escape to handle_escape_key() so HTML5 fullscreen exit is not bypassed
3158
3355
  wc.handle_escape_key()
3159
3356
  return event
3160
3357
 
@@ -3188,11 +3385,24 @@ class AppDelegate(Cocoa.NSObject):
3188
3385
  wc = BrowserWindowController.alloc().initWithIncognito_(True)
3189
3386
  self.controllers.append(wc)
3190
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
+
3191
3397
  def onNewTerminal_(self, sender):
3192
3398
  win = Cocoa.NSApp.keyWindow()
3193
3399
  wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else (self.controllers[0] if self.controllers else None)
3194
3400
  if wc:
3195
3401
  wc.add_tab("browser://terminal")
3402
+ else:
3403
+ self.onNewWindow_(None)
3404
+ if self.controllers:
3405
+ self.controllers[-1].add_tab("browser://terminal")
3196
3406
 
3197
3407
  def onCloseTab_(self, sender=None):
3198
3408
  win = Cocoa.NSApp.keyWindow()
@@ -3223,7 +3433,12 @@ class AppDelegate(Cocoa.NSObject):
3223
3433
  file_item = Cocoa.NSMenuItem.alloc().init()
3224
3434
  file_menu = Cocoa.NSMenu.alloc().initWithTitle_("File")
3225
3435
  file_menu.addItemWithTitle_action_keyEquivalent_("New Tab", "onNewTab:", "t")
3226
- file_menu.addItemWithTitle_action_keyEquivalent_("New Terminal Tab", "onNewTerminal:", "T").setTarget_(self)
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
+
3227
3442
  file_menu.addItemWithTitle_action_keyEquivalent_("New Window", "onNewWindow:", "n").setTarget_(self)
3228
3443
  inc_item = file_menu.addItemWithTitle_action_keyEquivalent_("New Incognito Window", "onNewIncognitoWindow:", "N")
3229
3444
  inc_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagCommand | Cocoa.NSEventModifierFlagShift)
@@ -3282,7 +3497,8 @@ class AppDelegate(Cocoa.NSObject):
3282
3497
  for i in range(1, 10):
3283
3498
  win_menu.addItemWithTitle_action_keyEquivalent_(f"Tab {i}", f"onSwitchTab{i}:", str(i))
3284
3499
  win_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3285
- 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)
3286
3502
  win_menu.addItemWithTitle_action_keyEquivalent_("Task Manager", "showTaskManager:", "").setTarget_(self)
3287
3503
  win_item.setSubmenu_(win_menu)
3288
3504
  main_menu.addItem_(win_item)
pybrowser/doh_server.py CHANGED
@@ -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
- DNS_CACHE = json.load(f)
98
+ loaded = json.load(f)
99
+ with DNS_LOCK:
100
+ DNS_CACHE = loaded if isinstance(loaded, dict) else {}
96
101
  except Exception:
97
- DNS_CACHE = {}
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
- class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
286
- allow_reuse_address = True
287
- daemon_threads = True
288
-
289
- def server_bind(self):
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
- try:
298
- server = ReusableTCPServer(("127.0.0.1", LOCAL_PROXY_PORT), DoHProxyHandler)
299
- threading.Thread(target=server.serve_forever, daemon=True).start()
300
- threading.Thread(target=dns_flush_worker, daemon=True).start()
301
- except Exception as e:
302
- print(f"[Proxy Warning] Could not start local proxy server: {e}")
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:
pybrowser/incognito.py CHANGED
@@ -33,4 +33,4 @@ def apply_incognito_preferences(config, shared_data_store=None):
33
33
  config.preferences().setElementFullscreenEnabled_(True)
34
34
  except Exception:
35
35
  pass
36
- return config
36
+ return config
pybrowser/settings.py CHANGED
@@ -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",
@@ -23,7 +24,7 @@ DEFAULT_URL = "https://www.google.com"
23
24
  DATA_DIR = os.path.expanduser("~/data")
24
25
  os.makedirs(DATA_DIR, exist_ok=True)
25
26
 
26
- _cur_dir = os.path.dirname(os.path.abspath(__file__))
27
+ _cur_dir = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
27
28
  _old_candidates = [
28
29
  os.path.abspath(os.path.join(_cur_dir, "..", "browser_data")),
29
30
  os.path.abspath(os.path.join(os.getcwd(), "browser_data")),
@@ -87,29 +88,40 @@ def sanitize_for_json(obj):
87
88
  """
88
89
  if obj is None:
89
90
  return None
90
- # Check booleans explicitly first because bool subclasses int in Python
91
+
91
92
  type_name = type(obj).__name__
92
- if isinstance(obj, bool) or "Boolean" in type_name or type_name == "bool":
93
+
94
+ # PyObjC booleans (__NSCFBoolean) must use boolValue() to accurately preserve False/True
95
+ if hasattr(obj, "boolValue") and ("Boolean" in type_name or "bool" in type_name.lower()):
96
+ return bool(obj.boolValue())
97
+
98
+ # Standard Python booleans
99
+ if isinstance(obj, bool) or type_name == "bool":
93
100
  return bool(obj)
101
+
102
+ # Number conversion for PyObjC NSNumber (e.g. float/double or int)
103
+ if hasattr(obj, "doubleValue") and not isinstance(obj, (int, float, str, bytes)):
104
+ val = obj.doubleValue()
105
+ return int(val) if val.is_integer() else val
106
+
94
107
  if isinstance(obj, (int, float)):
95
108
  return obj
109
+
96
110
  if isinstance(obj, (str, bytes)):
97
111
  return str(obj)
112
+
98
113
  if hasattr(obj, "items") or isinstance(obj, dict):
99
114
  return {str(k): sanitize_for_json(v) for k, v in obj.items()}
115
+
100
116
  if hasattr(obj, "__iter__") or isinstance(obj, (list, tuple, set)):
101
117
  return [sanitize_for_json(i) for i in obj]
118
+
102
119
  try:
103
- # Number conversion for PyObjC NSNumber
104
- if hasattr(obj, "boolValue") and ("Boolean" in type_name or "bool" in str(type(obj)).lower()):
105
- return bool(obj.boolValue())
106
- if hasattr(obj, "doubleValue"):
107
- val = obj.doubleValue()
108
- return int(val) if val.is_integer() else val
109
120
  if hasattr(obj, "stringValue"):
110
121
  return str(obj.stringValue())
111
122
  except Exception:
112
123
  pass
124
+
113
125
  return str(obj)
114
126
 
115
127
  def validate_and_get_download_dir(target_path=None):
@@ -177,7 +189,7 @@ class SettingsManager:
177
189
  "saved_logins": {},
178
190
  "history_enabled": True
179
191
  }
180
- self.settings = dict(self.defaults)
192
+ self.settings = copy.deepcopy(self.defaults)
181
193
  self.load()
182
194
 
183
195
  def load(self):
@@ -209,7 +221,8 @@ class SettingsManager:
209
221
  print(f"[Settings Save Error] {e}")
210
222
 
211
223
  def reset_defaults(self):
212
- self.settings = dict(self.defaults)
224
+ # Bug 25 Fix: Deep copy to prevent mutation of defaults dictionary
225
+ self.settings = copy.deepcopy(self.defaults)
213
226
  self.save()
214
227
 
215
228
  def get_search_url(self, query):
@@ -635,7 +648,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
635
648
  window._toastTimer = setTimeout(() => t.classList.remove('show'), 2400);
636
649
  }}
637
650
  function saveSetting(key, val, msg) {{
638
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
651
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
652
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
653
+ }}
639
654
  showToast(msg);
640
655
  }}
641
656
  function onDownloadInputChange(val) {{
@@ -662,12 +677,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
662
677
  document.getElementById('custom-startup-row').style.display = (val === 'custom_url') ? 'flex' : 'none';
663
678
  }}
664
679
  function browseDownloadFolder() {{
665
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'choose_download_directory' }});
680
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
681
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'choose_download_directory' }});
682
+ }}
666
683
  }}
667
684
  function updateAdblockFilters() {{
668
685
  const b = document.getElementById('update-filters-btn');
669
686
  b.innerText = "Syncing Lists..."; b.disabled = true;
670
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'update_adblock_filters' }});
687
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
688
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'update_adblock_filters' }});
689
+ }}
671
690
  }}
672
691
  function searchDomain() {{
673
692
  const q = document.getElementById('inspector-query').value.trim();
@@ -675,11 +694,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
675
694
  const box = document.getElementById('inspector-results');
676
695
  box.style.display = 'block';
677
696
  box.innerHTML = 'Searching blocklists...';
678
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
697
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
698
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
699
+ }}
679
700
  }}
680
- window.renderBlockedResults = function(jsonStr) {{
681
- let items = [];
682
- try {{ items = JSON.parse(jsonStr); }} catch(e) {{ items = []; }}
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
+ }}
683
707
  const box = document.getElementById('inspector-results');
684
708
  if (!items || items.length === 0) {{
685
709
  box.innerHTML = '<span style="color:#D93025;">No matches found. Domain is NOT currently blocked.</span>';
@@ -687,7 +711,7 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
687
711
  }}
688
712
  let out = '<div style="margin-bottom:6px;font-weight:600;color:var(--primary);">' + items.length + ' Matches Found:</div>';
689
713
  items.forEach(it => {{
690
- 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>';
691
715
  }});
692
716
  box.innerHTML = out;
693
717
  }};
@@ -698,12 +722,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
698
722
  if (document.getElementById('clr-favicons').checked) targets.push('favicons');
699
723
  if (document.getElementById('clr-dns').checked) targets.push('dns_cache');
700
724
  if (targets.length === 0) {{ showToast('Select at least one category'); return; }}
701
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'clear_browsing_data', targets: targets }});
725
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
726
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'clear_browsing_data', targets: targets }});
727
+ }}
702
728
  showToast('Browsing data cleared');
703
729
  }}
704
730
  function executeResetDefaults() {{
705
731
  if (confirm("Restore all browser settings to factory defaults?")) {{
706
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'reset_defaults' }});
732
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
733
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'reset_defaults' }});
734
+ }}
707
735
  }}
708
736
  }}
709
737
  function jumpTo(id, navBtn) {{
pybrowser/task_manager.py CHANGED
@@ -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 DNS_CACHE, DNS_LOCK
118
- with DNS_LOCK:
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
- pass
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()
pybrowser/terminal.py CHANGED
@@ -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
- bin_dir = os.path.join(get_data_dir(), "bin")
226
- ensure_doh_cli_helper(bin_dir, port)
227
- cur_path = env.get("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
228
- if bin_dir not in cur_path:
229
- env["PATH"] = f"{bin_dir}:{cur_path}"
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
- self.lock = threading.Lock()
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stupidpythonbrowser
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Native macOS Chromium/Material 3 Web Browser in PyObjC & WebKit
5
5
  Author: avevall
6
6
  Classifier: Environment :: MacOS X :: Cocoa
@@ -0,0 +1,14 @@
1
+ pybrowser/__init__.py,sha256=bgYpxNkjslEJjbQpWEwcro_Kc4dbmud1g31hd8J1TN0,218
2
+ pybrowser/browser.py,sha256=1zU9Joa-qcB7nrTYbPoBYJTidqT7fETHun8UzOX92Bc,170052
3
+ pybrowser/doh_server.py,sha256=MqmKNzYL3xqhVA1oY7B7DoiA6rEb9-Vxs343305HCog,16109
4
+ pybrowser/incognito.py,sha256=qg8b0Ie6-4sr9R7RFJ-bTn52f-rRQBJQcA77oF9Mmks,1676
5
+ pybrowser/main.py,sha256=DwtxNLZ0PrhH0RPUn3UzPFSHQgHcnO7AHriElxxOP2A,2332
6
+ pybrowser/settings.py,sha256=UrD88IMKt9goxFIalRQBFGMzM5TBO7dBSKd_S4Q_oTY,42357
7
+ pybrowser/task_manager.py,sha256=rNt_eCrGoi5OYgYDRg-jv-u326bqQLN0wSJnrGuLwPA,15252
8
+ pybrowser/terminal.py,sha256=8RChiArtp76OG6n3rbY8U81kU3V_Gybkl9hxq-nRd84,15285
9
+ stupidpythonbrowser-1.0.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
+ stupidpythonbrowser-1.0.4.dist-info/METADATA,sha256=GPXR_DEI9jFFi1ue6gwH30dExMlBFzjZQfiLqD92zZM,977
11
+ stupidpythonbrowser-1.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ stupidpythonbrowser-1.0.4.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
13
+ stupidpythonbrowser-1.0.4.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
14
+ stupidpythonbrowser-1.0.4.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- pybrowser/__init__.py,sha256=GVtXkxt_j9dwHYFRqGEWp4HzMX2VTp6HQAPyt8lqYAY,218
2
- pybrowser/browser.py,sha256=MhhhLRxGZjUiVg-BAaGBwAZas6tK2f-QZI2WLxvt81s,159687
3
- pybrowser/doh_server.py,sha256=uLlyu5wvn0cXSr2aDKa2EjOlXN4Mhzz81WM53jSA0s4,15253
4
- pybrowser/incognito.py,sha256=GEc-xeVkElsDPzFIVLhTgSXtDHK3whTzxdnz7wYVJ0o,1675
5
- pybrowser/main.py,sha256=DwtxNLZ0PrhH0RPUn3UzPFSHQgHcnO7AHriElxxOP2A,2332
6
- pybrowser/settings.py,sha256=1b1iaZ-DJImRVzDtQH8QFYjiBBDZmPqMnrOr8sG3L5E,41304
7
- pybrowser/task_manager.py,sha256=DRTazfw6MAqM360DkiMGIC0oZq6XuMBCpkMgHZAJ-II,15079
8
- pybrowser/terminal.py,sha256=vdu64F3DRf_3KlU_BVzfllPUhb6dGRFjhVqoVr5Xetw,15159
9
- stupidpythonbrowser-1.0.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
- stupidpythonbrowser-1.0.2.dist-info/METADATA,sha256=xOLz3ZmY8CofHyqRAbgEFhxoDa54sDzBHp4wKrJWoJc,977
11
- stupidpythonbrowser-1.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
- stupidpythonbrowser-1.0.2.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
13
- stupidpythonbrowser-1.0.2.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
14
- stupidpythonbrowser-1.0.2.dist-info/RECORD,,