QtPass Developer documentation
QtPass test coverage
Current view: top level - src/processoutputpanel.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 100.0 % 69 69
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 8 8

            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              : }
        

Generated by: LCOV version 2.0-1