Files
Jean-Pierre MartineauandClaude Opus 5.5 ffe31dac36 Docstrings and comments of meshterm/core are in ASD-STE100
The prose of each docstring and comment in the core package now follows
ASD-STE100 Simplified Technical English, with Canadian spelling: short
sentences, simple verb tenses, no dashes or semicolons, and one word for
one concept (docs/development/writing-style.md has the rules and the
glossary). The meaning and each reason stay. The code does not change:
the AST of each file, without its docstrings, is the same as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:29:03 -04:00

247 lines
9.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""The host that MeshTerm runs on, read one time, so that a bug report already has the answers.
Each value here answers a question that otherwise costs one more exchange with the person
who reported the problem: which build of MeshTerm, on which OS, in which terminal, how
wide, and with which colour depth. No value is about the mesh, and no value is private.
This module intentionally knows nothing about contacts, keys, positions, or messages.
Thus the user can paste the block that it fills in public, and nobody has to read the
block first.
Two probes are **ladders over environment variables**, not protocol queries. This is a
compromise, and we state it here: a terminal has no portable way to tell what it is. The
one sequence that comes close (``CSI > q``) needs a live tty, a reply that may never
arrive, and a timeout budget on a path that must never hang. Thus MeshTerm identifies
each terminal by the variable that the terminal sets about itself. This is the same
ladder that :func:`~meshterm.ui.termfont.detect_terminal_font` already uses for the font.
If MeshTerm does not recognize the terminal, it uses ``TERM``, not a guess.
"""
from __future__ import annotations
import os
import platform
import shutil
import sys
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
#: The first Windows 11 build. Windows 11 still reports its release as ``"10"`` through
#: each documented API. Thus the build number is the only thing that separates the two.
_WINDOWS_11_BUILD = 22000
#: The terminals that identify themselves, by the variable that each one sets. The order
#: is from the most specific to the least specific. An emulator inside another emulator
#: (the terminal of VS Code, a multiplexer) sets its own marker, and also inherits the
#: outer marker. Thus the inner identity must win.
#:
#: MeshTerm checks ``TERM_PROGRAM`` separately, between these variables and the ``TERM``
#: fallback, because the value of ``TERM_PROGRAM`` is the name. Thus it identifies
#: terminals that this table never has to list.
_TERMINAL_VARS: tuple[tuple[str, str], ...] = (
("WT_SESSION", "Windows Terminal"),
("ConEmuPID", "ConEmu"),
("TERMINAL_EMULATOR", ""), # JetBrains IDEs put the name in the value
("KITTY_WINDOW_ID", "kitty"),
("ALACRITTY_WINDOW_ID", "Alacritty"),
("KONSOLE_VERSION", "Konsole"),
("GNOME_TERMINAL_SCREEN", "GNOME Terminal"),
("VTE_VERSION", "VTE-based"),
)
#: The names that ``TERM_PROGRAM`` gives to terminals whose own name is not the name that
#: a person uses. The table has only the names that are really different. Each other
#: value passes through as it is. Thus this table does not grow into a catalogue of all
#: the terminals that exist.
_TERM_PROGRAM_NAMES: dict[str, str] = {
"Apple_Terminal": "Terminal.app",
"vscode": "VS Code",
}
@dataclass(frozen=True, slots=True)
class Host:
"""The machine and the Python that run MeshTerm.
Attributes:
install: How this copy was installed. ``"frozen"``: a one-file build, which has
its own bootloader, its own relaunch rules, and no source tree to read.
``"source"``: a checkout, so MeshTerm can ask for a commit. ``"package"``: a
wheel from an index. The three fail in different ways, and the person who
reports a problem usually does not know which one they have.
os: The OS and its version, as a person writes it: ``Windows 11``,
``macOS 15.6``, ``Debian GNU/Linux 12 (bookworm)``.
os_build: The exact build under that name: a Windows build number, or the kernel
release on all other systems. This value separates two hosts that have the
same name.
arch: The processor architecture. It tells an arm64 build from an x86_64 build
when a binary does not operate correctly.
python: The interpreter version, in three parts.
"""
install: str
os: str
os_build: str
arch: str
python: str
@dataclass(frozen=True, slots=True)
class Terminal:
"""The terminal in which MeshTerm draws, and what it can draw.
Attributes:
program: The name of the emulator. If nothing identified itself, the value of
``TERM``. ``None`` when ``TERM`` is not set either.
term: The ``TERM`` value, exactly. It is kept next to ``program``, not merged
into it, because a terminal that gives a false ``TERM`` is itself a frequent
cause of a rendering report.
colorterm: The ``COLORTERM`` value. A terminal uses it to claim truecolor. It is
the first thing to check when a screen comes in the wrong colours.
cols: The width of the terminal in cells.
rows: The height of the terminal in cells.
over_ssh: Whether this is an ssh session. It changes what MeshTerm can know (the
font is on the remote client). It is also the reason that several other
results here show ``unknown`` instead of a wrong value.
"""
program: str | None
term: str | None
colorterm: str | None
cols: int
rows: int
over_ssh: bool
def install_kind() -> str:
"""How this copy of MeshTerm was installed: ``frozen``, ``source``, or ``package``.
A frozen build is clear, because the bootloader sets the flag. After that, the
question is whether there is a repository above the package. MeshTerm can ask a
checkout for its commit, and a checkout can have uncommitted work. A wheel can do
neither.
Returns:
One of ``"frozen"``, ``"source"``, or ``"package"``.
"""
if getattr(sys, "frozen", False):
return "frozen"
# meshterm/core/hostinfo.py -> meshterm/core -> meshterm -> the tree that holds it
if (Path(__file__).resolve().parents[2] / ".git").exists():
return "source"
return "package"
def host() -> Host:
"""Read the facts of the machine.
Returns:
The :class:`Host` block.
"""
name, build = _os_name_and_build()
return Host(
install=install_kind(),
os=name,
os_build=build,
arch=platform.machine() or "unknown",
python=platform.python_version(),
)
def terminal(environ: Mapping[str, str] | None = None) -> Terminal:
"""Read the facts of the terminal.
Args:
environ: The environment to examine (the default is ``os.environ``). A test can
inject it, so that the ladder can be tested without a terminal of the correct
type.
Returns:
The :class:`Terminal` block.
"""
env = os.environ if environ is None else environ
size = shutil.get_terminal_size()
return Terminal(
program=terminal_program(env),
term=env.get("TERM") or None,
colorterm=env.get("COLORTERM") or None,
cols=size.columns,
rows=size.lines,
over_ssh=bool(env.get("SSH_TTY") or env.get("SSH_CONNECTION")),
)
def terminal_program(environ: Mapping[str, str] | None = None) -> str | None:
"""Name the terminal emulator from what it tells about itself.
The ladder: first, the terminals in :data:`_TERMINAL_VARS` that set a marker
variable. Then ``TERM_PROGRAM``, whose value is the name, so that it covers terminals
that this module does not know. Last, ``TERM``, which is the last honest answer that
there is.
Args:
environ: The environment to examine (the default is ``os.environ``).
Returns:
The name of the terminal, or ``None`` when nothing in the environment names one.
"""
env = os.environ if environ is None else environ
for var, name in _TERMINAL_VARS:
value = env.get(var)
if value:
return name or value
program = env.get("TERM_PROGRAM")
if program:
return _TERM_PROGRAM_NAMES.get(program, program)
return env.get("TERM") or None
def _os_name_and_build() -> tuple[str, str]:
"""The OS as a person writes it, and the exact build under that name."""
system = platform.system()
if system == "Windows":
return _windows()
if system == "Darwin":
version = platform.mac_ver()[0]
return (f"macOS {version}" if version else "macOS"), platform.release()
if system == "Linux":
return (_pretty_name() or "Linux"), platform.release()
return (system or "unknown"), platform.release()
def _windows() -> tuple[str, str]:
"""Windows by the name under which it is sold, and its build number.
``platform.win32_ver`` reports release ``"10"`` also on Windows 11, because the new
name never got into the version APIs. Thus the build number decides which Windows
this is. The build is also the useful half of the pair, because a fix is shipped for
a build.
"""
release, version, _service_pack, _kind = platform.win32_ver()
try:
build = int(version.rsplit(".", 1)[-1])
except ValueError:
build = 0
name = "Windows 11" if build >= _WINDOWS_11_BUILD else f"Windows {release or '?'}"
return name, version or platform.version()
def _pretty_name() -> str | None:
"""The name that the distribution gives itself, from ``/etc/os-release``.
This is the only place where the system itself states its Linux variant, so that
MeshTerm does not have to guess. It is the reason that a report can show "Calculinux"
or "Raspberry Pi OS" instead of ``Linux``. A host without the file (or without
permission to read it) has no answer.
"""
try:
text = Path("/etc/os-release").read_text(encoding="utf-8")
except OSError:
return None
for line in text.splitlines():
key, _, value = line.partition("=")
if key == "PRETTY_NAME":
return value.strip().strip('"') or None
return None