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

IJHack / QtPass / 24970081225

26 Apr 2026 11:45PM UTC coverage: 27.593%. Remained the same
24970081225

push

github

web-flow
fix: move processOutputWidget inside centralWidget so it's actually wired up (#1192)

#1172 added the process output panel as a sibling of centralWidget at
the QMainWindow level:

  <widget class="QMainWindow" name="MainWindow">
    <widget class="QWidget" name="centralWidget"> ... </widget>
    <widget class="QWidget" name="processOutputWidget"> ... </widget>  <-- here
    <widget class="QStatusBar" name="statusBar"/>
    <widget class="QToolBar" name="toolBar"> ... </widget>
  </widget>

QMainWindow recognises a fixed set of named slots for its top-level
children — centralWidget, statusBar, menuBar, toolBars and dock widgets.
Anything else parented at that level is created by uic but never placed
in the QMainWindow's layout, so it gets ignored by the QMainWindowLayout
and renders on top of (i.e. obscures) the centralWidget content.

The visible symptom is that with HEAD, the QtPass main window comes up
with the toolbar at top and the status bar at the bottom but a *blank*
centre — no profile selector, no search box, no tree view, no text
browser. Bisected against pre-#1172 (`8e257d633`) where the same
.ui structure (modulo this addition) renders correctly.

Move processOutputWidget to row=2/colspan=2 inside centralWidget's
QGridLayout (where it's a real layout participant) and let it stretch
across the bottom of the central area. The runtime call to
`statusBar()->addPermanentWidget(ui->processOutputWidget)` from
initStatusBar() then re-parents it to the status bar; at that point
QMainWindow's layout reclaims the row 2 space for the actual content
above it.

Verified by screenshot: search box, tree view, profile selector and
welcome browser all reappear; status bar with the lock icon still
shows; no test regressions (12/12 suites green); doxygen clean.

0 of 34 new or added lines in 1 file covered. (0.0%)

65 existing lines in 2 files now uncovered.

1825 of 6614 relevant lines covered (27.59%)

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

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

57
  m_qtPass = new QtPass(this);
×
58

59
  // register shortcut ctrl/cmd + Q to close the main window
60
  new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
×
61
  // register shortcut ctrl/cmd + C to copy the currently selected password
62
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this,
×
63
                SLOT(copyPasswordFromTreeview()));
×
64

65
  model.setNameFilters(QStringList() << "*.gpg");
×
66
  model.setNameFilterDisables(false);
×
67

68
  /*
69
   * I added this to solve Windows bug but now on GNU/Linux the main folder,
70
   * if hidden, disappear
71
   *
72
   * model.setFilter(QDir::NoDot);
73
   */
74

75
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
×
76

77
  QModelIndex rootDir = model.setRootPath(passStore);
×
78
  model.fetchMore(rootDir);
×
79

80
  proxyModel.setModelAndStore(&model, passStore);
×
81
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
×
82

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

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

112
  updateProfileBox();
×
113

114
  QtPassSettings::getPass()->updateEnv();
×
115
  clearPanelTimer.setInterval(MS_PER_SECOND *
×
116
                              QtPassSettings::getAutoclearPanelSeconds());
×
117
  clearPanelTimer.setSingleShot(true);
×
118
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
×
119

120
  searchTimer.setInterval(350);
×
121
  searchTimer.setSingleShot(true);
×
122

123
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
×
124

125
  initToolBarButtons();
×
126
  initStatusBar();
×
127

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

NEW
149
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderPressed,
×
NEW
150
          this, [this]() {
×
NEW
151
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
152
            m_autoScroll = sb->value() >= sb->maximum();
×
153
          });
×
NEW
154
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
×
155
          this, [this]() {
×
NEW
156
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
157
            m_autoScroll = sb->value() >= sb->maximum();
×
158
          });
×
159

160
  ui->lineEdit->setClearButtonEnabled(true);
×
161
  updateGrepButtonVisibility();
×
162

163
  setUiElementsEnabled(true);
×
164

165
  ui->lineEdit->setText(searchText);
×
166

167
  if (!m_qtPass->init()) {
×
168
    // no working config so this should just quit
169
    QApplication::quit();
×
170
    return;
171
  }
172

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

182
MainWindow::~MainWindow() { delete m_qtPass; }
×
183

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

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

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

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

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

295
/**
296
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
297
 */
298
void MainWindow::initStatusBar() {
×
299
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
×
300

301
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
×
302
                     .scaledToHeight(statusBar()->height());
×
303
  auto *logoApp = new QLabel(statusBar());
×
304
  logoApp->setPixmap(logo);
×
305
  statusBar()->addPermanentWidget(logoApp);
×
306

307
  // Build the process-output panel programmatically rather than from .ui:
308
  // QMainWindow's uic only places its top-level children into the
309
  // centralWidget / statusBar / menuBar / toolBars / dock-widget slots.
310
  // Defining processOutputWidget at that level in the .ui leaves it
311
  // unplaced and obscuring centralWidget; nesting it inside centralWidget
312
  // and reparenting on the fly works but leaves a residual empty grid
313
  // slot below the main content. Owning the construction here keeps the
314
  // widget's lifetime tied to the status bar from the start.
NEW
315
  m_processOutputWidget = new QWidget(statusBar());
×
NEW
316
  m_processOutputWidget->setObjectName(QStringLiteral("processOutputWidget"));
×
NEW
317
  auto *outputLayout = new QHBoxLayout(m_processOutputWidget);
×
NEW
318
  outputLayout->setObjectName(QStringLiteral("processOutputLayout"));
×
NEW
319
  outputLayout->setContentsMargins(0, 0, 0, 0);
×
NEW
320
  m_clearOutputButton = new QToolButton(m_processOutputWidget);
×
NEW
321
  m_clearOutputButton->setObjectName(QStringLiteral("clearOutputButton"));
×
NEW
322
  m_clearOutputButton->setText(tr("Clear"));
×
NEW
323
  m_clearOutputButton->setToolTip(tr("Clear output"));
×
NEW
324
  outputLayout->addWidget(m_clearOutputButton);
×
NEW
325
  m_processOutputEdit = new QTextEdit(m_processOutputWidget);
×
NEW
326
  m_processOutputEdit->setObjectName(QStringLiteral("processOutputEdit"));
×
NEW
327
  m_processOutputEdit->setReadOnly(true);
×
NEW
328
  m_processOutputEdit->setAcceptRichText(false);
×
NEW
329
  m_processOutputEdit->setMinimumSize(0, 80);
×
NEW
330
  m_processOutputEdit->setMaximumSize(QWIDGETSIZE_MAX, 150);
×
NEW
331
  outputLayout->addWidget(m_processOutputEdit);
×
332
  // Hide before adding so the status bar doesn't pre-allocate the panel's
333
  // 80 px minimum height when the user has the option turned off.
NEW
334
  m_processOutputWidget->setVisible(QtPassSettings::isShowProcessOutput());
×
NEW
335
  statusBar()->addPermanentWidget(m_processOutputWidget);
×
NEW
336
  connect(m_clearOutputButton, &QToolButton::clicked, this,
×
NEW
337
          &MainWindow::on_clearOutputButton_clicked);
×
UNCOV
338
}
×
339

