Files
MeshTerm/meshterm/core/relaunch.py
T
Jean-Pierre MartineauandClaude Opus 5 2d31810801 MeshTerm gets its own window on a Mac, and leaves yours alone
No Mac monospace face carries the Braille Patterns block, so Terminal --
which falls back generously rather than drawing a box -- borrows the
charts and the map's terrain from a proportional face and lands them at
the wrong width. Nothing announces itself as broken; it is just crooked,
and a reader with nothing to compare against never finds out why.

Fixing it means a font, and a font means a profile. The question that
shapes this module is whose terminal it is. MeshTerm's palette is the
16-colour VT set, crude on purpose, and nobody should have to read their
mail in it -- so nothing here touches the default profile or edits their
preferences. It adds a profile named MeshTerm and opens our own window in
it; theirs keeps its translucency, and quitting leaves nothing behind.

That is also the only route that works at all. Terminal rewrites its
preferences from memory when it quits, so a file written while it runs --
and MeshTerm always runs inside it -- is discarded later. Handing Terminal
a .terminal file lets Terminal do the importing.

The profile is generated from _VT_SLOTS rather than shipped, so the window
cannot disagree with the screen it holds. The NSColor and NSFont archives
are pinned to bytes read back off a profile that was installed on a real
Mac and confirmed rendering, not to a reading of the format.

relaunch.py is the half that was never Windows-specific -- own_command,
child_environment and its _PYI trap, the wanted size -- moved out of
winterminal so both terminals share one copy.

Not yet wired into startup: the launch itself wants a Mac to confirm, and
that machine is asleep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 13:35:29 -04:00

105 lines
4.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""What every relaunch of MeshTerm needs, whichever terminal it is relaunching into.
MeshTerm reopens itself when the terminal it was started from cannot draw it, and there
are two such terminals for two unrelated reasons. The classic Windows console has no font
fallback at all, so an icon it lacks is a box and no font choice fixes it
(:mod:`meshterm.core.winterminal`). macOS Terminal falls back generously — too generously:
no Mac monospace face carries the Braille Patterns block, so the charts are borrowed from a
*proportional* face and land at the wrong width (:mod:`meshterm.core.macterminal`). One
host shows nothing, the other shows something crooked.
The destinations could hardly be less alike. The act is the same one: work out the command
that would start this session again, hand it an environment a fresh launch can survive, and
let the caller exit. So it is written once, here, and each platform module supplies only
the part that is genuinely its own — which terminal, and how to ask it.
"""
from __future__ import annotations
import os
import sys
#: Set in the reopened session, so a relaunch can never chain into another one. Each
#: platform's own gate would usually refuse a second pass anyway — Windows Terminal is not
#: a classic console; a window opened from MeshTerm's profile is already in it — but this
#: is the belt to those braces, and it costs one environment variable.
REOPENED_ENV = "MESHTERM_REOPENED"
def own_command() -> list[str]:
"""The command that would start this same session again.
A frozen build is its own executable and takes the arguments as they were given. A
development or ``pip`` install is reached through its interpreter, because the console
script's own path is not something the receiving terminal can be relied on to resolve.
Returns:
The program and its arguments, ready to hand to a terminal.
"""
if getattr(sys, "frozen", False):
return [sys.executable, *sys.argv[1:]]
return [sys.executable, "-m", "meshterm", *sys.argv[1:]]
def child_environment() -> dict[str, str]:
"""The environment for a fresh launch of MeshTerm, minus this bundle's bookkeeping.
A PyInstaller one-file build runs in two stages: the executable unpacks itself into a
temporary directory and then runs itself again as a child, the two halves coordinating
through ``_PYI*`` environment variables. Those must not reach a *new* launch of the
executable. The bootloader would find them, conclude it is the second stage of a launch
it never made, check that its parent is the same program — find the terminal instead —
and abort::
[PYI-33368:ERROR] Security validation failure: parent process has different
executable!
Which is the whole session gone, in a brand new window holding a message nobody can act
on, while the window that offered the move has already closed. Removing them makes the
new process a first stage, which is what it actually is.
Returns:
The environment to start the new process with.
"""
environment = dict(os.environ)
environment[REOPENED_ENV] = "1"
if not getattr(sys, "frozen", False):
return environment
for key in [key for key in environment if key.startswith("_PYI")]:
del environment[key]
environment.pop("_MEIPASS2", None) # the name older bootloaders used
# The bootloader points the loader at the unpacked bundle and parks the caller's own
# value beside it. The new process unpacks its own copy, so it wants the original.
for name in ("LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "DYLD_FRAMEWORK_PATH"):
original = environment.pop(f"{name}_ORIG", None)
if original is not None:
environment[name] = original
else:
environment.pop(name, None)
return environment
def wanted_size() -> tuple[int, int]:
"""The window to ask for, as ``(cols, rows)``.
Asked for, because a window MeshTerm opens should be one MeshTerm fits in. A session
that lands in whatever size the reader's shell happens to launch at can arrive too
small for the app, and the failure is quiet and cosmetic: the startup wordmark is 71
columns, so at 70 it silently swaps to the narrow one drawn for the PicoCalc, and a
short window truncates every description onto the pager.
The numbers come from the platform's own stated minimum plus a margin, so they follow
it rather than repeating it. A terminal is free to clamp to what the display can show,
so this is a request and not a demand — on a small screen the app degrades exactly as
it would have anyway.
Returns:
The columns and rows to ask the terminal for.
"""
from ..platforms import get_platform
platform = get_platform()
return platform.readable_cols + 8, platform.readable_rows + 6