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

IJHack / QtPass / 35201678807

17 Sep 2026 08:48AM UTC coverage: 74.981% (+0.2%) from 74.81%
35201678807

Pull #1800

github

web-flow
Merge 1db22dd2d into 1b33d3b5b
Pull Request #1800: One resolution rule for external binaries

10 of 10 new or added lines in 1 file covered. (100.0%)

277 existing lines in 4 files now uncovered.

5784 of 7714 relevant lines covered (74.98%)

68.42 hits per line

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

42.98
/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 "passworddialog.h"
13
#include "passworddisplaypanel.h"
14
#include "pathvalidator.h"
15
#include "processoutputpanel.h"
16
#include "qpushbuttonasqrcode.h"
17
#include "qpushbuttonshowpassword.h"
18
#include "qpushbuttonwithclipboard.h"
19
#include "qtpass.h"
20
#include "qtpasssettings.h"
21
#include "templateio.h"
22
#include "totp.h"
23
#include "trayicon.h"
24
#include "ui_mainwindow.h"
25
#include "usersdialog.h"
26
#include "util.h"
27
#include "windowstatestore.h"
28
#include <QApplication>
29
#include <QCloseEvent>
30
#include <QDate>
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 <QProgressDialog>
43
#include <QPushButton>
44
#include <QShortcut>
45
#include <QTextCharFormat>
46
#include <QTextStream>
47
#include <QTimer>
48
#include <QTreeWidget>
49
#include <QUrl>
50
#include <utility>
51

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

67
  m_qtPass = new QtPass(this);
36 ✔
68

69
  // register shortcut ctrl/cmd + C to copy the currently selected password
70
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
36 ✔
71
                &MainWindow::copyPasswordFromTreeview);
36 ✔
72

73
  model.setNameFilters(QStringList() << "*.gpg");
108 ✔
74
  model.setNameFilterDisables(false);
36 ✔
75

76
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
36 ✔
77

78
  QModelIndex rootDir = model.setRootPath(passStore);
36 ✔
79
  model.fetchMore(rootDir);
36 ✔
80

81
  proxyModel.setModelAndStore(&model, passStore);
36 ✔
82
  proxyModel.setPass(QtPassSettings::getPass());
36 ✔
83

84
  ui->treeView->setModel(&proxyModel);
36 ✔
85
  ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir));
36 ✔
86
  ui->treeView->setColumnHidden(1, true);
36 ✔
87
  ui->treeView->setColumnHidden(2, true);
36 ✔
88
  ui->treeView->setColumnHidden(3, true);
36 ✔
89
  ui->treeView->setHeaderHidden(true);
36 ✔
90
  ui->treeView->setIndentation(15);
36 ✔
91
  ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
36 ✔
92
  ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
93
  ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
36 ✔
94
  ui->treeView->sortByColumn(0, Qt::AscendingOrder);
36 ✔
95
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
36 ✔
96
          &MainWindow::showContextMenu);
36 ✔
97
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
36 ✔
98
          &MainWindow::deselect);
36 ✔
99

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

117
  updateProfileBox();
36 ✔
118

119
  m_displayPanel = new PasswordDisplayPanel(
36 ✔
120
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
36 ✔
121
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
36 ✔
122
          &m_qtPass->clipboard(), &ClipboardManager::copyText);
36 ✔
123
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
36 ✔
124
          &QtPass::showTextAsQRCode);
36 ✔
125

126
  QtPassSettings::getPass()->updateEnv();
36 ✔
127
  clearPanelTimer.setSingleShot(true);
36 ✔
128
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
36 ✔
129

130
  searchTimer.setInterval(350);
36 ✔
131
  searchTimer.setSingleShot(true);
36 ✔
132

133
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
36 ✔
134

135
  // Install the search-box key filter once, not on every setUiElementsEnabled
136
  // call.
137
  ui->lineEdit->installEventFilter(this);
36 ✔
138
  ui->toolBar->installEventFilter(this);
36 ✔
139

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

152
  initToolBarButtons();
36 ✔
153
  initStatusBar();
36 ✔
154
  m_processOutput = new ProcessOutputPanel(this);
36 ✔
155
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
36 ✔
156
  // setVisible after addDockWidget so our explicit preference wins even if
157
  // QMainWindow applies any cached state when the dock is attached.
158
  // restoreWindow() runs before this (from the QtPass ctor above), so the
159
  // saved layout has already been processed.
160
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
36 ✔
161

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

183
  ui->lineEdit->setClearButtonEnabled(true);
36 ✔
184
  updateGrepButtonVisibility();
36 ✔
185

186
  setUiElementsEnabled(true);
36 ✔
187

188
  ui->lineEdit->setText(searchText);
36 ✔
189

190
  // Record whether startup configuration succeeded instead of calling
191
  // QApplication::quit() here: quit() before the event loop runs (exec() is
192
  // still ahead in main()) is a documented no-op, so a cancelled first-run