340
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
341
  return ui->treeView->currentIndex();
×
342
}
343

344
void MainWindow::cleanKeygenDialog() {
×
345
  if (this->keygenDialog != nullptr) {
×
346
    this->keygenDialog->close();
×
347
  }
348
  this->keygenDialog = nullptr;
×
349
}
×
350

351
/**
352
 * @brief Displays the given text in the main window text browser, optionally
353
 * marking it as an error and/or rendering it as HTML.
354
 * @example
355
 * MainWindow window;
356
 * window.flashText("Operation completed.", false, false);
357
 *
358
 * @param const QString &text - The text content to display.
359
 * @param const bool isError - If true, sets the text color to red before
360
 * displaying the text.
361
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
362
 * the existing HTML content.
363
 * @return void - No return value.
364
 */
365
void MainWindow::flashText(const QString &text, const bool isError,
×
366
                           const bool isHtml) {
367
  if (isError) {
×
368
    ui->textBrowser->setTextColor(Qt::red);
×
369
  }
370

371
  if (isHtml) {
×
372
    QString _text = text;
373
    if (!ui->textBrowser->toPlainText().isEmpty()) {
×
374
      _text = ui->textBrowser->toHtml() + _text;
×
375
    }
376
    ui->textBrowser->setHtml(_text);
×
377
  } else {
378
    ui->textBrowser->setText(text);
×
379
  }
380
}
×
381

382
/**
383
 * @brief MainWindow::config pops up the configuration screen and handles all
384
 * inter-window communication
385
 */
386
void MainWindow::applyTextBrowserSettings() {
×
387
  if (QtPassSettings::isUseMonospace()) {
×
388
    QFont monospace("Monospace");
×
389
    monospace.setStyleHint(QFont::Monospace);
×
390
    ui->textBrowser->setFont(monospace);
×
391
  } else {
×
392
    ui->textBrowser->setFont(QFont());
×
393
  }
394

395
  if (QtPassSettings::isNoLineWrapping()) {
×
396
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
397
  } else {
398
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
399
  }
400
}
×
401

402
void MainWindow::applyWindowFlagsSettings() {
×
403
  if (QtPassSettings::isAlwaysOnTop()) {
×
404
    Qt::WindowFlags flags = windowFlags();
405
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
406
  } else {
407
    this->setWindowFlags(Qt::Window);
×
408
  }
409
  this->show();
×
410
}
×
411

412
/**
413
 * @brief Opens and processes the application configuration dialog, then applies
414
 * any accepted settings.
415
 * @example
416
 * config();
417
 *
418
 * @return void - This function does not return a value.
419
 */
420
void MainWindow::config() {
×
421
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
422
  d->setModal(true);
×
423
  // Automatically default to pass if it's available
424
  if (m_qtPass->isFreshStart() &&
×
425
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
426
    QtPassSettings::setUsePass(true);
×
427
  }
428

429
  if (m_qtPass->isFreshStart()) {
×
430
    d->wizard(); // run initial setup wizard for first-time configuration
×
431
  }
432
  if (d->exec()) {
×
433
    if (d->result() == QDialog::Accepted) {
×
434
      applyTextBrowserSettings();
×
435
      applyWindowFlagsSettings();
×
436

437
      updateProfileBox();
×
438
      const QString passStore = QtPassSettings::getPassStore();
×
439
      proxyModel.setStore(passStore);
×
440
      ui->treeView->setRootIndex(
×
441
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
442
      deselect();
×
443
      ui->treeView->setCurrentIndex(QModelIndex());
×
444

445
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
446
        config();
×
447
      }
448
      QtPassSettings::getPass()->updateEnv();
×
449
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
450
                                  QtPassSettings::getAutoclearPanelSeconds());
×
451
      m_qtPass->setClipboardTimer();
×
452

453
      updateGitButtonVisibility();
×
454
      updateOtpButtonVisibility();
×
455
      updateGrepButtonVisibility();
×
456
      updateProcessOutputVisibility();
×
457
      if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
458
        initTrayIcon();
×
459
      } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
460
        destroyTrayIcon();
×
461
      }
462
    }
463

464
    m_qtPass->setFreshStart(false);
×
465
  }
466
}
×
467

468
/**
469
 * @brief MainWindow::onUpdate do a git pull
470
 */
471
void MainWindow::onUpdate(bool block) {
×
472
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
473
  if (block) {
×
474
    QtPassSettings::getPass()->GitPull_b();
×
475
  } else {
476
    QtPassSettings::getPass()->GitPull();
×
477
  }
478
}
×
479

480
/**
481
 * @brief MainWindow::onPush do a git push
482
 */
483
void MainWindow::onPush() {
×
484
  if (QtPassSettings::isUseGit()) {
×
485
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
486
    QtPassSettings::getPass()->GitPush();
×
487
  }
488
}
×
489

490
/**
491
 * @brief MainWindow::getFile get the selected file path
492
 * @param index
493
 * @param forPass returns relative path without '.gpg' extension
494
 * @return path
495
 * @return
496
 */
497
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
498
  if (!index.isValid() ||
×
499
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
500
    return {};
501
  }
502
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
503
  if (forPass) {
×
504
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
505
    filePath.replace(Util::endsWithGpg(), "");
×
506
  }
507
  return filePath;
508
}
509

510
/**
511
 * @brief MainWindow::on_treeView_clicked read the selected password file
512
 * @param index
513
 */
514
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
515
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
516
  currentDir =
517
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
518
  // Clear any previously cached clipped text before showing new password
519
  m_qtPass->clearClippedText();
×
520
  QString file = getFile(index, true);
×
521
  ui->passwordName->setText(file);
×
522
  if (!file.isEmpty() && !cleared) {
×
523
    QtPassSettings::getPass()->Show(file);
×
524
  } else {
525
    clearPanel(false);
×
526
    ui->actionEdit->setEnabled(false);
×
527
    ui->actionDelete->setEnabled(true);
×
528
  }
529
}
×
530

531
/**
532
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
533
 * TreeViewItem, open the edit Window
534
 * @param index
535
 */
536
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
537
  QFileInfo fileOrFolder =
538
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
539

540
  if (fileOrFolder.isFile()) {
×
541
    editPassword(getFile(index, true));
×
542
  }
543
}
×
544

