Coverage for vibefpmgui/console_view.py: 95%
47 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
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"""
7from __future__ import annotations
9import re
11from qtpy.QtCore import QRectF, Qt
12from qtpy.QtGui import QColor, QFont, QPainter, QTextCursor
13from qtpy.QtWidgets import QPlainTextEdit
15PROGRESS_RE = re.compile(r"^\[\s*(\d{1,3})%\]")
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)
24class ConsoleView(QPlainTextEdit):
25 """A read-only text view for command output, with fpm's progress as badges."""
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: 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true
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) 43 ↛ 46line 43 didn't jump to line 46 because the condition on line 43 was always true
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
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()