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

IJHack / QtPass / 27750739633

18 Jun 2026 09:40AM UTC coverage: 55.048% (-0.008%) from 55.056%
27750739633

push

github

web-flow
fix: set solid palette on textBrowser context menu (#1574)

* fix: set solid palette on textBrowser context menu

The standard context menu created by QTextBrowser::createStandardContextMenu()
inherits no explicit background, causing compositing window managers to render
it transparently over the password content. Explicitly root the menu's palette
in QApplication::palette() to guarantee a solid background — same principle as
the textBrowser stylesheet fix in #967.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: scope field stylesheets so context menus render opaque

The template-field value widgets (QTextBrowser / password QLineEdit) and
their buttons carried unscoped stylesheets ('background: transparent;').
Qt cascades unscoped widget stylesheets into child popups, so the fields'
standard context menus inherited the transparent background and rendered
see-through.

Scope every rule to its widget type (QPushButton / QLineEdit, QTextBrowser)
so the transparent background applies only to the field and not to the child
QMenu, which then falls back to the opaque native menu background.

Also reparent the main textBrowser's standard context menu to the main
window (away from textBrowser's stylesheet) for the same reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Anne Jan Brouwer <brouwer@annejan.com>

2 of 3 new or added lines in 2 files covered. (66.67%)

1 existing line in 1 file now uncovered.

3675 of 6676 relevant lines covered (55.05%)

29.35 hits per line

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

27.6
/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
  proxyModel.setPass(QtPassSettings::getPass());
12✔
91
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
12✔
92

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

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

126
  updateProfileBox();
12✔
127

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

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

139
  searchTimer.setInterval(350);
12✔
140
  searchTimer.setSingleShot(true);
12✔
141

142
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
12✔
143

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

148
  // Safety net: if a backend operation disables the UI but never signals
149
  // completion, re-enable after a timeout so the window can't get stuck.
150
  m_uiWatchdog.setSingleShot(true);
12✔
151
  m_uiWatchdog.setInterval(UiWatchdogMs);
12✔
152
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
12✔
153
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
154
    setUiElementsEnabled(true);
×
155
  });
×
156

157
  initToolBarButtons();
12✔
158
  initStatusBar();
12✔
159
  initProcessOutputPanel();
12✔
160

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

182
  ui->lineEdit->setClearButtonEnabled(true);
12✔
183
  updateGrepButtonVisibility();
12✔
184

185
  setUiElementsEnabled(true);
12✔
186

187
  ui->lineEdit->setText(searchText);
12✔
188

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

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

204
MainWindow::~MainWindow() { delete m_qtPass; }
36✔
205

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

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

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

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

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

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

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

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

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

375
  connect(m_clearOutputButton, &QToolButton::clicked, this,
12✔
376
          &MainWindow::on_clearOutputButton_clicked);
12✔
377

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

397
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
398
  return ui->treeView->currentIndex();
×
399
}
400

401
void MainWindow::cleanKeygenDialog() {
1✔
402
  if (m_keyGenDialog != nullptr) {
1✔
403
    m_keyGenDialog->close();
×
404
  }
405
  m_keyGenDialog = nullptr;
1✔
406
}
1✔
407

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

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

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

453
  if (s.noLineWrapping) {
×
454
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
455
  } else {
456
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
457
  }
458
}
×
459

460
void MainWindow::applyWindowFlagsSettings() {
×
461
  if (QtPassSettings::isAlwaysOnTop()) {
×
462
    Qt::WindowFlags flags = windowFlags();
463
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
464
  } else {
465
    this->setWindowFlags(Qt::Window);
×
466
  }
467
  this->show();
×
468
}
×
469

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

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

495
      updateProfileBox();
×
496
      const AppSettings s = QtPassSettings::load();
×
497
      proxyModel.setStore(s.passStore);
×
498
      ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
499
      deselect();
×
500
      ui->treeView->setCurrentIndex(QModelIndex());
×
501

502
      if (m_qtPass->isFreshStart() && !Util::configIsValid(s)) {
×
503
        config();
×
504
        return;
505
      }
506
      Pass *activePass = QtPassSettings::getPass();
×
507
      activePass->updateEnv();
×
508
      proxyModel.setPass(activePass);
×
509
      clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
×
510
      m_qtPass->setClipboardTimer();
×
511

512
      updateGitButtonVisibility();
×
513
      updateOtpButtonVisibility();
×
514
      updateGrepButtonVisibility();
×
515
      updateProcessOutputVisibility();
