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

IJHack / QtPass / 27472855302

13 Jun 2026 04:45PM UTC coverage: 55.376% (-0.04%) from 55.418%
27472855302

push

github

web-flow
feat: add "Open in browser" button for web URLs (#1516) (#1517)

* feat: add "Open in browser" button for web URLs (#1516)

Adds a discoverable per-field button that opens an entry's http(s) URL
in the default browser via QDesktopServices::openUrl, next to the
existing copy/QR buttons. Requested in #1516: a non-technical user
wants a visible affordance to reach a login page (autofill is then
handled by a browser extension such as browserpass).

URLs were already clickable inline (#226) via protocolRegex +
QTextBrowser::setOpenExternalLinks; this adds the obvious button most
users expect, and handles the discoverability gap.

Security: the button is gated by a new strict validator
Util::isLaunchableWebUrl() — deliberately stricter than protocolRegex
(which also allows ftp/ssh/webdav, fine for a click-through link but
too loose for a launch primitive). It accepts only:
- no control chars (CR/LF/NUL checked before QUrl parsing),
- valid QUrl with scheme exactly http/https (case-insensitive),
- non-empty host,
- no embedded userinfo (user:pass@host).
It rejects file://, javascript:, data:, ftp/ssh/webdav and scheme-less
inputs. The click handler re-validates before openUrl (defence in
depth). The permissive protocolRegex display path is unchanged.

The button only renders when the whole field value passes the
validator, so it never appears for non-web fields; URLs embedded in
prose keep the existing inline clickable link. No new setting (always
on for valid web URLs).

- src/util.{h,cpp}: Util::isLaunchableWebUrl validator (+ <QUrl>).
- src/mainwindow.cpp: render the button in addToGridLayout, themed
  "applications-internet" icon with bundled open-url.svg fallback,
  tooltip showing the full URL, pointing-hand cursor.
- icons/open-url.svg + resources.qrc: bundled fallback icon.
- tests/auto/util/tst_util.cpp: isLaunchableWebUrl accept/reject cases
  (http/https, uppercase scheme, whitespace; reject ftp/ssh/file/
  javascript/data/scheme-less/creds/CRLF/N... (continued)

11 of 25 new or added lines in 2 files covered. (44.0%)

18 existing lines in 2 files now uncovered.

3739 of 6752 relevant lines covered (55.38%)

36.67 hits per line

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

24.69
/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 "qpushbuttonasqrcode.h"
16
#include "qpushbuttonshowpassword.h"
17
#include "qpushbuttonwithclipboard.h"
18
#include "qtpass.h"
19
#include "qtpasssettings.h"
20
#include "trayicon.h"
21
#include "ui_mainwindow.h"
22
#include "usersdialog.h"
23
#include "util.h"
24
#include <QApplication>
25
#include <QCloseEvent>
26
#include <QDesktopServices>
27
#include <QDialog>
28
#include <QDirIterator>
29
#include <QDockWidget>
30
#include <QFileInfo>
31
#include <QHBoxLayout>
32
#include <QInputDialog>
33
#include <QLabel>
34
#include <QLineEdit>
35
#include <QMenu>
36
#include <QMessageBox>
37
#include <QPushButton>
38
#include <QScrollBar>
39
#include <QShortcut>
40
#include <QTextCursor>
41
#include <QTextEdit>
42
#include <QTimer>
43
#include <QToolButton>
44
#include <QTreeWidget>
45
#include <QUrl>
46
#include <utility>
47

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

63
  m_qtPass = new QtPass(this);
12✔
64

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

71
  model.setNameFilters(QStringList() << "*.gpg");
36✔
72
  model.setNameFilterDisables(false);
12✔
73

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

81
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
12✔
82

83
  QModelIndex rootDir = model.setRootPath(passStore);
12✔
84
  model.fetchMore(rootDir);
12✔
85

86
  proxyModel.setModelAndStore(&model, passStore);
12✔
87
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
12✔
88

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

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

118
  updateProfileBox();
12✔
119

120
  QtPassSettings::getPass()->updateEnv();
12✔
121
  clearPanelTimer.setInterval(MS_PER_SECOND *
12✔
122
                              QtPassSettings::getAutoclearPanelSeconds());
24✔
123
  clearPanelTimer.setSingleShot(true);
12✔
124
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
12✔
125

126
  searchTimer.setInterval(350);
12✔
127
  searchTimer.setSingleShot(true);
12✔
128

129
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
12✔
130

131
  initToolBarButtons();
12✔
132
  initStatusBar();
12✔
133
  initProcessOutputPanel();
12✔
134

135
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
12✔
136
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
24✔
137
            // Never route potentially-secret output through the panel:
138
            // - PASS_SHOW / PASS_OTP_GENERATE go via dedicated signals to
139
            //   the main text browser (which clears on a timer).
140
            // - PASS_GREP returns lines from password files; #252 must
141
            //   not leak those into a long-lived panel.
142
            // - PASS_INSERT's stdin is the password; stdout normally
143
            //   carries gpg/git progress only, but exclude defensively
144
            //   in case a future code path uses --echo or similar.
145
            if (isSensitiveProcess(pid)) {
×
146
              return;
147
            }
148
            if (!out.isEmpty()) {
×
149
              onProcessOutput(out, false, pid);
×
150
            }
151
            if (!err.isEmpty()) {
×
152
              onProcessOutput(err, true, pid);
×
153
            }
154
          });
155

156
  ui->lineEdit->setClearButtonEnabled(true);
12✔
157
  updateGrepButtonVisibility();
12✔
158

159
  setUiElementsEnabled(true);
12✔
160

161
  ui->lineEdit->setText(searchText);
12✔
162

163
  if (!m_qtPass->init()) {
12✔
164
    // no working config so this should just quit
165
    QApplication::quit();
×
166
    return;
167
  }
168

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

178
MainWindow::~MainWindow() { delete m_qtPass; }
36✔
179

180
/**
181
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
182
 * box and sets focus to it. Allows for easy searching, called at application
183
 * start and when receiving empty message in MainWindow::messageAvailable when
184
 * compiled with SINGLE_APP=1 (default).
185
 */
186
void MainWindow::focusInput() {
×
187
  // Resolve the QLineEdit through the live widget tree rather than the
188
  // cached `ui->lineEdit` pointer.
189
  //
190
  // On a fresh-config first launch the constructor calls
191
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
192
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
193
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
194
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
195
  // we observed the QLineEdit attached to the centralWidget gets
196
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
197
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
198
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
199
  // walks the current hierarchy and returns null cleanly when the
200
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
201
  // use-after-free.
202
  if (!isVisible()) {
×
203
    return;
204
  }
205
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
×
206
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
×
207
    return;
208
  }
209
  lineEdit->selectAll();
×
210
  lineEdit->setFocus();
×
211
  // Only mark the first-show focus pulse as done once it's actually
212
  // landed; setting it eagerly in showEvent() would consume the
213
  // one-shot if focusInput returned early (mid-rebuild widget state)
214
  // and we'd never retry.
215
  m_initialShowDone = true;
×
216
}
217

218
/**
219
 * @brief MainWindow::changeEvent sets focus to the search box
220
 * @param event
221
 */
222
void MainWindow::changeEvent(QEvent *event) {
12✔
223
  QWidget::changeEvent(event);
12✔
224
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
12✔
225
      isVisible()) {
226
    // Defer one event-loop tick so the synchronous activation dispatch
227
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
228
    // unwinds before we touch widget state — calling `focusInput()`
229
    // inline from this stack has segfaulted in past iterations because
230
    // mid-rebuild ui state isn't fully wired up yet.
231
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
×
232
                              Qt::QueuedConnection);
233
  }
234
}
12✔
235

236
/**
237
 * @brief First-show hook: run the initial focusInput() pulse once the
238
 *        window is actually mapped. The widget's internal data is fully
239
 *        initialised by this point, so QLineEdit::selectAll() is safe.
240
 * @param event Show event passed to the base class.
241
 */
242
void MainWindow::showEvent(QShowEvent *event) {
×
243
  QMainWindow::showEvent(event);
×
244
  if (m_initialShowDone) {
×
245
    return;
246
  }
247
  // Queue the focus pulse for the next event-loop tick so the platform
248
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
249
  // from the config wizard path) settle before we look up the line
250
  // edit. The `m_initialShowDone` latch is set inside focusInput()
251
  // *after* it actually focuses, so a transient failed lookup just
252
  // re-queues on the next show rather than silently dropping.
253
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
×
254
                            Qt::QueuedConnection);
255
}
256

257
/**
258
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
259
 */
