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

IJHack / QtPass / 35267216991

17 Sep 2026 07:50PM UTC coverage: 77.023% (+0.2%) from 76.864%
35267216991

push

github

web-flow
A first-run wizard instead of message boxes before the Settings dialog (#1816)

* A first-run wizard instead of message boxes before the Settings dialog

ConfigDialog::wizard() was a chain of modal boxes that ran before the
whole configuration dialog was shown: GnuPG missing, generate a key,
create the store?, pick recipients, then the full Settings dialog with
its four tabs for someone who had not used the program yet. Declining
"Create password-store?" left an invalid configuration and the dialog
came back. It mutated ConfigDialog widgets, drove KeygenDialog and
UsersDialog through the shared backend and re-pointed the backend at the
new store from inside the dialog.

FirstRunWizard is a QWizard with four pages after the intro: Programs
(gpg required, runnable check on the path, pass and Git optional and only
offerable when found), Your key (the secret keys the chosen gpg lists,
tickable, with a Generate button that runs KeygenDialog), Password store
(an existing store is used as it is, an empty or missing folder is
described and set up on Finish) and Ready (summary, hide passwords, tray
icon). It edits a copy of the settings; accept() creates and initialises
the store through ProfileInit for the ticked keys, saves the settings
and invalidates the backend. MainWindow::config() runs it on a fresh
start and the ConfigDialog otherwise; the constructor's loop is unchanged.
The six wizard helpers leave ConfigDialog.

tst_firstrunwizard drives the pages with a scripted gpg (page gating,
key listing and ticking, store status, Finish creating .gpg-id with the
fingerprint, an existing store left alone). tst_mainwindow's first-run
tests now drive the wizard: cancel stops startup, Finish sets up the
store and startup succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* First-run wizard: Git decided per folder, unusable keys unticked, no early writes

Review of the wizard tur... (continued)

291 of 361 new or added lines in 4 files covered. (80.61%)

22 existing lines in 3 files now uncovered.

6349 of 8243 relevant lines covered (77.02%)

72.37 hits per line

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

47.38
/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
#include "qtpasslogging.h"
6

7
#include "configdialog.h"
8
#include "enums.h"
9
#include "executor.h"
10
#include "exportpublickeydialog.h"
11
#include "filecontent.h"
12
#include "firstrunwizard.h"
13
#include "passworddialog.h"
14
#include "passworddisplaypanel.h"
15
#include "pathvalidator.h"
16
#include "processoutputpanel.h"
17
#include "qpushbuttonasqrcode.h"
18
#include "qpushbuttonshowpassword.h"
19
#include "qpushbuttonwithclipboard.h"
20
#include "qtpass.h"
21
#include "qtpasssettings.h"
22
#include "templateio.h"
23
#include "totp.h"
24
#include "trayicon.h"
25
#include "ui_mainwindow.h"
26
#include "usersdialog.h"
27
#include "util.h"
28
#include "windowstatestore.h"
29
#include <QApplication>
30
#include <QCloseEvent>
31
#include <QDate>
32
#include <QDesktopServices>
33
#include <QDialog>
34
#include <QDirIterator>
35
#include <QDockWidget>
36
#include <QFileInfo>
37
#include <QHBoxLayout>
38
#include <QInputDialog>
39
#include <QLabel>
40
#include <QLineEdit>
41
#include <QMenu>
42
#include <QMessageBox>
43
#include <QProgressDialog>
44
#include <QPushButton>
45
#include <QShortcut>
46
#include <QTextCharFormat>
47
#include <QTextStream>
48
#include <QTimer>
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)
41 ✔
60
    : QMainWindow(parent), ui(new Ui::MainWindow) {
41 ✔
61
#ifdef __APPLE__
62
  // extra treatment for mac os
63
  // see https://doc.qt.io/qt-6/qkeysequence.html#qt_set_sequence_auto_mnemonic
64
  qt_set_sequence_auto_mnemonic(true);
65
#endif
66
  ui->setupUi(this);
41 ✔
67

68
  m_qtPass = new QtPass(this);
41 ✔
69
  restoreWindow();
41 ✔
70
  connect(m_qtPass, &QtPass::outputReady, this,
41 ✔
71
          [this](const QString &html) { flashText(html, false, true); });
53 ✔
72
  connect(m_qtPass, &QtPass::operationFinished, this,
41 ✔
73
          [this]() { setUiElementsEnabled(true); });
53 ✔
74
  connect(m_qtPass, &QtPass::statusMessage, this,
41 ✔
75
          &MainWindow::showStatusMessage);
41 ✔
76
  connect(m_qtPass, &QtPass::pushRequested, this, &MainWindow::onPush);
41 ✔
77
  connect(m_qtPass, &QtPass::entryInserted, this,
41 ✔
78
          [this]() { on_treeView_clicked(getCurrentTreeViewIndex()); });
42 ✔
79
  connect(&m_qtPass->clipboard(), &ClipboardManager::statusMessage, this,
41 ✔
80
          [this](const QString &message) { showStatusMessage(message); });
48 ✔
81
  // Both backends: the active one can change through the configuration
82
  // dialog without a restart.
83
  connectPassSignals(QtPassSettings::getRealPass());
41 ✔
84
  connectPassSignals(QtPassSettings::getImitatePass());
41 ✔
85
  ImitatePass *ipass = QtPassSettings::getImitatePass();
41 ✔
86
  connect(ipass, &ImitatePass::startReencryptPath, this,
41 ✔
87
          &MainWindow::startReencryptPath);
41 ✔
88
  connect(ipass, &ImitatePass::reencryptProgress, this,
41 ✔
89
          &MainWindow::reencryptProgress);
41 ✔
90
  connect(ipass, &ImitatePass::endReencryptPath, this,
41 ✔
91
          &MainWindow::endReencryptPath);
41 ✔
92

93
  // register shortcut ctrl/cmd + C to copy the currently selected password
94
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
41 ✔
95
                &MainWindow::copyPasswordFromTreeview);
41 ✔
96

97
  m_tree = new StoreTree(ui->treeView, this);
41 ✔
98
  m_tree->setStore(QtPassSettings::getPassStore(Util::findPasswordStore()));
41 ✔
99
  m_tree->setPass(QtPassSettings::getPass());
41 ✔
100
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
41 ✔
101
          &MainWindow::showContextMenu);
41 ✔
102
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
41 ✔
103
          &MainWindow::deselect);
41 ✔
104

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

122
  updateProfileBox();
41 ✔
123

124
  m_displayPanel = new PasswordDisplayPanel(
41 ✔
125
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
41 ✔
126
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
41 ✔
127
          &m_qtPass->clipboard(), &ClipboardManager::copyText);
41 ✔
128
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
41 ✔
129
          &QtPass::showTextAsQRCode);
41 ✔
130

131
  QtPassSettings::getPass()->updateEnv();
41 ✔
132
  clearPanelTimer.setSingleShot(true);
41 ✔
133
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
41 ✔
134

135
  searchTimer.setInterval(350);
41 ✔
136
  searchTimer.setSingleShot(true);
41 ✔
137

138
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
41 ✔
139

140
  // Install the search-box key filter once, not on every setUiElementsEnabled
141
  // call.
142
  ui->lineEdit->installEventFilter(this);
41 ✔
143
  ui->toolBar->installEventFilter(this);
41 ✔
144

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

157
  initToolBarButtons();
41 ✔
158
  initStatusBar();
41 ✔
159
  m_processOutput = new ProcessOutputPanel(this);
41 ✔
160
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
41 ✔
161
  // setVisible after addDockWidget so our explicit preference wins even if
162
  // QMainWindow applies any cached state when the dock is attached.
163
  // restoreWindow() ran right after the QtPass construction above, so the
164
  // saved layout has already been processed.
165
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
41 ✔
166

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

188
  ui->lineEdit->setClearButtonEnabled(true);
41 ✔
189
  updateGrepButtonVisibility();
41 ✔
190

191
  setUiElementsEnabled(true);
41 ✔
192

193
  ui->lineEdit->setText(searchText);
41 ✔
194

195
  // Record whether startup configuration succeeded instead of calling
196
  // QApplication::quit() here: quit() before the event loop runs (exec() is
197
  // still ahead in main()) is a documented no-op, so a cancelled first-run
198
  // wizard used to leave the half-configured window showing anyway. main()
199
  // consults initSucceeded() and exits before show() when this is false.
200
  m_qtPass->init();
41 ✔
201
  // Ask again until the configuration is usable or the user gives up. The
202
  // dialog's OK button is not gated on Util::configIsValid(), so an accepted
203
  // first-run configuration can still point at a store without a .gpg-id
204
  // (the user declined to create it); only a cancel ends the loop.
205
  while (!Util::configIsValid(QtPassSettings::load())) {
42 ✔
206
    if (!config()) {
2 ✔
207
      return;
208
    }
209
  }
210
  m_freshStart = false;
40 ✔
211
  m_initSucceeded = true;
40 ✔
212

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

