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

IJHack / QtPass / 27501799111

14 Jun 2026 02:25PM UTC coverage: 57.43% (+1.0%) from 56.425%
27501799111

push

github

web-flow
fix: install event filter once and add UI watchdog (#1512) (#1536)

Third slice of the MainWindow decomposition (#1512), the UiState concern.
Per the issue's "or kill it" option this fixes the two real problems in the
setUiElementsEnabled hammer rather than wrapping it in a new class:

- The search-box key event filter was re-installed on every
  setUiElementsEnabled call; install it once in the constructor instead.
- Add a 30s single-shot watchdog: setUiElementsEnabled(false) arms it and
  setUiElementsEnabled(true) disarms it, so if a backend operation disables
  the UI but never signals completion the interface re-enables itself
  instead of staying stuck.

No behavioural change in the normal path. mainwindow tests pass; doxygen
and reuse clean.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

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

379 existing lines in 3 files now uncovered.

3977 of 6925 relevant lines covered (57.43%)

36.21 hits per line

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

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

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

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

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

66
  m_qtPass = new QtPass(this);
12✔
67

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

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

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

84
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
12✔
85

86
  QModelIndex rootDir = model.setRootPath(passStore);
12✔
87
  model.fetchMore(rootDir);
12✔
88

89
  proxyModel.setModelAndStore(&model, passStore);
12✔
90
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
12✔
91

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

108
  if (QtPassSettings::isUseMonospace()) {
12✔
109
    QFont monospace("Monospace");
×
110
    monospace.setStyleHint(QFont::Monospace);
×
111
    ui->textBrowser->setFont(monospace);
×
UNCOV
112
  }
×
113
  if (QtPassSettings::isNoLineWrapping()) {
12✔
UNCOV
114
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
115
  }
116
  ui->textBrowser->setOpenExternalLinks(true);
12✔
117
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
12✔
118
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
12✔
119
          &MainWindow::showBrowserContextMenu);
12✔
120

121
  updateProfileBox();
12✔
122

123
  m_displayPanel = new PasswordDisplayPanel(
12✔
124
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
12✔
125
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested, m_qtPass,
12✔
126
          &QtPass::copyTextToClipboard);
12✔
127
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
12✔
128
          &QtPass::showTextAsQRCode);
12✔
129

130
  QtPassSettings::getPass()->updateEnv();
12✔
131
  clearPanelTimer.setInterval(MS_PER_SECOND *
12✔
132
                              QtPassSettings::getAutoclearPanelSeconds());
24✔
133
  clearPanelTimer.setSingleShot(true);
12✔
134
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
12✔
135

136
  searchTimer.setInterval(350);
12✔
137
  searchTimer.setSingleShot(true);
12✔
138

139
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
12✔
140

141
  // Install the search-box key filter once, not on every setUiElementsEnabled
142
  // call.
143
  ui->lineEdit->installEventFilter(this);
12✔
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);
12✔
148
  m_uiWatchdog.setInterval(UiWatchdogMs);
12✔
149
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
12✔
NEW
150
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
NEW
151
    setUiElementsEnabled(true);
×
NEW
152
  });
×
153

154
  initToolBarButtons();
12✔
155
  initStatusBar();
12✔
156
  initProcessOutputPanel();
12✔
157

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

179
  ui->lineEdit->setClearButtonEnabled(true);
12✔
180
  updateGrepButtonVisibility();
12✔
181

182
  setUiElementsEnabled(true);
12✔
183

184
  ui->lineEdit->setText(searchText);
12✔
185

186
  if (!m_qtPass->init()) {
12✔
187
    // no working config so this should just quit
UNCOV
188
    QApplication::quit();
×
189
    return;
190
  }
191

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

201
MainWindow::~MainWindow() { delete m_qtPass; }
36✔
202

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

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

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

280
/**
281
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
282
 */
283
void MainWindow::initToolBarButtons() {
12✔
284
  connect(ui->actionAddPassword, &QAction::triggered, this,
12✔
285
          &MainWindow::addPassword);
12✔
286
  connect(ui->actionAddFolder, &QAction::triggered, this,
12✔
287
          &MainWindow::addFolder);
12✔
288
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
12✔
289
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
12✔
290
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
12✔
291
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
12✔
292
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
12✔
293
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
12✔
294
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
12✔
295

296
  ui->actionAddPassword->setIcon(
12✔
297
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
48✔
298
  ui->actionAddFolder->setIcon(
12✔
299
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
48✔
300
  ui->actionEdit->setIcon(QIcon::fromTheme(
12✔
301
      "document-properties", QIcon(":/icons/document-properties.svg")));
36✔
302
  ui->actionDelete->setIcon(
12✔
303
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
48✔
304
  ui->actionPush->setIcon(
12✔
305
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
48✔
306
  ui->actionUpdate->setIcon(
12✔
307
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
48✔
308
  ui->actionUsers->setIcon(QIcon::fromTheme(
12✔
309
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
36✔
310
  ui->actionConfig->setIcon(QIcon::fromTheme(
12✔
311
      "applications-system", QIcon(":/icons/applications-system.svg")));
24✔
312
}
12✔
313

314
/**
315
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
316
 */
317
void MainWindow::initStatusBar() {
12✔
318
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
24✔
319

320
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
24✔
321
                     .scaledToHeight(statusBar()->height());
12✔
322
  auto *logoApp = new QLabel(statusBar());
12✔
323
  logoApp->setPixmap(logo);
12✔
324
  statusBar()->addPermanentWidget(logoApp);
12✔
325
}
12✔
326

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

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

372
  connect(m_clearOutputButton, &QToolButton::clicked, this,
12✔
373
          &MainWindow::on_clearOutputButton_clicked);
12✔
374

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

UNCOV
394
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
UNCOV
395
  return ui->treeView->currentIndex();
×
396
}
397

398
void MainWindow::cleanKeygenDialog() {
1✔
399
  if (m_keygenDialog != nullptr) {
1✔
UNCOV
400
    m_keygenDialog->close();
×
401
  }
402
  m_keygenDialog = nullptr;
1✔
403
}
1✔
404

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

425
  if (isHtml) {
3✔
426
    QString _text = text;
427
    if (!ui->textBrowser->toPlainText().isEmpty()) {
2✔
428
      _text = ui->textBrowser->toHtml() + _text;
2✔
429
    }
430
    ui->textBrowser->setHtml(_text);
1✔
431
  } else {
432
    ui->textBrowser->setText(text);
2✔
433
  }
434
}
3✔
435

436
/**
437
 * @brief MainWindow::config pops up the configuration screen and handles all
438
 * inter-window communication
439
 */
UNCOV
440
void MainWindow::applyTextBrowserSettings() {
×
441
  if (QtPassSettings::isUseMonospace()) {
×
442
    QFont monospace("Monospace");
×
UNCOV
443
    monospace.setStyleHint(QFont::Monospace);
×
444
    ui->textBrowser->setFont(monospace);
×
UNCOV
445
  } else {
×
446
    ui->textBrowser->setFont(QFont());
×
447
  }
448

449
  if (QtPassSettings::isNoLineWrapping()) {
×
UNCOV
450
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
451
  } else {
UNCOV
452
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
453
  }
UNCOV
454
}
×
455

456
void MainWindow::applyWindowFlagsSettings() {
×
UNCOV
457
  if (QtPassSettings::isAlwaysOnTop()) {
×
458
    Qt::WindowFlags flags = windowFlags();
UNCOV
459
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
460
  } else {
UNCOV
461
    this->setWindowFlags(Qt::Window);
×
462
  }
UNCOV
463
  this->show();
×
UNCOV
464
}
×
465

466
/**
467
 * @brief Opens and processes the application configuration dialog, then applies
468
 * any accepted settings.
469
 * @example
470
 * config();
471
 *
472
 * @return void - This function does not return a value.
473
 */
UNCOV
474
void MainWindow::config() {
×
475
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
476
  d->setModal(true);
×
477
  // Automatically default to pass if it's available
478
  if (m_qtPass->isFreshStart() &&
×
479
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
480
    QtPassSettings::setUsePass(true);
×
481
  }
482

483
  if (m_qtPass->isFreshStart()) {
×
484
    d->wizard(); // run initial setup wizard for first-time configuration
×
485
  }
486
  if (d->exec()) {
×
487
    if (d->result() == QDialog::Accepted) {
×
488
      applyTextBrowserSettings();
×
489
      applyWindowFlagsSettings();
×
490

491
      updateProfileBox();
×
492
      const QString passStore = QtPassSettings::getPassStore();
×
UNCOV
493
      proxyModel.setStore(passStore);
×
494
      ui->treeView->setRootIndex(
×
495
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
496
      deselect();
×
497
      ui->treeView->setCurrentIndex(QModelIndex());
×
498

499
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
500
        config();
×
501
      }
502
      QtPassSettings::getPass()->updateEnv();
×
503
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
504
                                  QtPassSettings::getAutoclearPanelSeconds());
×
505
      m_qtPass->setClipboardTimer();
×
506

UNCOV
507
      updateGitButtonVisibility();
×
UNCOV
508
      updateOtpButtonVisibility();
×
UNCOV
509
      updateGrepButtonVisibility();
×
510
      updateProcessOutputVisibility();
×
UNCOV
511
      if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
×
512
        initTrayIcon();
×
UNCOV
513
      } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
×
UNCOV
514
        destroyTrayIcon();
×
515
      }
516
    }
517

518
    m_qtPass->setFreshStart(false);
×
519
  }
520
}
×
521

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

534
/**
535
 * @brief MainWindow::onPush do a git push
536
 */
UNCOV
537
void MainWindow::onPush() {
×
UNCOV
538
  if (QtPassSettings::isUseGit()) {
×
UNCOV
539
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
UNCOV
540
    QtPassSettings::getPass()->GitPush();
×
541
  }
UNCOV
542
}
×
543

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

564
/**
565
 * @brief MainWindow::on_treeView_clicked read the selected password file
566
 * @param index
567
 */
568
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
569
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
570
  m_currentDir =
571
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
572
  // Clear any previously cached clipped text before showing new password
573
  m_qtPass->clearClippedText();
×
UNCOV
574
  QString file = getFile(index, true);
×
575
  ui->passwordName->setText(file);
×
UNCOV
576
  if (!file.isEmpty() && !cleared) {
×
UNCOV
577
    QtPassSettings::getPass()->Show(file);
×
578
  } else {
UNCOV
579
    clearPanel(false);
×
UNCOV
580
    ui->actionEdit->setEnabled(false);
×
UNCOV
581
    ui->actionDelete->setEnabled(true);
×
582
  }
UNCOV
583
}
×
584

585
/**
586
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
587
 * TreeViewItem, open the edit Window
588
 * @param index
589
 */
UNCOV
590
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
591
  QFileInfo fileOrFolder =
UNCOV
592
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
593

UNCOV
594
  if (fileOrFolder.isFile()) {
×
UNCOV
595
    editPassword(getFile(index, true));
×
596
  }
UNCOV
597
}
×
598