260
void MainWindow::initToolBarButtons() {
12✔
261
  connect(ui->actionAddPassword, &QAction::triggered, this,
12✔
262
          &MainWindow::addPassword);
12✔
263
  connect(ui->actionAddFolder, &QAction::triggered, this,
12✔
264
          &MainWindow::addFolder);
12✔
265
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
12✔
266
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
12✔
267
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
12✔
268
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
12✔
269
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
12✔
270
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
12✔
271
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
12✔
272

273
  ui->actionAddPassword->setIcon(
12✔
274
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
48✔
275
  ui->actionAddFolder->setIcon(
12✔
276
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
48✔
277
  ui->actionEdit->setIcon(QIcon::fromTheme(
12✔
278
      "document-properties", QIcon(":/icons/document-properties.svg")));
36✔
279
  ui->actionDelete->setIcon(
12✔
280
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
48✔
281
  ui->actionPush->setIcon(
12✔
282
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
48✔
283
  ui->actionUpdate->setIcon(
12✔
284
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
48✔
285
  ui->actionUsers->setIcon(QIcon::fromTheme(
12✔
286
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
36✔
287
  ui->actionConfig->setIcon(QIcon::fromTheme(
12✔
288
      "applications-system", QIcon(":/icons/applications-system.svg")));
24✔
289
}
12✔
290

291
/**
292
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
293
 */
294
void MainWindow::initStatusBar() {
12✔
295
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
24✔
296

297
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
24✔
298
                     .scaledToHeight(statusBar()->height());
12✔
299
  auto *logoApp = new QLabel(statusBar());
12✔
300
  logoApp->setPixmap(logo);
12✔
301
  statusBar()->addPermanentWidget(logoApp);
12✔
302
}
12✔
303

304
/**
305
 * @brief Build the process-output panel as a bottom QDockWidget.
306
 *
307
 * The panel is constructed programmatically rather than declared in
308
 * mainwindow.ui: uic only places QMainWindow's top-level children into
309
 * the centralWidget / statusBar / menuBar / toolBars / dock-widget
310
 * slots, and the previous home (statusBar()->addPermanentWidget()) made
311
 * an 80–150 px tall QTextEdit sit inside what is otherwise a thin
312
 * status row. A QDockWidget at the bottom dock area is the conventional
313
 * place for an IDE-style output console, and it gives users
314
 * detach/move for free.
315
 */
316
void MainWindow::initProcessOutputPanel() {
12✔
317
  m_processOutputWidget = new QWidget;
12✔
318
  m_processOutputWidget->setObjectName(QStringLiteral("processOutputWidget"));
24✔
319
  auto *outputLayout = new QHBoxLayout(m_processOutputWidget);
12✔
320
  outputLayout->setObjectName(QStringLiteral("processOutputLayout"));
24✔
321
  outputLayout->setContentsMargins(0, 0, 0, 0);
12✔
322
  m_clearOutputButton = new QToolButton(m_processOutputWidget);
12✔
323
  m_clearOutputButton->setObjectName(QStringLiteral("clearOutputButton"));
24✔
324
  m_clearOutputButton->setText(tr("Clear"));
12✔
325
  m_clearOutputButton->setToolTip(tr("Clear output"));
12✔
326
  outputLayout->addWidget(m_clearOutputButton);
12✔
327
  m_processOutputEdit = new QTextEdit(m_processOutputWidget);
12✔
328
  m_processOutputEdit->setObjectName(QStringLiteral("processOutputEdit"));
24✔
329
  m_processOutputEdit->setReadOnly(true);
12✔
330
  m_processOutputEdit->setAcceptRichText(false);
12✔
331
  outputLayout->addWidget(m_processOutputEdit);
12✔
332

333
  m_processOutputDock = new QDockWidget(tr("Process Output"), this);
12✔
334
  m_processOutputDock->setObjectName(QStringLiteral("processOutputDock"));
24✔
335
  m_processOutputDock->setFeatures(QDockWidget::DockWidgetMovable |
12✔
336
                                   QDockWidget::DockWidgetFloatable);
337
  m_processOutputDock->setAllowedAreas(Qt::BottomDockWidgetArea |
12✔
338
                                       Qt::TopDockWidgetArea);
339
  m_processOutputDock->setWidget(m_processOutputWidget);
12✔
340
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutputDock);
12✔
341
  // setVisible after addDockWidget so our explicit preference wins
342
  // even if QMainWindow applies any cached state when the dock is
343
  // attached. restoreWindow() runs before this method (it's called
344
  // from the QtPass ctor, which is constructed at the top of the
345
  // MainWindow ctor), so the saved layout has already been processed
346
  // by the time we get here.
347
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
12✔
348

349
  connect(m_clearOutputButton, &QToolButton::clicked, this,
12✔
350
          &MainWindow::on_clearOutputButton_clicked);
12✔
351

352
  // Hysteresis: while the user is actively dragging the slider, don't
353
  // touch m_autoScroll on every tick — a brief overshoot at maximum
354
  // would silently re-arm auto-scroll without an explicit release. Only
355
  // commit on slider release. Wheel/keyboard scroll never sets
356
  // isSliderDown(), so they still update immediately.
357
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
12✔
358
          this, [this]() {
12✔
359
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
360
            if (sb->isSliderDown())
×
361
              return;
362
            m_autoScroll = sb->value() >= sb->maximum();
×
363
          });
364
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderReleased,
12✔
365
          this, [this]() {
12✔
366
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
367
            m_autoScroll = sb->value() >= sb->maximum();
×
368
          });
×
369
}
12✔
370

371
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
372
  return ui->treeView->currentIndex();
×
373
}
374

375
void MainWindow::cleanKeygenDialog() {
1✔
376
  if (m_keygenDialog != nullptr) {
1✔
377
    m_keygenDialog->close();
×
378
  }
379
  m_keygenDialog = nullptr;
1✔
380
}
1✔
381

382
/**
383
 * @brief Displays the given text in the main window text browser, optionally
384
 * marking it as an error and/or rendering it as HTML.
385
 * @example
386
 * MainWindow window;
387
 * window.flashText("Operation completed.", false, false);
388
 *
389
 * @param const QString &text - The text content to display.
390
 * @param const bool isError - If true, sets the text color to red before
391
 * displaying the text.
392
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
393
 * the existing HTML content.
394
 * @return void - No return value.
395
 */
396
void MainWindow::flashText(const QString &text, const bool isError,
3✔
397
                           const bool isHtml) {
398
  if (isError) {
3✔
399
    ui->textBrowser->setTextColor(Qt::red);
1✔
400
  }
401

402
  if (isHtml) {
3✔
403
    QString _text = text;
404
    if (!ui->textBrowser->toPlainText().isEmpty()) {
2✔
405
      _text = ui->textBrowser->toHtml() + _text;
2✔
406
    }
407
    ui->textBrowser->setHtml(_text);
1✔
408
  } else {
409
    ui->textBrowser->setText(text);
2✔
410
  }
411
}
3✔
412

413
/**
414
 * @brief MainWindow::config pops up the configuration screen and handles all
415
 * inter-window communication
416
 */
417
void MainWindow::applyTextBrowserSettings() {
×
418
  if (QtPassSettings::isUseMonospace()) {
×
419
    QFont monospace("Monospace");
×
420
    monospace.setStyleHint(QFont::Monospace);
×
421
    ui->textBrowser->setFont(monospace);
×
422
  } else {
×
423
    ui->textBrowser->setFont(QFont());
×
424
  }
425

426
  if (QtPassSettings::isNoLineWrapping()) {
×
427
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
428
  } else {
429
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
430
  }
431
}
×
432

433
void MainWindow::applyWindowFlagsSettings() {
×
434
  if (QtPassSettings::isAlwaysOnTop()) {
×
435
    Qt::WindowFlags flags = windowFlags();
436
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
437
  } else {
438
    this->setWindowFlags(Qt::Window);
×
439
  }
440
  this->show();
×
441
}
×
442

443
/**
444
 * @brief Opens and processes the application configuration dialog, then applies
445
 * any accepted settings.
446
 * @example
447
 * config();
448
 *
449
 * @return void - This function does not return a value.
450
 */
451
void MainWindow::config() {
×
452
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
453
  d->setModal(true);
×
454
  // Automatically default to pass if it's available
455
  if (m_qtPass->isFreshStart() &&
×
456
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
457
    QtPassSettings::setUsePass(true);
×
458
  }
459

460
  if (m_qtPass->isFreshStart()) {
×
461
    d->wizard(); // run initial setup wizard for first-time configuration
×
462
  }
463
  if (d->exec()) {
×
464
    if (d->result() == QDialog::Accepted) {
×
465
      applyTextBrowserSettings();
×
466
      applyWindowFlagsSettings();
×
467

468
      updateProfileBox();
×
469
      const QString passStore = QtPassSettings::getPassStore();
×
470
      proxyModel.setStore(passStore);
×
471
      ui->treeView->setRootIndex(
×
472
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
473
      deselect();
×
474
      ui->treeView->setCurrentIndex(QModelIndex());
×
475

476
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
477
        config();
×
478
      }
479
      QtPassSettings::getPass()->updateEnv();
×
480
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
481
                                  QtPassSettings::getAutoclearPanelSeconds());
×
482
      m_qtPass->setClipboardTimer();
×
483

484
      updateGitButtonVisibility();
×
485
      updateOtpButtonVisibility();
×
486
      updateGrepButtonVisibility();
×
487
      updateProcessOutputVisibility();
×
488
      if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
×
489
        initTrayIcon();
×
490
      } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
×
491
        destroyTrayIcon();
×
492
      }
493
    }
494

495
    m_qtPass->setFreshStart(false);
×
496
  }
497
}
×
498

