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

IJHack / QtPass / 27713458196

17 Jun 2026 07:13PM UTC coverage: 57.077% (-0.002%) from 57.079%
27713458196

push

github

web-flow
refactor(#1511): snapshot AppSettings in MainWindow multi-field read sites (#1560)

* refactor(#1511): snapshot AppSettings in MainWindow multi-field read sites

Replace per-call QtPassSettings getter chains with a single load() snapshot
in each function that reads 2+ settings fields:

- Constructor: useMonospace, noLineWrapping, autoclearPanelSeconds
- applyTextBrowserSettings: useMonospace, noLineWrapping
- config() accepted block: passStore, autoclearPanelSeconds, useTrayIcon (×2)
- restoreWindow: maximized, alwaysOnTop, useTrayIcon (×2), startMinimized
- on_grepButton_toggled: usePass, passStore (×2)
- onGrepFinished: hideContent, useAutoclearPanel
- on_grepResultsList_itemClicked: passStore, hideContent, useAutoclearPanel
- passOtpHandler: useAutoclearPanel (consolidates existing load() call)
- setPassword: passStore (×2, reuses existing load() for PasswordDialog)
- addPassword: passStore (×4, local QString passStore)
- showContextMenu: passStore (×2), usePass, passExecutable, gpgExecutable
- addFolder: passStore (×3), addGPGId
- editPassword: useGit, autoPull
- exportPublicKey: passSigningKey, gpgExecutable
- updateGitButtonVisibility: useGit, gitExecutable, passExecutable

Single-field reads (isShowProcessOutput, isHideOnClose, isUseOtp, etc.)
retain their individual getters per the team convention established in #1559.

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

* fix: return after recursive config() to prevent stale AppSettings snapshot

After the inner config() call handles all post-accept setup, continuing
with the outer snapshot s would apply autoclearPanelSeconds, useTrayIcon,
etc. from before the recursive dialog. Add return to exit cleanly.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

11 of 59 new or added lines in 1 file covered. (18.64%)

10 existing lines in 1 file now uncovered.

3964 of 6945 relevant lines covered (57.08%)

23.69 hits per line

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

27.74
/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✔
NEW
112
      QFont monospace("Monospace");
×
NEW
113
      monospace.setStyleHint(QFont::Monospace);
×
NEW
114
      ui->textBrowser->setFont(monospace);
×
NEW
115
    }
×
116
    if (s.noLineWrapping) {
12✔
NEW
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() {
×
NEW
444
  const AppSettings s = QtPassSettings::load();
×
NEW
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

NEW
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();
×
NEW
496
      const AppSettings s = QtPassSettings::load();
×
NEW
497
      proxyModel.setStore(s.passStore);
×
NEW
498
      ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
499
      deselect();
×
500
      ui->treeView->setCurrentIndex(QModelIndex());
×
501

NEW
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);
×
NEW
509
      clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
×
510
      m_qtPass->setClipboardTimer();
×
511

512
      updateGitButtonVisibility();
×
513
      updateOtpButtonVisibility();
×
514
      updateGrepButtonVisibility();
×
515
      updateProcessOutputVisibility();
×
NEW
516
      if (s.useTrayIcon && m_tray == nullptr) {
×
517
        initTrayIcon();
×
NEW
518
      } else if (!s.useTrayIcon && m_tray != nullptr) {
×
519
        destroyTrayIcon();
×
520
      }
UNCOV
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) {
×
NEW
679
  const AppSettings s = QtPassSettings::load();
×
680
  if (!p_output.isEmpty()) {
×
NEW
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
  }
NEW
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✔
UNCOV
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();
×
NEW
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
  proxyModel.setFilterRegularExpression(regExp);
×
825
  ui->treeView->setRootIndex(
×
826
      proxyModel.rootIndexFor(QtPassSettings::getPassStore()));
×
827

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1150
  QtPassSettings::getPass()->Remove(file, isDir);
×
1151
}
×
1152

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

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

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

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

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

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

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

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

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

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

1284
  ui->lineEdit->clear();
×
1285

1286
  QtPassSettings::setProfile(name);
×
1287

1288
  QtPassSettings::setPassStore(
×
1289
      QtPassSettings::getProfiles().value(name).value("path"));
×
1290
  QtPassSettings::setPassSigningKey(
×
1291
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1292
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1293

1294
  QtPassSettings::getPass()->updateEnv();
×
1295

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

1303
/**
1304
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1305
 * support
1306
 * it
1307
 */
1308
void MainWindow::initTrayIcon() {
×
1309
  m_tray = new TrayIcon(this);
×
1310
  // Setup tray icon
1311

1312
  if (m_tray == nullptr) {
1313
#ifdef QT_DEBUG
1314
    dbg() << "Allocating tray icon failed.";
1315
#endif
1316
    return;
1317
  }
1318

1319
  if (!m_tray->getIsAllocated()) {
×
1320
    destroyTrayIcon();
×
1321
  }
1322
}
1323

1324
/**
1325
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1326
 */
1327
void MainWindow::destroyTrayIcon() {
×
1328
  delete m_tray;
×
1329
  m_tray = nullptr;
×
1330
}
×
1331

1332
/**
1333
 * @brief MainWindow::closeEvent hide or quit
1334
 * @param event
1335
 */
1336
void MainWindow::closeEvent(QCloseEvent *event) {
×
1337
  if (QtPassSettings::isHideOnClose()) {
×
1338
    this->hide();
×
1339
    event->ignore();
1340
  } else {
1341
    m_qtPass->clearClipboard();
×
1342

1343
    QtPassSettings::setGeometry(saveGeometry());
×
1344
    QtPassSettings::setSavestate(saveState());
×
1345
    QtPassSettings::setMaximized(isMaximized());
×
1346
    if (!isMaximized()) {
×
1347
      QtPassSettings::setPos(pos());
×
1348
      QtPassSettings::setSize(size());
×
1349
    }
1350
    event->accept();
1351
  }
1352
}
×
1353

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

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

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

1411
  ui->treeView->setCurrentIndex(index);
×
1412

1413
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1414

1415
  QFileInfo fileOrFolder =
1416
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1417

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

1450
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1451
      contextMenu.addMenu(shareMenu);
×
1452

NEW
1453
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1454
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1455

NEW
1456
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1457
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1458
                                                 QFile(exePath).exists());
×
1459

1460
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1461
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1462
      connect(reencrypt, &QAction::triggered, this,
×
1463
              [this, dirPath]() { reencryptPath(dirPath); });
×
1464

1465
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1466
      exportKey->setEnabled(gpgAvailable);
×
1467
      connect(exportKey, &QAction::triggered, this,
×
1468
              &MainWindow::exportPublicKey);
×
1469

1470
      QAction *addRecipientAction =
1471
          shareMenu->addAction(tr("Add recipient..."));
×
1472
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1473
      connect(addRecipientAction, &QAction::triggered, this,
×
1474
              [this, dirPath]() { addRecipient(dirPath); });
×
1475

1476
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1477
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1478
    }