193
  // wizard used to leave the half-configured window showing anyway. main()
194
  // consults initSucceeded() and exits before show() when this is false.
195
  m_initSucceeded = m_qtPass->init();
36 ✔
196
  if (!m_initSucceeded) {
36 ✔
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() {
68 ✔
210
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
211
  // window still knows its last geometry here.
212
  saveWindowState();
36 ✔
213
  delete m_qtPass;
36 ✔
214
}
104 ✔
215

216
/**
217
 * @brief MainWindow::saveWindowState persist geometry and dock/toolbar
218
 *        layout. Idempotent, cheap, called from closeEvent() and the
219
 *        destructor.
220
 */
221
void MainWindow::saveWindowState() {
38 ✔
222
  QtPassSettings::setGeometry(saveGeometry());
38 ✔
223
  QtPassSettings::setSavestate(saveState());
38 ✔
224
}
38 ✔
225

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

264
/**
265
 * @brief MainWindow::changeEvent sets focus to the search box on activation
266
 * and re-derives palette-dependent styling on a theme change.
267
 * @param event
268
 */
269
void MainWindow::changeEvent(QEvent *event) {
54 ✔
270
  QWidget::changeEvent(event);
54 ✔
271
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
54 ✔
272
    // Desktop switched light/dark (e.g. KDE day/night). Top-level widgets
273
    // receive PaletteChange for that (ApplicationPaletteChange goes to the
274
    // QApplication object). Styling that bakes palette colours into
275
    // stylesheets must be re-derived by hand.
276
    m_displayPanel->refreshPalette();
5 ✔
277
  }
278
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
54 ✔
279
      isVisible()) {
280
    // Defer one event-loop tick so the synchronous activation dispatch
281
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
282
    // unwinds before we touch widget state — calling `focusInput()`
283
    // inline from this stack has segfaulted in past iterations because
284
    // mid-rebuild ui state isn't fully wired up yet.
285
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
9 ✔
286
                              Qt::QueuedConnection);
287
  }
288
}
54 ✔
289

290
/**
291
 * @brief First-show hook: run the initial focusInput() pulse once the
292
 *        window is actually mapped. The widget's internal data is fully
293
 *        initialised by this point, so QLineEdit::selectAll() is safe.
294
 * @param event Show event passed to the base class.
295
 */
296
void MainWindow::showEvent(QShowEvent *event) {
11 ✔
297
  QMainWindow::showEvent(event);
11 ✔
298
  if (m_firstShowCompleted) {
11 ✔
299
    return;
300
  }
301
  // Queue the focus pulse for the next event-loop tick so the platform
302
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
303
  // from the config wizard path) settle before we look up the line
304
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
305
  // *after* it actually focuses, so a transient failed lookup just
306
  // re-queues on the next show rather than silently dropping.
307
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
9 ✔
308
                            Qt::QueuedConnection);
309
}
310

311
/**
312
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
313
 */