222
MainWindow::~MainWindow() {
78 ✔
223
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
224
  // window still knows its last geometry here.
225
  saveWindowState();
41 ✔
226
  delete m_qtPass;
41 ✔
227
}
119 ✔
228

229
/**
230
 * @brief Connect the signals of one backend that the window answers itself:
231
 * decrypt results, errors, status text and the grep results.
232
 * @param pass The backend.
233
 */
234
void MainWindow::connectPassSignals(Pass *pass) {
82 ✔
235
  // A failed decrypt never emits finishedShow; drop the pending OTP/copy
236
  // request so a later decrypt of the same entry is not taken as its answer.
237
  connect(pass, &Pass::processErrorExit, this, &MainWindow::cancelOtpRequest);
82 ✔
238
  connect(pass, &Pass::critical, this, &MainWindow::critical);
82 ✔
239
  connect(pass, &Pass::startingExecuteWrapper, this,
82 ✔
240
          &MainWindow::executeWrapperStarted);
82 ✔
241
  connect(pass, &Pass::statusMsg, this, &MainWindow::showStatusMessage);
82 ✔
242
  connect(pass, &Pass::finishedShow, this, &MainWindow::passShowHandler);
82 ✔
243
  // Both check the file against their own pending request and ignore the
244
  // rest, so they can stay connected for good.
245
  connect(pass, &Pass::finishedShow, this, &MainWindow::otpFromFileToClipboard);
82 ✔
246
  connect(pass, &Pass::finishedShow, this,
82 ✔
247
          &MainWindow::passwordFromFileToClipboard);
82 ✔
248
  connect(pass, &Pass::finishedGrep, this, &MainWindow::onGrepFinished);
82 ✔
249
}
82 ✔
250

251
/**
252
 * @brief MainWindow::saveWindowState persist geometry and dock/toolbar
253
 *        layout. Idempotent, cheap, called from closeEvent() and the
254
 *        destructor.
255
 */
256
void MainWindow::saveWindowState() {
43 ✔
257
  QtPassSettings::setGeometry(saveGeometry());
43 ✔
258
  QtPassSettings::setSavestate(saveState());
43 ✔
259
}
43 ✔
260

261
/**
262
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
263
 * box and sets focus to it. Allows for easy searching, called at application
264
 * start and when receiving empty message in MainWindow::messageAvailable when
265
 * compiled with SINGLE_APP=1 (default).
266
 */
267
void MainWindow::focusInput() {
13 ✔
268
  // Resolve the QLineEdit through the live widget tree rather than the
269
  // cached `ui->lineEdit` pointer.
270
  //
271
  // On a fresh-config first launch the constructor calls
272
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
273
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
274
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
275
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
276
  // we observed the QLineEdit attached to the centralWidget gets
277
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
278
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
279
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
280
  // walks the current hierarchy and returns null cleanly when the
281
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
282
  // use-after-free.
283
  if (!isVisible()) {
13 ✔
284
    return;
285
  }
286
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
13 ✔
287
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
13 ✔
288
    return;
289
  }
290
  lineEdit->selectAll();
13 ✔
291
  lineEdit->setFocus();
13 ✔
292
  // Only mark the first-show focus pulse as done once it's actually
293
  // landed; setting it eagerly in showEvent() would consume the
294
  // one-shot if focusInput returned early (mid-rebuild widget state)
295
  // and we'd never retry.
296
  m_firstShowCompleted = true;
13 ✔
297
}
298

299
/**
300
 * @brief MainWindow::changeEvent sets focus to the search box on activation
301
 * and re-derives palette-dependent styling on a theme change.
302
 * @param event
303
 */
304
void MainWindow::changeEvent(QEvent *event) {
59 ✔
305
  QWidget::changeEvent(event);
59 ✔
306
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
59 ✔
307
    // Desktop switched light/dark (e.g. KDE day/night). Top-level widgets
308
    // receive PaletteChange for that (ApplicationPaletteChange goes to the
309
    // QApplication object). Styling that bakes palette colours into
310
    // stylesheets must be re-derived by hand.
311
    m_displayPanel->refreshPalette();
5 ✔
312
  }
313
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
59 ✔
314
      isVisible()) {
315
    // Defer one event-loop tick so the synchronous activation dispatch
316
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
317
    // unwinds before we touch widget state — calling `focusInput()`
318
    // inline from this stack has segfaulted in past iterations because
319
    // mid-rebuild ui state isn't fully wired up yet.
320
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
9 ✔
321
                              Qt::QueuedConnection);
322
  }
323
}
59 ✔
324

325
/**
326
 * @brief First-show hook: run the initial focusInput() pulse once the
327
 *        window is actually mapped. The widget's internal data is fully
328
 *        initialised by this point, so QLineEdit::selectAll() is safe.
329
 * @param event Show event passed to the base class.
330
 */
331
void MainWindow::showEvent(QShowEvent *event) {
11 ✔
332
  QMainWindow::showEvent(event);
11 ✔
333
  if (m_firstShowCompleted) {
11 ✔
334
    return;
335
  }
336
  // Queue the focus pulse for the next event-loop tick so the platform
337
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
338
  // from the config wizard path) settle before we look up the line
339
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
340
  // *after* it actually focuses, so a transient failed lookup just
341
  // re-queues on the next show rather than silently dropping.
342
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
9 ✔
343
                            Qt::QueuedConnection);
344
}
345

346
/**
347
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
348
 */