×
516
      if (s.useTrayIcon && m_tray == nullptr) {
×
517
        initTrayIcon();
×
518
      } else if (!s.useTrayIcon && m_tray != nullptr) {
×
519
        destroyTrayIcon();
×
520
      }
521
    }
×
522

523
    m_qtPass->setFreshStart(false);
×
524
  }
525
}
×
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 {
×
557
  if (!index.isValid() ||
×
558
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
559
    return {};
560
  }
561
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
562
  if (forPass) {
×
563
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
564
    filePath.replace(Util::endsWithGpg(), "");
×
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) {
×
574
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
575
  m_currentDir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
576
                              proxyModel, QtPassSettings::getPassStore());
×
577
  // Clear any previously cached clipped text before showing new password
578
  m_qtPass->clearClippedText();
×
579
  QString file = getFile(index, true);
×
580
  ui->passwordName->setText(file);
×
581
  if (!file.isEmpty() && !cleared) {
×
582
    QtPassSettings::getPass()->Show(file);
×
583
  } else {
584
    clearPanel(false);
×
585
    ui->actionEdit->setEnabled(false);
×
586
    ui->actionDelete->setEnabled(true);
×
587
  }
588
}
×
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) {
×
596
  QFileInfo fileOrFolder =
597
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
598

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

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

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

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

646
  // set clipped text
647
  m_qtPass->setClippedText(password, p_output);
×
648

649
  // first clear the current view:
650
  m_displayPanel->clear();
×
651

652
  // show what is needed:
653
  if (s.hideContent) {
×
654
    output = "***" + tr("Content hidden") + "***";
×
655
  } else if (!s.displayAsIs) {
×
656
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s);
×
657
    output = fileContent.getRemainingDataForDisplay();
×
658
  }
659

660
  if (s.useAutoclearPanel) {
×
661
    clearPanelTimer.start();
×
662
  }
663

664
  emit passShowHandlerFinished(output);
×
665
  setUiElementsEnabled(true);
×
666
}
×
667

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

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

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

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

771
  if (s.alwaysOnTop) {
12✔
772
    Qt::WindowFlags flags = windowFlags();
773
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
774
    show();
×
775
  }
776

777
  if (s.useTrayIcon && m_tray == nullptr) {
12✔
778
    initTrayIcon();
×
779
    if (s.startMinimized) {
×
780
      // since we are still in constructor, can't directly hide
781
      QTimer::singleShot(10, this, SLOT(hide()));
×
782
    }
783
  } else if (!s.useTrayIcon && m_tray != nullptr) {
12✔
784
    destroyTrayIcon();
×
785
  }
786
}
24✔
787

788
/**
789
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
790
 */
791
void MainWindow::onConfig() { config(); }
×
792

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

810
/**
811
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
812
 * time from two keypresses is elapsed
813
 */
814
void MainWindow::onTimeoutSearch() {
×
815
  QString query = ui->lineEdit->text();
×
816

817
  if (query.isEmpty()) {
×
818
    ui->treeView->collapseAll();
×
819
    deselect();
×
820
  }
821

822
  query.replace(QStringLiteral(" "), ".*");
×
823
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
824
  if (!regExp.isValid())
×
825
    return;
826
  proxyModel.setFilterRegularExpression(regExp);
×
827
  ui->treeView->setRootIndex(
×
828
      proxyModel.rootIndexFor(QtPassSettings::getPassStore()));
×
829

830
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
831
    selectFirstFile();
×
832
  } else {
833
    ui->actionEdit->setEnabled(false);
×
834
    ui->actionDelete->setEnabled(false);
×
835
  }
836
}
×
837

838
/**
839
 * @brief MainWindow::on_lineEdit_returnPressed get searching
840
 *
841
 * Select the first possible file in the tree
842
 */
843
void MainWindow::on_lineEdit_returnPressed() {
×
844
#ifdef QT_DEBUG
845
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
846
#endif
847

848
  if (m_grep.inGrepMode()) {
×
849
    const QString query = ui->lineEdit->text();
×
850
    if (!query.isEmpty()) {
×
851
      ui->grepResultsList->clear();
×
852
      ui->statusBar->showMessage(tr("Searching…"));
×
853
      if (m_grep.beginSearch()) {
854
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
855
      }
856
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
857
    } else {
858
      if (m_grep.cancelSearch()) {
859
        QApplication::restoreOverrideCursor();
×
860
      }
861
      ui->grepResultsList->clear();
×
862
      ui->grepResultsList->setVisible(false);
×
863
      ui->treeView->setVisible(true);
×
864
    }
865
    return;
866
  }
867

868
  if (proxyModel.rowCount() > 0) {
×
869
    selectFirstFile();
×
870
    on_treeView_clicked(ui->treeView->currentIndex());
×
871
  }
872
}
873

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

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