1479
  }
1480
  contextMenu.exec(globalPos);
×
1481
}
×
1482

1483
/**
1484
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1485
 * password window
1486
 * @param pos
1487
 */
1488
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1489
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
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() {
×
NEW
1511
  const AppSettings s = QtPassSettings::load();
×
1512
  bool ok;
1513
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
NEW
1514
                             proxyModel, s.passStore);
×
1515
  QString newdir = QInputDialog::getText(
1516
      this, tr("New file"),
×
1517
      tr("New Folder: \n(Will be placed in %1 )")
×
NEW
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
  }
NEW
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
  }
×
UNCOV
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()) {
×
NEW
1585
    const AppSettings s = QtPassSettings::load();
×
NEW
1586
    if (s.useGit && s.autoPull) {
×
UNCOV
1587
      onUpdate(true);
×
1588
    }
1589
    setPassword(file, false);
×
UNCOV
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
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1636
            &MainWindow::passwordFromFileToClipboard);
×
1637
    QtPassSettings::getPass()->Show(file);
×
1638
  }
1639
}
×
1640

1641
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1642
  QStringList tokens = text.split('\n');
×
1643
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1644
}
×
1645

1646
/**
1647
 * @brief Displays message in status bar
1648
 *
1649
 * @param msg     text to be displayed
1650
 * @param timeout time for which msg shall be visible
1651
 */
1652
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
2✔
1653
  ui->statusBar->showMessage(msg, timeout);
2✔
1654
}
2✔
1655

1656
/**
1657
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1658
 * @param dir Directory path to re-encrypt
1659
 */
1660
void MainWindow::reencryptPath(const QString &dir) {
×
1661
  QDir checkDir(dir);
×
1662
  if (!checkDir.exists()) {
×
1663
    QMessageBox::critical(this, tr("Error"),
×
1664
                          tr("Directory does not exist: %1").arg(dir));
×
1665
    return;
×
1666
  }
1667

1668
  int ret = QMessageBox::question(
×
1669
      this, tr("Re-encrypt passwords"),
×
1670
      tr("Re-encrypt all passwords in %1?\n\n"
×
1671
         "This will re-encrypt ALL password files in this folder "
1672
         "using the current recipients defined in .gpg-id.\n\n"
1673
         "This may rewrite many files and cannot be undone easily.\n\n"
1674
         "Continue?")
1675
          .arg(QDir(dir).dirName()),
×
1676
      QMessageBox::Yes | QMessageBox::No);
1677

1678
  if (ret != QMessageBox::Yes)
×
1679
    return;
1680

1681
  // Disable preemptively. ImitatePass::reencryptPath emits
1682
  // startReencryptPath asynchronously and the slot would re-run this,
1683
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1684
  startReencryptPath();
×
1685

1686
  QtPassSettings::getImitatePass()->reencryptPath(
×
1687
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1688
}
×
1689

1690
/**
1691
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1692
 */
1693
void MainWindow::startReencryptPath() {
×
1694
  setUiElementsEnabled(false);
×
1695
  ui->treeView->setDisabled(true);
×
1696
}
×
1697

1698
/**
1699
 * @brief MainWindow::endReencryptPath re-enable ui elements
1700
 */
1701
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1702

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

1745
/**
1746
 * @brief MainWindow::addRecipient open the recipient management dialog for
1747
 *        the supplied directory.
1748
 * @param dir Folder whose .gpg-id should be edited.
1749
 *
1750
 * Delegates to UsersDialog so users can tick/untick keys from their
1751
 * keyring as recipients of the folder; importing a foreign key into the
1752
 * keyring still has to happen via gpg (or QtPass settings) first.
1753
 */
1754
void MainWindow::addRecipient(const QString &dir) {
×
1755
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1756
  d.exec();
×
1757
}
×
1758

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

1779
void MainWindow::updateGitButtonVisibility() {
15✔
1780
  const AppSettings s = QtPassSettings::load();
15✔
1781
  if (!s.useGit || (s.gitExecutable.isEmpty() && s.passExecutable.isEmpty())) {
15✔
1782
    enableGitButtons(false);
15✔
1783
  } else {
1784
    enableGitButtons(true);
×
1785
  }
1786
}
15✔
1787

1788
void MainWindow::updateOtpButtonVisibility() {
15✔
1789
#if defined(Q_OS_WIN) || defined(__APPLE__)
1790
  ui->actionOtp->setVisible(false);
1791
#endif
1792
  if (!QtPassSettings::isUseOtp()) {
15✔
1793
    ui->actionOtp->setEnabled(false);
15✔
1794
  } else {
1795
    ui->actionOtp->setEnabled(true);
×
1796
  }
1797
}
15✔
1798

1799
void MainWindow::updateGrepButtonVisibility() {
12✔
1800
  const bool enabled = QtPassSettings::isUseGrepSearch();
12✔
1801
  ui->grepButton->setVisible(enabled);
12✔
1802
  ui->grepCaseButton->setVisible(enabled);
12✔
1803
  if (!enabled && m_grep.inGrepMode()) {
12✔
1804
    ui->grepButton->setChecked(false);
×
1805
  }
1806
}
12✔
1807

1808
void MainWindow::enableGitButtons(const bool &state) {
15✔
1809
  // Following GNOME guidelines is preferable disable buttons instead of hide
1810
  ui->actionPush->setEnabled(state);
15✔
1811
  ui->actionUpdate->setEnabled(state);
15✔
1812
}
15✔
1813

1814
/**
1815
 * @brief MainWindow::critical critical message popup wrapper.
1816
 * @param title
1817
 * @param msg
1818
 */
1819
void MainWindow::critical(const QString &title, const QString &msg) {
×
1820
  QMessageBox::critical(this, title, msg);
×
1821
}
×
1822

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

1840
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1841
  for (QString &line : lines) {
2✔
1842
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1843
    // indentation
1844
    line.remove('\r');
1✔
1845
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1846
      line.chop(1);
×
1847
    }
1848
    if (line.isEmpty()) {
1✔
1849
      continue;
×
1850
    }
1851

1852
    m_outputCounter++;
1✔
1853
    QString lineNumber = QString::number(m_outputCounter);
1✔
1854

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

1868
    m_processOutputEdit->append(coloredOutput);
1✔
1869
  }
1870

1871
  limitOutputLines();
1✔
1872

1873
  if (m_autoScroll) {
1✔
1874
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
1875
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
1876
  }
1877
}
1878

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

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

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

1986
/**
1987
 * @brief Updates the visibility of the process output panel.
1988
 *
1989
 * Shows or hides the process output widget based on the user's
1990
 * showProcessOutput setting.
1991
 */
1992
void MainWindow::updateProcessOutputVisibility() {
×
1993
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
1994
}
×
1995

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

2009
  QTextCursor cursor(doc);
×
2010
  cursor.movePosition(QTextCursor::Start);
×
2011
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2012
  cursor.removeSelectedText();
×
2013
}
×
2014

2015
/**
2016
 * @brief Clears the process output panel.
2017
 *
2018
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2019
 */
2020
void MainWindow::on_clearOutputButton_clicked() {
×
2021
  m_processOutputEdit->clear();
×
2022
  m_outputCounter = 0;
×
2023
  m_autoScroll = true;
×
2024
}
×
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