314
void MainWindow::initToolBarButtons() {
36 ✔
315
  connect(ui->actionAddPassword, &QAction::triggered, this,
36 ✔
316
          &MainWindow::addPassword);
36 ✔
317
  connect(ui->actionAddFolder, &QAction::triggered, this,
36 ✔
318
          &MainWindow::addFolder);
36 ✔
319
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
36 ✔
320
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
36 ✔
321
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
36 ✔
322
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
36 ✔
323
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
36 ✔
324
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
36 ✔
325
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
36 ✔
326

327
  // Menu-only actions. Quit ends the application, as Ctrl+Q/Cmd+Q does in
328
  // KeePassXC, Telegram and per the KDE, GNOME and macOS guidelines, tray
329
  // icon or not (#1788). Close window is what the old bare Ctrl+Q shortcut
330
  // did: close(), which closeEvent() turns into a hide when "hide on close"
331
  // is set. The X button and Alt+F4 are close() as well.
332
  connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
36 ✔
333
  connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
36 ✔
334
  connect(ui->actionFaq, &QAction::triggered, this, [] {
36 ✔
335
    QDesktopServices::openUrl(
×
336
        QUrl(QStringLiteral("https://qtpass.org/docs/md__f_a_q.html")));
×
337
  });
×
338
  connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
36 ✔
339
  connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
36 ✔
340

341
  ui->actionAddPassword->setIcon(
36 ✔
342
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
144 ✔
343
  ui->actionAddFolder->setIcon(
36 ✔
344
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
144 ✔
345
  ui->actionEdit->setIcon(QIcon::fromTheme(
36 ✔
346
      "document-properties", QIcon(":/icons/document-properties.svg")));
108 ✔
347
  ui->actionDelete->setIcon(
36 ✔
348
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
144 ✔
349
  ui->actionPush->setIcon(
36 ✔
350
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
144 ✔
351
  ui->actionUpdate->setIcon(
36 ✔
352
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
144 ✔
353
  ui->actionUsers->setIcon(QIcon::fromTheme(
36 ✔
354
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
108 ✔
355
  ui->actionConfig->setIcon(QIcon::fromTheme(
36 ✔
356
      "applications-system", QIcon(":/icons/applications-system.svg")));
72 ✔
357
}
36 ✔
358

359
/**
360
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
361
 */
362
void MainWindow::initStatusBar() {
36 ✔
363
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
72 ✔
364

365
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
72 ✔
366
                     .scaledToHeight(statusBar()->height());
36 ✔
367
  auto *logoApp = new QLabel(statusBar());
36 ✔
368
  logoApp->setPixmap(logo);
36 ✔
369
  statusBar()->addPermanentWidget(logoApp);
36 ✔
370
}
36 ✔
371

372
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1 ✔
373
  return ui->treeView->currentIndex();
1 ✔
374
}
375

376
/**
377
 * @brief Displays the given text in the main window text browser, optionally
378
 * marking it as an error and/or rendering it as HTML.
379
 * @example
380
 * MainWindow window;
381
 * window.flashText("Operation completed.", false, false);
382
 *
383
 * @param const QString &text - The text content to display.
384
 * @param const bool isError - If true, sets the text color to red before
385
 * displaying the text; otherwise any earlier error colour is cleared.
386
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
387
 * the existing HTML content.
388
 * @return void - No return value.
389
 */
390
void MainWindow::flashText(const QString &text, const bool isError,
15 ✔
391
                           const bool isHtml) {
392
  if (isError) {
15 ✔
393
    ui->textBrowser->setTextColor(Qt::red);
3 ✔
394
  } else {
395
    // setTextColor() merges the red foreground into the browser's current
396
    // char format, and setText()/setPlainText() re-applies that format to the
397
    // whole new document. Without clearing it, a plain-text non-error message
398
    // shown after an error would still be red. Remove the property rather
399
    // than pinning a palette colour: an explicit foreground would stop the
400
    // text from following runtime light/dark palette switches (#946).
401
    QTextCharFormat format = ui->textBrowser->currentCharFormat();
12 ✔
402
    format.clearForeground();
403
    ui->textBrowser->setCurrentCharFormat(format);
12 ✔
404
  }
405

406
  if (isHtml) {
15 ✔
407
    QString _text = text;
408
    if (!ui->textBrowser->toPlainText().isEmpty()) {
20 ✔
409
      _text = ui->textBrowser->toHtml() + _text;
6 ✔
410
    }
411
    ui->textBrowser->setHtml(_text);
10 ✔
412
  } else {
413
    ui->textBrowser->setText(text);
5 ✔
414
  }
415
}
15 ✔
416

417
/**
418
 * @brief MainWindow::config pops up the configuration screen and handles all
419
 * inter-window communication
420
 */
421
void MainWindow::applyTextBrowserSettings() {
3 ✔
422
  const AppSettings s = QtPassSettings::load();
3 ✔
423
  if (s.useMonospace) {
3 ✔
424
    QFont monospace("Monospace");
×
425
    monospace.setStyleHint(QFont::Monospace);
×
426
    ui->textBrowser->setFont(monospace);
×
427
  } else {
×
428
    ui->textBrowser->setFont(QFont());
3 ✔
429
  }
430

431
  if (s.noLineWrapping) {
3 ✔
432
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
433
  } else {
434
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
3 ✔
435
  }
436
}
3 ✔
437

438
void MainWindow::applyWindowFlagsSettings() {
46 ✔
439
  const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
46 ✔
440
  const Qt::WindowFlags flags = windowFlags();
441
  if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
46 ✔
442
    // Nothing to change. setWindowFlags() on a top-level widget rebuilds the
443
    // native window (setParent(nullptr, flags)), which used to happen on every
444
    // Settings OK because the "off" branch never compared equal — the same
445
    // teardown that left ui->lineEdit dangling (see focusInput()).
446
    return;
447
  }
448
  // Toggle just the one hint; the old "off" branch reset to Qt::Window and
449
  // discarded every other flag with it.
450
  const bool wasVisible = isVisible();
451
  setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
4 ✔
452
                           : (flags & ~Qt::WindowStaysOnTopHint));
453
  // setWindowFlags() hides a shown window; a window that was not shown yet
454
  // (first-run wizard from the constructor) is left to its normal show.
455
  if (wasVisible) {
2 ✔
456
    show();
2 ✔
457
  }
458
}
459

460
/**
461
 * @brief Opens and processes the application configuration dialog, then applies
462
 * any accepted settings.
463
 * @example
464
 * if (!config()) {
465
 *   // the user cancelled
466
 * }
467
 *
468
 * @return bool - True when the dialog was accepted, false when it was
469
 * cancelled. An accepted dialog can still leave the configuration invalid
470
 * (the OK button is not gated on Util::configIsValid()); QtPass::init() keeps
471
 * asking until the configuration is usable or this returns false.
472
 */
473
auto MainWindow::config() -> bool {
4 ✔
474
  ConfigDialog d(this);
4 ✔
475
  d.setModal(true);
4 ✔
476
  // Automatically default to pass if it's available
477
  if (m_qtPass->isFreshStart() &&
4 ✔
478
      QFile(QtPassSettings::getPassExecutable()).exists()) {
12 ✔
479
    QtPassSettings::setUsePass(true);
×
480
  }
481

482
  if (m_qtPass->isFreshStart()) {
4 ✔
483
    d.wizard(); // run initial setup wizard for first-time configuration
4 ✔
484
  }
485
  if (d.exec() != QDialog::Accepted) {
4 ✔
486
    return false;
487
  }
488

489
  applyTextBrowserSettings();
3 ✔
490
  applyWindowFlagsSettings();
3 ✔
491

492
  updateProfileBox();
3 ✔
493
  const AppSettings s = QtPassSettings::load();
3 ✔
494
  proxyModel.setStore(s.passStore);
3 ✔
495
  ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
3 ✔
496
  deselect();
3 ✔
497
  ui->treeView->setCurrentIndex(QModelIndex());
3 ✔
498

499
  Pass *activePass = QtPassSettings::getPass();
3 ✔
500
  activePass->updateEnv();
3 ✔
501
  proxyModel.setPass(activePass);
3 ✔
502
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
3 ✔
503
  m_qtPass->clipboard().setAutoclearTimer();
3 ✔
504

505
  updateGitButtonVisibility();
3 ✔
506
  updateOtpButtonVisibility();
3 ✔
507
  updateGrepButtonVisibility();
3 ✔
508
  updateProcessOutputVisibility();
3 ✔
509
  if (s.useTrayIcon && m_tray == nullptr) {
3 ✔
510
    initTrayIcon();
×
511
  } else if (!s.useTrayIcon && m_tray != nullptr) {
3 ✔
512
    destroyTrayIcon();
×
513
  }
514

515
  // Leave the fresh-start state in place while the accepted configuration is
516
  // still unusable (for example the user declined to create the store), so
517
  // the next attempt from QtPass::init() runs the first-run wizard again
518
  // instead of showing the bare dialog. The re-prompt itself lives in init():
519
  // recursing here re-ran the dialog on this stack frame and never reported
520
  // a cancel back to the caller.
521
  if (Util::configIsValid(s)) {
3 ✔
522
    m_qtPass->setFreshStart(false);
1 ✔
523
  }
524
  return true;
525
}
4 ✔
526

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

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

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

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

590
/**
591
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
592
 * TreeViewItem, open the edit Window
593
 * @param index
594
 */
595
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
1 ✔
596
  QFileInfo fileOrFolder =
597
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
1 ✔
598

599
  if (fileOrFolder.isFile()) {
1 ✔
600
    editPassword(getFile(index, true));
2 ✔
601
  }
602
}
1 ✔
603

604
/**
605
 * @brief MainWindow::deselect clear the selection, password and copy buffer
606
 */
607
void MainWindow::deselect() {
4 ✔
608
  m_shownFile.clear();
4 ✔
609
  cancelOtpRequest();
4 ✔
610
  m_qtPass->clipboard().clear();
4 ✔
611
  ui->treeView->clearSelection();
4 ✔
612
  ui->actionEdit->setEnabled(false);
4 ✔
613
  ui->actionDelete->setEnabled(false);
4 ✔
614
  ui->passwordName->setText("");
4 ✔
615
  clearPanel(false);
4 ✔
616
}
4 ✔
617

618
void MainWindow::executeWrapperStarted() {
8 ✔
619
  m_displayPanel->clear();
8 ✔
620
  ui->textBrowser->clear();
8 ✔
621
  setUiElementsEnabled(false);
8 ✔
622
  clearPanelTimer.stop();
8 ✔
623
  if (QtPassSettings::isShowProcessOutput()) {
8 ✔
624
    m_processOutput->setVisible(true);
3 ✔
625
  }
626
}
8 ✔
627

628
/**
629
 * @brief Handles displaying parsed password entry content in the main window.
630
 * @example
631
 * void result = MainWindow::passShowHandler(p_output);
632
 * // Updates the UI with parsed fields and emits
633
 * passShowHandlerFinished(output)
634
 *
635
 * @param p_output - The raw output text containing the password entry data.
636
 * @return void - This function does not return a value.
637
 */
638
void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
4 ✔
639
  // Only the entry we most recently asked to show may repaint the panel; a
640
  // slower decrypt of an entry the user has since left is dropped.
641
  if (!file.isEmpty() && !m_shownFile.isEmpty() && file != m_shownFile) {
4 ✔
642
    return;
×
643
  }
644
  const AppSettings s = QtPassSettings::load();
4 ✔
645
  QStringList templ =
646
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
4 ✔
647
  bool allFields = s.useTemplate && s.templateAllFields;
4 ✔
648
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
4 ✔
649
  QString output = p_output;
650
  // Display variant: empty when the password line is itself an otpauth URI, so
651
  // the shared secret is neither rendered nor copied to the clipboard.
652
  QString password = fileContent.getPasswordForDisplay();
4 ✔
653

654
  // set clipped text
655
  //
656
  // Skipped for an OTP request: the user asked for a one-time code, not the
657
  // password, and writing both in one event-loop turn can leave the Windows
658
  // clipboard empty (two OleSetClipboard calls back to back).
659
  if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
4 ✔
660
    m_qtPass->clipboard().copyIfAlways(password, p_output);
4 ✔
661
  }
662

663
  // first clear the current view:
664
  m_displayPanel->clear();
4 ✔
665

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

676
  if (s.useAutoclearPanel) {
4 ✔
677
    clearPanelTimer.start();
×
678
  }
679

680
  emit passShowHandlerFinished(output);
4 ✔
681
  setUiElementsEnabled(true);
4 ✔
682
}
8 ✔
683

684
/**
685
 * @brief Generates a one-time password from a decrypted entry and copies it.
686
 *
687
 * Connected to Pass::finishedShow for good; reacts only to the entry onOtp()
688
 * asked for. passShowHandler is connected first, so by the time this runs the
689
 * panel has already been repainted and the UI re-enabled.
690
 *
691
 * @param p_output - The decrypted entry content.
692
 * @return void - This function does not return a value.
693
 */
694
void MainWindow::otpFromFileToClipboard(const QString &p_output,
3 ✔
695
                                        const QString &file) {
696
  // Permanently connected: only the decrypt of the entry onOtp() asked for
697
  // is ours, everything else belongs to a tree click or a Ctrl+C.
698
  if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
3 ✔
699
    return;
3 ✔
700
  }
701
  m_otpRequestFile.clear();
×
702

703
  if (p_output.isEmpty()) {
×
704
    // Distinguish "could not read the entry" from "entry has no OTP".
705
    flashText(tr("Could not decrypt this password entry"), true);
×
706
    setUiElementsEnabled(true);
×
707
    return;
×
708
  }
709

710
  // passShowHandler is connected first, so it has already repainted the panel
711
  // for this same finishedShow. When it rendered the OTP row the current code
712
  // is derived and cached, so reuse it instead of re-loading settings and
713
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
714
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
715
  const QString shown = m_displayPanel->currentOtpCode();
×
716
  if (!shown.isEmpty()) {
×
717
    m_qtPass->clipboard().copyText(shown);
×
718
    showStatusMessage(tr("OTP code copied to clipboard"));
×
719
    setUiElementsEnabled(true);
×
720
    return;
721
  }
722

723
  const AppSettings s = QtPassSettings::load();
×
724
  // Parse with the same template settings passShowHandler uses, so an OTP
725
  // field is recognised identically in both paths.
726
  const QStringList templ =
727
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
×
728
  const bool allFields = s.useTemplate && s.templateAllFields;
×
729
  const FileContent fileContent =
730
      FileContent::parse(p_output, templ, allFields);
×
731

732
  const std::optional<Totp::Settings> settings =
733
      Totp::parse(fileContent.getOtpUri());
×
734
  if (settings.has_value()) {
×
735
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
×
736
    showStatusMessage(tr("OTP code copied to clipboard"));
×
737
  } else {
738
    flashText(tr("No OTP code found in this password entry"), true);
×
739
  }
740
  setUiElementsEnabled(true);
×
741
}
×
742