964
/**
965
 * @brief Navigate to the password entry when a grep result is clicked.
966
 */
967
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
968
                                                int /*column*/) {
969
  const AppSettings s = QtPassSettings::load();
×
970
  const QString entry = item->data(0, Qt::UserRole).toString();
×
971
  if (entry.isEmpty())
×
972
    return;
973
  const QString fullPath =
974
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
×
975
  QModelIndex srcIndex = model.index(fullPath);
×
976
  if (!srcIndex.isValid())
977
    return;
978
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
979
  if (!proxyIndex.isValid())
980
    return;
981
  ui->treeView->setCurrentIndex(proxyIndex);
×
982
  on_treeView_clicked(proxyIndex);
×
983
  if (s.hideContent || s.useAutoclearPanel)
×
984
    ui->grepResultsList->clear();
×
985
  ui->grepResultsList->setVisible(false);
×
986
  ui->treeView->setVisible(true);
×
987
  ui->treeView->scrollTo(proxyIndex);
×
988
  ui->treeView->setFocus();
×
989
}
×
990

991
/**
992
 * @brief MainWindow::selectFirstFile select the first possible file in the
993
 * tree
994
 */
995
void MainWindow::selectFirstFile() {
×
996
  QModelIndex index = proxyModel.rootIndexFor(QtPassSettings::getPassStore());
×
997
  index = firstFile(index);
×
998
  ui->treeView->setCurrentIndex(index);
×
999
}
×
1000

1001
/**
1002
 * @brief MainWindow::firstFile return location of first possible file
1003
 * @param parentIndex
1004
 * @return QModelIndex
1005
 */
1006
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
1007
  QModelIndex index = parentIndex;
×
1008
  int numRows = proxyModel.rowCount(parentIndex);
×
1009
  for (int row = 0; row < numRows; ++row) {
×
1010
    index = proxyModel.index(row, 0, parentIndex);
×
1011
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
1012
      return index;
×
1013
    }
1014
    if (proxyModel.hasChildren(index)) {
×
1015
      return firstFile(index);
×
1016
    }
1017
  }
1018
  return index;
×
1019
}
1020

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

1043
/**
1044
 * @brief MainWindow::setPassword open passworddialog
1045
 * @param file which pgp file
1046
 * @param isNew insert (not update)
1047
 */
1048
void MainWindow::setPassword(const QString &file, bool isNew) {
×
1049
  const AppSettings s = QtPassSettings::load();
×
1050
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
×
1051

1052
  if (isNew) {
×
1053
    const QString storePath = s.passStore;
1054
    QString folder = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1055
                                  proxyModel, s.passStore);
×
1056
    if (folder.isEmpty()) {
×
1057
      folder = storePath;
×
1058
    }
1059
    QHash<QString, QStringList> templates =
1060
        TemplateIO::readTemplates(storePath);
×
1061
    if (!templates.isEmpty()) {
1062
      QString defaultTemplate =
1063
          TemplateIO::getFolderTemplate(folder, storePath);
×
1064
      d.setAvailableTemplates(templates, defaultTemplate);
×
1065
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
1066
                    [&d]() { d.cycleTemplate(); });
×
1067
    }
1068
  }
×
1069

1070
  if (!d.exec()) {
×
1071
    ui->treeView->setFocus();
×
1072
  }
1073
}
×
1074

1075
/**
1076
 * @brief MainWindow::addPassword add a new password by showing a
1077
 * number of dialogs.
1078
 */
1079
void MainWindow::addPassword() {
×
1080
  const QString passStore = QtPassSettings::load().passStore;
×
1081
  bool ok;
1082
  QString dir = Util::getDir(ui->treeView->currentIndex(), true, model,
×
1083
                             proxyModel, passStore);
×
1084
  QString file = QInputDialog::getText(
1085
      this, tr("New file"),
×
1086
      tr("New password file: \n(Will be placed in %1 )")
×
1087
          .arg(passStore + Util::getDir(ui->treeView->currentIndex(), true,
×
1088
                                        model, proxyModel, passStore)),
1089
      QLineEdit::Normal, "", &ok);
×
1090
  if (!ok || file.isEmpty()) {
×
1091
    return;
1092
  }
1093
  file = dir + file;
×
1094
  if (!confirmPathInStore(passStore + file)) {
×
1095
    return;
1096
  }
1097
  setPassword(file);
×
1098
}
1099