499
/**
500
 * @brief MainWindow::onUpdate do a git pull
501
 */
502
void MainWindow::onUpdate(bool block) {
×
503
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
504
  if (block) {
×
505
    QtPassSettings::getPass()->GitPull_b();
×
506
  } else {
507
    QtPassSettings::getPass()->GitPull();
×
508
  }
509
}
×
510

511
/**
512
 * @brief MainWindow::onPush do a git push
513
 */
514
void MainWindow::onPush() {
×
515
  if (QtPassSettings::isUseGit()) {
×
516
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
517
    QtPassSettings::getPass()->GitPush();
×
518
  }
519
}
×
520

521
/**
522
 * @brief MainWindow::getFile get the selected file path
523
 * @param index
524
 * @param forPass returns relative path without '.gpg' extension
525
 * @return path
526
 * @return
527
 */
528
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
529
  if (!index.isValid() ||
×
530
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
531
    return {};
532
  }
533
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
534
  if (forPass) {
×
535
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
536
    filePath.replace(Util::endsWithGpg(), "");
×
537
  }
538
  return filePath;
539
}
540

541
/**
542
 * @brief MainWindow::on_treeView_clicked read the selected password file
543
 * @param index
544
 */
545
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
546
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
547
  m_currentDir =
548
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
549
  // Clear any previously cached clipped text before showing new password
550
  m_qtPass->clearClippedText();
×
551
  QString file = getFile(index, true);
×
552
  ui->passwordName->setText(file);
×
553
  if (!file.isEmpty() && !cleared) {
×
554
    QtPassSettings::getPass()->Show(file);
×
555
  } else {
556
    clearPanel(false);
×
557
    ui->actionEdit->setEnabled(false);
×
558
    ui->actionDelete->setEnabled(true);
×
559
  }
560
}
×
561

562
/**
563
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
564
 * TreeViewItem, open the edit Window
565
 * @param index
566
 */
567
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
568
  QFileInfo fileOrFolder =
569
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
570

571
  if (fileOrFolder.isFile()) {
×
572
    editPassword(getFile(index, true));
×
573
  }
574
}
×
575

576
/**
577
 * @brief MainWindow::deselect clear the selection, password and copy buffer
578
 */
579
void MainWindow::deselect() {
1✔
580
  m_currentDir = "";
1✔
581
  m_qtPass->clearClipboard();
1✔
582
  ui->treeView->clearSelection();
1✔
583
  ui->actionEdit->setEnabled(false);
1✔
584
  ui->actionDelete->setEnabled(false);
1✔
585
  ui->passwordName->setText("");
1✔
586
  clearPanel(false);
1✔
587
}
1✔
588

589
void MainWindow::executeWrapperStarted() {
×
590
  clearTemplateWidgets();
×
591
  ui->textBrowser->clear();
×
592
  setUiElementsEnabled(false);
×
593
  clearPanelTimer.stop();
×
594
  if (QtPassSettings::isShowProcessOutput()) {
×
595
    m_processOutputDock->setVisible(true);
×
596
  }
597
}
×
598

599
/**
600
 * @brief Handles displaying parsed password entry content in the main window.
601
 * @example
602
 * void result = MainWindow::passShowHandler(p_output);
603
 * // Updates the UI with parsed fields and emits
604
 * passShowHandlerFinished(output)
605
 *
606
 * @param p_output - The raw output text containing the password entry data.
607
 * @return void - This function does not return a value.
608
 */
609
void MainWindow::passShowHandler(const QString &p_output) {
×
610
  QStringList templ = QtPassSettings::isUseTemplate()
×
611
                          ? QtPassSettings::getPassTemplate().split("\n")
×
612
                          : QStringList();
×
613
  bool allFields =
614
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
615
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
616
  QString output = p_output;
617
  QString password = fileContent.getPassword();
×
618

619
  // set clipped text
620
  m_qtPass->setClippedText(password, p_output);
×
621

622
  // first clear the current view:
623
  clearTemplateWidgets();
×
624

625
  // show what is needed:
626
  if (QtPassSettings::isHideContent()) {
×
627
    output = "***" + tr("Content hidden") + "***";
×
628
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
629
    if (!password.isEmpty()) {
×
630
      // set the password, it is hidden if needed in addToGridLayout
631
      addToGridLayout(0, tr("Password"), password);
×
632
    }
633

634
    NamedValues namedValues = fileContent.getNamedValues();
×
635
    for (int j = 0; j < namedValues.length(); ++j) {
×
636
      const NamedValue &nv = namedValues.at(j);
637
      addToGridLayout(j + 1, nv.name, nv.value);
×
638
    }
639
    if (ui->gridLayout->count() == 0) {
×
640
      ui->verticalLayoutPassword->setSpacing(0);
×
641
    } else {
642
      ui->verticalLayoutPassword->setSpacing(6);
×
643
    }
644

645
    output = fileContent.getRemainingDataForDisplay();
×
646
  }
647

648
  if (QtPassSettings::isUseAutoclearPanel()) {
×
649
    clearPanelTimer.start();
×
650
  }
651

652
  emit passShowHandlerFinished(output);
×
653
  setUiElementsEnabled(true);
×
654
}
×
655

656
/**
657
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
658
 * and updating the UI state.
659
 * @example
660
 * void MainWindow::passOtpHandler(const QString &p_output);
661
 *
662
 * @param const QString &p_output - The OTP code text to process; if empty, an
663
 * error message is shown instead.
664
 * @return void - This function does not return a value.
665
 */
666
void MainWindow::passOtpHandler(const QString &p_output) {
×
667
  if (!p_output.isEmpty()) {
×
668
    addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
×
669
    m_qtPass->copyTextToClipboard(p_output);
×
670
    showStatusMessage(tr("OTP code copied to clipboard"));
×
671
  } else {
672
    flashText(tr("No OTP code found in this password entry"), true);
×
673
  }
674
  if (QtPassSettings::isUseAutoclearPanel()) {
×
675
    clearPanelTimer.start();
×
676
  }
677
  setUiElementsEnabled(true);
×
678
}
×
679

680
/**
681
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
682
 */
683
void MainWindow::clearPanel(bool notify) {
1✔
684
  while (ui->gridLayout->count() > 0) {
1✔
685
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
686
    delete item->widget();
×
687
    delete item;
×
688
  }
689
  const bool grepWasVisible = ui->grepResultsList->isVisible();
1✔
690
  ui->grepResultsList->clear();
1✔
691
  if (grepWasVisible) {
1✔
692
    ui->grepResultsList->setVisible(false);
×
693
    ui->treeView->setVisible(true);
×
694
    if (m_grepMode) {
×
695
      m_grepMode = false;
×
696
      ui->grepButton->blockSignals(true);
×
697
      ui->grepButton->setChecked(false);
×
698
      ui->grepButton->blockSignals(false);
×
699
      ui->lineEdit->blockSignals(true);
×
700
      ui->lineEdit->clear();
×
701
      ui->lineEdit->blockSignals(false);
×
702
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
703
    }
704
  }
705
  if (notify) {
1✔
706
    QString output = "***" + tr("Password and Content hidden") + "***";
×
707
    ui->textBrowser->setHtml(output);
×
708
  } else {
709
    ui->textBrowser->setHtml("");
2✔
710
  }
711
}
1✔
712

713
/**
714
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
715
 * elements
716
 * @param state
717
 */
718
void MainWindow::setUiElementsEnabled(bool state) {
15✔
719
  ui->treeView->setEnabled(state);
15✔
720
  ui->lineEdit->setEnabled(state);
15✔
721
  ui->lineEdit->installEventFilter(this);
15✔
722
  ui->actionAddPassword->setEnabled(state);
15✔
723
  ui->actionAddFolder->setEnabled(state);
15✔
724
  ui->actionUsers->setEnabled(state);
15✔
725
  ui->actionConfig->setEnabled(state);
15✔
726
  // is a file selected?
727
  state &= ui->treeView->currentIndex().isValid();
30✔
728
  ui->actionDelete->setEnabled(state);
15✔
729
  ui->actionEdit->setEnabled(state);
15✔
730
  updateGitButtonVisibility();
15✔
731
  updateOtpButtonVisibility();
15✔
732
}
15✔
733

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

756
  if (QtPassSettings::isAlwaysOnTop()) {
12✔
757
    Qt::WindowFlags flags = windowFlags();
758
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
759
    show();
×
760
  }
761

762
  if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
24✔
763
    initTrayIcon();
×
764
    if (QtPassSettings::isStartMinimized()) {
×
765
      // since we are still in constructor, can't directly hide
766
      QTimer::singleShot(10, this, SLOT(hide()));
×
767
    }
768
  } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
24✔
769
    destroyTrayIcon();
×
770
  }
771
}
12✔
772

