steam-backlog-enforcer/steam_backlog_enforcer/_echo.py
2026-10-10 16:07:42 +02:00

56 lines
1.6 KiB
Python

"""User-facing CLI output.
A leaf module so every command can print without importing the install
machinery. Split out of :mod:`steam_backlog_enforcer.game_install` to keep
that module under the 250-line cap.
Output normally goes to stdout. A web job installs a sink with
:func:`routed_echo`, and every ``_echo`` inside it becomes a ``log`` event
instead: the commands keep one way of talking, whoever is listening.
"""
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
import sys
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
# Receives the raw text (message + end) when set; stdout otherwise.
_SINK: ContextVar[Callable[[str], None] | None] = ContextVar("echo_sink", default=None)
def _echo(msg: str = "", *, end: str = "\n", flush: bool = False) -> None:
"""Write user-facing CLI output to stdout, or to the active sink.
Args:
msg: Text to output.
end: String appended after the message.
flush: Whether to flush stdout immediately.
"""
sink = _SINK.get()
if sink is not None:
sink(msg + end)
return
sys.stdout.write(msg + end)
if flush:
sys.stdout.flush()
@contextmanager
def routed_echo(sink: Callable[[str], None]) -> Iterator[None]:
"""Send every ``_echo`` in this context to *sink* instead of stdout.
Args:
sink: Called with the raw text, ``end`` included, so it can treat
carriage returns and newlines the way a terminal would.
"""
token = _SINK.set(sink)
try:
yield
finally:
_SINK.reset(token)