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

IJHack / QtPass / 24613312963

18 Apr 2026 08:32PM UTC coverage: 22.627% (+0.7%) from 21.908%
24613312963

push

github

web-flow
feat: implement pass grep content search (#109) (#1037)

* feat: implement pass grep content search (#109)

Add full content search (pass grep) for both RealPass and ImitatePass
backends, with a UI toggle button and results panel in the main window.

**RealPass** delegates to `pass grep [-i] <pattern>` and parses the
ANSI-coloured output: entry headers are identified by the `\x1B[94m`
blue-colour escape before stripping ANSI, giving reliable detection
regardless of locale.

**ImitatePass** runs a `QThread::create` background worker that
iterates every `.gpg` file under the store with `QDirIterator`,
decrypts each with `Executor::executeBlocking`, and applies a
`QRegularExpression` to the plaintext. Results are marshalled back
to the main thread via `QMetaObject::invokeMethod(Qt::QueuedConnection)`.
A `QPointer<ImitatePass>` guards against use-after-free if the object
is destroyed while the thread is running.

**UI changes**
- A checkable `grepButton` (QToolButton, `*`) sits next to the search
  field; toggling it switches between filename-filter mode and content-
  search mode and updates the placeholder text.
- `on_lineEdit_textChanged` skips the proxy-model filter in grep mode.
- `on_lineEdit_returnPressed` calls `Pass::Grep()` in grep mode.
- `onGrepFinished` populates a `QTreeWidget` (`grepResultsList`) with
  entry names as top-level items and matching lines as children.
- Clicking a result navigates the treeView to the corresponding entry
  and triggers the normal show-password flow.

Closes #109

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

* fix: address code review findings for pass grep feature

- enums.h: move PASS_OTP_GENERATE and PASS_GREP before the sentinel
  values PROCESS_COUNT/INVALID so the count remains correct
- pass.cpp: handle PASS_GREP exit code 1 (no matches) as empty results
  instead of routing through processErrorExit; exit codes > 1 are still
  errors
- realpass.cpp: add '--' before the search pattern so patt... (continued)

81 of 243 new or added lines in 7 files covered. (33.33%)

347 existing lines in 7 files now uncovered.

1304 of 5763 relevant lines covered (22.63%)

8.53 hits per line

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

0.0
/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 "filecontent.h"
11
#include "passworddialog.h"
12
#include "qpushbuttonasqrcode.h"
13
#include "qpushbuttonshowpassword.h"
14
#include "qpushbuttonwithclipboard.h"
15
#include "qtpass.h"
16
#include "qtpasssettings.h"
17
#include "trayicon.h"
18
#include "ui_mainwindow.h"
19
#include "usersdialog.h"
20
#include "util.h"
21
#include <QApplication>
22
#include <QCloseEvent>
23
#include <QDesktopServices>
24
#include <QDialog>
25
#include <QDirIterator>
26
#include <QFileInfo>
27
#include <QInputDialog>
28
#include <QLabel>
29
#include <QMenu>
30
#include <QMessageBox>
31
#include <QShortcut>
32
#include <QTimer>
33
#include <QTreeWidget>
34
#include <utility>
35

36
/**
37
 * @brief MainWindow::MainWindow handles all of the main functionality and also
38
 * the main window.
39
 * @param searchText for searching from cli
40
 * @param parent pointer
41
 */
42
MainWindow::MainWindow(const QString &searchText, QWidget *parent)
×
43
    : QMainWindow(parent), ui(new Ui::MainWindow), keygen(nullptr),
×
44
      tray(nullptr) {
×
45
#ifdef __APPLE__
46
  // extra treatment for mac os
47
  // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
48
  qt_set_sequence_auto_mnemonic(true);
49
#endif
50
  ui->setupUi(this);
×
51

52
  m_qtPass = new QtPass(this);
×
53

54
  // register shortcut ctrl/cmd + Q to close the main window
55
  new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
×
56
  // register shortcut ctrl/cmd + C to copy the currently selected password
57
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this,
×
58
                SLOT(copyPasswordFromTreeview()));
×
59

60
  model.setNameFilters(QStringList() << "*.gpg");
×
61
  model.setNameFilterDisables(false);
×
62

63
  /*
64
   * I added this to solve Windows bug but now on GNU/Linux the main folder,
65
   * if hidden, disappear
66
   *
67
   * model.setFilter(QDir::NoDot);
68
   */
69

70
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
×
71

72
  QModelIndex rootDir = model.setRootPath(passStore);
×
73
  model.fetchMore(rootDir);
×
74

75
  proxyModel.setModelAndStore(&model, passStore);
×
76
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
×
77

78
  ui->treeView->setModel(&proxyModel);
×
79
  ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir));
×
80
  ui->treeView->setColumnHidden(1, true);
×
81
  ui->treeView->setColumnHidden(2, true);
×
82
  ui->treeView->setColumnHidden(3, true);
×
83
  ui->treeView->setHeaderHidden(true);
×
84
  ui->treeView->setIndentation(15);
×
85
  ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
×
86
  ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
×
87
  ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
×
88
  ui->treeView->sortByColumn(0, Qt::AscendingOrder);
×
89
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
×
90
          &MainWindow::showContextMenu);
×
91
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
×
92
          &MainWindow::deselect);
×
93

94
  if (QtPassSettings::isUseMonospace()) {
×
95
    QFont monospace("Monospace");
×
96
    monospace.setStyleHint(QFont::Monospace);
×
97
    ui->textBrowser->setFont(monospace);
×
98
  }
×
99
  if (QtPassSettings::isNoLineWrapping()) {
×
100
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
101
  }
102
  ui->textBrowser->setOpenExternalLinks(true);
×
103
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
×
104
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
×
105
          &MainWindow::showBrowserContextMenu);
×
106

107
  updateProfileBox();
×
108

109
  QtPassSettings::getPass()->updateEnv();
×
110
  clearPanelTimer.setInterval(MS_PER_SECOND *
×
111
                              QtPassSettings::getAutoclearPanelSeconds());
×
112
  clearPanelTimer.setSingleShot(true);
×
113
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
×
114

115
  searchTimer.setInterval(350);
×
116
  searchTimer.setSingleShot(true);
×
117

118
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
×
119

120
  initToolBarButtons();
×
121
  initStatusBar();
×
122

123
  ui->lineEdit->setClearButtonEnabled(true);
×
NEW
124
  updateGrepButtonVisibility();
×
125

126
  setUiElementsEnabled(true);
×
127

128
  QTimer::singleShot(10, this, SLOT(focusInput()));
129

130
  ui->lineEdit->setText(searchText);
×
131

132
  if (!m_qtPass->init()) {
×
133
    // no working config so this should just quit
134
    QApplication::quit();
×
135
  }
136
}
×
137

138
MainWindow::~MainWindow() { delete m_qtPass; }
×
139

140
/**
141
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
142
 * box and sets focus to it. Allows for easy searching, called at application
143
 * start and when receiving empty message in MainWindow::messageAvailable when
144
 * compiled with SINGLE_APP=1 (default).
145
 */
