stupidpythonbrowser 1.0.1__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/terminal.py ADDED
@@ -0,0 +1,451 @@
1
+ """
2
+ PyBrowser Embedded Terminal Session Manager.
3
+ Spawns native login shells over pseudo-terminals (PTY) and bridges I/O to WebKit.
4
+ Integrates with PyBrowser's DNS-over-HTTPS (DoH) proxy server for secure network I/O.
5
+ """
6
+
7
+ import os
8
+ import pty
9
+ import select
10
+ import subprocess
11
+ import termios
12
+ import fcntl
13
+ import struct
14
+ import threading
15
+ import base64
16
+ import time
17
+ import signal
18
+ import socket
19
+ import Cocoa
20
+
21
+
22
+ def dispatch_to_main(fn, *args):
23
+ """Executes a function on the macOS main Cocoa run loop."""
24
+ if Cocoa.NSThread.isMainThread():
25
+ fn(*args)
26
+ else:
27
+ Cocoa.NSOperationQueue.mainQueue().addOperationWithBlock_(lambda: fn(*args))
28
+
29
+
30
+ def get_proxy_port():
31
+ """Resolves the local proxy port configured in PyBrowser settings."""
32
+ try:
33
+ from pybrowser.settings import LOCAL_PROXY_PORT
34
+ return int(LOCAL_PROXY_PORT)
35
+ except ImportError:
36
+ try:
37
+ from settings import LOCAL_PROXY_PORT
38
+ return int(LOCAL_PROXY_PORT)
39
+ except ImportError:
40
+ return 8888
41
+
42
+
43
+ def get_data_dir():
44
+ """Resolves the PyBrowser data directory path."""
45
+ try:
46
+ from pybrowser.settings import DATA_DIR
47
+ return DATA_DIR
48
+ except ImportError:
49
+ try:
50
+ from settings import DATA_DIR
51
+ return DATA_DIR
52
+ except ImportError:
53
+ return os.path.expanduser("~/data")
54
+
55
+
56
+ def ensure_doh_proxy_running():
57
+ """Ensures that PyBrowser's local DoH proxy server is running and accepting connections."""
58
+ port = get_proxy_port()
59
+ try:
60
+ with socket.create_connection(("127.0.0.1", port), timeout=0.15):
61
+ return True
62
+ except Exception:
63
+ pass
64
+
65
+ try:
66
+ try:
67
+ from pybrowser.doh_server import (
68
+ start_proxy_server, load_dns_cache, load_adblock_filters_from_disk
69
+ )
70
+ except ImportError:
71
+ from doh_server import (
72
+ start_proxy_server, load_dns_cache, load_adblock_filters_from_disk
73
+ )
74
+
75
+ try:
76
+ load_dns_cache()
77
+ except Exception:
78
+ pass
79
+ try:
80
+ load_adblock_filters_from_disk()
81
+ except Exception:
82
+ pass
83
+
84
+ start_proxy_server()
85
+
86
+ deadline = time.time() + 1.0
87
+ while time.time() < deadline:
88
+ try:
89
+ with socket.create_connection(("127.0.0.1", port), timeout=0.1):
90
+ return True
91
+ except Exception:
92
+ time.sleep(0.05)
93
+ except Exception as e:
94
+ print(f"[Terminal DoH Integration Warning] Could not ensure proxy server: {e}")
95
+ return False
96
+
97
+
98
+ def ensure_doh_cli_helper(bin_dir, proxy_port):
99
+ """Creates CLI helpers inside ~/data/bin for manual DNS inspection and proxy routing."""
100
+ try:
101
+ os.makedirs(bin_dir, exist_ok=True)
102
+ helper_path = os.path.join(bin_dir, "doh-resolve")
103
+ helper_code = f"""#!/usr/bin/env python3
104
+ import sys
105
+ import urllib.request
106
+ import json
107
+ import socket
108
+
109
+ try:
110
+ from pybrowser.doh_server import doh_resolve
111
+ except ImportError:
112
+ try:
113
+ from doh_server import doh_resolve
114
+ except ImportError:
115
+ doh_resolve = None
116
+
117
+ def main():
118
+ if len(sys.argv) < 2:
119
+ print("Usage: doh-resolve <hostname>")
120
+ sys.exit(1)
121
+ hostname = sys.argv[1].strip()
122
+ if doh_resolve:
123
+ try:
124
+ ip = doh_resolve(hostname)
125
+ print(f"{{hostname}} -> {{ip}} (via PyBrowser DoH Engine)")
126
+ return
127
+ except Exception:
128
+ pass
129
+
130
+ try:
131
+ proxy_handler = urllib.request.ProxyHandler({{
132
+ "http": "http://127.0.0.1:{proxy_port}",
133
+ "https": "http://127.0.0.1:{proxy_port}"
134
+ }})
135
+ opener = urllib.request.build_opener(proxy_handler)
136
+ url = "https://cloudflare-dns.com/dns-query?name=" + hostname + "&type=A"
137
+ req = urllib.request.Request(url, headers={{"Accept": "application/dns-json", "User-Agent": "PyBrowser-DoH-CLI"}})
138
+ with opener.open(req, timeout=5) as resp:
139
+ data = json.loads(resp.read().decode('utf-8'))
140
+ answers = [ans.get("data") for ans in data.get("Answer", []) if ans.get("type") == 1]
141
+ if answers:
142
+ for ip in answers:
143
+ print(f"{{hostname}} -> {{ip}} (via PyBrowser DoH Proxy)")
144
+ return
145
+ except Exception:
146
+ pass
147
+
148
+ try:
149
+ ip = socket.gethostbyname(hostname)
150
+ print(f"{{hostname}} -> {{ip}}")
151
+ except Exception as e:
152
+ print(f"Resolution failed for {{hostname}}: {{e}}")
153
+
154
+ if __name__ == "__main__":
155
+ main()
156
+ """
157
+ with open(helper_path, "w", encoding="utf-8") as f:
158
+ f.write(helper_code)
159
+ os.chmod(helper_path, 0o755)
160
+ except Exception:
161
+ pass
162
+
163
+
164
+ class TerminalSession:
165
+ """Non-blocking PTY session streaming raw I/O over Base64 to xterm.js."""
166
+ def __init__(self, webview, cols=80, rows=24, on_close=None):
167
+ self.webview = webview
168
+ self.cols = max(10, int(cols))
169
+ self.rows = max(5, int(rows))
170
+ self.alive = True
171
+ self.master_fd = -1
172
+ self.proc = None
173
+ self.lock = threading.Lock()
174
+ self.on_close = on_close
175
+ self.spawn()
176
+
177
+ def _apply_doh_environment(self, env):
178
+ """Configures shell environment variables to route network requests and DNS via doh_server."""
179
+ port = get_proxy_port()
180
+ proxy_url = f"http://127.0.0.1:{port}"
181
+ no_proxy_hosts = "localhost,127.0.0.1,::1,.local"
182
+
183
+ # Standard HTTP/HTTPS/All proxy environment variables (lowercase and uppercase)
184
+ env["http_proxy"] = proxy_url
185
+ env["https_proxy"] = proxy_url
186
+ env["all_proxy"] = proxy_url
187
+ env["ftp_proxy"] = proxy_url
188
+
189
+ env["HTTP_PROXY"] = proxy_url
190
+ env["HTTPS_PROXY"] = proxy_url
191
+ env["ALL_PROXY"] = proxy_url
192
+ env["FTP_PROXY"] = proxy_url
193
+
194
+ env["no_proxy"] = no_proxy_hosts
195
+ env["NO_PROXY"] = no_proxy_hosts
196
+
197
+ # Git proxy configuration: ensure git clone / fetch always routes through doh_server
198
+ try:
199
+ count = int(env.get("GIT_CONFIG_COUNT", "0"))
200
+ except (ValueError, TypeError):
201
+ count = 0
202
+ has_git_proxy = False
203
+ for i in range(count):
204
+ if env.get(f"GIT_CONFIG_KEY_{i}") == "http.proxy":
205
+ has_git_proxy = True
206
+ break
207
+ if not has_git_proxy:
208
+ env[f"GIT_CONFIG_KEY_{count}"] = "http.proxy"
209
+ env[f"GIT_CONFIG_VALUE_{count}"] = proxy_url
210
+ env["GIT_CONFIG_COUNT"] = str(count + 1)
211
+
212
+ # Git SSH proxy tunnel fallback via netcat through our CONNECT proxy
213
+ if os.path.exists("/usr/bin/nc") and "GIT_SSH_COMMAND" not in env:
214
+ env["GIT_SSH_COMMAND"] = f"ssh -o ProxyCommand='/usr/bin/nc -X connect -x 127.0.0.1:{port} %h %p'"
215
+
216
+ # Package manager & developer CLI proxy overrides
217
+ env["NPM_CONFIG_PROXY"] = proxy_url
218
+ env["NPM_CONFIG_HTTPS_PROXY"] = proxy_url
219
+ env["npm_config_proxy"] = proxy_url
220
+ env["npm_config_https_proxy"] = proxy_url
221
+ env["PIP_PROXY"] = proxy_url
222
+ env["CARGO_HTTP_PROXY"] = proxy_url
223
+
224
+ # Inject PyBrowser CLI tools into PATH
225
+ bin_dir = os.path.join(get_data_dir(), "bin")
226
+ ensure_doh_cli_helper(bin_dir, port)
227
+ cur_path = env.get("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
228
+ if bin_dir not in cur_path:
229
+ env["PATH"] = f"{bin_dir}:{cur_path}"
230
+
231
+ # PyBrowser session indicators
232
+ env["PYBROWSER_DOH_PROXY"] = proxy_url
233
+ env["PYBROWSER_DOH_PORT"] = str(port)
234
+
235
+ def spawn(self):
236
+ try:
237
+ # Ensure DoH proxy server is initialized and listening
238
+ ensure_doh_proxy_running()
239
+
240
+ self.master_fd, slave_fd = pty.openpty()
241
+ winsize = struct.pack("HHHH", int(self.rows), int(self.cols), 0, 0)
242
+ fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize)
243
+
244
+ flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
245
+ fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
246
+
247
+ env = os.environ.copy()
248
+ env["TERM"] = "xterm-256color"
249
+ env["LANG"] = "en_US.UTF-8"
250
+ env["SHELL"] = "/bin/zsh" if os.path.exists("/bin/zsh") else "/bin/bash"
251
+ env["COLUMNS"] = str(self.cols)
252
+ env["LINES"] = str(self.rows)
253
+
254
+ # Apply DoH proxy routing to all commands run in this terminal
255
+ self._apply_doh_environment(env)
256
+
257
+ # Pre-exec setup to establish controlling terminal for SIGWINCH support
258
+ def _preexec_child():
259
+ try:
260
+ if hasattr(os, "login_tty"):
261
+ os.login_tty(slave_fd)
262
+ elif hasattr(pty, "login_tty"):
263
+ pty.login_tty(slave_fd)
264
+ else:
265
+ os.setsid()
266
+ try:
267
+ fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
268
+ except Exception:
269
+ pass
270
+ except Exception:
271
+ pass
272
+
273
+ self.proc = subprocess.Popen(
274
+ [env["SHELL"], "-l"],
275
+ stdin=slave_fd,
276
+ stdout=slave_fd,
277
+ stderr=slave_fd,
278
+ preexec_fn=_preexec_child,
279
+ close_fds=True,
280
+ env=env,
281
+ cwd=os.path.expanduser("~")
282
+ )
283
+ os.close(slave_fd)
284
+ threading.Thread(target=self._read_loop, daemon=True).start()
285
+ except Exception as e:
286
+ print(f"[Terminal Spawn Error] {e}")
287
+
288
+ def _read_loop(self):
289
+ while self.alive and self.master_fd >= 0:
290
+ try:
291
+ r, _, _ = select.select([self.master_fd], [], [], 0.05)
292
+ if r:
293
+ with self.lock:
294
+ if self.master_fd < 0:
295
+ break
296
+ try:
297
+ data = os.read(self.master_fd, 8192)
298
+ except (BlockingIOError, InterruptedError):
299
+ continue
300
+ except OSError:
301
+ break
302
+
303
+ if not data:
304
+ break
305
+ b64_str = base64.b64encode(data).decode('ascii')
306
+ wv = self.webview
307
+ if wv:
308
+ dispatch_to_main(lambda b=b64_str, w=wv: self._flush_to_webview(w, b))
309
+ except (OSError, ValueError):
310
+ break
311
+ self.close()
312
+
313
+ def _flush_to_webview(self, wv, b64_str):
314
+ try:
315
+ wv.evaluateJavaScript_completionHandler_(
316
+ f"if (window.writeToTerm) window.writeToTerm('{b64_str}');", None
317
+ )
318
+ except Exception:
319
+ pass
320
+
321
+ def write(self, text):
322
+ if not self.alive or self.master_fd < 0:
323
+ return
324
+ try:
325
+ data = text.encode('utf-8')
326
+ total = 0
327
+ while total < len(data) and self.master_fd >= 0:
328
+ try:
329
+ with self.lock:
330
+ if self.master_fd < 0:
331
+ break
332
+ n = os.write(self.master_fd, data[total:])
333
+ total += n
334
+ except (BlockingIOError, InterruptedError):
335
+ time.sleep(0.001)
336
+ except OSError:
337
+ break
338
+ except Exception as e:
339
+ print(f"[Terminal Write Error] {e}")
340
+
341
+ def resize(self, cols, rows):
342
+ c = max(10, int(cols))
343
+ r = max(5, int(rows))
344
+ if self.cols == c and self.rows == r:
345
+ return
346
+ self.cols = c
347
+ self.rows = r
348
+ if self.master_fd >= 0 and self.alive:
349
+ try:
350
+ winsize = struct.pack("HHHH", int(self.rows), int(self.cols), 0, 0)
351
+ fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize)
352
+ except Exception as e:
353
+ print(f"[Terminal Resize Error] {e}")
354
+
355
+ # Send SIGWINCH directly to child process group to immediately update vim/less
356
+ if self.proc:
357
+ try:
358
+ pgid = os.getpgid(self.proc.pid)
359
+ os.killpg(pgid, signal.SIGWINCH)
360
+ except Exception:
361
+ pass
362
+
363
+ def close(self):
364
+ if not self.alive and self.master_fd < 0 and self.proc is None:
365
+ return
366
+ self.alive = False
367
+ with self.lock:
368
+ if self.master_fd >= 0:
369
+ try:
370
+ os.close(self.master_fd)
371
+ except Exception:
372
+ pass
373
+ self.master_fd = -1
374
+
375
+ if self.proc:
376
+ try:
377
+ pgid = os.getpgid(self.proc.pid)
378
+ os.killpg(pgid, signal.SIGTERM)
379
+ try:
380
+ self.proc.wait(timeout=0.2)
381
+ except subprocess.TimeoutExpired:
382
+ os.killpg(pgid, signal.SIGKILL)
383
+ self.proc.wait(timeout=0.2)
384
+ except Exception:
385
+ try:
386
+ self.proc.terminate()
387
+ self.proc.wait(timeout=0.2)
388
+ except Exception:
389
+ pass
390
+ self.proc = None
391
+
392
+ self.webview = None
393
+ if self.on_close:
394
+ try:
395
+ self.on_close(self)
396
+ except Exception:
397
+ pass
398
+ self.on_close = None
399
+
400
+
401
+ class TerminalManager:
402
+ def __init__(self):
403
+ self.sessions = {}
404
+ self.lock = threading.Lock()
405
+
406
+ def get_or_create(self, webview, cols=None, rows=None):
407
+ with self.lock:
408
+ session = self.sessions.get(webview)
409
+ if session and session.alive:
410
+ if cols is not None and rows is not None:
411
+ session.resize(cols, rows)
412
+ return session
413
+ if session:
414
+ session.close()
415
+
416
+ c = cols if cols is not None else 80
417
+ r = rows if rows is not None else 24
418
+ session = TerminalSession(
419
+ webview, c, r,
420
+ on_close=lambda s: self._remove_session(webview, s)
421
+ )
422
+ self.sessions[webview] = session
423
+ return session
424
+
425
+ def _remove_session(self, webview, session):
426
+ with self.lock:
427
+ if self.sessions.get(webview) is session:
428
+ self.sessions.pop(webview, None)
429
+
430
+ def write_active(self, webview, text):
431
+ with self.lock:
432
+ session = self.sessions.get(webview)
433
+ if not session or not session.alive:
434
+ session = self.get_or_create(webview)
435
+ session.write(text)
436
+
437
+ def close_for_webview(self, webview):
438
+ with self.lock:
439
+ session = self.sessions.pop(webview, None)
440
+ if session:
441
+ session.close()
442
+
443
+ def close_all(self):
444
+ with self.lock:
445
+ sessions = list(self.sessions.values())
446
+ self.sessions.clear()
447
+ for s in sessions:
448
+ s.close()
449
+
450
+
451
+ TERMINAL_MANAGER = TerminalManager()
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: stupidpythonbrowser
3
+ Version: 1.0.1
4
+ Summary: Native macOS Chromium/Material 3 Web Browser in PyObjC & WebKit
5
+ Author: avevall
6
+ Classifier: Environment :: MacOS X :: Cocoa
7
+ Classifier: Operating System :: MacOS :: MacOS X
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pyobjc-core>=10.0
18
+ Requires-Dist: pyobjc-framework-Cocoa>=10.0
19
+ Requires-Dist: pyobjc-framework-WebKit>=10.0
20
+ Requires-Dist: pyobjc-framework-Network>=10.0
21
+ Requires-Dist: psutil>=5.9.0
22
+ Dynamic: license-file
23
+
24
+ # pythonbrowser
25
+ This is mainly just a personal project, don't take it too seriously and don't expect too much out of it.
@@ -0,0 +1,14 @@
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [gui_scripts]
2
+ pybrowser = pybrowser.main:main