773
/**
774
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
775
 */
776
void MainWindow::onConfig() { config(); }
×
777

778
/**
779
 * @brief Executes when the string in the search box changes, collapses the
780
 * TreeView
781
 * @param arg1
782
 */
783
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
784
  if (m_grepMode)
×
785
    return;
786
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
787
  ui->treeView->expandAll();
×
788
  clearPanel(false);
×
789
  ui->passwordName->setText("");
×
790
  ui->actionEdit->setEnabled(false);
×
791
  ui->actionDelete->setEnabled(false);
×
792
  searchTimer.start();
×
793
}
794

795
/**
796
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
797
 * time from two keypresses is elapsed
798
 */
799
void MainWindow::onTimeoutSearch() {
×
800
  QString query = ui->lineEdit->text();
×
801

802
  if (query.isEmpty()) {
×
803
    ui->treeView->collapseAll();
×
804
    deselect();
×
805
  }
806

807
  query.replace(QStringLiteral(" "), ".*");
×
808
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
809
  proxyModel.setFilterRegularExpression(regExp);
×
810
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
811
      model.setRootPath(QtPassSettings::getPassStore())));
×
812

813
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
814
    selectFirstFile();
×
815
  } else {
816
    ui->actionEdit->setEnabled(false);
×
817
    ui->actionDelete->setEnabled(false);
×
818
  }
819
}
×
820

821
/**
822
 * @brief MainWindow::on_lineEdit_returnPressed get searching
823
 *
824
 * Select the first possible file in the tree
825
 */
826
void MainWindow::on_lineEdit_returnPressed() {
×
827
#ifdef QT_DEBUG
828
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
829
#endif
830

831
  if (m_grepMode) {
×
832
    const QString query = ui->lineEdit->text();
×
833
    if (!query.isEmpty()) {
×
834
      m_grepCancelled = false;
×
835
      ui->grepResultsList->clear();
×
836
      ui->statusBar->showMessage(tr("Searching…"));
×
837
      if (!m_grepBusy) {
×
838
        m_grepBusy = true;
×
839
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
840
      }
841
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
842
    } else {
843
      m_grepCancelled = true;
×
844
      if (m_grepBusy) {
×
845
        m_grepBusy = false;
×
846
        QApplication::restoreOverrideCursor();
×
847
      }
848
      ui->grepResultsList->clear();
×
849
      ui->grepResultsList->setVisible(false);
×
850
      ui->treeView->setVisible(true);
×
851
    }
852
    return;
853
  }
854

855
  if (proxyModel.rowCount() > 0) {
×
856
    selectFirstFile();
×
857
    on_treeView_clicked(ui->treeView->currentIndex());
×
858
  }
859
}
860

861
/**
862
 * @brief Toggle grep (content search) mode.
863
 */
864
void MainWindow::on_grepButton_toggled(bool checked) {
×
865
  m_grepMode = checked;
×
866
  if (checked) {
×
867
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
868
    ui->lineEdit->clear();
×
869
    searchTimer.stop();
×
870
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
871
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
872
        model.setRootPath(QtPassSettings::getPassStore())));
×
873
    ui->grepResultsList->setVisible(false);
×
874
    // Keep treeView visible until results arrive
875
  } else {
876
    if (m_grepBusy) {
×
877
      m_grepBusy = false;
×
878
      m_grepCancelled = true;
×
879
      QApplication::restoreOverrideCursor();
×
880
    }
881
    searchTimer.stop();
×
882
    ui->lineEdit->blockSignals(true);
×
883
    ui->lineEdit->clear();
×
884
    ui->lineEdit->blockSignals(false);
×
885
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
886
    ui->grepResultsList->clear();
×
887
    ui->grepResultsList->setVisible(false);
×
888
    ui->treeView->setVisible(true);
×
889
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
890
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
891
        model.setRootPath(QtPassSettings::getPassStore())));
×
892
  }
893
}
×
894

895
/**
896
 * @brief Display grep results in grepResultsList.
897
 */
898
void MainWindow::onGrepFinished(
×
899
    const QList<QPair<QString, QStringList>> &results) {
900
  if (m_grepBusy) {
×
901
    m_grepBusy = false;
×
902
    QApplication::restoreOverrideCursor();
×
903
  }
904
  if (m_grepCancelled) {
×
905
    m_grepCancelled = false;
×
906
    return;
×
907
  }
908
  setUiElementsEnabled(true);
×
909
  if (!m_grepMode)
×
910
    return;
911
  ui->grepResultsList->clear();
×
912
  if (results.isEmpty()) {
×
913
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
914
    ui->grepResultsList->setVisible(false);
×
915
    ui->treeView->setVisible(true);
×
916
    return;
×
917
  }
918
  const bool hideContent = QtPassSettings::isHideContent();
×
919
  int totalLines = 0;
920
  for (const auto &pair : results) {
×
921
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
922
    entryItem->setText(0, pair.first);
×
923
    entryItem->setData(0, Qt::UserRole, pair.first);
×
924
    for (const QString &line : pair.second) {
×
925
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
926
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
927
                                       : line);
928
      lineItem->setData(0, Qt::UserRole, pair.first);
×
929
      ++totalLines;
×
930
    }
931
  }
932
  ui->grepResultsList->expandAll();
×
933
  ui->treeView->setVisible(false);
×
934
  ui->grepResultsList->setVisible(true);
×
935
  ui->statusBar->showMessage(
×
936
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
937
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
938
      3000);
939
  if (QtPassSettings::isUseAutoclearPanel())
×
940
    clearPanelTimer.start();
×
941
}
942

943
/**
944
 * @brief Navigate to the password entry when a grep result is clicked.
945
 */
946
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
947
                                                int /*column*/) {
948
  const QString entry = item->data(0, Qt::UserRole).toString();
×
949
  if (entry.isEmpty())
×
950
    return;
951
  const QString fullPath = QDir::cleanPath(
952
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
953
  QModelIndex srcIndex = model.index(fullPath);
×
954
  if (!srcIndex.isValid())
955
    return;
956
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
957
  if (!proxyIndex.isValid())
958
    return;
959
  ui->treeView->setCurrentIndex(proxyIndex);
×
960
  on_treeView_clicked(proxyIndex);
×
961
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
962
    ui->grepResultsList->clear();
×
963
  ui->grepResultsList->setVisible(false);
×
964
  ui->treeView->setVisible(true);
×
965
  ui->treeView->scrollTo(proxyIndex);
×
966
  ui->treeView->setFocus();
×
967
}
968

969
/**
970
 * @brief MainWindow::selectFirstFile select the first possible file in the
971
 * tree
972
 */
973
void MainWindow::selectFirstFile() {
×
974
  QModelIndex index = proxyModel.mapFromSource(
×
975
      model.setRootPath(QtPassSettings::getPassStore()));
×
976
  index = firstFile(index);
×
977
  ui->treeView->setCurrentIndex(index);
×
978
}
×
979

980
/**
981
 * @brief MainWindow::firstFile return location of first possible file
982
 * @param parentIndex
983
 * @return QModelIndex
984
 */
985
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
986
  QModelIndex index = parentIndex;
×
987
  int numRows = proxyModel.rowCount(parentIndex);
×
988
  for (int row = 0; row < numRows; ++row) {
×
989
    index = proxyModel.index(row, 0, parentIndex);
×
990
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
991
      return index;
×
992
    }
993
    if (proxyModel.hasChildren(index)) {
×
994
      return firstFile(index);
×
995
    }
996
  }
997
  return index;
×
998
}
999

1000
/**
1001
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1002
 * the password store and warn the user.
1003
 *
1004
 * Used before file/folder creation, move, and rename to stop user-typed
1005
 * names like "../../etc/passwd" or absolute paths from escaping the
1006
 * configured store root via the input dialogs.
1007
 *
1008
 * @param candidate Absolute candidate path to validate.
1009
 * @return true if the path is inside the password store; false otherwise (a
1010
 * warning dialog is shown in that case).
1011
 */
1012
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1013
  if (Util::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1014
    return true;
1015
  }
1016
  QMessageBox::warning(this, tr("Invalid name"),
×
1017
                       tr("That name would resolve outside the password "
×
1018
                          "store. Please choose a different name."));
1019
  return false;
×
1020
}
1021

1022
/**
1023
 * @brief MainWindow::setPassword open passworddialog
1024
 * @param file which pgp file
1025
 * @param isNew insert (not update)
1026
 */
1027
void MainWindow::setPassword(const QString &file, bool isNew) {
×
1028
  PasswordDialog d(file, isNew, this);
×
1029

1030
  if (isNew) {
×
1031
    QString storePath = QtPassSettings::getPassStore();
×
1032
    QString folder =
1033
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1034
    if (folder.isEmpty()) {
×
1035
      folder = storePath;
×
1036
    }
1037
    QHash<QString, QStringList> templates = Util::readTemplates(storePath);
×
1038
    if (!templates.isEmpty()) {
1039
      QString defaultTemplate = Util::getFolderTemplate(folder, storePath);
×
1040
      d.setAvailableTemplates(templates, defaultTemplate);
×
1041
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
1042
                    [&d]() { d.cycleTemplate(); });
×
1043
    }
1044
  }