545
/**
546
 * @brief MainWindow::deselect clear the selection, password and copy buffer
547
 */
548
void MainWindow::deselect() {
×
549
  currentDir = "";
×
550
  m_qtPass->clearClipboard();
×
551
  ui->treeView->clearSelection();
×
552
  ui->actionEdit->setEnabled(false);
×
553
  ui->actionDelete->setEnabled(false);
×
554
  ui->passwordName->setText("");
×
555
  clearPanel(false);
×
556
}
×
557

558
void MainWindow::executeWrapperStarted() {
×
559
  clearTemplateWidgets();
×
560
  ui->textBrowser->clear();
×
561
  setUiElementsEnabled(false);
×
562
  clearPanelTimer.stop();
×
563
  if (QtPassSettings::isShowProcessOutput()) {
×
NEW
564
    m_processOutputWidget->setVisible(true);
×
565
  }
566
}
×
567

568
/**
569
 * @brief Handles displaying parsed password entry content in the main window.
570
 * @example
571
 * void result = MainWindow::passShowHandler(p_output);
572
 * // Updates the UI with parsed fields and emits
573
 * passShowHandlerFinished(output)
574
 *
575
 * @param p_output - The raw output text containing the password entry data.
576
 * @return void - This function does not return a value.
577
 */
578
void MainWindow::passShowHandler(const QString &p_output) {
×
579
  QStringList templ = QtPassSettings::isUseTemplate()
×
580
                          ? QtPassSettings::getPassTemplate().split("\n")
×
581
                          : QStringList();
×
582
  bool allFields =
583
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
584
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
585
  QString output = p_output;
586
  QString password = fileContent.getPassword();
×
587

588
  // set clipped text
589
  m_qtPass->setClippedText(password, p_output);
×
590

591
  // first clear the current view:
592
  clearTemplateWidgets();
×
593

594
  // show what is needed:
595
  if (QtPassSettings::isHideContent()) {
×
596
    output = "***" + tr("Content hidden") + "***";
×
597
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
598
    if (!password.isEmpty()) {
×
599
      // set the password, it is hidden if needed in addToGridLayout
600
      addToGridLayout(0, tr("Password"), password);
×
601
    }
602

603
    NamedValues namedValues = fileContent.getNamedValues();
×
604
    for (int j = 0; j < namedValues.length(); ++j) {
×
605
      const NamedValue &nv = namedValues.at(j);
606
      addToGridLayout(j + 1, nv.name, nv.value);
×
607
    }
608
    if (ui->gridLayout->count() == 0) {
×
609
      ui->verticalLayoutPassword->setSpacing(0);
×
610
    } else {
611
      ui->verticalLayoutPassword->setSpacing(6);
×
612
    }
613

614
    output = fileContent.getRemainingDataForDisplay();
×
615
  }
616

617
  if (QtPassSettings::isUseAutoclearPanel()) {
×
618
    clearPanelTimer.start();
×
619
  }
620

621
  emit passShowHandlerFinished(output);
×
622
  setUiElementsEnabled(true);
×
623
}
×
624

625
/**
626
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
627
 * and updating the UI state.
628
 * @example
629
 * void MainWindow::passOtpHandler(const QString &p_output);
630
 *
631
 * @param const QString &p_output - The OTP code text to process; if empty, an
632
 * error message is shown instead.
633
 * @return void - This function does not return a value.
634
 */
635
void MainWindow::passOtpHandler(const QString &p_output) {
×
636
  if (!p_output.isEmpty()) {
×
637
    addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
×
638
    m_qtPass->copyTextToClipboard(p_output);
×
639
    showStatusMessage(tr("OTP code copied to clipboard"));
×
640
  } else {
641
    flashText(tr("No OTP code found in this password entry"), true);
×
642
  }
643
  if (QtPassSettings::isUseAutoclearPanel()) {
×
644
    clearPanelTimer.start();
×
645
  }
646
  setUiElementsEnabled(true);
×
647
}
×
648

649
/**
650
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
651
 */
652
void MainWindow::clearPanel(bool notify) {
×
653
  while (ui->gridLayout->count() > 0) {
×
654
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
655
    delete item->widget();
×
656
    delete item;
×
657
  }
658
  const bool grepWasVisible = ui->grepResultsList->isVisible();
×
659
  ui->grepResultsList->clear();
×
660
  if (grepWasVisible) {
×
661
    ui->grepResultsList->setVisible(false);
×
662
    ui->treeView->setVisible(true);
×
663
    if (m_grepMode) {
×
664
      m_grepMode = false;
×
665
      ui->grepButton->blockSignals(true);
×
666
      ui->grepButton->setChecked(false);
×
667
      ui->grepButton->blockSignals(false);
×
668
      ui->lineEdit->blockSignals(true);
×
669
      ui->lineEdit->clear();
×
670
      ui->lineEdit->blockSignals(false);
×
671
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
672
    }
673
  }
674
  if (notify) {
×
675
    QString output = "***" + tr("Password and Content hidden") + "***";
×
676
    ui->textBrowser->setHtml(output);
×
677
  } else {
678
    ui->textBrowser->setHtml("");
×
679
  }
680
}
×
681

682
/**
683
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
684
 * elements
685
 * @param state
686
 */
687
void MainWindow::setUiElementsEnabled(bool state) {
×
688
  ui->treeView->setEnabled(state);
×
689
  ui->lineEdit->setEnabled(state);
×
690
  ui->lineEdit->installEventFilter(this);
×
691
  ui->actionAddPassword->setEnabled(state);
×
692
  ui->actionAddFolder->setEnabled(state);
×
693
  ui->actionUsers->setEnabled(state);
×
694
  ui->actionConfig->setEnabled(state);
×
695
  // is a file selected?
696
  state &= ui->treeView->currentIndex().isValid();
×
697
  ui->actionDelete->setEnabled(state);
×
698
  ui->actionEdit->setEnabled(state);
×
699
  updateGitButtonVisibility();
×
700
  updateOtpButtonVisibility();
×
701
}
×
702

703
/**
704
 * @brief Restores the main window geometry, state, position, size, and
705
 * tray/icon settings from saved application settings.
706
 * @example
707
 * MainWindow window;
708
 * window.restoreWindow();
709
 *
710
 * @return void - This function does not return a value.
711
 */
