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
NO_WINDOW: dict = {}
def gui_python() -> str:
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).

def run( args: list[str], timeout: float, limit: int | None = None, env: dict | None = None, cwd: str | None = None) -> tuple[int, bytes | None, str]:
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.