×
1045

1046
  if (!d.exec()) {
×
1047
    ui->treeView->setFocus();
×
1048
  }
1049
}
×
1050

1051
/**
1052
 * @brief MainWindow::addPassword add a new password by showing a
1053
 * number of dialogs.
1054
 */
1055
void MainWindow::addPassword() {
×
1056
  bool ok;
1057
  QString dir =
1058
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1059
  QString file =
1060
      QInputDialog::getText(this, tr("New file"),
×
1061
                            tr("New password file: \n(Will be placed in %1 )")
×
1062
                                .arg(QtPassSettings::getPassStore() +
×
1063
                                     Util::getDir(ui->treeView->currentIndex(),
×
1064
                                                  true, model, proxyModel)),
1065
                            QLineEdit::Normal, "", &ok);
×
1066
  if (!ok || file.isEmpty()) {
×
1067
    return;
1068
  }
1069
  file = dir + file;
×
1070
  if (!confirmPathInStore(QtPassSettings::getPassStore() + file)) {
×
1071
    return;
1072
  }
1073
  setPassword(file);
×
1074
}
1075

1076
/**
1077
 * @brief MainWindow::onDelete remove password, if you are
1078
 * sure.
1079
 */
1080
void MainWindow::onDelete() {
×
1081
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1082
  if (!currentIndex.isValid()) {
1083
    // This fixes https://github.com/IJHack/QtPass/issues/556
1084
    // Otherwise the entire password directory would be deleted if
1085
    // nothing is selected in the tree view.
1086
    return;
×
1087
  }
1088

1089
  QFileInfo fileOrFolder =
1090
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1091
  QString file = "";
×
1092
  bool isDir = false;
1093

1094
  if (fileOrFolder.isFile()) {
×
1095
    file = getFile(ui->treeView->currentIndex(), true);
×
1096
  } else {
1097
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1098
    isDir = true;
1099
  }
1100

1101
  QString dirMessage = tr(" and the whole content?");
1102
  if (isDir) {
×
1103
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1104
                    QDirIterator::Subdirectories);
×
1105
    bool okDir = true;
1106
    while (it.hasNext() && okDir) {
×
1107
      it.next();
×
1108
      if (QFileInfo(it.filePath()).isFile()) {
×
1109
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1110
          okDir = false;
1111
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1112
                          "there are unexpected files in the given folder, "
1113
                          "check them before continue.</strong>");
1114
        }
1115
      }
1116
    }
1117
  }
×
1118

1119
  if (QMessageBox::question(
×
1120
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1121
          tr("Are you sure you want to delete %1%2?")
×
1122
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1123
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1124
    return;
1125
  }
1126

1127
  QtPassSettings::getPass()->Remove(file, isDir);
×
1128
}
×
1129

1130
/**
1131
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
1132
 */
1133
void MainWindow::onOtp() {
×
1134
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1135
  if (!file.isEmpty()) {
×
1136
    if (QtPassSettings::isUseOtp()) {
×
1137
      setUiElementsEnabled(false);
×
1138
      QtPassSettings::getPass()->OtpGenerate(file);
×
1139
    }
1140
  } else {
1141
    flashText(tr("No password selected for OTP generation"), true);
×
1142
  }
1143
}
×
1144

1145
/**
1146
 * @brief MainWindow::onEdit try and edit (selected) password.
1147
 */
1148
void MainWindow::onEdit() {
×
1149
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1150
  editPassword(file);
×
1151
}
×
1152

1153
/**
1154
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1155
 * @param dir folder to edit users for.
1156
 */
1157
void MainWindow::userDialog(const QString &dir) {
×
1158
  if (!dir.isEmpty()) {
×
1159
    m_currentDir = dir;
×
1160
  }
1161
  onUsers();
×
1162
}
×
1163

1164
/**
1165
 * @brief MainWindow::onUsers edit users for the current
1166
 * folder,
1167
 * gets lists and opens UserDialog.
1168
 */
1169
void MainWindow::onUsers() {
×
1170
  QString dir =
1171
      m_currentDir.isEmpty()
1172
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
1173
          : m_currentDir;
×
1174

1175
  UsersDialog d(dir, this);
×
1176
  if (!d.exec()) {
×
1177
    ui->treeView->setFocus();
×
1178
  }
1179
}
×
1180

1181
/**
1182
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1183
 * @param message
1184
 */
1185
void MainWindow::messageAvailable(const QString &message) {
×
1186
  show();
×
1187
  raise();
×
1188
  if (message.isEmpty()) {
×
1189
    focusInput();
×
1190
  } else {
1191
    ui->treeView->expandAll();
×
1192
    ui->lineEdit->setText(message);
×
1193
    on_lineEdit_returnPressed();
×
1194
  }
1195
}
×
1196

1197
/**
1198
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1199
 * @param batch
1200
 * @param keygenWindow
1201
 */
1202
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1203
  m_keygenDialog = keygenWindow;
×
1204
  emit generateGPGKeyPair(batch);
×
1205
}
×
1206

1207
/**
1208
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1209
 * select a more appropriate one to view too
1210
 */
1211
void MainWindow::updateProfileBox() {
12✔
1212
  QHash<QString, QHash<QString, QString>> profiles =
1213
      QtPassSettings::getProfiles();
12✔
1214

1215
  if (profiles.isEmpty()) {
1216
    ui->profileWidget->hide();
×
1217
  } else {
1218
    ui->profileWidget->show();
12✔
1219
    ui->profileBox->setEnabled(profiles.size() > 1);
24✔
1220
    ui->profileBox->clear();
12✔
1221
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
12✔
1222
    while (i.hasNext()) {
12✔
1223
      i.next();
1224
      if (!i.key().isEmpty()) {
12✔
1225
        ui->profileBox->addItem(i.key());
12✔
1226
      }
1227
    }
1228
    ui->profileBox->model()->sort(0);
12✔
1229
  }
1230
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
24✔
1231
  if (index != -1) { //  -1 for not found
12✔
1232
    ui->profileBox->setCurrentIndex(index);
×
1233
  }
1234
}
12✔
1235

1236
/**
1237
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1238
 * correct "profile"
1239
 * @param name
1240
 */
1241
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1242
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1243
#else
1244
/**
1245
 * @brief Handles changes to the selected profile in the profile combo box.
1246
 * @details Ignores the event during a fresh start or when the selected profile
1247
 * matches the current profile. Otherwise, it clears the password field, updates
1248
 * the active profile and related settings, refreshes the environment, and
1249
 * resets the tree view and action states to reflect the newly selected profile.
1250
 *
1251
 * @param name - The newly selected profile name.
1252
 * @return void - This function does not return a value.
1253
 *
1254
 */
1255
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
12✔
1256
#endif
1257
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
12✔
1258
    return;
12✔
1259
  }
1260

1261
  ui->lineEdit->clear();
×
1262

1263
  QtPassSettings::setProfile(name);
×
1264

1265
  QtPassSettings::setPassStore(
×
1266
      QtPassSettings::getProfiles().value(name).value("path"));
×
1267
  QtPassSettings::setPassSigningKey(
×
1268
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1269
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1270

1271
  QtPassSettings::getPass()->updateEnv();
×
1272

1273
  const QString passStore = QtPassSettings::getPassStore();
×
1274
  proxyModel.setStore(passStore);
×
1275
  ui->treeView->setRootIndex(
×
1276
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1277
  deselect();
×
1278
  ui->treeView->setCurrentIndex(QModelIndex());
×
1279
}
1280

1281
/**
1282
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1283
 * support
1284
 * it
1285
 */
1286
void MainWindow::initTrayIcon() {
×
1287
  m_tray = new TrayIcon(this);
×
1288
  // Setup tray icon
1289

1290
  if (m_tray == nullptr) {
1291
#ifdef QT_DEBUG
1292
    dbg() << "Allocating tray icon failed.";
1293
#endif
1294
    return;
1295
  }
1296

1297
  if (!m_tray->getIsAllocated()) {
×
1298
    destroyTrayIcon();
×
1299
  }
1300
}
1301

1302
/**
1303
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1304
 */
1305
void MainWindow::destroyTrayIcon() {
×
1306
  delete m_tray;
×
1307
  m_tray = nullptr;
×
1308
}
×
1309

1310
/**
1311
 * @brief MainWindow::closeEvent hide or quit
1312
 * @param event
1313
 */
1314
void MainWindow::closeEvent(QCloseEvent *event) {
×
1315
  if (QtPassSettings::isHideOnClose()) {
×
1316
    this->hide();
×
1317
    event->ignore();
1318
  } else {
1319
    m_qtPass->clearClipboard();
×
1320

1321
    QtPassSettings::setGeometry(saveGeometry());
×
1322
    QtPassSettings::setSavestate(saveState());
×
1323
    QtPassSettings::setMaximized(isMaximized());
×
1324
    if (!isMaximized()) {
×
1325
      QtPassSettings::setPos(pos());
×
1326
      QtPassSettings::setSize(size());
×
1327
    }
1328
    event->accept();
1329
  }
1330
}
×
1331

1332
/**
1333
 * @brief MainWindow::eventFilter filter out some events and focus the
1334
 * treeview
1335
 * @param obj
1336
 * @param event
1337
 * @return
1338
 */
1339
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
3✔
1340
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
3✔
1341
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1342
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1343
      ui->treeView->setFocus();
×
1344
    }