712
void MainWindow::restoreWindow() {
×
713
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
×
714
  restoreGeometry(geometry);
×
715
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
×
716
  restoreState(savestate);
×
717
  QPoint position = QtPassSettings::getPos(pos());
×
718
  move(position);
×
719
  QSize newSize = QtPassSettings::getSize(size());
×
720
  resize(newSize);
×
721
  if (QtPassSettings::isMaximized(isMaximized())) {
×
722
    showMaximized();
×
723
  }
724

725
  if (QtPassSettings::isAlwaysOnTop()) {
×
726
    Qt::WindowFlags flags = windowFlags();
727
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
728
    show();
×
729
  }
730

731
  if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
732
    initTrayIcon();
×
733
    if (QtPassSettings::isStartMinimized()) {
×
734
      // since we are still in constructor, can't directly hide
735
      QTimer::singleShot(10, this, SLOT(hide()));
×
736
    }
737
  } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
738
    destroyTrayIcon();
×
739
  }
740
}
×
741

742
/**
743
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
744
 */
745
void MainWindow::onConfig() { config(); }
×
746

747
/**
748
 * @brief Executes when the string in the search box changes, collapses the
749
 * TreeView
750
 * @param arg1
751
 */
752
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
753
  if (m_grepMode)
×
754
    return;
755
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
756
  ui->treeView->expandAll();
×
757
  clearPanel(false);
×
758
  ui->passwordName->setText("");
×
759
  ui->actionEdit->setEnabled(false);
×
760
  ui->actionDelete->setEnabled(false);
×
761
  searchTimer.start();
×
762
}
763

764
/**
765
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
766
 * time from two keypresses is elapsed
767
 */
768
void MainWindow::onTimeoutSearch() {
×
769
  QString query = ui->lineEdit->text();
×
770

771
  if (query.isEmpty()) {
×
772
    ui->treeView->collapseAll();
×
773
    deselect();
×
774
  }
775

776
  query.replace(QStringLiteral(" "), ".*");
×
777
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
778
  proxyModel.setFilterRegularExpression(regExp);
×
779
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
780
      model.setRootPath(QtPassSettings::getPassStore())));
×
781

782
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
783
    selectFirstFile();
×
784
  } else {
785
    ui->actionEdit->setEnabled(false);
×
786
    ui->actionDelete->setEnabled(false);
×
787
  }
788
}
×
789

790
/**
791
 * @brief MainWindow::on_lineEdit_returnPressed get searching
792
 *
793
 * Select the first possible file in the tree
794
 */
795
void MainWindow::on_lineEdit_returnPressed() {
×
796
#ifdef QT_DEBUG
797
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
798
#endif
799

800
  if (m_grepMode) {
×
801
    const QString query = ui->lineEdit->text();
×
802
    if (!query.isEmpty()) {
×
803
      m_grepCancelled = false;
×
804
      ui->grepResultsList->clear();
×
805
      ui->statusBar->showMessage(tr("Searching…"));
×
806
      if (!m_grepBusy) {
×
807
        m_grepBusy = true;
×
808
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
809
      }
810
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
811
    } else {
812
      m_grepCancelled = true;
×
813
      if (m_grepBusy) {
×
814
        m_grepBusy = false;
×
815
        QApplication::restoreOverrideCursor();
×
816
      }
817
      ui->grepResultsList->clear();
×
818
      ui->grepResultsList->setVisible(false);
×
819
      ui->treeView->setVisible(true);
×
820
    }
821
    return;
822
  }
823

824
  if (proxyModel.rowCount() > 0) {
×
825
    selectFirstFile();
×
826
    on_treeView_clicked(ui->treeView->currentIndex());
×
827
  }
828
}
829

830
/**
831
 * @brief Toggle grep (content search) mode.
832
 */
833
void MainWindow::on_grepButton_toggled(bool checked) {
×
834
  m_grepMode = checked;
×
835
  if (checked) {
×
836
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
837
    ui->lineEdit->clear();
×
838
    searchTimer.stop();
×
839
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
840
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
841
        model.setRootPath(QtPassSettings::getPassStore())));
×
842
    ui->grepResultsList->setVisible(false);
×
843
    // Keep treeView visible until results arrive
844
  } else {
845
    if (m_grepBusy) {
×
846
      m_grepBusy = false;
×
847
      m_grepCancelled = true;
×
848
      QApplication::restoreOverrideCursor();
×
849
    }
850
    searchTimer.stop();
×
851
    ui->lineEdit->blockSignals(true);
×
852
    ui->lineEdit->clear();
×
853
    ui->lineEdit->blockSignals(false);
×
854
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
855
    ui->grepResultsList->clear();
×
856
    ui->grepResultsList->setVisible(false);
×
857
    ui->treeView->setVisible(true);
×
858
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
859
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
860
        model.setRootPath(QtPassSettings::getPassStore())));
×
861
  }
862
}
×
863

864
/**
865
 * @brief Display grep results in grepResultsList.
866
 */
867
void MainWindow::onGrepFinished(
×
868
    const QList<QPair<QString, QStringList>> &results) {
869
  if (m_grepBusy) {
×
870
    m_grepBusy = false;
×
871
    QApplication::restoreOverrideCursor();
×
872
  }
873
  if (m_grepCancelled) {
×
874
    m_grepCancelled = false;
×
875
    return;
×
876
  }
877
  setUiElementsEnabled(true);
×
878
  if (!m_grepMode)
×
879
    return;
880
  ui->grepResultsList->clear();
×
881
  if (results.isEmpty()) {
×
882
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
883
    ui->grepResultsList->setVisible(false);
×
884
    ui->treeView->setVisible(true);
×
885
    return;
×
886
  }
887
  const bool hideContent = QtPassSettings::isHideContent();
×
888
  int totalLines = 0;
889
  for (const auto &pair : results) {
×
890
    QTreeWidgetItem *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
891
    entryItem->setText(0, pair.first);
×
892
    entryItem->setData(0, Qt::UserRole, pair.first);
×
893
    for (const QString &line : pair.second) {
×
894
      QTreeWidgetItem *lineItem = new QTreeWidgetItem(entryItem);
×
895
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
896
                                       : line);
897
      lineItem->setData(0, Qt::UserRole, pair.first);
×
898
      ++totalLines;
×
899
    }
900
  }
901
  ui->grepResultsList->expandAll();
×
902
  ui->treeView->setVisible(false);
×
903
  ui->grepResultsList->setVisible(true);
×
904
  ui->statusBar->showMessage(
×
905
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
906
          tr("in %n entr(ies).", nullptr, results.size()),
×
907
      3000);
908
  if (QtPassSettings::isUseAutoclearPanel())
×
909
    clearPanelTimer.start();
×
910
}
911

912
/**
913
 * @brief Navigate to the password entry when a grep result is clicked.
914
 */