146
void MainWindow::focusInput() {
×
147
  ui->lineEdit->selectAll();
×
148
  ui->lineEdit->setFocus();
×
149
}
×
150

151
/**
152
 * @brief MainWindow::changeEvent sets focus to the search box
153
 * @param event
154
 */
155
void MainWindow::changeEvent(QEvent *event) {
×
156
  QWidget::changeEvent(event);
×
157
  if (event->type() == QEvent::ActivationChange) {
×
158
    if (isActiveWindow()) {
×
159
      focusInput();
×
160
    }
161
  }
162
}
×
163

164
/**
165
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
166
 */
167
void MainWindow::initToolBarButtons() {
×
168
  connect(ui->actionAddPassword, &QAction::triggered, this,
×
169
          &MainWindow::addPassword);
×
170
  connect(ui->actionAddFolder, &QAction::triggered, this,
×
171
          &MainWindow::addFolder);
×
172
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
×
173
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
×
174
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
×
175
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
×
176
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
×
177
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
×
178
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
×
179

180
  ui->actionAddPassword->setIcon(
×
181
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
×
182
  ui->actionAddFolder->setIcon(
×
183
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
×
184
  ui->actionEdit->setIcon(QIcon::fromTheme(
×
185
      "document-properties", QIcon(":/icons/document-properties.svg")));
×
186
  ui->actionDelete->setIcon(
×
187
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
×
188
  ui->actionPush->setIcon(
×
189
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
×
190
  ui->actionUpdate->setIcon(
×
191
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
×
192
  ui->actionUsers->setIcon(QIcon::fromTheme(
×
193
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
×
194
  ui->actionConfig->setIcon(QIcon::fromTheme(
×
195
      "applications-system", QIcon(":/icons/applications-system.svg")));
×
196
}
×
197

198
/**
199
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
200
 */
201
void MainWindow::initStatusBar() {
×
202
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
×
203

204
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
×
205
                     .scaledToHeight(statusBar()->height());
×
206
  auto *logoApp = new QLabel(statusBar());
×
207
  logoApp->setPixmap(logo);
×
208
  statusBar()->addPermanentWidget(logoApp);
×
209
}
×
210

211
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
212
  return ui->treeView->currentIndex();
×
213
}
214

215
void MainWindow::cleanKeygenDialog() {
×
216
  this->keygen->close();
×
217
  this->keygen = nullptr;
×
218
}
×
219

220
/**
221
 * @brief Displays the given text in the main window text browser, optionally
222
 * marking it as an error and/or rendering it as HTML.
223
 * @example
224
 * MainWindow window;
225
 * window.flashText("Operation completed.", false, false);
226
 *
227
 * @param const QString &text - The text content to display.
228
 * @param const bool isError - If true, sets the text color to red before
229
 * displaying the text.
230
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
231
 * the existing HTML content.
232
 * @return void - No return value.
233
 */
234
void MainWindow::flashText(const QString &text, const bool isError,
×
235
                           const bool isHtml) {
236
  if (isError) {
×
237
    ui->textBrowser->setTextColor(Qt::red);
×
238
  }
239

240
  if (isHtml) {
×
241
    QString _text = text;
242
    if (!ui->textBrowser->toPlainText().isEmpty()) {
×
243
      _text = ui->textBrowser->toHtml() + _text;
×
244
    }
245
    ui->textBrowser->setHtml(_text);
×
246
  } else {
247
    ui->textBrowser->setText(text);
×
248
  }
249
}
×
250

251
/**
252
 * @brief MainWindow::config pops up the configuration screen and handles all
253
 * inter-window communication
254
 */
255
void MainWindow::applyTextBrowserSettings() {
×
256
  if (QtPassSettings::isUseMonospace()) {
×
257
    QFont monospace("Monospace");
×
258
    monospace.setStyleHint(QFont::Monospace);
×
259
    ui->textBrowser->setFont(monospace);
×
260
  } else {
×
261
    ui->textBrowser->setFont(QFont());
×
262
  }
263

264
  if (QtPassSettings::isNoLineWrapping()) {
×
265
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
266
  } else {
267
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
268
  }
269
}
×
270

271
void MainWindow::applyWindowFlagsSettings() {
×
272
  if (QtPassSettings::isAlwaysOnTop()) {
×
273
    Qt::WindowFlags flags = windowFlags();
274
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
275
  } else {
276
    this->setWindowFlags(Qt::Window);
×
277
  }
278
  this->show();
×
279
}
×
280

281
/**
282
 * @brief Opens and processes the application configuration dialog, then applies
283
 * any accepted settings.
284
 * @example
285
 * config();
286
 *
287
 * @return void - This function does not return a value.
288
 */
289
void MainWindow::config() {
×
290
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
291
  d->setModal(true);
×
292
  // Automatically default to pass if it's available
293
  if (m_qtPass->isFreshStart() &&
×
294
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
295
    QtPassSettings::setUsePass(true);
×
296
  }
297

298
  if (m_qtPass->isFreshStart()) {
×
299
    d->wizard(); //  does shit
×
300
  }
301
  if (d->exec()) {
×
302
    if (d->result() == QDialog::Accepted) {
×
303
      applyTextBrowserSettings();
×
304
      applyWindowFlagsSettings();
×
305

306
      updateProfileBox();
×
307
      const QString passStore = QtPassSettings::getPassStore();
×
308
      proxyModel.setStore(passStore);
×
309
      ui->treeView->setRootIndex(
×
310
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
311
      deselect();
×
312
      ui->treeView->setCurrentIndex(QModelIndex());
×
313

314
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
315
        config();
×
316
      }
317
      QtPassSettings::getPass()->updateEnv();
×
318
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
319
                                  QtPassSettings::getAutoclearPanelSeconds());
×
320
      m_qtPass->setClipboardTimer();
×
321

322
      updateGitButtonVisibility();
×
323
      updateOtpButtonVisibility();
×
NEW
324
      updateGrepButtonVisibility();
×
325
      if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
326
        initTrayIcon();
×
327
      } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
328
        destroyTrayIcon();
×
329
      }
330
    }
331

332
    m_qtPass->setFreshStart(false);
×
333
  }
334
}
×
335

336
/**
337
 * @brief MainWindow::onUpdate do a git pull
338
 */
339
void MainWindow::onUpdate(bool block) {
×
340
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
341
  if (block) {
×
342
    QtPassSettings::getPass()->GitPull_b();
×
343
  } else {
344
    QtPassSettings::getPass()->GitPull();
×
345
  }
346
}
×
347

348
/**
349
 * @brief MainWindow::onPush do a git push
350
 */
351
void MainWindow::onPush() {
×
352
  if (QtPassSettings::isUseGit()) {
×
353
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
354
    QtPassSettings::getPass()->GitPush();
×
355
  }
356
}
×
357

358
/**
359
 * @brief MainWindow::getFile get the selected file path
360
 * @param index
361
 * @param forPass returns relative path without '.gpg' extension
362
 * @return path
363
 * @return
364
 */
365
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
366
  if (!index.isValid() ||
×
367
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
368
    return {};
369
  }
370
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
371
  if (forPass) {
×
372
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
373
    filePath.replace(Util::endsWithGpg(), "");
×
374
  }
375
  return filePath;
376
}
377

378
/**
379
 * @brief MainWindow::on_treeView_clicked read the selected password file
380
 * @param index
381
 */
382
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
383
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
384
  currentDir =
385
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
386
  // Clear any previously cached clipped text before showing new password
387
  m_qtPass->clearClippedText();
×
388
  QString file = getFile(index, true);
×
NEW
389
  ui->passwordName->setText(file);
×
390
  if (!file.isEmpty() && !cleared) {
×
391
    QtPassSettings::getPass()->Show(file);
×
392
  } else {
393
    clearPanel(false);
×
394
    ui->actionEdit->setEnabled(false);
×
395
    ui->actionDelete->setEnabled(true);
×
396
  }
397
}
×
398

399
/**
400
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
401
 * TreeViewItem, open the edit Window
402
 * @param index
403
 */
404
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
405
  QFileInfo fileOrFolder =
406
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
407

408
  if (fileOrFolder.isFile()) {
×
409
    editPassword(getFile(index, true));
×
410
  }
411
}
×
412