1345
  }
1346
  return QObject::eventFilter(obj, event);
3✔
1347
}
1348

1349
/**
1350
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1351
 * @param event
1352
 */
1353
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1354
  switch (event->key()) {
×
1355
  case Qt::Key_Delete:
×
1356
    onDelete();
×
1357
    break;
×
1358
  case Qt::Key_Return:
×
1359
  case Qt::Key_Enter:
1360
    if (proxyModel.rowCount() > 0) {
×
1361
      on_treeView_clicked(ui->treeView->currentIndex());
×
1362
    }
1363
    break;
1364
  case Qt::Key_Escape:
×
1365
    ui->lineEdit->clear();
×
1366
    break;
×
1367
  default:
1368
    break;
1369
  }
1370
}
×
1371

1372
/**
1373
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1374
 * menu
1375
 * @param pos
1376
 */
1377
void MainWindow::showContextMenu(const QPoint &pos) {
×
1378
  QModelIndex index = ui->treeView->indexAt(pos);
×
1379
  bool selected = true;
1380
  if (!index.isValid()) {
1381
    ui->treeView->clearSelection();
×
1382
    ui->actionDelete->setEnabled(false);
×
1383
    ui->actionEdit->setEnabled(false);
×
1384
    m_currentDir = "";
×
1385
    selected = false;
1386
  }
1387

1388
  ui->treeView->setCurrentIndex(index);
×
1389

1390
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1391

1392
  QFileInfo fileOrFolder =
1393
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1394

1395
  QMenu contextMenu;
×
1396
  if (!selected || fileOrFolder.isDir()) {
×
1397
    QAction *openFolder =
1398
        contextMenu.addAction(tr("Open folder with file manager"));
×
1399
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1400
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1401
    QAction *users = contextMenu.addAction(tr("Users"));
×
1402
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1403
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1404
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1405
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1406
  } else if (fileOrFolder.isFile()) {
×
1407
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1408
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1409
  }
1410
  if (selected) {
×
1411
    contextMenu.addSeparator();
×
1412
    if (fileOrFolder.isDir()) {
×
1413
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1414
      connect(renameFolder, &QAction::triggered, this,
×
1415
              &MainWindow::renameFolder);
×
1416
    } else if (fileOrFolder.isFile()) {
×
1417
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1418
      connect(renamePassword, &QAction::triggered, this,
×
1419
              &MainWindow::renamePassword);
×
1420
    }
1421
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1422
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1423
    if (fileOrFolder.isDir()) {
×
1424
      QString dirPath = QDir::cleanPath(
1425
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1426

1427
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1428
      contextMenu.addMenu(shareMenu);
×
1429

1430
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1431
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1432

1433
      QString exePath = QtPassSettings::isUsePass()
×
1434
                            ? QtPassSettings::getPassExecutable()
×
1435
                            : QtPassSettings::getGpgExecutable();
×
1436
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1437
                                                 QFile(exePath).exists());
×
1438

1439
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1440
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1441
      connect(reencrypt, &QAction::triggered, this,
×
1442
              [this, dirPath]() { reencryptPath(dirPath); });
×
1443

1444
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1445
      exportKey->setEnabled(gpgAvailable);
×
1446
      connect(exportKey, &QAction::triggered, this,
×
1447
              &MainWindow::exportPublicKey);
×
1448

1449
      QAction *addRecipientAction =
1450
          shareMenu->addAction(tr("Add recipient..."));
×
1451
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1452
      connect(addRecipientAction, &QAction::triggered, this,
×
1453
              [this, dirPath]() { addRecipient(dirPath); });
×
1454

1455
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1456
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1457
    }
1458
  }
1459
  contextMenu.exec(globalPos);
×
1460
}
×
1461

1462
/**
1463
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1464
 * password window
1465
 * @param pos
1466
 */
1467
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1468
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1469
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1470

1471
  contextMenu->exec(globalPos);
×
1472
  delete contextMenu;
×
1473
}
×
1474

1475
/**
1476
 * @brief MainWindow::openFolder open the folder in the default file manager
1477
 */
1478
void MainWindow::openFolder() {
×
1479
  QString dir =
1480
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1481

1482
  QString path = QDir::toNativeSeparators(dir);
×
1483
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1484
}
×
1485

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

1535
/**
1536
 * @brief MainWindow::renameFolder rename an existing folder
1537
 */
1538
void MainWindow::renameFolder() {
×
1539
  bool ok;
1540
  QString srcDir = QDir::cleanPath(
1541
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1542
  QString srcDirName = QDir(srcDir).dirName();
×
1543
  QString newName =
1544
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1545
                            QLineEdit::Normal, srcDirName, &ok);
×
1546
  if (!ok || newName.isEmpty()) {
×
1547
    return;
1548
  }
1549
  QString destDir = srcDir;
1550
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1551
  if (!confirmPathInStore(destDir)) {
×
1552
    return;
1553
  }
1554
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1555
}
1556

1557
/**
1558
 * @brief MainWindow::editPassword read password and open edit window via
1559
 * MainWindow::onEdit()
1560
 */
1561
void MainWindow::editPassword(const QString &file) {
×
1562
  if (!file.isEmpty()) {
×
1563
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1564
      onUpdate(true);
×
1565
    }
1566
    setPassword(file, false);
×
1567
  }
1568
}
×
1569

1570
/**
1571
 * @brief MainWindow::renamePassword rename an existing password
1572
 */
1573
void MainWindow::renamePassword() {
×
1574
  bool ok;
1575
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1576
  QString filePath = QFileInfo(file).path();
×
1577
  QString fileName = QFileInfo(file).fileName();
×
1578
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1579
    fileName.chop(4);
×
1580
  }
1581

1582
  QString newName =
1583
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1584
                            QLineEdit::Normal, fileName, &ok);
×
1585
  if (!ok || newName.isEmpty()) {
×
1586
    return;
1587
  }
1588
  QString newFile = QDir(filePath).filePath(newName);
×
1589
  if (!confirmPathInStore(newFile)) {
×
1590
    return;
1591
  }
1592
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1593
}
1594

1595
/**
1596
 * @brief MainWindow::clearTemplateWidgets empty the template widget fields in
1597
 * the UI
1598
 */
1599
void MainWindow::clearTemplateWidgets() {
×
1600
  while (ui->gridLayout->count() > 0) {
×
1601
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
1602
    delete item->widget();
×
1603
    delete item;
×
1604
  }
1605
  ui->verticalLayoutPassword->setSpacing(0);
×
1606
}
×
1607

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

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

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

1636
/**
1637
 * @brief MainWindow::addToGridLayout add a field to the template grid
1638
 * @param position
1639
 * @param field
1640
 * @param value
1641
 */
1642
void MainWindow::addToGridLayout(int position, const QString &field,
×
1643
                                 const QString &value) {
1644
  QString trimmedField = field.trimmed();
1645
  QString trimmedValue = value.trimmed();
1646

1647
  const QString buttonStyle =
1648
      "border-style: none; background: transparent; padding: 0; margin: 0; "
1649
      "icon-size: 16px; color: inherit;";
×
1650

1651
  // Combine the Copy button and the line edit in one widget
1652
  auto *frame = new QFrame();
×
1653
  QLayout *ly = new QHBoxLayout();
×
1654
  ly->setContentsMargins(5, 2, 2, 2);
×
1655
  ly->setSpacing(0);
×
1656
  frame->setLayout(ly);
×
1657
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER) {
×
1658
    auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
×
1659
    connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
×
1660
            &QtPass::copyTextToClipboard);
×
1661

1662
    fieldLabel->setStyleSheet(buttonStyle);
×
1663
    frame->layout()->addWidget(fieldLabel);
×
1664
  }
1665

1666
  if (QtPassSettings::isUseQrencode()) {
×
1667
    auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
×
1668
    connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
×
1669
            &QtPass::showTextAsQRCode);
×
1670
    qrbutton->setStyleSheet(buttonStyle);
×
1671
    frame->layout()->addWidget(qrbutton);
×
1672
  }
1673

1674
  // Show an explicit "open in browser" button when the value is a safe
1675
  // http(s) URL. The inline clickable link still works for URLs embedded in
1676
  // prose; this button is the discoverable affordance for url fields.
1677
  // Never on the password field: its value is a secret and must not be
1678
  // surfaced in a tooltip or handed to the browser.