743
/**
744
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
745
 */
746
void MainWindow::clearPanel(bool notify) {
4 ✔
747
  m_displayPanel->clear();
4 ✔
748
  const bool grepWasVisible = ui->grepResultsList->isVisible();
4 ✔
749
  ui->grepResultsList->clear();
4 ✔
750
  if (grepWasVisible) {
4 ✔
751
    ui->grepResultsList->setVisible(false);
×
752
    ui->treeView->setVisible(true);
×
753
    if (m_grep.inGrepMode()) {
×
754
      m_grep.clearGrepMode();
755
      ui->grepButton->blockSignals(true);
×
756
      ui->grepButton->setChecked(false);
×
757
      ui->grepButton->blockSignals(false);
×
758
      ui->lineEdit->blockSignals(true);
×
759
      ui->lineEdit->clear();
×
760
      ui->lineEdit->blockSignals(false);
×
761
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
762
    }
763
  }
764
  if (notify) {
4 ✔
765
    QString output = "***" + tr("Password and Content hidden") + "***";
×
766
    ui->textBrowser->setHtml(output);
×
767
  } else {
768
    ui->textBrowser->setHtml("");
8 ✔
769
  }
770
}
4 ✔
771

772
/**
773
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
774
 * elements
775
 * @param state
776
 */