599
/**
600
 * @brief MainWindow::deselect clear the selection, password and copy buffer
601
 */
602
void MainWindow::deselect() {
1✔
603
  m_currentDir = "";
1✔
604
  m_qtPass->clearClipboard();
1✔
605
  ui->treeView->clearSelection();
1✔
606
  ui->actionEdit->setEnabled(false);
1✔
607
  ui->actionDelete->setEnabled(false);
1✔
608
  ui->passwordName->setText("");
1✔
609
  clearPanel(false);
1✔
610
}
1✔
611

612
void MainWindow::executeWrapperStarted() {
×
UNCOV
613
  m_displayPanel->clear();
×
UNCOV
614
  ui->textBrowser->clear();
×
UNCOV
615
  setUiElementsEnabled(false);
×
UNCOV
616
  clearPanelTimer.stop();
×
UNCOV
617
  if (QtPassSettings::isShowProcessOutput()) {
×
UNCOV
618
    m_processOutputDock->setVisible(true);
×
619
  }
UNCOV
620
}
×
621

622
/**
623
 * @brief Handles displaying parsed password entry content in the main window.
624
 * @example
625
 * void result = MainWindow::passShowHandler(p_output);
626
 * // Updates the UI with parsed fields and emits
627
 * passShowHandlerFinished(output)
628
 *
629
 * @param p_output - The raw output text containing the password entry data.
630
 * @return void - This function does not return a value.
631
 */
632
void MainWindow::passShowHandler(const QString &p_output) {
×
UNCOV
633
  QStringList templ = QtPassSettings::isUseTemplate()
×
UNCOV
634
                          ? QtPassSettings::getPassTemplate().split("\n")
×
635
                          : QStringList();
×
636
  bool allFields =
UNCOV
637
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
638
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
639
  QString output = p_output;
UNCOV
640
  QString password = fileContent.getPassword();
×
641

642
  // set clipped text
643
  m_qtPass->setClippedText(password, p_output);
×
644

645
  // first clear the current view:
646
  m_displayPanel->clear();
×
647

648
  // show what is needed:
649
  if (QtPassSettings::isHideContent()) {
×
650
    output = "***" + tr("Content hidden") + "***";
×
UNCOV
651
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
652
    m_displayPanel->displayFields(password, fileContent.getNamedValues());
×
UNCOV
653
    output = fileContent.getRemainingDataForDisplay();
×
654
  }
655

UNCOV
656
  if (QtPassSettings::isUseAutoclearPanel()) {
×
657
    clearPanelTimer.start();
×
658
  }
659

660
  emit passShowHandlerFinished(output);
×
UNCOV
661
  setUiElementsEnabled(true);
×
UNCOV
662
}
×
663

664
/**
665
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
666
 * and updating the UI state.
667
 * @example
668
 * void MainWindow::passOtpHandler(const QString &p_output);
669
 *
670
 * @param const QString &p_output - The OTP code text to process; if empty, an
671
 * error message is shown instead.
672
 * @return void - This function does not return a value.
673
 */
UNCOV
674
void MainWindow::passOtpHandler(const QString &p_output) {
×
UNCOV
675
  if (!p_output.isEmpty()) {
×
UNCOV
676
    m_displayPanel->appendField(tr("OTP Code"), p_output);
×
UNCOV
677
    m_qtPass->copyTextToClipboard(p_output);
×
UNCOV
678
    showStatusMessage(tr("OTP code copied to clipboard"));
×
679
  } else {
UNCOV
680
    flashText(tr("No OTP code found in this password entry"), true);
×
681
  }
682
  if (QtPassSettings::isUseAutoclearPanel()) {
×
683
    clearPanelTimer.start();
×
684
  }
685
  setUiElementsEnabled(true);
×
UNCOV
686
}
×
687

688
/**
689
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
690
 */
691
void MainWindow::clearPanel(bool notify) {
1✔
692
  m_displayPanel->clear();
1✔
693
  const bool grepWasVisible = ui->grepResultsList->isVisible();
1✔
694
  ui->grepResultsList->clear();
1✔
695
  if (grepWasVisible) {
1✔
UNCOV
696
    ui->grepResultsList->setVisible(false);
×
UNCOV
697
    ui->treeView->setVisible(true);
×
UNCOV
698
    if (m_grep.inGrepMode()) {
×
699
      m_grep.clearGrepMode();
700
      ui->grepButton->blockSignals(true);
×
701
      ui->grepButton->setChecked(false);
×
702
      ui->grepButton->blockSignals(false);
×
UNCOV
703
      ui->lineEdit->blockSignals(true);
×
UNCOV
704
      ui->lineEdit->clear();
×
UNCOV
705
      ui->lineEdit->blockSignals(false);
×
UNCOV
706
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
707
    }
708
  }
709
  if (notify) {
1✔
710
    QString output = "***" + tr("Password and Content hidden") + "***";
×
711
    ui->textBrowser->setHtml(output);
×
712
  } else {
713
    ui->textBrowser->setHtml("");
2✔
714
  }
715
}
1✔
716

717
/**
718
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
719
 * elements
720
 * @param state
721
 */