413
/**
414
 * @brief MainWindow::deselect clear the selection, password and copy buffer
415
 */
416
void MainWindow::deselect() {
×
417
  currentDir = "";
×
418
  m_qtPass->clearClipboard();
×
419
  ui->treeView->clearSelection();
×
420
  ui->actionEdit->setEnabled(false);
×
421
  ui->actionDelete->setEnabled(false);
×
422
  ui->passwordName->setText("");
×
423
  clearPanel(false);
×
424
}
×
425

426
void MainWindow::executeWrapperStarted() {
×
427
  clearTemplateWidgets();
×
428
  ui->textBrowser->clear();
×
429
  setUiElementsEnabled(false);
×
430
  clearPanelTimer.stop();
×
431
}
×
432

433
/**
434
 * @brief Handles displaying parsed password entry content in the main window.
435
 * @example
436
 * void result = MainWindow::passShowHandler(p_output);
437
 * // Updates the UI with parsed fields and emits
438
 * passShowHandlerFinished(output)
439
 *
440
 * @param p_output - The raw output text containing the password entry data.
441
 * @return void - This function does not return a value.
442
 */
443
void MainWindow::passShowHandler(const QString &p_output) {
×
444
  QStringList templ = QtPassSettings::isUseTemplate()
×
445
                          ? QtPassSettings::getPassTemplate().split("\n")
×
446
                          : QStringList();
×
447
  bool allFields =
448
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
449
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
450
  QString output = p_output;
451
  QString password = fileContent.getPassword();
×
452

453
  // set clipped text
454
  m_qtPass->setClippedText(password, p_output);
×
455

456
  // first clear the current view:
457
  clearTemplateWidgets();
×
458

459
  // show what is needed:
460
  if (QtPassSettings::isHideContent()) {
×
461
    output = "***" + tr("Content hidden") + "***";
×
462
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
463
    if (!password.isEmpty()) {
×
464
      // set the password, it is hidden if needed in addToGridLayout
465
      addToGridLayout(0, tr("Password"), password);
×
466
    }
467

468
    NamedValues namedValues = fileContent.getNamedValues();
×
469
    for (int j = 0; j < namedValues.length(); ++j) {
×
470
      const NamedValue &nv = namedValues.at(j);
471
      addToGridLayout(j + 1, nv.name, nv.value);
×
472
    }
473
    if (ui->gridLayout->count() == 0) {
×
474
      ui->verticalLayoutPassword->setSpacing(0);
×
475
    } else {
476
      ui->verticalLayoutPassword->setSpacing(6);
×
477
    }
478

479
    output = fileContent.getRemainingDataForDisplay();
×
480
  }
481

482
  if (QtPassSettings::isUseAutoclearPanel()) {
×
483
    clearPanelTimer.start();
×
484
  }
485

486
  emit passShowHandlerFinished(output);
×
487
  setUiElementsEnabled(true);
×
488
}
×
489

490
/**
491
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
492
 * and updating the UI state.
493
 * @example
494
 * void MainWindow::passOtpHandler(const QString &p_output);
495
 *
496
 * @param const QString &p_output - The OTP code text to process; if empty, an
497
 * error message is shown instead.
498
 * @return void - This function does not return a value.
499
 */
500
void MainWindow::passOtpHandler(const QString &p_output) {
×
501
  if (!p_output.isEmpty()) {
×
502
    addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
×
503
    m_qtPass->copyTextToClipboard(p_output);
×
504
    showStatusMessage(tr("OTP code copied to clipboard"));
×
505
  } else {
506
    flashText(tr("No OTP code found in this password entry"), true);
×
507
  }
508
  if (QtPassSettings::isUseAutoclearPanel()) {
×
509
    clearPanelTimer.start();
×
510
  }
511
  setUiElementsEnabled(true);
×
512
}
×
513

514
/**
515
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
516
 */
517
void MainWindow::clearPanel(bool notify) {
×
518
  while (ui->gridLayout->count() > 0) {
×
519
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
520
    delete item->widget();
×
521
    delete item;
×
522
  }
NEW
523
  const bool grepWasVisible = ui->grepResultsList->isVisible();
×
NEW
524
  ui->grepResultsList->clear();
×
NEW
525
  if (grepWasVisible) {
×
NEW
526
    ui->grepResultsList->setVisible(false);
×
NEW
527
    ui->treeView->setVisible(true);
×
NEW
528
    if (m_grepMode) {
×
NEW
529
      m_grepMode = false;
×
NEW
530
      ui->grepButton->blockSignals(true);
×
NEW
531
      ui->grepButton->setChecked(false);
×
NEW
532
      ui->grepButton->blockSignals(false);
×
NEW
533
      ui->lineEdit->blockSignals(true);
×
NEW
534
      ui->lineEdit->clear();
×
NEW
535
      ui->lineEdit->blockSignals(false);
×
NEW
536
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
537
    }
538
  }
539
  if (notify) {
×
540
    QString output = "***" + tr("Password and Content hidden") + "***";
×
541
    ui->textBrowser->setHtml(output);
×
542
  } else {
543
    ui->textBrowser->setHtml("");
×
544
  }
545
}
×
546

547
/**
548
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
549
 * elements
550
 * @param state
551
 */
552
void MainWindow::setUiElementsEnabled(bool state) {
×
553
  ui->treeView->setEnabled(state);
×
554
  ui->lineEdit->setEnabled(state);
×
555
  ui->lineEdit->installEventFilter(this);
×
556
  ui->actionAddPassword->setEnabled(state);
×
557
  ui->actionAddFolder->setEnabled(state);
×
558
  ui->actionUsers->setEnabled(state);
×
559
  ui->actionConfig->setEnabled(state);
×
560
  // is a file selected?
561
  state &= ui->treeView->currentIndex().isValid();
×
562
  ui->actionDelete->setEnabled(state);
×
563
  ui->actionEdit->setEnabled(state);
×
564
  updateGitButtonVisibility();
×
565
  updateOtpButtonVisibility();
×
566
}
×
567