777
void MainWindow::setUiElementsEnabled(bool state) {
64 ✔
778
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
779
  // the user's escape hatch and endReencryptPath() releases the interface, so
780
  // neither an unrelated completion nor the watchdog may re-enable it early.
781
  if (state && m_reencryptRunning) {
64 ✔
782
    m_uiWatchdog.stop();
1 ✔
783
    return;
1 ✔
784
  }
785
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
786
  if (state) {
787
    m_uiWatchdog.stop();
49 ✔
788
  } else {
789
    m_uiWatchdog.start();
14 ✔
790
  }
791
  ui->treeView->setEnabled(state);
63 ✔
792
  ui->lineEdit->setEnabled(state);
63 ✔
793
  ui->actionAddPassword->setEnabled(state);
63 ✔
794
  ui->actionAddFolder->setEnabled(state);
63 ✔
795
  ui->actionUsers->setEnabled(state);
63 ✔
796
  ui->actionConfig->setEnabled(state);
63 ✔
797
  // is a file selected?
798
  state &= ui->treeView->currentIndex().isValid();
126 ✔
799
  ui->actionDelete->setEnabled(state);
63 ✔
800
  ui->actionEdit->setEnabled(state);
63 ✔
801
  updateGitButtonVisibility();
63 ✔
802
  // `state` is now "UI enabled AND a file is selected", which is exactly when
803
  // generating an OTP makes sense.
804
  updateOtpButtonVisibility(state);
63 ✔
805
}
806

