From aff10288402db5d18678bb25aca2c3c8f21bc4a4 Mon Sep 17 00:00:00 2001 From: pmp-p Date: Sat, 2 Dec 2023 03:38:16 +0100 Subject: [PATCH] add safarie6 detection fix --- pygbag/__main__.py | 28 ++--- pygbag/aio.py | 31 ++--- pygbag/support/cross/__EMSCRIPTEN__.py | 4 +- pygbag/support/cross/aio/pep0723.py | 41 +++--- pygbag/support/cross/aio/toplevel.py | 168 ++++++++++++++----------- pygbag/support/pygbag_tools.py | 35 +++--- pygbag/support/pygbag_ux.py | 25 ++++ pygbag/support/pythonrc.py | 48 ++++--- static/pythons.js | 51 ++++++-- static/vtx.js | 45 +++++-- 10 files changed, 290 insertions(+), 186 deletions(-) diff --git a/pygbag/__main__.py b/pygbag/__main__.py index 6869b12..657315d 100644 --- a/pygbag/__main__.py +++ b/pygbag/__main__.py @@ -93,17 +93,31 @@ def __repr__(self): __str__ = __repr__ + class window_navigator: + userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" + # fake host document.window import platform as fakehost + def truc(*argv, **kw): print("truc", argv, kw) + def prompt(): + # FIXME for js style console should be + # CSI(f"{TTY.LINES+TTY.CONSOLE};1H{prompt}") + print('\r>>> ',end='') + + fakehost.is_browser = False + fakehost.async_input = async_input fakehost.window = NoOp("platform.window") fakehost.window.console = NoOp("platform.window.console") fakehost.window.console.log = print fakehost.window.get_terminal_console = truc fakehost.window.RAW_MODE = 0 + fakehost.window.navigator = window_navigator + fakehost.readline = lambda :"" + fakehost.prompt = prompt def set_raw_mode(mode): import platform as fakehost @@ -222,20 +236,6 @@ def eval(self, source): self.buffer.insert(0, "#") print(f"178: {count} lines queued for async eval") - async def input_console(self, prompt=">>> "): - if len(self.buffer): - return self.buffer.pop(0) - - # if program wants I/O do not empty buffers - if self.shell.is_interactive: - if async_input: - maybe = await async_input() - else: - maybe = "" - - if len(maybe): - return maybe - return None async def async_get_pkg(cls, want, ex, resume): print( diff --git a/pygbag/aio.py b/pygbag/aio.py index b3b0c71..fd8f050 100644 --- a/pygbag/aio.py +++ b/pygbag/aio.py @@ -12,20 +12,20 @@ from pathlib import Path try: - import aioconsole, aiohttp + #import aioconsole, aiohttp + import aiohttp except Exception as e: print(e,f""" -pygbag simulator rely on both aioconsole and aiohttp +pygbag simulator rely on : aiohttp asyncio_socks_server token_util please use : - {sys.executable} -m pip install aioconsole aiohttp asyncio_socks_server token_util + {sys.executable} -m pip install aiohttp asyncio_socks_server token_util """) raise SystemExit - import aio.pep0723 if aio.pep0723.Config.dev_mode: @@ -35,20 +35,21 @@ import pygbag.__main__ - - async def custom_async_input(): - import platform - if platform.window.RAW_MODE: -# TODO: FIXME: implement embed.os_read and debug focus handler - #return await asyncio.sleep(0) - ... - return await aioconsole.ainput("››› ") +# +# async def custom_async_input(): +# import platform +# if platform.window.RAW_MODE: +## TODO: FIXME: implement embed.os_read and debug focus handler +# #return await asyncio.sleep(0) +# ... +# return await aioconsole.ainput("››› ") aio.loop.create_task( pygbag.__main__.import_site( sourcefile=sys.argv[0], simulator=True, - async_input=custom_async_input, +# async_input=custom_async_input, + async_input=None, async_pkg=aio.pep0723.check_list(filename=sys.argv[0]), ) ) @@ -61,7 +62,7 @@ async def custom_async_input(): aio.started = True while not aio.exit: - next = time.time() + 0.016 + next = time.time() + 0.0155 try: aio.loop._run_once() except KeyboardInterrupt: @@ -72,7 +73,7 @@ async def custom_async_input(): past = int(-dt * 1000) # do not spam for <4 ms late (50Hz vs 60Hz) if past > 4: - print(f"aio: violation frame is {past} ms late") + print(f"\raio: violation frame is {past} ms late") # too late do not sleep at all else: time.sleep(dt) diff --git a/pygbag/support/cross/__EMSCRIPTEN__.py b/pygbag/support/cross/__EMSCRIPTEN__.py index 6f64a07..d6e1670 100644 --- a/pygbag/support/cross/__EMSCRIPTEN__.py +++ b/pygbag/support/cross/__EMSCRIPTEN__.py @@ -340,7 +340,7 @@ def fix_preload_table_apk(): aio.defer(execfile, [f"{ROOTDIR}/{loadermain}"], {}) else: pdb(f"no {loadermain} found for {sys.argv[0]} in {ROOTDIR}") - aio.defer(embed.prompt, (), {}, delay=2000) + #aio.defer(embed.prompt, (), {}, delay=2000) # C should unlock aio loop when preload count reach 0. @@ -350,7 +350,7 @@ def fix_preload_table_apk(): global fix_preload_table_apk, ROOTDIR pdb("no assets preloaded") os.chdir(ROOTDIR) - aio.defer(embed.prompt, (), {}) + #aio.defer(embed.prompt, (), {}) # unlock embed looper because no preloading embed.run() diff --git a/pygbag/support/cross/aio/pep0723.py b/pygbag/support/cross/aio/pep0723.py index fa816b1..41fe50a 100644 --- a/pygbag/support/cross/aio/pep0723.py +++ b/pygbag/support/cross/aio/pep0723.py @@ -63,8 +63,8 @@ class Config: mapping = { "pygame": "pygame.base", - "pygame-ce": "pygame.base", - "python-i18n" : "i18n", + "pygame_ce": "pygame.base", + "python_i18n" : "i18n", "pillow" : "PIL", } @@ -246,6 +246,19 @@ async def install_pkg(sysconf, wheel_url, wheel_pkg): target.write(pkg.read()) install(target_filename, sysconf) +def do_patches(): + global PATCHLIST + # apply any patches + while len(PATCHLIST): + dep = PATCHLIST.pop(0) + print(f"254: patching {dep}") + try: + import platform + platform.patches.pop(dep)() + except Exception as e: + sys.print_exception(e) + + # FIXME: HISTORY and invalidate caches async def pip_install(pkg, sysconf={}): global sconf @@ -264,7 +277,11 @@ async def pip_install(pkg, sysconf={}): pkg = Config.mapping[pkg.lower()] if pkg in HISTORY: return - print("228: package renamed to", pkg) + print("279: package renamed to", pkg) + + if pkg in platform.patches: + if not pkg in PATCHLIST: + PATCHLIST.append(pkg) for repo in Config.pkg_repolist: if pkg in repo: @@ -280,13 +297,13 @@ async def pip_install(pkg, sysconf={}): if line.find("-py3-none-any.whl") > 0: wheel_url = line.split('"', 2)[1] else: - print("270: ERROR: cannot find package :", pkg) + print("283: ERROR: cannot find package :", pkg) except FileNotFoundError: - print("200: ERROR: cannot find package :", pkg) + print("285: ERROR: cannot find package :", pkg) return except: - print("204: ERROR: cannot find package :", pkg) + print("289: ERROR: cannot find package :", pkg) return if wheel_url: @@ -296,7 +313,7 @@ async def pip_install(pkg, sysconf={}): if pkg not in HISTORY: HISTORY.append(pkg) except: - print("212: INVALID", pkg, "from", wheel_url) + print("299: INVALID", pkg, "from", wheel_url) PYGAME=0 async def parse_code(code, env): @@ -418,15 +435,7 @@ async def check_list(code=None, filename=None): platform.explore(sconf["platlib"]) await asyncio.sleep(0) - # apply any patches - while len(PATCHLIST): - dep = PATCHLIST.pop(0) - print(f"314: patching {dep}") - try: - import platform - platform.patches.pop(dep)() - except Exception as e: - sys.print_exception(e) + do_patches() print("-" * 40) print() diff --git a/pygbag/support/cross/aio/toplevel.py b/pygbag/support/cross/aio/toplevel.py index dce4911..6b5cd59 100644 --- a/pygbag/support/cross/aio/toplevel.py +++ b/pygbag/support/cross/aio/toplevel.py @@ -1,4 +1,5 @@ import sys +import os import aio # https://bugs.python.org/issue34616 @@ -46,11 +47,20 @@ def parse_sync(shell, line, **env): print("NoOp shell", line) self.shell = shell + self.rv = None - # need to subclass - # @staticmethod - # def get_pkg(want, ex=None, resume=None): + try: + sys.ps1 + except AttributeError: + sys.ps1 = ">>> " + + try: + sys.ps2 + except AttributeError: + sys.ps2 = "--- " + + def runsource(self, source, filename="", symbol="single"): if len(self.buffer) > 1: @@ -123,97 +133,101 @@ def banner(self): self.write("\nPython %s on %s\n%s\n" % (sys.version, sys.platform, cprt)) - def prompt(self): + def prompt(self, prompt=None): if not self.__class__.muted and self.shell.is_interactive: - if embed: - embed.prompt() + import platform + # that is the browser one + #platform.prompt(prompt or sys.ps1) + embed.prompt() - async def interact(self): + async def input_console(self, prompt=">>> "): + if len(self.buffer): + return self.buffer.pop(0) - # in raw mode we don't want that loop to read input - import sys - from platform import window + # if program wants I/O do not empty buffers + if self.shell.is_interactive: + if not aio.cross.simulator: + maybe = embed.readline() + elif aio.async_input: + maybe = await aio.async_input() + else: + maybe = "" - if sys.platform in ('emscripten','wasi') and not aio.cross.simulator: - raw_mix = True - else: - raw_mix = False + if len(maybe): + return maybe + return None - # multiline input clumsy sentinel - last_line = "" - try: - sys.ps1 - except AttributeError: - sys.ps1 = ">>> " + # can be used to mix console and app + async def interact_step(self, prompt=None): - try: - sys.ps2 - except AttributeError: - sys.ps2 = "--- " + if aio.exit: + return - prompt = sys.ps1 + if prompt is None: + prompt = sys.ps1 - while not aio.exit: - await asyncio.sleep(0) - #if raw_mix: - if window.RAW_MODE: - continue + try: + try: + self.line = await self.input_console(prompt) + if self.line is None: + return - if aio.exit: + except EOFError: + self.write("\n") return - - try: - try: - self.line = await self.input_console(prompt) - if self.line is None: - continue - - except EOFError: - self.write("\n") - break - else: - if self.push(self.line): - if self.one_liner: - prompt = sys.ps2 - if embed: - embed.set_ps2() - print("Sorry, multi line input editing is not supported", file=sys.stderr) - self.one_liner = False - self.resetbuffer() - else: - continue + else: + if self.push(self.line): + if self.one_liner: + prompt = sys.ps2 + if embed: + embed.set_ps2() + print("Sorry, multi line input editing is not supported", file=sys.stderr) + self.one_liner = False + self.resetbuffer() else: - prompt = sys.ps1 - - except KeyboardInterrupt: - self.write("\nKeyboardInterrupt\n") - self.resetbuffer() - self.one_liner = True + return + else: + prompt = sys.ps1 - if aio.exit: - return + except KeyboardInterrupt: + self.write("\nKeyboardInterrupt\n") + self.resetbuffer() + self.one_liner = True - try: - # if async prepare is required - while len(self.shell.coro): - self.rv = await self.shell.coro.pop(0) - - # if self.rv not in [undefined, None, False, True]: - if inspect.isawaitable(self.rv): - await self.rv - except RuntimeError as re: - if str(re).endswith("awaited coroutine"): - ... - else: - sys.print_exception(ex) + if aio.exit: + return - except Exception as ex: - print(type(self.rv), self.rv) + try: + # if async prepare is required + while len(self.shell.coro): + self.rv = await self.shell.coro.pop(0) + + # if self.rv not in [undefined, None, False, True]: + if inspect.isawaitable(self.rv): + await self.rv + except RuntimeError as re: + if str(re).endswith("awaited coroutine"): + ... + else: sys.print_exception(ex) - self.prompt() + except Exception as ex: + print(type(self.rv), self.rv) + sys.print_exception(ex) + self.prompt() + + async def interact(self): + # in repl+raw mode we don't want that loop to read input + import sys + from platform import window + + while not aio.exit: + await asyncio.sleep(0) + if window.RAW_MODE: + continue + await self.interact_step(sys.ps1) aio.exit_now(0) @classmethod @@ -222,6 +236,8 @@ def make_instance(cls, shell, ns="__main__"): vars(__import__(ns)), shell=shell, ) + import platform + platform.shell = shell shell.runner = cls.instance del AsyncInteractiveConsole.make_instance diff --git a/pygbag/support/pygbag_tools.py b/pygbag/support/pygbag_tools.py index 472896a..9adf411 100644 --- a/pygbag/support/pygbag_tools.py +++ b/pygbag/support/pygbag_tools.py @@ -2,22 +2,25 @@ import pygbag from pathlib import Path -# ====================== pygame - -def pg_load(fn, alpha=True): - import pygame - from pathlib import Path - - if Path(fn).is_file(): - media = pygame.image.load(fn) - else: - media = pygame.image.load( Path(__file__).parent / "offline.png" ) - - if alpha: - return media.convert_alpha() - else: - return media.convert() - +## ====================== pygame + +#def pg_load(fn, alpha=True): +# import pygame +# from pathlib import Path + +# if Path(fn).is_file(): +# media = pygame.image.load(fn) +# else: +# media = pygame.image.load( Path(__file__).parent / "offline.png" ) + +# try: +# if alpha: +# return media.convert_alpha() +# else: +# return media.convert() +# # offscreen case +# except: +# return media #======================= network diff --git a/pygbag/support/pygbag_ux.py b/pygbag/support/pygbag_ux.py index 69a5ce7..1ce3d5b 100644 --- a/pygbag/support/pygbag_ux.py +++ b/pygbag/support/pygbag_ux.py @@ -45,3 +45,28 @@ def ur(*argv): ret.append(w) ret.append(h) return ret + + + +from pathlib import Path + +# ====================== pygame + +def pg_load(fn, alpha=True): + import pygame + from pathlib import Path + + if Path(fn).is_file(): + media = pygame.image.load(fn) + else: + media = pygame.image.load( Path(__file__).parent / "offline.png" ) + + try: + if alpha: + return media.convert_alpha() + else: + return media.convert() + # offscreen case + except: + return media + diff --git a/pygbag/support/pythonrc.py b/pygbag/support/pythonrc.py index 0f34f8c..10773d7 100644 --- a/pygbag/support/pythonrc.py +++ b/pygbag/support/pythonrc.py @@ -593,7 +593,7 @@ def stop(cls, *argv, **env): if not cls.pgzrunning: # pgzrun does its own cleanup call aio.defer(aio.recycle.cleanup, (), {}, delay=500) - aio.defer(embed.prompt, (), {}, delay=800) + aio.defer(platform.prompt, (), {}, delay=800) @classmethod def uptime(cls, *argv, **env): @@ -666,14 +666,16 @@ async def preload_code(cls, code, callback=None, hint=""): for dep in deps: await aio.pep0723.pip_install(dep) + aio.pep0723.do_patches() + PyConfig.imports_ready = True return True @classmethod def interactive(cls, prompt=False): if prompt: - TopLevel_async_handler.mute_state = False - TopLevel_async_handler.muted = False + aio.toplevel.handler.mute_state = False + aio.toplevel.handler.muted = False if cls.is_interactive: return @@ -682,13 +684,15 @@ def interactive(cls, prompt=False): DBG("651: starting EventTarget in a few seconds") print() - TopLevel_async_handler.instance.banner() + aio.toplevel.handler.instance.banner() aio.create_task(platform.EventTarget.process()) cls.is_interactive = True if not shell.pgzrunning: - del __import__("__main__").__file__ + # __main__@stdin has no __file__ + if hasattr( __import__("__main__") , "__file__"): + del __import__("__main__").__file__ if prompt: cls.runner.prompt() else: @@ -702,11 +706,11 @@ def check_code(file_name): has_pygame = False with open(file_name, "r") as code_file: code = code_file.read() - code = code.rsplit(TopLevel_async_handler.HTML_MARK, 1)[0] + code = code.rsplit(aio.toplevel.handler.HTML_MARK, 1)[0] # do not check site/final/packed code # preload code must be fully async and no pgzero based - if TopLevel_async_handler.muted: + if aio.toplevel.handler.muted: return True if code[0:320].find("#!pgzrun") >= 0: @@ -725,6 +729,7 @@ def check_code(file_name): code = "" shell.pgzrunning = None + DBG(f"690: : runpy({main=})") # REMOVE THAT IT SHOULD BE DONE IN SIM ANALYSER AND HANDLED PROPERLY if not check_code(main): @@ -747,13 +752,13 @@ def check_code(file_name): await cls.preload_code(code, **kw) # get an async executor to catch import errors - if TopLevel_async_handler.instance: + if aio.toplevel.handler.instance: DBG("715: starting shell") - TopLevel_async_handler.instance.start_console(shell) + aio.toplevel.handler.instance.start_console(shell) else: pdb("718: no async handler loader, starting a default async console") shell.debug() - await TopLevel_async_handler.start_toplevel(platform.shell, console=True) + await aio.toplevel.handler.start_toplevel(platform.shell, console=True) # TODO: check if that thing really works if shell.pgzrunning: @@ -765,15 +770,17 @@ def check_code(file_name): pgzrun.go = lambda: None cb = kw.pop("callback", None) - await TopLevel_async_handler.async_imports(cb, "pygame.base", "pgzero", "pyfxr", **kw) + await aio.toplevel.handler.async_imports(cb, "pygame.base", "pgzero", "pyfxr", **kw) import pgzero import pgzero.runner pgzero.runner.prepare_mod(__main__) + # finally eval async - TopLevel_async_handler.instance.eval(code) + aio.toplevel.handler.instance.eval(code) + # go back to prompt - if not TopLevel_async_handler.muted: + if not aio.toplevel.handler.muted: print("going interactive") DBG("746: TODO detect input/print to select repl debug") cls.interactive() @@ -783,11 +790,11 @@ def check_code(file_name): @classmethod async def source(cls, main, *args, **kw): # this is not interactive turn off prompting - TopLevel_async_handler.muted = True + aio.toplevel.handler.muted = True try: return await cls.runpy(main, *args, **kw) finally: - TopLevel_async_handler.muted = TopLevel_async_handler.mute_state + aio.toplevel.handler.muted = aio.toplevel.handler.mute_state @classmethod def parse_sync(shell, line, **env): @@ -1073,18 +1080,7 @@ class TopLevel_async_handler(aio.toplevel.AsyncInteractiveConsole): #repodata = "repodata.json" - async def input_console(self, prompt=">>> "): - if len(self.buffer): - return self.buffer.pop(0) - - # if program wants I/O do not empty buffers - if self.shell.is_interactive: - maybe = embed.readline() - if len(maybe): - return maybe - return None - # raise EOFError def eval(self, source): for count, line in enumerate(source.split("\n")): diff --git a/static/pythons.js b/static/pythons.js index 652e3bd..8eb5833 100644 --- a/static/pythons.js +++ b/static/pythons.js @@ -50,21 +50,25 @@ const FETCH_FLAGS = { window.get_terminal_cols = function () { - var cdefault = vm.config.cols || 132 - const cols = (window.terminal && terminal.dataset.cols) || cdefault + var cdefault = 132 + if (window.terminal) + if (vm && vm.config.columns) + cdefault = Number(vm.config.columns || cdefault) + const cols = (window.terminal && terminal.dataset.columns) || cdefault return Number(cols) } window.get_terminal_console = function () { var cdefault = 0 if (window.terminal) - if (vm && vm.config.debug) - cdefault = 10 + if (vm && vm.config.console) + cdefault = Number( vm.config.console || cdefault) return Number( (window.terminal && terminal.dataset.console) || cdefault ) } window.get_terminal_lines = function () { - return Number( (window.terminal && terminal.dataset.lines) || vm.config.lines) + get_terminal_console() + return Number( (window.terminal && terminal.dataset.lines) || vm.config.lines) + // + get_terminal_console() for the phy size } @@ -1006,10 +1010,30 @@ async function feat_vtx(debug_hidden) { } const { WasmTerminal } = await import("./vtx.js") - - vm.vt = new WasmTerminal("terminal", get_terminal_cols(), get_terminal_lines(), [ + const lines = get_terminal_lines() // including virtual get_terminal_console() + const py = window.document.body.clientHeight + var fntsize = Math.floor(py/lines) - 4 + + if (lines<40) + fntsize -= 1 + + if (py>600) + fntsize += 1 + if (py>720) + fntsize += 1 + if (py>1024) + fntsize += 1 + console.warn("fnt:",window.document.body.clientHeight ,"/", lines,"=", fntsize) + vm.vt = new WasmTerminal( + "terminal", + get_terminal_cols(), + lines, + fntsize, + config.fbdev, + [ { url : (config.cdn || "./") + "xtermjsixel/xterm-addon-image-worker.js", sixelSupport:true} - ] ) + ] + ) } @@ -1114,6 +1138,10 @@ function feat_snd() { // to set user media engagement status and possibly make it blocking MM.UME = !vm.config.ume_block MM.is_safari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + if (!MM.is_safari) + MM.is_safari = navigator.userAgent.search("iPhone")>=0; + if (!MM.UME && !MM.is_safari) MM_play( {auto:1, test:1, media: new Audio(config.cdn+"empty.ogg")} , 1) @@ -2327,8 +2355,10 @@ console.warn("TODO: merge/replace location options over script options") //FIXME: should debug force -i or just display vt ? config.interactive = config.interactive || (location.search.search("-i")>=0) //??= - config.cols = cfg.cols || 132 + config.columns = cfg.columns || 132 config.lines = cfg.lines || 32 + config.console = cfg.console || 10 + config.fbdev = cfg.os.search("fbdev")>=0 config.gui_debug = config.gui_debug || 2 //??= @@ -2431,8 +2461,9 @@ function auto_start(cfg) { cfg = { module : false, python : script.dataset.python, - cols : script.dataset.cols, + cols : script.dataset.columns, lines : script.dataset.lines, + console : script.dataset.console, url : script.src, os : script.dataset.os || "gui", text : code, diff --git a/static/vtx.js b/static/vtx.js index d0a8502..8d89382 100644 --- a/static/vtx.js +++ b/static/vtx.js @@ -41,33 +41,57 @@ if (!window.Terminal) { export class WasmTerminal { - constructor(hostid, cols, rows, addons_list) { + constructor(hostid, cols, rows, fontsize, is_fbdev, addons_list) { this.input = '' this.resolveInput = null this.activeInput = true this.inputStartCursor = null this.nodup = 1 - - + var theme = { background: '#1a1c1f' } + var transparency = false + + if (is_fbdev) { + theme = { + foreground: '#ffffff', + background: 'rgba(0, 0, 0, 0)', + cursor: '#ffffff', + selection: 'rgba(255, 255, 255, 0.3)', + black: '#000000', + red: '#e06c75', + brightRed: '#e06c75', + green: '#A4EFA1', + brightGreen: '#A4EFA1', + brightYellow: '#EDDC96', + yellow: '#EDDC96', + magenta: '#e39ef7', + brightMagenta: '#e39ef7', + cyan: '#5fcbd8', + brightBlue: '#5fcbd8', + brightCyan: '#5fcbd8', + blue: '#5fcbd8', + white: '#d0d0d0', + brightBlack: '#808080', + brightWhite: '#ffffff' + } + transparency = true + } this.xterm = new Terminal( { -// allowTransparency: true, + theme: theme, + allowTransparency: transparency, allowProposedApi : true , // xterm 0.5 + sixel - scrollback: 10000, - fontSize: 14, - theme: { background: '#1a1c1f' }, + scrollback: 0, + fontSize: (fontsize || 12), cols: (cols || 132), rows: (rows || 32) } ); - //this.xterm.activeProtocol("ANY"); - if (typeof(Worker) !== "undefined") { for (const addon of (addons_list||[]) ) { - console.warn(hostid,cols,rows, addon) + console.warn(hostid, cols, rows, addon) const imageAddon = new ImageAddon.ImageAddon(addon.url , addon); this.xterm.loadAddon(imageAddon); this.sixel = function write(data) { @@ -75,7 +99,6 @@ export class WasmTerminal { } } - } else { console.warn("No worker support, not loading xterm addons") this.sixel = function ni() {