1100
/**
1101
 * @brief MainWindow::onDelete remove password, if you are
1102
 * sure.
1103
 */
1104
void MainWindow::onDelete() {
×
1105
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1106
  if (!currentIndex.isValid()) {
1107
    // This fixes https://github.com/IJHack/QtPass/issues/556
1108
    // Otherwise the entire password directory would be deleted if
1109
    // nothing is selected in the tree view.
1110
    return;
×
1111
  }
1112

1113
  QFileInfo fileOrFolder =
1114
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1115
  QString file = "";
×
1116
  bool isDir = false;
1117

1118
  if (fileOrFolder.isFile()) {
×
1119
    file = getFile(ui->treeView->currentIndex(), true);
×
1120
  } else {
1121
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel,
×
1122
                        QtPassSettings::getPassStore());
×
1123
    isDir = true;
1124
  }
1125

1126
  QString dirMessage = tr(" and the whole content?");
1127
  if (isDir) {
×
1128
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1129
                    QDirIterator::Subdirectories);
×
1130
    bool okDir = true;
1131
    while (it.hasNext() && okDir) {
×
1132
      it.next();
×
1133
      if (QFileInfo(it.filePath()).isFile()) {
×
1134
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1135
          okDir = false;
1136
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1137
                          "there are unexpected files in the given folder, "
1138
                          "check them before continue.</strong>");
1139
        }
1140
      }
1141
    }
1142
  }
×
1143

1144
  if (QMessageBox::question(
×
1145
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1146
          tr("Are you sure you want to delete %1%2?")
×
1147
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1148
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1149
    return;
1150
  }
1151

1152
  QtPassSettings::getPass()->Remove(file, isDir);
×
1153
}
×
1154

1155
/**
1156
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
1157
 */
1158
void MainWindow::onOtp() {
×
1159
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1160
  if (!file.isEmpty()) {
×
1161
    if (QtPassSettings::isUseOtp()) {
×
1162
      setUiElementsEnabled(false);
×
1163
      QtPassSettings::getPass()->OtpGenerate(file);
×
1164
    }
1165
  } else {
1166
    flashText(tr("No password selected for OTP generation"), true);
×
1167
  }
1168
}
×
1169

1170
/**
1171
 * @brief MainWindow::onEdit try and edit (selected) password.
1172
 */
1173
void MainWindow::onEdit() {
×
1174
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1175
  editPassword(file);
×
1176
}
×
1177

1178
/**
1179
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1180
 * @param dir folder to edit users for.
1181
 */
1182
void MainWindow::userDialog(const QString &dir) {
×
1183
  if (!dir.isEmpty()) {
×
1184
    m_currentDir = dir;
×
1185
  }
1186
  onUsers();
×
1187
}
×
1188

1189
/**
1190
 * @brief MainWindow::onUsers edit users for the current
1191
 * folder,
1192
 * gets lists and opens UserDialog.
1193
 */
1194
void MainWindow::onUsers() {
×
1195
  QString dir = m_currentDir.isEmpty()
1196
                    ? Util::getDir(ui->treeView->currentIndex(), false, model,
×
1197
                                   proxyModel, QtPassSettings::getPassStore())
×
1198
                    : m_currentDir;
×
1199

1200
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1201
  if (!d.exec()) {
×
1202
    ui->treeView->setFocus();
×
1203
  }
1204
}
×
1205

1206
/**
1207
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1208
 * @param message
1209
 */
1210
void MainWindow::messageAvailable(const QString &message) {
×
1211
  show();
×
1212
  raise();
×
1213
  if (message.isEmpty()) {
×
1214
    focusInput();
×
1215
  } else {
1216
    ui->treeView->expandAll();
×
1217
    ui->lineEdit->setText(message);
×
1218
    on_lineEdit_returnPressed();
×
1219
  }
1220
}
×
1221

1222
/**
1223
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1224
 * @param batch
1225
 * @param keygenWindow
1226
 */
1227
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1228
  m_keyGenDialog = keygenWindow;
×
1229
  emit generateGPGKeyPair(batch);
×
1230
}
×
1231

1232
/**
1233
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1234
 * select a more appropriate one to view too
1235
 */
