stupidpythonbrowser 1.0.3__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.3"
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,
@@ -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 (imgSrc && window.webkit?.messageHandlers?.bridge) {
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
- document.addEventListener('submit', (e) => {
498
- if (!e.target || !e.target.querySelector) return;
499
- let pw = e.target.querySelector('input[type="password"]');
500
- let user = e.target.querySelector('input[type="text"], input[type="email"]');
501
- if (pw && pw.value && window.webkit?.messageHandlers?.bridge) {
502
- window.webkit.messageHandlers.bridge.postMessage({
503
- cmd: 'prompt_save_password', host: location.hostname,
504
- username: user ? user.value : 'unknown', password: pw.value
505
- });
506
- }
507
- }, true);
508
- window.addEventListener('load', () => {
509
- if (location.hostname && window.webkit?.messageHandlers?.bridge) {
510
- window.webkit.messageHandlers.bridge.postMessage({cmd: 'check_autofill', host: location.hostname});
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
- 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))
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 (DEDICATED TOP BAR FOR DRAGGING, EXPANDING & ZEN TRAFFIC LIGHTS)
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", "New Tab")
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
- 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]
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
- t = b.get("title", u)
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='this.style.display=\"none\"'>" if host else ""
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='{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>")
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
- 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>")
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:'${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>"
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
- 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 "")
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
- self.setStringValue_(self.raw_url or "")
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_(url or "")
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
- objc.super(ChromeTabView, self).updateTrackingAreas() if hasattr(objc.super(ChromeTabView, self), 'updateTrackingAreas') else None
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
- 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))
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", ""), b.get("url", "")
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
- 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)
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
- 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)
1445
1528
  else:
1446
- 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)
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_(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))
1553
1663
  sub_attrs = {Cocoa.NSFontAttributeName: Cocoa.NSFont.systemFontOfSize_(11), Cocoa.NSForegroundColorAttributeName: sub_color}
1554
- 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))
1555
1665
 
1556
1666
  class QuickSwitcherController(Cocoa.NSWindowController):
1557
1667
  def init(self):
1558
1668
  w, h = 600, 400
1559
- 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_(
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", "Tab"),
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
- 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)
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
- 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)]
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
- uc.addScriptMessageHandler_name_(self, "bridge")
2037
- uc.addScriptMessageHandler_name_(self, "audio")
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
- 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}}"
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
- 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
+
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: {parsed.host() or 'Internal Page'}", "", "").setEnabled_(False)
2549
- 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)
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
- self.window().makeFirstResponder_(self.web_container)
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
- wv = self.get_active_webview()
2767
- if wv:
2768
- 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)
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
- new_zoom = float(val)
2793
- for t in self.tabs:
2794
- t["zoom"] = new_zoom
2795
- twv = t.get("webview")
2796
- if twv:
2797
- 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
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
- with DNS_LOCK:
2872
- DNS_CACHE.clear()
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
- if not wc.tab_bar_visible:
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
- 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
+
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)
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",
@@ -188,7 +189,7 @@ class SettingsManager:
188
189
  "saved_logins": {},
189
190
  "history_enabled": True
190
191
  }
191
- self.settings = dict(self.defaults)
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
- self.settings = dict(self.defaults)
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
- window.renderBlockedResults = function(jsonStr) {{
700
- let items = [];
701
- 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
+ }}
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
  }};
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.3
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=YkMBq-JvIHSHfOPXmnX-6ijq1yaWClhLlzHmzCedz3w,218
2
- pybrowser/browser.py,sha256=6gyJM0Kbu3O-pL85H_F1vBCOY8bAUcgo2ztsqOyxEX4,160861
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=EoiWTESrdWvctWZ1Z2rS6gmnSHYPJvHQXIp3H7kO0Dg,42080
7
- pybrowser/task_manager.py,sha256=DRTazfw6MAqM360DkiMGIC0oZq6XuMBCpkMgHZAJ-II,15079
8
- pybrowser/terminal.py,sha256=vdu64F3DRf_3KlU_BVzfllPUhb6dGRFjhVqoVr5Xetw,15159
9
- stupidpythonbrowser-1.0.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
- stupidpythonbrowser-1.0.3.dist-info/METADATA,sha256=5bCF5arbZzKhtF1Xcplnx7Ji5WgnNCPKj-HjFeBadWk,977
11
- stupidpythonbrowser-1.0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
- stupidpythonbrowser-1.0.3.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
13
- stupidpythonbrowser-1.0.3.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
14
- stupidpythonbrowser-1.0.3.dist-info/RECORD,,