722
void MainWindow::setUiElementsEnabled(bool state) {
15✔
723
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
724
  if (state) {
15✔
725
    m_uiWatchdog.stop();
13✔
726
  } else {
727
    m_uiWatchdog.start();
2✔
728
  }
729
  ui->treeView->setEnabled(state);
15✔
730
  ui->lineEdit->setEnabled(state);
15✔
731
  ui->actionAddPassword->setEnabled(state);
15✔
732
  ui->actionAddFolder->setEnabled(state);
15✔
733
  ui->actionUsers->setEnabled(state);
15✔
734
  ui->actionConfig->setEnabled(state);
15✔
735
  // is a file selected?
736
  state &= ui->treeView->currentIndex().isValid();
30✔
737
  ui->actionDelete->setEnabled(state);
15✔
738
  ui->actionEdit->setEnabled(state);
15✔
739
  updateGitButtonVisibility();
15✔
740
  updateOtpButtonVisibility();
15✔
741
}
15✔
742

743
/**
744
 * @brief Restores the main window geometry, state, position, size, and
745
 * tray/icon settings from saved application settings.
746
 * @example
747
 * MainWindow window;
748
 * window.restoreWindow();
749
 *
750
 * @return void - This function does not return a value.
751
 */
752
void MainWindow::restoreWindow() {
12✔
753
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
12✔
754
  restoreGeometry(geometry);
12✔
755
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
12✔
756
  restoreState(savestate);
12✔
757
  QPoint position = QtPassSettings::getPos(pos());
12✔
758
  move(position);
12✔
759
  QSize newSize = QtPassSettings::getSize(size());
12✔
760
  resize(newSize);
12✔
761
  if (QtPassSettings::isMaximized(isMaximized())) {
12✔
UNCOV
762
    showMaximized();
×
763
  }
764

765
  if (QtPassSettings::isAlwaysOnTop()) {
12✔
766
    Qt::WindowFlags flags = windowFlags();
UNCOV
767
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
UNCOV
768
    show();
×
769
  }
770

771
  if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
24✔
UNCOV
772
    initTrayIcon();
×
773
    if (QtPassSettings::isStartMinimized()) {
×
774
      // since we are still in constructor, can't directly hide
UNCOV
775
      QTimer::singleShot(10, this, SLOT(hide()));
×
776
    }
777
  } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
24✔
778
    destroyTrayIcon();
×
779
  }
780
}
12✔
781

782
/**
783
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
784
 */
UNCOV
785
void MainWindow::onConfig() { config(); }
×
786

787
/**
788
 * @brief Executes when the string in the search box changes, collapses the
789
 * TreeView
790
 * @param arg1
791
 */
UNCOV
792
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
UNCOV
793
  if (m_grep.inGrepMode())
×
794
    return;
UNCOV
795
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
796
  ui->treeView->expandAll();
×
UNCOV
797
  clearPanel(false);
×
UNCOV
798
  ui->passwordName->setText("");
×
UNCOV
799
  ui->actionEdit->setEnabled(false);
×
UNCOV
800
  ui->actionDelete->setEnabled(false);
×
UNCOV
801
  searchTimer.start();
×
802
}
803

804
/**
805
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
806
 * time from two keypresses is elapsed
807
 */
808
void MainWindow::onTimeoutSearch() {
×
809
  QString query = ui->lineEdit->text();
×
810

811
  if (query.isEmpty()) {
×
812
    ui->treeView->collapseAll();
×
UNCOV
813
    deselect();
×
814
  }
815

UNCOV
816
  query.replace(QStringLiteral(" "), ".*");
×
UNCOV
817
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
UNCOV
818
  proxyModel.setFilterRegularExpression(regExp);
×
819
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
820
      model.setRootPath(QtPassSettings::getPassStore())));
×
821

822
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
823
    selectFirstFile();
×
824
  } else {
UNCOV
825
    ui->actionEdit->setEnabled(false);
×
UNCOV
826
    ui->actionDelete->setEnabled(false);
×
827
  }
828
}
×
829

830
/**
831
 * @brief MainWindow::on_lineEdit_returnPressed get searching
832
 *
833
 * Select the first possible file in the tree
834
 */
UNCOV
835
void MainWindow::on_lineEdit_returnPressed() {
×
836
#ifdef QT_DEBUG
837
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
838
#endif
839

UNCOV
840
  if (m_grep.inGrepMode()) {
×
UNCOV
841
    const QString query = ui->lineEdit->text();
×
UNCOV
842
    if (!query.isEmpty()) {
×
UNCOV
843
      ui->grepResultsList->clear();
×
UNCOV
844
      ui->statusBar->showMessage(tr("Searching…"));
×
845
      if (m_grep.beginSearch()) {
846
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
847
      }
UNCOV
848
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
849
    } else {
850
      if (m_grep.cancelSearch()) {
851
        QApplication::restoreOverrideCursor();
×
852
      }
853
      ui->grepResultsList->clear();
×
854
      ui->grepResultsList->setVisible(false);
×
855
      ui->treeView->setVisible(true);
×
856
    }
857
    return;
858
  }
859

UNCOV
860
  if (proxyModel.rowCount() > 0) {
×
861
    selectFirstFile();
×
UNCOV
862
    on_treeView_clicked(ui->treeView->currentIndex());
×
863
  }
864
}
865

866
/**
867
 * @brief Toggle grep (content search) mode.
868
 */
869
void MainWindow::on_grepButton_toggled(bool checked) {
×
870
  if (checked) {
×
871
    m_grep.enterGrepMode();
UNCOV
872
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
873
    // The regex dialect depends on the backend (see Pass::Grep): the pass
874
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
875
    ui->lineEdit->setToolTip(
×
876
        QtPassSettings::isUsePass()
×
877
            ? tr("Content search uses POSIX basic regular expressions "
×
878
                 "(pass grep).")
879
            : tr("Content search uses Perl-compatible regular expressions "
880
                 "(PCRE)."));
UNCOV
881
    ui->lineEdit->clear();
×
UNCOV
882
    searchTimer.stop();
×
UNCOV
883
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
884
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
885
        model.setRootPath(QtPassSettings::getPassStore())));
×
886
    ui->grepResultsList->setVisible(false);
×
887
    // Keep treeView visible until results arrive
888
  } else {
889
    if (m_grep.leaveGrepMode()) {
890
      QApplication::restoreOverrideCursor();
×
891
    }
892
    searchTimer.stop();
×
UNCOV
893
    ui->lineEdit->blockSignals(true);
×
UNCOV
894
    ui->lineEdit->clear();
×
UNCOV
895
    ui->lineEdit->blockSignals(false);
×
896
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
897
    ui->lineEdit->setToolTip(QString());
×
898
    ui->grepResultsList->clear();
×
899
    ui->grepResultsList->setVisible(false);
×
900
    ui->treeView->setVisible(true);
×
901
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
UNCOV
902
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
UNCOV
903
        model.setRootPath(QtPassSettings::getPassStore())));
×
904
  }
905
}
×
906

907
/**
908
 * @brief Display grep results in grepResultsList.
909
 */
910
void MainWindow::onGrepFinished(
×
911
    const QList<QPair<QString, QStringList>> &results) {
912
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
913
  if (outcome.restoreCursor) {
×
914
    QApplication::restoreOverrideCursor();
×
915
  }
916
  // Re-enable the UI before the discard check so a cancelled search can never
917
  // leave controls disabled.
918
  setUiElementsEnabled(true);
×
919
  if (outcome.discard) {
×
920
    return;
×
921
  }
922
  if (!m_grep.inGrepMode())
×
923
    return;
UNCOV
924
  ui->grepResultsList->clear();
×
UNCOV
925
  if (results.isEmpty()) {
×
UNCOV
926
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
927
    ui->grepResultsList->setVisible(false);
×
UNCOV
928
    ui->treeView->setVisible(true);
×
929
    return;
×
930
  }
931
  const bool hideContent = QtPassSettings::isHideContent();
×
932
  int totalLines = 0;
933
  for (const auto &pair : results) {
×
934
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
935
    entryItem->setText(0, pair.first);
×
UNCOV
936
    entryItem->setData(0, Qt::UserRole, pair.first);
×
937
    for (const QString &line : pair.second) {
×
938
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
UNCOV
939
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
940
                                       : line);
941
      lineItem->setData(0, Qt::UserRole, pair.first);
×
942
      ++totalLines;
×
943
    }
944
  }