NEW
1679
  if (trimmedField != tr("Password") &&
×
NEW
1680
      Util::isLaunchableWebUrl(trimmedValue)) {
×
NEW
1681
    auto *urlButton = new QPushButton(this);
×
NEW
1682
    urlButton->setIcon(QIcon::fromTheme(QStringLiteral("applications-internet"),
×
NEW
1683
                                        QIcon(":/icons/open-url.svg")));
×
NEW
1684
    urlButton->setToolTip(
×
NEW
1685
        tr("Open %1 in browser").arg(trimmedValue.toHtmlEscaped()));
×
NEW
1686
    urlButton->setStyleSheet(buttonStyle);
×
NEW
1687
    urlButton->setCursor(Qt::PointingHandCursor);
×
NEW
1688
    connect(urlButton, &QPushButton::clicked, this, [trimmedValue]() {
×
1689
      // Re-validate before launching (defence in depth: the value is
1690
      // immutable here, but never hand an unvalidated string to the OS
1691
      // URL handler).
NEW
1692
      if (Util::isLaunchableWebUrl(trimmedValue)) {
×
NEW
1693
        QDesktopServices::openUrl(QUrl(trimmedValue));
×
1694
      }
NEW
1695
    });
×
NEW
1696
    frame->layout()->addWidget(urlButton);
×
1697
  }
1698

1699
  // set the echo mode to password, if the field is "password"
1700
  const QString lineStyle =
1701
      QtPassSettings::isUseMonospace()
×
1702
          ? "border-style: none; background: transparent; font-family: "
1703
            "monospace;"
1704
          : "border-style: none; background: transparent;";
×
1705

1706
  if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
×
1707
    auto *line = new QLineEdit();
×
1708
    line->setObjectName(trimmedField);
×
1709
    line->setText(trimmedValue);
×
1710
    line->setReadOnly(true);
×
1711
    line->setStyleSheet(lineStyle);
×
1712
    line->setContentsMargins(0, 0, 0, 0);
×
1713
    line->setEchoMode(QLineEdit::Password);
×
1714
    auto *showButton = new QPushButtonShowPassword(line, this);
×
1715
    showButton->setStyleSheet(buttonStyle);
×
1716
    showButton->setContentsMargins(0, 0, 0, 0);
×
1717
    frame->layout()->addWidget(showButton);
×
1718
    frame->layout()->addWidget(line);
×
1719
  } else {
1720
    auto *line = new QTextBrowser();
×
1721
    line->setOpenExternalLinks(true);
×
1722
    line->setOpenLinks(true);
×
1723
    line->setMaximumHeight(26);
×
1724
    line->setMinimumHeight(26);
×
1725
    line->setSizePolicy(
×
1726
        QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1727
    line->setObjectName(trimmedField);
×
1728
    trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
1729
    line->setText(trimmedValue);
×
1730
    line->setReadOnly(true);
×
1731
    line->setStyleSheet(lineStyle);
×
1732
    line->setContentsMargins(0, 0, 0, 0);
×
1733
    frame->layout()->addWidget(line);
×
1734
  }
1735

1736
  frame->setStyleSheet(
×
1737
      ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1738

1739
  // set into the layout
1740
  ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
×
1741
  ui->gridLayout->addWidget(frame, position, 1);
×
1742
}
×
1743

1744
/**
1745
 * @brief Displays message in status bar
1746
 *
1747
 * @param msg     text to be displayed
1748
 * @param timeout time for which msg shall be visible
1749
 */
1750
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
2✔
1751
  ui->statusBar->showMessage(msg, timeout);
2✔
1752
}
2✔
1753

1754
/**
1755
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1756
 * @param dir Directory path to re-encrypt
1757
 */
1758
void MainWindow::reencryptPath(const QString &dir) {
×
1759
  QDir checkDir(dir);
×
1760
  if (!checkDir.exists()) {
×
1761
    QMessageBox::critical(this, tr("Error"),
×
1762
                          tr("Directory does not exist: %1").arg(dir));
×
1763
    return;
×
1764
  }
1765

1766
  int ret = QMessageBox::question(
×
1767
      this, tr("Re-encrypt passwords"),
×
1768
      tr("Re-encrypt all passwords in %1?\n\n"
×
1769
         "This will re-encrypt ALL password files in this folder "
1770
         "using the current recipients defined in .gpg-id.\n\n"
1771
         "This may rewrite many files and cannot be undone easily.\n\n"
1772
         "Continue?")
1773
          .arg(QDir(dir).dirName()),
×
1774
      QMessageBox::Yes | QMessageBox::No);
1775

1776
  if (ret != QMessageBox::Yes)
×
1777
    return;
1778

1779
  // Disable preemptively. ImitatePass::reencryptPath emits
1780
  // startReencryptPath asynchronously and the slot would re-run this,
1781
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1782
  startReencryptPath();
×
1783

1784
  QtPassSettings::getImitatePass()->reencryptPath(
×
1785
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1786
}
×
1787

1788
/**
1789
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1790
 */
1791
void MainWindow::startReencryptPath() {
×
1792
  setUiElementsEnabled(false);
×
1793
  ui->treeView->setDisabled(true);
×
1794
}
×
1795

1796
/**
1797
 * @brief MainWindow::endReencryptPath re-enable ui elements
1798
 */
1799
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1800

1801
/**
1802
 * @brief MainWindow::exportPublicKey export the configured signing key in
1803
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1804
 *
1805
 * Falls back to a help dialog when no signing key is configured or gpg is
1806
 * unavailable, so the user still gets actionable guidance.
1807
 */
1808
void MainWindow::exportPublicKey() {
×
1809
  QString identity = QtPassSettings::getPassSigningKey();
×
1810
  if (identity.isEmpty()) {
×
1811
    QMessageBox::information(
×
1812
        this, tr("Export Public Key"),
×
1813
        tr("<h3>Export Your Public Key</h3>"
×
1814
           "<p>No signing key is configured. Set one in QtPass Settings "
1815
           "&gt; GPG keys, or run this in a terminal:</p>"
1816
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1817
           "&gt;</pre>"
1818
           "<p>Then send the file to your teammates.</p>"));
1819
    return;
×
1820
  }
1821
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
1822
  if (gpgExe.isEmpty()) {
×
1823
    gpgExe = QStringLiteral("gpg");
×
1824
  }
1825
  QStringList args = {"--armor", "--export"};
×
1826
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1827
  QString stdOut;
×
1828
  QString stdErr;
×
1829
  int exitCode =
1830
      Executor::executeBlocking(gpgExe, args, QString(), &stdOut, &stdErr);
×
1831
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1832
    QMessageBox::warning(this, tr("Export Public Key"),
×
1833
                         tr("Could not export public key for %1.\n\n%2")
×
1834
                             .arg(identity, stdErr.isEmpty()
×
1835
                                                ? tr("No output from gpg.")
×
1836
                                                : stdErr));
1837
    return;
1838
  }
1839
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1840
  dialog.exec();
×
1841
}
×
1842

1843
/**
1844
 * @brief MainWindow::addRecipient open the recipient management dialog for
1845
 *        the supplied directory.
1846
 * @param dir Folder whose .gpg-id should be edited.
1847
 *
1848
 * Delegates to UsersDialog so users can tick/untick keys from their
1849
 * keyring as recipients of the folder; importing a foreign key into the
1850
 * keyring still has to happen via gpg (or QtPass settings) first.
1851
 */
1852
void MainWindow::addRecipient(const QString &dir) {
×
1853
  UsersDialog d(dir, this);
×
1854
  d.exec();
×
1855
}
×
1856

1857
/**
1858
 * @brief MainWindow::showShareHelp show help about GPG sharing
1859
 */
1860
void MainWindow::showShareHelp() {
×
1861
  QMessageBox::information(
×
1862
      this, tr("Sharing Passwords with GPG"),
×
1863
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1864
         "<p>To share passwords with other users:</p>"
1865
         "<ol>"
1866
         "<li><b>Export your public key</b> and send it to teammates</li>"
1867
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1868
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1869
         "them</li>"
1870
         "</ol>"
1871
         "<p>Only people who have a matching secret key can decrypt the "
1872
         "passwords.</p>"
1873
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1874
         "<p>See the FAQ for more details.</p>"));
1875
}
×
1876

1877
void MainWindow::updateGitButtonVisibility() {
15✔
1878
  if (!QtPassSettings::isUseGit() ||
30✔
1879
      (QtPassSettings::getGitExecutable().isEmpty() &&
15✔
1880
       QtPassSettings::getPassExecutable().isEmpty())) {
15✔
1881
    enableGitButtons(false);
15✔
1882
  } else {
1883
    enableGitButtons(true);
×
1884
  }
1885
}
15✔
1886

1887
void MainWindow::updateOtpButtonVisibility() {
15✔
1888
#if defined(Q_OS_WIN) || defined(__APPLE__)
1889
  ui->actionOtp->setVisible(false);
1890
#endif
1891
  if (!QtPassSettings::isUseOtp()) {
15✔
1892
    ui->actionOtp->setEnabled(false);
15✔
1893
  } else {
1894
    ui->actionOtp->setEnabled(true);
×
1895
  }
1896
}
15✔
1897