568
/**
569
 * @brief Restores the main window geometry, state, position, size, and
570
 * tray/icon settings from saved application settings.
571
 * @example
572
 * MainWindow window;
573
 * window.restoreWindow();
574
 *
575
 * @return void - This function does not return a value.
576
 */
577
void MainWindow::restoreWindow() {
×
578
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
×
579
  restoreGeometry(geometry);
×
580
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
×
581
  restoreState(savestate);
×
582
  QPoint position = QtPassSettings::getPos(pos());
×
583
  move(position);
×
584
  QSize newSize = QtPassSettings::getSize(size());
×
585
  resize(newSize);
×
586
  if (QtPassSettings::isMaximized(isMaximized())) {
×
587
    showMaximized();
×
588
  }
589

590
  if (QtPassSettings::isAlwaysOnTop()) {
×
591
    Qt::WindowFlags flags = windowFlags();
592
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
593
    show();
×
594
  }
595

596
  if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
597
    initTrayIcon();
×
598
    if (QtPassSettings::isStartMinimized()) {
×
599
      // since we are still in constructor, can't directly hide
600
      QTimer::singleShot(10, this, SLOT(hide()));
×
601
    }
602
  } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
603
    destroyTrayIcon();
×
604
  }
605
}
×
606

607
/**
608
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
609
 */
610
void MainWindow::onConfig() { config(); }
×
611

612
/**
613
 * @brief Executes when the string in the search box changes, collapses the
614
 * TreeView
615
 * @param arg1
616
 */
617
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
NEW
618
  if (m_grepMode)
×
619
    return;
620
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
621
  ui->treeView->expandAll();
×
622
  clearPanel(false);
×
623
  ui->passwordName->setText("");
×
624
  ui->actionEdit->setEnabled(false);
×
625
  ui->actionDelete->setEnabled(false);
×
626
  searchTimer.start();
×
627
}
628

629
/**
630
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
631
 * time from two keypresses is elapsed
632
 */
633
void MainWindow::onTimeoutSearch() {
×
634
  QString query = ui->lineEdit->text();
×
635

636
  if (query.isEmpty()) {
×
637
    ui->treeView->collapseAll();
×
638
    deselect();
×
639
  }
640

641
  query.replace(QStringLiteral(" "), ".*");
×
642
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
643
  proxyModel.setFilterRegularExpression(regExp);
×
644
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
645
      model.setRootPath(QtPassSettings::getPassStore())));
×
646

647
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
648
    selectFirstFile();
×
649
  } else {
650
    ui->actionEdit->setEnabled(false);
×
651
    ui->actionDelete->setEnabled(false);
×
652
  }
653
}
×
654

655
/**
656
 * @brief MainWindow::on_lineEdit_returnPressed get searching
657
 *
658
 * Select the first possible file in the tree
659
 */
660
void MainWindow::on_lineEdit_returnPressed() {
×
661
#ifdef QT_DEBUG
662
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
663
#endif
664

NEW
665
  if (m_grepMode) {
×
NEW
666
    const QString query = ui->lineEdit->text();
×
NEW
667
    if (!query.isEmpty()) {
×
NEW
668
      m_grepCancelled = false;
×
NEW
669
      ui->grepResultsList->clear();
×
NEW
670
      ui->statusBar->showMessage(tr("Searching…"));
×
NEW
671
      if (!m_grepBusy) {
×
NEW
672
        m_grepBusy = true;
×
NEW
673
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
674
      }
NEW
675
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
676
    } else {
NEW
677
      m_grepCancelled = true;
×
NEW
678
      if (m_grepBusy) {
×
NEW
679
        m_grepBusy = false;
×
NEW
680
        QApplication::restoreOverrideCursor();
×
681
      }
NEW
682
      ui->grepResultsList->clear();
×
NEW
683
      ui->grepResultsList->setVisible(false);
×
NEW
684
      ui->treeView->setVisible(true);
×
685
    }
686
    return;
687
  }
688

689
  if (proxyModel.rowCount() > 0) {
×
690
    selectFirstFile();
×
691
    on_treeView_clicked(ui->treeView->currentIndex());
×
692
  }
693
}
694

695
/**
696
 * @brief Toggle grep (content search) mode.
697
 */
NEW
698
void MainWindow::on_grepButton_toggled(bool checked) {
×
NEW
699
  m_grepMode = checked;
×
NEW
700
  if (checked) {
×
NEW
701
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
NEW
702
    ui->lineEdit->clear();
×
NEW
703
    searchTimer.stop();
×
NEW
704
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
NEW
705
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
NEW
706
        model.setRootPath(QtPassSettings::getPassStore())));
×
NEW
707
    ui->grepResultsList->setVisible(false);
×
708
    // Keep treeView visible until results arrive
709
  } else {
NEW
710
    if (m_grepBusy) {
×
NEW
711
      m_grepBusy = false;
×
NEW
712
      m_grepCancelled = true;
×
NEW
713
      QApplication::restoreOverrideCursor();
×
714
    }
NEW
715
    searchTimer.stop();
×
NEW
716
    ui->lineEdit->blockSignals(true);
×
NEW
717
    ui->lineEdit->clear();
×
NEW
718
    ui->lineEdit->blockSignals(false);
×
NEW
719
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
NEW
720
    ui->grepResultsList->clear();
×
NEW
721
    ui->grepResultsList->setVisible(false);
×
NEW
722
    ui->treeView->setVisible(true);
×
NEW
723
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
NEW
724
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
NEW
725
        model.setRootPath(QtPassSettings::getPassStore())));
×
726
  }
NEW
727
}
×
728

729
/**
730
 * @brief Display grep results in grepResultsList.
731
 */
NEW
732
void MainWindow::onGrepFinished(
×
733
    const QList<QPair<QString, QStringList>> &results) {
NEW
734
  if (m_grepBusy) {
×
NEW
735
    m_grepBusy = false;
×
NEW
736
    QApplication::restoreOverrideCursor();
×
737
  }
NEW
738
  if (m_grepCancelled) {
×
NEW
739
    m_grepCancelled = false;
×
NEW
740
    return;
×
741
  }
NEW
742
  setUiElementsEnabled(true);
×
NEW
743
  if (!m_grepMode)
×
744
    return;
NEW
745
  ui->grepResultsList->clear();
×
NEW
746
  if (results.isEmpty()) {
×
NEW
747
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
NEW
748
    ui->grepResultsList->setVisible(false);
×
NEW
749
    ui->treeView->setVisible(true);
×
NEW
750
    return;
×
751
  }
NEW
752
  const bool hideContent = QtPassSettings::isHideContent();
×
753
  int totalLines = 0;
NEW
754
  for (const auto &pair : results) {
×
NEW
755
    QTreeWidgetItem *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
NEW
756
    entryItem->setText(0, pair.first);
×
NEW
757
    entryItem->setData(0, Qt::UserRole, pair.first);
×
NEW
758
    for (const QString &line : pair.second) {
×
NEW
759
      QTreeWidgetItem *lineItem = new QTreeWidgetItem(entryItem);
×
NEW
760
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
761
                                       : line);
NEW
762
      lineItem->setData(0, Qt::UserRole, pair.first);
×
NEW
763
      ++totalLines;
×
764
    }
765
  }
NEW
766
  ui->grepResultsList->expandAll();
×
NEW
767
  ui->treeView->setVisible(false);
×
NEW
768
  ui->grepResultsList->setVisible(true);
×
NEW
769
  ui->statusBar->showMessage(
×
NEW
770
      tr("Found %n match(es) in %1 entr(ies).", nullptr, totalLines)
×
NEW
771
          .arg(results.size()),
×
772
      3000);
NEW
773
  if (QtPassSettings::isUseAutoclearPanel())
×
NEW
774
    clearPanelTimer.start();
×
775
}
776

777
/**
778
 * @brief Navigate to the password entry when a grep result is clicked.
779
 */
NEW
780
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
781
                                                int /*column*/) {
NEW
782
  const QString entry = item->data(0, Qt::UserRole).toString();
×
NEW
783
  if (entry.isEmpty())
×
784
    return;
785
  const QString fullPath = QDir::cleanPath(
NEW
786
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
NEW
787
  QModelIndex srcIndex = model.index(fullPath);
×
788
  if (!srcIndex.isValid())
789
    return;
NEW
790
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
791
  if (!proxyIndex.isValid())
792
    return;
NEW
793
  ui->treeView->setCurrentIndex(proxyIndex);
×
NEW
794
  on_treeView_clicked(proxyIndex);
×
NEW
795
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
NEW
796
    ui->grepResultsList->clear();
×
NEW
797
  ui->grepResultsList->setVisible(false);
×
NEW
798
  ui->treeView->setVisible(true);
×
NEW
799
  ui->treeView->scrollTo(proxyIndex);
×
NEW
800
  ui->treeView->setFocus();
×
801
}
802

803
/**
804
 * @brief MainWindow::selectFirstFile select the first possible file in the
805
 * tree
806
 */
807
void MainWindow::selectFirstFile() {
×
808
  QModelIndex index = proxyModel.mapFromSource(
×
809
      model.setRootPath(QtPassSettings::getPassStore()));
×
810
  index = firstFile(index);
×
811
  ui->treeView->setCurrentIndex(index);
×
812
}
×
813

814
/**
815
 * @brief MainWindow::firstFile return location of first possible file
816
 * @param parentIndex
817
 * @return QModelIndex
818
 */
819
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
820
  QModelIndex index = parentIndex;
×
821
  int numRows = proxyModel.rowCount(parentIndex);
×
822
  for (int row = 0; row < numRows; ++row) {
×
823
    index = proxyModel.index(row, 0, parentIndex);
×
824
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
825
      return index;
×
826
    }
827
    if (proxyModel.hasChildren(index)) {
×
828
      return firstFile(index);
×
829
    }
830
  }
831
  return index;
×
832
}
833

834
/**
835
 * @brief MainWindow::setPassword open passworddialog
836
 * @param file which pgp file
837
 * @param isNew insert (not update)
838
 */
839
void MainWindow::setPassword(const QString &file, bool isNew) {
×
840
  PasswordDialog d(file, isNew, this);
×
841

842
  if (!d.exec()) {
×
843
    ui->treeView->setFocus();
×
844
  }
845
}
×
846

847
/**
848
 * @brief MainWindow::addPassword add a new password by showing a
849
 * number of dialogs.
850
 */
851
void MainWindow::addPassword() {
×
852
  bool ok;
853
  QString dir =
854
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
855
  QString file =
856
      QInputDialog::getText(this, tr("New file"),
×
857
                            tr("New password file: \n(Will be placed in %1 )")
×
858
                                .arg(QtPassSettings::getPassStore() +
×
859
                                     Util::getDir(ui->treeView->currentIndex(),
×
860
                                                  true, model, proxyModel)),
861
                            QLineEdit::Normal, "", &ok);
×
862
  if (!ok || file.isEmpty()) {
×
863
    return;
864
  }
865
  file = dir + file;
×
866
  setPassword(file);
×
867
}
868

869
/**
870
 * @brief MainWindow::onDelete remove password, if you are
871
 * sure.
872
 */
873
void MainWindow::onDelete() {
×
874
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
875
  if (!currentIndex.isValid()) {
876
    // This fixes https://github.com/IJHack/QtPass/issues/556
877
    // Otherwise the entire password directory would be deleted if
878
    // nothing is selected in the tree view.
879
    return;
×
880
  }
881

882
  QFileInfo fileOrFolder =
883
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
884
  QString file = "";
×
885
  bool isDir = false;
886

887
  if (fileOrFolder.isFile()) {
×
888
    file = getFile(ui->treeView->currentIndex(), true);
×
889
  } else {
890
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
891
    isDir = true;
892
  }
893

894
  QString dirMessage = tr(" and the whole content?");
895
  if (isDir) {
×
896
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
897
                    QDirIterator::Subdirectories);
×
898
    bool okDir = true;
899
    while (it.hasNext() && okDir) {
×
900
      it.next();
×
901
      if (QFileInfo(it.filePath()).isFile()) {
×
902
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
903
          okDir = false;
904
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
905
                          "there are unexpected files in the given folder, "
906
                          "check them before continue.</strong>");
907
        }
908
      }
909
    }
910
  }
×
911

912
  if (QMessageBox::question(
×
913
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
914
          tr("Are you sure you want to delete %1%2?")
×
915
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
916
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
917
    return;
918
  }
919

920
  QtPassSettings::getPass()->Remove(file, isDir);
×
921
}
×
922

923
/**
924
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
925
 */
926
void MainWindow::onOtp() {
×
927
  QString file = getFile(ui->treeView->currentIndex(), true);
×
928
  if (!file.isEmpty()) {
×
929
    if (QtPassSettings::isUseOtp()) {
×
930
      setUiElementsEnabled(false);
×
931
      QtPassSettings::getPass()->OtpGenerate(file);
×
932
    }
933
  } else {
934
    flashText(tr("No password selected for OTP generation"), true);
×
935
  }
936
}
×
937

938
/**
939
 * @brief MainWindow::onEdit try and edit (selected) password.
940
 */
941
void MainWindow::onEdit() {
×
942
  QString file = getFile(ui->treeView->currentIndex(), true);
×
943
  editPassword(file);
×
944
}
×
945

946
/**
947
 * @brief MainWindow::userDialog see MainWindow::onUsers()
948
 * @param dir folder to edit users for.
949
 */
950
void MainWindow::userDialog(const QString &dir) {
×
951
  if (!dir.isEmpty()) {
×
952
    currentDir = dir;
×
953
  }
954
  onUsers();
×
955
}
×
956

