• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 32877107209

25 Aug 2026 05:18PM UTC coverage: 64.269%. First build
32877107209

Pull #1625

github

web-flow
Merge d0ca44ddb into 3f4b12cb0
Pull Request #1625: feat: native RFC 6238 TOTP, replacing the pass-otp shell-out

406 of 496 new or added lines in 11 files covered. (81.85%)

4619 of 7187 relevant lines covered (64.27%)

69.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.06
/src/mainwindow.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "mainwindow.h"
4

5
#ifdef QT_DEBUG
6
#include "debughelper.h"
7
#endif
8

9
#include "configdialog.h"
10
#include "enums.h"
11
#include "executor.h"
12
#include "exportpublickeydialog.h"
13
#include "filecontent.h"
14
#include "passworddialog.h"
15
#include "passworddisplaypanel.h"
16
#include "pathvalidator.h"
17
#include "qpushbuttonasqrcode.h"
18
#include "qpushbuttonshowpassword.h"
19
#include "qpushbuttonwithclipboard.h"
20
#include "qtcompat.h"
21
#include "qtpass.h"
22
#include "qtpasssettings.h"
23
#include "templateio.h"
24
#include "totp.h"
25
#include "trayicon.h"
26
#include "ui_mainwindow.h"
27
#include "usersdialog.h"
28
#include "util.h"
29
#include <QApplication>
30
#include <QCloseEvent>
31
#include <QDesktopServices>
32
#include <QDialog>
33
#include <QDirIterator>
34
#include <QDockWidget>
35
#include <QFileInfo>
36
#include <QHBoxLayout>
37
#include <QInputDialog>
38
#include <QLabel>
39
#include <QLineEdit>
40
#include <QMenu>
41
#include <QMessageBox>
42
#include <QPushButton>
43
#include <QScrollBar>
44
#include <QShortcut>
45
#include <QTextCursor>
46
#include <QTextEdit>
47
#include <QTimer>
48
#include <QToolButton>
49
#include <QTreeWidget>
50
#include <QUrl>
51
#include <utility>
52

53
/**
54
 * @brief MainWindow::MainWindow handles all of the main functionality and also
55
 * the main window.
56
 * @param searchText for searching from cli
57
 * @param parent pointer
58
 */
59
MainWindow::MainWindow(const QString &searchText, QWidget *parent)
16✔
60
    : QMainWindow(parent), ui(new Ui::MainWindow) {
16✔
61
#ifdef __APPLE__
62
  // extra treatment for mac os
63
  // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
64
  qt_set_sequence_auto_mnemonic(true);
65
#endif
66
  ui->setupUi(this);
16✔
67

68
  m_qtPass = new QtPass(this);
16✔
69

70
  // register shortcut ctrl/cmd + Q to close the main window
71
  new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
16✔
72
  // register shortcut ctrl/cmd + C to copy the currently selected password
73
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this,
32✔
74
                SLOT(copyPasswordFromTreeview()));
32✔
75

76
  model.setNameFilters(QStringList() << "*.gpg");
48✔
77
  model.setNameFilterDisables(false);
16✔
78

79
  /*
80
   * I added this to solve Windows bug but now on GNU/Linux the main folder,
81
   * if hidden, disappear
82
   *
83
   * model.setFilter(QDir::NoDot);
84
   */
85

86
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
16✔
87

88
  QModelIndex rootDir = model.setRootPath(passStore);
16✔
89
  model.fetchMore(rootDir);
16✔
90

91
  proxyModel.setModelAndStore(&model, passStore);
16✔
92
  proxyModel.setPass(QtPassSettings::getPass());
16✔
93
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
16✔
94

95
  ui->treeView->setModel(&proxyModel);
16✔
96
  ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir));
16✔
97
  ui->treeView->setColumnHidden(1, true);
16✔
98
  ui->treeView->setColumnHidden(2, true);
16✔
99
  ui->treeView->setColumnHidden(3, true);
16✔
100
  ui->treeView->setHeaderHidden(true);
16✔
101
  ui->treeView->setIndentation(15);
16✔
102
  ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
16✔
103
  ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
16✔
104
  ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
16✔
105
  ui->treeView->sortByColumn(0, Qt::AscendingOrder);
16✔
106
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
16✔
107
          &MainWindow::showContextMenu);
16✔
108
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
16✔
109
          &MainWindow::deselect);
16✔
110

111
  {
112
    const AppSettings s = QtPassSettings::load();
16✔
113
    if (s.useMonospace) {
16✔
114
      QFont monospace("Monospace");
×
115
      monospace.setStyleHint(QFont::Monospace);
×
116
      ui->textBrowser->setFont(monospace);
×
117
    }
×
118
    if (s.noLineWrapping) {
16✔
119
      ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
120
    }
121
    clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
16✔
122
  }
16✔
123
  ui->textBrowser->setOpenExternalLinks(true);
16✔
124
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
16✔
125
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
16✔
126
          &MainWindow::showBrowserContextMenu);
16✔
127

128
  updateProfileBox();
16✔
129

130
  m_displayPanel = new PasswordDisplayPanel(
16✔
131
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
16✔
132
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested, m_qtPass,
16✔
133
          &QtPass::copyTextToClipboard);
16✔
134
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
16✔
135
          &QtPass::showTextAsQRCode);
16✔
136

137
  QtPassSettings::getPass()->updateEnv();
16✔
138
  clearPanelTimer.setSingleShot(true);
16✔
139
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
16✔
140

141
  searchTimer.setInterval(350);
16✔
142
  searchTimer.setSingleShot(true);
16✔
143

144
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
16✔
145

146
  // Install the search-box key filter once, not on every setUiElementsEnabled
147
  // call.
148
  ui->lineEdit->installEventFilter(this);
16✔
149

150
  // Safety net: if a backend operation disables the UI but never signals
151
  // completion, re-enable after a timeout so the window can't get stuck.
152
  m_uiWatchdog.setSingleShot(true);
16✔
153
  m_uiWatchdog.setInterval(UiWatchdogMs);
16✔
154
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
16✔
155
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
156
    // Drop any in-flight OTP request so a late finishedShow cannot be mistaken
157
    // for the answer to it.
NEW
158
    cancelOtpRequest();
×
159
    setUiElementsEnabled(true);
×
160
  });
×
161

162
  initToolBarButtons();
16✔
163
  initStatusBar();
16✔
164
  initProcessOutputPanel();
16✔
165

166
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
16✔
167
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
32✔
168
            // Never route potentially-secret output through the panel:
169
            // - PASS_SHOW / PASS_OTP_GENERATE go via dedicated signals to
170
            //   the main text browser (which clears on a timer).
171
            // - PASS_GREP returns lines from password files; #252 must
172
            //   not leak those into a long-lived panel.
173
            // - PASS_INSERT's stdin is the password; stdout normally
174
            //   carries gpg/git progress only, but exclude defensively
175
            //   in case a future code path uses --echo or similar.
176
            if (isSensitiveProcess(pid)) {
×
177
              return;
178
            }
179
            if (!out.isEmpty()) {
×
180
              onProcessOutput(out, false, pid);
×
181
            }
182
            if (!err.isEmpty()) {
×
183
              onProcessOutput(err, true, pid);
×
184
            }
185
          });
186

187
  ui->lineEdit->setClearButtonEnabled(true);
16✔
188
  updateGrepButtonVisibility();
16✔
189

190
  setUiElementsEnabled(true);
16✔
191

192
  ui->lineEdit->setText(searchText);
16✔
193

194
  if (!m_qtPass->init()) {
16✔
195
    // no working config so this should just quit
196
    QApplication::quit();
×
197
    return;
198
  }
199

200
  // Initial focus is handled in showEvent() once the window is actually
201
  // mapped. Scheduling it here via a 10 ms QTimer was racy: if the timer
202
  // fires while the window has not yet been realised — e.g. an
203
  // ActivationChange queued by main()'s `activateWindow()` call before
204
  // `show()`, or a nested QDialog::exec() inside init() — the
205
  // QLineEdit's internal text engine hasn't been wired up and
206
  // selectAll() segfaults inside Qt (see #1187, #1188).
207
}
×
208

209
MainWindow::~MainWindow() { delete m_qtPass; }
62✔
210

211
/**
212
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
213
 * box and sets focus to it. Allows for easy searching, called at application
214
 * start and when receiving empty message in MainWindow::messageAvailable when
215
 * compiled with SINGLE_APP=1 (default).
216
 */
217
void MainWindow::focusInput() {
4✔
218
  // Resolve the QLineEdit through the live widget tree rather than the
219
  // cached `ui->lineEdit` pointer.
220
  //
221
  // On a fresh-config first launch the constructor calls
222
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
223
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
224
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
225
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
226
  // we observed the QLineEdit attached to the centralWidget gets
227
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
228
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
229
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
230
  // walks the current hierarchy and returns null cleanly when the
231
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
232
  // use-after-free.
233
  if (!isVisible()) {
4✔
234
    return;
235
  }
236
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
4✔
237
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
4✔
238
    return;
239
  }
240
  lineEdit->selectAll();
4✔
241
  lineEdit->setFocus();
4✔
242
  // Only mark the first-show focus pulse as done once it's actually
243
  // landed; setting it eagerly in showEvent() would consume the
244
  // one-shot if focusInput returned early (mid-rebuild widget state)
245
  // and we'd never retry.
246
  m_firstShowCompleted = true;
4✔
247
}
248

249
/**
250
 * @brief MainWindow::changeEvent sets focus to the search box
251
 * @param event
252
 */
253
void MainWindow::changeEvent(QEvent *event) {
19✔
254
  QWidget::changeEvent(event);
19✔
255
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
19✔
256
      isVisible()) {
257
    // Defer one event-loop tick so the synchronous activation dispatch
258
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
259
    // unwinds before we touch widget state — calling `focusInput()`
260
    // inline from this stack has segfaulted in past iterations because
261
    // mid-rebuild ui state isn't fully wired up yet.
262
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
2✔
263
                              Qt::QueuedConnection);
264
  }
265
}
19✔
266

267
/**
268
 * @brief First-show hook: run the initial focusInput() pulse once the
269
 *        window is actually mapped. The widget's internal data is fully
270
 *        initialised by this point, so QLineEdit::selectAll() is safe.
271
 * @param event Show event passed to the base class.
272
 */