945
  ui->grepResultsList->expandAll();
×
UNCOV
946
  ui->treeView->setVisible(false);
×
947
  ui->grepResultsList->setVisible(true);
×
UNCOV
948
  ui->statusBar->showMessage(
×
949
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
950
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
951
      3000);
952
  if (QtPassSettings::isUseAutoclearPanel())
×
953
    clearPanelTimer.start();
×
954
}
955

956
/**
957
 * @brief Navigate to the password entry when a grep result is clicked.
958
 */
UNCOV
959
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
960
                                                int /*column*/) {
961
  const QString entry = item->data(0, Qt::UserRole).toString();
×
962
  if (entry.isEmpty())
×
963
    return;
964
  const QString fullPath = QDir::cleanPath(
965
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
966
  QModelIndex srcIndex = model.index(fullPath);
×
967
  if (!srcIndex.isValid())
968
    return;
969
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
970
  if (!proxyIndex.isValid())
971
    return;
UNCOV
972
  ui->treeView->setCurrentIndex(proxyIndex);
×
UNCOV
973
  on_treeView_clicked(proxyIndex);
×
UNCOV
974
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
975
    ui->grepResultsList->clear();
×
UNCOV
976
  ui->grepResultsList->setVisible(false);
×
977
  ui->treeView->setVisible(true);
×
978
  ui->treeView->scrollTo(proxyIndex);
×
UNCOV
979
  ui->treeView->setFocus();
×
980
}
981

982
/**
983
 * @brief MainWindow::selectFirstFile select the first possible file in the
984
 * tree
985
 */
UNCOV
986
void MainWindow::selectFirstFile() {
×
UNCOV
987
  QModelIndex index = proxyModel.mapFromSource(
×
988
      model.setRootPath(QtPassSettings::getPassStore()));
×
989
  index = firstFile(index);
×
990
  ui->treeView->setCurrentIndex(index);
×
991
}
×
992

993
/**
994
 * @brief MainWindow::firstFile return location of first possible file
995
 * @param parentIndex
996
 * @return QModelIndex
997
 */
UNCOV
998
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
UNCOV
999
  QModelIndex index = parentIndex;
×
UNCOV
1000
  int numRows = proxyModel.rowCount(parentIndex);
×
UNCOV
1001
  for (int row = 0; row < numRows; ++row) {
×
1002
    index = proxyModel.index(row, 0, parentIndex);
×
1003
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
1004
      return index;
×
1005
    }
1006
    if (proxyModel.hasChildren(index)) {
×
1007
      return firstFile(index);
×
1008
    }
1009
  }
UNCOV
1010
  return index;
×
1011
}
1012

1013
/**
1014
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1015
 * the password store and warn the user.
1016
 *
1017
 * Used before file/folder creation, move, and rename to stop user-typed
1018
 * names like "../../etc/passwd" or absolute paths from escaping the
1019
 * configured store root via the input dialogs.
1020
 *
1021
 * @param candidate Absolute candidate path to validate.
1022
 * @return true if the path is inside the password store; false otherwise (a
1023
 * warning dialog is shown in that case).
1024
 */
UNCOV
1025
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1026
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1027
    return true;
1028
  }
UNCOV
1029
  QMessageBox::warning(this, tr("Invalid name"),
×
UNCOV
1030
                       tr("That name would resolve outside the password "
×
1031
                          "store. Please choose a different name."));
UNCOV
1032
  return false;
×
1033
}
1034

1035
/**
1036
 * @brief MainWindow::setPassword open passworddialog
1037
 * @param file which pgp file
1038
 * @param isNew insert (not update)
1039
 */
UNCOV
1040
void MainWindow::setPassword(const QString &file, bool isNew) {
×
1041
  PasswordDialog d(file, isNew, this);
×
1042

UNCOV
1043
  if (isNew) {
×
UNCOV
1044
    QString storePath = QtPassSettings::getPassStore();
×
1045
    QString folder =
1046
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
UNCOV
1047
    if (folder.isEmpty()) {
×
1048
      folder = storePath;
×
1049
    }
1050
    QHash<QString, QStringList> templates =
UNCOV
1051
        TemplateIO::readTemplates(storePath);
×
1052
    if (!templates.isEmpty()) {
1053
      QString defaultTemplate =
UNCOV
1054
          TemplateIO::getFolderTemplate(folder, storePath);
×
UNCOV
1055
      d.setAvailableTemplates(templates, defaultTemplate);
×
1056
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
1057
                    [&d]() { d.cycleTemplate(); });
×
1058
    }
1059
  }
×
1060

UNCOV
1061
  if (!d.exec()) {
×
1062
    ui->treeView->setFocus();
×
1063
  }
1064
}
×
1065

1066
/**
1067
 * @brief MainWindow::addPassword add a new password by showing a
1068
 * number of dialogs.
1069
 */
1070
void MainWindow::addPassword() {
×
1071
  bool ok;
1072
  QString dir =
1073
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1074
  QString file =
1075
      QInputDialog::getText(this, tr("New file"),
×
UNCOV
1076
                            tr("New password file: \n(Will be placed in %1 )")
×
1077
                                .arg(QtPassSettings::getPassStore() +
×
1078
                                     Util::getDir(ui->treeView->currentIndex(),
×
1079
                                                  true, model, proxyModel)),
1080
                            QLineEdit::Normal, "", &ok);
×
UNCOV
1081
  if (!ok || file.isEmpty()) {
×
1082
    return;
1083
  }
UNCOV
1084
  file = dir + file;
×
UNCOV
1085
  if (!confirmPathInStore(QtPassSettings::getPassStore() + file)) {
×
1086
    return;
1087
  }
UNCOV
1088
  setPassword(file);
×
1089
}
1090

1091
/**
1092
 * @brief MainWindow::onDelete remove password, if you are
1093
 * sure.
1094
 */
UNCOV
1095
void MainWindow::onDelete() {
×
1096
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1097
  if (!currentIndex.isValid()) {
1098
    // This fixes https://github.com/IJHack/QtPass/issues/556
1099
    // Otherwise the entire password directory would be deleted if
1100
    // nothing is selected in the tree view.
1101
    return;
×
1102
  }
1103

1104
  QFileInfo fileOrFolder =
UNCOV
1105
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
UNCOV
1106
  QString file = "";
×
1107
  bool isDir = false;
1108

UNCOV
1109
  if (fileOrFolder.isFile()) {
×
UNCOV
1110
    file = getFile(ui->treeView->currentIndex(), true);
×
1111
  } else {
1112
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1113
    isDir = true;
1114
  }
1115

1116
  QString dirMessage = tr(" and the whole content?");
1117
  if (isDir) {
×
UNCOV
1118
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
UNCOV
1119
                    QDirIterator::Subdirectories);
×
1120
    bool okDir = true;
1121
    while (it.hasNext() && okDir) {
×
1122
      it.next();
×
UNCOV
1123
      if (QFileInfo(it.filePath()).isFile()) {
×
UNCOV
1124
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1125
          okDir = false;
1126
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1127
                          "there are unexpected files in the given folder, "
1128
                          "check them before continue.</strong>");
1129
        }
1130
      }
1131
    }
UNCOV
1132
  }
×
1133

1134
  if (QMessageBox::question(
×
1135
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
UNCOV
1136
          tr("Are you sure you want to delete %1%2?")
×
1137
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1138
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1139
    return;
1140
  }
1141

1142
  QtPassSettings::getPass()->Remove(file, isDir);
×
UNCOV
1143
}
×
1144

1145
/**
1146
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
1147
 */
1148
void MainWindow::onOtp() {
×
UNCOV
1149
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1150
  if (!file.isEmpty()) {
×
1151
    if (QtPassSettings::isUseOtp()) {
×
1152
      setUiElementsEnabled(false);
×
1153
      QtPassSettings::getPass()->OtpGenerate(file);
×
1154
    }
1155
  } else {
UNCOV
1156
    flashText(tr("No password selected for OTP generation"), true);
×
1157
  }
1158
}
×
1159

1160
/**
1161
 * @brief MainWindow::onEdit try and edit (selected) password.
1162
 */