915
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
916
                                                int /*column*/) {
917
  const QString entry = item->data(0, Qt::UserRole).toString();
×
918
  if (entry.isEmpty())
×
919
    return;
920
  const QString fullPath = QDir::cleanPath(
921
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
922
  QModelIndex srcIndex = model.index(fullPath);
×
923
  if (!srcIndex.isValid())
924
    return;
925
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
926
  if (!proxyIndex.isValid())
927
    return;
928
  ui->treeView->setCurrentIndex(proxyIndex);
×
929
  on_treeView_clicked(proxyIndex);
×
930
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
931
    ui->grepResultsList->clear();
×
932
  ui->grepResultsList->setVisible(false);
×
933
  ui->treeView->setVisible(true);
×
934
  ui->treeView->scrollTo(proxyIndex);
×
935
  ui->treeView->setFocus();
×
936
}
937

938
/**
939
 * @brief MainWindow::selectFirstFile select the first possible file in the
940
 * tree
941
 */
942
void MainWindow::selectFirstFile() {
×
943
  QModelIndex index = proxyModel.mapFromSource(
×
944
      model.setRootPath(QtPassSettings::getPassStore()));
×
945
  index = firstFile(index);
×
946
  ui->treeView->setCurrentIndex(index);
×
947
}
×
948

949
/**
950
 * @brief MainWindow::firstFile return location of first possible file
951
 * @param parentIndex
952
 * @return QModelIndex
953
 */
954
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
955
  QModelIndex index = parentIndex;
×
956
  int numRows = proxyModel.rowCount(parentIndex);
×
957
  for (int row = 0; row < numRows; ++row) {
×
958
    index = proxyModel.index(row, 0, parentIndex);
×
959
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
960
      return index;
×
961
    }
962
    if (proxyModel.hasChildren(index)) {
×
963
      return firstFile(index);
×
964
    }
965
  }
966
  return index;
×
967
}
968

969
/**
970
 * @brief MainWindow::setPassword open passworddialog
971
 * @param file which pgp file
972
 * @param isNew insert (not update)
973
 */
974
void MainWindow::setPassword(const QString &file, bool isNew) {
×
975
  PasswordDialog d(file, isNew, this);
×
976

977
  if (isNew) {
×
978
    QString storePath = QtPassSettings::getPassStore();
×
979
    QString folder =
980
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
981
    if (folder.isEmpty()) {
×
982
      folder = storePath;
×
983
    }
984
    QHash<QString, QStringList> templates = Util::readTemplates(storePath);
×
985
    if (!templates.isEmpty()) {
986
      QString defaultTemplate = Util::getFolderTemplate(folder, storePath);
×
987
      d.setAvailableTemplates(templates, defaultTemplate);
×
988
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
989
                    [&d]() { d.cycleTemplate(); });
×
990
    }
991
  }
×
992

993
  if (!d.exec()) {
×
994
    ui->treeView->setFocus();
×
995
  }
996
}
×
997

998
/**
999
 * @brief MainWindow::addPassword add a new password by showing a
1000
 * number of dialogs.
1001
 */
1002
void MainWindow::addPassword() {
×
1003
  bool ok;
1004
  QString dir =
1005
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1006
  QString file =
1007
      QInputDialog::getText(this, tr("New file"),
×
1008
                            tr("New password file: \n(Will be placed in %1 )")
×
1009
                                .arg(QtPassSettings::getPassStore() +
×
1010
                                     Util::getDir(ui->treeView->currentIndex(),
×
1011
                                                  true, model, proxyModel)),
1012
                            QLineEdit::Normal, "", &ok);
×
1013
  if (!ok || file.isEmpty()) {
×
1014
    return;
1015
  }
1016
  file = dir + file;
×
1017
  setPassword(file);
×
1018
}
1019

1020
/**
1021
 * @brief MainWindow::onDelete remove password, if you are
1022
 * sure.
1023
 */
1024
void MainWindow::onDelete() {
×
1025
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1026
  if (!currentIndex.isValid()) {
1027
    // This fixes https://github.com/IJHack/QtPass/issues/556
1028
    // Otherwise the entire password directory would be deleted if
1029
    // nothing is selected in the tree view.
1030
    return;
×
1031
  }
1032

1033
  QFileInfo fileOrFolder =
1034
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1035
  QString file = "";
×
1036
  bool isDir = false;
1037

1038
  if (fileOrFolder.isFile()) {
×
1039
    file = getFile(ui->treeView->currentIndex(), true);
×
1040
  } else {
1041
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1042
    isDir = true;
1043
  }
1044

1045
  QString dirMessage = tr(" and the whole content?");
1046
  if (isDir) {
×
1047
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1048
                    QDirIterator::Subdirectories);
×
1049
    bool okDir = true;
1050
    while (it.hasNext() && okDir) {
×
1051
      it.next();
×
1052
      if (QFileInfo(it.filePath()).isFile()) {
×
1053
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1054
          okDir = false;
1055
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1056
                          "there are unexpected files in the given folder, "
1057
                          "check them before continue.</strong>");
1058
        }
1059
      }
1060
    }
1061
  }
×
1062

1063
  if (QMessageBox::question(
×
1064
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1065
          tr("Are you sure you want to delete %1%2?")
×
1066
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1067
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1068
    return;
1069
  }
1070

1071
  QtPassSettings::getPass()->Remove(file, isDir);
×
1072
}
×
1073

1074
/**
1075
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
1076
 */
1077
void MainWindow::onOtp() {
×
1078
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1079
  if (!file.isEmpty()) {
×
1080
    if (QtPassSettings::isUseOtp()) {
×
1081
      setUiElementsEnabled(false);
×
1082
      QtPassSettings::getPass()->OtpGenerate(file);
×
1083
    }
1084
  } else {
1085
    flashText(tr("No password selected for OTP generation"), true);
×
1086
  }
1087
}
×
1088

1089
/**
1090
 * @brief MainWindow::onEdit try and edit (selected) password.
1091
 */
1092
void MainWindow::onEdit() {
×
1093
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1094
  editPassword(file);
×
1095
}
×
1096

1097
/**
1098
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1099
 * @param dir folder to edit users for.
1100
 */
1101
void MainWindow::userDialog(const QString &dir) {
×
1102
  if (!dir.isEmpty()) {
×
1103
    currentDir = dir;
×
1104
  }
1105
  onUsers();
×
1106
}
×
1107

1108
/**
1109
 * @brief MainWindow::onUsers edit users for the current
1110
 * folder,
1111
 * gets lists and opens UserDialog.
1112
 */
1113
void MainWindow::onUsers() {
×
1114
  QString dir =
1115
      currentDir.isEmpty()
1116
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
1117
          : currentDir;
×
1118

1119
  UsersDialog d(dir, this);
×
1120
  if (!d.exec()) {
×
1121
    ui->treeView->setFocus();
×
1122
  }
1123
}
×
1124

1125
/**
1126
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1127
 * @param message
1128
 */