273
void MainWindow::showEvent(QShowEvent *event) {
2✔
274
  QMainWindow::showEvent(event);
2✔
275
  if (m_firstShowCompleted) {
2✔
276
    return;
277
  }
278
  // Queue the focus pulse for the next event-loop tick so the platform
279
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
280
  // from the config wizard path) settle before we look up the line
281
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
282
  // *after* it actually focuses, so a transient failed lookup just
283
  // re-queues on the next show rather than silently dropping.
284
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
2✔
285
                            Qt::QueuedConnection);
286
}
287

288
/**
289
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
290
 */
291
void MainWindow::initToolBarButtons() {
16✔
292
  connect(ui->actionAddPassword, &QAction::triggered, this,
16✔
293
          &MainWindow::addPassword);
16✔
294
  connect(ui->actionAddFolder, &QAction::triggered, this,
16✔
295
          &MainWindow::addFolder);
16✔
296
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
16✔
297
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
16✔
298
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
16✔
299
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
16✔
300
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
16✔
301
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
16✔
302
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
16✔
303

304
  ui->actionAddPassword->setIcon(
16✔
305
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
64✔
306
  ui->actionAddFolder->setIcon(
16✔
307
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
64✔
308
  ui->actionEdit->setIcon(QIcon::fromTheme(
16✔
309
      "document-properties", QIcon(":/icons/document-properties.svg")));
48✔
310
  ui->actionDelete->setIcon(
16✔
311
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
64✔
312
  ui->actionPush->setIcon(
16✔
313
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
64✔
314
  ui->actionUpdate->setIcon(
16✔
315
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
64✔
316
  ui->actionUsers->setIcon(QIcon::fromTheme(
16✔
317
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
48✔
318
  ui->actionConfig->setIcon(QIcon::fromTheme(
16✔
319
      "applications-system", QIcon(":/icons/applications-system.svg")));
32✔
320
}
16✔
321

322
/**
323
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
324
 */
325
void MainWindow::initStatusBar() {
16✔
326
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
32✔
327

328
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
32✔
329
                     .scaledToHeight(statusBar()->height());
16✔
330
  auto *logoApp = new QLabel(statusBar());
16✔
331
  logoApp->setPixmap(logo);
16✔
332
  statusBar()->addPermanentWidget(logoApp);
16✔
333
}
16✔
334

335
/**
336
 * @brief Build the process-output panel as a bottom QDockWidget.
337
 *
338
 * The panel is constructed programmatically rather than declared in
339
 * mainwindow.ui: uic only places QMainWindow's top-level children into
340
 * the centralWidget / statusBar / menuBar / toolBars / dock-widget
341
 * slots, and the previous home (statusBar()->addPermanentWidget()) made
342
 * an 80–150 px tall QTextEdit sit inside what is otherwise a thin
343
 * status row. A QDockWidget at the bottom dock area is the conventional
344
 * place for an IDE-style output console, and it gives users
345
 * detach/move for free.
346
 */
347
void MainWindow::initProcessOutputPanel() {
16✔
348
  m_processOutputWidget = new QWidget;
16✔
349
  m_processOutputWidget->setObjectName(QStringLiteral("processOutputWidget"));
32✔
350
  auto *outputLayout = new QHBoxLayout(m_processOutputWidget);
16✔
351
  outputLayout->setObjectName(QStringLiteral("processOutputLayout"));
32✔
352
  outputLayout->setContentsMargins(0, 0, 0, 0);
16✔
353
  m_clearOutputButton = new QToolButton(m_processOutputWidget);
16✔
354
  m_clearOutputButton->setObjectName(QStringLiteral("clearOutputButton"));
32✔
355
  m_clearOutputButton->setText(tr("Clear"));
16✔
356
  m_clearOutputButton->setToolTip(tr("Clear output"));
16✔
357
  outputLayout->addWidget(m_clearOutputButton);
16✔
358
  m_processOutputEdit = new QTextEdit(m_processOutputWidget);
16✔
359
  m_processOutputEdit->setObjectName(QStringLiteral("processOutputEdit"));
32✔
360
  m_processOutputEdit->setReadOnly(true);
16✔
361
  m_processOutputEdit->setAcceptRichText(false);
16✔
362
  outputLayout->addWidget(m_processOutputEdit);
16✔
363

364
  m_processOutputDock = new QDockWidget(tr("Process Output"), this);
16✔
365
  m_processOutputDock->setObjectName(QStringLiteral("processOutputDock"));
32✔
366
  m_processOutputDock->setFeatures(QDockWidget::DockWidgetMovable |
16✔
367
                                   QDockWidget::DockWidgetFloatable);
368
  m_processOutputDock->setAllowedAreas(Qt::BottomDockWidgetArea |
16✔
369
                                       Qt::TopDockWidgetArea);
370
  m_processOutputDock->setWidget(m_processOutputWidget);
16✔
371
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutputDock);
16✔
372
  // setVisible after addDockWidget so our explicit preference wins
373
  // even if QMainWindow applies any cached state when the dock is
374
  // attached. restoreWindow() runs before this method (it's called
375
  // from the QtPass ctor, which is constructed at the top of the
376
  // MainWindow ctor), so the saved layout has already been processed
377
  // by the time we get here.
378
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
16✔
379

380
  connect(m_clearOutputButton, &QToolButton::clicked, this,
16✔
381
          &MainWindow::on_clearOutputButton_clicked);
16✔
382

383
  // Hysteresis: while the user is actively dragging the slider, don't
384
  // touch m_autoScroll on every tick — a brief overshoot at maximum
385
  // would silently re-arm auto-scroll without an explicit release. Only
386
  // commit on slider release. Wheel/keyboard scroll never sets
387
  // isSliderDown(), so they still update immediately.
388
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
16✔
389
          this, [this]() {
16✔
390
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
391
            if (sb->isSliderDown())
×
392
              return;
393
            m_autoScroll = sb->value() >= sb->maximum();
×
394
          });
395
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderReleased,
16✔
396
          this, [this]() {
16✔
397
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
398
            m_autoScroll = sb->value() >= sb->maximum();
×
399
          });
×
400
}
16✔
401

402
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1✔
403
  return ui->treeView->currentIndex();
1✔
404
}
405

406
void MainWindow::cleanKeygenDialog() {
1✔
407
  if (m_keyGenDialog != nullptr) {
1✔
408
    m_keyGenDialog->close();
×
409
  }
410
  m_keyGenDialog = nullptr;
411
}
1✔
412

413
/**
414
 * @brief Displays the given text in the main window text browser, optionally
415
 * marking it as an error and/or rendering it as HTML.
416
 * @example
417
 * MainWindow window;
418
 * window.flashText("Operation completed.", false, false);
419
 *
420
 * @param const QString &text - The text content to display.
421
 * @param const bool isError - If true, sets the text color to red before
422
 * displaying the text.
423
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
424
 * the existing HTML content.
425
 * @return void - No return value.
426
 */
427
void MainWindow::flashText(const QString &text, const bool isError,
9✔
428
                           const bool isHtml) {
429
  if (isError) {
9✔
430
    ui->textBrowser->setTextColor(Qt::red);
2✔
431
  }
432

433
  if (isHtml) {
9✔
434
    QString _text = text;
435
    if (!ui->textBrowser->toPlainText().isEmpty()) {
12✔
436
      _text = ui->textBrowser->toHtml() + _text;
2✔
437
    }
438
    ui->textBrowser->setHtml(_text);
6✔
439
  } else {
440
    ui->textBrowser->setText(text);
3✔
441
  }
442
}
9✔
443

444
/**
445
 * @brief MainWindow::config pops up the configuration screen and handles all
446
 * inter-window communication
447
 */
448
void MainWindow::applyTextBrowserSettings() {
×
449
  const AppSettings s = QtPassSettings::load();
×
450
  if (s.useMonospace) {
×
451
    QFont monospace("Monospace");
×
452
    monospace.setStyleHint(QFont::Monospace);
×
453
    ui->textBrowser->setFont(monospace);
×
454
  } else {
×
455
    ui->textBrowser->setFont(QFont());
×
456
  }
457

458
  if (s.noLineWrapping) {
×
459
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
460
  } else {
461
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
462
  }
463
}
×
464

465
void MainWindow::applyWindowFlagsSettings() {
×
466
  if (QtPassSettings::isAlwaysOnTop()) {
×
467
    Qt::WindowFlags flags = windowFlags();
468
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
469
  } else {
470
    this->setWindowFlags(Qt::Window);
×
471
  }
472
  this->show();
×
473
}
×
474

475
/**
476
 * @brief Opens and processes the application configuration dialog, then applies
477
 * any accepted settings.
478
 * @example
479
 * config();
480
 *
481
 * @return void - This function does not return a value.
482
 */
483
void MainWindow::config() {
×
484
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
485
  d->setModal(true);
×
486
  // Automatically default to pass if it's available
487
  if (m_qtPass->isFreshStart() &&
×
488
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
489
    QtPassSettings::setUsePass(true);
×
490
  }
491

492
  if (m_qtPass->isFreshStart()) {
×
493
    d->wizard(); // run initial setup wizard for first-time configuration
×
494
  }
495
  if (d->exec()) {
×
496
    if (d->result() == QDialog::Accepted) {
×
497
      applyTextBrowserSettings();
×
498
      applyWindowFlagsSettings();
×
499

500
      updateProfileBox();
×
501
      const AppSettings s = QtPassSettings::load();
×
502
      proxyModel.setStore(s.passStore);
×
503
      ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
504
      deselect();
×
505
      ui->treeView->setCurrentIndex(QModelIndex());
×
506

507
      if (m_qtPass->isFreshStart() && !Util::configIsValid(s)) {
×
508
        config();
×
509
        return;
510
      }
511
      Pass *activePass = QtPassSettings::getPass();
×
512
      activePass->updateEnv();
×
513
      proxyModel.setPass(activePass);
×
514
      clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
×
515
      m_qtPass->setClipboardTimer();
×
516

517
      updateGitButtonVisibility();
×
518
      updateOtpButtonVisibility();
×
519
      updateGrepButtonVisibility();
×
520
      updateProcessOutputVisibility();
×
521
      if (s.useTrayIcon && m_tray == nullptr) {
×
522
        initTrayIcon();
×
523
      } else if (!s.useTrayIcon && m_tray != nullptr) {
×
524
        destroyTrayIcon();
×
525
      }
526
    }
×
527

528
    m_qtPass->setFreshStart(false);
×
529
  }
530
}
×
531

532
/**
533
 * @brief MainWindow::onUpdate do a git pull
534
 */
535
void MainWindow::onUpdate(bool block) {
×
536
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
537
  if (block) {
×
538
    QtPassSettings::getPass()->GitPull_b();
×
539
  } else {
540
    QtPassSettings::getPass()->GitPull();
×
541
  }
542
}
×
543

544
/**
545
 * @brief MainWindow::onPush do a git push
546
 */
547
void MainWindow::onPush() {
×
548
  if (QtPassSettings::isUseGit()) {
×
549
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
550
    QtPassSettings::getPass()->GitPush();
×
551
  }
552
}
×
553

554
/**
555
 * @brief MainWindow::getFile get the selected file path
556
 * @param index
557
 * @param forPass returns relative path without '.gpg' extension
558
 * @return path
559
 * @return
560
 */
561
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
3✔
562
  if (!index.isValid() ||
3✔
563
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
6✔
564
    return {};
565
  }
566
  QString filePath = model.filePath(proxyModel.mapToSource(index));
3✔
567
  if (forPass) {
3✔
568
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
9✔
569
    filePath.replace(Util::endsWithGpg(), "");
6✔
570
  }
571
  return filePath;
572
}
573

574
/**
575
 * @brief MainWindow::on_treeView_clicked read the selected password file
576
 * @param index
577
 */
578
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
2✔
579
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
2✔
580
  m_currentDir = Util::getDir(ui->treeView->currentIndex(), false, model,
4✔
581
                              proxyModel, QtPassSettings::getPassStore());
4✔
582
  QString file = getFile(index, true);
2✔
583
  ui->passwordName->setText(file);
2✔
584
  if (!file.isEmpty() && !cleared) {
2✔
585
    // Remember what the panel is about to show, so onOtp() can tell whether the
586
    // code it can see belongs to the entry that is currently selected.
587
    m_shownFile = file;
2✔
588
    QtPassSettings::getPass()->Show(file);
4✔
589
  } else {
NEW
590
    m_shownFile.clear();
×
591
    clearPanel(false);
×
592
    ui->actionEdit->setEnabled(false);
×
593
    ui->actionDelete->setEnabled(true);
×
594
  }
595
}
2✔
596

597
/**
598
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
599
 * TreeViewItem, open the edit Window
600
 * @param index
601
 */
602
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
1✔
603
  QFileInfo fileOrFolder =
604
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
1✔
605

606
  if (fileOrFolder.isFile()) {
1✔
607
    editPassword(getFile(index, true));
2✔
608
  }
609
}
1✔
610