807
/**
808
 * @brief Restores the main window geometry, state, position, size, and
809
 * tray/icon settings from saved application settings.
810
 * @example
811
 * MainWindow window;
812
 * window.restoreWindow();
813
 *
814
 * @return void - This function does not return a value.
815
 */
816
void MainWindow::restoreWindow() {
43 ✔
817
  // saveGeometry() covers position, size, maximized state and screen; the
818
  // separate pos/size/maximized keys that used to be applied on top of it
819
  // are what discarded the restored position, and the unconditional
820
  // re-centre in main() then discarded the rest. Centre only when nothing
821
  // was saved.
822
  const QByteArray geometry = QtPassSettings::getGeometry();
86 ✔
823
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
43 ✔
824
    WindowStateStore::centreOnCursorScreen(*this);
4 ✔
825
  }
826
  restoreState(QtPassSettings::getSavestate(saveState()));
43 ✔
827
  const AppSettings s = QtPassSettings::load();
43 ✔
828

829
  applyWindowFlagsSettings();
43 ✔
830

831
  if (s.useTrayIcon && m_tray == nullptr) {
43 ✔
832
    initTrayIcon();
×
833
    if (s.startMinimized) {
×
834
      // since we are still in constructor, can't directly hide
835
      QTimer::singleShot(10, this, &MainWindow::hide);
×
836
    }
837
  } else if (!s.useTrayIcon && m_tray != nullptr) {
43 ✔
838
    destroyTrayIcon();
×
839
  }
840
}
86 ✔
841

842
/**
843
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
844
 */
845
void MainWindow::onConfig() { config(); }
×
846

847
/**
848
 * @brief MainWindow::showAbout version, link and licence in a standard box.
849
 */
850
void MainWindow::showAbout() {
×
851
  QMessageBox::about(
×
852
      this, tr("About QtPass"),
×
853
      tr("<h3>QtPass %1</h3>"
×
854
         "<p>A multi-platform GUI for <a "
855
         "href=\"https://www.passwordstore.org/\">"
856
         "pass</a>, the standard Unix password manager.</p>"
857
         "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
858
         "<a href=\"https://github.com/IJHack/QtPass\">Source and "
859
         "issues</a></p>"
860
         "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
861
         "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
862
         "version 3</a> or later.</p>")
863
          .arg(QStringLiteral(VERSION),
×
864
               QString::number(QDate::currentDate().year())));
×
865
}
×
866

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

884
/**
885
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
886
 * time from two keypresses is elapsed
887
 */
888
void MainWindow::onTimeoutSearch() {
×
889
  QString query = ui->lineEdit->text();
×
890

891
  if (query.isEmpty()) {
×
892
    ui->treeView->collapseAll();
×
893
    deselect();
×
894
  }
895

896
  query.replace(QStringLiteral(" "), ".*");
×
897
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
898
  if (!regExp.isValid())
×
899
    return;
900
  proxyModel.setFilterRegularExpression(regExp);
×
901
  ui->treeView->setRootIndex(
×
902
      proxyModel.rootIndexFor(QtPassSettings::getPassStore()));
×
903

904
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
905
    selectFirstFile();
×
906
  } else {
907
    ui->actionEdit->setEnabled(false);
×
908
    ui->actionDelete->setEnabled(false);
×
909
  }
910
}
×
911

912
/**
913
 * @brief MainWindow::on_lineEdit_returnPressed get searching
914
 *
915
 * Select the first possible file in the tree
916
 */