957
/**
958
 * @brief MainWindow::onUsers edit users for the current
959
 * folder,
960
 * gets lists and opens UserDialog.
961
 */
962
void MainWindow::onUsers() {
×
963
  QString dir =
964
      currentDir.isEmpty()
965
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
966
          : currentDir;
×
967

968
  UsersDialog d(dir, this);
×
969
  if (!d.exec()) {
×
970
    ui->treeView->setFocus();
×
971
  }
972
}
×
973

974
/**
975
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
976
 * @param message
977
 */
978
void MainWindow::messageAvailable(const QString &message) {
×
979
  if (message.isEmpty()) {
×
980
    focusInput();
×
981
  } else {
982
    ui->treeView->expandAll();
×
983
    ui->lineEdit->setText(message);
×
984
    on_lineEdit_returnPressed();
×
985
  }
986
  show();
×
987
  raise();
×
988
}
×
989

990
/**
991
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
992
 * @param batch
993
 * @param keygenWindow
994
 */
995
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
996
  keygen = keygenWindow;
×
997
  emit generateGPGKeyPair(batch);
×
998
}
×
999

1000
/**
1001
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1002
 * select a more appropriate one to view too
1003
 */
1004
void MainWindow::updateProfileBox() {
×
1005
  QHash<QString, QHash<QString, QString>> profiles =
1006
      QtPassSettings::getProfiles();
×
1007

1008
  if (profiles.isEmpty()) {
1009
    ui->profileWidget->hide();
×
1010
  } else {
1011
    ui->profileWidget->show();
×
1012
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1013
    ui->profileBox->clear();
×
1014
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
×
1015
    while (i.hasNext()) {
×
1016
      i.next();
1017
      if (!i.key().isEmpty()) {
×
1018
        ui->profileBox->addItem(i.key());
×
1019
      }
1020
    }
1021
    ui->profileBox->model()->sort(0);
×
1022
  }
1023
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
×
1024
  if (index != -1) { //  -1 for not found
×
1025
    ui->profileBox->setCurrentIndex(index);
×
1026
  }
1027
}
×
1028

1029
/**
1030
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1031
 * correct "profile"
1032
 * @param name
1033
 */
1034
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1035
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1036
#else
1037
/**
1038
 * @brief Handles changes to the selected profile in the profile combo box.
1039
 * @details Ignores the event during a fresh start or when the selected profile
1040
 * matches the current profile. Otherwise, it clears the password field, updates
1041
 * the active profile and related settings, refreshes the environment, and
1042
 * resets the tree view and action states to reflect the newly selected profile.
1043
 *
1044
 * @param name - The newly selected profile name.
1045
 * @return void - This function does not return a value.
1046
 *
1047
 */
1048
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1049
#endif
1050
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
1051
    return;
×
1052
  }
1053

1054
  ui->lineEdit->clear();
×
1055

1056
  QtPassSettings::setProfile(name);
×
1057

1058
  QtPassSettings::setPassStore(
×
1059
      QtPassSettings::getProfiles().value(name).value("path"));
×
1060
  QtPassSettings::setPassSigningKey(
×
1061
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1062
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1063

1064
  QtPassSettings::getPass()->updateEnv();
×
1065

1066
  const QString passStore = QtPassSettings::getPassStore();
×
1067
  proxyModel.setStore(passStore);
×
1068
  ui->treeView->setRootIndex(
×
1069
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1070
  deselect();
×
1071
  ui->treeView->setCurrentIndex(QModelIndex());
×
1072
}
1073

1074
/**
1075
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1076
 * support
1077
 * it
1078
 */
1079
void MainWindow::initTrayIcon() {
×
1080
  this->tray = new TrayIcon(this);
×
1081
  // Setup tray icon
1082

1083
  if (tray == nullptr) {
1084
#ifdef QT_DEBUG
1085
    dbg() << "Allocating tray icon failed.";
1086
#endif
1087
  }
1088

1089
  if (!tray->getIsAllocated()) {
×
1090
    destroyTrayIcon();
×
1091
  }
1092
}
×
1093

1094
/**
1095
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1096
 */
1097
void MainWindow::destroyTrayIcon() {
×
1098
  delete this->tray;
×
1099
  tray = nullptr;
×
1100
}
×
1101

1102
/**
1103
 * @brief MainWindow::closeEvent hide or quit
1104
 * @param event
1105
 */
1106
void MainWindow::closeEvent(QCloseEvent *event) {
×
1107
  if (QtPassSettings::isHideOnClose()) {
×
1108
    this->hide();
×
1109
    event->ignore();
1110
  } else {
1111
    m_qtPass->clearClipboard();
×
1112

1113
    QtPassSettings::setGeometry(saveGeometry());
×
1114
    QtPassSettings::setSavestate(saveState());
×
1115
    QtPassSettings::setMaximized(isMaximized());
×
1116
    if (!isMaximized()) {
×
1117
      QtPassSettings::setPos(pos());
×
1118
      QtPassSettings::setSize(size());
×
1119
    }
1120
    event->accept();
1121
  }
1122
}
×
1123

1124
/**
1125
 * @brief MainWindow::eventFilter filter out some events and focus the
1126
 * treeview
1127
 * @param obj
1128
 * @param event
1129
 * @return
1130
 */
1131
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
×
1132
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
×
1133
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1134
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1135
      ui->treeView->setFocus();
×
1136
    }
1137
  }
1138
  return QObject::eventFilter(obj, event);
×
1139
}
1140

1141
/**
1142
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1143
 * @param event
1144
 */
1145
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1146
  switch (event->key()) {
×
1147
  case Qt::Key_Delete:
×
1148
    onDelete();
×
1149
    break;
×
1150
  case Qt::Key_Return:
×
1151
  case Qt::Key_Enter:
1152
    if (proxyModel.rowCount() > 0) {
×
1153
      on_treeView_clicked(ui->treeView->currentIndex());
×
1154
    }
1155
    break;
1156
  case Qt::Key_Escape:
×
1157
    ui->lineEdit->clear();
×
1158
    break;
×
1159
  default:
1160
    break;
1161
  }
1162
}
×
1163

1164
/**
1165
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1166
 * menu
1167
 * @param pos
1168
 */
