stupidpythonbrowser 1.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pybrowser/__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.2"
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,6 +417,42 @@ 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
458
  if (e.target && !e.target.muted) window.webkit.messageHandlers.audio.postMessage({cmd:'audio_play'});
@@ -841,8 +881,7 @@ class BookmarksBarView(Cocoa.NSView):
841
881
  if not isinstance(b, dict) or not b.get("url"):
842
882
  continue
843
883
  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))
884
+ btn_w = max(50, min(160, len(title) * 7.5 + 26))
846
885
  btn = Cocoa.NSButton.alloc().initWithFrame_(Cocoa.NSMakeRect(x, 2, btn_w, h - 4))
847
886
  btn.setTitle_(title)
848
887
  btn.setBezelStyle_(Cocoa.NSBezelStyleInline)
@@ -850,10 +889,29 @@ class BookmarksBarView(Cocoa.NSView):
850
889
  btn.setTarget_(self)
851
890
  btn.setAction_("onBookmarkClick:")
852
891
  btn.setToolTip_(b.get("url", ""))
892
+
893
+ # Dynamically query and bind favicon
894
+ try:
895
+ host = urllib.parse.urlparse(b.get("url", "")).netloc
896
+ if host:
897
+ FaviconCache.get_favicon(host, lambda img, b_ref=btn: self._apply_button_icon(b_ref, img))
898
+ except Exception:
899
+ pass
900
+
853
901
  self.addSubview_(btn)
854
902
  x += btn_w + 4.0
855
903
  self.setNeedsDisplay_(True)
856
904
 
905
+ def _apply_button_icon(self, btn, img):
906
+ if img and btn:
907
+ try:
908
+ scaled = img.copy()
909
+ scaled.setSize_(Cocoa.NSMakeSize(14, 14))
910
+ btn.setImage_(scaled)
911
+ btn.setImagePosition_(Cocoa.NSImageLeft)
912
+ except Exception:
913
+ pass
914
+
857
915
  def onBookmarkClick_(self, sender):
858
916
  url = sender.toolTip()
859
917
  if url and self.controller:
@@ -968,7 +1026,6 @@ class InternalSchemeHandler(Cocoa.NSObject):
968
1026
  self.active_tasks.discard(task)
969
1027
 
970
1028
  def get_terminal_html(self):
971
- # Fullscreen responsive xterm with ResizeObserver to ensure vim/less utilize entire viewport
972
1029
  bg = "#1e1e2e"
973
1030
  fg = "#c792ea"
974
1031
  return f"""<!DOCTYPE html><html><head><meta charset='utf-8'>
@@ -992,6 +1049,19 @@ const fit = new FitAddon.FitAddon();
992
1049
  term.loadAddon(fit);
993
1050
  term.open(document.getElementById('t'));
994
1051
 
1052
+ // Guarantee Esc (0x1b) is sent directly to child PTY (vim/nano/fzf) without being swallowed
1053
+ term.attachCustomKeyEventHandler(e => {{
1054
+ if (e.key === 'Escape' || e.keyCode === 27) {{
1055
+ if (e.type === 'keydown') {{
1056
+ if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.terminalInput) {{
1057
+ window.webkit.messageHandlers.terminalInput.postMessage('\\x1b');
1058
+ }}
1059
+ }}
1060
+ return false;
1061
+ }}
1062
+ return true;
1063
+ }});
1064
+
995
1065
  const postResize = () => {{
996
1066
  try {{
997
1067
  fit.fit();
@@ -1001,9 +1071,7 @@ const postResize = () => {{
1001
1071
  }} catch(e) {{}}
1002
1072
  }};
1003
1073
 
1004
- const ro = new ResizeObserver(() => {{
1005
- postResize();
1006
- }});
1074
+ const ro = new ResizeObserver(() => {{ postResize(); }});
1007
1075
  ro.observe(document.getElementById('t'));
1008
1076
 
1009
1077
  setTimeout(postResize, 30);
@@ -1038,10 +1106,12 @@ window.writeToTerm = b => {{
1038
1106
  u = b["url"]
1039
1107
  t = b.get("title", u)
1040
1108
  safe_u = json.dumps(u)
1109
+ host = urllib.parse.urlparse(u).netloc
1110
+ fav_tag = f"<img src='https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32' width='16' height='16' style='vertical-align:middle;margin-right:8px;border-radius:3px;' onerror='this.style.display=\"none\"'>" if host else ""
1041
1111
  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>")
1112
+ items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{html.escape(u)}' style='color:#1a73e8;text-decoration:none;'>{html.escape(t)}</a></span> <button onclick='{del_call}' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
1043
1113
  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>"
1114
+ return f"<!DOCTYPE html><html><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title' style='padding:4px 8px;margin-right:8px;'><input id='u' placeholder='https://...' style='padding:4px 8px;margin-right:8px;width:260px;'><button onclick='window.webkit.messageHandlers.bridge.postMessage({{cmd:\"add_bookmark\",title:document.getElementById(\"t\").value,url:document.getElementById(\"u\").value}});setTimeout(()=>location.reload(),100)' style='padding:4px 12px;'>Add</button></div><ul style='list-style:none;padding:0;'>{rows or '<li>No bookmarks</li>'}</ul></body></html>"
1045
1115
 
1046
1116
  def get_history_html(self):
1047
1117
  records = query_history()
@@ -1274,9 +1344,10 @@ class OmniboxPanelCoordinator:
1274
1344
 
1275
1345
  def fetch_remote():
1276
1346
  try:
1277
- url = f"http://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
1347
+ # Secured endpoint via HTTPS to avoid keystroke sniffing
1348
+ url = f"https://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
1278
1349
  req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
1279
- with urllib.request.urlopen(req, timeout=0.8) as resp:
1350
+ with urllib.request.urlopen(req, timeout=1.0) as resp:
1280
1351
  data = json.loads(resp.read().decode('utf-8'))
1281
1352
  remote = [{"type": "search", "text": s, "url": s} for s in data[1][:5]]
1282
1353
  if req_id == self.current_req_id:
@@ -1551,7 +1622,6 @@ class QuickSwitcherController(Cocoa.NSWindowController):
1551
1622
  elif sel == "insertNewline:":
1552
1623
  if self.filtered_tabs and self.selected_index >= 0:
1553
1624
  t = self.filtered_tabs[self.selected_index]
1554
- # Fixed: Resolve current tab index dynamically by ID to avoid desync/crash
1555
1625
  cur_idx = t["wc"].get_tab_index_by_id(t["id"])
1556
1626
  if cur_idx is not None:
1557
1627
  t["wc"].window().makeKeyAndOrderFront_(None)
@@ -1601,7 +1671,6 @@ class PiPPanelController(Cocoa.NSWindowController):
1601
1671
  self.window().makeKeyAndOrderFront_(None)
1602
1672
 
1603
1673
  def exec_js(self, js):
1604
- # Fixed: Prefer active window controller or stored browser controller
1605
1674
  app_delegate = Cocoa.NSApp.delegate()
1606
1675
  wc = None
1607
1676
  key_win = Cocoa.NSApp.keyWindow()
@@ -1770,7 +1839,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1770
1839
  self.home_btn.setHidden_(not SETTINGS.settings.get("show_home_button", False))
1771
1840
 
1772
1841
  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
1842
  self.omnibox_container = Cocoa.NSView.alloc().initWithFrame_(Cocoa.NSMakeRect(omni_x, 3, max(180, w - omni_x - 162), 28))
1775
1843
  self.toolbar.addSubview_(self.omnibox_container)
1776
1844
 
@@ -1798,7 +1866,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1798
1866
  self.star_btn.setAutoresizingMask_(Cocoa.NSViewMinXMargin)
1799
1867
  self.omnibox_container.addSubview_(self.star_btn)
1800
1868
 
1801
- # Fixed: Buttons pushed cleanly to the right edge with uniform 30px spacing
1802
1869
  self.pip_btn = btn("⧉", w - 154, "onTogglePiP:", "Media Controls")
1803
1870
  self.dl_btn = btn("⬇", w - 124, "onShowDownloads:", "Downloads")
1804
1871
  self.share_btn = btn("↗", w - 94, "onShareCurrentPage:", "Share Page")
@@ -1853,7 +1920,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1853
1920
  omni_x = 132 if show_home else 102
1854
1921
  tb_w = self.toolbar.bounds().size.width
1855
1922
 
1856
- # Fixed: Expand Omnibox to fill empty space, eliminated gap between zen and menu buttons
1857
1923
  if hasattr(self, 'omnibox_container'):
1858
1924
  box_w = max(180, tb_w - omni_x - 162)
1859
1925
  self.omnibox_container.setFrame_(Cocoa.NSMakeRect(omni_x, 3, box_w, 28))
@@ -1878,7 +1944,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
1878
1944
  sub.removeFromSuperview()
1879
1945
  w = self.tab_strip.bounds().size.width
1880
1946
  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
1947
  x = 8.0
1883
1948
  tab_h = 28 if not is_vert else 32
1884
1949
  y = self.tab_strip.bounds().size.height - 38 - tab_h if is_vert else 3
@@ -1965,6 +2030,8 @@ class BrowserWindowController(Cocoa.NSWindowController):
1965
2030
  uc.addScriptMessageHandler_name_(self, "terminalInput")
1966
2031
  uc.addScriptMessageHandler_name_(self, "terminalResize")
1967
2032
 
2033
+ # Injected scripts: Command+click capture, audio monitoring, context menus, and fullscreen handlers
2034
+ uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(COMMAND_CLICK_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
1968
2035
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(AUDIO_MONITOR_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
1969
2036
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IMAGE_CONTEXT_MENU_JS, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
1970
2037
  uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(KEYCHAIN_AUTOSAVE_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
@@ -2009,7 +2076,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2009
2076
  except Exception:
2010
2077
  pass
2011
2078
 
2012
- initial_zoom = float(SETTINGS.settings.get("default_zoom", 0.8))
2079
+ initial_zoom = float(SETTINGS.settings.get("default_zoom", 1.0))
2013
2080
  tab = {
2014
2081
  "id": str(uuid.uuid4()), "webview": wv, "url": url, "title": "New Tab",
2015
2082
  "favicon": None, "pinned": pinned, "group_id": None, "zoom": initial_zoom,
@@ -2049,8 +2116,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
2049
2116
  self.wake_tab(index)
2050
2117
  wv = tab["webview"]
2051
2118
 
2052
- # Fixed: Keep webviews attached and toggle visibility with setHidden_
2053
- # This completely preserves DOM focus, text inputs, and render state
2054
2119
  for i, t in enumerate(self.tabs):
2055
2120
  twv = t.get("webview")
2056
2121
  if twv:
@@ -2066,7 +2131,6 @@ class BrowserWindowController(Cocoa.NSWindowController):
2066
2131
  self.update_progress_for_webview(wv)
2067
2132
  self.relayout_tabs()
2068
2133
 
2069
- # Fixed: Restore keyboard focus and text cursor to the active input element automatically
2070
2134
  self.window().makeFirstResponder_(wv)
2071
2135
  wv.evaluateJavaScript_completionHandler_(
2072
2136
  "if (document.activeElement && typeof document.activeElement.focus === 'function') { document.activeElement.focus(); }",
@@ -2258,7 +2322,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2258
2322
  def apply_zoom_to_webview(self, wv, tab=None):
2259
2323
  if not wv:
2260
2324
  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))
2325
+ 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
2326
  target_zoom = max(0.5, min(2.0, target_zoom))
2263
2327
  try:
2264
2328
  if hasattr(wv, "setAllowsMagnification_"): wv.setAllowsMagnification_(False)
@@ -2268,18 +2332,19 @@ class BrowserWindowController(Cocoa.NSWindowController):
2268
2332
  pass
2269
2333
 
2270
2334
  def restore_normal_zoom(self):
2335
+ if self.is_active_tab_terminal():
2336
+ return
2271
2337
  wv = self.get_active_webview()
2272
2338
  if wv:
2273
- self.apply_zoom_to_webview(wv)
2339
+ tab = self.tabs[self.active_tab_index] if (0 <= self.active_tab_index < len(self.tabs)) else None
2340
+ self.apply_zoom_to_webview(wv, tab)
2274
2341
  wv.evaluateJavaScript_completionHandler_(REFLOW_RESTORE_JS, None)
2275
2342
 
2276
2343
  def handle_fullscreen_enter(self, auto_hide_bars=True):
2277
2344
  if auto_hide_bars:
2278
2345
  if not self.tab_bar_visible:
2279
- # Tab was already in Zen mode before entering fullscreen
2280
2346
  self.zen_auto_entered = False
2281
2347
  else:
2282
- # Tab was in normal mode, auto-enter Zen mode for fullscreen
2283
2348
  self.zen_auto_entered = True
2284
2349
  self.tab_bar_visible = False
2285
2350
  self.relayout_views()
@@ -2292,22 +2357,27 @@ class BrowserWindowController(Cocoa.NSWindowController):
2292
2357
  pass
2293
2358
 
2294
2359
  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!
2360
+ if restore_bars and (self.zen_auto_entered or not self.tab_bar_visible):
2361
+ self.tab_bar_visible = True
2362
+ self.zen_auto_entered = False
2363
+ self.relayout_views()
2302
2364
  wv = self.get_active_webview()
2303
2365
  if wv:
2304
2366
  wv.evaluateJavaScript_completionHandler_("if (window.setTerminalFullscreen) window.setTerminalFullscreen(false);", None)
2305
2367
  self.restore_normal_zoom()
2306
2368
 
2369
+ def is_active_tab_terminal(self):
2370
+ if 0 <= self.active_tab_index < len(self.tabs):
2371
+ url = self.tabs[self.active_tab_index].get("url") or ""
2372
+ return "browser://terminal" in url
2373
+ return False
2374
+
2307
2375
  def handle_escape_key(self):
2308
2376
  if self.find_bar and not self.find_bar.isHidden():
2309
2377
  self.find_bar.setHidden_(True)
2310
2378
  return
2379
+ if self.is_active_tab_terminal():
2380
+ return
2311
2381
  wv = self.get_active_webview()
2312
2382
  if not self.tab_bar_visible:
2313
2383
  self.handle_fullscreen_exit(restore_bars=True)
@@ -2339,21 +2409,21 @@ class BrowserWindowController(Cocoa.NSWindowController):
2339
2409
  def onZoomIn_(self, sender=None):
2340
2410
  if 0 <= self.active_tab_index < len(self.tabs):
2341
2411
  tab = self.tabs[self.active_tab_index]
2342
- z = min(2.0, round(tab.get("zoom", 0.8) + 0.1, 2))
2412
+ z = min(2.0, round(tab.get("zoom", 1.0) + 0.1, 2))
2343
2413
  tab["zoom"] = z
2344
2414
  self.apply_zoom_to_webview(tab.get("webview"), tab)
2345
2415
 
2346
2416
  def onZoomOut_(self, sender=None):
2347
2417
  if 0 <= self.active_tab_index < len(self.tabs):
2348
2418
  tab = self.tabs[self.active_tab_index]
2349
- z = max(0.5, round(tab.get("zoom", 0.8) - 0.1, 2))
2419
+ z = max(0.5, round(tab.get("zoom", 1.0) - 0.1, 2))
2350
2420
  tab["zoom"] = z
2351
2421
  self.apply_zoom_to_webview(tab.get("webview"), tab)
2352
2422
 
2353
2423
  def onResetZoom_(self, sender=None):
2354
2424
  if 0 <= self.active_tab_index < len(self.tabs):
2355
2425
  tab = self.tabs[self.active_tab_index]
2356
- tab["zoom"] = float(SETTINGS.settings.get("default_zoom", 0.8))
2426
+ tab["zoom"] = float(SETTINGS.settings.get("default_zoom", 1.0))
2357
2427
  self.apply_zoom_to_webview(tab.get("webview"), tab)
2358
2428
  self.restore_normal_zoom()
2359
2429
 
@@ -2377,7 +2447,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
2377
2447
  menu.addItemWithTitle_action_keyEquivalent_("Bookmark Current Page", "onBookmarkCurrentPage:", "d").setTarget_(self)
2378
2448
  bm_text = "Hide Bookmarks Bar" if SETTINGS.settings.get("bookmarks_bar_visible", True) else "Show Bookmarks Bar"
2379
2449
  menu.addItemWithTitle_action_keyEquivalent_(bm_text, "toggleBookmarksBar:", "b").setTarget_(self)
2380
- menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "").setTarget_(self)
2450
+ zen_item = menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "\x1b")
2451
+ zen_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagOption)
2452
+ zen_item.setTarget_(self)
2381
2453
  menu.addItemWithTitle_action_keyEquivalent_("Find in Page", "onFindInPage:", "f").setTarget_(self)
2382
2454
  menu.addItem_(Cocoa.NSMenuItem.separatorItem())
2383
2455
  menu.addItemWithTitle_action_keyEquivalent_("Settings", "openSettings:", ",").setTarget_(self)
@@ -2629,8 +2701,9 @@ class BrowserWindowController(Cocoa.NSWindowController):
2629
2701
  cmd = body.get("cmd") if isinstance(body, dict) else None
2630
2702
 
2631
2703
  if cmd == "open_url":
2632
- if body.get("url"):
2633
- self.add_tab(body.get("url"))
2704
+ target_url = body.get("url")
2705
+ if target_url:
2706
+ self.add_tab(target_url)
2634
2707
  elif cmd == "toggle_fullscreen_terminal":
2635
2708
  self.toggle_tab_bar()
2636
2709
  elif cmd == "context_target_image":
@@ -2677,10 +2750,12 @@ class BrowserWindowController(Cocoa.NSWindowController):
2677
2750
  wv.evaluateJavaScript_completionHandler_(f"let pw = document.querySelector('input[type=password]'); let user = document.querySelector('input[type=text], input[type=email]'); if(pw) pw.value = '{pwd}'; if(user) user.value = '{saved_user}';", None)
2678
2751
  elif cmd == "save_setting":
2679
2752
  key, val = body.get("key"), body.get("val")
2753
+ val = sanitize_for_json(val)
2680
2754
  if key == "download_path":
2681
2755
  val = os.path.expanduser(str(val).strip())
2682
2756
  SETTINGS.settings[key] = val
2683
2757
  SETTINGS.save()
2758
+
2684
2759
  if key in ("bookmarks_bar_visible", "show_home_button", "vertical_tabs_enabled"):
2685
2760
  self.relayout_views()
2686
2761
  elif key == "theme_mode":
@@ -2693,21 +2768,35 @@ class BrowserWindowController(Cocoa.NSWindowController):
2693
2768
  for sub in c.tab_strip.subviews():
2694
2769
  sub.setNeedsDisplay_(True)
2695
2770
  elif key in ("default_zoom",):
2696
- wv = self.get_active_webview()
2697
- if wv:
2698
- self.apply_zoom_to_webview(wv)
2771
+ new_zoom = float(val)
2772
+ for t in self.tabs:
2773
+ t["zoom"] = new_zoom
2774
+ twv = t.get("webview")
2775
+ if twv:
2776
+ self.apply_zoom_to_webview(twv, t)
2699
2777
  elif key in ("user_agent_mode", "custom_user_agent"):
2700
2778
  ua = SETTINGS.get_user_agent()
2701
2779
  for t in self.tabs:
2702
2780
  if t.get("webview"):
2703
2781
  t["webview"].setCustomUserAgent_(ua)
2782
+ elif key == "js_enabled":
2783
+ js_flag = bool(val)
2784
+ for t in self.tabs:
2785
+ twv = t.get("webview")
2786
+ if twv and hasattr(twv.configuration().preferences(), "setJavaScriptEnabled_"):
2787
+ twv.configuration().preferences().setJavaScriptEnabled_(js_flag)
2788
+ elif key == "block_popups":
2789
+ pop_flag = not bool(val)
2790
+ for t in self.tabs:
2791
+ twv = t.get("webview")
2792
+ if twv and hasattr(twv.configuration().preferences(), "setJavaScriptCanOpenWindowsAutomatically_"):
2793
+ twv.configuration().preferences().setJavaScriptCanOpenWindowsAutomatically_(pop_flag)
2704
2794
  elif key == "doh_provider":
2705
2795
  with DNS_LOCK:
2706
2796
  DNS_CACHE.clear()
2707
2797
  elif key in ("adblock_enabled", "adblock_stevenblack_enabled", "adblock_base_enabled", "custom_adblock_domains"):
2708
2798
  compile_webkit_adblock_rules()
2709
2799
  elif cmd == "choose_download_directory":
2710
- # Fixed: Asynchronous modal sheet targeting caller webview without blocking WebKit IPC
2711
2800
  calling_wv = message.webView()
2712
2801
  panel = Cocoa.NSOpenPanel.openPanel()
2713
2802
  panel.setCanChooseDirectories_(True)
@@ -2718,22 +2807,22 @@ class BrowserWindowController(Cocoa.NSWindowController):
2718
2807
  cur_dir = validate_and_get_download_dir()
2719
2808
  panel.setDirectoryURL_(Foundation.NSURL.fileURLWithPath_(cur_dir))
2720
2809
 
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)
2810
+ res = panel.runModal()
2811
+ if res == Cocoa.NSModalResponseOK and panel.URLs():
2812
+ chosen_path = panel.URLs()[0].path()
2813
+ if os.access(chosen_path, os.W_OK):
2814
+ SETTINGS.settings["download_path"] = chosen_path
2815
+ SETTINGS.save()
2816
+ if calling_wv:
2817
+ path_json = json.dumps(chosen_path)
2818
+ calling_wv.evaluateJavaScript_completionHandler_(
2819
+ f"if (document.getElementById('dl-path-input')) document.getElementById('dl-path-input').value = {path_json};"
2820
+ f"if (document.getElementById('dl-path-display')) document.getElementById('dl-path-display').innerText = {path_json};"
2821
+ "showToast('Download directory updated');", None
2822
+ )
2823
+ else:
2824
+ if calling_wv:
2825
+ calling_wv.evaluateJavaScript_completionHandler_("showToast('Error: Selected directory is not writable');", None)
2737
2826
  elif cmd == "search_blocked_domains":
2738
2827
  matches = search_blocked_domains(body.get("query", ""), limit=100)
2739
2828
  json_res = json.dumps(matches)
@@ -2755,6 +2844,7 @@ class BrowserWindowController(Cocoa.NSWindowController):
2755
2844
  clear_history()
2756
2845
  if "favicons" in targets:
2757
2846
  FaviconCache.clear()
2847
+ self.bookmarks_bar.reload_bookmarks()
2758
2848
  if "dns_cache" in targets:
2759
2849
  with DNS_LOCK:
2760
2850
  DNS_CACHE.clear()
@@ -2764,10 +2854,12 @@ class BrowserWindowController(Cocoa.NSWindowController):
2764
2854
  elif cmd == "reset_defaults":
2765
2855
  SETTINGS.reset_defaults()
2766
2856
  self.relayout_views()
2857
+ def_zoom = float(SETTINGS.settings.get("default_zoom", 1.0))
2767
2858
  for tab in self.tabs:
2859
+ tab["zoom"] = def_zoom
2768
2860
  wv = tab.get("webview")
2769
2861
  if wv:
2770
- self.apply_zoom_to_webview(wv)
2862
+ self.apply_zoom_to_webview(wv, tab)
2771
2863
  wv = self.get_active_webview()
2772
2864
  if wv:
2773
2865
  wv.reload()
@@ -2848,9 +2940,10 @@ class BrowserWindowController(Cocoa.NSWindowController):
2848
2940
 
2849
2941
  def webView_decidePolicyForNavigationAction_decisionHandler_(self, wv, action, handler):
2850
2942
  mods = action.modifierFlags() if hasattr(action, "modifierFlags") else 0
2851
- if bool(mods & Cocoa.NSEventModifierFlagCommand) and action.navigationType() == WebKit.WKNavigationTypeLinkActivated:
2943
+ cmd_pressed = bool(mods & Cocoa.NSEventModifierFlagCommand)
2944
+ if cmd_pressed or action.navigationType() == WebKit.WKNavigationTypeLinkActivated:
2852
2945
  url = action.request().URL().absoluteString() if action.request().URL() else None
2853
- if url:
2946
+ if cmd_pressed and url:
2854
2947
  self.add_tab(url)
2855
2948
  handler(WebKit.WKNavigationActionPolicyCancel)
2856
2949
  return
@@ -3025,25 +3118,45 @@ class AppDelegate(Cocoa.NSObject):
3025
3118
  Cocoa.NSEvent.addLocalMonitorForEventsMatchingMask_handler_(Cocoa.NSEventMaskLeftMouseDown, click_monitor)
3026
3119
 
3027
3120
  def key_monitor(event):
3028
- flags = event.modifierFlags()
3121
+ raw_flags = event.modifierFlags()
3122
+ flags = raw_flags & Cocoa.NSEventModifierFlagDeviceIndependentFlagsMask
3029
3123
  cmd = bool(flags & Cocoa.NSEventModifierFlagCommand)
3030
3124
  shift = bool(flags & Cocoa.NSEventModifierFlagShift)
3031
3125
  opt = bool(flags & Cocoa.NSEventModifierFlagOption)
3032
3126
  key = event.keyCode()
3033
3127
  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
3128
 
3037
- if key == 53: # Escape
3129
+ win = event.window() or Cocoa.NSApp.keyWindow() or Cocoa.NSApp.mainWindow()
3130
+ wc = None
3131
+ if win and isinstance(win.delegate(), BrowserWindowController):
3132
+ wc = win.delegate()
3133
+ elif hasattr(self, 'controllers') and self.controllers:
3134
+ for c in self.controllers:
3135
+ if c.window() == win:
3136
+ wc = c
3137
+ break
3138
+ if not wc and self.controllers:
3139
+ wc = self.controllers[0]
3140
+
3141
+ if key == 53: # Escape key
3038
3142
  if shift and not cmd and not opt:
3039
3143
  self.process_manager.show()
3040
3144
  return None
3041
- if opt and wc:
3145
+ if opt and not cmd and not shift and wc:
3042
3146
  wc.toggle_tab_bar()
3043
3147
  return None
3044
3148
  if wc:
3149
+ # Allow Escape to pass directly into terminal tabs (vim, nano, fzf, less)
3150
+ if wc.is_active_tab_terminal():
3151
+ return event
3152
+ if wc.find_bar and not wc.find_bar.isHidden():
3153
+ wc.find_bar.setHidden_(True)
3154
+ return None
3155
+ if not wc.tab_bar_visible:
3156
+ wc.toggle_tab_bar()
3157
+ return None
3045
3158
  wc.handle_escape_key()
3046
- return None
3159
+ return event
3047
3160
 
3048
3161
  if cmd and shift and not opt:
3049
3162
  if key == 0 or chars == 'a':
@@ -3143,8 +3256,10 @@ class AppDelegate(Cocoa.NSObject):
3143
3256
  view_menu.addItemWithTitle_action_keyEquivalent_("Reload Page", "onReload:", "r")
3144
3257
  view_menu.addItemWithTitle_action_keyEquivalent_("Focus Omnibox", "onFocusOmnibox:", "l")
3145
3258
  view_menu.addItemWithTitle_action_keyEquivalent_("Picture-in-Picture", "onTogglePiP:", "p")
3259
+ zen_item = view_menu.addItemWithTitle_action_keyEquivalent_("Toggle Zen Mode", "onToggleZenMode:", "\x1b")
3260
+ zen_item.setKeyEquivalentModifierMask_(Cocoa.NSEventModifierFlagOption)
3146
3261
  view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
3147
- view_menu.addItemWithTitle_action_keyEquivalent_("Zoom In", "onZoomIn:", "+")
3262
+ view_menu.addItemWithTitle_action_keyEquivalent_("Zoom In", "onZoomIn:", "=")
3148
3263
  view_menu.addItemWithTitle_action_keyEquivalent_("Zoom Out", "onZoomOut:", "-")
3149
3264
  view_menu.addItemWithTitle_action_keyEquivalent_("Actual Size", "onResetZoom:", "0")
3150
3265
  view_menu.addItem_(Cocoa.NSMenuItem.separatorItem())
pybrowser/settings.py CHANGED
@@ -80,6 +80,38 @@ 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
+ # Check booleans explicitly first because bool subclasses int in Python
91
+ type_name = type(obj).__name__
92
+ if isinstance(obj, bool) or "Boolean" in type_name or type_name == "bool":
93
+ return bool(obj)
94
+ if isinstance(obj, (int, float)):
95
+ return obj
96
+ if isinstance(obj, (str, bytes)):
97
+ return str(obj)
98
+ if hasattr(obj, "items") or isinstance(obj, dict):
99
+ return {str(k): sanitize_for_json(v) for k, v in obj.items()}
100
+ if hasattr(obj, "__iter__") or isinstance(obj, (list, tuple, set)):
101
+ return [sanitize_for_json(i) for i in obj]
102
+ try:
103
+ # Number conversion for PyObjC NSNumber
104
+ if hasattr(obj, "boolValue") and ("Boolean" in type_name or "bool" in str(type(obj)).lower()):
105
+ return bool(obj.boolValue())
106
+ if hasattr(obj, "doubleValue"):
107
+ val = obj.doubleValue()
108
+ return int(val) if val.is_integer() else val
109
+ if hasattr(obj, "stringValue"):
110
+ return str(obj.stringValue())
111
+ except Exception:
112
+ pass
113
+ return str(obj)
114
+
83
115
  def validate_and_get_download_dir(target_path=None):
84
116
  if not target_path:
85
117
  target_path = SETTINGS.settings.get("download_path", os.path.expanduser("~/Downloads"))
@@ -130,7 +162,7 @@ class SettingsManager:
130
162
  "vertical_tabs_collapsed": False,
131
163
  "show_home_button": False,
132
164
  "home_url": "https://www.google.com",
133
- "default_zoom": 0.8,
165
+ "default_zoom": 1.0,
134
166
  "doh_provider": "cloudflare",
135
167
  "doh_custom_url": "",
136
168
  "custom_adblock_domains": "",
@@ -158,18 +190,20 @@ class SettingsManager:
158
190
  except Exception as e:
159
191
  print(f"[Settings Load Warning] {e}")
160
192
  try:
161
- z = float(self.settings.get("default_zoom", 0.8))
193
+ z = float(self.settings.get("default_zoom", 1.0))
162
194
  self.settings["default_zoom"] = max(0.5, min(2.0, z))
163
195
  except Exception:
164
- self.settings["default_zoom"] = 0.8
196
+ self.settings["default_zoom"] = 1.0
165
197
  self.save()
166
198
 
167
199
  def save(self):
168
200
  try:
169
201
  dir_name = os.path.dirname(SETTINGS_FILE) or "."
202
+ os.makedirs(dir_name, exist_ok=True)
203
+ sanitized = sanitize_for_json(self.settings)
170
204
  fd, temp_path = tempfile.mkstemp(dir=dir_name)
171
205
  with os.fdopen(fd, 'w', encoding='utf-8') as f:
172
- json.dump(self.settings, f, indent=2)
206
+ json.dump(sanitized, f, indent=2)
173
207
  os.replace(temp_path, SETTINGS_FILE)
174
208
  except Exception as e:
175
209
  print(f"[Settings Save Error] {e}")
@@ -199,7 +233,7 @@ class SettingsManager:
199
233
  domains = set()
200
234
  custom = self.settings.get("custom_adblock_domains", "")
201
235
  if custom:
202
- for d in re.split(r'[\r\n,]+', custom):
236
+ for d in re.split(r'[\r\n,]+', str(custom)):
203
237
  d = d.strip().lower()
204
238
  if d:
205
239
  domains.add(d)
@@ -226,7 +260,7 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
226
260
  tab_action = s.get("tab_bar_double_click_action", "maximize")
227
261
  downloads_enabled = s.get("downloads_enabled", True)
228
262
  cur_dl_path = s.get("download_path", os.path.expanduser("~/Downloads"))
229
- cur_zoom = str(s.get("default_zoom", 1.0))
263
+ cur_zoom = f"{float(s.get('default_zoom', 1.0)):.2f}"
230
264
 
231
265
  return f"""<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Settings</title>
232
266
  <style>
@@ -296,7 +330,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
296
330
  </div>
297
331
  <div id="custom-search-row" class="row" style="display: {'flex' if engine_preset == 'custom' else 'none'};">
298
332
  <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')">
333
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(custom_search_url)}"
334
+ onchange="saveSetting('custom_search_url', this.value, 'Custom search URL saved')"
335
+ onkeydown="if(event.key==='Enter') this.blur();">
300
336
  </div>
301
337
  <div class="row">
302
338
  <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 +363,13 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
327
363
  </select>
328
364
  </div>
329
365
  <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>
366
+ <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
367
  <select class="m3-select" onchange="saveSetting('default_zoom', parseFloat(this.value), 'Zoom level updated')">
332
368
  <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>
369
+ <option value="0.80" {'selected' if cur_zoom == '0.80' else ''}>80%</option>
370
+ <option value="1.00" {'selected' if cur_zoom in ('1.00', '1.0', '1') else ''}>100% (Default)</option>
335
371
  <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>
372
+ <option value="1.50" {'selected' if cur_zoom == '1.50' else ''}>150%</option>
337
373
  </select>
338
374
  </div>
339
375
  <div class="row">
@@ -359,7 +395,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
359
395
  </div>
360
396
  <div class="row">
361
397
  <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')">
