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

IJHack / QtPass / 27477250065

13 Jun 2026 07:49PM UTC coverage: 55.57%. Remained the same
27477250065

push

github

web-flow
refactor: extract isPathInStore into PathValidator module (#1514) (#1524)

* refactor: extract isPathInStore into PathValidator module (#1514)

Move Util::isPathInStore into a dedicated PathValidator class
(src/pathvalidator.{h,cpp}), continuing the Util grab-bag split.
The store-boundary check is a self-contained, security-sensitive
concern with no dependency on the rest of Util, so it earns its own
focused module.

- Verbatim move of the impl (walk to nearest existing ancestor,
  canonicalise, re-append leaf, compare against canonical store root).
- Update callers: StoreModel drag-drop (2 sites), MainWindow
  confirmPathInStore (1 site).
- Retarget the isPathInStore* cases in tst_util to PathValidator.
- Wire pathvalidator.cpp/.h into src/src.pro.

#1514 stays open for the remaining Template I/O slice
(readTemplates/writeTemplates/getFolderTemplate).

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

* l10n: fill empty unfinished translations flagged on review

Fill the empty <translation type="unfinished"> entries surfaced by the
qmake6 .ts refresh, keeping type="unfinished" so Weblate can refine.

- 10 locales (af, ar, bg, bn, ca, cs, de_DE, de_LU, el, en_GB): best-effort
  translations for the two new MainWindow strings "That name would resolve
  outside the password store..." and "Open %1 in browser", placeholders
  preserved. en_GB only lacked the latter.
- en_US: fill all 106 empty entries with the source text, converting British
  spellings (behaviour->behavior, etc.) and preserving %1/%2 placeholders.

All 11 files validated with lrelease6 (no errors/warnings).

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

---------

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

24 of 26 new or added lines in 3 files covered. (92.31%)

9 existing lines in 1 file now uncovered.

3731 of 6714 relevant lines covered (55.57%)

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

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

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

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

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

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

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

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

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

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

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

119
  updateProfileBox();
12✔
120

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

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

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

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

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

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

160
  setUiElementsEnabled(true);
12✔
161

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1264
  QtPassSettings::setProfile(name);
×
1265

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1971
  limitOutputLines();
1✔
1972

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

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

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

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

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

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

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

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