From 5e5dc2d54a3342f424273c0e5b73d7cb3de68a93 Mon Sep 17 00:00:00 2001 From: Jessey van Offeren Date: Fri, 22 May 2026 10:00:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(share):=20render=20colors=20in=20the=20sha?= =?UTF-8?q?red=20terminal=20=E2=80=94=200.24.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The terminal view rendered monochrome (QPlainTextEdit.setPlainText), dropping pyte's per-cell attributes. Rewritten as a QTextEdit that renders fg/bg/bold/ reverse per cell (block cursor = inverted cell), preserving scrollback. The session already runs the host's $SHELL + config with TERM=xterm-256color, so fish/ls/git/prompts now look the same as locally. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 7 ++ pyproject.toml | 2 +- src/rigdoctor/__init__.py | 2 +- src/rigdoctor/gui/terminal_widget.py | 107 +++++++++++++++++++++------ 4 files changed, 94 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c38622..79d4561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to RigDoctor are recorded here. Format follows (`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git release tag (so the auto-updater, D18, can compare versions). +## [0.24.0] - 2026-05-22 +### Added +- **Shared terminal is now in color.** The terminal view renders pyte's per-cell foreground/ + background, bold, and reverse, so the host's real shell keeps its theming — fish, `ls`, + `git`, prompts, etc. look the same as locally (the session already runs the host's `$SHELL` + with its config and `TERM=xterm-256color`; only the rendering was monochrome). + ## [0.23.0] - 2026-05-22 ### Added - **Crash-logger trigger modes (M9 / D6)** via `systemd --user`, no root: **manual**, diff --git a/pyproject.toml b/pyproject.toml index c8aac31..5280f47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rigdoctor" -version = "0.23.0" +version = "0.24.0" description = "Modular hardware monitoring & crash diagnostics for Linux gamers." readme = "README.md" requires-python = ">=3.11" diff --git a/src/rigdoctor/__init__.py b/src/rigdoctor/__init__.py index 39487d5..8fd561a 100644 --- a/src/rigdoctor/__init__.py +++ b/src/rigdoctor/__init__.py @@ -1,3 +1,3 @@ """RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers.""" -__version__ = "0.23.0" +__version__ = "0.24.0" diff --git a/src/rigdoctor/gui/terminal_widget.py b/src/rigdoctor/gui/terminal_widget.py index c2de92f..6a32b98 100644 --- a/src/rigdoctor/gui/terminal_widget.py +++ b/src/rigdoctor/gui/terminal_widget.py @@ -1,30 +1,66 @@ -"""A minimal terminal view: renders PTY output via pyte and emits keystrokes (M12, Tier 3). +"""A terminal view: renders PTY output via pyte (with colors) and emits keystrokes (M12). Used by both sides of a shared session — the host (mirrors its local PTY, can also type, e.g. -a sudo password) and the guest (renders the streamed PTY, sends keystrokes). Monochrome for -now; cursor addressing / layout (vim, top) work via pyte. +a sudo password) and the guest (renders the streamed PTY, sends keystrokes). Renders pyte's +per-cell foreground/background/bold/reverse so the host's real shell (e.g. fish) keeps its +colors and theming; cursor addressing (vim, top) works via pyte. Scrollback is preserved. """ from __future__ import annotations +import html as _html + import pyte from PySide6.QtCore import Qt, Signal -from PySide6.QtGui import QFontDatabase, QFontMetrics, QTextCursor -from PySide6.QtWidgets import QPlainTextEdit +from PySide6.QtGui import QFontDatabase, QFontMetrics +from PySide6.QtWidgets import QTextEdit + +# ANSI named colors → RGB (a dark, modern palette). pyte also yields 6-hex strings for +# 256-color / truecolor, which we pass through, and "default" which maps to the theme. +_FG_DEFAULT = "#d6dae0" +_BG_DEFAULT = "#0d0f13" +_NAMED = { + "black": "#2a2f39", "red": "#f87171", "green": "#4ade80", "brown": "#e5c07b", + "yellow": "#e5c07b", "blue": "#60a5fa", "magenta": "#c084fc", "cyan": "#38bdf8", + "white": "#d6dae0", +} +_BRIGHT = { # bold brightens the standard 8 + "black": "#5b626c", "red": "#fca5a5", "green": "#86efac", "brown": "#fde68a", + "yellow": "#fde68a", "blue": "#93c5fd", "magenta": "#d8b4fe", "cyan": "#7dd3fc", + "white": "#ffffff", +} +_HISTORY_RENDER = 400 # cap scrollback rows rendered per frame (perf) -class TerminalView(QPlainTextEdit): +def _color(name: str, default: str, bright: bool) -> str: + if name == "default": + return default + table = _BRIGHT if bright else _NAMED + if name in table: + return table[name] + if len(name) == 6: # pyte 256/truecolor as a hex string + try: + int(name, 16) + return "#" + name + except ValueError: + pass + return default + + +class TerminalView(QTextEdit): keys = Signal(bytes) # user keystrokes -> bytes for the PTY resized = Signal(int, int) # rows, cols def __init__(self, rows: int = 24, cols: int = 80): super().__init__() - self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) + self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) self.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)) self.setUndoRedoEnabled(False) - self.setMinimumHeight(260) + self.setReadOnly(False) # we capture keys ourselves; no local editing + self.setStyleSheet(f"QTextEdit {{ background: {_BG_DEFAULT}; border: none; }}") + self.setMinimumHeight(320) self._rows, self._cols = rows, cols - self._screen = pyte.HistoryScreen(cols, rows, history=1000, ratio=0.5) + self._screen = pyte.HistoryScreen(cols, rows, history=2000, ratio=0.5) self._stream = pyte.ByteStream(self._screen) def grid(self) -> tuple[int, int]: @@ -38,24 +74,51 @@ class TerminalView(QPlainTextEdit): self._screen.reset() self._render() - def _row_text(self, row) -> str: - return "".join(row[x].data for x in range(self._cols)).rstrip() + # --- rendering --------------------------------------------------------------------- + def _span(self, style, text: str) -> str: + fg_name, bg_name, bold, reverse = style + fg = _color(fg_name, _FG_DEFAULT, bold) + bg = _color(bg_name, _BG_DEFAULT, False) + if reverse: + fg, bg = bg, fg + esc = _html.escape(text, quote=False).replace(" ", " ") + weight = "font-weight:bold;" if bold else "" + return f'{esc}' + + def _row_html(self, row, cursor_x) -> str: + out: list[str] = [] + buf: list[str] = [] + cur_style = None + for x in range(self._cols): + ch = row[x] + reverse = ch.reverse + if cursor_x is not None and x == cursor_x and self.hasFocus(): + reverse = not reverse # block cursor = inverted cell + style = (ch.fg, ch.bg, ch.bold, reverse) + if style != cur_style: + if buf: + out.append(self._span(cur_style, "".join(buf))) + buf = [] + cur_style = style + buf.append(ch.data or " ") + if buf: + out.append(self._span(cur_style, "".join(buf))) + return "".join(out) def _render(self) -> None: bar = self.verticalScrollBar() at_bottom = bar.value() >= bar.maximum() - 2 prev = bar.value() - history = [self._row_text(r) for r in self._screen.history.top] # scrollback - self.setPlainText("\n".join(history + list(self._screen.display))) - if at_bottom: # follow output; place caret at the real (row, col) - cursor = self.textCursor() - cursor.movePosition(QTextCursor.MoveOperation.Start) - cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.MoveAnchor, len(history) + self._screen.cursor.y) - cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.MoveAnchor, self._screen.cursor.x) - self.setTextCursor(cursor) - self.ensureCursorVisible() - else: # user scrolled up to read — keep their place - bar.setValue(prev) + + history = list(self._screen.history.top)[-_HISTORY_RENDER:] + lines = [self._row_html(r, None) for r in history] + cur_y = self._screen.cursor.y + for y in range(self._rows): + cursor_x = self._screen.cursor.x if y == cur_y else None + lines.append(self._row_html(self._screen.buffer[y], cursor_x)) + self.setHtml('
' + "
".join(lines) + "
") + + bar.setValue(bar.maximum() if at_bottom else prev) def resizeEvent(self, event): # noqa: N802 (Qt override) super().resizeEvent(event)