349
void MainWindow::initToolBarButtons() {
41 ✔
350
  connect(ui->actionAddPassword, &QAction::triggered, this,
41 ✔
351
          &MainWindow::addPassword);
41 ✔
352
  connect(ui->actionAddFolder, &QAction::triggered, this,
41 ✔
353
          &MainWindow::addFolder);
41 ✔
354
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
41 ✔
355
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
41 ✔
356
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
41 ✔
357
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
41 ✔
358
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
41 ✔
359
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
41 ✔
360
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
41 ✔
361

362
  // Menu-only actions. Quit ends the application, as Ctrl+Q/Cmd+Q does in
363
  // KeePassXC, Telegram and per the KDE, GNOME and macOS guidelines, tray
364
  // icon or not (#1788). Close window is what the old bare Ctrl+Q shortcut
365
  // did: close(), which closeEvent() turns into a hide when "hide on close"
366
  // is set. The X button and Alt+F4 are close() as well.
367
  connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
41 ✔
368
  connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
41 ✔
369
  connect(ui->actionFaq, &QAction::triggered, this, [] {
41 ✔
370
    QDesktopServices::openUrl(
×
371
        QUrl(QStringLiteral("https://qtpass.org/docs/md__f_a_q.html")));
×
372
  });
×
373
  connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
41 ✔
374
  connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
41 ✔
375

376
  ui->actionAddPassword->setIcon(
41 ✔
377
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
164 ✔
378
  ui->actionAddFolder->setIcon(
41 ✔
379
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
164 ✔
380
  ui->actionEdit->setIcon(QIcon::fromTheme(
41 ✔
381
      "document-properties", QIcon(":/icons/document-properties.svg")));
123 ✔
382
  ui->actionDelete->setIcon(
41 ✔
383
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
164 ✔
384
  ui->actionPush->setIcon(
41 ✔
385
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
164 ✔
386
  ui->actionUpdate->setIcon(
41 ✔
387
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
164 ✔
388
  ui->actionUsers->setIcon(QIcon::fromTheme(
41 ✔
389
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
123 ✔
390
  ui->actionConfig->setIcon(QIcon::fromTheme(
41 ✔
391
      "applications-system", QIcon(":/icons/applications-system.svg")));
82 ✔
392
}
41 ✔
393

394
/**
395
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
396
 */
397
void MainWindow::initStatusBar() {
41 ✔
398
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
82 ✔
399

400
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
82 ✔
401
                     .scaledToHeight(statusBar()->height());
41 ✔
402
  auto *logoApp = new QLabel(statusBar());
41 ✔
403
  logoApp->setPixmap(logo);
41 ✔
404
  statusBar()->addPermanentWidget(logoApp);
41 ✔
405
}
41 ✔
406

407
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1 ✔
408
  return ui->treeView->currentIndex();
1 ✔
409
}
410

411
/**
412
 * @brief Displays the given text in the main window text browser, optionally
413
 * marking it as an error and/or rendering it as HTML.
414
 * @example
415
 * MainWindow window;
416
 * window.flashText("Operation completed.", false, false);
417
 *
418
 * @param const QString &text - The text content to display.
419
 * @param const bool isError - If true, sets the text color to red before
420
 * displaying the text; otherwise any earlier error colour is cleared.
421
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
422
 * the existing HTML content.
423
 * @return void - No return value.
424
 */
425
void MainWindow::flashText(const QString &text, const bool isError,
24 ✔
426
                           const bool isHtml) {
427
  if (isError) {
24 ✔
428
    ui->textBrowser->setTextColor(Qt::red);
3 ✔
429
  } else {
430
    // setTextColor() merges the red foreground into the browser's current
431
    // char format, and setText()/setPlainText() re-applies that format to the
432
    // whole new document. Without clearing it, a plain-text non-error message
433
    // shown after an error would still be red. Remove the property rather
434
    // than pinning a palette colour: an explicit foreground would stop the
435
    // text from following runtime light/dark palette switches (#946).
436
    QTextCharFormat format = ui->textBrowser->currentCharFormat();
21 ✔
437
    format.clearForeground();
438
    ui->textBrowser->setCurrentCharFormat(format);
21 ✔
439
  }
440

441
  if (isHtml) {
24 ✔
442
    QString _text = text;
443
    if (!ui->textBrowser->toPlainText().isEmpty()) {
38 ✔
444
      _text = ui->textBrowser->toHtml() + _text;
16 ✔
445
    }
446
    ui->textBrowser->setHtml(_text);
19 ✔
447
  } else {
448
    ui->textBrowser->setText(text);
5 ✔
449
  }
450
}
24 ✔
451

452
/**
453
 * @brief MainWindow::config pops up the configuration screen and handles all
454
 * inter-window communication
455
 */
456
void MainWindow::applyTextBrowserSettings() {
1 ✔
457
  const AppSettings s = QtPassSettings::load();
1 ✔
458
  if (s.useMonospace) {
1 ✔
459
    QFont monospace("Monospace");
×
460
    monospace.setStyleHint(QFont::Monospace);
×
461
    ui->textBrowser->setFont(monospace);
×
462
  } else {
×
463
    ui->textBrowser->setFont(QFont());
1 ✔
464
  }
465

466
  if (s.noLineWrapping) {
1 ✔
467
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
468
  } else {
469
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
1 ✔
470
  }
471
}
1 ✔
472

473
void MainWindow::applyWindowFlagsSettings() {
49 ✔
474
  const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
49 ✔
475
  const Qt::WindowFlags flags = windowFlags();
476
  if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
49 ✔
477
    // Nothing to change. setWindowFlags() on a top-level widget rebuilds the
478
    // native window (setParent(nullptr, flags)), which used to happen on every
479
    // Settings OK because the "off" branch never compared equal — the same
480
    // teardown that left ui->lineEdit dangling (see focusInput()).
481
    return;
482
  }
483
  // Toggle just the one hint; the old "off" branch reset to Qt::Window and
484
  // discarded every other flag with it.
485
  const bool wasVisible = isVisible();
486
  setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
4 ✔
487
                           : (flags & ~Qt::WindowStaysOnTopHint));
488
  // setWindowFlags() hides a shown window; a window that was not shown yet
489
  // (first-run wizard from the constructor) is left to its normal show.
490
  if (wasVisible) {
2 ✔
491
    show();
2 ✔
492
  }
493
}
494

495
/**
496
 * @brief Opens the configuration dialog (or, before the first usable
497
 * configuration, the first-run wizard) and applies what was accepted.
498
 * @example
499
 * if (!config()) {
500
 *   // the user cancelled
501
 * }
502
 *
503
 * @return bool - True when the dialog was accepted, false when it was
504
 * cancelled. An accepted dialog can still leave the configuration invalid
505
 * (the OK button is not gated on Util::configIsValid()); the constructor
506
 * keeps asking until the configuration is usable or this returns false.
507
 */
508
auto MainWindow::config() -> bool {
2 ✔
509
  if (m_freshStart) {
2 ✔
510
    // No usable configuration yet: the wizard finds the programs, the key
511
    // and the store, and creates the store when it has to.
512
    FirstRunWizard wizard(this);
2 ✔
513
    if (wizard.exec() != QDialog::Accepted) {
2 ✔
514
      return false;
515
    }
516
  } else {
2 ✔
NEW
517
    ConfigDialog d(this);
×
NEW
518
    d.setModal(true);
×
NEW
519
    if (d.exec() != QDialog::Accepted) {
×
520
      return false;
521
    }
UNCOV
522
  }
×
523

524
  applyTextBrowserSettings();
1 ✔
525
  applyWindowFlagsSettings();
1 ✔
526

527
  updateProfileBox();
1 ✔
528
  const AppSettings s = QtPassSettings::load();
1 ✔
529
  m_tree->setStore(s.passStore);
1 ✔
530
  deselect();
1 ✔
531
  ui->treeView->setCurrentIndex(QModelIndex());
1 ✔
532

533
  Pass *activePass = QtPassSettings::getPass();
1 ✔
534
  activePass->updateEnv();
1 ✔
535
  m_tree->setPass(activePass);
1 ✔
536
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
1 ✔
537
  m_qtPass->clipboard().setAutoclearTimer();
1 ✔
538

539
  updateGitButtonVisibility();
1 ✔
540
  updateOtpButtonVisibility();
1 ✔
541
  updateGrepButtonVisibility();
1 ✔
542
  updateProcessOutputVisibility();
1 ✔
543
  if (s.useTrayIcon && m_tray == nullptr) {
1 ✔
544
    initTrayIcon();
×
545
  } else if (!s.useTrayIcon && m_tray != nullptr) {
1 ✔
546
    destroyTrayIcon();
×
547
  }
548

549
  // Leave the fresh-start state in place while the accepted configuration is
550
  // still unusable (for example the user declined to create the store), so
551
  // the next attempt from the constructor's loop runs the first-run wizard
552
  // again instead of showing the bare dialog. The re-prompt itself lives
553
  // there: recursing here re-ran the dialog on this stack frame and never
554
  // reported a cancel back to the caller.
555
  if (Util::configIsValid(s)) {
1 ✔
556
    m_freshStart = false;
1 ✔
557
  }
558
  return true;
559
}
1 ✔
560

561
/**
562
 * @brief MainWindow::onUpdate do a git pull
563
 */
564
void MainWindow::onUpdate(bool block) {
×
565
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
566
  if (block) {
×
567
    QtPassSettings::getPass()->GitPull_b();
×
568
  } else {
569
    QtPassSettings::getPass()->GitPull();
×
570
  }
571
}
×
572

573
/**
574
 * @brief MainWindow::onPush do a git push
575
 */
576
void MainWindow::onPush() {
×
577
  if (QtPassSettings::isUseGit()) {
×
578
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
579
    QtPassSettings::getPass()->GitPush();
×
580
  }
581
}
×
582

583
/**
584
 * @brief MainWindow::getFile get the selected file path
585
 * @param index
586
 * @param forPass returns relative path without '.gpg' extension
587
 * @return path
588
 * @return
589
 */
590
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
12 ✔
591
  return m_tree->fileFor(index, forPass);
12 ✔
592
}
593

594
/**
595
 * @brief MainWindow::on_treeView_clicked read the selected password file
596
 * @param index
597
 */
598
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
3 ✔
599
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
3 ✔
600
  QString file = getFile(index, true);
3 ✔
601
  ui->passwordName->setText(file);
3 ✔
602
  if (!file.isEmpty() && !cleared) {
3 ✔
603
    // Remember what the panel is about to show, so onOtp() can tell whether the
604
    // code it can see belongs to the entry that is currently selected.
605
    m_shownFile = file;
3 ✔
606
    QtPassSettings::getPass()->Show(file);
6 ✔
607
  } else {
608
    m_shownFile.clear();
×
609
    clearPanel(false);
×
610
    ui->actionEdit->setEnabled(false);
×
611
    ui->actionDelete->setEnabled(true);
×
612
  }
613
}
3 ✔
614

615
/**
616
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
617
 * TreeViewItem, open the edit Window
618
 * @param index
619
 */
620
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
1 ✔
621
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
1 ✔
622

623
  if (fileOrFolder.isFile()) {
1 ✔
624
    editPassword(getFile(index, true));
2 ✔
625
  }
626
}
1 ✔
627

628
/**
629
 * @brief MainWindow::deselect clear the selection, password and copy buffer
630
 */
631
void MainWindow::deselect() {
2 ✔
632
  m_shownFile.clear();
2 ✔
633
  cancelOtpRequest();
2 ✔
634
  m_qtPass->clipboard().clear();
2 ✔
635
  ui->treeView->clearSelection();
2 ✔
636
  ui->actionEdit->setEnabled(false);
2 ✔
637
  ui->actionDelete->setEnabled(false);
2 ✔
638
  ui->passwordName->setText("");
2 ✔
639
  clearPanel(false);
2 ✔
640
}
2 ✔
641

642
void MainWindow::executeWrapperStarted() {
13 ✔
643
  m_displayPanel->clear();
13 ✔
644
  ui->textBrowser->clear();
13 ✔
645
  setUiElementsEnabled(false);
13 ✔
646
  clearPanelTimer.stop();
13 ✔
647
  if (QtPassSettings::isShowProcessOutput()) {
13 ✔
648
    m_processOutput->setVisible(true);
8 ✔
649
  }
650
}
13 ✔
651

652
/**
653
 * @brief Handles displaying parsed password entry content in the main window:
654
 * the panel shows the parsed fields, the text browser what is left over.
655
 * @param p_output - The raw output text containing the password entry data.
656
 * @param file - The entry the output belongs to; anything but the entry most
657
 * recently asked for is dropped.
658
 */
659
void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
6 ✔
660
  // Only the entry we most recently asked to show may repaint the panel; a
661
  // slower decrypt of an entry the user has since left is dropped.
662
  if (!file.isEmpty() && !m_shownFile.isEmpty() && file != m_shownFile) {
6 ✔
663
    return;
×
664
  }
665
  const AppSettings s = QtPassSettings::load();
6 ✔
666
  QStringList templ =
667
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
6 ✔
668
  bool allFields = s.useTemplate && s.templateAllFields;
6 ✔
669
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
6 ✔
670
  QString output = p_output;
671
  // Display variant: empty when the password line is itself an otpauth URI, so
672
  // the shared secret is neither rendered nor copied to the clipboard.
673
  QString password = fileContent.getPasswordForDisplay();
6 ✔
674

675
  // set clipped text
676
  //
677
  // Skipped for an OTP request: the user asked for a one-time code, not the
678
  // password, and writing both in one event-loop turn can leave the Windows
679
  // clipboard empty (two OleSetClipboard calls back to back).
680
  if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
6 ✔
681
    m_qtPass->clipboard().copyIfAlways(password, p_output);
5 ✔
682
  }
683

684
  // first clear the current view:
685
  m_displayPanel->clear();
6 ✔
686

687
  // show what is needed:
688
  if (s.hideContent) {
6 ✔
689
    output = "***" + tr("Content hidden") + "***";
×
690
  } else if (!s.displayAsIs) {
6 ✔
691
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
12 ✔
692
                                  s.useOtp ? fileContent.getOtpUri()
6 ✔
693
                                           : QString());
694
    output = fileContent.getRemainingDataForDisplay();
12 ✔
695
  }
696

697
  if (s.useAutoclearPanel) {
6 ✔
698
    clearPanelTimer.start();
×
699
  }
700

701
  flashText(QtPass::formatOutput(output), false, true);
12 ✔
702
  setUiElementsEnabled(true);
6 ✔
703
}
12 ✔
704

705
/**
706
 * @brief Generates a one-time password from a decrypted entry and copies it.
707
 *
708
 * Connected to Pass::finishedShow for good; reacts only to the entry onOtp()
709
 * asked for. passShowHandler is connected first, so by the time this runs the
710
 * panel has already been repainted and the UI re-enabled.
711
 *
712
 * @param p_output - The decrypted entry content.
713
 * @return void - This function does not return a value.
714
 */
715
void MainWindow::otpFromFileToClipboard(const QString &p_output,
9 ✔
716
                                        const QString &file) {
717
  // Permanently connected: only the decrypt of the entry onOtp() asked for
718
  // is ours, everything else belongs to a tree click or a Ctrl+C.
719
  if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
9 ✔
720
    return;
8 ✔
721
  }
722
  m_otpRequestFile.clear();
2 ✔
723

724
  if (p_output.isEmpty()) {
2 ✔
725
    // Distinguish "could not read the entry" from "entry has no OTP".
726
    flashText(tr("Could not decrypt this password entry"), true);
×
727
    setUiElementsEnabled(true);
×
728
    return;
×
729
  }
730

731
  // passShowHandler is connected first, so it has already repainted the panel
732
  // for this same finishedShow. When it rendered the OTP row the current code
733
  // is derived and cached, so reuse it instead of re-loading settings and
734
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
735
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
736
  const QString shown = m_displayPanel->currentOtpCode();
2 ✔
737
  if (!shown.isEmpty()) {
2 ✔
738
    m_qtPass->clipboard().copyText(shown);
1 ✔
739
    showStatusMessage(tr("OTP code copied to clipboard"));
1 ✔
740
    setUiElementsEnabled(true);
1 ✔
741
    return;
742
  }
743

744
  const AppSettings s = QtPassSettings::load();
1 ✔
745
  // Parse with the same template settings passShowHandler uses, so an OTP
746
  // field is recognised identically in both paths.
747
  const QStringList templ =
748
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
1 ✔
749
  const bool allFields = s.useTemplate && s.templateAllFields;
1 ✔
750
  const FileContent fileContent =
751
      FileContent::parse(p_output, templ, allFields);
1 ✔
752

753
  const std::optional<Totp::Settings> settings =
754
      Totp::parse(fileContent.getOtpUri());
2 ✔
755
  if (settings.has_value()) {
1 ✔
756
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
2 ✔
757
    showStatusMessage(tr("OTP code copied to clipboard"));
2 ✔
758
  } else {
759
    flashText(tr("No OTP code found in this password entry"), true);
×
760
  }
761
  setUiElementsEnabled(true);
1 ✔
762
}
2 ✔
763

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

793
/**
794
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
795
 * elements
796
 * @param state
797
 */
798
void MainWindow::setUiElementsEnabled(bool state) {
90 ✔
799
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
800
  // the user's escape hatch and endReencryptPath() releases the interface, so
801
  // neither an unrelated completion nor the watchdog may re-enable it early.
802
  if (state && m_reencryptRunning) {
90 ✔
803
    m_uiWatchdog.stop();
1 ✔
804
    return;
1 ✔
805
  }
806
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
807
  if (state) {
808
    m_uiWatchdog.stop();
65 ✔
809
  } else {
810
    m_uiWatchdog.start();
24 ✔
811
  }
812
  ui->treeView->setEnabled(state);
89 ✔
813
  ui->lineEdit->setEnabled(state);
89 ✔
814
  ui->actionAddPassword->setEnabled(state);
89 ✔
815
  ui->actionAddFolder->setEnabled(state);
89 ✔
816
  ui->actionUsers->setEnabled(state);
89 ✔
817
  ui->actionConfig->setEnabled(state);
89 ✔
818
  // is a file selected?
819
  state &= ui->treeView->currentIndex().isValid();
178 ✔
820
  ui->actionDelete->setEnabled(state);
89 ✔
821
  ui->actionEdit->setEnabled(state);
89 ✔
822
  updateGitButtonVisibility();
89 ✔
823
  // `state` is now "UI enabled AND a file is selected", which is exactly when
824
  // generating an OTP makes sense.
825
  updateOtpButtonVisibility(state);
89 ✔
826
}
827

828
/**
829
 * @brief Restores the main window geometry, state, position, size, and
830
 * tray/icon settings from saved application settings.
831
 * @example
832
 * MainWindow window;
833
 * window.restoreWindow();
834
 *
835
 * @return void - This function does not return a value.
836
 */
837
void MainWindow::restoreWindow() {
48 ✔
838
  // saveGeometry() covers position, size, maximized state and screen; the
839
  // separate pos/size/maximized keys that used to be applied on top of it
840
  // are what discarded the restored position, and the unconditional
841
  // re-centre in main() then discarded the rest. Centre only when nothing
842
  // was saved.
843
  const QByteArray geometry = QtPassSettings::getGeometry();
96 ✔
844
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
48 ✔
845
    WindowStateStore::centreOnCursorScreen(*this);
4 ✔
846
  }
847
  restoreState(QtPassSettings::getSavestate(saveState()));
48 ✔
848
  const AppSettings s = QtPassSettings::load();
48 ✔
849

850
  applyWindowFlagsSettings();
48 ✔
851

852
  if (s.useTrayIcon && m_tray == nullptr) {
48 ✔
853
    initTrayIcon();
×
854
    if (s.startMinimized) {
×
855
      // since we are still in constructor, can't directly hide
856
      QTimer::singleShot(10, this, &MainWindow::hide);
×
857
    }
858
  } else if (!s.useTrayIcon && m_tray != nullptr) {
48 ✔
859
    destroyTrayIcon();
×
860
  }
861
}
96 ✔
862

863
/**
864
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
865
 */
866
void MainWindow::onConfig() { config(); }
×
867

868
/**
869
 * @brief MainWindow::showAbout version, link and licence in a standard box.
870
 */
871
void MainWindow::showAbout() {
×
872
  QMessageBox::about(
×
873
      this, tr("About QtPass"),
×
874
      tr("<h3>QtPass %1</h3>"
×
875
         "<p>A multi-platform GUI for <a "
876
         "href=\"https://www.passwordstore.org/\">"
877
         "pass</a>, the standard Unix password manager.</p>"
878
         "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
879
         "<a href=\"https://github.com/IJHack/QtPass\">Source and "
880
         "issues</a></p>"
881
         "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
882
         "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
883
         "version 3</a> or later.</p>")
884
          .arg(QStringLiteral(VERSION),
×
885
               QString::number(QDate::currentDate().year())));
×
886
}
×
887

888
/**
889
 * @brief Executes when the string in the search box changes, collapses the
890
 * TreeView
891
 * @param arg1
892
 */
893
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
894
  if (m_grep.inGrepMode())
×
895
    return;
896
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
897
  ui->treeView->expandAll();
×
898
  clearPanel(false);
×
899
  ui->passwordName->setText("");
×
900
  ui->actionEdit->setEnabled(false);
×
901
  ui->actionDelete->setEnabled(false);
×
902
  searchTimer.start();
×
903
}
904

905
/**
906
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
907
 * time from two keypresses is elapsed
908
 */
909
void MainWindow::onTimeoutSearch() {
×
910
  QString query = ui->lineEdit->text();
×
911

912
  if (query.isEmpty()) {
×
913
    ui->treeView->collapseAll();
×
914
    deselect();
×
915
  }
916

917
  query.replace(QStringLiteral(" "), ".*");
×
918
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
919
  if (!regExp.isValid())
×
920
    return;
921
  m_tree->setFilter(regExp);
×
922

923
  if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
×
924
    selectFirstFile();
×
925
  } else {
926
    ui->actionEdit->setEnabled(false);
×
927
    ui->actionDelete->setEnabled(false);
×
928
  }
929
}
×
930

931
/**
932
 * @brief MainWindow::on_lineEdit_returnPressed get searching
933
 *
934
 * Select the first possible file in the tree
935
 */
936
void MainWindow::on_lineEdit_returnPressed() {
×
937
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
×
938
                    << m_tree->proxy().rowCount();
×
939

940
  if (m_grep.inGrepMode()) {
×
941
    const QString query = ui->lineEdit->text();
×
942
    if (!query.isEmpty()) {
×
943
      ui->grepResultsList->clear();
×
944
      ui->statusBar->showMessage(tr("Searching…"));
×
945
      if (m_grep.beginSearch()) {
946
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
947
      }
948
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
949
    } else {
950
      if (m_grep.cancelSearch()) {
951
        QApplication::restoreOverrideCursor();
×
952
      }
953
      ui->grepResultsList->clear();
×
954
      ui->grepResultsList->setVisible(false);
×
955
      ui->treeView->setVisible(true);
×
956
    }
957
    return;
958
  }
959

960
  if (m_tree->proxy().rowCount() > 0) {
×
961
    selectFirstFile();
×
962
    on_treeView_clicked(ui->treeView->currentIndex());
×
963
  }
964
}
965

966
/**
967
 * @brief Toggle grep (content search) mode.
968
 */
969
void MainWindow::on_grepButton_toggled(bool checked) {
×
970
  const AppSettings s = QtPassSettings::load();
×
971
  if (checked) {
×
972
    m_grep.enterGrepMode();
973
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
974
    // The regex dialect depends on the backend (see Pass::Grep): the pass
975
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
976
    ui->lineEdit->setToolTip(
×
977
        s.usePass
×
978
            ? tr("Content search uses POSIX basic regular expressions "
×
979
                 "(pass grep).")
980
            : tr("Content search uses Perl-compatible regular expressions "
981
                 "(PCRE)."));
982
    ui->lineEdit->clear();
×
983
    searchTimer.stop();
×
984
    m_tree->setFilter(QRegularExpression());
×
985
    ui->grepResultsList->setVisible(false);
×
986
    // Keep treeView visible until results arrive
987
  } else {
988
    if (m_grep.leaveGrepMode()) {
989
      QApplication::restoreOverrideCursor();
×
990
    }
991
    searchTimer.stop();
×
992
    ui->lineEdit->blockSignals(true);
×
993
    ui->lineEdit->clear();
×
994
    ui->lineEdit->blockSignals(false);
×
995
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
996
    ui->lineEdit->setToolTip(QString());
×
997
    ui->grepResultsList->clear();
×
998
    ui->grepResultsList->setVisible(false);
×
999
    ui->treeView->setVisible(true);
×
1000
    m_tree->setFilter(QRegularExpression());
×
1001
  }
1002
}
×
1003

1004
/**
1005
 * @brief Display grep results in grepResultsList.
1006
 */
1007
void MainWindow::onGrepFinished(
×
1008
    const QList<QPair<QString, QStringList>> &results) {
1009
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
1010
  if (outcome.restoreCursor) {
×
1011
    QApplication::restoreOverrideCursor();
×
1012
  }
1013
  // Re-enable the UI before the discard check so a cancelled search can never
1014
  // leave controls disabled.
1015
  setUiElementsEnabled(true);
×
1016
  if (outcome.discard) {
×
1017
    return;
×
1018
  }
1019
  if (!m_grep.inGrepMode())
×
1020
    return;
1021
  ui->grepResultsList->clear();
×
1022
  if (results.isEmpty()) {
×
1023
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
1024
    ui->grepResultsList->setVisible(false);
×
1025
    ui->treeView->setVisible(true);
×
1026
    return;
×
1027
  }
1028
  const AppSettings s = QtPassSettings::load();
×
1029
  const bool hideContent = s.hideContent;
×
1030
  int totalLines = 0;
1031
  for (const auto &pair : results) {
×
1032
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
1033
    entryItem->setText(0, pair.first);
×
1034
    entryItem->setData(0, Qt::UserRole, pair.first);
×
1035
    for (const QString &line : pair.second) {
×
1036
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
1037
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
1038
                                       : line);
1039
      lineItem->setData(0, Qt::UserRole, pair.first);
×
1040
      ++totalLines;
×
1041
    }
1042
  }
1043
  ui->grepResultsList->expandAll();
×
1044
  ui->treeView->setVisible(false);
×
1045
  ui->grepResultsList->setVisible(true);
×
1046
  ui->statusBar->showMessage(
×
1047
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
1048
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
1049
      3000);
1050
  if (s.useAutoclearPanel)
×
1051
    clearPanelTimer.start();
×
1052
}
×
1053

1054
/**
1055
 * @brief Navigate to the password entry when a grep result is clicked.
1056
 */
1057
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
1058
                                                int /*column*/) {
1059
  const AppSettings s = QtPassSettings::load();
×
1060
  const QString entry = item->data(0, Qt::UserRole).toString();
×
1061
  if (entry.isEmpty())
×
1062
    return;
1063
  const QString fullPath =
1064
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
×
1065
  const QModelIndex proxyIndex = m_tree->indexFor(fullPath);
×
1066
  if (!proxyIndex.isValid())
1067
    return;
1068
  ui->treeView->setCurrentIndex(proxyIndex);
×
1069
  on_treeView_clicked(proxyIndex);
×
1070
  if (s.hideContent || s.useAutoclearPanel)
×
1071
    ui->grepResultsList->clear();
×
1072
  ui->grepResultsList->setVisible(false);
×
1073
  ui->treeView->setVisible(true);
×
1074
  ui->treeView->scrollTo(proxyIndex);
×
1075
  ui->treeView->setFocus();
×
1076
}
×
1077

1078
/**
1079
 * @brief MainWindow::selectFirstFile select the first possible file in the
1080
 * tree
1081
 */
1082
void MainWindow::selectFirstFile() {
×
1083
  QModelIndex index = firstFile(m_tree->rootIndex());
×
1084
  ui->treeView->setCurrentIndex(index);
×
1085
}
×
1086

1087
/**
1088
 * @brief MainWindow::firstFile return location of first possible file
1089
 * @param parentIndex
1090
 * @return QModelIndex
1091
 */
1092
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
1093
  StoreModel &proxy = m_tree->proxy();
×
1094
  int numRows = proxy.rowCount(parentIndex);
×
1095
  for (int row = 0; row < numRows; ++row) {
×
1096
    QModelIndex index = proxy.index(row, 0, parentIndex);
×
1097
    if (m_tree->fileInfo(index).isFile()) {
×
1098
      return index;
×
1099
    }
1100
    if (proxy.hasChildren(index)) {
×
1101
      QModelIndex childFile = firstFile(index);
×
1102
      if (childFile.isValid())
1103
        return childFile;
×
1104
    }
1105
  }
1106
  return QModelIndex();
1107
}
1108

1109
/**
1110
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1111
 * the password store and warn the user.
1112
 *
1113
 * Used before file/folder creation, move, and rename to stop user-typed
1114
 * names like "../../etc/passwd" or absolute paths from escaping the
1115
 * configured store root via the input dialogs.
1116
 *
1117
 * @param candidate Absolute candidate path to validate.
1118
 * @return true if the path is inside the password store; false otherwise (a
1119
 * warning dialog is shown in that case).
1120
 */
1121
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1122
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1123
    return true;
1124
  }
1125
  QMessageBox::warning(this, tr("Invalid name"),
×
1126
                       tr("That name would resolve outside the password "
×
1127
                          "store. Please choose a different name."));
1128
  return false;
×
1129
}
1130

1131
/**
1132
 * @brief MainWindow::setPassword open passworddialog
1133
 * @param file which pgp file
1134
 * @param isNew insert (not update)
1135
 */
1136
void MainWindow::setPassword(const QString &file, bool isNew) {
1 ✔
1137
  const AppSettings s = QtPassSettings::load();
1 ✔
1138
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
1 ✔
1139

1140
  if (isNew) {
1 ✔
1141
    const QString storePath = s.passStore;
1142
    QString folder = m_tree->currentDir(false);
×
1143
    if (folder.isEmpty()) {
×
1144
      folder = storePath;
×
1145
    }
1146
    QHash<QString, QStringList> templates =
1147
        TemplateIO::readTemplates(storePath);
×
1148
    if (!templates.isEmpty()) {
1149
      QString defaultTemplate =
1150
          TemplateIO::getFolderTemplate(folder, storePath);
×
1151
      d.setAvailableTemplates(templates, defaultTemplate);
×
1152
    }
1153
  }
×
1154

1155
  if (!d.exec()) {
1 ✔
1156
    ui->treeView->setFocus();
×
1157
  }
1158
}
1 ✔
1159

1160
/**
1161
 * @brief MainWindow::addPassword add a new password by showing a
1162
 * number of dialogs.
1163
 */
1164
void MainWindow::addPassword() {
×
1165
  const QString passStore = QtPassSettings::load().passStore;
×
1166
  bool ok;
1167
  QString dir = m_tree->currentDir(true);
×
1168
  QString file =
1169
      QInputDialog::getText(this, tr("New file"),
×
1170
                            tr("New password file: \n(Will be placed in %1 )")
×
1171
                                .arg(passStore + m_tree->currentDir(true)),
×
1172
                            QLineEdit::Normal, "", &ok);
×
1173
  if (!ok || file.isEmpty()) {
×
1174
    return;
1175
  }
1176
  file = dir + file;
×
1177
  if (!confirmPathInStore(passStore + file)) {
×
1178
    return;
1179
  }
1180
  setPassword(file);
×
1181
}
1182

1183
/**
1184
 * @brief MainWindow::onDelete remove password, if you are
1185
 * sure.
1186
 */
1187
void MainWindow::onDelete() {
×
1188
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1189
  if (!currentIndex.isValid()) {
1190
    // This fixes https://github.com/IJHack/QtPass/issues/556
1191
    // Otherwise the entire password directory would be deleted if
1192
    // nothing is selected in the tree view.
1193
    return;
×
1194
  }
1195

1196
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
×
1197
  QString file = "";
×
1198
  bool isDir = false;
1199

1200
  if (fileOrFolder.isFile()) {
×
1201
    file = getFile(ui->treeView->currentIndex(), true);
×
1202
  } else {
1203
    file = m_tree->currentDir(true);
×
1204
    isDir = true;
1205
  }
1206

1207
  QString dirMessage = tr(" and the whole content?");
1208
  if (isDir) {
×
1209
    QDirIterator it(m_tree->fileSystem().rootPath() + QDir::separator() + file,
×
1210
                    QDirIterator::Subdirectories);
×
1211
    bool okDir = true;
1212
    while (it.hasNext() && okDir) {
×
1213
      it.next();
×
1214
      if (QFileInfo(it.filePath()).isFile()) {
×
1215
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1216
          okDir = false;
1217
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1218
                          "there are unexpected files in the given folder, "
1219
                          "check them before continue.</strong>");
1220
        }
1221
      }
1222
    }
1223
  }
×
1224

1225
  if (QMessageBox::question(
×
1226
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1227
          tr("Are you sure you want to delete %1%2?")
×
1228
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1229
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1230
    return;
1231
  }
1232

1233
  QtPassSettings::getPass()->Remove(file, isDir);
×
1234
}
×
1235

1236
/**
1237
 * @brief MainWindow::cancelOtpRequest forget the pending OTP and copy requests.
1238
 *
1239
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1240
 * decrypt never emits finishedShow; if the request stayed pending, the next
1241
 * successful decrypt of that same entry (the user clicking it again) would be
1242
 * taken as the answer and copy something the user no longer asked for.
1243
 */
1244
void MainWindow::cancelOtpRequest() {
12 ✔
1245
  m_otpRequestFile.clear();
12 ✔
1246
  m_copyRequestFile.clear();
12 ✔
1247
}
12 ✔
1248

1249
/**
1250
 * @brief MainWindow::onOtp generate the selected entry's OTP code and copy it.
1251
 *
1252
 * Decrypts the entry once and derives the code in-process, so this works with
1253
 * either backend and on every platform. The code itself is produced by
1254
 * otpFromFileToClipboard once Pass::finishedShow arrives.
1255
 */
1256
void MainWindow::onOtp() {
4 ✔
1257
  QString file = getFile(ui->treeView->currentIndex(), true);
4 ✔
1258
  if (file.isEmpty()) {
4 ✔
1259
    flashText(tr("No password selected for OTP generation"), true);
×
1260
    return;
×
1261
  }
1262
  if (!QtPassSettings::isUseOtp()) {
4 ✔
1263
    // Normally unreachable (the action is hidden), but never fail silently.
1264
    flashText(tr("No OTP code found in this password entry"), true);
×
1265
    return;
×
1266
  }
1267

1268
  // Fast path: the panel already shows a live code for the selected entry, so
1269
  // there is nothing to decrypt. This also keeps the clipboard write single —
1270
  // going through Show() would let passShowHandler copy the password first, and
1271
  // two QClipboard::setMimeData calls in one event-loop turn can leave the
1272
  // Windows clipboard empty.
1273
  //
1274
  // Only when the panel is showing *this* entry: arrow-key navigation and
1275
  // right-clicking move the tree's currentIndex without emitting
1276
  // QTreeView::clicked, so the visible code can belong to a different account.
1277
  const QString shown =
1278
      (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
4 ✔
1279
  if (!shown.isEmpty()) {
4 ✔
1280
    m_qtPass->clipboard().copyText(shown);
1 ✔
1281
    showStatusMessage(tr("OTP code copied to clipboard"));
2 ✔
1282
    return;
1283
  }
1284

1285
  // Fallback: no OTP row on screen, so decrypt once. The request file stops
1286
  // passShowHandler putting the password on the clipboard for a request that
1287
  // only asked for a code, and tells otpFromFileToClipboard which decrypt is
1288
  // its answer.
1289
  m_otpRequestFile = file;
3 ✔
1290
  setUiElementsEnabled(false);
3 ✔
1291
  // passShowHandler repaints the panel for this Show too, so keep the marker in
1292
  // step or a second request would decrypt again instead of taking the fast
1293
  // path. Safe to set now: executeWrapperStarted() clears the panel on every
1294
  // command, so a failed decrypt leaves currentOtpCode() empty.
1295
  m_shownFile = file;
3 ✔
1296
  QtPassSettings::getPass()->Show(file);
6 ✔
1297
}
1298

1299
/**
1300
 * @brief MainWindow::onEdit try and edit (selected) password.
1301
 */
1302
void MainWindow::onEdit() {
×
1303
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1304
  editPassword(file);
×
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
  const QString dir = m_tree->currentDir(false);
×
1314

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

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

1337
/**
1338
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1339
 * select a more appropriate one to view too
1340
 */
1341
void MainWindow::updateProfileBox() {
42 ✔
1342
  const Profiles profiles = QtPassSettings::getProfiles();
42 ✔
1343

1344
  if (profiles.isEmpty()) {
×
1345
    ui->profileWidget->hide();
42 ✔
1346
  } else {
1347
    ui->profileWidget->show();
×
1348
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1349
    ui->profileBox->clear();
×
1350
    for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
×
1351
      if (!i.key().isEmpty()) {
×
1352
        ui->profileBox->addItem(i.key());
×
1353
      }
1354
    }
1355
    ui->profileBox->model()->sort(0);
×
1356
  }
1357
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
84 ✔
1358
  if (index != -1) { //  -1 for not found
42 ✔
1359
    ui->profileBox->setCurrentIndex(index);
×
1360
  }
1361
}
42 ✔
1362

1363
/**
1364
 * @brief Handles changes to the selected profile in the profile combo box.
1365
 * @details Ignores the event during a fresh start or when the selected profile
1366
 * matches the current profile. Otherwise, it clears the password field, updates
1367
 * the active profile and related settings, refreshes the environment, and
1368
 * resets the tree view and action states to reflect the newly selected profile.
1369
 *
1370
 * @param name - The newly selected profile name.
1371
 * @return void - This function does not return a value.
1372
 *
1373
 */
1374
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1375
  if (m_freshStart || name == QtPassSettings::getProfile()) {
×
1376
    return;
×
1377
  }
1378

1379
  ui->lineEdit->clear();
×
1380

1381
  const Profile prof = QtPassSettings::getProfiles().value(name);
×
1382
  AppSettings s = QtPassSettings::load();
×
1383
  s.activeProfile = name;
×
1384
  s.passStore = prof.path;
×
1385
  s.passSigningKey = prof.signingKey;
×
1386
  // A profile without its own Git flags keeps the global values.
1387
  s.useGit = prof.useGit.value_or(s.useGit);
×
1388
  s.autoPush = prof.autoPush.value_or(s.autoPush);
×
1389
  s.autoPull = prof.autoPull.value_or(s.autoPull);
×
1390
  QtPassSettings::save(s);
×
1391
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1392

1393
  QtPassSettings::getPass()->updateEnv();
×
1394

1395
  m_tree->setStore(QtPassSettings::getPassStore());
×
1396
  deselect();
×
1397
  ui->treeView->setCurrentIndex(QModelIndex());
×
1398
}
×
1399

1400
/**
1401
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1402
 * support
1403
 * it
1404
 */
1405
void MainWindow::initTrayIcon() {
×
1406
  m_tray = new TrayIcon(this);
×
1407
  if (!m_tray->getIsAllocated()) {
×
1408
    destroyTrayIcon();
×
1409
  }
1410
}
×
1411

1412
/**
1413
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1414
 */
1415
void MainWindow::destroyTrayIcon() {
×
1416
  delete m_tray;
×
1417
  m_tray = nullptr;
×
1418
}
×
1419

1420
/**
1421
 * @brief MainWindow::closeEvent hide or quit
1422
 * @param event
1423
 */
1424
void MainWindow::closeEvent(QCloseEvent *event) {
2 ✔
1425
  // Save in both branches: a tray user only ever hides the window, and
1426
  // used to quit through the tray menu with nothing ever written.
1427
  saveWindowState();
2 ✔
1428
  if (QtPassSettings::isHideOnClose()) {
2 ✔
1429
    this->hide();
2 ✔
1430
    event->ignore();
1431
  } else {
1432
    m_qtPass->clipboard().clear();
×
1433
    event->accept();
1434
    // A visible QSystemTrayIcon keeps the application alive after the last
1435
    // window closes, so quitOnLastWindowClosed never fires and the window
1436
    // merely vanishes into the tray. Quit explicitly so closing the window
1437
    // actually exits when "hide on close" is disabled.
1438
    QApplication::quit();
×
1439
  }
1440
}
2 ✔
1441

1442
/**
1443
 * @brief MainWindow::eventFilter filter out some events and focus the
1444
 * treeview
1445
 * @param obj
1446
 * @param event
1447
 * @return
1448
 */
1449
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
1,319 ✔
1450
  if (obj == ui->toolBar && event->type() == QEvent::PaletteChange) {
1,319 ✔
1451
    // KDE's Breeze style gives top toolbars a "header" palette of its own.
1452
    // After a runtime light/dark switch it recomputes that palette from a
1453
    // cached kdeglobals and stamps the previous theme's colours back on, so
1454
    // the toolbar stays dark on a light window (or vice versa) with matching
1455
    // invisible icons. A legitimate header tint is close to the window
1456
    // colour; a stale theme is not. When the two disagree, drop the imposed
1457
    // palette so the toolbar follows the application palette again. Deferred
1458
    // so we never re-enter the style while it is still applying its palette.
1459
    QTimer::singleShot(0, this, &MainWindow::dropStaleToolBarPalette);
6 ✔
1460
  }
1461
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
1,319 ✔
1462
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1463
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1464
      ui->treeView->setFocus();
×
1465
    }
1466
  }
1467
  return QObject::eventFilter(obj, event);
1,319 ✔
1468
}
1469

1470
/**
1471
 * @brief Reset the toolbar palette when a style left it in the wrong theme.
1472
 *
1473
 * Compares the lightness of the toolbar's Window colour with the application
1474
 * palette; a difference above kStaleToolBarLightness means the toolbar shows
1475
 * the other theme. See the PaletteChange branch in eventFilter().
1476
 */
1477
void MainWindow::dropStaleToolBarPalette() {
3 ✔
1478
  if (!ui->toolBar->testAttribute(Qt::WA_SetPalette)) {
3 ✔
1479
    return;
1480
  }
1481
  constexpr int kStaleToolBarLightness = 64;
1482
  const int barLightness =
1483
      ui->toolBar->palette().color(QPalette::Window).lightness();
4 ✔
1484
  const int appLightness =
1485
      QApplication::palette().color(QPalette::Window).lightness();
4 ✔
1486
  if (qAbs(barLightness - appLightness) > kStaleToolBarLightness) {
2 ✔
1487
    ui->toolBar->setPalette(QPalette());
1 ✔
1488
    // The style also paints the tools-area background on the main window
1489
    // from the same stale palette, and the toolbar is transparent by default;
1490
    // paint our own background so the reset is actually visible.
1491
    ui->toolBar->setAutoFillBackground(true);
1 ✔
1492
  }
1493
}
1494

1495
/**
1496
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1497
 * @param event
1498
 */
1499
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1500
  switch (event->key()) {
×
1501
  case Qt::Key_Delete:
×
1502
    onDelete();
×
1503
    break;
×
1504
  case Qt::Key_Return:
×
1505
  case Qt::Key_Enter:
1506
    if (m_tree->proxy().rowCount() > 0) {
×
1507
      on_treeView_clicked(ui->treeView->currentIndex());
×
1508
    }
1509
    break;
1510
  case Qt::Key_Escape:
×
1511
    ui->lineEdit->clear();
×
1512
    break;
×
1513
  default:
1514
    break;
1515
  }
1516
}
×
1517

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

1534
  ui->treeView->setCurrentIndex(index);
×
1535

1536
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1537

1538
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
×
1539

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

1571
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1572
      contextMenu.addMenu(shareMenu);
×
1573

1574
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1575
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1576

1577
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1578
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1579
                                                 QFile(exePath).exists());
×
1580

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

1586
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1587
      exportKey->setEnabled(gpgAvailable);
×
1588
      connect(exportKey, &QAction::triggered, this,
×
1589
              &MainWindow::exportPublicKey);
×
1590

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

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

1604
/**
1605
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1606
 * password window
1607
 * @param pos
1608
 */
1609
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1610
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1611
  // createStandardContextMenu() parents the menu to textBrowser. A stylesheet
1612
  // on the browser (it used to carry "background: palette(base)") cascades to
1613
  // the child QMenu and breaks its opaque native background. Reparent to the
1614
  // main window so the menu paints solid regardless of browser styling.
1615
  contextMenu->setParent(this, contextMenu->windowFlags());
×
1616
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1617

1618
  contextMenu->exec(globalPos);
×
1619
  delete contextMenu;
×
1620
}
×
1621

1622
/**
1623
 * @brief MainWindow::openFolder open the folder in the default file manager
1624
 */
1625
void MainWindow::openFolder() {
×
1626
  QString dir = m_tree->currentDir(false);
×
1627

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

1632
/**
1633
 * @brief MainWindow::addFolder add a new folder to store passwords in
1634
 */
1635
void MainWindow::addFolder() {
×
1636
  const AppSettings s = QtPassSettings::load();
×
1637
  bool ok;
1638
  QString dir = m_tree->currentDir(false);
×
1639
  QString newdir =
1640
      QInputDialog::getText(this, tr("New folder"),
×
1641
                            tr("New Folder: \n(Will be placed in %1 )")
×
1642
                                .arg(s.passStore + m_tree->currentDir(true)),
×
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
  // A .gpg-id only counts when it is signed once a signing key is configured:
1657
  // ImitatePass::verifyGpgIdFile and pass (PASSWORD_STORE_SIGNING_KEY) both
1658
  // refuse to encrypt into a folder whose .gpg-id has no matching
1659
  // .gpg-id.sig. Signing here would need the signing secret key and a
1660
  // passphrase prompt just to create a folder, so in that case leave the
1661
  // folder without its own .gpg-id: it inherits the parent's signed list,
1662
  // which encrypts to the same recipients. UsersDialog (Init) is the place to
1663
  // give it a distinct, signed list.
1664
  const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
×
1665
  if (s.addGPGId && !signingConfigured) {
×
1666
    // Seed the new folder's .gpg-id from the recipients that are currently
1667
    // in effect for its parent. The previous implementation walked
1668
    // listKeys("", true) and wrote every key whose `enabled` flag was set,
1669
    // but that flag is only toggled inside UsersDialog, so the loop wrote
1670
    // nothing and left a zero-byte .gpg-id that shadowed the parent's
1671
    // recipients (see #1682).
1672
    if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
×
1673
      QMessageBox::warning(
×
1674
          this, tr("Error"),
×
1675
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1676
      return;
×
1677
    }
1678
  }
1679
}
×
1680

1681
/**
1682
 * @brief MainWindow::renameFolder rename an existing folder
1683
 */
1684
void MainWindow::renameFolder() {
×
1685
  bool ok;
1686
  QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
×
1687
  QString srcDirName = QDir(srcDir).dirName();
×
1688
  QString newName =
1689
      QInputDialog::getText(this, tr("Rename folder"), 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() {
4 ✔
1750
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
4 ✔
1751

1752
  if (fileOrFolder.isFile()) {
4 ✔
1753
    QString file = getFile(ui->treeView->currentIndex(), true);
4 ✔
1754
    // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
1755
    // an earlier request is simply not this one's.
1756
    m_copyRequestFile = file;
4 ✔
1757
    // This Show repaints the panel as well; see onOtp().
1758
    m_shownFile = file;
4 ✔
1759
    QtPassSettings::getPass()->Show(file);
8 ✔
1760
  }
1761
}
4 ✔
1762

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

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

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

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

1816
  if (ret != QMessageBox::Yes)
×
1817
    return;
1818

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

1824
  QtPassSettings::getImitatePass()->reencryptPath(
×
1825
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1826
}
×
1827

1828
/**
1829
 * @brief MainWindow::startReencryptPath disable ui elements and treeview and
1830
 * show the progress dialog. Idempotent: MainWindow::reencryptPath calls it
1831
 * before ImitatePass emits startReencryptPath.
1832
 */
1833
void MainWindow::startReencryptPath() {
3 ✔
1834
  m_reencryptRunning = true;
3 ✔
1835
  setUiElementsEnabled(false);
3 ✔
1836
  // The worker always ends with endReencryptPath(), and Cancel is available
1837
  // throughout, so the generic watchdog is not needed for this operation.
1838
  m_uiWatchdog.stop();
3 ✔
1839
  ui->treeView->setDisabled(true);
3 ✔
1840
  if (m_reencryptProgress)
3 ✔
1841
    return;
1842
  auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
2 ✔
1843
                                       tr("Cancel"), 0, 0, this);
4 ✔
1844
  progress->setWindowTitle(tr("Re-encrypt passwords"));
2 ✔
1845
  progress->setWindowModality(Qt::WindowModal);
2 ✔
1846
  progress->setAutoClose(false);
2 ✔
1847
  progress->setAutoReset(false);
2 ✔
1848
  progress->setMinimumDuration(0);
2 ✔
1849
  connect(progress, &QProgressDialog::canceled, this, [this]() {
2 ✔
1850
    QtPassSettings::getImitatePass()->cancelReencryptPath();
×
1851
    showStatusMessage(tr("Cancelling re-encryption"), 5000);
×
1852
  });
×
1853
  m_reencryptProgress = progress;
1854
  progress->show();
2 ✔
1855
}
1856

1857
/**
1858
 * @brief MainWindow::reencryptProgress show how many files were checked
1859
 * @param current files checked so far
1860
 * @param total files found under the folder
1861
 */
1862
void MainWindow::reencryptProgress(int current, int total) {
4 ✔
1863
  if (!m_reencryptProgress)
4 ✔
1864
    return;
1865
  m_reencryptProgress->setMaximum(total);
3 ✔
1866
  m_reencryptProgress->setLabelText(
3 ✔
1867
      tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
6 ✔
1868
  // On a modal QProgressDialog setValue() calls processEvents(), which can
1869
  // deliver the queued completion: endReencryptPath() then hides the dialog
1870
  // and resets m_reencryptProgress under our feet. Keep it last and touch
1871
  // nothing afterwards.
1872
  m_reencryptProgress->setValue(current);
3 ✔
1873
}
1874

1875
/**
1876
 * @brief MainWindow::endReencryptPath re-enable ui elements
1877
 */
1878
void MainWindow::endReencryptPath() {
2 ✔
1879
  m_reencryptRunning = false;
2 ✔
1880
  if (m_reencryptProgress) {
2 ✔
1881
    m_reencryptProgress->hide();
2 ✔
1882
    m_reencryptProgress->deleteLater();
2 ✔
1883
    m_reencryptProgress = nullptr;
1884
  }
1885
  setUiElementsEnabled(true);
2 ✔
1886
}
2 ✔
1887

1888
/**
1889
 * @brief MainWindow::exportPublicKey export the configured signing key in
1890
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1891
 *
1892
 * Falls back to a help dialog when no signing key is configured or gpg is
1893
 * unavailable, so the user still gets actionable guidance.
1894
 */
1895
void MainWindow::exportPublicKey() {
×
1896
  const AppSettings s = QtPassSettings::load();
×
1897
  const QString identity = s.passSigningKey;
1898
  if (identity.isEmpty()) {
×
1899
    QMessageBox::information(
×
1900
        this, tr("Export Public Key"),
×
1901
        tr("<h3>Export Your Public Key</h3>"
×
1902
           "<p>No signing key is configured. Set one in QtPass Settings "
1903
           "&gt; GPG keys, or run this in a terminal:</p>"
1904
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1905
           "&gt;</pre>"
1906
           "<p>Then send the file to your teammates.</p>"));
1907
    return;
×
1908
  }
1909
  QString gpgExe = s.gpgExecutable;
1910
  if (gpgExe.isEmpty()) {
×
1911
    gpgExe = QStringLiteral("gpg");
×
1912
  }
1913
  QStringList args = {"--armor", "--export"};
×
1914
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1915
  QString stdOut;
×
1916
  QString stdErr;
×
1917
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
×
1918
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1919
    QMessageBox::warning(this, tr("Export Public Key"),
×
1920
                         tr("Could not export public key for %1.\n\n%2")
×
1921
                             .arg(identity, stdErr.isEmpty()
×
1922
                                                ? tr("No output from gpg.")
×
1923
                                                : stdErr));
1924
    return;
1925
  }
1926
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1927
  dialog.exec();
×
1928
}
×
1929

1930
/**
1931
 * @brief MainWindow::addRecipient open the recipient management dialog for
1932
 *        the supplied directory.
1933
 * @param dir Folder whose .gpg-id should be edited.
1934
 *
1935
 * Delegates to UsersDialog so users can tick/untick keys from their
1936
 * keyring as recipients of the folder; importing a foreign key into the
1937
 * keyring still has to happen via gpg (or QtPass settings) first.
1938
 */
1939
void MainWindow::addRecipient(const QString &dir) {
×
1940
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1941
  d.exec();
×
1942
}
×
1943

1944
/**
1945
 * @brief MainWindow::showShareHelp show help about GPG sharing
1946
 */
1947
void MainWindow::showShareHelp() {
×
1948
  QMessageBox::information(
×
1949
      this, tr("Sharing Passwords with GPG"),
×
1950
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1951
         "<p>To share passwords with other users:</p>"
1952
         "<ol>"
1953
         "<li><b>Export your public key</b> and send it to teammates</li>"
1954
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1955
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1956
         "them</li>"
1957
         "</ol>"
1958
         "<p>Only people who have a matching secret key can decrypt the "
1959
         "passwords.</p>"
1960
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1961
         "<p>See the FAQ for more details.</p>"));
1962
}
×
1963

1964
void MainWindow::updateGitButtonVisibility() {
90 ✔
1965
  const AppSettings s = QtPassSettings::load();
90 ✔
1966
  if (!s.useGit || (s.gitExecutable.isEmpty() && s.passExecutable.isEmpty())) {
90 ✔
1967
    enableGitButtons(false);
90 ✔
1968
  } else {
1969
    enableGitButtons(true);
×
1970
  }
1971
}
90 ✔
1972

1973
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
90 ✔
1974
  // No platform gating any more: codes are generated in-process, so this works
1975
  // on Windows and macOS and with either backend. It used to be hidden there
1976
  // because the pass-otp extension is Unix-only.
1977
  //
1978
  // Kept visible but disabled when OTP is off, as it was before: hiding it
1979
  // removed the only discoverable trace of the feature, and since an OTP field
1980
  // is suppressed from the panel unconditionally (its value is a secret), a
1981
  // user with the setting off would see nothing at all where their OTP data
1982
  // used to be.
1983
  //
1984
  // Enablement must honour uiEnabled: this is called from
1985
  // setUiElementsEnabled(), and ignoring the argument re-enabled the action
1986
  // while a decrypt was in flight, letting the user stack Show calls.
1987
  ui->actionOtp->setVisible(true);
90 ✔
1988
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
90 ✔
1989
}
90 ✔
1990

1991
void MainWindow::updateGrepButtonVisibility() {
42 ✔
1992
  const bool enabled = QtPassSettings::isUseGrepSearch();
42 ✔
1993
  ui->grepButton->setVisible(enabled);
42 ✔
1994
  ui->grepCaseButton->setVisible(enabled);
42 ✔
1995
  if (!enabled && m_grep.inGrepMode()) {
42 ✔
1996
    ui->grepButton->setChecked(false);
×
1997
  }
1998
}
42 ✔
1999

2000
void MainWindow::enableGitButtons(const bool &state) {
90 ✔
2001
  // Following GNOME guidelines is preferable disable buttons instead of hide
2002
  ui->actionPush->setEnabled(state);
90 ✔
2003
  ui->actionUpdate->setEnabled(state);
90 ✔
2004
}
90 ✔
2005

2006
/**
2007
 * @brief MainWindow::critical critical message popup wrapper.
2008
 * @param title
2009
 * @param msg
2010
 */
2011
void MainWindow::critical(const QString &title, const QString &msg) {
×
2012
  QMessageBox::critical(this, title, msg);
×
2013
}
×
2014

2015
/**
2016
 * @brief Route a process's output to the console, unless the panel is
2017
 *        switched off in the settings.
2018
 * @param output The stdout/stderr text from the process.
2019
 * @param isError true if this is error output (stderr).
2020
 * @param pid The process that ran, for the line prefix.
2021
 */
2022
void MainWindow::onProcessOutput(const QString &output, bool isError,
2 ✔
2023
                                 Enums::PROCESS pid) {
2024
  if (!QtPassSettings::isShowProcessOutput()) {
2 ✔
2025
    return;
2026
  }
2027
  m_processOutput->append(output, isError,
1 ✔
2028
                          ProcessOutputPanel::processName(pid));
2 ✔
2029
}
2030

2031
/**
2032
 * @brief Show or hide the console per the showProcessOutput setting.
2033
 */
2034
void MainWindow::updateProcessOutputVisibility() {
1 ✔
2035
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
1 ✔
2036
}
1 ✔
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