UNCOV
1163
void MainWindow::onEdit() {
×
1164
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1165
  editPassword(file);
×
1166
}
×
1167

1168
/**
1169
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1170
 * @param dir folder to edit users for.
1171
 */
1172
void MainWindow::userDialog(const QString &dir) {
×
UNCOV
1173
  if (!dir.isEmpty()) {
×
1174
    m_currentDir = dir;
×
1175
  }
UNCOV
1176
  onUsers();
×
UNCOV
1177
}
×
1178

1179
/**
1180
 * @brief MainWindow::onUsers edit users for the current
1181
 * folder,
1182
 * gets lists and opens UserDialog.
1183
 */
UNCOV
1184
void MainWindow::onUsers() {
×
1185
  QString dir =
1186
      m_currentDir.isEmpty()
UNCOV
1187
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
1188
          : m_currentDir;
×
1189

1190
  UsersDialog d(dir, this);
×
UNCOV
1191
  if (!d.exec()) {
×
1192
    ui->treeView->setFocus();
×
1193
  }
UNCOV
1194
}
×
1195

1196
/**
1197
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1198
 * @param message
1199
 */
1200
void MainWindow::messageAvailable(const QString &message) {
×
UNCOV
1201
  show();
×
UNCOV
1202
  raise();
×
1203
  if (message.isEmpty()) {
×
1204
    focusInput();
×
1205
  } else {
1206
    ui->treeView->expandAll();
×
1207
    ui->lineEdit->setText(message);
×
1208
    on_lineEdit_returnPressed();
×
1209
  }
1210
}
×
1211

1212
/**
1213
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1214
 * @param batch
1215
 * @param keygenWindow
1216
 */
1217
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1218
  m_keygenDialog = keygenWindow;
×
1219
  emit generateGPGKeyPair(batch);
×
1220
}
×
1221

1222
/**
1223
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1224
 * select a more appropriate one to view too
1225
 */
1226
void MainWindow::updateProfileBox() {
12✔
1227
  QHash<QString, QHash<QString, QString>> profiles =
1228
      QtPassSettings::getProfiles();
12✔
1229

1230
  if (profiles.isEmpty()) {
UNCOV
1231
    ui->profileWidget->hide();
×
1232
  } else {
1233
    ui->profileWidget->show();
12✔
1234
    ui->profileBox->setEnabled(profiles.size() > 1);
24✔
1235
    ui->profileBox->clear();
12✔
1236
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
12✔
1237
    while (i.hasNext()) {
12✔
1238
      i.next();
1239
      if (!i.key().isEmpty()) {
12✔
1240
        ui->profileBox->addItem(i.key());
12✔
1241
      }
1242
    }
1243
    ui->profileBox->model()->sort(0);
12✔
1244
  }
1245
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
24✔
1246
  if (index != -1) { //  -1 for not found
12✔
1247
    ui->profileBox->setCurrentIndex(index);
×
1248
  }
1249
}
12✔
1250

1251
/**
1252
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1253
 * correct "profile"
1254
 * @param name
1255
 */
1256
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1257
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1258
#else
1259
/**
1260
 * @brief Handles changes to the selected profile in the profile combo box.
1261
 * @details Ignores the event during a fresh start or when the selected profile
1262
 * matches the current profile. Otherwise, it clears the password field, updates
1263
 * the active profile and related settings, refreshes the environment, and
1264
 * resets the tree view and action states to reflect the newly selected profile.
1265
 *
1266
 * @param name - The newly selected profile name.
1267
 * @return void - This function does not return a value.
1268
 *
1269
 */
1270
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
12✔
1271
#endif
1272
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
12✔
1273
    return;
12✔
1274
  }
1275

UNCOV
1276
  ui->lineEdit->clear();
×
1277

UNCOV
1278
  QtPassSettings::setProfile(name);
×
1279

UNCOV
1280
  QtPassSettings::setPassStore(
×
UNCOV
1281
      QtPassSettings::getProfiles().value(name).value("path"));
×
UNCOV
1282
  QtPassSettings::setPassSigningKey(
×
UNCOV
1283
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
UNCOV
1284
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1285

UNCOV
1286
  QtPassSettings::getPass()->updateEnv();
×
1287

UNCOV
1288
  const QString passStore = QtPassSettings::getPassStore();
×
UNCOV
1289
  proxyModel.setStore(passStore);
×
UNCOV
1290
  ui->treeView->setRootIndex(
×
UNCOV
1291
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1292
  deselect();
×
UNCOV
1293
  ui->treeView->setCurrentIndex(QModelIndex());
×
1294
}
1295

1296
/**
1297
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1298
 * support
1299
 * it
1300
 */
UNCOV
1301
void MainWindow::initTrayIcon() {
×
1302
  m_tray = new TrayIcon(this);
×
1303
  // Setup tray icon
1304

1305
  if (m_tray == nullptr) {
1306
#ifdef QT_DEBUG
1307
    dbg() << "Allocating tray icon failed.";
1308
#endif
1309
    return;
1310
  }
1311

UNCOV
1312
  if (!m_tray->getIsAllocated()) {
×
UNCOV
1313
    destroyTrayIcon();
×
1314
  }
1315
}
1316

1317
/**
1318
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1319
 */
UNCOV
1320
void MainWindow::destroyTrayIcon() {
×
UNCOV
1321
  delete m_tray;
×
UNCOV
1322
  m_tray = nullptr;
×
UNCOV
1323
}
×
1324

1325
/**
1326
 * @brief MainWindow::closeEvent hide or quit
1327
 * @param event
1328
 */
1329
void MainWindow::closeEvent(QCloseEvent *event) {
×
UNCOV
1330
  if (QtPassSettings::isHideOnClose()) {
×
UNCOV
1331
    this->hide();
×
1332
    event->ignore();
1333
  } else {
UNCOV
1334
    m_qtPass->clearClipboard();
×
1335

1336
    QtPassSettings::setGeometry(saveGeometry());
×
1337
    QtPassSettings::setSavestate(saveState());
×
1338
    QtPassSettings::setMaximized(isMaximized());
×
1339
    if (!isMaximized()) {
×
UNCOV
1340
      QtPassSettings::setPos(pos());
×
UNCOV
1341
      QtPassSettings::setSize(size());
×
1342
    }
1343
    event->accept();
1344
  }
1345
}
×
1346

1347
/**
1348
 * @brief MainWindow::eventFilter filter out some events and focus the
1349
 * treeview
1350
 * @param obj
1351
 * @param event
1352
 * @return
1353
 */
1354
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
39✔
1355
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
39✔
1356
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1357
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
UNCOV
1358
      ui->treeView->setFocus();
×
1359
    }
1360
  }
1361
  return QObject::eventFilter(obj, event);
39✔
1362
}
1363

1364
/**
1365
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1366
 * @param event
1367
 */
UNCOV
1368
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
UNCOV
1369
  switch (event->key()) {
×
UNCOV
1370
  case Qt::Key_Delete:
×
UNCOV
1371
    onDelete();
×
1372
    break;
×
1373
  case Qt::Key_Return:
×
1374
  case Qt::Key_Enter:
UNCOV
1375
    if (proxyModel.rowCount() > 0) {
×
UNCOV
1376
      on_treeView_clicked(ui->treeView->currentIndex());
×
1377
    }
1378
    break;
UNCOV
1379
  case Qt::Key_Escape:
×
UNCOV
1380
    ui->lineEdit->clear();
×
UNCOV
1381
    break;
×
1382
  default:
1383
    break;
1384
  }
1385
}
×
1386

1387
/**
1388
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1389
 * menu
1390
 * @param pos
1391
 */
1392
void MainWindow::showContextMenu(const QPoint &pos) {
×
UNCOV
1393
  QModelIndex index = ui->treeView->indexAt(pos);
×
1394
  bool selected = true;
1395
  if (!index.isValid()) {
1396
    ui->treeView->clearSelection();
×
1397
    ui->actionDelete->setEnabled(false);
×
UNCOV
1398
    ui->actionEdit->setEnabled(false);
×
UNCOV
1399
    m_currentDir = "";
×
1400
    selected = false;
1401
  }
1402

UNCOV
1403
  ui->treeView->setCurrentIndex(index);
×
1404

UNCOV
1405
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1406

1407
  QFileInfo fileOrFolder =
1408
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1409

UNCOV
1410
  QMenu contextMenu;
×
UNCOV
1411
  if (!selected || fileOrFolder.isDir()) {
×
1412
    QAction *openFolder =
1413
        contextMenu.addAction(tr("Open folder with file manager"));
×
1414
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1415
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
UNCOV
1416
    QAction *users = contextMenu.addAction(tr("Users"));
×
UNCOV
1417
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
UNCOV
1418
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1419
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
UNCOV
1420
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1421
  } else if (fileOrFolder.isFile()) {
×
UNCOV
1422
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
UNCOV
1423
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1424
  }
UNCOV
1425
  if (selected) {
×
1426
    contextMenu.addSeparator();
×
1427
    if (fileOrFolder.isDir()) {
×
UNCOV
1428
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1429
      connect(renameFolder, &QAction::triggered, this,
×
1430
              &MainWindow::renameFolder);
×
1431
    } else if (fileOrFolder.isFile()) {
×
1432
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1433
      connect(renamePassword, &QAction::triggered, this,
×
1434
              &MainWindow::renamePassword);
×
1435
    }
1436
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1437
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1438
    if (fileOrFolder.isDir()) {
×
1439
      QString dirPath = QDir::cleanPath(
UNCOV
1440
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1441

1442
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1443
      contextMenu.addMenu(shareMenu);
×
1444

1445
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1446
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1447

1448
      QString exePath = QtPassSettings::isUsePass()
×
1449
                            ? QtPassSettings::getPassExecutable()
×
1450
                            : QtPassSettings::getGpgExecutable();
×
UNCOV
1451
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1452
                                                 QFile(exePath).exists());
×
1453

1454
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
UNCOV
1455
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1456
      connect(reencrypt, &QAction::triggered, this,
×
UNCOV
1457
              [this, dirPath]() { reencryptPath(dirPath); });
×
1458

1459
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
UNCOV
1460
      exportKey->setEnabled(gpgAvailable);
×
1461
      connect(exportKey, &QAction::triggered, this,
×
1462
              &MainWindow::exportPublicKey);
×
1463

1464
      QAction *addRecipientAction =
1465
          shareMenu->addAction(tr("Add recipient..."));
×
1466
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1467
      connect(addRecipientAction, &QAction::triggered, this,
×
1468
              [this, dirPath]() { addRecipient(dirPath); });
×
1469

1470
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1471
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1472
    }
1473
  }
UNCOV
1474
  contextMenu.exec(globalPos);
×
1475
}
×
1476

1477
/**
1478
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1479
 * password window
1480
 * @param pos
1481
 */
1482
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1483
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1484
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1485

1486
  contextMenu->exec(globalPos);
×
1487
  delete contextMenu;
×
UNCOV
1488
}
×
1489

1490
/**
1491
 * @brief MainWindow::openFolder open the folder in the default file manager
1492
 */
UNCOV
1493
void MainWindow::openFolder() {
×
1494
  QString dir =
UNCOV
1495
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1496

UNCOV
1497
  QString path = QDir::toNativeSeparators(dir);
×
1498
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1499
}
×
1500

1501
/**
1502
 * @brief MainWindow::addFolder add a new folder to store passwords in
1503
 */
1504
void MainWindow::addFolder() {
×
1505
  bool ok;
1506
  QString dir =
UNCOV
1507
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1508
  QString newdir =
1509
      QInputDialog::getText(this, tr("New file"),
×
UNCOV
1510
                            tr("New Folder: \n(Will be placed in %1 )")
×
1511
                                .arg(QtPassSettings::getPassStore() +
×
UNCOV
1512
                                     Util::getDir(ui->treeView->currentIndex(),
×
1513
                                                  true, model, proxyModel)),
1514
                            QLineEdit::Normal, "", &ok);
×
1515
  if (!ok || newdir.isEmpty()) {
×
1516
    return;
1517
  }
1518
  newdir.prepend(dir);
UNCOV
1519
  if (!confirmPathInStore(newdir)) {
×
1520
    return;
1521
  }
UNCOV
1522
  if (!QDir().mkdir(newdir)) {
×
1523
    QMessageBox::warning(this, tr("Error"),
×
UNCOV
1524
                         tr("Failed to create folder: %1").arg(newdir));
×
1525
    return;
×
1526
  }
1527
  if (QtPassSettings::isAddGPGId(true)) {
×
1528
    QString gpgIdFile = newdir + "/.gpg-id";
×
UNCOV
1529
    QFile gpgId(gpgIdFile);
×
1530
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1531
      QMessageBox::warning(
×
UNCOV
1532
          this, tr("Error"),
×
UNCOV
1533
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1534
      return;
1535
    }
UNCOV
1536
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
UNCOV
1537
    for (const UserInfo &user : users) {
×
1538
      if (user.enabled) {
×
1539
        gpgId.write((user.key_id + "\n").toUtf8());
×
1540
      }
1541
    }
UNCOV
1542
    gpgId.close();
×
1543
    // Lock to owner-only access; see ImitatePass::writeGpgIdFile for
1544
    // rationale (NFS / USB / unusual umask scenarios). Best-effort on
1545
    // platforms where setPermissions is a no-op.
1546
    QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
×
1547
  }
×
1548
}
1549

1550
/**
1551
 * @brief MainWindow::renameFolder rename an existing folder
1552
 */
1553
void MainWindow::renameFolder() {
×
1554
  bool ok;
1555
  QString srcDir = QDir::cleanPath(
UNCOV
1556
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
UNCOV
1557
  QString srcDirName = QDir(srcDir).dirName();
×
1558
  QString newName =
UNCOV
1559
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
UNCOV
1560
                            QLineEdit::Normal, srcDirName, &ok);
×
UNCOV
1561
  if (!ok || newName.isEmpty()) {
×
1562
    return;
1563
  }
1564
  QString destDir = srcDir;
UNCOV
1565
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
UNCOV
1566
  if (!confirmPathInStore(destDir)) {
×
1567
    return;
1568
  }
1569
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1570
}
1571

1572
/**
1573
 * @brief MainWindow::editPassword read password and open edit window via
1574
 * MainWindow::onEdit()
1575
 */
1576
void MainWindow::editPassword(const QString &file) {
×
1577
  if (!file.isEmpty()) {
×
UNCOV
1578
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
UNCOV
1579
      onUpdate(true);
×
1580
    }
1581
    setPassword(file, false);
×
1582
  }
UNCOV
1583
}
×
1584

1585
/**
1586
 * @brief MainWindow::renamePassword rename an existing password
1587
 */
UNCOV
1588
void MainWindow::renamePassword() {
×
1589
  bool ok;
UNCOV
1590
  QString file = getFile(ui->treeView->currentIndex(), false);
×
UNCOV
1591
  QString filePath = QFileInfo(file).path();
×
1592
  QString fileName = QFileInfo(file).fileName();
×
1593
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1594
    fileName.chop(4);
×
1595
  }
1596

1597
  QString newName =
UNCOV
1598
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1599
                            QLineEdit::Normal, fileName, &ok);
×
UNCOV
1600
  if (!ok || newName.isEmpty()) {
×
1601
    return;
1602
  }
UNCOV
1603
  QString newFile = QDir(filePath).filePath(newName);
×
1604
  if (!confirmPathInStore(newFile)) {
×
1605
    return;
1606
  }
1607
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1608
}
1609

1610
/**
1611
 * @brief Copies the password of the selected file from the tree view to the
1612
 * clipboard.
1613
 * @example
1614
 * MainWindow::copyPasswordFromTreeview();
1615
 *
1616
 * @return void - This function does not return a value.
1617
 */
UNCOV
1618
void MainWindow::copyPasswordFromTreeview() {
×
1619
  QFileInfo fileOrFolder =
1620
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1621

UNCOV
1622
  if (fileOrFolder.isFile()) {
×
1623
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1624
    // Disconnect any previous connection to avoid accumulation
UNCOV
1625
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1626
               &MainWindow::passwordFromFileToClipboard);
UNCOV
1627
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
UNCOV
1628
            &MainWindow::passwordFromFileToClipboard);
×
UNCOV
1629
    QtPassSettings::getPass()->Show(file);
×
1630
  }
1631
}
×
1632

1633
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1634
  QStringList tokens = text.split('\n');
×
UNCOV
1635
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1636
}
×
1637

1638
/**
1639
 * @brief Displays message in status bar
1640
 *
1641
 * @param msg     text to be displayed
1642
 * @param timeout time for which msg shall be visible
1643
 */
1644
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
2✔
1645
  ui->statusBar->showMessage(msg, timeout);
2✔
1646
}
2✔
1647

1648
/**
1649
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1650
 * @param dir Directory path to re-encrypt
1651
 */
1652
void MainWindow::reencryptPath(const QString &dir) {
×
UNCOV
1653
  QDir checkDir(dir);
×
1654
  if (!checkDir.exists()) {
×
UNCOV
1655
    QMessageBox::critical(this, tr("Error"),
×
1656
                          tr("Directory does not exist: %1").arg(dir));
×
1657
    return;
×
1658
  }
1659

1660
  int ret = QMessageBox::question(
×
UNCOV
1661
      this, tr("Re-encrypt passwords"),
×
1662
      tr("Re-encrypt all passwords in %1?\n\n"
×
1663
         "This will re-encrypt ALL password files in this folder "
1664
         "using the current recipients defined in .gpg-id.\n\n"
1665
         "This may rewrite many files and cannot be undone easily.\n\n"
1666
         "Continue?")
UNCOV
1667
          .arg(QDir(dir).dirName()),
×
1668
      QMessageBox::Yes | QMessageBox::No);
1669

UNCOV
1670
  if (ret != QMessageBox::Yes)
×
1671
    return;
1672

1673
  // Disable preemptively. ImitatePass::reencryptPath emits
1674
  // startReencryptPath asynchronously and the slot would re-run this,
1675
  // but setEnabled(false) is idempotent so the duplicate is harmless.
UNCOV
1676
  startReencryptPath();
×
1677

UNCOV
1678
  QtPassSettings::getImitatePass()->reencryptPath(
×
UNCOV
1679
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1680
}
×
1681

1682
/**
1683
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1684
 */
1685
void MainWindow::startReencryptPath() {
×
1686
  setUiElementsEnabled(false);
×
1687
  ui->treeView->setDisabled(true);
×
1688
}
×
1689

1690
/**
1691
 * @brief MainWindow::endReencryptPath re-enable ui elements
1692
 */
1693
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1694

1695
/**
1696
 * @brief MainWindow::exportPublicKey export the configured signing key in
1697
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1698
 *
1699
 * Falls back to a help dialog when no signing key is configured or gpg is
1700
 * unavailable, so the user still gets actionable guidance.
1701
 */
1702
void MainWindow::exportPublicKey() {
×
UNCOV
1703
  QString identity = QtPassSettings::getPassSigningKey();
×
UNCOV
1704
  if (identity.isEmpty()) {
×
UNCOV
1705
    QMessageBox::information(
×
UNCOV
1706
        this, tr("Export Public Key"),
×
UNCOV
1707
        tr("<h3>Export Your Public Key</h3>"
×
1708
           "<p>No signing key is configured. Set one in QtPass Settings "
1709
           "&gt; GPG keys, or run this in a terminal:</p>"
1710
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1711
           "&gt;</pre>"
1712
           "<p>Then send the file to your teammates.</p>"));
1713
    return;
×
1714
  }
1715
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
1716
  if (gpgExe.isEmpty()) {
×
1717
    gpgExe = QStringLiteral("gpg");
×
1718
  }
1719
  QStringList args = {"--armor", "--export"};
×
UNCOV
1720
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
UNCOV
1721
  QString stdOut;
×
UNCOV
1722
  QString stdErr;
×
1723
  int exitCode =
1724
      Executor::executeBlocking(gpgExe, args, QString(), &stdOut, &stdErr);
×
UNCOV
1725
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1726
    QMessageBox::warning(this, tr("Export Public Key"),
×
1727
                         tr("Could not export public key for %1.\n\n%2")
×
UNCOV
1728
                             .arg(identity, stdErr.isEmpty()
×
UNCOV
1729
                                                ? tr("No output from gpg.")
×
1730
                                                : stdErr));
1731
    return;
1732
  }
UNCOV
1733
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
UNCOV
1734
  dialog.exec();
×
1735
}
×
1736

1737
/**
1738
 * @brief MainWindow::addRecipient open the recipient management dialog for
1739
 *        the supplied directory.
1740
 * @param dir Folder whose .gpg-id should be edited.
1741
 *
1742
 * Delegates to UsersDialog so users can tick/untick keys from their
1743
 * keyring as recipients of the folder; importing a foreign key into the
1744
 * keyring still has to happen via gpg (or QtPass settings) first.
1745
 */
1746
void MainWindow::addRecipient(const QString &dir) {
×
1747
  UsersDialog d(dir, this);
×
1748
  d.exec();
×
1749
}
×
1750

1751
/**
1752
 * @brief MainWindow::showShareHelp show help about GPG sharing
1753
 */
1754
void MainWindow::showShareHelp() {
×
1755
  QMessageBox::information(
×
1756
      this, tr("Sharing Passwords with GPG"),
×
UNCOV
1757
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1758
         "<p>To share passwords with other users:</p>"
1759
         "<ol>"
1760
         "<li><b>Export your public key</b> and send it to teammates</li>"
1761
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1762
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1763
         "them</li>"
1764
         "</ol>"
1765
         "<p>Only people who have a matching secret key can decrypt the "
1766
         "passwords.</p>"
1767
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1768
         "<p>See the FAQ for more details.</p>"));
UNCOV
1769
}
×
1770

1771
void MainWindow::updateGitButtonVisibility() {
15✔
1772
  if (!QtPassSettings::isUseGit() ||
30✔
1773
      (QtPassSettings::getGitExecutable().isEmpty() &&
15✔
1774
       QtPassSettings::getPassExecutable().isEmpty())) {
15✔
1775
    enableGitButtons(false);
15✔
1776
  } else {
UNCOV
1777
    enableGitButtons(true);
×
1778
  }
1779
}
15✔
1780

1781
void MainWindow::updateOtpButtonVisibility() {
15✔
1782
#if defined(Q_OS_WIN) || defined(__APPLE__)
1783
  ui->actionOtp->setVisible(false);
1784
#endif
1785
  if (!QtPassSettings::isUseOtp()) {
15✔
1786
    ui->actionOtp->setEnabled(false);
15✔
1787
  } else {
UNCOV
1788
    ui->actionOtp->setEnabled(true);
×
1789
  }
1790
}
15✔
1791

1792
void MainWindow::updateGrepButtonVisibility() {
12✔
1793
  const bool enabled = QtPassSettings::isUseGrepSearch();
12✔
1794
  ui->grepButton->setVisible(enabled);
12✔
1795
  ui->grepCaseButton->setVisible(enabled);
12✔
1796
  if (!enabled && m_grep.inGrepMode()) {
12✔
1797
    ui->grepButton->setChecked(false);
×
1798
  }
1799
}
12✔
1800

1801
void MainWindow::enableGitButtons(const bool &state) {
15✔
1802
  // Following GNOME guidelines is preferable disable buttons instead of hide
1803
  ui->actionPush->setEnabled(state);
15✔
1804
  ui->actionUpdate->setEnabled(state);
15✔
1805
}
15✔
1806

1807
/**
1808
 * @brief MainWindow::critical critical message popup wrapper.
1809
 * @param title
1810
 * @param msg
1811
 */
UNCOV
1812
void MainWindow::critical(const QString &title, const QString &msg) {
×
1813
  QMessageBox::critical(this, title, msg);
×
UNCOV
1814
}
×
1815

1816
/**
1817
 * @brief Appends processed command output to the output panel.
1818
 *
1819
 * Appends text to the process output text edit, with per-line numbering,
1820
 * optional command prefix, and color coding for errors vs. success.
1821
 * Handles auto-scrolling and line limits.
1822
 *
1823
 * @param output The raw output text from the command.
1824
 * @param isError true if this is error output (stderr).
1825
 * @param linePrefix Optional command name to prefix each line with.
1826
 */
