Line data Source code
1 : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements ProcessOutputPanel; the sticky auto-scroll hysteresis is
6 : * set up in the constructor, the line cap in limitLines().
7 : */
8 : #include "processoutputpanel.h"
9 : #include "processinfo.h"
10 : #include <QHBoxLayout>
11 : #include <QScrollBar>
12 : #include <QTextCursor>
13 : #include <QTextDocument>
14 : #include <QTextEdit>
15 : #include <QToolButton>
16 :
17 140 : ProcessOutputPanel::ProcessOutputPanel(QWidget *parent)
18 140 : : QDockWidget(tr("Process output"), parent) {
19 280 : setObjectName(QStringLiteral("processOutputDock"));
20 140 : setFeatures(QDockWidget::DockWidgetMovable |
21 : QDockWidget::DockWidgetFloatable);
22 140 : setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea);
23 :
24 140 : auto *contents = new QWidget(this);
25 280 : contents->setObjectName(QStringLiteral("processOutputWidget"));
26 140 : auto *layout = new QHBoxLayout(contents);
27 280 : layout->setObjectName(QStringLiteral("processOutputLayout"));
28 140 : layout->setContentsMargins(0, 0, 0, 0);
29 140 : m_clearButton = new QToolButton(contents);
30 280 : m_clearButton->setObjectName(QStringLiteral("clearOutputButton"));
31 280 : m_clearButton->setText(tr("Clear"));
32 140 : m_clearButton->setToolTip(tr("Clear output"));
33 140 : layout->addWidget(m_clearButton);
34 140 : m_edit = new QTextEdit(contents);
35 280 : m_edit->setObjectName(QStringLiteral("processOutputEdit"));
36 140 : m_edit->setReadOnly(true);
37 140 : m_edit->setAcceptRichText(false);
38 140 : layout->addWidget(m_edit);
39 140 : setWidget(contents);
40 :
41 140 : connect(m_clearButton, &QToolButton::clicked, this,
42 140 : &ProcessOutputPanel::clear);
43 :
44 : // Hysteresis: while the user is actively dragging the slider, don't
45 : // touch m_autoScroll on every tick — a brief overshoot at maximum
46 : // would silently re-arm auto-scroll without an explicit release. Only
47 : // commit on slider release. Wheel/keyboard scroll never sets
48 : // isSliderDown(), so they still update immediately.
49 140 : QScrollBar *bar = m_edit->verticalScrollBar();
50 140 : connect(bar, &QScrollBar::valueChanged, this, [this, bar]() {
51 116 : if (bar->isSliderDown())
52 : return;
53 113 : m_autoScroll = bar->value() >= bar->maximum();
54 : });
55 140 : connect(bar, &QScrollBar::sliderReleased, this,
56 142 : [this, bar]() { m_autoScroll = bar->value() >= bar->maximum(); });
57 140 : }
58 :
59 20 : void ProcessOutputPanel::append(const QString &output, bool isError,
60 : const QString &linePrefix) {
61 20 : QStringList lines = output.split('\n', Qt::SkipEmptyParts);
62 1166 : for (QString &line : lines) {
63 : // Right-trim only: remove trailing CR and whitespace, preserve leading
64 : // indentation
65 1146 : line.remove('\r');
66 2303 : while (!line.isEmpty() && line.back().isSpace()) {
67 6 : line.chop(1);
68 : }
69 1146 : if (line.isEmpty()) {
70 1 : continue;
71 : }
72 :
73 1145 : m_counter++;
74 : const QColor textColor =
75 2285 : isError ? QColor(Qt::red) : m_edit->palette().color(QPalette::Text);
76 : // Apply the optional prefix per line so multi-line output stays
77 : // attributed to its command (e.g. all 3 lines of a `git push` show
78 : // "git push: ..." rather than only the first).
79 : const QString prefixed =
80 2308 : linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
81 : // white-space: pre keeps the indentation the trimming above preserved;
82 : // rich text would otherwise collapse it.
83 2290 : m_edit->append(
84 1145 : QStringLiteral(
85 : "<span style=\"color: %1; white-space: pre;\">%2: %3</span>")
86 3435 : .arg(textColor.name(), QString::number(m_counter),
87 2290 : prefixed.toHtmlEscaped()));
88 : }
89 :
90 20 : limitLines();
91 :
92 20 : if (m_autoScroll) {
93 38 : m_edit->verticalScrollBar()->setValue(
94 19 : m_edit->verticalScrollBar()->maximum());
95 : }
96 20 : }
97 :
98 20 : void ProcessOutputPanel::limitLines() {
99 20 : QTextDocument *doc = m_edit->document();
100 20 : const int excess = doc->blockCount() - MaxLines;
101 20 : if (excess <= 0) {
102 19 : return;
103 : }
104 1 : QTextCursor cursor(doc);
105 1 : cursor.movePosition(QTextCursor::Start);
106 1 : cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
107 1 : cursor.removeSelectedText();
108 1 : }
109 :
110 1 : void ProcessOutputPanel::clear() {
111 1 : m_edit->clear();
112 1 : m_counter = 0;
113 1 : m_autoScroll = true;
114 1 : }
115 :
116 30 : auto ProcessOutputPanel::processName(Enums::PROCESS pid) -> QString {
117 30 : return Enums::processLabel(pid);
118 : }
119 :
120 12 : auto ProcessOutputPanel::isSensitiveProcess(Enums::PROCESS pid) -> bool {
121 12 : return Enums::processInfo(pid).secret;
122 : }
|