611
/**
612
 * @brief MainWindow::deselect clear the selection, password and copy buffer
613
 */
614
void MainWindow::deselect() {
1✔
615
  m_currentDir = "";
1✔
616
  m_shownFile.clear();
1✔
617
  cancelOtpRequest();
1✔
618
  m_qtPass->clearClipboard();
1✔
619
  ui->treeView->clearSelection();
1✔
620
  ui->actionEdit->setEnabled(false);
1✔
621
  ui->actionDelete->setEnabled(false);
1✔
622
  ui->passwordName->setText("");
1✔
623
  clearPanel(false);
1✔
624
}
1✔
625

626
void MainWindow::executeWrapperStarted() {
5✔
627
  m_displayPanel->clear();
5✔
628
  ui->textBrowser->clear();
5✔
629
  setUiElementsEnabled(false);
5✔
630
  clearPanelTimer.stop();
5✔
631
  if (QtPassSettings::isShowProcessOutput()) {
5✔
632
    m_processOutputDock->setVisible(true);
×
633
  }
634
}
5✔
635

636
/**
637
 * @brief Handles displaying parsed password entry content in the main window.
638
 * @example
639
 * void result = MainWindow::passShowHandler(p_output);
640
 * // Updates the UI with parsed fields and emits
641
 * passShowHandlerFinished(output)
642
 *
643
 * @param p_output - The raw output text containing the password entry data.
644
 * @return void - This function does not return a value.
645
 */
646
void MainWindow::passShowHandler(const QString &p_output) {
3✔
647
  const AppSettings s = QtPassSettings::load();
3✔
648
  QStringList templ =
649
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
3✔
650
  bool allFields = s.useTemplate && s.templateAllFields;
3✔
651
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
3✔
652
  QString output = p_output;
653
  // Display variant: empty when the password line is itself an otpauth URI, so
654
  // the shared secret is neither rendered nor copied to the clipboard.
655
  QString password = fileContent.getPasswordForDisplay();
3✔
656

657
  // set clipped text
658
  //
659
  // Skipped for an OTP request: the user asked for a one-time code, not the
660
  // password, and writing both in one event-loop turn can leave the Windows
661
  // clipboard empty (two OleSetClipboard calls back to back).
662
  if (!m_otpRequestPending) {
3✔
663
    m_qtPass->setClippedText(password, p_output);
3✔
664
  }
665

666
  // first clear the current view:
667
  m_displayPanel->clear();
3✔
668

669
  // show what is needed:
670
  if (s.hideContent) {
3✔
671
    output = "***" + tr("Content hidden") + "***";
×
672
  } else if (!s.displayAsIs) {
3✔
673
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
6✔
674
                                  s.useOtp ? fileContent.getOtpUri()
3✔
675
                                           : QString());
676
    output = fileContent.getRemainingDataForDisplay();
6✔
677
  }
678

679
  if (s.useAutoclearPanel) {
3✔
680
    clearPanelTimer.start();
×
681
  }
682

683
  emit passShowHandlerFinished(output);
3✔
684
  setUiElementsEnabled(true);
3✔
685
}
6✔
686

687
/**
688
 * @brief Generates a one-time password from a decrypted entry and copies it.
689
 *
690
 * Connected as a one-shot to Pass::finishedShow by onOtp(). passShowHandler is
691
 * connected first, so by the time this runs the panel has already been
692
 * repainted and the UI re-enabled.
693
 *
694
 * @param p_output - The decrypted entry content.
695
 * @return void - This function does not return a value.
696
 */
NEW
697
void MainWindow::otpFromFileToClipboard(const QString &p_output) {
×
NEW
698
  disconnectSingleShot(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
699
                       &MainWindow::otpFromFileToClipboard);
700
  // A failed decrypt never fires finishedShow, and Qt::SingleShotConnection
701
  // only self-disconnects when it does fire, so a connection armed by an
702
  // earlier failed request can still be live here. Ignore it rather than
703
  // hijacking an unrelated entry's decrypted content.
NEW
704
  if (!m_otpRequestPending) {
×
NEW
705
    return;
×
706
  }
707
  // finishedShow carries no request identity, so make sure this decrypt is the
708
  // one we asked for and not a tree click that happened to land first.
NEW
709
  if (m_otpRequestFile != getFile(ui->treeView->currentIndex(), true)) {
×
NEW
710
    cancelOtpRequest();
×
NEW
711
    setUiElementsEnabled(true);
×
NEW
712
    return;
×
713
  }
NEW
714
  m_otpRequestPending = false;
×
NEW
715
  m_otpRequestFile.clear();
×
716

NEW
717
  if (p_output.isEmpty()) {
×
718
    // Distinguish "could not read the entry" from "entry has no OTP".
NEW
719
    flashText(tr("Could not decrypt this password entry"), true);
×
NEW
720
    setUiElementsEnabled(true);
×
NEW
721
    return;
×
722
  }
723

724
  // passShowHandler is connected first, so it has already repainted the panel
725
  // for this same finishedShow. When it rendered the OTP row the current code
726
  // is derived and cached, so reuse it instead of re-loading settings and
727
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
728
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
NEW
729
  const QString shown = m_displayPanel->currentOtpCode();
×
NEW
730
  if (!shown.isEmpty()) {
×
NEW
731
    m_qtPass->copyTextToClipboard(shown);
×
NEW
732
    showStatusMessage(tr("OTP code copied to clipboard"));
×
NEW
733
    setUiElementsEnabled(true);
×
734
    return;
735
  }
736

737
  const AppSettings s = QtPassSettings::load();
×
738
  // Parse with the same template settings passShowHandler uses, so an OTP
739
  // field is recognised identically in both paths.
740
  const QStringList templ =
NEW
741
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
×
NEW
742
  const bool allFields = s.useTemplate && s.templateAllFields;
×
743
  const FileContent fileContent =
NEW
744
      FileContent::parse(p_output, templ, allFields);
×
745

746
  const std::optional<Totp::Settings> settings =
NEW
747
      Totp::parse(fileContent.getOtpUri());
×
NEW
748
  if (settings.has_value()) {
×
NEW
749
    m_qtPass->copyTextToClipboard(Totp::generateNow(*settings));
×
750
    showStatusMessage(tr("OTP code copied to clipboard"));
×
751
  } else {
752
    flashText(tr("No OTP code found in this password entry"), true);
×
753
  }
754
  setUiElementsEnabled(true);
×
755
}
×
756

757
/**
758
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
759
 */
760
void MainWindow::clearPanel(bool notify) {
1✔
761
  m_displayPanel->clear();
1✔
762
  const bool grepWasVisible = ui->grepResultsList->isVisible();
1✔
763
  ui->grepResultsList->clear();
1✔
764
  if (grepWasVisible) {
1✔
765
    ui->grepResultsList->setVisible(false);
×
766
    ui->treeView->setVisible(true);
×
767
    if (m_grep.inGrepMode()) {
×
768
      m_grep.clearGrepMode();
769
      ui->grepButton->blockSignals(true);
×
770
      ui->grepButton->setChecked(false);
×
771
      ui->grepButton->blockSignals(false);
×
772
      ui->lineEdit->blockSignals(true);
×
773
      ui->lineEdit->clear();
×
774
      ui->lineEdit->blockSignals(false);
×
775
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
776
    }
777
  }
778
  if (notify) {
1✔
779
    QString output = "***" + tr("Password and Content hidden") + "***";
×
780
    ui->textBrowser->setHtml(output);
×
781
  } else {
782
    ui->textBrowser->setHtml("");
2✔
783
  }
784
}
1✔
785