1129
void MainWindow::messageAvailable(const QString &message) {
×
1130
  show();
×
1131
  raise();
×
1132
  if (message.isEmpty()) {
×
1133
    focusInput();
×
1134
  } else {
1135
    ui->treeView->expandAll();
×
1136
    ui->lineEdit->setText(message);
×
1137
    on_lineEdit_returnPressed();
×
1138
  }
1139
}
×
1140

1141
/**
1142
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1143
 * @param batch
1144
 * @param keygenWindow
1145
 */
1146
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1147
  keygenDialog = keygenWindow;
×
1148
  emit generateGPGKeyPair(batch);
×
1149
}
×
1150

1151
/**
1152
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1153
 * select a more appropriate one to view too
1154
 */
1155
void MainWindow::updateProfileBox() {
×
1156
  QHash<QString, QHash<QString, QString>> profiles =
1157
      QtPassSettings::getProfiles();
×
1158

1159
  if (profiles.isEmpty()) {
1160
    ui->profileWidget->hide();
×
1161
  } else {
1162
    ui->profileWidget->show();
×
1163
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1164
    ui->profileBox->clear();
×
1165
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
×
1166
    while (i.hasNext()) {
×
1167
      i.next();
1168
      if (!i.key().isEmpty()) {
×
1169
        ui->profileBox->addItem(i.key());
×
1170
      }
1171
    }
1172
    ui->profileBox->model()->sort(0);
×
1173
  }
1174
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
×
1175
  if (index != -1) { //  -1 for not found
×
1176
    ui->profileBox->setCurrentIndex(index);
×
1177
  }
1178
}
×
1179

1180
/**
1181
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1182
 * correct "profile"
1183
 * @param name
1184
 */
1185
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1186
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1187
#else
1188
/**
1189
 * @brief Handles changes to the selected profile in the profile combo box.
1190
 * @details Ignores the event during a fresh start or when the selected profile
1191
 * matches the current profile. Otherwise, it clears the password field, updates
1192
 * the active profile and related settings, refreshes the environment, and
1193
 * resets the tree view and action states to reflect the newly selected profile.
1194
 *
1195
 * @param name - The newly selected profile name.
1196
 * @return void - This function does not return a value.
1197
 *
1198
 */
1199
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1200
#endif
1201
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
1202
    return;
×
1203
  }
1204

1205
  ui->lineEdit->clear();
×
1206

1207
  QtPassSettings::setProfile(name);
×
1208

1209
  QtPassSettings::setPassStore(
×
1210
      QtPassSettings::getProfiles().value(name).value("path"));
×
1211
  QtPassSettings::setPassSigningKey(
×
1212
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1213
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1214

1215
  QtPassSettings::getPass()->updateEnv();
×
1216

1217
  const QString passStore = QtPassSettings::getPassStore();
×
1218
  proxyModel.setStore(passStore);
×
1219
  ui->treeView->setRootIndex(
×
1220
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1221
  deselect();
×
1222
  ui->treeView->setCurrentIndex(QModelIndex());
×
1223
}
1224

1225
/**
1226
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1227
 * support
1228
 * it
1229
 */
1230
void MainWindow::initTrayIcon() {
×
1231
  this->tray = new TrayIcon(this);
×
1232
  // Setup tray icon
1233

1234
  if (tray == nullptr) {
1235
#ifdef QT_DEBUG
1236
    dbg() << "Allocating tray icon failed.";
1237
#endif
1238
    return;
1239
  }
1240

1241
  if (!tray->getIsAllocated()) {
×
1242
    destroyTrayIcon();
×
1243
  }
1244
}
1245

1246
/**
1247
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1248
 */
1249
void MainWindow::destroyTrayIcon() {
×
1250
  delete this->tray;
×
1251
  tray = nullptr;
×
1252
}
×
1253

1254
/**
1255
 * @brief MainWindow::closeEvent hide or quit
1256
 * @param event
1257
 */
1258
void MainWindow::closeEvent(QCloseEvent *event) {
×
1259
  if (QtPassSettings::isHideOnClose()) {
×
1260
    this->hide();
×
1261
    event->ignore();
1262
  } else {
1263
    m_qtPass->clearClipboard();
×
1264

1265
    QtPassSettings::setGeometry(saveGeometry());
×
1266
    QtPassSettings::setSavestate(saveState());
×
1267
    QtPassSettings::setMaximized(isMaximized());
×
1268
    if (!isMaximized()) {
×
1269
      QtPassSettings::setPos(pos());
×
1270
      QtPassSettings::setSize(size());
×
1271
    }
1272
    event->accept();
1273
  }
1274
}
×
1275

1276
/**
1277
 * @brief MainWindow::eventFilter filter out some events and focus the
1278
 * treeview
1279
 * @param obj
1280
 * @param event
1281
 * @return
1282
 */
1283
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
×
1284
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
×
1285
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1286
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1287
      ui->treeView->setFocus();
×
1288
    }
1289
  }
1290
  return QObject::eventFilter(obj, event);
×
1291
}
1292

1293
/**
1294
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1295
 * @param event
1296
 */
1297
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1298
  switch (event->key()) {
×
1299
  case Qt::Key_Delete:
×
1300
    onDelete();
×
1301
    break;
×
1302
  case Qt::Key_Return:
×
1303
  case Qt::Key_Enter:
1304
    if (proxyModel.rowCount() > 0) {
×
1305
      on_treeView_clicked(ui->treeView->currentIndex());
×
1306
    }
1307
    break;
1308
  case Qt::Key_Escape:
×
1309
    ui->lineEdit->clear();
×
1310
    break;
×
1311
  default:
1312
    break;
1313
  }
1314
}
×
1315

1316
/**
1317
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1318
 * menu
1319
 * @param pos
1320
 */