1236
void MainWindow::updateProfileBox() {
12✔
1237
  QHash<QString, QHash<QString, QString>> profiles =
1238
      QtPassSettings::getProfiles();
12✔
1239

1240
  if (profiles.isEmpty()) {
1241
    ui->profileWidget->hide();
×
1242
  } else {
1243
    ui->profileWidget->show();
12✔
1244
    ui->profileBox->setEnabled(profiles.size() > 1);
24✔
1245
    ui->profileBox->clear();
12✔
1246
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
12✔
1247
    while (i.hasNext()) {
12✔
1248
      i.next();
1249
      if (!i.key().isEmpty()) {
12✔
1250
        ui->profileBox->addItem(i.key());
12✔
1251
      }
1252
    }
1253
    ui->profileBox->model()->sort(0);
12✔
1254
  }
1255
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
24✔
1256
  if (index != -1) { //  -1 for not found
12✔
1257
    ui->profileBox->setCurrentIndex(index);
×
1258
  }
1259
}
12✔
1260

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

1286
  ui->lineEdit->clear();
×
1287

1288
  const QHash<QString, QString> prof =
1289
      QtPassSettings::getProfiles().value(name);
×
1290
  AppSettings s = QtPassSettings::load();
×
1291
  s.activeProfile = name;
×
1292
  s.passStore = prof.value("path");
×
1293
  s.passSigningKey = prof.value("signingKey");
×
1294
  QtPassSettings::save(s);
×
1295
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1296

1297
  QtPassSettings::getPass()->updateEnv();
×
1298

1299
  const QString passStore = QtPassSettings::getPassStore();
×
1300
  proxyModel.setStore(passStore);
×
1301
  ui->treeView->setRootIndex(proxyModel.rootIndexFor(passStore));
×
1302
  deselect();
×
1303
  ui->treeView->setCurrentIndex(QModelIndex());
×
1304
}
×
1305

1306
/**
1307
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1308
 * support
1309
 * it
1310
 */
1311
void MainWindow::initTrayIcon() {
×
1312
  m_tray = new TrayIcon(this);
×
1313
  if (!m_tray->getIsAllocated()) {
×
1314
    destroyTrayIcon();
×
1315
  }
1316
}
×
1317

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

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

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

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

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

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

1405
  ui->treeView->setCurrentIndex(index);
×
1406

1407
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1408

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

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

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

1447
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1448
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1449

1450
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1451
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1452
                                                 QFile(exePath).exists());
×
1453

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

1459
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
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
  }
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
  // createStandardContextMenu() parents the menu to textBrowser, which carries
1485
  // a "background: palette(base)" stylesheet. Qt cascades that stylesheet to
1486
  // the child QMenu and breaks its opaque native background, leaving the menu
1487
  // transparent. Reparent to the main window (no stylesheet) so it paints
1488
  // solid.
NEW
1489
  contextMenu->setParent(this, contextMenu->windowFlags());
×
UNCOV
1490
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1491

1492
  contextMenu->exec(globalPos);
×
1493
  delete contextMenu;
×
1494
}
×
1495

1496
/**
1497
 * @brief MainWindow::openFolder open the folder in the default file manager
1498
 */
1499
void MainWindow::openFolder() {
×
1500
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1501
                             proxyModel, QtPassSettings::getPassStore());
×
1502

1503
  QString path = QDir::toNativeSeparators(dir);
×
1504
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1505
}
×
1506

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

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

1579
/**
1580
 * @brief MainWindow::editPassword read password and open edit window via
1581
 * MainWindow::onEdit()
1582
 */
1583
void MainWindow::editPassword(const QString &file) {
×
1584
  if (!file.isEmpty()) {
×
1585
    const AppSettings s = QtPassSettings::load();
×
1586
    if (s.useGit && s.autoPull) {
×
1587
      onUpdate(true);
×
1588
    }
1589
    setPassword(file, false);
×
1590
  }
×
1591
}
×
1592

1593
/**
1594
 * @brief MainWindow::renamePassword rename an existing password
1595
 */
1596
void MainWindow::renamePassword() {
×
1597
  bool ok;
1598
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1599
  QString filePath = QFileInfo(file).path();
×
1600
  QString fileName = QFileInfo(file).fileName();
×
1601
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1602
    fileName.chop(4);
×
1603
  }
1604

1605
  QString newName =
1606
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1607
                            QLineEdit::Normal, fileName, &ok);
×
1608
  if (!ok || newName.isEmpty()) {
×
1609
    return;
1610
  }
1611
  QString newFile = QDir(filePath).filePath(newName);
×
1612
  if (!confirmPathInStore(newFile)) {
×
1613
    return;
1614
  }
1615
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1616
}
1617

1618
/**
1619
 * @brief Copies the password of the selected file from the tree view to the
1620
 * clipboard.
1621
 * @example
1622
 * MainWindow::copyPasswordFromTreeview();
1623
 *
1624
 * @return void - This function does not return a value.
1625
 */
1626
void MainWindow::copyPasswordFromTreeview() {
×
1627
  QFileInfo fileOrFolder =
1628
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1629

1630
  if (fileOrFolder.isFile()) {
×
1631
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1632
    // Disconnect any previous connection to avoid accumulation
1633
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1634
               &MainWindow::passwordFromFileToClipboard);
1635
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1636
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1637
            &MainWindow::passwordFromFileToClipboard, Qt::SingleShotConnection);
×
1638
#else
1639
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
1640
            &MainWindow::passwordFromFileToClipboard);
1641
#endif
1642
    QtPassSettings::getPass()->Show(file);
×
1643
  }
1644
}
×
1645

1646
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1647
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1648
  // Qt 5: no SingleShotConnection flag — disconnect manually on first fire.
1649
  disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
1650
             &MainWindow::passwordFromFileToClipboard);
1651
#endif
1652
  QStringList tokens = text.split('\n');
×
1653
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1654
}
×
1655

1656
/**
1657
 * @brief Displays message in status bar
1658
 *
1659
 * @param msg     text to be displayed
1660
 * @param timeout time for which msg shall be visible
1661
 */
1662
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
2✔
1663
  ui->statusBar->showMessage(msg, timeout);
2✔
1664
}
2✔
1665

1666
/**
1667
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1668
 * @param dir Directory path to re-encrypt
1669
 */
1670
void MainWindow::reencryptPath(const QString &dir) {
×
1671
  QDir checkDir(dir);
×
1672
  if (!checkDir.exists()) {
×
1673
    QMessageBox::critical(this, tr("Error"),
×
1674
                          tr("Directory does not exist: %1").arg(dir));
×
1675
    return;
×
1676
  }
1677

1678
  int ret = QMessageBox::question(
×
1679
      this, tr("Re-encrypt passwords"),
×
1680
      tr("Re-encrypt all passwords in %1?\n\n"
×
1681
         "This will re-encrypt ALL password files in this folder "
1682
         "using the current recipients defined in .gpg-id.\n\n"
1683
         "This may rewrite many files and cannot be undone easily.\n\n"
1684
         "Continue?")
1685
          .arg(QDir(dir).dirName()),
×
1686
      QMessageBox::Yes | QMessageBox::No);
1687

1688
  if (ret != QMessageBox::Yes)
×
1689
    return;
1690

1691
  // Disable preemptively. ImitatePass::reencryptPath emits
1692
  // startReencryptPath asynchronously and the slot would re-run this,
1693
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1694
  startReencryptPath();
×
1695

1696
  QtPassSettings::getImitatePass()->reencryptPath(
×
1697
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1698
}
×
1699

1700
/**
1701
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1702
 */
1703
void MainWindow::startReencryptPath() {
×
1704
  setUiElementsEnabled(false);
×
1705
  ui->treeView->setDisabled(true);
×
1706
}
×
1707

1708
/**
1709
 * @brief MainWindow::endReencryptPath re-enable ui elements
1710
 */
1711
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1712

1713
/**
1714
 * @brief MainWindow::exportPublicKey export the configured signing key in
1715
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1716
 *
1717
 * Falls back to a help dialog when no signing key is configured or gpg is
1718
 * unavailable, so the user still gets actionable guidance.
1719
 */
1720
void MainWindow::exportPublicKey() {
×
1721
  const AppSettings s = QtPassSettings::load();
×
1722
  const QString identity = s.passSigningKey;
1723
  if (identity.isEmpty()) {
×
1724
    QMessageBox::information(
×
1725
        this, tr("Export Public Key"),
×
1726
        tr("<h3>Export Your Public Key</h3>"
×
1727
           "<p>No signing key is configured. Set one in QtPass Settings "
1728
           "&gt; GPG keys, or run this in a terminal:</p>"
1729
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1730
           "&gt;</pre>"
1731
           "<p>Then send the file to your teammates.</p>"));
1732
    return;
×
1733
  }
1734
  QString gpgExe = s.gpgExecutable;
1735
  if (gpgExe.isEmpty()) {
×
1736
    gpgExe = QStringLiteral("gpg");
×
1737
  }
1738
  QStringList args = {"--armor", "--export"};
×
1739
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1740
  QString stdOut;
×
1741
  QString stdErr;
×
1742
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
×
1743
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1744
    QMessageBox::warning(this, tr("Export Public Key"),
×
1745
                         tr("Could not export public key for %1.\n\n%2")
×
1746
                             .arg(identity, stdErr.isEmpty()
×
1747
                                                ? tr("No output from gpg.")
×
1748
                                                : stdErr));
1749
    return;
1750
  }
1751
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1752
  dialog.exec();
×
1753
}
×
1754

1755
/**
1756
 * @brief MainWindow::addRecipient open the recipient management dialog for
1757
 *        the supplied directory.
1758
 * @param dir Folder whose .gpg-id should be edited.
1759
 *
1760
 * Delegates to UsersDialog so users can tick/untick keys from their
1761
 * keyring as recipients of the folder; importing a foreign key into the
1762
 * keyring still has to happen via gpg (or QtPass settings) first.
1763
 */
1764
void MainWindow::addRecipient(const QString &dir) {
×
1765
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1766
  d.exec();
×
1767
}
×
1768

1769
/**
1770
 * @brief MainWindow::showShareHelp show help about GPG sharing
1771
 */
1772
void MainWindow::showShareHelp() {
×
1773
  QMessageBox::information(
×
1774
      this, tr("Sharing Passwords with GPG"),
×
1775
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1776
         "<p>To share passwords with other users:</p>"
1777
         "<ol>"
1778
         "<li><b>Export your public key</b> and send it to teammates</li>"
1779
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1780
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1781
         "them</li>"
1782
         "</ol>"
1783
         "<p>Only people who have a matching secret key can decrypt the "
1784
         "passwords.</p>"
1785
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1786
         "<p>See the FAQ for more details.</p>"));
1787
}
×
1788

1789
void MainWindow::updateGitButtonVisibility() {
15✔
1790
  const AppSettings s = QtPassSettings::load();
15✔
1791
  if (!s.useGit || (s.gitExecutable.isEmpty() && s.passExecutable.isEmpty())) {
15✔
1792
    enableGitButtons(false);
15✔
1793
  } else {
1794
    enableGitButtons(true);
×
1795
  }
1796
}
15✔
1797

1798
void MainWindow::updateOtpButtonVisibility() {
15✔
1799
#if defined(Q_OS_WIN) || defined(__APPLE__)
1800
  ui->actionOtp->setVisible(false);
1801
#endif
1802
  if (!QtPassSettings::isUseOtp()) {
15✔
1803
    ui->actionOtp->setEnabled(false);
15✔
1804
  } else {
1805
    ui->actionOtp->setEnabled(true);
×
1806
  }
1807
}
15✔
1808

1809
void MainWindow::updateGrepButtonVisibility() {
12✔
1810
  const bool enabled = QtPassSettings::isUseGrepSearch();
12✔
1811
  ui->grepButton->setVisible(enabled);
12✔
1812
  ui->grepCaseButton->setVisible(enabled);
12✔
1813
  if (!enabled && m_grep.inGrepMode()) {
12✔
1814
    ui->grepButton->setChecked(false);
×
1815
  }
1816
}
12✔
1817

1818
void MainWindow::enableGitButtons(const bool &state) {
15✔
1819
  // Following GNOME guidelines is preferable disable buttons instead of hide
1820
  ui->actionPush->setEnabled(state);
15✔
1821
  ui->actionUpdate->setEnabled(state);
15✔
1822
}
15✔
1823

1824
/**
1825
 * @brief MainWindow::critical critical message popup wrapper.
1826
 * @param title
1827
 * @param msg
1828
 */
1829
void MainWindow::critical(const QString &title, const QString &msg) {
×
1830
  QMessageBox::critical(this, title, msg);
×
1831
}
×
1832

1833
/**
1834
 * @brief Appends processed command output to the output panel.
1835
 *
1836
 * Appends text to the process output text edit, with per-line numbering,
1837
 * optional command prefix, and color coding for errors vs. success.
1838
 * Handles auto-scrolling and line limits.
1839
 *
1840
 * @param output The raw output text from the command.
1841
 * @param isError true if this is error output (stderr).
1842
 * @param linePrefix Optional command name to prefix each line with.
1843
 */
1844
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2✔
1845
                                     const QString &linePrefix) {
1846
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
1847
    return;
1✔
1848
  }
1849

1850
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1851
  for (QString &line : lines) {
2✔
1852
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1853
    // indentation
1854
    line.remove('\r');
1✔
1855
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1856
      line.chop(1);
×
1857
    }
1858
    if (line.isEmpty()) {
1✔
1859
      continue;
×
1860
    }
1861

1862
    m_outputCounter++;
1✔
1863
    QString lineNumber = QString::number(m_outputCounter);
1✔
1864

1865
    QColor textColor =
1866
        isError ? QColor(Qt::red)
1✔
1867
                : m_processOutputEdit->palette().color(QPalette::Text);
2✔
1868
    QString colorHex = textColor.name();
1✔
1869
    // Apply the optional prefix per line so multi-line output stays
1870
    // attributed to its command (e.g. all 3 lines of a `git push` show
1871
    // "git push: ..." rather than only the first).
1872
    QString prefixed =
1873
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2✔
1874
    QString coloredOutput =
1875
        QString("<span style=\"color: %1;\">%2: %3</span>")
1✔
1876
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2✔
1877

1878
    m_processOutputEdit->append(coloredOutput);
1✔
1879
  }
1880

1881
  limitOutputLines();
1✔
1882

1883
  if (m_autoScroll) {
1✔
1884
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
1885
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
1886
  }
1887
}
1888

1889
/**
1890
 * @brief Handles process output from the Pass executor.
1891
 *
1892
 * Called when any non-sensitive process completes. Filters out password-
1893
 * related commands (pass show, insert, etc.) and delegates to
1894
 * appendProcessOutput.
1895
 *
1896
 * @param output The stdout/stderr text from the process.
1897
 * @param isError true if this is error output (stderr).
1898
 * @param pid The process ID identifying which command ran.
1899
 */
1900
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
1901
                                 Enums::PROCESS pid) {
1902
  appendProcessOutput(output, isError, getProcessName(pid));
2✔
1903
}
2✔
1904

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

1959
/**
1960
 * @brief Checks if a process ID represents a sensitive operation whose
1961
 * output should not be shown in the process output panel.
1962
 *
1963
 * Password-related commands (pass show, OTP generate, grep, insert)
1964
 * display their output in other UI areas, so we skip them here.
1965
 *
1966
 * @param pid The process ID to check.
1967
 * @return true if the process is sensitive and should be filtered.
1968
 */
1969
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
1970
  switch (pid) {
×
1971
  case Enums::PASS_SHOW:
1972
  case Enums::PASS_OTP_GENERATE:
1973
  case Enums::PASS_GREP:
1974
  case Enums::PASS_INSERT:
1975
    return true;
1976
  case Enums::GIT_INIT:
1977
  case Enums::GIT_ADD:
1978
  case Enums::GIT_COMMIT:
1979
  case Enums::GIT_RM:
1980
  case Enums::GIT_PULL:
1981
  case Enums::GIT_PUSH:
1982
  case Enums::GIT_MOVE:
1983
  case Enums::GIT_COPY:
1984
  case Enums::PASS_REMOVE:
1985
  case Enums::PASS_INIT:
1986
  case Enums::PASS_MOVE:
1987
  case Enums::PASS_COPY:
1988
  case Enums::GPG_GENKEYS:
1989
  case Enums::PROCESS_COUNT:
1990
  case Enums::INVALID:
1991
    break;
1992
  }
1993
  return false;
×
1994
}
1995

1996
/**
1997
 * @brief Updates the visibility of the process output panel.
1998
 *
1999
 * Shows or hides the process output widget based on the user's
2000
 * showProcessOutput setting.
2001
 */
2002
void MainWindow::updateProcessOutputVisibility() {
×
2003
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
2004
}
×
2005

2006
/**
2007
 * @brief Limits the output panel to max lines, trimming old excess.
2008
 *
2009
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
2010
 * Called after each append to prevent unbounded growth.
2011
 */
2012
void MainWindow::limitOutputLines() {
1✔
2013
  QTextDocument *doc = m_processOutputEdit->document();
1✔
2014
  int excess = doc->blockCount() - MaxOutputLines;
1✔
2015
  if (excess <= 0) {
1✔
2016
    return;
1✔
2017
  }
2018

2019
  QTextCursor cursor(doc);
×
2020
  cursor.movePosition(QTextCursor::Start);
×
2021
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2022
  cursor.removeSelectedText();
×
2023
}
×
2024

2025
/**
2026
 * @brief Clears the process output panel.
2027
 *
2028
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2029
 */
2030
void MainWindow::on_clearOutputButton_clicked() {
×
2031
  m_processOutputEdit->clear();
×
2032
  m_outputCounter = 0;
×
2033
  m_autoScroll = true;
×
2034
}
×
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