786
/**
787
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
788
 * elements
789
 * @param state
790
 */
791
void MainWindow::setUiElementsEnabled(bool state) {
29✔
792
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
793
  if (state) {
29✔
794
    m_uiWatchdog.stop();
22✔
795
  } else {
796
    m_uiWatchdog.start();
7✔
797
  }
798
  ui->treeView->setEnabled(state);
29✔
799
  ui->lineEdit->setEnabled(state);
29✔
800
  ui->actionAddPassword->setEnabled(state);
29✔
801
  ui->actionAddFolder->setEnabled(state);
29✔
802
  ui->actionUsers->setEnabled(state);
29✔
803
  ui->actionConfig->setEnabled(state);
29✔
804
  // is a file selected?
805
  state &= ui->treeView->currentIndex().isValid();
58✔
806
  ui->actionDelete->setEnabled(state);
29✔
807
  ui->actionEdit->setEnabled(state);
29✔
808
  updateGitButtonVisibility();
29✔
809
  // `state` is now "UI enabled AND a file is selected", which is exactly when
810
  // generating an OTP makes sense.
811
  updateOtpButtonVisibility(state);
29✔
812
}
29✔
813

814
/**
815
 * @brief Restores the main window geometry, state, position, size, and
816
 * tray/icon settings from saved application settings.
817
 * @example
818
 * MainWindow window;
819
 * window.restoreWindow();
820
 *
821
 * @return void - This function does not return a value.
822
 */
823
void MainWindow::restoreWindow() {
16✔
824
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
16✔
825
  restoreGeometry(geometry);
16✔
826
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
16✔
827
  restoreState(savestate);
16✔
828
  QPoint position = QtPassSettings::getPos(pos());
16✔
829
  move(position);
16✔
830
  QSize newSize = QtPassSettings::getSize(size());
16✔
831
  resize(newSize);
16✔
832
  const AppSettings s = QtPassSettings::load();
16✔
833
  if (s.maximized) {
16✔
834
    showMaximized();
×
835
  }
836

837
  if (s.alwaysOnTop) {
16✔
838
    Qt::WindowFlags flags = windowFlags();
839
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
840
    show();
×
841
  }
842

843
  if (s.useTrayIcon && m_tray == nullptr) {
16✔
844
    initTrayIcon();
×
845
    if (s.startMinimized) {
×
846
      // since we are still in constructor, can't directly hide
847
      QTimer::singleShot(10, this, SLOT(hide()));
×
848
    }
849
  } else if (!s.useTrayIcon && m_tray != nullptr) {
16✔
850
    destroyTrayIcon();
×
851
  }
852
}
32✔
853

854
/**
855
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
856
 */
857
void MainWindow::onConfig() { config(); }
×
858

859
/**
860
 * @brief Executes when the string in the search box changes, collapses the
861
 * TreeView
862
 * @param arg1
863
 */
864
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
865
  if (m_grep.inGrepMode())
×
866
    return;
867
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
868
  ui->treeView->expandAll();
×
869
  clearPanel(false);
×
870
  ui->passwordName->setText("");
×
871
  ui->actionEdit->setEnabled(false);
×
872
  ui->actionDelete->setEnabled(false);
×
873
  searchTimer.start();
×
874
}
875

876
/**
877
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
878
 * time from two keypresses is elapsed
879
 */
880
void MainWindow::onTimeoutSearch() {
×
881
  QString query = ui->lineEdit->text();
×
882

883
  if (query.isEmpty()) {
×
884
    ui->treeView->collapseAll();
×
885
    deselect();
×
886
  }
887

888
  query.replace(QStringLiteral(" "), ".*");
×
889
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
890
  if (!regExp.isValid())
×
891
    return;
892
  proxyModel.setFilterRegularExpression(regExp);
×
893
  ui->treeView->setRootIndex(
×
894
      proxyModel.rootIndexFor(QtPassSettings::getPassStore()));
×
895

896
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
897
    selectFirstFile();
×
898
  } else {
899
    ui->actionEdit->setEnabled(false);
×
900
    ui->actionDelete->setEnabled(false);
×
901
  }
902
}
×
903

904
/**
905
 * @brief MainWindow::on_lineEdit_returnPressed get searching
906
 *
907
 * Select the first possible file in the tree
908
 */
909
void MainWindow::on_lineEdit_returnPressed() {
×
910
#ifdef QT_DEBUG
911
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
912
#endif
913

914
  if (m_grep.inGrepMode()) {
×
915
    const QString query = ui->lineEdit->text();
×
916
    if (!query.isEmpty()) {
×
917
      ui->grepResultsList->clear();
×
918
      ui->statusBar->showMessage(tr("Searching…"));
×
919
      if (m_grep.beginSearch()) {
920
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
921
      }
922
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
923
    } else {
924
      if (m_grep.cancelSearch()) {
925
        QApplication::restoreOverrideCursor();
×
926
      }
927
      ui->grepResultsList->clear();
×
928
      ui->grepResultsList->setVisible(false);
×
929
      ui->treeView->setVisible(true);
×
930
    }
931
    return;
932
  }
933

934
  if (proxyModel.rowCount() > 0) {
×
935
    selectFirstFile();
×
936
    on_treeView_clicked(ui->treeView->currentIndex());
×
937
  }
938
}
939

940
/**
941
 * @brief Toggle grep (content search) mode.
942
 */
943
void MainWindow::on_grepButton_toggled(bool checked) {
×
944
  const AppSettings s = QtPassSettings::load();
×
945
  if (checked) {
×
946
    m_grep.enterGrepMode();
947
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
948
    // The regex dialect depends on the backend (see Pass::Grep): the pass
949
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
950
    ui->lineEdit->setToolTip(
×
951
        s.usePass
×
952
            ? tr("Content search uses POSIX basic regular expressions "
×
953
                 "(pass grep).")
954
            : tr("Content search uses Perl-compatible regular expressions "
955
                 "(PCRE)."));
956
    ui->lineEdit->clear();
×
957
    searchTimer.stop();
×
958
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
959
    ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
960
    ui->grepResultsList->setVisible(false);
×
961
    // Keep treeView visible until results arrive
962
  } else {
963
    if (m_grep.leaveGrepMode()) {
964
      QApplication::restoreOverrideCursor();
×
965
    }
966
    searchTimer.stop();
×
967
    ui->lineEdit->blockSignals(true);
×
968
    ui->lineEdit->clear();
×
969
    ui->lineEdit->blockSignals(false);
×
970
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
971
    ui->lineEdit->setToolTip(QString());
×
972
    ui->grepResultsList->clear();
×
973
    ui->grepResultsList->setVisible(false);
×
974
    ui->treeView->setVisible(true);
×
975
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
976
    ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
977
  }
978
}
×
979

980
/**
981
 * @brief Display grep results in grepResultsList.
982
 */
983
void MainWindow::onGrepFinished(
×
984
    const QList<QPair<QString, QStringList>> &results) {
985
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
986
  if (outcome.restoreCursor) {
×
987
    QApplication::restoreOverrideCursor();
×
988
  }
989
  // Re-enable the UI before the discard check so a cancelled search can never
990
  // leave controls disabled.
991
  setUiElementsEnabled(true);
×
992
  if (outcome.discard) {
×
993
    return;
×
994
  }
995
  if (!m_grep.inGrepMode())
×
996
    return;
997
  ui->grepResultsList->clear();
×
998
  if (results.isEmpty()) {
×
999
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
1000
    ui->grepResultsList->setVisible(false);
×
1001
    ui->treeView->setVisible(true);
×
1002
    return;
×
1003
  }
1004
  const AppSettings s = QtPassSettings::load();
×
1005
  const bool hideContent = s.hideContent;
×
1006
  int totalLines = 0;
1007
  for (const auto &pair : results) {
×
1008
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
1009
    entryItem->setText(0, pair.first);
×
1010
    entryItem->setData(0, Qt::UserRole, pair.first);
×
1011
    for (const QString &line : pair.second) {
×
1012
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
1013
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
1014
                                       : line);
1015
      lineItem->setData(0, Qt::UserRole, pair.first);
×
1016
      ++totalLines;
×
1017
    }
1018
  }
1019
  ui->grepResultsList->expandAll();
×
1020
  ui->treeView->setVisible(false);
×
1021
  ui->grepResultsList->setVisible(true);
×
1022
  ui->statusBar->showMessage(
×
1023
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
1024
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
1025
      3000);
1026
  if (s.useAutoclearPanel)
×
1027
    clearPanelTimer.start();
×
1028
}
×
1029

1030
/**
1031
 * @brief Navigate to the password entry when a grep result is clicked.
1032
 */
1033
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
1034
                                                int /*column*/) {
1035
  const AppSettings s = QtPassSettings::load();
×
1036
  const QString entry = item->data(0, Qt::UserRole).toString();
×
1037
  if (entry.isEmpty())
×
1038
    return;
1039
  const QString fullPath =
1040
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
×
1041
  QModelIndex srcIndex = model.index(fullPath);
×
1042
  if (!srcIndex.isValid())
1043
    return;
1044
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
1045
  if (!proxyIndex.isValid())
1046
    return;
1047
  ui->treeView->setCurrentIndex(proxyIndex);
×
1048
  on_treeView_clicked(proxyIndex);
×
1049
  if (s.hideContent || s.useAutoclearPanel)
×
1050
    ui->grepResultsList->clear();
×
1051
  ui->grepResultsList->setVisible(false);
×
1052
  ui->treeView->setVisible(true);
×
1053
  ui->treeView->scrollTo(proxyIndex);
×
1054
  ui->treeView->setFocus();
×
1055
}
×
1056

1057
/**
1058
 * @brief MainWindow::selectFirstFile select the first possible file in the
1059
 * tree
1060
 */
1061
void MainWindow::selectFirstFile() {
×
1062
  QModelIndex index = proxyModel.rootIndexFor(QtPassSettings::getPassStore());
×
1063
  index = firstFile(index);
×
1064
  ui->treeView->setCurrentIndex(index);
×
1065
}
×
1066