917
void MainWindow::on_lineEdit_returnPressed() {
×
918
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed" << proxyModel.rowCount();
×
919

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

940
  if (proxyModel.rowCount() > 0) {
×
941
    selectFirstFile();
×
942
    on_treeView_clicked(ui->treeView->currentIndex());
×
943
  }
944
}
945

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

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

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

1063
/**
1064
 * @brief MainWindow::selectFirstFile select the first possible file in the
1065
 * tree
1066
 */
1067
void MainWindow::selectFirstFile() {
×
1068
  QModelIndex index = proxyModel.rootIndexFor(QtPassSettings::getPassStore());
×
1069
  index = firstFile(index);
×
1070
  ui->treeView->setCurrentIndex(index);
×
1071
}
×
1072

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

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

1116
/**
1117
 * @brief MainWindow::setPassword open passworddialog
1118
 * @param file which pgp file
1119
 * @param isNew insert (not update)
1120
 */
1121
void MainWindow::setPassword(const QString &file, bool isNew) {
1 ✔
1122
  const AppSettings s = QtPassSettings::load();
1 ✔
1123
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
1 ✔
1124

1125
  if (isNew) {
1 ✔
1126
    const QString storePath = s.passStore;
1127
    QString folder = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1128
                                  proxyModel, s.passStore);
×
1129
    if (folder.isEmpty()) {
×
1130
      folder = storePath;
×
1131
    }
1132
    QHash<QString, QStringList> templates =
1133
        TemplateIO::readTemplates(storePath);
×
1134
    if (!templates.isEmpty()) {
1135
      QString defaultTemplate =
1136
          TemplateIO::getFolderTemplate(folder, storePath);
×
1137
      d.setAvailableTemplates(templates, defaultTemplate);
×
1138
    }
1139
  }
×
1140

1141
  if (!d.exec()) {
1 ✔
1142
    ui->treeView->setFocus();
×
1143
  }
1144
}
1 ✔
1145

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

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

1184
  QFileInfo fileOrFolder =
1185
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1186
  QString file = "";
×
1187
  bool isDir = false;
1188

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

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

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

1223
  QtPassSettings::getPass()->Remove(file, isDir);
×
1224
}
×
1225

1226
/**
1227
 * @brief MainWindow::cancelOtpRequest forget the pending OTP and copy requests.
1228
 *
1229
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1230
 * decrypt never emits finishedShow; if the request stayed pending, the next
1231
 * successful decrypt of that same entry (the user clicking it again) would be
1232
 * taken as the answer and copy something the user no longer asked for.
1233
 */
1234
void MainWindow::cancelOtpRequest() {
7 ✔
1235
  m_otpRequestFile.clear();
7 ✔
1236
  m_copyRequestFile.clear();
7 ✔
1237
}
7 ✔
1238

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

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

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

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

1297
/**
1298
 * @brief MainWindow::onUsers edit users for the current
1299
 * folder,
1300
 * gets lists and opens UserDialog.
1301
 */
1302
void MainWindow::onUsers() {
×
1303
  const QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1304
                                   proxyModel, QtPassSettings::getPassStore());
×
1305

1306
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1307
  if (!d.exec()) {
×
1308
    ui->treeView->setFocus();
×
1309
  }
1310
}
×
1311

1312
/**
1313
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1314
 * @param message
1315
 */
1316
void MainWindow::messageAvailable(const QString &message) {
×
1317
  show();
×
1318
  raise();
×
1319
  if (message.isEmpty()) {
×
1320
    focusInput();
×
1321
  } else {
1322
    ui->treeView->expandAll();
×
1323
    ui->lineEdit->setText(message);
×
1324
    on_lineEdit_returnPressed();
×
1325
  }
1326
}
×
1327

1328
/**
1329
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1330
 * select a more appropriate one to view too
1331
 */
1332
void MainWindow::updateProfileBox() {
39 ✔
1333
  const Profiles profiles = QtPassSettings::getProfiles();
39 ✔
1334

UNCOV
1335
  if (profiles.isEmpty()) {
×
1336
    ui->profileWidget->hide();
39 ✔
1337
  } else {
UNCOV
1338
    ui->profileWidget->show();
×
1339
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1340
    ui->profileBox->clear();
×
1341
    for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
×
1342
      if (!i.key().isEmpty()) {
×
1343
        ui->profileBox->addItem(i.key());
×
1344
      }
1345
    }
1346
    ui->profileBox->model()->sort(0);
×
1347
  }
1348
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
78 ✔
1349
  if (index != -1) { //  -1 for not found
39 ✔
UNCOV
1350
    ui->profileBox->setCurrentIndex(index);
×
1351
  }
1352
}
39 ✔
1353

1354
/**
1355
 * @brief Handles changes to the selected profile in the profile combo box.
1356
 * @details Ignores the event during a fresh start or when the selected profile
1357
 * matches the current profile. Otherwise, it clears the password field, updates
1358
 * the active profile and related settings, refreshes the environment, and
1359
 * resets the tree view and action states to reflect the newly selected profile.
1360
 *
1361
 * @param name - The newly selected profile name.
1362
 * @return void - This function does not return a value.
1363
 *
1364
 */
UNCOV
1365
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
UNCOV
1366
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
UNCOV
1367
    return;
×
1368
  }
1369

1370
  ui->lineEdit->clear();
×
1371

UNCOV
1372
  const Profile prof = QtPassSettings::getProfiles().value(name);
×
1373
  AppSettings s = QtPassSettings::load();
×
UNCOV
1374
  s.activeProfile = name;
×
UNCOV
1375
  s.passStore = prof.path;
×
1376
  s.passSigningKey = prof.signingKey;
×
1377
  // A profile without its own Git flags keeps the global values.
1378
  s.useGit = prof.useGit.value_or(s.useGit);
×
1379
  s.autoPush = prof.autoPush.value_or(s.autoPush);
×
1380
  s.autoPull = prof.autoPull.value_or(s.autoPull);
×
UNCOV
1381
  QtPassSettings::save(s);
×
UNCOV
1382
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1383

1384
  QtPassSettings::getPass()->updateEnv();
×
1385

1386
  const QString passStore = QtPassSettings::getPassStore();
×
1387
  proxyModel.setStore(passStore);
×
1388
  ui->treeView->setRootIndex(proxyModel.rootIndexFor(passStore));
×
1389
  deselect();
×
1390
  ui->treeView->setCurrentIndex(QModelIndex());
×
1391
}
×
1392

1393
/**
1394
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1395
 * support
1396
 * it
1397
 */
1398
void MainWindow::initTrayIcon() {
×
1399
  m_tray = new TrayIcon(this);
×
1400
  if (!m_tray->getIsAllocated()) {
×
UNCOV
1401
    destroyTrayIcon();
×
1402
  }
UNCOV
1403
}
×
1404

1405
/**
1406
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1407
 */
1408
void MainWindow::destroyTrayIcon() {
×
1409
  delete m_tray;
×
1410
  m_tray = nullptr;
×
UNCOV
1411
}
×
1412

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

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

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

1488
/**
1489
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1490
 * @param event
1491
 */
UNCOV
1492
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
UNCOV
1493
  switch (event->key()) {
×
UNCOV
1494
  case Qt::Key_Delete:
×
UNCOV
1495
    onDelete();
×
UNCOV
1496
    break;
×
UNCOV
1497
  case Qt::Key_Return:
×
1498
  case Qt::Key_Enter:
UNCOV
1499
    if (proxyModel.rowCount() > 0) {
×
UNCOV
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
 */
UNCOV
1516
void MainWindow::showContextMenu(const QPoint &pos) {
×
UNCOV
1517
  const AppSettings s = QtPassSettings::load();
×
1518
  QModelIndex index = ui->treeView->indexAt(pos);
×
1519
  bool selected = true;
1520
  if (!index.isValid()) {
UNCOV
1521
    ui->treeView->clearSelection();
×
UNCOV
1522
    ui->actionDelete->setEnabled(false);
×
UNCOV
1523
    ui->actionEdit->setEnabled(false);
×
1524
    selected = false;
1525
  }
1526

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

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

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

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

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

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

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

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

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

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

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

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

1613
  contextMenu->exec(globalPos);
×
1614
  delete contextMenu;
×
UNCOV
1615
}
×
1616

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

1624
  QString path = QDir::toNativeSeparators(dir);
×
UNCOV
1625
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
UNCOV
1626
}
×
1627

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

1679
/**
1680
 * @brief MainWindow::renameFolder rename an existing folder
1681
 */
1682
void MainWindow::renameFolder() {
×
1683
  bool ok;
1684
  QString srcDir =
UNCOV
1685
      QDir::cleanPath(Util::getDir(ui->treeView->currentIndex(), false, model,
×
1686
                                   proxyModel, QtPassSettings::getPassStore()));
×
UNCOV
1687
  QString srcDirName = QDir(srcDir).dirName();
×
1688
  QString newName =
UNCOV
1689
      QInputDialog::getText(this, tr("Rename folder"), tr("Rename Folder To: "),
×
UNCOV
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 ✔
UNCOV
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;
UNCOV
1721
  QString file = getFile(ui->treeView->currentIndex(), false);
×
UNCOV
1722
  QString filePath = QFileInfo(file).path();
×
UNCOV
1723
  QString fileName = QFileInfo(file).fileName();
×
UNCOV
1724
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
UNCOV
1725
    fileName.chop(4);
×
1726
  }
1727

1728
  QString newName =
UNCOV
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);
×
UNCOV
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() {
3 ✔
1750
  QFileInfo fileOrFolder =
1751
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
3 ✔
1752

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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