1898
void MainWindow::updateGrepButtonVisibility() {
12✔
1899
  const bool enabled = QtPassSettings::isUseGrepSearch();
12✔
1900
  ui->grepButton->setVisible(enabled);
12✔
1901
  ui->grepCaseButton->setVisible(enabled);
12✔
1902
  if (!enabled && m_grepMode) {
12✔
1903
    ui->grepButton->setChecked(false);
×
1904
  }
1905
}
12✔
1906

1907
void MainWindow::enableGitButtons(const bool &state) {
15✔
1908
  // Following GNOME guidelines is preferable disable buttons instead of hide
1909
  ui->actionPush->setEnabled(state);
15✔
1910
  ui->actionUpdate->setEnabled(state);
15✔
1911
}
15✔
1912

1913
/**
1914
 * @brief MainWindow::critical critical message popup wrapper.
1915
 * @param title
1916
 * @param msg
1917
 */
1918
void MainWindow::critical(const QString &title, const QString &msg) {
×
1919
  QMessageBox::critical(this, title, msg);
×
1920
}
×
1921

1922
/**
1923
 * @brief Appends processed command output to the output panel.
1924
 *
1925
 * Appends text to the process output text edit, with per-line numbering,
1926
 * optional command prefix, and color coding for errors vs. success.
1927
 * Handles auto-scrolling and line limits.
1928
 *
1929
 * @param output The raw output text from the command.
1930
 * @param isError true if this is error output (stderr).
1931
 * @param linePrefix Optional command name to prefix each line with.
1932
 */
1933
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2✔
1934
                                     const QString &linePrefix) {
1935
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
1936
    return;
1✔
1937
  }
1938

1939
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1940
  for (QString &line : lines) {
2✔
1941
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1942
    // indentation
1943
    line.remove('\r');
1✔
1944
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1945
      line.chop(1);
×
1946
    }
1947
    if (line.isEmpty()) {
1✔
1948
      continue;
×
1949
    }
1950

1951
    m_outputCounter++;
1✔
1952
    QString lineNumber = QString::number(m_outputCounter);
1✔
1953

1954
    QColor textColor =
1955
        isError ? QColor(Qt::red)
1✔
1956
                : m_processOutputEdit->palette().color(QPalette::Text);
2✔
1957
    QString colorHex = textColor.name();
1✔
1958
    // Apply the optional prefix per line so multi-line output stays
1959
    // attributed to its command (e.g. all 3 lines of a `git push` show
1960
    // "git push: ..." rather than only the first).
1961
    QString prefixed =
1962
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2✔
1963
    QString coloredOutput =
1964
        QString("<span style=\"color: %1;\">%2: %3</span>")
1✔
1965
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2✔
1966

1967
    m_processOutputEdit->append(coloredOutput);
1✔
1968
  }
1969

1970
  limitOutputLines();
1✔
1971

1972
  if (m_autoScroll) {
1✔
1973
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
1974
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
1975
  }
1976
}
1977

1978
/**
1979
 * @brief Handles process output from the Pass executor.
1980
 *
1981
 * Called when any non-sensitive process completes. Filters out password-
1982
 * related commands (pass show, insert, etc.) and delegates to
1983
 * appendProcessOutput.
1984
 *
1985
 * @param output The stdout/stderr text from the process.
1986
 * @param isError true if this is error output (stderr).
1987
 * @param pid The process ID identifying which command ran.
1988
 */
1989
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
1990
                                 Enums::PROCESS pid) {
1991
  appendProcessOutput(output, isError, getProcessName(pid));
2✔
1992
}
2✔
1993

1994
/**
1995
 * @brief Maps a process ID to its human-readable command name.
1996
 *
1997
 * Returns static strings for git/pass commands that appear in output.
1998
 * Password-related commands return empty (they are filtered).
1999
 *
2000
 * @param pid The process ID to look up.
2001
 * @return QString with command name, or empty if filtered.
2002
 */
2003
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
2✔
2004
  switch (pid) {
2✔
2005
  case Enums::GIT_INIT:
×
2006
    return QStringLiteral("git init"); // no-tr
×
2007
  case Enums::GIT_ADD:
×
2008
    return QStringLiteral("git add"); // no-tr
×
2009
  case Enums::GIT_COMMIT:
×
2010
    return QStringLiteral("git commit"); // no-tr
×
2011
  case Enums::GIT_RM:
×
2012
    return QStringLiteral("git rm"); // no-tr
×
2013
  case Enums::GIT_PULL:
×
2014
    return QStringLiteral("git pull"); // no-tr
×
2015
  case Enums::GIT_PUSH:
×
2016
    return QStringLiteral("git push"); // no-tr
×
2017
  case Enums::GIT_MOVE:
×
2018
    return QStringLiteral("git mv"); // no-tr
×
2019
  case Enums::GIT_COPY:
×
2020
    // ImitatePass::Copy literally invokes `git cp` (a git-extras
2021
    // subcommand), so the label matches what's run. Stock-git users
2022
    // without git-extras will see the underlying "'cp' is not a git
2023
    // command" failure surfaced in the process output panel.
2024
    return QStringLiteral("git cp"); // no-tr
×
2025
  case Enums::PASS_INSERT:
×
2026
    return QStringLiteral("pass insert"); // no-tr
×
2027
  case Enums::PASS_REMOVE:
×
2028
    return QStringLiteral("pass rm"); // no-tr
×
2029
  case Enums::PASS_INIT:
×
2030
    return QStringLiteral("pass init"); // no-tr
×
2031
  case Enums::PASS_MOVE:
×
2032
    return QStringLiteral("pass mv"); // no-tr
×
2033
  case Enums::PASS_COPY:
×
2034
    return QStringLiteral("pass cp"); // no-tr
×
2035
  case Enums::PASS_GREP:
×
2036
    return QStringLiteral("pass grep"); // no-tr
×
2037
  case Enums::GPG_GENKEYS:
×
2038
    return QStringLiteral("gpg --gen-key"); // no-tr
×
2039
  case Enums::PASS_SHOW:
2040
  case Enums::PASS_OTP_GENERATE:
2041
  case Enums::PROCESS_COUNT:
2042
  case Enums::INVALID:
2043
    break;
2044
  }
2045
  return {};
2046
}
2047

2048
/**
2049
 * @brief Checks if a process ID represents a sensitive operation whose
2050
 * output should not be shown in the process output panel.
2051
 *
2052
 * Password-related commands (pass show, OTP generate, grep, insert)
2053
 * display their output in other UI areas, so we skip them here.
2054
 *
2055
 * @param pid The process ID to check.
2056
 * @return true if the process is sensitive and should be filtered.
2057
 */
2058
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
2059
  switch (pid) {
×
2060
  case Enums::PASS_SHOW:
2061
  case Enums::PASS_OTP_GENERATE:
2062
  case Enums::PASS_GREP:
2063
  case Enums::PASS_INSERT:
2064
    return true;
2065
  case Enums::GIT_INIT:
2066
  case Enums::GIT_ADD:
2067
  case Enums::GIT_COMMIT:
2068
  case Enums::GIT_RM:
2069
  case Enums::GIT_PULL:
2070
  case Enums::GIT_PUSH:
2071
  case Enums::GIT_MOVE:
2072
  case Enums::GIT_COPY:
2073
  case Enums::PASS_REMOVE:
2074
  case Enums::PASS_INIT:
2075
  case Enums::PASS_MOVE:
2076
  case Enums::PASS_COPY:
2077
  case Enums::GPG_GENKEYS:
2078
  case Enums::PROCESS_COUNT:
2079
  case Enums::INVALID:
2080
    break;
2081
  }
2082
  return false;
×
2083
}
2084

2085
/**
2086
 * @brief Updates the visibility of the process output panel.
2087
 *
2088
 * Shows or hides the process output widget based on the user's
2089
 * showProcessOutput setting.
2090
 */
2091
void MainWindow::updateProcessOutputVisibility() {
×
2092
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
2093
}
×
2094

2095
/**
2096
 * @brief Limits the output panel to max lines, trimming old excess.
2097
 *
2098
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
2099
 * Called after each append to prevent unbounded growth.
2100
 */
2101
void MainWindow::limitOutputLines() {
1✔
2102
  QTextDocument *doc = m_processOutputEdit->document();
1✔
2103
  int excess = doc->blockCount() - MaxOutputLines;
1✔
2104
  if (excess <= 0) {
1✔
2105
    return;
1✔
2106
  }
2107

2108
  QTextCursor cursor(doc);
×
2109
  cursor.movePosition(QTextCursor::Start);
×
2110
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2111
  cursor.removeSelectedText();
×
2112
}
×
2113

2114
/**
2115
 * @brief Clears the process output panel.
2116
 *
2117
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2118
 */
2119
void MainWindow::on_clearOutputButton_clicked() {
×
2120
  m_processOutputEdit->clear();
×
2121
  m_outputCounter = 0;
×
2122
  m_autoScroll = true;
×
2123
}
×
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