1169
void MainWindow::showContextMenu(const QPoint &pos) {
×
1170
  QModelIndex index = ui->treeView->indexAt(pos);
×
1171
  bool selected = true;
1172
  if (!index.isValid()) {
1173
    ui->treeView->clearSelection();
×
1174
    ui->actionDelete->setEnabled(false);
×
1175
    ui->actionEdit->setEnabled(false);
×
1176
    currentDir = "";
×
1177
    selected = false;
1178
  }
1179

1180
  ui->treeView->setCurrentIndex(index);
×
1181

1182
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1183

1184
  QFileInfo fileOrFolder =
1185
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1186

1187
  QMenu contextMenu;
×
1188
  if (!selected || fileOrFolder.isDir()) {
×
1189
    QAction *openFolder =
1190
        contextMenu.addAction(tr("Open folder with file manager"));
×
1191
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1192
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1193
    QAction *users = contextMenu.addAction(tr("Users"));
×
1194
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1195
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1196
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1197
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1198
  } else if (fileOrFolder.isFile()) {
×
1199
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1200
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1201
  }
1202
  if (selected) {
×
1203
    contextMenu.addSeparator();
×
1204
    if (fileOrFolder.isDir()) {
×
1205
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1206
      connect(renameFolder, &QAction::triggered, this,
×
1207
              &MainWindow::renameFolder);
×
1208
    } else if (fileOrFolder.isFile()) {
×
1209
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1210
      connect(renamePassword, &QAction::triggered, this,
×
1211
              &MainWindow::renamePassword);
×
1212
    }
1213
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1214
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1215
    if (fileOrFolder.isDir()) {
×
1216
      QString dirPath = QDir::cleanPath(
1217
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1218
      QAction *reencrypt = contextMenu.addAction(tr("Re-encrypt"));
×
1219
      connect(reencrypt, &QAction::triggered, this,
×
1220
              [this, dirPath]() { reencryptPath(dirPath); });
×
1221
    }
1222
  }
1223
  contextMenu.exec(globalPos);
×
1224
}
×
1225

1226
/**
1227
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1228
 * password window
1229
 * @param pos
1230
 */
1231
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1232
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1233
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1234

1235
  contextMenu->exec(globalPos);
×
1236
  delete contextMenu;
×
1237
}
×
1238

1239
/**
1240
 * @brief MainWindow::openFolder open the folder in the default file manager
1241
 */
1242
void MainWindow::openFolder() {
×
1243
  QString dir =
1244
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1245

1246
  QString path = QDir::toNativeSeparators(dir);
×
1247
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1248
}
×
1249

1250
/**
1251
 * @brief MainWindow::addFolder add a new folder to store passwords in
1252
 */
1253
void MainWindow::addFolder() {
×
1254
  bool ok;
1255
  QString dir =
1256
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1257
  QString newdir =
1258
      QInputDialog::getText(this, tr("New file"),
×
1259
                            tr("New Folder: \n(Will be placed in %1 )")
×
1260
                                .arg(QtPassSettings::getPassStore() +
×
1261
                                     Util::getDir(ui->treeView->currentIndex(),
×
1262
                                                  true, model, proxyModel)),
1263
                            QLineEdit::Normal, "", &ok);
×
1264
  if (!ok || newdir.isEmpty()) {
×
1265
    return;
1266
  }
1267
  newdir.prepend(dir);
1268
  if (!QDir().mkdir(newdir)) {
×
1269
    QMessageBox::warning(this, tr("Error"),
×
1270
                         tr("Failed to create folder: %1").arg(newdir));
×
1271
    return;
×
1272
  }
1273
  if (QtPassSettings::isAddGPGId(true)) {
×
1274
    QString gpgIdFile = newdir + "/.gpg-id";
×
1275
    QFile gpgId(gpgIdFile);
×
1276
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1277
      QMessageBox::warning(
×
1278
          this, tr("Error"),
×
1279
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1280
      return;
1281
    }
1282
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1283
    for (const UserInfo &user : users) {
×
1284
      if (user.enabled) {
×
1285
        gpgId.write((user.key_id + "\n").toUtf8());
×
1286
      }
1287
    }
1288
    gpgId.close();
×
1289
  }
×
1290
}
1291

1292
/**
1293
 * @brief MainWindow::renameFolder rename an existing folder
1294
 */
1295
void MainWindow::renameFolder() {
×
1296
  bool ok;
1297
  QString srcDir = QDir::cleanPath(
1298
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1299
  QString srcDirName = QDir(srcDir).dirName();
×
1300
  QString newName =
1301
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1302
                            QLineEdit::Normal, srcDirName, &ok);
×
1303
  if (!ok || newName.isEmpty()) {
×
1304
    return;
1305
  }
1306
  QString destDir = srcDir;
1307
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1308
  QtPassSettings::getPass()->Move(srcDir, destDir);
×
1309
}
1310

1311
/**
1312
 * @brief MainWindow::editPassword read password and open edit window via
1313
 * MainWindow::onEdit()
1314
 */
1315
void MainWindow::editPassword(const QString &file) {
×
1316
  if (!file.isEmpty()) {
×
1317
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1318
      onUpdate(true);
×
1319
    }
1320
    setPassword(file, false);
×
1321
  }
1322
}
×
1323

1324
/**
1325
 * @brief MainWindow::renamePassword rename an existing password
1326
 */
1327
void MainWindow::renamePassword() {
×
1328
  bool ok;
1329
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1330
  QString filePath = QFileInfo(file).path();
×
1331
  QString fileName = QFileInfo(file).fileName();
×
1332
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1333
    fileName.chop(4);
×
1334
  }
1335

1336
  QString newName =
1337
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1338
                            QLineEdit::Normal, fileName, &ok);
×
1339
  if (!ok || newName.isEmpty()) {
×
1340
    return;
1341
  }
1342
  QString newFile = QDir(filePath).filePath(newName);
×
1343
  QtPassSettings::getPass()->Move(file, newFile);
×
1344
}
1345

1346
/**
1347
 * @brief MainWindow::clearTemplateWidgets empty the template widget fields in
1348
 * the UI
1349
 */
1350
void MainWindow::clearTemplateWidgets() {
×
1351
  while (ui->gridLayout->count() > 0) {
×
1352
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
1353
    delete item->widget();
×
1354
    delete item;
×
1355
  }
1356
  ui->verticalLayoutPassword->setSpacing(0);
×
1357
}
×
1358

1359
/**
1360
 * @brief Copies the password of the selected file from the tree view to the
1361
 * clipboard.
1362
 * @example
1363
 * MainWindow::copyPasswordFromTreeview();
1364
 *
1365
 * @return void - This function does not return a value.
1366
 */
1367
void MainWindow::copyPasswordFromTreeview() {
×
1368
  QFileInfo fileOrFolder =
1369
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1370

1371
  if (fileOrFolder.isFile()) {
×
1372
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1373
    // Disconnect any previous connection to avoid accumulation
1374
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1375
               &MainWindow::passwordFromFileToClipboard);
1376
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1377
            &MainWindow::passwordFromFileToClipboard);
×
1378
    QtPassSettings::getPass()->Show(file);
×
1379
  }
1380
}
×
1381

1382
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1383
  QStringList tokens = text.split('\n');
×
1384
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1385
}
×
1386

1387
/**
1388
 * @brief MainWindow::addToGridLayout add a field to the template grid
1389
 * @param position
1390
 * @param field
1391
 * @param value
1392
 */
1393
void MainWindow::addToGridLayout(int position, const QString &field,
×
1394
                                 const QString &value) {
1395
  QString trimmedField = field.trimmed();
1396
  QString trimmedValue = value.trimmed();
1397

1398
  const QString buttonStyle =
1399
      "border-style: none; background: transparent; padding: 0; margin: 0; "
1400
      "icon-size: 16px; color: inherit;";
×
1401

1402
  // Combine the Copy button and the line edit in one widget
1403
  auto *frame = new QFrame();
×
1404
  QLayout *ly = new QHBoxLayout();
×
1405
  ly->setContentsMargins(5, 2, 2, 2);
×
1406
  ly->setSpacing(0);
×
1407
  frame->setLayout(ly);
×
1408
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER) {
×
1409
    auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
×
1410
    connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
×
1411
            &QtPass::copyTextToClipboard);
×
1412

1413
    fieldLabel->setStyleSheet(buttonStyle);
×
1414
    frame->layout()->addWidget(fieldLabel);
×
1415
  }
1416

1417
  if (QtPassSettings::isUseQrencode()) {
×
1418
    auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
×
1419
    connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
×
1420
            &QtPass::showTextAsQRCode);
×
1421
    qrbutton->setStyleSheet(buttonStyle);
×
1422
    frame->layout()->addWidget(qrbutton);
×
1423
  }
1424

1425
  // set the echo mode to password, if the field is "password"
1426
  const QString lineStyle =
1427
      QtPassSettings::isUseMonospace()
×
1428
          ? "border-style: none; background: transparent; font-family: "
1429
            "monospace;"
1430
          : "border-style: none; background: transparent;";
×
1431

1432
  if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
×
1433
    auto *line = new QLineEdit();
×
1434
    line->setObjectName(trimmedField);
×
1435
    line->setText(trimmedValue);
×
1436
    line->setReadOnly(true);
×
1437
    line->setStyleSheet(lineStyle);
×
1438
    line->setContentsMargins(0, 0, 0, 0);
×
1439
    line->setEchoMode(QLineEdit::Password);
×
1440
    auto *showButton = new QPushButtonShowPassword(line, this);
×
1441
    showButton->setStyleSheet(buttonStyle);
×
1442
    showButton->setContentsMargins(0, 0, 0, 0);
×
1443
    frame->layout()->addWidget(showButton);
×
1444
    frame->layout()->addWidget(line);
×
1445
  } else {
1446
    auto *line = new QTextBrowser();
×
1447
    line->setOpenExternalLinks(true);
×
1448
    line->setOpenLinks(true);
×
1449
    line->setMaximumHeight(26);
×
1450
    line->setMinimumHeight(26);
×
1451
    line->setSizePolicy(
×
1452
        QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1453
    line->setObjectName(trimmedField);
×
1454
    trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
1455
    line->setText(trimmedValue);
×
1456
    line->setReadOnly(true);
×
1457
    line->setStyleSheet(lineStyle);
×
1458
    line->setContentsMargins(0, 0, 0, 0);
×
1459
    frame->layout()->addWidget(line);
×
1460
  }
1461

1462
  frame->setStyleSheet(
×
1463
      ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1464

1465
  // set into the layout
1466
  ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
×
1467
  ui->gridLayout->addWidget(frame, position, 1);
×
1468
}
×
1469

1470
/**
1471
 * @brief Displays message in status bar
1472
 *
1473
 * @param msg     text to be displayed
1474
 * @param timeout time for which msg shall be visible
1475
 */
1476
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
×
1477
  ui->statusBar->showMessage(msg, timeout);
×
1478
}
×
1479

1480
/**
1481
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1482
 * @param dir Directory path to re-encrypt
1483
 */
1484
void MainWindow::reencryptPath(const QString &dir) {
×
1485
  QDir checkDir(dir);
×
1486
  if (!checkDir.exists()) {
×
1487
    QMessageBox::critical(this, tr("Error"),
×
1488
                          tr("Directory does not exist: %1").arg(dir));
×
1489
    return;
×
1490
  }
1491

1492
  int ret = QMessageBox::question(
×
1493
      this, tr("Re-encrypt passwords"),
×
1494
      tr("Re-encrypt all passwords in %1?\n\n"
×
1495
         "This will re-encrypt ALL password files in this folder "
1496
         "using the current recipients defined in .gpg-id.\n\n"
1497
         "This may rewrite many files and cannot be undone easily.\n\n"
1498
         "Continue?")
1499
          .arg(QDir(dir).dirName()),
×
1500
      QMessageBox::Yes | QMessageBox::No);
1501

1502
  if (ret != QMessageBox::Yes)
×
1503
    return;
1504

1505
  // Prevent double execution - use same method as startReencryptPath
1506
  setUiElementsEnabled(false);
×
1507
  ui->treeView->setDisabled(true);
×
1508

1509
  QtPassSettings::getImitatePass()->reencryptPath(
×
1510
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1511
}
×
1512

1513
/**
1514
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1515
 */
1516
void MainWindow::startReencryptPath() {
×
1517
  setUiElementsEnabled(false);
×
1518
  ui->treeView->setDisabled(true);
×
1519
}
×
1520

1521
/**
1522
 * @brief MainWindow::endReencryptPath re-enable ui elements
1523
 */
1524
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1525

1526
void MainWindow::updateGitButtonVisibility() {
×
1527
  if (!QtPassSettings::isUseGit() ||
×
1528
      (QtPassSettings::getGitExecutable().isEmpty() &&
×
1529
       QtPassSettings::getPassExecutable().isEmpty())) {
×
1530
    enableGitButtons(false);
×
1531
  } else {
1532
    enableGitButtons(true);
×
1533
  }
1534
}
×
1535

1536
void MainWindow::updateOtpButtonVisibility() {
×
1537
#if defined(Q_OS_WIN) || defined(__APPLE__)
1538
  ui->actionOtp->setVisible(false);
1539
#endif
1540
  if (!QtPassSettings::isUseOtp()) {
×
1541
    ui->actionOtp->setEnabled(false);
×
1542
  } else {
1543
    ui->actionOtp->setEnabled(true);
×
1544
  }
1545
}
×
1546

NEW
1547
void MainWindow::updateGrepButtonVisibility() {
×
NEW
1548
  const bool enabled = QtPassSettings::isUseGrepSearch();
×
NEW
1549
  ui->grepButton->setVisible(enabled);
×
NEW
1550
  ui->grepCaseButton->setVisible(enabled);
×
NEW
1551
  if (!enabled && m_grepMode) {
×
NEW
1552
    ui->grepButton->setChecked(false);
×
1553
  }
NEW
1554
}
×
1555

UNCOV
1556
void MainWindow::enableGitButtons(const bool &state) {
×
1557
  // Following GNOME guidelines is preferable disable buttons instead of hide
1558
  ui->actionPush->setEnabled(state);
×
1559
  ui->actionUpdate->setEnabled(state);
×
1560
}
×
1561

1562
/**
1563
 * @brief MainWindow::critical critical message popup wrapper.
1564
 * @param title
1565
 * @param msg
1566
 */
1567
void MainWindow::critical(const QString &title, const QString &msg) {
×
1568
  QMessageBox::critical(this, title, msg);
×
1569
}
×
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