1321
void MainWindow::showContextMenu(const QPoint &pos) {
×
1322
  QModelIndex index = ui->treeView->indexAt(pos);
×
1323
  bool selected = true;
1324
  if (!index.isValid()) {
1325
    ui->treeView->clearSelection();
×
1326
    ui->actionDelete->setEnabled(false);
×
1327
    ui->actionEdit->setEnabled(false);
×
1328
    currentDir = "";
×
1329
    selected = false;
1330
  }
1331

1332
  ui->treeView->setCurrentIndex(index);
×
1333

1334
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1335

1336
  QFileInfo fileOrFolder =
1337
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1338

1339
  QMenu contextMenu;
×
1340
  if (!selected || fileOrFolder.isDir()) {
×
1341
    QAction *openFolder =
1342
        contextMenu.addAction(tr("Open folder with file manager"));
×
1343
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1344
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1345
    QAction *users = contextMenu.addAction(tr("Users"));
×
1346
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1347
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1348
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1349
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1350
  } else if (fileOrFolder.isFile()) {
×
1351
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1352
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1353
  }
1354
  if (selected) {
×
1355
    contextMenu.addSeparator();
×
1356
    if (fileOrFolder.isDir()) {
×
1357
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1358
      connect(renameFolder, &QAction::triggered, this,
×
1359
              &MainWindow::renameFolder);
×
1360
    } else if (fileOrFolder.isFile()) {
×
1361
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1362
      connect(renamePassword, &QAction::triggered, this,
×
1363
              &MainWindow::renamePassword);
×
1364
    }
1365
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1366
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1367
    if (fileOrFolder.isDir()) {
×
1368
      QString dirPath = QDir::cleanPath(
1369
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1370

1371
      QMenu *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1372
      contextMenu.addMenu(shareMenu);
×
1373

1374
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1375
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1376

1377
      QString exePath = QtPassSettings::isUsePass()
×
1378
                            ? QtPassSettings::getPassExecutable()
×
1379
                            : QtPassSettings::getGpgExecutable();
×
1380
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1381
                                                 QFile(exePath).exists());
×
1382

1383
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1384
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1385
      connect(reencrypt, &QAction::triggered, this,
×
1386
              [this, dirPath]() { reencryptPath(dirPath); });
×
1387

1388
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1389
      exportKey->setEnabled(gpgAvailable);
×
1390
      connect(exportKey, &QAction::triggered, this,
×
1391
              &MainWindow::exportPublicKey);
×
1392

1393
      QAction *addRecipientAction =
1394
          shareMenu->addAction(tr("Add recipient..."));
×
1395
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1396
      connect(addRecipientAction, &QAction::triggered, this,
×
1397
              [this, dirPath]() { addRecipient(dirPath); });
×
1398

1399
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1400
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1401
    }
1402
  }
1403
  contextMenu.exec(globalPos);
×
1404
}
×
1405

1406
/**
1407
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1408
 * password window
1409
 * @param pos
1410
 */
1411
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1412
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1413
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1414

1415
  contextMenu->exec(globalPos);
×
1416
  delete contextMenu;
×
1417
}
×
1418

1419
/**
1420
 * @brief MainWindow::openFolder open the folder in the default file manager
1421
 */
1422
void MainWindow::openFolder() {
×
1423
  QString dir =
1424
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1425

1426
  QString path = QDir::toNativeSeparators(dir);
×
1427
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1428
}
×
1429

1430
/**
1431
 * @brief MainWindow::addFolder add a new folder to store passwords in
1432
 */
1433
void MainWindow::addFolder() {
×
1434
  bool ok;
1435
  QString dir =
1436
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1437
  QString newdir =
1438
      QInputDialog::getText(this, tr("New file"),
×
1439
                            tr("New Folder: \n(Will be placed in %1 )")
×
1440
                                .arg(QtPassSettings::getPassStore() +
×
1441
                                     Util::getDir(ui->treeView->currentIndex(),
×
1442
                                                  true, model, proxyModel)),
1443
                            QLineEdit::Normal, "", &ok);
×
1444
  if (!ok || newdir.isEmpty()) {
×
1445
    return;
1446
  }
1447
  newdir.prepend(dir);
1448
  if (!QDir().mkdir(newdir)) {
×
1449
    QMessageBox::warning(this, tr("Error"),
×
1450
                         tr("Failed to create folder: %1").arg(newdir));
×
1451
    return;
×
1452
  }
1453
  if (QtPassSettings::isAddGPGId(true)) {
×
1454
    QString gpgIdFile = newdir + "/.gpg-id";
×
1455
    QFile gpgId(gpgIdFile);
×
1456
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1457
      QMessageBox::warning(
×
1458
          this, tr("Error"),
×
1459
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1460
      return;
1461
    }
1462
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1463
    for (const UserInfo &user : users) {
×
1464
      if (user.enabled) {
×
1465
        gpgId.write((user.key_id + "\n").toUtf8());
×
1466
      }
1467
    }
1468
    gpgId.close();
×
1469
  }
×
1470
}
1471

1472
/**
1473
 * @brief MainWindow::renameFolder rename an existing folder
1474
 */
1475
void MainWindow::renameFolder() {
×
1476
  bool ok;
1477
  QString srcDir = QDir::cleanPath(
1478
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1479
  QString srcDirName = QDir(srcDir).dirName();
×
1480
  QString newName =
1481
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1482
                            QLineEdit::Normal, srcDirName, &ok);
×
1483
  if (!ok || newName.isEmpty()) {
×
1484
    return;
1485
  }
1486
  QString destDir = srcDir;
1487
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1488
  QtPassSettings::getPass()->Move(srcDir, destDir);
×
1489
}
1490

1491
/**
1492
 * @brief MainWindow::editPassword read password and open edit window via
1493
 * MainWindow::onEdit()
1494
 */
1495
void MainWindow::editPassword(const QString &file) {
×
1496
  if (!file.isEmpty()) {
×
1497
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1498
      onUpdate(true);
×
1499
    }
1500
    setPassword(file, false);
×
1501
  }
1502
}
×
1503

1504
/**
1505
 * @brief MainWindow::renamePassword rename an existing password
1506
 */
1507
void MainWindow::renamePassword() {
×
1508
  bool ok;
1509
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1510
  QString filePath = QFileInfo(file).path();
×
1511
  QString fileName = QFileInfo(file).fileName();
×
1512
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1513
    fileName.chop(4);
×
1514
  }
1515

1516
  QString newName =
1517
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1518
                            QLineEdit::Normal, fileName, &ok);
×
1519
  if (!ok || newName.isEmpty()) {
×
1520
    return;
1521
  }
1522
  QString newFile = QDir(filePath).filePath(newName);
×
1523
  QtPassSettings::getPass()->Move(file, newFile);
×
1524
}
1525

1526
/**
1527
 * @brief MainWindow::clearTemplateWidgets empty the template widget fields in
1528
 * the UI
1529
 */
1530
void MainWindow::clearTemplateWidgets() {
×
1531
  while (ui->gridLayout->count() > 0) {
×
1532
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
1533
    delete item->widget();
×
1534
    delete item;
×
1535
  }
1536
  ui->verticalLayoutPassword->setSpacing(0);
×
1537
}
×
1538

1539
/**
1540
 * @brief Copies the password of the selected file from the tree view to the
1541
 * clipboard.
1542
 * @example
1543
 * MainWindow::copyPasswordFromTreeview();
1544
 *
1545
 * @return void - This function does not return a value.
1546
 */
1547
void MainWindow::copyPasswordFromTreeview() {
×
1548
  QFileInfo fileOrFolder =
1549
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1550

1551
  if (fileOrFolder.isFile()) {
×
1552
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1553
    // Disconnect any previous connection to avoid accumulation
1554
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1555
               &MainWindow::passwordFromFileToClipboard);
1556
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1557
            &MainWindow::passwordFromFileToClipboard);