1067
/**
1068
 * @brief MainWindow::firstFile return location of first possible file
1069
 * @param parentIndex
1070
 * @return QModelIndex
1071
 */
1072
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
1073
  int numRows = proxyModel.rowCount(parentIndex);
×
1074
  for (int row = 0; row < numRows; ++row) {
×
1075
    QModelIndex index = proxyModel.index(row, 0, parentIndex);
×
1076
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
1077
      return index;
×
1078
    }
1079
    if (proxyModel.hasChildren(index)) {
×
1080
      QModelIndex childFile = firstFile(index);
×
1081
      if (childFile.isValid())
1082
        return childFile;
×
1083
    }
1084
  }
1085
  return QModelIndex();
1086
}
1087

1088
/**
1089
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1090
 * the password store and warn the user.
1091
 *
1092
 * Used before file/folder creation, move, and rename to stop user-typed
1093
 * names like "../../etc/passwd" or absolute paths from escaping the
1094
 * configured store root via the input dialogs.
1095
 *
1096
 * @param candidate Absolute candidate path to validate.
1097
 * @return true if the path is inside the password store; false otherwise (a
1098
 * warning dialog is shown in that case).
1099
 */
1100
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1101
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1102
    return true;
1103
  }
1104
  QMessageBox::warning(this, tr("Invalid name"),
×
1105
                       tr("That name would resolve outside the password "
×
1106
                          "store. Please choose a different name."));
1107
  return false;
×
1108
}
1109

1110
/**
1111
 * @brief MainWindow::setPassword open passworddialog
1112
 * @param file which pgp file
1113
 * @param isNew insert (not update)
1114
 */
1115
void MainWindow::setPassword(const QString &file, bool isNew) {
1✔
1116
  const AppSettings s = QtPassSettings::load();
1✔
1117
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
1✔
1118

1119
  if (isNew) {
1✔
1120
    const QString storePath = s.passStore;
1121
    QString folder = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1122
                                  proxyModel, s.passStore);
×
1123
    if (folder.isEmpty()) {
×
1124
      folder = storePath;
×
1125
    }
1126
    QHash<QString, QStringList> templates =
1127
        TemplateIO::readTemplates(storePath);
×
1128
    if (!templates.isEmpty()) {
1129
      QString defaultTemplate =
1130
          TemplateIO::getFolderTemplate(folder, storePath);
×
1131
      d.setAvailableTemplates(templates, defaultTemplate);
×
1132
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
1133
                    [&d]() { d.cycleTemplate(); });
×
1134
    }
1135
  }
×
1136

1137
  if (!d.exec()) {
1✔
1138
    ui->treeView->setFocus();
×
1139
  }
1140
}
1✔
1141

1142
/**
1143
 * @brief MainWindow::addPassword add a new password by showing a
1144
 * number of dialogs.
1145
 */
1146
void MainWindow::addPassword() {
×
1147
  const QString passStore = QtPassSettings::load().passStore;
×
1148
  bool ok;
1149
  QString dir = Util::getDir(ui->treeView->currentIndex(), true, model,
×
1150
                             proxyModel, passStore);
×
1151
  QString file = QInputDialog::getText(
1152
      this, tr("New file"),
×
1153
      tr("New password file: \n(Will be placed in %1 )")
×
1154
          .arg(passStore + Util::getDir(ui->treeView->currentIndex(), true,
×
1155
                                        model, proxyModel, passStore)),
1156
      QLineEdit::Normal, "", &ok);
×
1157
  if (!ok || file.isEmpty()) {
×
1158
    return;
1159
  }
1160
  file = dir + file;
×
1161
  if (!confirmPathInStore(passStore + file)) {
×
1162
    return;
1163
  }
1164
  setPassword(file);
×
1165
}
1166

1167
/**
1168
 * @brief MainWindow::onDelete remove password, if you are
1169
 * sure.
1170
 */
1171
void MainWindow::onDelete() {
×
1172
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1173
  if (!currentIndex.isValid()) {
1174
    // This fixes https://github.com/IJHack/QtPass/issues/556
1175
    // Otherwise the entire password directory would be deleted if
1176
    // nothing is selected in the tree view.
1177
    return;
×
1178
  }
1179

1180
  QFileInfo fileOrFolder =
1181
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1182
  QString file = "";
×
1183
  bool isDir = false;
1184

1185
  if (fileOrFolder.isFile()) {
×
1186
    file = getFile(ui->treeView->currentIndex(), true);
×
1187
  } else {
1188
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel,
×
1189
                        QtPassSettings::getPassStore());
×
1190
    isDir = true;
1191
  }
1192

1193
  QString dirMessage = tr(" and the whole content?");
1194
  if (isDir) {
×
1195
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1196
                    QDirIterator::Subdirectories);
×
1197
    bool okDir = true;
1198
    while (it.hasNext() && okDir) {
×
1199
      it.next();
×
1200
      if (QFileInfo(it.filePath()).isFile()) {
×
1201
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1202
          okDir = false;
1203
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1204
                          "there are unexpected files in the given folder, "
1205
                          "check them before continue.</strong>");
1206
        }
1207
      }
1208
    }
1209
  }
×
1210

1211
  if (QMessageBox::question(
×
1212
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1213
          tr("Are you sure you want to delete %1%2?")
×
1214
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1215
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1216
    return;
1217
  }
1218

1219
  QtPassSettings::getPass()->Remove(file, isDir);
×
1220
}
×
1221

1222
/**
1223
 * @brief MainWindow::cancelOtpRequest abandon an in-flight OTP request.
1224
 *
1225
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1226
 * decrypt never emits finishedShow, and the error handler re-enables the UI —
1227
 * which stops the watchdog that was the only other thing clearing the flag.
1228
 * Left set, it permanently suppressed passShowHandler's copy-on-select and let
1229
 * the still-armed one-shot claim the next unrelated decrypt.
1230
 */
1231
void MainWindow::cancelOtpRequest() {
1✔
1232
  m_otpRequestPending = false;
1✔
1233
  m_otpRequestFile.clear();
1✔
1234
}
1✔
1235

1236
/**
1237
 * @brief MainWindow::onOtp generate the selected entry's OTP code and copy it.
1238
 *
1239
 * Decrypts the entry once and derives the code in-process, so this works with
1240
 * either backend and on every platform. The code itself is produced by
1241
 * otpFromFileToClipboard once Pass::finishedShow arrives.
1242
 */
1243
void MainWindow::onOtp() {
×
1244
  QString file = getFile(ui->treeView->currentIndex(), true);
×
NEW
1245
  if (file.isEmpty()) {
×
1246
    flashText(tr("No password selected for OTP generation"), true);
×
NEW
1247
    return;
×
1248
  }
NEW
1249
  if (!QtPassSettings::isUseOtp()) {
×
1250
    // Normally unreachable (the action is hidden), but never fail silently.
NEW
1251
    flashText(tr("No OTP code found in this password entry"), true);
×
NEW
1252
    return;
×
1253
  }
1254

1255
  // Fast path: the panel already shows a live code for the selected entry, so
1256
  // there is nothing to decrypt. This also keeps the clipboard write single —
1257
  // going through Show() would let passShowHandler copy the password first, and
1258
  // two QClipboard::setMimeData calls in one event-loop turn can leave the
1259
  // Windows clipboard empty.
1260
  //
1261
  // Only when the panel is showing *this* entry: arrow-key navigation and
1262
  // right-clicking move the tree's currentIndex without emitting
1263
  // QTreeView::clicked, so the visible code can belong to a different account.
1264
  const QString shown =
NEW
1265
      (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
×
NEW
1266
  if (!shown.isEmpty()) {
×
NEW
1267
    m_qtPass->copyTextToClipboard(shown);
×
NEW
1268
    showStatusMessage(tr("OTP code copied to clipboard"));
×
1269
    return;
1270
  }
1271

1272
  // Fallback: no OTP row on screen, so decrypt once. The flag stops
1273
  // passShowHandler putting the password on the clipboard for a request that
1274
  // only asked for a code.
NEW
1275
  m_otpRequestPending = true;
×
NEW
1276
  m_otpRequestFile = file;
×
NEW
1277
  setUiElementsEnabled(false);
×
NEW
1278
  connectSingleShot(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1279
                    &MainWindow::otpFromFileToClipboard);
1280
  // passShowHandler repaints the panel for this Show too, so keep the marker in
1281
  // step or a second request would decrypt again instead of taking the fast
1282
  // path. Safe to set now: executeWrapperStarted() clears the panel on every
1283
  // command, so a failed decrypt leaves currentOtpCode() empty.
NEW
1284
  m_shownFile = file;
×
NEW
1285
  QtPassSettings::getPass()->Show(file);
×
1286
}
1287

1288
/**
1289
 * @brief MainWindow::onEdit try and edit (selected) password.
1290
 */
1291
void MainWindow::onEdit() {
×
1292
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1293
  editPassword(file);
×
1294
}
×
1295

1296
/**
1297
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1298
 * @param dir folder to edit users for.
1299
 */
1300
void MainWindow::userDialog(const QString &dir) {
×
1301
  if (!dir.isEmpty()) {
×
1302
    m_currentDir = dir;
×
1303
  }
1304
  onUsers();
×
1305
}
×
1306

1307
/**
1308
 * @brief MainWindow::onUsers edit users for the current
1309
 * folder,
1310
 * gets lists and opens UserDialog.
1311
 */
1312
void MainWindow::onUsers() {
×
1313
  QString dir = m_currentDir.isEmpty()
1314
                    ? Util::getDir(ui->treeView->currentIndex(), false, model,
×
1315
                                   proxyModel, QtPassSettings::getPassStore())
×
1316
                    : m_currentDir;
×
1317

1318
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1319
  if (!d.exec()) {
×
1320
    ui->treeView->setFocus();
×
1321
  }
1322
}
×
1323

1324
/**
1325
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1326
 * @param message
1327
 */
1328
void MainWindow::messageAvailable(const QString &message) {
×
1329
  show();
×
1330
  raise();
×
1331
  if (message.isEmpty()) {
×
1332
    focusInput();
×
1333
  } else {
1334
    ui->treeView->expandAll();
×
1335
    ui->lineEdit->setText(message);
×
1336
    on_lineEdit_returnPressed();
×
1337
  }
1338
}
×
1339

1340
/**
1341
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1342
 * @param batch
1343
 * @param keygenWindow
1344
 */
1345
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1346
  m_keyGenDialog = keygenWindow;
1347
  emit generateGPGKeyPair(batch);
×
1348
}
×
1349

1350
/**
1351
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1352
 * select a more appropriate one to view too
1353
 */
1354
void MainWindow::updateProfileBox() {
16✔
1355
  QHash<QString, QHash<QString, QString>> profiles =
1356
      QtPassSettings::getProfiles();
16✔
1357

1358
  if (profiles.isEmpty()) {
1359
    ui->profileWidget->hide();
×
1360
  } else {
1361
    ui->profileWidget->show();
16✔
1362
    ui->profileBox->setEnabled(profiles.size() > 1);
32✔
1363
    ui->profileBox->clear();
16✔
1364
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
16✔
1365
    while (i.hasNext()) {
16✔
1366
      i.next();
1367
      if (!i.key().isEmpty()) {
16✔
1368
        ui->profileBox->addItem(i.key());
16✔
1369
      }
1370
    }
1371
    ui->profileBox->model()->sort(0);
16✔
1372
  }
1373
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
32✔
1374
  if (index != -1) { //  -1 for not found
16✔
1375
    ui->profileBox->setCurrentIndex(index);
×
1376
  }
1377
}
16✔
1378

1379
/**
1380
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1381
 * correct "profile"
1382
 * @param name
1383
 */
1384
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1385
void MainWindow::on_profileBox_currentIndexChanged(const QString &name) {
1386
#else
1387
/**
1388
 * @brief Handles changes to the selected profile in the profile combo box.
1389
 * @details Ignores the event during a fresh start or when the selected profile
1390
 * matches the current profile. Otherwise, it clears the password field, updates
1391
 * the active profile and related settings, refreshes the environment, and
1392
 * resets the tree view and action states to reflect the newly selected profile.
1393
 *
1394
 * @param name - The newly selected profile name.
1395
 * @return void - This function does not return a value.
1396
 *
1397
 */
1398
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
16✔
1399
#endif
1400
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
16✔
1401
    return;
16✔
1402
  }
1403

1404
  ui->lineEdit->clear();
×
1405

1406
  const QHash<QString, QString> prof =
1407
      QtPassSettings::getProfiles().value(name);
×
1408
  AppSettings s = QtPassSettings::load();
×
1409
  s.activeProfile = name;
×
1410
  s.passStore = prof.value("path");
×
1411
  s.passSigningKey = prof.value("signingKey");
×
1412
  QtPassSettings::save(s);
×
1413
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1414

1415
  QtPassSettings::getPass()->updateEnv();
×
1416

1417
  const QString passStore = QtPassSettings::getPassStore();
×
1418
  proxyModel.setStore(passStore);
×
1419
  ui->treeView->setRootIndex(proxyModel.rootIndexFor(passStore));
×
1420
  deselect();
×
1421
  ui->treeView->setCurrentIndex(QModelIndex());
×
1422
}
×
1423

1424
/**
1425
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1426
 * support
1427
 * it
1428
 */
1429
void MainWindow::initTrayIcon() {
×
1430
  m_tray = new TrayIcon(this);
×
1431
  if (!m_tray->getIsAllocated()) {
×
1432
    destroyTrayIcon();
×
1433
  }
1434
}
×
1435

1436
/**
1437
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1438
 */
1439
void MainWindow::destroyTrayIcon() {
×
1440
  delete m_tray;
×
1441
  m_tray = nullptr;
×
1442
}
×
1443

1444
/**
1445
 * @brief MainWindow::closeEvent hide or quit
1446
 * @param event
1447
 */
1448
void MainWindow::closeEvent(QCloseEvent *event) {
×
1449
  if (QtPassSettings::isHideOnClose()) {
×
1450
    this->hide();
×
1451
    event->ignore();
1452
  } else {
1453
    m_qtPass->clearClipboard();
×
1454

1455
    QtPassSettings::setGeometry(saveGeometry());
×
1456
    QtPassSettings::setSavestate(saveState());
×
1457
    QtPassSettings::setMaximized(isMaximized());
×
1458
    if (!isMaximized()) {
×
1459
      QtPassSettings::setPos(pos());
×
1460
      QtPassSettings::setSize(size());
×
1461
    }
1462
    event->accept();
1463
    // A visible QSystemTrayIcon keeps the application alive after the last
1464
    // window closes, so quitOnLastWindowClosed never fires and the window
1465
    // merely vanishes into the tray. Quit explicitly so closing the window
1466
    // actually exits when "hide on close" is disabled.
1467
    QApplication::quit();
×
1468
  }
1469
}
×
1470

1471
/**
1472
 * @brief MainWindow::eventFilter filter out some events and focus the
1473
 * treeview
1474
 * @param obj
1475
 * @param event
1476
 * @return
1477
 */
1478
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
99✔
1479
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
99✔
1480
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1481
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1482
      ui->treeView->setFocus();
×
1483
    }
1484
  }
1485
  return QObject::eventFilter(obj, event);
99✔
1486
}
1487

1488
/**
1489
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1490
 * @param event
1491
 */
1492
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1493
  switch (event->key()) {
×
1494
  case Qt::Key_Delete:
×
1495
    onDelete();
×
1496
    break;
×
1497
  case Qt::Key_Return:
×
1498
  case Qt::Key_Enter:
1499
    if (proxyModel.rowCount() > 0) {
×
1500
      on_treeView_clicked(ui->treeView->currentIndex());
×
1501
    }
1502
    break;
1503
  case Qt::Key_Escape:
×
1504
    ui->lineEdit->clear();
×
1505
    break;
×
1506
  default:
1507
    break;
1508
  }
1509
}
×
1510

1511
/**
1512
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1513
 * menu
1514
 * @param pos
1515
 */
1516
void MainWindow::showContextMenu(const QPoint &pos) {
×
1517
  const AppSettings s = QtPassSettings::load();
×
1518
  QModelIndex index = ui->treeView->indexAt(pos);
×
1519
  bool selected = true;
1520
  if (!index.isValid()) {
1521
    ui->treeView->clearSelection();
×
1522
    ui->actionDelete->setEnabled(false);
×
1523
    ui->actionEdit->setEnabled(false);
×
1524
    m_currentDir = "";
×
1525
    selected = false;
1526
  }
1527

1528
  ui->treeView->setCurrentIndex(index);
×
1529

1530
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1531

1532
  QFileInfo fileOrFolder =
1533
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1534

1535
  QMenu contextMenu;
×
1536
  if (!selected || fileOrFolder.isDir()) {
×
1537
    QAction *openFolder =
1538
        contextMenu.addAction(tr("Open folder with file manager"));
×
1539
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1540
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1541
    QAction *users = contextMenu.addAction(tr("Users"));
×
1542
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1543
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1544
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1545
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1546
  } else if (fileOrFolder.isFile()) {
×
1547
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1548
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1549
  }
1550
  if (selected) {
×
1551
    contextMenu.addSeparator();
×
1552
    if (fileOrFolder.isDir()) {
×
1553
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1554
      connect(renameFolder, &QAction::triggered, this,
×
1555
              &MainWindow::renameFolder);
×
1556
    } else if (fileOrFolder.isFile()) {
×
1557
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1558
      connect(renamePassword, &QAction::triggered, this,
×
1559
              &MainWindow::renamePassword);
×
1560
    }
1561
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1562
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1563
    if (fileOrFolder.isDir()) {
×
1564
      QString dirPath = QDir::cleanPath(Util::getDir(
×
1565
          ui->treeView->currentIndex(), false, model, proxyModel, s.passStore));
×
1566

1567
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1568
      contextMenu.addMenu(shareMenu);
×
1569

1570
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1571
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1572

1573
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1574
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1575
                                                 QFile(exePath).exists());
×
1576

1577
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1578
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1579
      connect(reencrypt, &QAction::triggered, this,
×
1580
              [this, dirPath]() { reencryptPath(dirPath); });
×
1581

1582
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1583
      exportKey->setEnabled(gpgAvailable);
×
1584
      connect(exportKey, &QAction::triggered, this,
×
1585
              &MainWindow::exportPublicKey);
×
1586

1587
      QAction *addRecipientAction =
1588
          shareMenu->addAction(tr("Add recipient..."));
×
1589
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1590
      connect(addRecipientAction, &QAction::triggered, this,
×
1591
              [this, dirPath]() { addRecipient(dirPath); });
×
1592

1593
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1594
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1595
    }
1596
  }
1597
  contextMenu.exec(globalPos);
×
1598
}
×
1599

1600
/**
1601
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1602
 * password window
1603
 * @param pos
1604
 */
1605
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1606
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1607
  // createStandardContextMenu() parents the menu to textBrowser, which carries
1608
  // a "background: palette(base)" stylesheet. Qt cascades that stylesheet to
1609
  // the child QMenu and breaks its opaque native background, leaving the menu
1610
  // transparent. Reparent to the main window (no stylesheet) so it paints
1611
  // solid.
1612
  contextMenu->setParent(this, contextMenu->windowFlags());
×
1613
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1614

1615
  contextMenu->exec(globalPos);
×
1616
  delete contextMenu;
×
1617
}
×
1618

1619
/**
1620
 * @brief MainWindow::openFolder open the folder in the default file manager
1621
 */
1622
void MainWindow::openFolder() {
×
1623
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1624
                             proxyModel, QtPassSettings::getPassStore());
×
1625

1626
  QString path = QDir::toNativeSeparators(dir);
×
1627
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1628
}
×
1629

1630
/**
1631
 * @brief MainWindow::addFolder add a new folder to store passwords in
1632
 */
1633
void MainWindow::addFolder() {
×
1634
  const AppSettings s = QtPassSettings::load();
×
1635
  bool ok;
1636
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1637
                             proxyModel, s.passStore);
×
1638
  QString newdir = QInputDialog::getText(
1639
      this, tr("New file"),
×
1640
      tr("New Folder: \n(Will be placed in %1 )")
×
1641
          .arg(s.passStore + Util::getDir(ui->treeView->currentIndex(), true,
×
1642
                                          model, proxyModel, s.passStore)),
1643
      QLineEdit::Normal, "", &ok);
×
1644
  if (!ok || newdir.isEmpty()) {
×
1645
    return;
1646
  }
1647
  newdir.prepend(dir);
1648
  if (!confirmPathInStore(newdir)) {
×
1649
    return;
1650
  }
1651
  if (!QDir().mkdir(newdir)) {
×
1652
    QMessageBox::warning(this, tr("Error"),
×
1653
                         tr("Failed to create folder: %1").arg(newdir));
×
1654
    return;
×
1655
  }
1656
  if (s.addGPGId) {
×
1657
    QString gpgIdFile = newdir + "/.gpg-id";
×
1658
    QFile gpgId(gpgIdFile);
×
1659
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1660
      QMessageBox::warning(
×
1661
          this, tr("Error"),
×
1662
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1663
      return;
1664
    }
1665
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1666
    for (const UserInfo &user : users) {
×
1667
      if (user.enabled) {
×
1668
        gpgId.write((user.key_id + "\n").toUtf8());
×
1669
      }
1670
    }
1671
    gpgId.close();
×
1672
    // Lock to owner-only access; see ImitatePass::writeGpgIdFile for
1673
    // rationale (NFS / USB / unusual umask scenarios). Best-effort on
1674
    // platforms where setPermissions is a no-op.
1675
    QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
×
1676
  }
×
1677
}
×
1678

1679
/**
1680
 * @brief MainWindow::renameFolder rename an existing folder
1681
 */
1682
void MainWindow::renameFolder() {
×
1683
  bool ok;
1684
  QString srcDir =
1685
      QDir::cleanPath(Util::getDir(ui->treeView->currentIndex(), false, model,
×
1686
                                   proxyModel, QtPassSettings::getPassStore()));
×
1687
  QString srcDirName = QDir(srcDir).dirName();
×
1688
  QString newName =
1689
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1690
                            QLineEdit::Normal, srcDirName, &ok);
×
1691
  if (!ok || newName.isEmpty()) {
×
1692
    return;
1693
  }
1694
  QString destDir = srcDir;
1695
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1696
  if (!confirmPathInStore(destDir)) {
×
1697
    return;
1698
  }
1699
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1700
}
1701

1702
/**
1703
 * @brief MainWindow::editPassword read password and open edit window via
1704
 * MainWindow::onEdit()
1705
 */
1706
void MainWindow::editPassword(const QString &file) {
1✔
1707
  if (!file.isEmpty()) {
1✔
1708
    const AppSettings s = QtPassSettings::load();
1✔
1709
    if (s.useGit && s.autoPull) {
1✔
1710
      onUpdate(true);
×
1711
    }
1712
    setPassword(file, false);
1✔
1713
  }
1✔
1714
}
1✔
1715

1716
/**
1717
 * @brief MainWindow::renamePassword rename an existing password
1718
 */
1719
void MainWindow::renamePassword() {
×
1720
  bool ok;
1721
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1722
  QString filePath = QFileInfo(file).path();
×
1723
  QString fileName = QFileInfo(file).fileName();
×
1724
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1725
    fileName.chop(4);
×
1726
  }
1727

1728
  QString newName =
1729
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1730
                            QLineEdit::Normal, fileName, &ok);
×
1731
  if (!ok || newName.isEmpty()) {
×
1732
    return;
1733
  }
1734
  QString newFile = QDir(filePath).filePath(newName);
×
1735
  if (!confirmPathInStore(newFile)) {
×
1736
    return;
1737
  }
1738
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1739
}
1740

1741
/**
1742
 * @brief Copies the password of the selected file from the tree view to the
1743
 * clipboard.
1744
 * @example
1745
 * MainWindow::copyPasswordFromTreeview();
1746
 *
1747
 * @return void - This function does not return a value.
1748
 */
1749
void MainWindow::copyPasswordFromTreeview() {
×
1750
  QFileInfo fileOrFolder =
1751
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1752

1753
  if (fileOrFolder.isFile()) {
×
1754
    QString file = getFile(ui->treeView->currentIndex(), true);
×
NEW
1755
    connectSingleShot(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1756
                      &MainWindow::passwordFromFileToClipboard);
1757
    // This Show repaints the panel as well; see onOtp().
NEW
1758
    m_shownFile = file;
×
1759
    QtPassSettings::getPass()->Show(file);
×
1760
  }
1761
}
×
1762

1763
void MainWindow::passwordFromFileToClipboard(const QString &text) {
2✔
1764
  disconnectSingleShot(QtPassSettings::getPass(), &Pass::finishedShow, this,
2✔
1765
                       &MainWindow::passwordFromFileToClipboard);
1766
  const QStringList tokens = text.split('\n');
2✔
1767
  if (tokens.isEmpty()) {
2✔
1768
    return;
1769
  }
1770
  // An entry created by `pass otp insert` has the otpauth:// URI as its first
1771
  // line. That URI carries the shared TOTP secret (the seed, not a code), so
1772
  // copying it to the clipboard would leak 2FA material. Skip it, matching the
1773
  // display path (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1774
  if (FileContent::isOtpUriValue(tokens[0])) {
2✔
1775
    flashText(tr("This entry holds an OTP secret, not a password"), true);
1✔
1776
    return;
1✔
1777
  }
1778
  m_qtPass->copyTextToClipboard(tokens[0]);
1✔
1779
}
1780

1781
/**
1782
 * @brief Displays message in status bar
1783
 *
1784
 * @param msg     text to be displayed
1785
 * @param timeout time for which msg shall be visible
1786
 */
1787
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
6✔
1788
  ui->statusBar->showMessage(msg, timeout);
6✔
1789
}
6✔
1790

1791
/**
1792
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1793
 * @param dir Directory path to re-encrypt
1794
 */
1795
void MainWindow::reencryptPath(const QString &dir) {
×
1796
  QDir checkDir(dir);
×
1797
  if (!checkDir.exists()) {
×
1798
    QMessageBox::critical(this, tr("Error"),
×
1799
                          tr("Directory does not exist: %1").arg(dir));
×
1800
    return;
×
1801
  }
1802

1803
  int ret = QMessageBox::question(
×
1804
      this, tr("Re-encrypt passwords"),
×
1805
      tr("Re-encrypt all passwords in %1?\n\n"
×
1806
         "This will re-encrypt ALL password files in this folder "
1807
         "using the current recipients defined in .gpg-id.\n\n"
1808
         "This may rewrite many files and cannot be undone easily.\n\n"
1809
         "Continue?")
1810
          .arg(QDir(dir).dirName()),
×
1811
      QMessageBox::Yes | QMessageBox::No);
1812

1813
  if (ret != QMessageBox::Yes)
×
1814
    return;
1815

1816
  // Disable preemptively. ImitatePass::reencryptPath emits
1817
  // startReencryptPath asynchronously and the slot would re-run this,
1818
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1819
  startReencryptPath();
×
1820

1821
  QtPassSettings::getImitatePass()->reencryptPath(
×
1822
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1823
}
×
1824

1825
/**
1826
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1827
 */
1828
void MainWindow::startReencryptPath() {
×
1829
  setUiElementsEnabled(false);
×
1830
  ui->treeView->setDisabled(true);
×
1831
}
×
1832

1833
/**
1834
 * @brief MainWindow::endReencryptPath re-enable ui elements
1835
 */
1836
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1837

1838
/**
1839
 * @brief MainWindow::exportPublicKey export the configured signing key in
1840
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1841
 *
1842
 * Falls back to a help dialog when no signing key is configured or gpg is
1843
 * unavailable, so the user still gets actionable guidance.
1844
 */
1845
void MainWindow::exportPublicKey() {
×
1846
  const AppSettings s = QtPassSettings::load();
×
1847
  const QString identity = s.passSigningKey;
1848
  if (identity.isEmpty()) {
×
1849
    QMessageBox::information(
×
1850
        this, tr("Export Public Key"),
×
1851
        tr("<h3>Export Your Public Key</h3>"
×
1852
           "<p>No signing key is configured. Set one in QtPass Settings "
1853
           "&gt; GPG keys, or run this in a terminal:</p>"
1854
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1855
           "&gt;</pre>"
1856
           "<p>Then send the file to your teammates.</p>"));
1857
    return;
×
1858
  }
1859
  QString gpgExe = s.gpgExecutable;
1860
  if (gpgExe.isEmpty()) {
×
1861
    gpgExe = QStringLiteral("gpg");
×
1862
  }
1863
  QStringList args = {"--armor", "--export"};
×
1864
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1865
  QString stdOut;
×
1866
  QString stdErr;
×
1867
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
×
1868
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1869
    QMessageBox::warning(this, tr("Export Public Key"),
×
1870
                         tr("Could not export public key for %1.\n\n%2")
×
1871
                             .arg(identity, stdErr.isEmpty()
×
1872
                                                ? tr("No output from gpg.")
×
1873
                                                : stdErr));
1874
    return;
1875
  }
1876
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1877
  dialog.exec();
×
1878
}
×
1879

1880
/**
1881
 * @brief MainWindow::addRecipient open the recipient management dialog for
1882
 *        the supplied directory.
1883
 * @param dir Folder whose .gpg-id should be edited.
1884
 *
1885
 * Delegates to UsersDialog so users can tick/untick keys from their
1886
 * keyring as recipients of the folder; importing a foreign key into the
1887
 * keyring still has to happen via gpg (or QtPass settings) first.
1888
 */
1889
void MainWindow::addRecipient(const QString &dir) {
×
1890
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1891
  d.exec();
×
1892
}
×
1893

1894
/**
1895
 * @brief MainWindow::showShareHelp show help about GPG sharing
1896
 */
1897
void MainWindow::showShareHelp() {
×
1898
  QMessageBox::information(
×
1899
      this, tr("Sharing Passwords with GPG"),
×
1900
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1901
         "<p>To share passwords with other users:</p>"
1902
         "<ol>"
1903
         "<li><b>Export your public key</b> and send it to teammates</li>"
1904
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1905
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1906
         "them</li>"
1907
         "</ol>"
1908
         "<p>Only people who have a matching secret key can decrypt the "
1909
         "passwords.</p>"
1910
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1911
         "<p>See the FAQ for more details.</p>"));
1912
}
×
1913

1914
void MainWindow::updateGitButtonVisibility() {
29✔
1915
  const AppSettings s = QtPassSettings::load();
29✔
1916
  if (!s.useGit || (s.gitExecutable.isEmpty() && s.passExecutable.isEmpty())) {
29✔
1917
    enableGitButtons(false);
29✔
1918
  } else {
1919
    enableGitButtons(true);
×
1920
  }
1921
}
29✔
1922

1923
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
29✔
1924
  // No platform gating any more: codes are generated in-process, so this works
1925
  // on Windows and macOS and with either backend. It used to be hidden there
1926
  // because the pass-otp extension is Unix-only.
1927
  //
1928
  // Kept visible but disabled when OTP is off, as it was before: hiding it
1929
  // removed the only discoverable trace of the feature, and since an OTP field
1930
  // is suppressed from the panel unconditionally (its value is a secret), a
1931
  // user with the setting off would see nothing at all where their OTP data
1932
  // used to be.
1933
  //
1934
  // Enablement must honour uiEnabled: this is called from
1935
  // setUiElementsEnabled(), and ignoring the argument re-enabled the action
1936
  // while a decrypt was in flight, letting the user stack Show calls.
1937
  ui->actionOtp->setVisible(true);
29✔
1938
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
40✔
1939
}
29✔
1940

1941
void MainWindow::updateGrepButtonVisibility() {
16✔
1942
  const bool enabled = QtPassSettings::isUseGrepSearch();
16✔
1943
  ui->grepButton->setVisible(enabled);
16✔
1944
  ui->grepCaseButton->setVisible(enabled);
16✔
1945
  if (!enabled && m_grep.inGrepMode()) {
16✔
1946
    ui->grepButton->setChecked(false);
×
1947
  }
1948
}
16✔
1949

1950
void MainWindow::enableGitButtons(const bool &state) {
29✔
1951
  // Following GNOME guidelines is preferable disable buttons instead of hide
1952
  ui->actionPush->setEnabled(state);
29✔
1953
  ui->actionUpdate->setEnabled(state);
29✔
1954
}
29✔
1955

1956
/**
1957
 * @brief MainWindow::critical critical message popup wrapper.
1958
 * @param title
1959
 * @param msg
1960
 */
1961
void MainWindow::critical(const QString &title, const QString &msg) {
×
1962
  QMessageBox::critical(this, title, msg);
×
1963
}
×
1964

1965
/**
1966
 * @brief Appends processed command output to the output panel.
1967
 *
1968
 * Appends text to the process output text edit, with per-line numbering,
1969
 * optional command prefix, and color coding for errors vs. success.
1970
 * Handles auto-scrolling and line limits.
1971
 *
1972
 * @param output The raw output text from the command.
1973
 * @param isError true if this is error output (stderr).
1974
 * @param linePrefix Optional command name to prefix each line with.
1975
 */
1976
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2✔
1977
                                     const QString &linePrefix) {
1978
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
1979
    return;
1✔
1980
  }
1981

1982
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1983
  for (QString &line : lines) {
2✔
1984
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1985
    // indentation
1986
    line.remove('\r');
1✔
1987
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1988
      line.chop(1);
×
1989
    }
1990
    if (line.isEmpty()) {
1✔
1991
      continue;
×
1992
    }
1993

1994
    m_outputCounter++;
1✔
1995
    QString lineNumber = QString::number(m_outputCounter);
1✔
1996

1997
    QColor textColor =
1998
        isError ? QColor(Qt::red)
1✔
1999
                : m_processOutputEdit->palette().color(QPalette::Text);
2✔
2000
    QString colorHex = textColor.name();
1✔
2001
    // Apply the optional prefix per line so multi-line output stays
2002
    // attributed to its command (e.g. all 3 lines of a `git push` show
2003
    // "git push: ..." rather than only the first).
2004
    QString prefixed =
2005
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2✔
2006
    QString coloredOutput =
2007
        QString("<span style=\"color: %1;\">%2: %3</span>")
1✔
2008
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2✔
2009

2010
    m_processOutputEdit->append(coloredOutput);
1✔
2011
  }
2012

2013
  limitOutputLines();
1✔
2014

2015
  if (m_autoScroll) {
1✔
2016
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
2017
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
2018
  }
2019
}
2020

2021
/**
2022
 * @brief Handles process output from the Pass executor.
2023
 *
2024
 * Called when any non-sensitive process completes. Filters out password-
2025
 * related commands (pass show, insert, etc.) and delegates to
2026
 * appendProcessOutput.
2027
 *
2028
 * @param output The stdout/stderr text from the process.
2029
 * @param isError true if this is error output (stderr).
2030
 * @param pid The process ID identifying which command ran.
2031
 */
2032
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
2033
                                 Enums::PROCESS pid) {
2034
  appendProcessOutput(output, isError, getProcessName(pid));
2✔
2035
}
2✔
2036

2037
/**
2038
 * @brief Maps a process ID to its human-readable command name.
2039
 *
2040
 * Returns static strings for git/pass commands that appear in output.
2041
 * Password-related commands return empty (they are filtered).
2042
 *
2043
 * @param pid The process ID to look up.
2044
 * @return QString with command name, or empty if filtered.
2045
 */
2046
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
2✔
2047
  switch (pid) {
2✔
2048
  case Enums::GIT_INIT:
×
2049
    return QStringLiteral("git init"); // no-tr
×
2050
  case Enums::GIT_ADD:
×
2051
    return QStringLiteral("git add"); // no-tr
×
2052
  case Enums::GIT_COMMIT:
×
2053
    return QStringLiteral("git commit"); // no-tr
×
2054
  case Enums::GIT_RM:
×
2055
    return QStringLiteral("git rm"); // no-tr
×
2056
  case Enums::GIT_PULL:
×
2057
    return QStringLiteral("git pull"); // no-tr
×
2058
  case Enums::GIT_PUSH:
×
2059
    return QStringLiteral("git push"); // no-tr
×
2060
  case Enums::GIT_MOVE:
×
2061
    return QStringLiteral("git mv"); // no-tr
×
2062
  case Enums::GIT_COPY:
×
2063
    // ImitatePass::Copy literally invokes `git cp` (a git-extras
2064
    // subcommand), so the label matches what's run. Stock-git users
2065
    // without git-extras will see the underlying "'cp' is not a git
2066
    // command" failure surfaced in the process output panel.
2067
    return QStringLiteral("git cp"); // no-tr
×
2068
  case Enums::PASS_INSERT:
×
2069
    return QStringLiteral("pass insert"); // no-tr
×
2070
  case Enums::PASS_REMOVE:
×
2071
    return QStringLiteral("pass rm"); // no-tr
×
2072
  case Enums::PASS_INIT:
×
2073
    return QStringLiteral("pass init"); // no-tr
×
2074
  case Enums::PASS_MOVE:
×
2075
    return QStringLiteral("pass mv"); // no-tr
×
2076
  case Enums::PASS_COPY:
×
2077
    return QStringLiteral("pass cp"); // no-tr
×
2078
  case Enums::PASS_GREP:
×
2079
    return QStringLiteral("pass grep"); // no-tr
×
2080
  case Enums::GPG_GENKEYS:
×
2081
    return QStringLiteral("gpg --gen-key"); // no-tr
×
2082
  case Enums::PASS_SHOW:
2083
  case Enums::PASS_OTP_GENERATE:
2084
  case Enums::PROCESS_COUNT:
2085
  case Enums::INVALID:
2086
    break;
2087
  }
2088
  return {};
2089
}
2090

2091
/**
2092
 * @brief Checks if a process ID represents a sensitive operation whose
2093
 * output should not be shown in the process output panel.
2094
 *
2095
 * Password-related commands (pass show, OTP generate, grep, insert)
2096
 * display their output in other UI areas, so we skip them here.
2097
 *
2098
 * @param pid The process ID to check.
2099
 * @return true if the process is sensitive and should be filtered.
2100
 */
2101
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
2102
  switch (pid) {
×
2103
  case Enums::PASS_SHOW:
2104
  case Enums::PASS_OTP_GENERATE:
2105
  case Enums::PASS_GREP:
2106
  case Enums::PASS_INSERT:
2107
    return true;
2108
  case Enums::GIT_INIT:
2109
  case Enums::GIT_ADD:
2110
  case Enums::GIT_COMMIT:
2111
  case Enums::GIT_RM:
2112
  case Enums::GIT_PULL:
2113
  case Enums::GIT_PUSH:
2114
  case Enums::GIT_MOVE:
2115
  case Enums::GIT_COPY:
2116
  case Enums::PASS_REMOVE:
2117
  case Enums::PASS_INIT:
2118
  case Enums::PASS_MOVE:
2119
  case Enums::PASS_COPY:
2120
  case Enums::GPG_GENKEYS:
2121
  case Enums::PROCESS_COUNT:
2122
  case Enums::INVALID:
2123
    break;
2124
  }
2125
  return false;
×
2126
}
2127

2128
/**
2129
 * @brief Updates the visibility of the process output panel.
2130
 *
2131
 * Shows or hides the process output widget based on the user's
2132
 * showProcessOutput setting.
2133
 */
2134
void MainWindow::updateProcessOutputVisibility() {
×
2135
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
2136
}
×
2137

2138
/**
2139
 * @brief Limits the output panel to max lines, trimming old excess.
2140
 *
2141
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
2142
 * Called after each append to prevent unbounded growth.
2143
 */
2144
void MainWindow::limitOutputLines() {
1✔
2145
  QTextDocument *doc = m_processOutputEdit->document();
1✔
2146
  int excess = doc->blockCount() - MaxOutputLines;
1✔
2147
  if (excess <= 0) {
1✔
2148
    return;
1✔
2149
  }
2150

2151
  QTextCursor cursor(doc);
×
2152
  cursor.movePosition(QTextCursor::Start);
×
2153
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2154
  cursor.removeSelectedText();
×
2155
}
×
2156

2157
/**
2158
 * @brief Clears the process output panel.
2159
 *
2160
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2161
 */
2162
void MainWindow::on_clearOutputButton_clicked() {
×
2163
  m_processOutputEdit->clear();
×
2164
  m_outputCounter = 0;
×
2165
  m_autoScroll = true;
×
2166
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc