vibefpmgui.console_view

The Output panel's text: fpm's build progress ("[ 25%]" at a line's start) drawn as a rounded badge, "25%", its color going from blue at 0% to green at 100%. Drawn over the text, which stays as fpm wrote it (so it's selected and copied as such).

 1"""The Output panel's text: fpm's build progress ("[ 25%]" at a line's
 2start) drawn as a rounded badge, "25%", its color going from blue at 0%
 3to green at 100%. Drawn over the text, which stays as fpm wrote it (so
 4it's selected and copied as such).
 5"""
 6
 7from __future__ import annotations
 8
 9import re
10
11from qtpy.QtCore import QRectF, Qt
12from qtpy.QtGui import QColor, QFont, QPainter, QTextCursor
13from qtpy.QtWidgets import QPlainTextEdit
14
15PROGRESS_RE = re.compile(r"^\[\s*(\d{1,3})%\]")
16
17
18def progress_color(percent: int) -> QColor:
19    """Blue at 0%, green at 100% (through teal), bright enough for white text."""
20    fraction = max(0, min(100, percent)) / 100
21    return QColor.fromHsvF(0.60 - 0.24 * fraction, 0.70, 0.78)
22
23
24class ConsoleView(QPlainTextEdit):
25    """A read-only text view for command output, with fpm's progress as badges."""
26
27    def progress_badges(self) -> list[tuple[int, QRectF]]:
28        """(percent, where its badge is drawn) for the lines on view."""
29        out = []
30        block = self.firstVisibleBlock()
31        offset = self.contentOffset()
32        height = self.viewport().height()
33        while block.isValid():
34            geometry = self.blockBoundingGeometry(block).translated(offset)
35            if geometry.top() > height:
36                break
37            match = PROGRESS_RE.match(block.text())
38            if match and block.isVisible():
39                start, end = QTextCursor(block), QTextCursor(block)
40                start.setPosition(block.position() + match.start())
41                end.setPosition(block.position() + match.end())
42                left, right = self.cursorRect(start), self.cursorRect(end)
43                if left.top() == right.top():  # (not wrapped in the middle)
44                    out.append((int(match.group(1)), QRectF(left.left(), left.top() + 1, right.left() - left.left(),
45                                                            left.height() - 2)))
46            block = block.next()
47        return out
48
49    def paintEvent(self, event):
50        super().paintEvent(event)
51        badges = self.progress_badges()
52        if not badges:
53            return
54        painter = QPainter(self.viewport())
55        painter.setRenderHint(QPainter.Antialiasing)
56        font = QFont(self.font())
57        font.setBold(True)
58        font.setPointSizeF(max(6.0, font.pointSizeF() * 0.85))
59        painter.setFont(font)
60        for percent, rect in badges:
61            painter.setPen(Qt.NoPen)
62            painter.setBrush(progress_color(percent))
63            painter.drawRoundedRect(rect, rect.height() / 2, rect.height() / 2)
64            painter.setPen(QColor("white"))
65            painter.drawText(rect, Qt.AlignCenter, f"{percent}%")
66        painter.end()
PROGRESS_RE = re.compile('^\\[\\s*(\\d{1,3})%\\]')
def progress_color(percent: int) -> PySide6.QtGui.QColor:
19def progress_color(percent: int) -> QColor:
20    """Blue at 0%, green at 100% (through teal), bright enough for white text."""
21    fraction = max(0, min(100, percent)) / 100
22    return QColor.fromHsvF(0.60 - 0.24 * fraction, 0.70, 0.78)

Blue at 0%, green at 100% (through teal), bright enough for white text.

class ConsoleView(PySide6.QtWidgets.QPlainTextEdit):
25class ConsoleView(QPlainTextEdit):
26    """A read-only text view for command output, with fpm's progress as badges."""
27
28    def progress_badges(self) -> list[tuple[int, QRectF]]:
29        """(percent, where its badge is drawn) for the lines on view."""
30        out = []
31        block = self.firstVisibleBlock()
32        offset = self.contentOffset()
33        height = self.viewport().height()
34        while block.isValid():
35            geometry = self.blockBoundingGeometry(block).translated(offset)
36            if geometry.top() > height:
37                break
38            match = PROGRESS_RE.match(block.text())
39            if match and block.isVisible():
40                start, end = QTextCursor(block), QTextCursor(block)
41                start.setPosition(block.position() + match.start())
42                end.setPosition(block.position() + match.end())
43                left, right = self.cursorRect(start), self.cursorRect(end)
44                if left.top() == right.top():  # (not wrapped in the middle)
45                    out.append((int(match.group(1)), QRectF(left.left(), left.top() + 1, right.left() - left.left(),
46                                                            left.height() - 2)))
47            block = block.next()
48        return out
49
50    def paintEvent(self, event):
51        super().paintEvent(event)
52        badges = self.progress_badges()
53        if not badges:
54            return
55        painter = QPainter(self.viewport())
56        painter.setRenderHint(QPainter.Antialiasing)
57        font = QFont(self.font())
58        font.setBold(True)
59        font.setPointSizeF(max(6.0, font.pointSizeF() * 0.85))
60        painter.setFont(font)
61        for percent, rect in badges:
62            painter.setPen(Qt.NoPen)
63            painter.setBrush(progress_color(percent))
64            painter.drawRoundedRect(rect, rect.height() / 2, rect.height() / 2)
65            painter.setPen(QColor("white"))
66            painter.drawText(rect, Qt.AlignCenter, f"{percent}%")
67        painter.end()

A read-only text view for command output, with fpm's progress as badges.

def progress_badges(self) -> list[tuple[int, PySide6.QtCore.QRectF]]:
28    def progress_badges(self) -> list[tuple[int, QRectF]]:
29        """(percent, where its badge is drawn) for the lines on view."""
30        out = []
31        block = self.firstVisibleBlock()
32        offset = self.contentOffset()
33        height = self.viewport().height()
34        while block.isValid():
35            geometry = self.blockBoundingGeometry(block).translated(offset)
36            if geometry.top() > height:
37                break
38            match = PROGRESS_RE.match(block.text())
39            if match and block.isVisible():
40                start, end = QTextCursor(block), QTextCursor(block)
41                start.setPosition(block.position() + match.start())
42                end.setPosition(block.position() + match.end())
43                left, right = self.cursorRect(start), self.cursorRect(end)
44                if left.top() == right.top():  # (not wrapped in the middle)
45                    out.append((int(match.group(1)), QRectF(left.left(), left.top() + 1, right.left() - left.left(),
46                                                            left.height() - 2)))
47            block = block.next()
48        return out

(percent, where its badge is drawn) for the lines on view.

def paintEvent(self, event):
50    def paintEvent(self, event):
51        super().paintEvent(event)
52        badges = self.progress_badges()
53        if not badges:
54            return
55        painter = QPainter(self.viewport())
56        painter.setRenderHint(QPainter.Antialiasing)
57        font = QFont(self.font())
58        font.setBold(True)
59        font.setPointSizeF(max(6.0, font.pointSizeF() * 0.85))
60        painter.setFont(font)
61        for percent, rect in badges:
62            painter.setPen(Qt.NoPen)
63            painter.setBrush(progress_color(percent))
64            painter.drawRoundedRect(rect, rect.height() / 2, rect.height() / 2)
65            painter.setPen(QColor("white"))
66            painter.drawText(rect, Qt.AlignCenter, f"{percent}%")
67        painter.end()

paintEvent(self, e: PySide6.QtGui.QPaintEvent, /) -> None

staticMetaObject = PySide6.QtCore.QMetaObject("ConsoleView" inherits "QPlainTextEdit": )