×
1558
    QtPassSettings::getPass()->Show(file);
×
1559
  }
1560
}
×
1561

1562
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1563
  QStringList tokens = text.split('\n');
×
1564
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1565
}
×
1566

1567
/**
1568
 * @brief MainWindow::addToGridLayout add a field to the template grid
1569
 * @param position
1570
 * @param field
1571
 * @param value
1572
 */
1573
void MainWindow::addToGridLayout(int position, const QString &field,
×
1574
                                 const QString &value) {
1575
  QString trimmedField = field.trimmed();
1576
  QString trimmedValue = value.trimmed();
1577

1578
  const QString buttonStyle =
1579
      "border-style: none; background: transparent; padding: 0; margin: 0; "
1580
      "icon-size: 16px; color: inherit;";
×
1581

1582
  // Combine the Copy button and the line edit in one widget
1583
  auto *frame = new QFrame();
×
1584
  QLayout *ly = new QHBoxLayout();
×
1585
  ly->setContentsMargins(5, 2, 2, 2);
×
1586
  ly->setSpacing(0);
×
1587
  frame->setLayout(ly);
×
1588
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER) {
×
1589
    auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
×
1590
    connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
×
1591
            &QtPass::copyTextToClipboard);
×
1592

1593
    fieldLabel->setStyleSheet(buttonStyle);
×
1594
    frame->layout()->addWidget(fieldLabel);
×
1595
  }
1596

1597
  if (QtPassSettings::isUseQrencode()) {
×
1598
    auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
×
1599
    connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
×
1600
            &QtPass::showTextAsQRCode);
×
1601
    qrbutton->setStyleSheet(buttonStyle);
×
1602
    frame->layout()->addWidget(qrbutton);
×
1603
  }
1604

1605
  // set the echo mode to password, if the field is "password"
1606
  const QString lineStyle =
1607
      QtPassSettings::isUseMonospace()
×
1608
          ? "border-style: none; background: transparent; font-family: "
1609
            "monospace;"
1610
          : "border-style: none; background: transparent;";
×
1611

1612
  if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
×
1613
    auto *line = new QLineEdit();
×
1614
    line->setObjectName(trimmedField);
×
1615
    line->setText(trimmedValue);
×
1616
    line->setReadOnly(true);
×
1617
    line->setStyleSheet(lineStyle);
×
1618
    line->setContentsMargins(0, 0, 0, 0);
×
1619
    line->setEchoMode(QLineEdit::Password);
×
1620
    auto *showButton = new QPushButtonShowPassword(line, this);
×
1621
    showButton->setStyleSheet(buttonStyle);
×
1622
    showButton->setContentsMargins(0, 0, 0, 0);
×
1623
    frame->layout()->addWidget(showButton);
×
1624
    frame->layout()->addWidget(line);
×
1625
  } else {
1626
    auto *line = new QTextBrowser();
×
1627
    line->setOpenExternalLinks(true);
×
1628
    line->setOpenLinks(true);
×
1629
    line->setMaximumHeight(26);
×
1630
    line->setMinimumHeight(26);
×
1631
    line->setSizePolicy(
×
1632
        QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1633
    line->setObjectName(trimmedField);
×
1634
    trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
1635
    line->setText(trimmedValue);
×
1636
    line->setReadOnly(true);
×
1637
    line->setStyleSheet(lineStyle);
×
1638
    line->setContentsMargins(0, 0, 0, 0);
×
1639
    frame->layout()->addWidget(line);
×
1640
  }
1641

1642
  frame->setStyleSheet(
×
1643
      ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1644

1645
  // set into the layout
1646
  ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
×
1647
  ui->gridLayout->addWidget(frame, position, 1);
×
1648
}
×
1649

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

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

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

1682
  if (ret != QMessageBox::Yes)
×
1683
    return;
1684

1685
  // Prevent double execution - use same method as startReencryptPath
1686
  setUiElementsEnabled(false);
×
1687
  ui->treeView->setDisabled(true);
×
1688

1689
  QtPassSettings::getImitatePass()->reencryptPath(
×
1690
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1691
}
×
1692

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

1701
/**
1702
 * @brief MainWindow::endReencryptPath re-enable ui elements
1703
 */
1704
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1705

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

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

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

1782
void MainWindow::updateGitButtonVisibility() {
×
1783
  if (!QtPassSettings::isUseGit() ||
×
1784
      (QtPassSettings::getGitExecutable().isEmpty() &&
×
1785
       QtPassSettings::getPassExecutable().isEmpty())) {
×
1786
    enableGitButtons(false);
×
1787
  } else {
1788
    enableGitButtons(true);
×
1789
  }
1790
}
×
1791

1792
void MainWindow::updateOtpButtonVisibility() {
×
1793
#if defined(Q_OS_WIN) || defined(__APPLE__)
1794
  ui->actionOtp->setVisible(false);
1795
#endif
1796
  if (!QtPassSettings::isUseOtp()) {
×
1797
    ui->actionOtp->setEnabled(false);
×
1798
  } else {
1799
    ui->actionOtp->setEnabled(true);
×
1800
  }
1801
}
×
1802

1803
void MainWindow::updateGrepButtonVisibility() {
×
1804
  const bool enabled = QtPassSettings::isUseGrepSearch();
×
1805
  ui->grepButton->setVisible(enabled);
×
1806
  ui->grepCaseButton->setVisible(enabled);
×
1807
  if (!enabled && m_grepMode) {
×
1808
    ui->grepButton->setChecked(false);
×
1809
  }
1810
}
×
1811

1812
void MainWindow::enableGitButtons(const bool &state) {
×
1813
  // Following GNOME guidelines is preferable disable buttons instead of hide
1814
  ui->actionPush->setEnabled(state);
×
1815
  ui->actionUpdate->setEnabled(state);
×
1816
}
×
1817

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

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

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

1856
    m_outputCounter++;
×
1857
    QString lineNumber = QString::number(m_outputCounter);
×
1858

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

NEW
1872
    m_processOutputEdit->append(coloredOutput);
×
1873
  }
1874

1875
  limitOutputLines();
×
1876

1877
  if (m_autoScroll) {
×
NEW
1878
    m_processOutputEdit->verticalScrollBar()->setValue(
×
NEW
1879
        m_processOutputEdit->verticalScrollBar()->maximum());
×
1880
  }
1881
}
1882

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

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

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

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

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

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

2015
/**
2016
 * @brief Clears the process output panel.
2017
 *
2018
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2019
 */
2020
void MainWindow::on_clearOutputButton_clicked() {
×
NEW
2021
  m_processOutputEdit->clear();
×
2022
  m_outputCounter = 0;
×
2023
  m_autoScroll = true;
×
2024
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc