vibefpmgui.console
Starting console programs (git, fpm, fortitude, fprettify…) from the app without a console window flashing up on Windows (no Qt).
A GUI app on Windows has no console; a console program it starts with Python's subprocess gets a new one, shown for as long as the program runs, unless it's started with CREATE_NO_WINDOW. (QProcess does that by itself when the app has no console.)
1"""Starting console programs (git, fpm, fortitude, fprettify…) from the app 2without a console window flashing up on Windows (no Qt). 3 4A GUI app on Windows has no console; a console program it starts with 5Python's subprocess gets a new one, shown for as long as the program runs, 6unless it's started with CREATE_NO_WINDOW. (QProcess does that by itself 7when the app has no console.) 8""" 9 10from __future__ import annotations 11 12import os 13import subprocess 14import sys 15import tempfile 16from pathlib import Path 17 18# subprocess's keyword arguments for no console window: **NO_WINDOW in each call. 19NO_WINDOW: dict = {"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {} 20 21 22def gui_python() -> str: 23 """The Python to start another window of the app with: on Windows its 24 pythonw.exe (python.exe would open a console window beside it, there 25 for as long as the window is).""" 26 if sys.platform == "win32": 27 windowless = Path(sys.executable).with_name("pythonw.exe") 28 if windowless.is_file(): 29 return str(windowless) 30 return sys.executable 31 32 33def run(args: list[str], timeout: float, limit: int | None = None, env: dict | None = None, 34 cwd: str | None = None) -> tuple[int, bytes | None, str]: 35 """Run a program to its end, without a console window: (exit code, its 36 output, or None if over ``limit`` bytes, its error message). Raises 37 OSError if it can't be started, TimeoutError after ``timeout`` seconds 38 (it's killed). 39 40 Its output goes to temporary files, not pipes, and only it is waited for 41 (without Python's lock: the window goes on): a pipe is only done when 42 every process holding it has closed it, and a process started meanwhile 43 by another thread (the Terminal's shell, fortls, fpm) could hold it too, 44 for as long as it runs.""" 45 with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err: 46 process = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=out, stderr=err, env=env, cwd=cwd, 47 **NO_WINDOW) 48 try: 49 code = process.wait(timeout=timeout) 50 except subprocess.TimeoutExpired: 51 process.kill() 52 process.wait() 53 raise TimeoutError(f"{Path(args[0]).name} took over {timeout:.0f} s") from None 54 size = out.seek(0, os.SEEK_END) 55 out.seek(0) 56 err.seek(0) 57 output = None if limit is not None and size > limit else out.read() 58 message = err.read().decode("utf-8", "replace").strip() 59 return code, output, message
23def gui_python() -> str: 24 """The Python to start another window of the app with: on Windows its 25 pythonw.exe (python.exe would open a console window beside it, there 26 for as long as the window is).""" 27 if sys.platform == "win32": 28 windowless = Path(sys.executable).with_name("pythonw.exe") 29 if windowless.is_file(): 30 return str(windowless) 31 return sys.executable
The Python to start another window of the app with: on Windows its pythonw.exe (python.exe would open a console window beside it, there for as long as the window is).
34def run(args: list[str], timeout: float, limit: int | None = None, env: dict | None = None, 35 cwd: str | None = None) -> tuple[int, bytes | None, str]: 36 """Run a program to its end, without a console window: (exit code, its 37 output, or None if over ``limit`` bytes, its error message). Raises 38 OSError if it can't be started, TimeoutError after ``timeout`` seconds 39 (it's killed). 40 41 Its output goes to temporary files, not pipes, and only it is waited for 42 (without Python's lock: the window goes on): a pipe is only done when 43 every process holding it has closed it, and a process started meanwhile 44 by another thread (the Terminal's shell, fortls, fpm) could hold it too, 45 for as long as it runs.""" 46 with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err: 47 process = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=out, stderr=err, env=env, cwd=cwd, 48 **NO_WINDOW) 49 try: 50 code = process.wait(timeout=timeout) 51 except subprocess.TimeoutExpired: 52 process.kill() 53 process.wait() 54 raise TimeoutError(f"{Path(args[0]).name} took over {timeout:.0f} s") from None 55 size = out.seek(0, os.SEEK_END) 56 out.seek(0) 57 err.seek(0) 58 output = None if limit is not None and size > limit else out.read() 59 message = err.read().decode("utf-8", "replace").strip() 60 return code, output, message
Run a program to its end, without a console window: (exit code, its
output, or None if over limit bytes, its error message). Raises
OSError if it can't be started, TimeoutError after timeout seconds
(it's killed).
Its output goes to temporary files, not pipes, and only it is waited for (without Python's lock: the window goes on): a pipe is only done when every process holding it has closed it, and a process started meanwhile by another thread (the Terminal's shell, fortls, fpm) could hold it too, for as long as it runs.