starplast.console
A console pane, and the stdout capture that feeds it.
The build and the search both report progress by printing, and in a GUI that output goes nowhere -- the user watches a frozen window while the terminal they cannot see fills up. This tees stdout and stderr into a widget without taking them away from the terminal, so running from a shell still behaves normally and running from a launcher is no longer silent.
Capture is deliberately a tee rather than a redirect. Swallowing stderr would hide tracebacks from anyone debugging from a terminal, which is where they are actually read.
Lines that came through logging_util carry their level as a [LEVEL] prefix, and the pane reads
it: it colors by level and filters by minimum level, so a warning is findable in a walk that
printed four thousand progress lines. Ordinary prints are left levelless rather than being called
INFO -- most output here is print, and folding it into a level would make filtering by that level
useless.
Lines are appended as they arrive rather than by re-rendering the whole pane. Re-rendering was quadratic in the number of lines, on the one screen whose entire purpose is watching something long run: 5,000 lines meant 5,000 full rebuilds of a 5,000-line document.
Forwards writes to the original stream and emits them as a signal.
A file-like object handed to sys.stdout is written to from worker threads, so the text is
emitted as a queued signal rather than touching a widget directly. Writing to a QTextEdit off
the GUI thread is a crash that shows up as an unrelated stack days later.
pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.
Do NOT close the wrapped stream -- this is a tee, not an owner.
Python's logging shutdown calls close() on whatever it finds at sys.stderr. Without this the interpreter reported "'Tee' object has no attribute 'close'" at exit, and closing the real stream instead would take stderr down for everything still running.
Read-only log with a timestamp column, a filter, and copy/clear.
Put the original streams back. Called on close, so a crash does not leave a dead tee.