stupidpythonbrowser 1.0.1__py3-none-any.whl → 1.0.3__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.1"
6
+ __version__ = "1.0.3"
7
7
  __all__ = [
8
8
  "browser",
9
9
  "settings",
@@ -11,4 +11,4 @@ __all__ = [
11
11
  "incognito",
12
12
  "task_manager",
13
13
  "terminal",
14
- ]
14
+ ]
pybrowser/browser.py CHANGED
@@ -35,7 +35,8 @@ from pybrowser.settings import (
35
35
  SETTINGS, UA_PRESETS, DEFAULT_URL, DATA_DIR, SESSION_FILE, BOOKMARKS_FILE,
36
36
  SETTINGS_FILE, HISTORY_FILE, ADBLOCK_CACHE_FILE, DNS_CACHE_FILE, USERSCRIPTS_DIR,
37
37
  FILTER_LIST_URL, LOCAL_PROXY_PORT, BASE_ADBLOCK_DOMAINS, M3_COLORS, GROUP_COLORS,
38
- validate_and_get_download_dir, get_settings_html, sanitize_suggested_filename
38
+ validate_and_get_download_dir, get_settings_html, sanitize_suggested_filename,
39
+ sanitize_for_json
39
40
  )
40
41
  from pybrowser.doh_server import (
41
42
  DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, BASE_ADBLOCK_DOMAINS, ADBLOCK_LOCK,
@@ -83,11 +84,14 @@ def get_m3_color(token, view, is_incognito=False):
83
84
  return hex_to_nscolor(colors[1 if is_dark else 0])
84
85
 
85
86
  def atomic_write_json(filepath, data):
87
+ """Safely writes JSON to disk with recursive PyObjC sanitization to prevent TypeErrors."""
86
88
  try:
87
89
  dir_name = os.path.dirname(filepath) or "."
90
+ os.makedirs(dir_name, exist_ok=True)
91
+ sanitized = sanitize_for_json(data)
88
92
  fd, temp_path = tempfile.mkstemp(dir=dir_name)
89
93
  with os.fdopen(fd, 'w', encoding='utf-8') as f:
90
- json.dump(data, f, indent=2)
94
+ json.dump(sanitized, f, indent=2)
91
95
  os.replace(temp_path, filepath)
92
96
  except Exception as e:
93
97
  print(f"[Atomic JSON Write Error] {e}")
@@ -398,7 +402,7 @@ class FaviconCache:
398
402
  try:
399
403
  url = f"https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32"
400
404
  req = urllib.request.Request(url, headers={"User-Agent": SETTINGS.get_user_agent()})
401
- with urllib.request.urlopen(req, timeout=2.5) as resp:
405
+ with urllib.request.urlopen(req, timeout=3.0) as resp:
402
406
  raw = resp.read()
403
407
  if raw:
404
408
  img = Cocoa.NSImage.alloc().initWithData_(Foundation.NSData.dataWithBytes_length_(raw, len(raw)))
@@ -413,14 +417,58 @@ class FaviconCache:
413
417
  # ============================================================================
414
418
  # INJECTED SCRIPTS & FULLSCREEN UTILITIES
415
419
  # ============================================================================
420
+ COMMAND_CLICK_JS = """
421
+ (function() {
422
+ function findAnchor(el) {
423
+ while (el && el !== document) {
424
+ if (el.tagName === 'A' && el.href) return el;
425
+ el = el.parentElement;
426
+ }
427
+ return null;
428
+ }
429
+ document.addEventListener('click', function(e) {
430
+ if (e.metaKey || e.ctrlKey || e.button === 1) {
431
+ const a = findAnchor(e.target);
432
+ if (a && a.href && !a.href.startsWith('javascript:')) {
433
+ e.preventDefault();
434
+ e.stopPropagation();
435
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
436
+ window.webkit.messageHandlers.bridge.postMessage({cmd: 'open_url', url: a.href});
437
+ }
438
+ }
439
+ }
440
+ }, true);
441
+ document.addEventListener('auxclick', function(e) {
442
+ if (e.button === 1) {
443
+ const a = findAnchor(e.target);
444
+ if (a && a.href && !a.href.startsWith('javascript:')) {
445
+ e.preventDefault();
446
+ e.stopPropagation();
447
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {
448
+ window.webkit.messageHandlers.bridge.postMessage({cmd: 'open_url', url: a.href});
449
+ }
450
+ }
451
+ }
452
+ }, true);
453
+ })();
454
+ """
455
+
416
456
  AUDIO_MONITOR_JS = """
417
457
  document.addEventListener('play', (e) => {
418
- if (e.target && !e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
458
+ if (e.target && !e.target.muted && window.webkit?.messageHandlers?.audio) {
459
+ window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
460
+ }
461
+ }, true);
462
+ document.addEventListener('pause', () => {
463
+ if (window.webkit?.messageHandlers?.audio) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
464
+ }, true);
465
+ document.addEventListener('ended', () => {
466
+ if (window.webkit?.messageHandlers?.audio) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
419
467
  }, true);
420
- document.addEventListener('pause', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
421
- document.addEventListener('ended', () => window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'}), true);
422
468
  document.addEventListener('volumechange', (e) => {
423
- if (e.target && e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
469
+ if (e.target && e.target.muted && window.webkit?.messageHandlers?.audio) {
470
+ window.webkit.messageHandlers.audio.postMessage({cmd:'audio_pause'});
471
+ }
424
472
  }, true);
425
473
  """
426
474
 
@@ -450,7 +498,7 @@ document.addEventListener('submit', (e) => {
450
498
  if (!e.target || !e.target.querySelector) return;
451
499
  let pw = e.target.querySelector('input[type="password"]');
452
500
  let user = e.target.querySelector('input[type="text"], input[type="email"]');
453
- if (pw && pw.value) {
501
+ if (pw && pw.value && window.webkit?.messageHandlers?.bridge) {
454
502
  window.webkit.messageHandlers.bridge.postMessage({
455
503
  cmd: 'prompt_save_password', host: location.hostname,
456
504
  username: user ? user.value : 'unknown', password: pw.value
@@ -841,8 +889,7 @@ class BookmarksBarView(Cocoa.NSView):
841
889
  if not isinstance(b, dict) or not b.get("url"):
842
890
  continue
843
891
  title = b.get("title", b["url"])[:24]
844
- # Fixed: Removed hardcoded default 🔖 favicon prefix from bookmarks
845
- btn_w = max(40, min(140, len(title) * 7.5 + 14))
892
+ btn_w = max(50, min(160, len(title) * 7.5 + 26))
846
893
  btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
847
894
  btn.setTitle_(title)
848
895
  btn.setBezelStyle_(Cocoa.NSBezelStyleInline)
@@ -850,10 +897,29 @@ class BookmarksBarView(Cocoa.NSView):
850
897
  btn.setTarget_(self)
851
898
  btn.setAction_("onBookmarkClick:")
852
899
  btn.setToolTip_(b.get("url", ""))
900
+
901
+ # Dynamically query and bind favicon
902
+ try:
903
+ host = urllib.parse.urlparse(b.get("url", "")).netloc
904
+ if host:
905
+ FaviconCache.get_favicon(host, lambda img, b_ref=btn: self._apply_button_icon(b_ref, img))
906
+ except Exception:
907
+ pass
908
+
853
909
  self.addSubview_(btn)
854
910
  x += btn_w + 4.0
855
911
  self.setNeedsDisplay_(True)
856
912
 
913
+ def _apply_button_icon(self, btn, img):
914
+ if img and btn:
915
+ try:
916
+ scaled = img.copy()
917
+ scaled.setSize_(Cocoa.NSMakeSize(14, 14))
918
+ btn.setImage_(scaled)
919
+ btn.setImagePosition_(Cocoa.NSImageLeft)
920
+ except Exception:
921
+ pass
922
+
857
923
  def onBookmarkClick_(self, sender):
858
924
  url = sender.toolTip()
859
925
  if url and self.controller:
@@ -968,7 +1034,6 @@ class InternalSchemeHandler(Cocoa.NSObject):
968
1034
  self.active_tasks.discard(task)
969
1035
 
970
1036
  def get_terminal_html(self):
971
- # Fullscreen responsive xterm with ResizeObserver to ensure vim/less utilize entire viewport
972
1037
  bg = "#1e1e2e"
973
1038
  fg = "#c792ea"
974
1039
  return f"""<!DOCTYPE html><html><head><meta charset='utf-8'>
@@ -992,6 +1057,19 @@ const fit = new FitAddon.FitAddon();
992
1057
  term.loadAddon(fit);
993
1058
  term.open(document.getElementById('t'));
994
1059
 
1060
+ // Guarantee Esc (0x1b) is sent directly to child PTY (vim/nano/fzf) without being swallowed
1061
+ term.attachCustomKeyEventHandler(e => {{
1062
+ if (e.key === 'Escape' || e.keyCode === 27) {{
1063
+ if (e.type === 'keydown') {{
1064
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.terminalInput) {{
1065
+ window.webkit.messageHandlers.terminalInput.postMessage('\\x1b');
1066
+ }}
1067
+ }}
1068
+ return false;
1069
+ }}
1070
+ return true;
1071
+ }});
1072
+
995
1073
  const postResize = () => {{
996
1074
  try {{
997
1075
  fit.fit();
@@ -1001,9 +1079,7 @@ const postResize = () => {{
1001
1079
  }} catch(e) {{}}
1002
1080
  }};
1003
1081
 
1004
- const ro = new ResizeObserver(() => {{
1005
- postResize();
1006
- }});
1082
+ const ro = new ResizeObserver(() => {{ postResize(); }});
1007
1083
  ro.observe(document.getElementById('t'));
1008
1084
 
1009
1085
  setTimeout(postResize, 30);
@@ -1038,22 +1114,24 @@ window.writeToTerm = b => {{
1038
1114
  u = b["url"]
1039
1115
  t = b.get("title", u)
1040
1116
  safe_u = json.dumps(u)
1041
- del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: {safe_u}}}); setTimeout(() => location.reload(), 100);"
1042
- items.append(f"<li><a href='{html.escape(u)}'>{html.escape(t)}</a> <button onclick='{del_call}'>Delete</button></li>")
1117
+ 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 ""
1119
+ 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>")
1043
1121
  rows = "".join(items)
1044
- return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title'><input id='u' placeholder='https://...'><button onclick='window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)'>Add</button></div><ul>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
1122
+ 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>"
1045
1123
 
1046
1124
  def get_history_html(self):
1047
1125
  records = query_history()
1048
1126
  items = []
1049
1127
  for r in records:
1050
- del_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
1128
+ del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
1051
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>")
1052
1130
  rows = "".join(items)
1053
- return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick=\"window.webkit.messageHandlers.bridge.postMessage({{cmd:'clear_browsing_data',targets:['history']}});setTimeout(()=>location.reload(),100)\">Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"
1131
+ 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>"
1054
1132
 
1055
1133
  def get_downloads_html(self):
1056
- return "<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Downloads</h2><div id='dl-list'>No active downloads.</div><script>const dls={};window.updateDownload=(id,name,path,status,progress)=>{dls[id]={name,path,status,progress};render()};function render(){const el=document.getElementById('dl-list');el.innerHTML=Object.values(dls).map(d=>`<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'><b>${d.name}</b> (${d.status}) <button onclick=\"window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_show',path:'${d.path}'})\">Show in Finder</button></div>`).join('')};window.webkit.messageHandlers.bridge.postMessage({cmd:'dl_request_sync'});</script></body></html>"
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>"
1057
1135
 
1058
1136
  def get_extensions_html(self):
1059
1137
  scripts = get_installed_userscripts()
@@ -1062,8 +1140,8 @@ window.writeToTerm = b => {{
1062
1140
  f = s["file"]
1063
1141
  safe_f = json.dumps(f)
1064
1142
  checked = "checked" if s["enabled"] else ""
1065
- toggle_call = f"window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
1066
- del_call = f"if(confirm('Delete?')){{window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
1143
+ toggle_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
1144
+ 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);}}"
1067
1145
  items.append(
1068
1146
  f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
1069
1147
  f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
@@ -1072,7 +1150,7 @@ window.writeToTerm = b => {{
1072
1150
  f"</div>"
1073
1151
  )
1074
1152
  rows = "".join(items)
1075
- return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='let id=prompt(\"CRX ID or URL:\");if(id)window.webkit.messageHandlers.bridge.postMessage({{cmd:\"install_crx\",id:id}})'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"
1153
+ 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>"
1076
1154
 
1077
1155
  # ============================================================================
1078
1156
  # WEBKIT SUBCLASS & IMAGE CONTEXT MENU
@@ -1200,7 +1278,7 @@ class OmniboxSuggestionView(Cocoa.NSView):
1200
1278
  opts = Cocoa.NSTrackingMouseMoved | Cocoa.NSTrackingActiveAlways | Cocoa.NSTrackingInVisibleRect
1201
1279
  self.tracking_area = Cocoa.NSTrackingArea.alloc().initWithRect_options_owner_userInfo_(Cocoa.NSZeroRect, opts, self, None)
1202
1280
  self.addTrackingArea_(self.tracking_area)
1203
- objc.super(OmniboxSuggestionView, self).updateTrackingAreas()
1281
+ objc.super(ChromeTabView, self).updateTrackingAreas() if hasattr(objc.super(ChromeTabView, self), 'updateTrackingAreas') else None
1204
1282
 
1205
1283
  def mouseMoved_(self, event):
1206
1284
  loc = self.convertPoint_fromView_(event.locationInWindow(), None)
@@ -1274,9 +1352,10 @@ class OmniboxPanelCoordinator:
1274
1352
 
1275
1353
  def fetch_remote():
1276
1354
  try:
1277
- url = f"http://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
1355
+ # Secured endpoint via HTTPS to avoid keystroke sniffing
1356
+ url = f"https://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
1278
1357
  req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
1279
- with urllib.request.urlopen(req, timeout=0.8) as resp:
1358
+ with urllib.request.urlopen(req, timeout=1.0) as resp:
1280
1359
  data = json.loads(resp.read().decode('utf-8'))
1281
1360
  remote = [{"type": "search", "text": s, "url": s} for s in data[1][:5]]
1282
1361
  if req_id == self.current_req_id:
@@ -1551,7 +1630,6 @@ class QuickSwitcherController(Cocoa.NSWindowController):
1551
1630
  elif sel == "insertNewline:":
1552
1631
  if self.filtered_tabs and self.selected_index >= 0:
1553
1632
  t = self.filtered_tabs[self.selected_index]
1554
- # Fixed: Resolve current tab index dynamically by ID to avoid desync/crash
1555
1633
  cur_idx = t["wc"].get_tab_index_by_id(t["id"])
1556
1634
  if cur_idx is not None:
1557
1635
  t["wc"].window().makeKeyAndOrderFront_(None)
@@ -1601,7 +1679,6 @@ class PiPPanelController(Cocoa.NSWindowController):
1601
1679
  self.window().makeKeyAndOrderFront_(None)
1602
1680
 
1603
1681
  def exec_js(self, js):
1604
- # Fixed: Prefer active window controller or stored browser controller
1605
1682
  app_delegate = Cocoa.NSApp.delegate()
1606
1683
  wc = None
1607
1684
  key_win = Cocoa.NSApp.keyWindow()
@@ -1770,7 +1847,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1770
1847
  self.home_btn.setHidden_(not SETTINGS.settings.get("show_home_button", False))
1771
1848
 
1772
1849
  omni_x = 132 if SETTINGS.settings.get("show_home_button", False) else 102
1773
- # Clean uniform 30px step for toolbar buttons, omnibox expands dynamically
1774
1850
  self.omnibox_container = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(omni_x, 3, max(180, w - omni_x - 162), 28))
1775
1851
  self.toolbar.addSubview_(self.omnibox_container)
1776
1852
 
@@ -1798,7 +1874,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1798
1874
  self.star_btn.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
1799
1875
  self.omnibox_container.addSubview_(self.star_btn)
1800
1876
 
1801
- # Fixed: Buttons pushed cleanly to the right edge with uniform 30px spacing
1802
1877
  self.pip_btn = btn("⧉", w - 154, "onTogglePiP:", "Media Controls")
1803
1878
  self.dl_btn = btn("⬇", w - 124, "onShowDownloads:", "Downloads")
1804
1879
  self.share_btn = btn("↗", w - 94, "onShareCurrentPage:", "Share Page")
@@ -1853,7 +1928,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1853
1928
  omni_x = 132 if show_home else 102
1854
1929
  tb_w = self.toolbar.bounds().size.width
1855
1930
 
1856
- # Fixed: Expand Omnibox to fill empty space, eliminated gap between zen and menu buttons
1857
1931
  if hasattr(self, 'omnibox_container'):
1858
1932
  box_w = max(180, tb_w - omni_x - 162)
1859
1933
  self.omnibox_container.setFrame_(Cocoa.NSMakeRect(omni_x, 3, box_w, 28))
@@ -1878,7 +1952,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1878
1952
  sub.removeFromSuperview()
1879
1953
  w = self.tab_strip.bounds().size.width
1880
1954
  is_vert = SETTINGS.settings.get("vertical_tabs_enabled", False)
1881
- # Because traffic lights sit in the title bar above, horizontal tabs start cleanly at x = 8.0
1882
1955
  x = 8.0
1883
1956
  tab_h = 28 if not is_vert else 32
1884
1957
  y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
@@ -1965,6 +2038,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
1965
2038
  uc.addScriptMessageHandler_name_(self, "terminalInput")
1966
2039
  uc.addScriptMessageHandler_name_(self, "terminalResize")
1967
2040
 
2041
+ # Injected scripts: Command+click capture, audio monitoring, context menus, and fullscreen handlers
2042
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(COMMAND_CLICK_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
1968
2043
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(AUDIO_MONITOR_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1969
2044
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IMAGE_CONTEXT_MENU_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
1970
2045
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(KEYCHAIN_AUTOSAVE_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
@@ -2009,7 +2084,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2009
2084
  except Exception:
2010
2085
  pass
2011
2086
 
2012
- initial_zoom = float(SETTINGS.settings.get("default_zoom", 0.8))
2087
+ initial_zoom = float(SETTINGS.settings.get("default_zoom", 1.0))
2013
2088
  tab = {
2014
2089
  "id": str(uuid.uuid4()), "webview": wv, "url": url, "title": "New Tab",
2015
2090
  "favicon": None, "pinned": pinned, "group_id": None, "zoom": initial_zoom,
@@ -2049,8 +2124,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
2049
2124
  self.wake_tab(index)
2050
2125
  wv = tab["webview"]
2051
2126
 
2052
- # Fixed: Keep webviews attached and toggle visibility with setHidden_
2053
- # This completely preserves DOM focus, text inputs, and render state
2054
2127
  for i, t in enumerate(self.tabs):
2055
2128
  twv = t.get("webview")
2056
2129
  if twv:
@@ -2066,7 +2139,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
2066
2139
  self.update_progress_for_webview(wv)
2067
2140
  self.relayout_tabs()
2068
2141
 
2069
- # Fixed: Restore keyboard focus and text cursor to the active input element automatically
2070
2142
  self.window().makeFirstResponder_(wv)
2071
2143
  wv.evaluateJavaScript_completionHandler_(
2072
2144
  "if (document.activeElement && typeof document.activeElement.focus === 'function') { document.activeElement.focus(); }",
@@ -2258,7 +2330,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2258
2330
  def apply_zoom_to_webview(self, wv, tab=None):
2259
2331
  if not wv:
2260
2332
  return
2261
- target_zoom = tab.get("zoom", float(SETTINGS.settings.get("default_zoom", 0.8))) if tab else float(SETTINGS.settings.get("default_zoom", 0.8))
2333
+ target_zoom = tab.get("zoom", float(SETTINGS.settings.get("default_zoom", 1.0))) if tab else float(SETTINGS.settings.get("default_zoom", 1.0))
2262
2334
  target_zoom = max(0.5, min(2.0, target_zoom))
2263
2335
  try:
2264
2336
  if hasattr(wv, "setAllowsMagnification_"): wv.setAllowsMagnification_(False)
@@ -2268,18 +2340,19 @@ class BrowserWindowController(Cocoa.NSWindowController):
2268
2340
  pass
2269
2341
 
2270
2342
  def restore_normal_zoom(self):
2343
+ if self.is_active_tab_terminal():
2344
+ return
2271
2345
  wv = self.get_active_webview()
2272
2346
  if wv:
2273
- self.apply_zoom_to_webview(wv)
2347
+ tab = self.tabs[self.active_tab_index] if (0 <= self.active_tab_index < len(self.tabs)) else None
2348
+ self.apply_zoom_to_webview(wv, tab)
2274
2349
  wv.evaluateJavaScript_completionHandler_(REFLOW_RESTORE_JS, None)
2275
2350
 
2276
2351
  def handle_fullscreen_enter(self, auto_hide_bars=True):
2277
2352
  if auto_hide_bars:
2278
2353
  if not self.tab_bar_visible:
2279
- # Tab was already in Zen mode before entering fullscreen
2280
2354
  self.zen_auto_entered = False
2281
2355
  else:
2282
- # Tab was in normal mode, auto-enter Zen mode for fullscreen
2283
2356
  self.zen_auto_entered = True
2284
2357
  self.tab_bar_visible = False
2285
2358
  self.relayout_views()
@@ -2292,22 +2365,27 @@ class BrowserWindowController(Cocoa.NSWindowController):
2292
2365
  pass
2293
2366
 
2294
2367
  def handle_fullscreen_exit(self, restore_bars=True):
2295
- if restore_bars:
2296
- # Fixed: Only exit Zen mode if it was entered automatically by fullscreen
2297
- if self.zen_auto_entered:
2298
- self.tab_bar_visible = True
2299
- self.zen_auto_entered = False
2300
- self.relayout_views()
2301
- # If tab was already in Zen mode before fullscreen, keep Zen mode on!
2368
+ if restore_bars and (self.zen_auto_entered or not self.tab_bar_visible):
2369
+ self.tab_bar_visible = True
2370
+ self.zen_auto_entered = False
2371
+ self.relayout_views()
2302
2372
  wv = self.get_active_webview()
2303
2373
  if wv:
2304
2374
  wv.evaluateJavaScript_completionHandler_("if (window.setTerminalFullscreen) window.setTerminalFullscreen(false);", None)
2305
2375
  self.restore_normal_zoom()
2306
2376
 
2377
+ def is_active_tab_terminal(self):
2378
+ if 0 <= self.active_tab_index < len(self.tabs):
2379
+ url = self.tabs[self.active_tab_index].get("url") or ""
2380
+ return "browser://terminal" in url
2381
+ return False
2382
+
2307
2383
  def handle_escape_key(self):
2308
2384
  if self.find_bar and not self.find_bar.isHidden():
2309
2385
  self.find_bar.setHidden_(True)
2310
2386
  return
2387
+ if self.is_active_tab_terminal():
2388
+ return
2311
2389
  wv = self.get_active_webview()
2312
2390
  if not self.tab_bar_visible:
2313
2391
  self.handle_fullscreen_exit(restore_bars=True)
@@ -2339,21 +2417,21 @@ class BrowserWindowController(Cocoa.NSWindowController):
2339
2417
  def onZoomIn_(self, sender=None):
2340
2418
  if 0 <= self.active_tab_index < len(self.tabs):
2341
2419
  tab = self.tabs[self.active_tab_index]
2342
- z = min(2.0, round(tab.get("zoom", 0.8) + 0.1, 2))
2420
+ z = min(2.0, round(tab.get("zoom", 1.0) + 0.1, 2))
2343
2421
  tab["zoom"] = z
2344
2422
  self.apply_zoom_to_webview(tab.get("webview"), tab)
2345
2423
 
2346
2424
  def onZoomOut_(self, sender=None):
2347
2425
  if 0 <= self.active_tab_index < len(self.tabs):
2348
2426
  tab = self.tabs[self.active_tab_index]
2349
- z = max(0.5, round(tab.get("zoom", 0.8) - 0.1, 2))
2427
+ z = max(0.5, round(tab.get("zoom", 1.0) - 0.1, 2))
2350
2428
  tab["zoom"] = z
2351
2429
  self.apply_zoom_to_webview(tab.get("webview"), tab)
2352
2430
 
2353
2431
  def onResetZoom_(self, sender=None):
2354
2432
  if 0 <= self.active_tab_index < len(self.tabs):
2355
2433
  tab = self.tabs[self.active_tab_index]
2356
- tab["zoom"] = float(SETTINGS.settings.get("default_zoom", 0.8))
2434
+ tab["zoom"] = float(SETTINGS.settings.get("default_zoom", 1.0))
2357
2435
  self.apply_zoom_to_webview(tab.get("webview"), tab)
2358
2436
  self.restore_normal_zoom()
2359
2437
 
@@ -2377,7 +2455,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
2377
2455
  menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
2378
2456
  bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
2379
2457
  menu.addItemWithTitle_action_keyEquivalent_(bm_text, "toggleBookmarksBar:", "b").setTarget_(self)
2380
- menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "").setTarget_(self)
2458
+ zen_item = menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "\x1b")
2459
+ zen_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagOption)
2460
+ zen_item.setTarget_(self)
2381
2461
  menu.addItemWithTitle_action_keyEquivalent_("Find in Page", "onFindInPage:", "f").setTarget_(self)
2382
2462
  menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2383
2463
  menu.addItemWithTitle_action_keyEquivalent_("Settings", "openSettings:", ",").setTarget_(self)
@@ -2613,10 +2693,14 @@ class BrowserWindowController(Cocoa.NSWindowController):
2613
2693
  return
2614
2694
  elif name == "terminalResize":
2615
2695
  b = message.body()
2616
- TERMINAL_MANAGER.get_or_create(message.webView(), b.get("cols"), b.get("rows"))
2696
+ TERMINAL_MANAGER.get_or_create(
2697
+ message.webView(),
2698
+ b.get("cols") if hasattr(b, "get") else None,
2699
+ b.get("rows") if hasattr(b, "get") else None
2700
+ )
2617
2701
  return
2618
2702
  elif name == "audio":
2619
- b = message.body() if isinstance(message.body(), dict) else {}
2703
+ b = message.body() if hasattr(message.body(), "get") else {}
2620
2704
  has_a = (b.get("cmd") == "audio_play")
2621
2705
  for t in self.tabs:
2622
2706
  if t.get("webview") == message.webView():
@@ -2626,11 +2710,18 @@ class BrowserWindowController(Cocoa.NSWindowController):
2626
2710
  return
2627
2711
 
2628
2712
  body = message.body()
2629
- cmd = body.get("cmd") if isinstance(body, dict) else None
2713
+ cmd = None
2714
+ if hasattr(body, "get"):
2715
+ cmd = body.get("cmd")
2716
+ elif isinstance(body, dict):
2717
+ cmd = body.get("cmd")
2718
+ if cmd is not None:
2719
+ cmd = str(cmd)
2630
2720
 
2631
2721
  if cmd == "open_url":
2632
- if body.get("url"):
2633
- self.add_tab(body.get("url"))
2722
+ target_url = body.get("url")
2723
+ if target_url:
2724
+ self.add_tab(target_url)
2634
2725
  elif cmd == "toggle_fullscreen_terminal":
2635
2726
  self.toggle_tab_bar()
2636
2727
  elif cmd == "context_target_image":
@@ -2676,11 +2767,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
2676
2767
  if wv:
2677
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)
2678
2769
  elif cmd == "save_setting":
2679
- key, val = body.get("key"), body.get("val")
2770
+ raw_key, raw_val = body.get("key"), body.get("val")
2771
+ if raw_key is None:
2772
+ return
2773
+ key = str(raw_key)
2774
+ val = sanitize_for_json(raw_val)
2680
2775
  if key == "download_path":
2681
2776
  val = os.path.expanduser(str(val).strip())
2682
2777
  SETTINGS.settings[key] = val
2683
2778
  SETTINGS.save()
2779
+
2684
2780
  if key in ("bookmarks_bar_visible", "show_home_button", "vertical_tabs_enabled"):
2685
2781
  self.relayout_views()
2686
2782
  elif key == "theme_mode":
@@ -2693,21 +2789,35 @@ class BrowserWindowController(Cocoa.NSWindowController):
2693
2789
  for sub in c.tab_strip.subviews():
2694
2790
  sub.setNeedsDisplay_(True)
2695
2791
  elif key in ("default_zoom",):
2696
- wv = self.get_active_webview()
2697
- if wv:
2698
- self.apply_zoom_to_webview(wv)
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)
2699
2798
  elif key in ("user_agent_mode", "custom_user_agent"):
2700
2799
  ua = SETTINGS.get_user_agent()
2701
2800
  for t in self.tabs:
2702
2801
  if t.get("webview"):
2703
2802
  t["webview"].setCustomUserAgent_(ua)
2803
+ elif key == "js_enabled":
2804
+ js_flag = bool(val)
2805
+ for t in self.tabs:
2806
+ twv = t.get("webview")
2807
+ if twv and hasattr(twv.configuration().preferences(), "setJavaScriptEnabled_"):
2808
+ twv.configuration().preferences().setJavaScriptEnabled_(js_flag)
2809
+ elif key == "block_popups":
2810
+ pop_flag = not bool(val)
2811
+ for t in self.tabs:
2812
+ twv = t.get("webview")
2813
+ if twv and hasattr(twv.configuration().preferences(), "setJavaScriptCanOpenWindowsAutomatically_"):
2814
+ twv.configuration().preferences().setJavaScriptCanOpenWindowsAutomatically_(pop_flag)
2704
2815
  elif key == "doh_provider":
2705
2816
  with DNS_LOCK:
2706
2817
  DNS_CACHE.clear()
2707
2818
  elif key in ("adblock_enabled", "adblock_stevenblack_enabled", "adblock_base_enabled", "custom_adblock_domains"):
2708
2819
  compile_webkit_adblock_rules()
2709
2820
  elif cmd == "choose_download_directory":
2710
- # Fixed: Asynchronous modal sheet targeting caller webview without blocking WebKit IPC
2711
2821
  calling_wv = message.webView()
2712
2822
  panel = Cocoa.NSOpenPanel.openPanel()
2713
2823
  panel.setCanChooseDirectories_(True)
@@ -2718,22 +2828,22 @@ class BrowserWindowController(Cocoa.NSWindowController):
2718
2828
  cur_dir = validate_and_get_download_dir()
2719
2829
  panel.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(cur_dir))
2720
2830
 
2721
- def on_panel_done(result):
2722
- if result == Cocoa.NSModalResponseOK and panel.URLs():
2723
- chosen_path = panel.URLs()[0].path()
2724
- if os.access(chosen_path, os.W_OK):
2725
- SETTINGS.settings["download_path"] = chosen_path
2726
- SETTINGS.save()
2727
- if calling_wv:
2728
- path_json = json.dumps(chosen_path)
2729
- calling_wv.evaluateJavaScript_completionHandler_(
2730
- f"if (document.getElementById('dl-path-input')) document.getElementById('dl-path-input').value = {path_json}; if (document.getElementById('dl-path-display')) document.getElementById('dl-path-display').innerText = {path_json}; showToast('Download directory updated');", None
2731
- )
2732
- else:
2733
- if calling_wv:
2734
- calling_wv.evaluateJavaScript_completionHandler_("showToast('Error: Selected directory is not writable');", None)
2735
-
2736
- panel.beginSheetModalForWindow_completionHandler_(self.window(), on_panel_done)
2831
+ res = panel.runModal()
2832
+ if res == Cocoa.NSModalResponseOK and panel.URLs():
2833
+ chosen_path = str(panel.URLs()[0].path())
2834
+ if os.access(chosen_path, os.W_OK):
2835
+ SETTINGS.settings["download_path"] = chosen_path
2836
+ SETTINGS.save()
2837
+ if calling_wv:
2838
+ path_json = json.dumps(chosen_path)
2839
+ calling_wv.evaluateJavaScript_completionHandler_(
2840
+ f"if (document.getElementById('dl-path-input')) document.getElementById('dl-path-input').value = {path_json};"
2841
+ f"if (document.getElementById('dl-path-display')) document.getElementById('dl-path-display').innerText = {path_json};"
2842
+ "showToast('Download directory updated');", None
2843
+ )
2844
+ else:
2845
+ if calling_wv:
2846
+ calling_wv.evaluateJavaScript_completionHandler_("showToast('Error: Selected directory is not writable');", None)
2737
2847
  elif cmd == "search_blocked_domains":
2738
2848
  matches = search_blocked_domains(body.get("query", ""), limit=100)
2739
2849
  json_res = json.dumps(matches)
@@ -2750,11 +2860,13 @@ class BrowserWindowController(Cocoa.NSWindowController):
2750
2860
  dispatch_to_main(on_done)
2751
2861
  threading.Thread(target=fetch_task, daemon=True).start()
2752
2862
  elif cmd == "clear_browsing_data":
2753
- targets = body.get("targets", [])
2863
+ raw_targets = body.get("targets", [])
2864
+ targets = list(raw_targets) if hasattr(raw_targets, "__iter__") else []
2754
2865
  if "history" in targets:
2755
2866
  clear_history()
2756
2867
  if "favicons" in targets:
2757
2868
  FaviconCache.clear()
2869
+ self.bookmarks_bar.reload_bookmarks()
2758
2870
  if "dns_cache" in targets:
2759
2871
  with DNS_LOCK:
2760
2872
  DNS_CACHE.clear()
@@ -2764,10 +2876,12 @@ class BrowserWindowController(Cocoa.NSWindowController):
2764
2876
  elif cmd == "reset_defaults":
2765
2877
  SETTINGS.reset_defaults()
2766
2878
  self.relayout_views()
2879
+ def_zoom = float(SETTINGS.settings.get("default_zoom", 1.0))
2767
2880
  for tab in self.tabs:
2881
+ tab["zoom"] = def_zoom
2768
2882
  wv = tab.get("webview")
2769
2883
  if wv:
2770
- self.apply_zoom_to_webview(wv)
2884
+ self.apply_zoom_to_webview(wv, tab)
2771
2885
  wv = self.get_active_webview()
2772
2886
  if wv:
2773
2887
  wv.reload()
@@ -2848,9 +2962,10 @@ class BrowserWindowController(Cocoa.NSWindowController):
2848
2962
 
2849
2963
  def webView_decidePolicyForNavigationAction_decisionHandler_(self, wv, action, handler):
2850
2964
  mods = action.modifierFlags() if hasattr(action, "modifierFlags") else 0
2851
- if bool(mods & Cocoa.NSEventModifierFlagCommand) and action.navigationType() == WebKit.WKNavigationTypeLinkActivated:
2965
+ cmd_pressed = bool(mods & Cocoa.NSEventModifierFlagCommand)
2966
+ if cmd_pressed or action.navigationType() == WebKit.WKNavigationTypeLinkActivated:
2852
2967
  url = action.request().URL().absoluteString() if action.request().URL() else None
2853
- if url:
2968
+ if cmd_pressed and url:
2854
2969
  self.add_tab(url)
2855
2970
  handler(WebKit.WKNavigationActionPolicyCancel)
2856
2971
  return
@@ -3025,25 +3140,45 @@ class AppDelegate(Cocoa.NSObject):
3025
3140
  Cocoa.NSEvent.addLocalMonitorForEventsMatchingMask_handler_(Cocoa.NSEventMaskLeftMouseDown, click_monitor)
3026
3141
 
3027
3142
  def key_monitor(event):
3028
- flags = event.modifierFlags()
3143
+ raw_flags = event.modifierFlags()
3144
+ flags = raw_flags & Cocoa.NSEventModifierFlagDeviceIndependentFlagsMask
3029
3145
  cmd = bool(flags & Cocoa.NSEventModifierFlagCommand)
3030
3146
  shift = bool(flags & Cocoa.NSEventModifierFlagShift)
3031
3147
  opt = bool(flags & Cocoa.NSEventModifierFlagOption)
3032
3148
  key = event.keyCode()
3033
3149
  chars = event.charactersIgnoringModifiers().lower() if event.charactersIgnoringModifiers() else ""
3034
- win = Cocoa.NSApp.keyWindow()
3035
- wc = win.delegate() if win and isinstance(win.delegate(), BrowserWindowController) else None
3036
3150
 
3037
- if key == 53: # Escape
3151
+ win = event.window() or Cocoa.NSApp.keyWindow() or Cocoa.NSApp.mainWindow()
3152
+ wc = None
3153
+ if win and isinstance(win.delegate(), BrowserWindowController):
3154
+ wc = win.delegate()
3155
+ elif hasattr(self, 'controllers') and self.controllers:
3156
+ for c in self.controllers:
3157
+ if c.window() == win:
3158
+ wc = c
3159
+ break
3160
+ if not wc and self.controllers:
3161
+ wc = self.controllers[0]
3162
+
3163
+ if key == 53: # Escape key
3038
3164
  if shift and not cmd and not opt:
3039
3165
  self.process_manager.show()
3040
3166
  return None
3041
- if opt and wc:
3167
+ if opt and not cmd and not shift and wc:
3042
3168
  wc.toggle_tab_bar()
3043
3169
  return None
3044
3170
  if wc:
3171
+ # Allow Escape to pass directly into terminal tabs (vim, nano, fzf, less)
3172
+ if wc.is_active_tab_terminal():
3173
+ return event
3174
+ if wc.find_bar and not wc.find_bar.isHidden():
3175
+ wc.find_bar.setHidden_(True)
3176
+ return None
3177
+ if not wc.tab_bar_visible:
3178
+ wc.toggle_tab_bar()
3179
+ return None
3045
3180
  wc.handle_escape_key()
3046
- return None
3181
+ return event
3047
3182
 
3048
3183
  if cmd and shift and not opt:
3049
3184
  if key == 0 or chars == 'a':
@@ -3143,8 +3278,10 @@ class AppDelegate(Cocoa.NSObject):
3143
3278
  view_menu.addItemWithTitle_action_keyEquivalent_("Reload Page", "onReload:", "r")
3144
3279
  view_menu.addItemWithTitle_action_keyEquivalent_("Focus Omnibox", "onFocusOmnibox:", "l")
3145
3280
  view_menu.addItemWithTitle_action_keyEquivalent_("Picture-in-Picture", "onTogglePiP:", "p")
3281
+ zen_item = view_menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "\x1b")
3282
+ zen_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagOption)
3146
3283
  view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3147
- view_menu.addItemWithTitle_action_keyEquivalent_("Zoom In", "onZoomIn:", "+")
3284
+ view_menu.addItemWithTitle_action_keyEquivalent_("Zoom In", "onZoomIn:", "=")
3148
3285
  view_menu.addItemWithTitle_action_keyEquivalent_("Zoom Out", "onZoomOut:", "-")
3149
3286
  view_menu.addItemWithTitle_action_keyEquivalent_("Actual Size", "onResetZoom:", "0")
3150
3287
  view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
pybrowser/settings.py CHANGED
@@ -23,7 +23,7 @@ DEFAULT_URL = "https://www.google.com"
23
23
  DATA_DIR = os.path.expanduser("~/data")
24
24
  os.makedirs(DATA_DIR, exist_ok=True)
25
25
 
26
- _cur_dir = os.path.dirname(os.path.abspath(__file__))
26
+ _cur_dir = os.path.dirname(os.path.abspath(__file__)) if "__file__" in globals() else os.getcwd()
27
27
  _old_candidates = [
28
28
  os.path.abspath(os.path.join(_cur_dir, "..", "browser_data")),
29
29
  os.path.abspath(os.path.join(os.getcwd(), "browser_data")),
@@ -80,6 +80,49 @@ GROUP_COLORS = {
80
80
  "Green": "#1E8E3E", "Pink": "#E52592", "Purple": "#9334E6", "Cyan": "#12B5CB"
81
81
  }
82
82
 
83
+ def sanitize_for_json(obj):
84
+ """
85
+ Recursively converts PyObjC wrapper types (__NSCFBoolean, NSString, NSNumber,
86
+ NSArray, NSDictionary) into standard Python primitives for json.dump compatibility.
87
+ """
88
+ if obj is None:
89
+ return None
90
+
91
+ type_name = type(obj).__name__
92
+
93
+ # PyObjC booleans (__NSCFBoolean) must use boolValue() to accurately preserve False/True
94
+ if hasattr(obj, "boolValue") and ("Boolean" in type_name or "bool" in type_name.lower()):
95
+ return bool(obj.boolValue())
96
+
97
+ # Standard Python booleans
98
+ if isinstance(obj, bool) or type_name == "bool":
99
+ return bool(obj)
100
+
101
+ # Number conversion for PyObjC NSNumber (e.g. float/double or int)
102
+ if hasattr(obj, "doubleValue") and not isinstance(obj, (int, float, str, bytes)):
103
+ val = obj.doubleValue()
104
+ return int(val) if val.is_integer() else val
105
+
106
+ if isinstance(obj, (int, float)):
107
+ return obj
108
+
109
+ if isinstance(obj, (str, bytes)):
110
+ return str(obj)
111
+
112
+ if hasattr(obj, "items") or isinstance(obj, dict):
113
+ return {str(k): sanitize_for_json(v) for k, v in obj.items()}
114
+
115
+ if hasattr(obj, "__iter__") or isinstance(obj, (list, tuple, set)):
116
+ return [sanitize_for_json(i) for i in obj]
117
+
118
+ try:
119
+ if hasattr(obj, "stringValue"):
120
+ return str(obj.stringValue())
121
+ except Exception:
122
+ pass
123
+
124
+ return str(obj)
125
+
83
126
  def validate_and_get_download_dir(target_path=None):
84
127
  if not target_path:
85
128
  target_path = SETTINGS.settings.get("download_path", os.path.expanduser("~/Downloads"))
@@ -130,7 +173,7 @@ class SettingsManager:
130
173
  "vertical_tabs_collapsed": False,
131
174
  "show_home_button": False,
132
175
  "home_url": "https://www.google.com",
133
- "default_zoom": 0.8,
176
+ "default_zoom": 1.0,
134
177
  "doh_provider": "cloudflare",
135
178
  "doh_custom_url": "",
136
179
  "custom_adblock_domains": "",
@@ -158,18 +201,20 @@ class SettingsManager:
158
201
  except Exception as e:
159
202
  print(f"[Settings Load Warning] {e}")
160
203
  try:
161
- z = float(self.settings.get("default_zoom", 0.8))
204
+ z = float(self.settings.get("default_zoom", 1.0))
162
205
  self.settings["default_zoom"] = max(0.5, min(2.0, z))
163
206
  except Exception:
164
- self.settings["default_zoom"] = 0.8
207
+ self.settings["default_zoom"] = 1.0
165
208
  self.save()
166
209
 
167
210
  def save(self):
168
211
  try:
169
212
  dir_name = os.path.dirname(SETTINGS_FILE) or "."
213
+ os.makedirs(dir_name, exist_ok=True)
214
+ sanitized = sanitize_for_json(self.settings)
170
215
  fd, temp_path = tempfile.mkstemp(dir=dir_name)
171
216
  with os.fdopen(fd, 'w', encoding='utf-8') as f:
172
- json.dump(self.settings, f, indent=2)
217
+ json.dump(sanitized, f, indent=2)
173
218
  os.replace(temp_path, SETTINGS_FILE)
174
219
  except Exception as e:
175
220
  print(f"[Settings Save Error] {e}")
@@ -199,7 +244,7 @@ class SettingsManager:
199
244
  domains = set()
200
245
  custom = self.settings.get("custom_adblock_domains", "")
201
246
  if custom:
202
- for d in re.split(r'[\r\n,]+', custom):
247
+ for d in re.split(r'[\r\n,]+', str(custom)):
203
248
  d = d.strip().lower()
204
249
  if d:
205
250
  domains.add(d)
@@ -226,7 +271,7 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
226
271
  tab_action = s.get("tab_bar_double_click_action", "maximize")
227
272
  downloads_enabled = s.get("downloads_enabled", True)
228
273
  cur_dl_path = s.get("download_path", os.path.expanduser("~/Downloads"))
229
- cur_zoom = str(s.get("default_zoom", 1.0))
274
+ cur_zoom = f"{float(s.get('default_zoom', 1.0)):.2f}"
230
275
 
231
276
  return f"""<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Settings</title>
232
277
  <style>
@@ -296,7 +341,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
296
341
  </div>
297
342
  <div id="custom-search-row" class="row" style="display: {'flex' if engine_preset == 'custom' else 'none'};">
298
343
  <div class="label-group"><div class="label-title">Custom Search URL Template</div><div class="label-desc">Use <code>%s</code> as query placeholder</div></div>
299
- <input type="text" class="m3-input" style="width:280px;" value="{html.escape(custom_search_url)}" onchange="saveSetting('custom_search_url', this.value, 'Custom search URL saved')">
344
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(custom_search_url)}"
345
+ onchange="saveSetting('custom_search_url', this.value, 'Custom search URL saved')"
346
+ onkeydown="if(event.key==='Enter') this.blur();">
300
347
  </div>
301
348
  <div class="row">
302
349
  <div class="label-group"><div class="label-title">Search in New Tab</div><div class="label-desc">Open Omnibox search results in a new tab</div></div>
@@ -327,13 +374,13 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
327
374
  </select>
328
375
  </div>
329
376
  <div class="row">
330
- <div class="label-group"><div class="label-title">Default Zoom Level</div><div class="label-desc">Default page scaling factor for new web views</div></div>
377
+ <div class="label-group"><div class="label-title">Default Zoom Level</div><div class="label-desc">Default page scaling factor across all web views</div></div>
331
378
  <select class="m3-select" onchange="saveSetting('default_zoom', parseFloat(this.value), 'Zoom level updated')">
332
379
  <option value="0.75" {'selected' if cur_zoom == '0.75' else ''}>75%</option>
333
- <option value="0.8" {'selected' if cur_zoom in ('0.8', '0.80') else ''}>80%</option>
334
- <option value="1.0" {'selected' if cur_zoom in ('1', '1.0', '1.00') else ''}>100% (Default)</option>
380
+ <option value="0.80" {'selected' if cur_zoom == '0.80' else ''}>80%</option>
381
+ <option value="1.00" {'selected' if cur_zoom in ('1.00', '1.0', '1') else ''}>100% (Default)</option>
335
382
  <option value="1.25" {'selected' if cur_zoom == '1.25' else ''}>125%</option>
336
- <option value="1.5" {'selected' if cur_zoom == '1.5' else ''}>150%</option>
383
+ <option value="1.50" {'selected' if cur_zoom == '1.50' else ''}>150%</option>
337
384
  </select>
338
385
  </div>
339
386
  <div class="row">
@@ -359,7 +406,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
359
406
  </div>
360
407
  <div class="row">
361
408
  <div class="label-group"><div class="label-title">Home Page URL</div><div class="label-desc">Webpage loaded on clicking home button</div></div>
362
- <input type="text" class="m3-input" style="width:260px;" value="{html.escape(s.get('home_url', 'https://www.google.com'))}" onchange="saveSetting('home_url', this.value, 'Home URL saved')">
409
+ <input type="text" class="m3-input" style="width:260px;" value="{html.escape(s.get('home_url', 'https://www.google.com'))}"
410
+ onchange="saveSetting('home_url', this.value, 'Home URL saved')"
411
+ onkeydown="if(event.key==='Enter') this.blur();">
363
412
  </div>
364
413
  </div>
365
414
  </div>
@@ -395,6 +444,13 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
395
444
  <span class="slider"></span>
396
445
  </label>
397
446
  </div>
447
+ <div class="row">
448
+ <div class="label-group"><div class="label-title">Block Unsolicited Popups</div><div class="label-desc">Prevent background scripts from opening intrusive windows</div></div>
449
+ <label class="switch">
450
+ <input type="checkbox" {'checked' if s.get('block_popups', True) else ''} onchange="saveSetting('block_popups', this.checked, 'Popup blocker toggled')">
451
+ <span class="slider"></span>
452
+ </label>
453
+ </div>
398
454
  <div class="row-col">
399
455
  <div class="label-group" style="max-width: 100%;">
400
456
  <div class="label-title">Rule & Domain Inspector</div>
@@ -426,7 +482,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
426
482
  </div>
427
483
  <div id="custom-doh-row" class="row" style="display: {'flex' if doh_provider == 'custom' else 'none'};">
428
484
  <div class="label-group"><div class="label-title">Custom DoH URL</div><div class="label-desc">Must support DNS-over-HTTPS JSON API</div></div>
429
- <input type="text" class="m3-input" style="width:280px;" value="{html.escape(doh_custom_url)}" placeholder="https://..." onchange="saveSetting('doh_custom_url', this.value, 'Custom DoH URL saved')">
485
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(doh_custom_url)}" placeholder="https://..."
486
+ onchange="saveSetting('doh_custom_url', this.value, 'Custom DoH URL saved')"
487
+ onkeydown="if(event.key==='Enter') this.blur();">
430
488
  </div>
431
489
  <div class="row">
432
490
  <div class="label-group"><div class="label-title">HTTPS-Only Mode</div><div class="label-desc">Upgrades unencrypted HTTP requests to secure HTTPS</div></div>
@@ -444,7 +502,8 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
444
502
  <div class="label-title">Custom Blocked Domains</div>
445
503
  <div class="label-desc">Add additional domains to block (comma- or line-separated)</div>
446
504
  </div>
447
- <textarea class="m3-input" style="width:100%; height:75px; resize:vertical;" placeholder="adserver.com&#10;tracker.telemetry.io" onchange="saveSetting('custom_adblock_domains', this.value, 'Custom domains saved')">{html.escape(s.get('custom_adblock_domains', ''))}</textarea>
505
+ <textarea class="m3-input" style="width:100%; height:75px; resize:vertical;" placeholder="adserver.com&#10;tracker.telemetry.io"
506
+ onchange="saveSetting('custom_adblock_domains', this.value, 'Custom domains saved')">{html.escape(s.get('custom_adblock_domains', ''))}</textarea>
448
507
  </div>
449
508
  </div>
450
509
  </div>
@@ -462,7 +521,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
462
521
  </div>
463
522
  <div id="custom-startup-row" class="row" style="display: {'flex' if startup_behavior == 'custom_url' else 'none'};">
464
523
  <div class="label-group"><div class="label-title">Startup URL</div><div class="label-desc">Webpage loaded on initial application launch</div></div>
465
- <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('startup_url', 'https://www.google.com'))}" onchange="saveSetting('startup_url', this.value, 'Startup URL saved')">
524
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('startup_url', 'https://www.google.com'))}"
525
+ onchange="saveSetting('startup_url', this.value, 'Startup URL saved')"
526
+ onkeydown="if(event.key==='Enter') this.blur();">
466
527
  </div>
467
528
  <div class="row">
468
529
  <div class="label-group"><div class="label-title">Window State on Launch</div><div class="label-desc">Window geometry upon application launch</div></div>
@@ -500,8 +561,8 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
500
561
  </div>
501
562
  <div style="display:flex; gap:8px; align-items:center;">
502
563
  <input type="text" id="dl-path-input" class="m3-input" style="width:240px;" value="{html.escape(cur_dl_path)}"
503
- onchange="saveSetting('download_path', this.value, 'Download path saved')"
504
- onkeydown="if(event.key==='Enter') saveSetting('download_path', this.value, 'Download path saved')">
564
+ onchange="onDownloadInputChange(this.value)"
565
+ onkeydown="if(event.key==='Enter') {{ onDownloadInputChange(this.value); this.blur(); }}">
505
566
  <button class="btn" onclick="browseDownloadFolder()">Browse…</button>
506
567
  </div>
507
568
  </div>
@@ -523,7 +584,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
523
584
  </div>
524
585
  <div id="custom-ua-row" class="row" style="display: {'flex' if ua_mode == 'custom' else 'none'};">
525
586
  <div class="label-group"><div class="label-title">Custom User-Agent Header</div><div class="label-desc">Raw User-Agent string sent with requests</div></div>
526
- <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('custom_user_agent', ''))}" onchange="saveSetting('custom_user_agent', this.value, 'Custom User-Agent saved')">
587
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('custom_user_agent', ''))}"
588
+ onchange="saveSetting('custom_user_agent', this.value, 'Custom User-Agent saved')"
589
+ onkeydown="if(event.key==='Enter') this.blur();">
527
590
  </div>
528
591
  <div class="row">
529
592
  <div class="label-group"><div class="label-title">WebGL Hardware Acceleration</div><div class="label-desc">Enable 3D canvas and accelerated GPU graphics</div></div>
@@ -583,9 +646,18 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
583
646
  window._toastTimer = setTimeout(() => t.classList.remove('show'), 2400);
584
647
  }}
585
648
  function saveSetting(key, val, msg) {{
586
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
649
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
650
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
651
+ }}
587
652
  showToast(msg);
588
653
  }}
654
+ function onDownloadInputChange(val) {{
655
+ const trimmed = (val || '').trim();
656
+ if (!trimmed) return;
657
+ saveSetting('download_path', trimmed, 'Download path saved');
658
+ const disp = document.getElementById('dl-path-display');
659
+ if (disp) disp.innerText = trimmed;
660
+ }}
589
661
  function onSearchPresetChange(val) {{
590
662
  saveSetting('search_engine_preset', val, 'Search engine updated');
591
663
  document.getElementById('custom-search-row').style.display = (val === 'custom') ? 'flex' : 'none';
@@ -603,12 +675,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
603
675
  document.getElementById('custom-startup-row').style.display = (val === 'custom_url') ? 'flex' : 'none';
604
676
  }}
605
677
  function browseDownloadFolder() {{
606
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'choose_download_directory' }});
678
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
679
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'choose_download_directory' }});
680
+ }}
607
681
  }}
608
682
  function updateAdblockFilters() {{
609
683
  const b = document.getElementById('update-filters-btn');
610
684
  b.innerText = "Syncing Lists..."; b.disabled = true;
611
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'update_adblock_filters' }});
685
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
686
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'update_adblock_filters' }});
687
+ }}
612
688
  }}
613
689
  function searchDomain() {{
614
690
  const q = document.getElementById('inspector-query').value.trim();
@@ -616,7 +692,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
616
692
  const box = document.getElementById('inspector-results');
617
693
  box.style.display = 'block';
618
694
  box.innerHTML = 'Searching blocklists...';
619
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
695
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
696
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
697
+ }}
620
698
  }}
621
699
  window.renderBlockedResults = function(jsonStr) {{
622
700
  let items = [];
@@ -639,12 +717,16 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
639
717
  if (document.getElementById('clr-favicons').checked) targets.push('favicons');
640
718
  if (document.getElementById('clr-dns').checked) targets.push('dns_cache');
641
719
  if (targets.length === 0) {{ showToast('Select at least one category'); return; }}
642
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'clear_browsing_data', targets: targets }});
720
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
721
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'clear_browsing_data', targets: targets }});
722
+ }}
643
723
  showToast('Browsing data cleared');
644
724
  }}
645
725
  function executeResetDefaults() {{
646
726
  if (confirm("Restore all browser settings to factory defaults?")) {{
647
- window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'reset_defaults' }});
727
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.bridge) {{
728
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'reset_defaults' }});
729
+ }}
648
730
  }}
649
731
  }}
650
732
  function jumpTo(id, navBtn) {{
pybrowser/terminal.py CHANGED
@@ -322,7 +322,7 @@ class TerminalSession:
322
322
  if not self.alive or self.master_fd < 0:
323
323
  return
324
324
  try:
325
- data = text.encode('utf-8')
325
+ data = text.encode('utf-8') if isinstance(text, str) else text
326
326
  total = 0
327
327
  while total < len(data) and self.master_fd >= 0:
328
328
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stupidpythonbrowser
3
- Version: 1.0.1
3
+ Version: 1.0.3
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=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,,
@@ -1,14 +0,0 @@
1
- pybrowser/__init__.py,sha256=aJCcRt702Cy9fTLbE1N3qudmz28OxHG9SgrpPt4wHHY,217
2
- pybrowser/browser.py,sha256=dp_zsN_oHJ0p1b_t-L7eTa7ugIe2mPmE47Bmki8pUg8,154448
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=JCPBQumdJ-fTgBAatp2TVRCY8fIavA12255_eYWeIXI,38817
7
- pybrowser/task_manager.py,sha256=DRTazfw6MAqM360DkiMGIC0oZq6XuMBCpkMgHZAJ-II,15079
8
- pybrowser/terminal.py,sha256=3pXosw9IfpcuPE59lHDpahHMeJq5lVNt-OrRgAIDshc,15124
9
- stupidpythonbrowser-1.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
- stupidpythonbrowser-1.0.1.dist-info/METADATA,sha256=yzzmqRIqveRn2vVhM0bdVLouTr9T4KTsHIniJVilQmk,977
11
- stupidpythonbrowser-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
- stupidpythonbrowser-1.0.1.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
13
- stupidpythonbrowser-1.0.1.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
14
- stupidpythonbrowser-1.0.1.dist-info/RECORD,,