398
+ <input type="text" class="m3-input" style="width:260px;" value="{html.escape(s.get('home_url', 'https://www.google.com'))}"
399
+ onchange="saveSetting('home_url', this.value, 'Home URL saved')"
400
+ onkeydown="if(event.key==='Enter') this.blur();">
363
401
  </div>
364
402
  </div>
365
403
  </div>
@@ -395,6 +433,13 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
395
433
  <span class="slider"></span>
396
434
  </label>
397
435
  </div>
436
+ <div class="row">
437
+ <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>
438
+ <label class="switch">
439
+ <input type="checkbox" {'checked' if s.get('block_popups', True) else ''} onchange="saveSetting('block_popups', this.checked, 'Popup blocker toggled')">
440
+ <span class="slider"></span>
441
+ </label>
442
+ </div>
398
443
  <div class="row-col">
399
444
  <div class="label-group" style="max-width: 100%;">
400
445
  <div class="label-title">Rule & Domain Inspector</div>
@@ -426,7 +471,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
426
471
  </div>
427
472
  <div id="custom-doh-row" class="row" style="display: {'flex' if doh_provider == 'custom' else 'none'};">
428
473
  <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')">
474
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(doh_custom_url)}" placeholder="https://..."
475
+ onchange="saveSetting('doh_custom_url', this.value, 'Custom DoH URL saved')"
476
+ onkeydown="if(event.key==='Enter') this.blur();">
430
477
  </div>
431
478
  <div class="row">
432
479
  <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 +491,8 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
444
491
  <div class="label-title">Custom Blocked Domains</div>
445
492
  <div class="label-desc">Add additional domains to block (comma- or line-separated)</div>
446
493
  </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>
494
+ <textarea class="m3-input" style="width:100%; height:75px; resize:vertical;" placeholder="adserver.com&#10;tracker.telemetry.io"
495
+ onchange="saveSetting('custom_adblock_domains', this.value, 'Custom domains saved')">{html.escape(s.get('custom_adblock_domains', ''))}</textarea>
448
496
  </div>
449
497
  </div>
450
498
  </div>
@@ -462,7 +510,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
462
510
  </div>
463
511
  <div id="custom-startup-row" class="row" style="display: {'flex' if startup_behavior == 'custom_url' else 'none'};">
464
512
  <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')">
513
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('startup_url', 'https://www.google.com'))}"
514
+ onchange="saveSetting('startup_url', this.value, 'Startup URL saved')"
515
+ onkeydown="if(event.key==='Enter') this.blur();">
466
516
  </div>
467
517
  <div class="row">
468
518
  <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 +550,8 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
500
550
  </div>
501
551
  <div style="display:flex; gap:8px; align-items:center;">
502
552
  <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')">
553
+ onchange="onDownloadInputChange(this.value)"
554
+ onkeydown="if(event.key==='Enter') {{ onDownloadInputChange(this.value); this.blur(); }}">
505
555
  <button class="btn" onclick="browseDownloadFolder()">Browse…</button>
506
556
  </div>
507
557
  </div>
@@ -523,7 +573,9 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
523
573
  </div>
524
574
  <div id="custom-ua-row" class="row" style="display: {'flex' if ua_mode == 'custom' else 'none'};">
525
575
  <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')">
576
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('custom_user_agent', ''))}"
577
+ onchange="saveSetting('custom_user_agent', this.value, 'Custom User-Agent saved')"
578
+ onkeydown="if(event.key==='Enter') this.blur();">
527
579
  </div>
528
580
  <div class="row">
529
581
  <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>
@@ -586,6 +638,13 @@ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
586
638
  window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
587
639
  showToast(msg);
588
640
  }}
641
+ function onDownloadInputChange(val) {{
642
+ const trimmed = (val || '').trim();
643
+ if (!trimmed) return;
644
+ saveSetting('download_path', trimmed, 'Download path saved');
645
+ const disp = document.getElementById('dl-path-display');
646
+ if (disp) disp.innerText = trimmed;
647
+ }}
589
648
  function onSearchPresetChange(val) {{
590
649
  saveSetting('search_engine_preset', val, 'Search engine updated');
591
650
  document.getElementById('custom-search-row').style.display = (val === 'custom') ? 'flex' : 'none';
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.2
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=GVtXkxt_j9dwHYFRqGEWp4HzMX2VTp6HQAPyt8lqYAY,218
2
+ pybrowser/browser.py,sha256=MhhhLRxGZjUiVg-BAaGBwAZas6tK2f-QZI2WLxvt81s,159687
3
+ pybrowser/doh_server.py,sha256=uLlyu5wvn0cXSr2aDKa2EjOlXN4Mhzz81WM53jSA0s4,15253
4
+ pybrowser/incognito.py,sha256=GEc-xeVkElsDPzFIVLhTgSXtDHK3whTzxdnz7wYVJ0o,1675
5
+ pybrowser/main.py,sha256=DwtxNLZ0PrhH0RPUn3UzPFSHQgHcnO7AHriElxxOP2A,2332
6
+ pybrowser/settings.py,sha256=1b1iaZ-DJImRVzDtQH8QFYjiBBDZmPqMnrOr8sG3L5E,41304
7
+ pybrowser/task_manager.py,sha256=DRTazfw6MAqM360DkiMGIC0oZq6XuMBCpkMgHZAJ-II,15079
8
+ pybrowser/terminal.py,sha256=vdu64F3DRf_3KlU_BVzfllPUhb6dGRFjhVqoVr5Xetw,15159
9
+ stupidpythonbrowser-1.0.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
+ stupidpythonbrowser-1.0.2.dist-info/METADATA,sha256=xOLz3ZmY8CofHyqRAbgEFhxoDa54sDzBHp4wKrJWoJc,977
11
+ stupidpythonbrowser-1.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ stupidpythonbrowser-1.0.2.dist-info/entry_points.txt,sha256=ntsWkCj6QvxpckUUie3agZV8YJQdV5tQMzYfPK4NopM,46
13
+ stupidpythonbrowser-1.0.2.dist-info/top_level.txt,sha256=-Qio7UVGVcEO-lZkeLbyefDgD31D6lsx3CxPpKMipKE,10
14
+ stupidpythonbrowser-1.0.2.dist-info/RECORD,,
@@ -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,,