1827
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2✔
1828
                                     const QString &linePrefix) {
1829
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
1830
    return;
1✔
1831
  }
1832

1833
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1834
  for (QString &line : lines) {
2✔
1835
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1836
    // indentation
1837
    line.remove('\r');
1✔
1838
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1839
      line.chop(1);
×
1840
    }
1841
    if (line.isEmpty()) {
1✔
1842
      continue;
×
1843
    }
1844

1845
    m_outputCounter++;
1✔
1846
    QString lineNumber = QString::number(m_outputCounter);
1✔
1847

1848
    QColor textColor =
1849
        isError ? QColor(Qt::red)
1✔
1850
                : m_processOutputEdit->palette().color(QPalette::Text);
2✔
1851
    QString colorHex = textColor.name();
1✔
1852
    // Apply the optional prefix per line so multi-line output stays
1853
    // attributed to its command (e.g. all 3 lines of a `git push` show
1854
    // "git push: ..." rather than only the first).
1855
    QString prefixed =
1856
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2✔
1857
    QString coloredOutput =
1858
        QString("<span style=\"color: %1;\">%2: %3</span>")
1✔
1859
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2✔
1860

1861
    m_processOutputEdit->append(coloredOutput);
1✔
1862
  }
1863

1864
  limitOutputLines();
1✔
1865

1866
  if (m_autoScroll) {
1✔
1867
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
1868
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
1869
  }
1870
}
1871

1872
/**
1873
 * @brief Handles process output from the Pass executor.
1874
 *
1875
 * Called when any non-sensitive process completes. Filters out password-
1876
 * related commands (pass show, insert, etc.) and delegates to
1877
 * appendProcessOutput.
1878
 *
1879
 * @param output The stdout/stderr text from the process.
1880
 * @param isError true if this is error output (stderr).
1881
 * @param pid The process ID identifying which command ran.
1882
 */
1883
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
1884
                                 Enums::PROCESS pid) {
1885
  appendProcessOutput(output, isError, getProcessName(pid));
2✔
1886
}
2✔
1887

1888
/**
1889
 * @brief Maps a process ID to its human-readable command name.
1890
 *
1891
 * Returns static strings for git/pass commands that appear in output.
1892
 * Password-related commands return empty (they are filtered).
1893
 *
1894
 * @param pid The process ID to look up.
1895
 * @return QString with command name, or empty if filtered.
1896
 */
1897
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
2✔
1898
  switch (pid) {
2✔
UNCOV
1899
  case Enums::GIT_INIT:
×
UNCOV
1900
    return QStringLiteral("git init"); // no-tr
×
UNCOV
1901
  case Enums::GIT_ADD:
×
UNCOV
1902
    return QStringLiteral("git add"); // no-tr
×
UNCOV
1903
  case Enums::GIT_COMMIT:
×
UNCOV
1904
    return QStringLiteral("git commit"); // no-tr
×
UNCOV
1905
  case Enums::GIT_RM:
×
1906
    return QStringLiteral("git rm"); // no-tr
×
UNCOV
1907
  case Enums::GIT_PULL:
×
UNCOV
1908
    return QStringLiteral("git pull"); // no-tr
×
UNCOV
1909
  case Enums::GIT_PUSH:
×
UNCOV
1910
    return QStringLiteral("git push"); // no-tr
×
UNCOV
1911
  case Enums::GIT_MOVE:
×
UNCOV
1912
    return QStringLiteral("git mv"); // no-tr
×
UNCOV
1913
  case Enums::GIT_COPY:
×
1914
    // ImitatePass::Copy literally invokes `git cp` (a git-extras
1915
    // subcommand), so the label matches what's run. Stock-git users
1916
    // without git-extras will see the underlying "'cp' is not a git
1917
    // command" failure surfaced in the process output panel.
UNCOV
1918
    return QStringLiteral("git cp"); // no-tr
×
UNCOV
1919
  case Enums::PASS_INSERT:
×
UNCOV
1920
    return QStringLiteral("pass insert"); // no-tr
×
UNCOV
1921
  case Enums::PASS_REMOVE:
×
UNCOV
1922
    return QStringLiteral("pass rm"); // no-tr
×
UNCOV
1923
  case Enums::PASS_INIT:
×
UNCOV
1924
    return QStringLiteral("pass init"); // no-tr
×
1925
  case Enums::PASS_MOVE:
×
UNCOV
1926
    return QStringLiteral("pass mv"); // no-tr
×
UNCOV
1927
  case Enums::PASS_COPY:
×
UNCOV
1928
    return QStringLiteral("pass cp"); // no-tr
×
UNCOV
1929
  case Enums::PASS_GREP:
×
UNCOV
1930
    return QStringLiteral("pass grep"); // no-tr
×
UNCOV
1931
  case Enums::GPG_GENKEYS:
×
UNCOV
1932
    return QStringLiteral("gpg --gen-key"); // no-tr
×
1933
  case Enums::PASS_SHOW:
1934
  case Enums::PASS_OTP_GENERATE:
1935
  case Enums::PROCESS_COUNT:
1936
  case Enums::INVALID:
1937
    break;
1938
  }
1939
  return {};
1940
}
1941

1942
/**
1943
 * @brief Checks if a process ID represents a sensitive operation whose
1944
 * output should not be shown in the process output panel.
1945
 *
1946
 * Password-related commands (pass show, OTP generate, grep, insert)
1947
 * display their output in other UI areas, so we skip them here.
1948
 *
1949
 * @param pid The process ID to check.
1950
 * @return true if the process is sensitive and should be filtered.
1951
 */
UNCOV
1952
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
UNCOV
1953
  switch (pid) {
×
1954
  case Enums::PASS_SHOW:
1955
  case Enums::PASS_OTP_GENERATE:
1956
  case Enums::PASS_GREP:
1957
  case Enums::PASS_INSERT:
1958
    return true;
1959
  case Enums::GIT_INIT:
1960
  case Enums::GIT_ADD:
1961
  case Enums::GIT_COMMIT:
1962
  case Enums::GIT_RM:
1963
  case Enums::GIT_PULL:
1964
  case Enums::GIT_PUSH:
1965
  case Enums::GIT_MOVE:
1966
  case Enums::GIT_COPY:
1967
  case Enums::PASS_REMOVE:
1968
  case Enums::PASS_INIT:
1969
  case Enums::PASS_MOVE:
1970
  case Enums::PASS_COPY:
1971
  case Enums::GPG_GENKEYS:
1972
  case Enums::PROCESS_COUNT:
1973
  case Enums::INVALID:
1974
    break;
1975
  }
1976
  return false;
×
1977
}
1978

1979
/**
1980
 * @brief Updates the visibility of the process output panel.
1981
 *
1982
 * Shows or hides the process output widget based on the user's
1983
 * showProcessOutput setting.
1984
 */
UNCOV
1985
void MainWindow::updateProcessOutputVisibility() {
×
UNCOV
1986
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
UNCOV
1987
}
×
1988

1989
/**
1990
 * @brief Limits the output panel to max lines, trimming old excess.
1991
 *
1992
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
1993
 * Called after each append to prevent unbounded growth.
1994
 */
1995
void MainWindow::limitOutputLines() {
1✔
1996
  QTextDocument *doc = m_processOutputEdit->document();
1✔
1997
  int excess = doc->blockCount() - MaxOutputLines;
1✔
1998
  if (excess <= 0) {
1✔
1999
    return;
1✔
2000
  }
2001

UNCOV
2002
  QTextCursor cursor(doc);
×
UNCOV
2003
  cursor.movePosition(QTextCursor::Start);
×
UNCOV
2004
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
UNCOV
2005
  cursor.removeSelectedText();
×
UNCOV
2006
}
×
2007

2008
/**
2009
 * @brief Clears the process output panel.
2010
 *
2011
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2012
 */
UNCOV
2013
void MainWindow::on_clearOutputButton_clicked() {
×
UNCOV
2014
  m_processOutputEdit->clear();
×
UNCOV
2015
  m_outputCounter = 0;
×
UNCOV
2016
  m_autoScroll = true;
×
UNCOV
2017
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc