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

IJHack / QtPass / 28659639755

03 Jul 2026 12:10PM UTC coverage: 56.754% (+0.03%) from 56.72%
28659639755

push

github

web-flow
fix: keep clipboard autoclear tracking when navigating between entries (#1607)

* fix: keep clipboard autoclear tracking when navigating between entries

clippedText records what copyTextToClipboard() placed on the clipboard so the
autoclear timer knows what to clear. But setClippedText() (called on every entry
show) and clearClippedText() (called on every tree navigation) both overwrote
it, so after copying a password and then selecting another entry the timer saw
clippedText != clipboard and refused to clear — leaving the copied password on
the clipboard indefinitely, past its autoclear window.

The staged value was never consumed anywhere (on-demand copy uses the field
value directly), so it only corrupted the autoclear tracker. Make clippedText a
pure record of the current clipboard contents: setClippedText() now only copies
in "always copy" mode (via copyTextToClipboard, which sets the tracker) and no
longer overwrites it otherwise, and the vestigial clearClippedText() and its
navigation call are removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* docs: update setClippedText docstring to match conditional copy behavior

setClippedText only copies (and thus updates the autoclear tracker) in
"always copy" mode; document that instead of implying it always mutates the
tracked value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

1 existing line in 1 file now uncovered.

3811 of 6715 relevant lines covered (56.75%)

30.8 hits per line

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

27.58
/src/mainwindow.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "mainwindow.h"
4

5
#ifdef QT_DEBUG
6
#include "debughelper.h"
7
#endif
8

9
#include "configdialog.h"
10
#include "enums.h"
11
#include "executor.h"
12
#include "exportpublickeydialog.h"
13
#include "filecontent.h"
14
#include "passworddialog.h"
15
#include "passworddisplaypanel.h"
16
#include "pathvalidator.h"
17
#include "qpushbuttonasqrcode.h"
18
#include "qpushbuttonshowpassword.h"
19
#include "qpushbuttonwithclipboard.h"
20
#include "qtpass.h"
21
#include "qtpasssettings.h"
22
#include "templateio.h"
23
#include "trayicon.h"
24
#include "ui_mainwindow.h"
25
#include "usersdialog.h"
26
#include "util.h"
27
#include <QApplication>
28
#include <QCloseEvent>
29
#include <QDesktopServices>
30
#include <QDialog>
31
#include <QDirIterator>
32
#include <QDockWidget>
33
#include <QFileInfo>
34
#include <QHBoxLayout>
35
#include <QInputDialog>
36
#include <QLabel>
37
#include <QLineEdit>
38
#include <QMenu>
39
#include <QMessageBox>
40
#include <QPushButton>
41
#include <QScrollBar>
42
#include <QShortcut>
43
#include <QTextCursor>
44
#include <QTextEdit>
45
#include <QTimer>
46
#include <QToolButton>
47
#include <QTreeWidget>
48
#include <QUrl>
49
#include <utility>
50

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

66
  m_qtPass = new QtPass(this);
12✔
67

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

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

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

84
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
12✔
85

86
  QModelIndex rootDir = model.setRootPath(passStore);
12✔
87
  model.fetchMore(rootDir);
12✔
88

89
  proxyModel.setModelAndStore(&model, passStore);
12✔
90
  proxyModel.setPass(QtPassSettings::getPass());
12✔
91
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
12✔
92

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

109
  {
110
    const AppSettings s = QtPassSettings::load();
12✔
111
    if (s.useMonospace) {
12✔
112
      QFont monospace("Monospace");
×
113
      monospace.setStyleHint(QFont::Monospace);
×
114
      ui->textBrowser->setFont(monospace);
×
115
    }
×
116
    if (s.noLineWrapping) {
12✔
117
      ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
118
    }
119
    clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
12✔
120
  }
12✔
121
  ui->textBrowser->setOpenExternalLinks(true);
12✔
122
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
12✔
123
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
12✔
124
          &MainWindow::showBrowserContextMenu);
12✔
125

126
  updateProfileBox();
12✔
127

128
  m_displayPanel = new PasswordDisplayPanel(
12✔
129
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
12✔
130
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested, m_qtPass,
12✔
131
          &QtPass::copyTextToClipboard);
12✔
132
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
12✔
133
          &QtPass::showTextAsQRCode);
12✔
134

135
  QtPassSettings::getPass()->updateEnv();
12✔
136
  clearPanelTimer.setSingleShot(true);
12✔
137
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
12✔
138

139
  searchTimer.setInterval(350);
12✔
140
  searchTimer.setSingleShot(true);
12✔
141

142
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
12✔
143

144
  // Install the search-box key filter once, not on every setUiElementsEnabled
145
  // call.
146
  ui->lineEdit->installEventFilter(this);
12✔
147

148
  // Safety net: if a backend operation disables the UI but never signals
149
  // completion, re-enable after a timeout so the window can't get stuck.
150
  m_uiWatchdog.setSingleShot(true);
12✔
151
  m_uiWatchdog.setInterval(UiWatchdogMs);
12✔
152
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
12✔
153
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
154
    setUiElementsEnabled(true);
×
155
  });
×
156

157
  initToolBarButtons();
12✔
158
  initStatusBar();
12✔
159
  initProcessOutputPanel();
12✔
160

161
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
12✔
162
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
24✔
163
            // Never route potentially-secret output through the panel:
164
            // - PASS_SHOW / PASS_OTP_GENERATE go via dedicated signals to
165
            //   the main text browser (which clears on a timer).
166
            // - PASS_GREP returns lines from password files; #252 must
167
            //   not leak those into a long-lived panel.
168
            // - PASS_INSERT's stdin is the password; stdout normally
169
            //   carries gpg/git progress only, but exclude defensively
170
            //   in case a future code path uses --echo or similar.
171
            if (isSensitiveProcess(pid)) {
×
172
              return;
173
            }
174
            if (!out.isEmpty()) {
×
175
              onProcessOutput(out, false, pid);
×
176
            }
177
            if (!err.isEmpty()) {
×
178
              onProcessOutput(err, true, pid);
×
179
            }
180
          });
181

182
  ui->lineEdit->setClearButtonEnabled(true);
12✔
183
  updateGrepButtonVisibility();
12✔
184

185
  setUiElementsEnabled(true);
12✔
186

187
  ui->lineEdit->setText(searchText);
12✔
188

189
  if (!m_qtPass->init()) {
12✔
190
    // no working config so this should just quit
191
    QApplication::quit();
×
192
    return;
193
  }
194

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

204
MainWindow::~MainWindow() { delete m_qtPass; }
36✔
205

206
/**
207
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
208
 * box and sets focus to it. Allows for easy searching, called at application
209
 * start and when receiving empty message in MainWindow::messageAvailable when
210
 * compiled with SINGLE_APP=1 (default).
211
 */
212
void MainWindow::focusInput() {
×
213
  // Resolve the QLineEdit through the live widget tree rather than the
214
  // cached `ui->lineEdit` pointer.
215
  //
216
  // On a fresh-config first launch the constructor calls
217
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
218
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
219
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
220
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
221
  // we observed the QLineEdit attached to the centralWidget gets
222
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
223
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
224
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
225
  // walks the current hierarchy and returns null cleanly when the
226
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
227
  // use-after-free.
228
  if (!isVisible()) {
×
229
    return;
230
  }
231
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
×
232
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
×
233
    return;
234
  }
235
  lineEdit->selectAll();
×
236
  lineEdit->setFocus();
×
237
  // Only mark the first-show focus pulse as done once it's actually
238
  // landed; setting it eagerly in showEvent() would consume the
239
  // one-shot if focusInput returned early (mid-rebuild widget state)
240
  // and we'd never retry.
241
  m_firstShowCompleted = true;
×
242
}
243

244
/**
245
 * @brief MainWindow::changeEvent sets focus to the search box
246
 * @param event
247
 */
248
void MainWindow::changeEvent(QEvent *event) {
12✔
249
  QWidget::changeEvent(event);
12✔
250
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
12✔
251
      isVisible()) {
252
    // Defer one event-loop tick so the synchronous activation dispatch
253
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
254
    // unwinds before we touch widget state — calling `focusInput()`
255
    // inline from this stack has segfaulted in past iterations because
256
    // mid-rebuild ui state isn't fully wired up yet.
257
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
×
258
                              Qt::QueuedConnection);
259
  }
260
}
12✔
261

262
/**
263
 * @brief First-show hook: run the initial focusInput() pulse once the
264
 *        window is actually mapped. The widget's internal data is fully
265
 *        initialised by this point, so QLineEdit::selectAll() is safe.
266
 * @param event Show event passed to the base class.
267
 */
268
void MainWindow::showEvent(QShowEvent *event) {
×
269
  QMainWindow::showEvent(event);
×
270
  if (m_firstShowCompleted) {
×
271
    return;
272
  }
273
  // Queue the focus pulse for the next event-loop tick so the platform
274
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
275
  // from the config wizard path) settle before we look up the line
276
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
277
  // *after* it actually focuses, so a transient failed lookup just
278
  // re-queues on the next show rather than silently dropping.
279
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
×
280
                            Qt::QueuedConnection);
281
}
282

283
/**
284
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
285
 */
286
void MainWindow::initToolBarButtons() {
12✔
287
  connect(ui->actionAddPassword, &QAction::triggered, this,
12✔
288
          &MainWindow::addPassword);
12✔
289
  connect(ui->actionAddFolder, &QAction::triggered, this,
12✔
290
          &MainWindow::addFolder);
12✔
291
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
12✔
292
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
12✔
293
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
12✔
294
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
12✔
295
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
12✔
296
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
12✔
297
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
12✔
298

299
  ui->actionAddPassword->setIcon(
12✔
300
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
48✔
301
  ui->actionAddFolder->setIcon(
12✔
302
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
48✔
303
  ui->actionEdit->setIcon(QIcon::fromTheme(
12✔
304
      "document-properties", QIcon(":/icons/document-properties.svg")));
36✔
305
  ui->actionDelete->setIcon(
12✔
306
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
48✔
307
  ui->actionPush->setIcon(
12✔
308
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
48✔
309
  ui->actionUpdate->setIcon(
12✔
310
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
48✔
311
  ui->actionUsers->setIcon(QIcon::fromTheme(
12✔
312
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
36✔
313
  ui->actionConfig->setIcon(QIcon::fromTheme(
12✔
314
      "applications-system", QIcon(":/icons/applications-system.svg")));
24✔
315
}
12✔
316

317
/**
318
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
319
 */
320
void MainWindow::initStatusBar() {
12✔
321
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
24✔
322

323
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
24✔
324
                     .scaledToHeight(statusBar()->height());
12✔
325
  auto *logoApp = new QLabel(statusBar());
12✔
326
  logoApp->setPixmap(logo);
12✔
327
  statusBar()->addPermanentWidget(logoApp);
12✔
328
}
12✔
329

330
/**
331
 * @brief Build the process-output panel as a bottom QDockWidget.
332
 *
333
 * The panel is constructed programmatically rather than declared in
334
 * mainwindow.ui: uic only places QMainWindow's top-level children into
335
 * the centralWidget / statusBar / menuBar / toolBars / dock-widget
336
 * slots, and the previous home (statusBar()->addPermanentWidget()) made
337
 * an 80–150 px tall QTextEdit sit inside what is otherwise a thin
338
 * status row. A QDockWidget at the bottom dock area is the conventional
339
 * place for an IDE-style output console, and it gives users
340
 * detach/move for free.
341
 */
342
void MainWindow::initProcessOutputPanel() {
12✔
343
  m_processOutputWidget = new QWidget;
12✔
344
  m_processOutputWidget->setObjectName(QStringLiteral("processOutputWidget"));
24✔
345
  auto *outputLayout = new QHBoxLayout(m_processOutputWidget);
12✔
346
  outputLayout->setObjectName(QStringLiteral("processOutputLayout"));
24✔
347
  outputLayout->setContentsMargins(0, 0, 0, 0);
12✔
348
  m_clearOutputButton = new QToolButton(m_processOutputWidget);
12✔
349
  m_clearOutputButton->setObjectName(QStringLiteral("clearOutputButton"));
24✔
350
  m_clearOutputButton->setText(tr("Clear"));
12✔
351
  m_clearOutputButton->setToolTip(tr("Clear output"));
12✔
352
  outputLayout->addWidget(m_clearOutputButton);
12✔
353
  m_processOutputEdit = new QTextEdit(m_processOutputWidget);
12✔
354
  m_processOutputEdit->setObjectName(QStringLiteral("processOutputEdit"));
24✔
355
  m_processOutputEdit->setReadOnly(true);
12✔
356
  m_processOutputEdit->setAcceptRichText(false);
12✔
357
  outputLayout->addWidget(m_processOutputEdit);
12✔
358

359
  m_processOutputDock = new QDockWidget(tr("Process Output"), this);
12✔
360
  m_processOutputDock->setObjectName(QStringLiteral("processOutputDock"));
24✔
361
  m_processOutputDock->setFeatures(QDockWidget::DockWidgetMovable |
12✔
362
                                   QDockWidget::DockWidgetFloatable);
363
  m_processOutputDock->setAllowedAreas(Qt::BottomDockWidgetArea |
12✔
364
                                       Qt::TopDockWidgetArea);
365
  m_processOutputDock->setWidget(m_processOutputWidget);
12✔
366
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutputDock);
12✔
367
  // setVisible after addDockWidget so our explicit preference wins
368
  // even if QMainWindow applies any cached state when the dock is
369
  // attached. restoreWindow() runs before this method (it's called
370
  // from the QtPass ctor, which is constructed at the top of the
371
  // MainWindow ctor), so the saved layout has already been processed
372
  // by the time we get here.
373
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
12✔
374

375
  connect(m_clearOutputButton, &QToolButton::clicked, this,
12✔
376
          &MainWindow::on_clearOutputButton_clicked);
12✔
377

378
  // Hysteresis: while the user is actively dragging the slider, don't
379
  // touch m_autoScroll on every tick — a brief overshoot at maximum
380
  // would silently re-arm auto-scroll without an explicit release. Only
381
  // commit on slider release. Wheel/keyboard scroll never sets
382
  // isSliderDown(), so they still update immediately.
383
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
12✔
384
          this, [this]() {
12✔
385
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
386
            if (sb->isSliderDown())
×
387
              return;
388
            m_autoScroll = sb->value() >= sb->maximum();
×
389
          });
390
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderReleased,
12✔
391
          this, [this]() {
12✔
392
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
393
            m_autoScroll = sb->value() >= sb->maximum();
×
394
          });
×
395
}
12✔
396

397
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
398
  return ui->treeView->currentIndex();
×
399
}
400

401
void MainWindow::cleanKeygenDialog() {
1✔
402
  if (m_keyGenDialog != nullptr) {
1✔
403
    m_keyGenDialog->close();
×
404
  }
405
  m_keyGenDialog = nullptr;
406
}
1✔
407

408
/**
409
 * @brief Displays the given text in the main window text browser, optionally
410
 * marking it as an error and/or rendering it as HTML.
411
 * @example
412
 * MainWindow window;
413
 * window.flashText("Operation completed.", false, false);
414
 *
415
 * @param const QString &text - The text content to display.
416
 * @param const bool isError - If true, sets the text color to red before
417
 * displaying the text.
418
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
419
 * the existing HTML content.
420
 * @return void - No return value.
421
 */
422
void MainWindow::flashText(const QString &text, const bool isError,
3✔
423
                           const bool isHtml) {
424
  if (isError) {
3✔
425
    ui->textBrowser->setTextColor(Qt::red);
1✔
426
  }
427

428
  if (isHtml) {
3✔
429
    QString _text = text;
430
    if (!ui->textBrowser->toPlainText().isEmpty()) {
2✔
431
      _text = ui->textBrowser->toHtml() + _text;
2✔
432
    }
433
    ui->textBrowser->setHtml(_text);
1✔
434
  } else {
435
    ui->textBrowser->setText(text);
2✔
436
  }
437
}
3✔
438

439
/**
440
 * @brief MainWindow::config pops up the configuration screen and handles all
441
 * inter-window communication
442
 */
443
void MainWindow::applyTextBrowserSettings() {
×
444
  const AppSettings s = QtPassSettings::load();
×
445
  if (s.useMonospace) {
×
446
    QFont monospace("Monospace");
×
447
    monospace.setStyleHint(QFont::Monospace);
×
448
    ui->textBrowser->setFont(monospace);
×
449
  } else {
×
450
    ui->textBrowser->setFont(QFont());
×
451
  }
452

453
  if (s.noLineWrapping) {
×
454
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
455
  } else {
456
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
457
  }
458
}
×
459

460
void MainWindow::applyWindowFlagsSettings() {
×
461
  if (QtPassSettings::isAlwaysOnTop()) {
×
462
    Qt::WindowFlags flags = windowFlags();
463
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
464
  } else {
465
    this->setWindowFlags(Qt::Window);
×
466
  }
467
  this->show();
×
468
}
×
469

470
/**
471
 * @brief Opens and processes the application configuration dialog, then applies
472
 * any accepted settings.
473
 * @example
474
 * config();
475
 *
476
 * @return void - This function does not return a value.
477
 */
478
void MainWindow::config() {
×
479
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
480
  d->setModal(true);
×
481
  // Automatically default to pass if it's available
482
  if (m_qtPass->isFreshStart() &&
×
483
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
484
    QtPassSettings::setUsePass(true);
×
485
  }
486

487
  if (m_qtPass->isFreshStart()) {
×
488
    d->wizard(); // run initial setup wizard for first-time configuration
×
489
  }
490
  if (d->exec()) {
×
491
    if (d->result() == QDialog::Accepted) {
×
492
      applyTextBrowserSettings();
×
493
      applyWindowFlagsSettings();
×
494

495
      updateProfileBox();
×
496
      const AppSettings s = QtPassSettings::load();
×
497
      proxyModel.setStore(s.passStore);
×
498
      ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
499
      deselect();
×
500
      ui->treeView->setCurrentIndex(QModelIndex());
×
501

502
      if (m_qtPass->isFreshStart() && !Util::configIsValid(s)) {
×
503
        config();
×
504
        return;
505
      }
506
      Pass *activePass = QtPassSettings::getPass();
×
507
      activePass->updateEnv();
×
508
      proxyModel.setPass(activePass);
×
509
      clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
×
510
      m_qtPass->setClipboardTimer();
×
511

512
      updateGitButtonVisibility();
×
513
      updateOtpButtonVisibility();
×
514
      updateGrepButtonVisibility();
×
515
      updateProcessOutputVisibility();
×
516
      if (s.useTrayIcon && m_tray == nullptr) {
×
517
        initTrayIcon();
×
518
      } else if (!s.useTrayIcon && m_tray != nullptr) {
×
519
        destroyTrayIcon();
×
520
      }
521
    }
×
522

523
    m_qtPass->setFreshStart(false);
×
524
  }
525
}
×
526

527
/**
528
 * @brief MainWindow::onUpdate do a git pull
529
 */
530
void MainWindow::onUpdate(bool block) {
×
531
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
532
  if (block) {
×
533
    QtPassSettings::getPass()->GitPull_b();
×
534
  } else {
535
    QtPassSettings::getPass()->GitPull();
×
536
  }
537
}
×
538

539
/**
540
 * @brief MainWindow::onPush do a git push
541
 */
542
void MainWindow::onPush() {
×
543
  if (QtPassSettings::isUseGit()) {
×
544
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
545
    QtPassSettings::getPass()->GitPush();
×
546
  }
547
}
×
548

549
/**
550
 * @brief MainWindow::getFile get the selected file path
551
 * @param index
552
 * @param forPass returns relative path without '.gpg' extension
553
 * @return path
554
 * @return
555
 */
556
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
557
  if (!index.isValid() ||
×
558
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
559
    return {};
560
  }
561
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
562
  if (forPass) {
×
563
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
564
    filePath.replace(Util::endsWithGpg(), "");
×
565
  }
566
  return filePath;
567
}
568

569
/**
570
 * @brief MainWindow::on_treeView_clicked read the selected password file
571
 * @param index
572
 */
573
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
574
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
575
  m_currentDir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
576
                              proxyModel, QtPassSettings::getPassStore());
×
UNCOV
577
  QString file = getFile(index, true);
×
578
  ui->passwordName->setText(file);
×
579
  if (!file.isEmpty() && !cleared) {
×
580
    QtPassSettings::getPass()->Show(file);
×
581
  } else {
582
    clearPanel(false);
×
583
    ui->actionEdit->setEnabled(false);
×
584
    ui->actionDelete->setEnabled(true);
×
585
  }
586
}
×
587

588
/**
589
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
590
 * TreeViewItem, open the edit Window
591
 * @param index
592
 */
593
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
594
  QFileInfo fileOrFolder =
595
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
596

597
  if (fileOrFolder.isFile()) {
×
598
    editPassword(getFile(index, true));
×
599
  }
600
}
×
601

602
/**
603
 * @brief MainWindow::deselect clear the selection, password and copy buffer
604
 */
605
void MainWindow::deselect() {
1✔
606
  m_currentDir = "";
1✔
607
  m_qtPass->clearClipboard();
1✔
608
  ui->treeView->clearSelection();
1✔
609
  ui->actionEdit->setEnabled(false);
1✔
610
  ui->actionDelete->setEnabled(false);
1✔
611
  ui->passwordName->setText("");
1✔
612
  clearPanel(false);
1✔
613
}
1✔
614

615
void MainWindow::executeWrapperStarted() {
×
616
  m_displayPanel->clear();
×
617
  ui->textBrowser->clear();
×
618
  setUiElementsEnabled(false);
×
619
  clearPanelTimer.stop();
×
620
  if (QtPassSettings::isShowProcessOutput()) {
×
621
    m_processOutputDock->setVisible(true);
×
622
  }
623
}
×
624

625
/**
626
 * @brief Handles displaying parsed password entry content in the main window.
627
 * @example
628
 * void result = MainWindow::passShowHandler(p_output);
629
 * // Updates the UI with parsed fields and emits
630
 * passShowHandlerFinished(output)
631
 *
632
 * @param p_output - The raw output text containing the password entry data.
633
 * @return void - This function does not return a value.
634
 */
635
void MainWindow::passShowHandler(const QString &p_output) {
×
636
  const AppSettings s = QtPassSettings::load();
×
637
  QStringList templ =
638
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
×
639
  bool allFields = s.useTemplate && s.templateAllFields;
×
640
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
641
  QString output = p_output;
642
  QString password = fileContent.getPassword();
×
643

644
  // set clipped text
645
  m_qtPass->setClippedText(password, p_output);
×
646

647
  // first clear the current view:
648
  m_displayPanel->clear();
×
649

650
  // show what is needed:
651
  if (s.hideContent) {
×
652
    output = "***" + tr("Content hidden") + "***";
×
653
  } else if (!s.displayAsIs) {
×
654
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s);
×
655
    output = fileContent.getRemainingDataForDisplay();
×
656
  }
657

658
  if (s.useAutoclearPanel) {
×
659
    clearPanelTimer.start();
×
660
  }
661

662
  emit passShowHandlerFinished(output);
×
663
  setUiElementsEnabled(true);
×
664
}
×
665

666
/**
667
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
668
 * and updating the UI state.
669
 * @example
670
 * void MainWindow::passOtpHandler(const QString &p_output);
671
 *
672
 * @param const QString &p_output - The OTP code text to process; if empty, an
673
 * error message is shown instead.
674
 * @return void - This function does not return a value.
675
 */
676
void MainWindow::passOtpHandler(const QString &p_output) {
×
677
  const AppSettings s = QtPassSettings::load();
×
678
  if (!p_output.isEmpty()) {
×
679
    m_displayPanel->appendField(tr("OTP Code"), p_output, s);
×
680
    m_qtPass->copyTextToClipboard(p_output);
×
681
    showStatusMessage(tr("OTP code copied to clipboard"));
×
682
  } else {
683
    flashText(tr("No OTP code found in this password entry"), true);
×
684
  }
685
  if (s.useAutoclearPanel) {
×
686
    clearPanelTimer.start();
×
687
  }
688
  setUiElementsEnabled(true);
×
689
}
×
690

691
/**
692
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
693
 */
694
void MainWindow::clearPanel(bool notify) {
1✔
695
  m_displayPanel->clear();
1✔
696
  const bool grepWasVisible = ui->grepResultsList->isVisible();
1✔
697
  ui->grepResultsList->clear();
1✔
698
  if (grepWasVisible) {
1✔
699
    ui->grepResultsList->setVisible(false);
×
700
    ui->treeView->setVisible(true);
×
701
    if (m_grep.inGrepMode()) {
×
702
      m_grep.clearGrepMode();
703
      ui->grepButton->blockSignals(true);
×
704
      ui->grepButton->setChecked(false);
×
705
      ui->grepButton->blockSignals(false);
×
706
      ui->lineEdit->blockSignals(true);
×
707
      ui->lineEdit->clear();
×
708
      ui->lineEdit->blockSignals(false);
×
709
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
710
    }
711
  }
712
  if (notify) {
1✔
713
    QString output = "***" + tr("Password and Content hidden") + "***";
×
714
    ui->textBrowser->setHtml(output);
×
715
  } else {
716
    ui->textBrowser->setHtml("");
2✔
717
  }
718
}
1✔
719

720
/**
721
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
722
 * elements
723
 * @param state
724
 */
725
void MainWindow::setUiElementsEnabled(bool state) {
15✔
726
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
727
  if (state) {
15✔
728
    m_uiWatchdog.stop();
13✔
729
  } else {
730
    m_uiWatchdog.start();
2✔
731
  }
732
  ui->treeView->setEnabled(state);
15✔
733
  ui->lineEdit->setEnabled(state);
15✔
734
  ui->actionAddPassword->setEnabled(state);
15✔
735
  ui->actionAddFolder->setEnabled(state);
15✔
736
  ui->actionUsers->setEnabled(state);
15✔
737
  ui->actionConfig->setEnabled(state);
15✔
738
  // is a file selected?
739
  state &= ui->treeView->currentIndex().isValid();
30✔
740
  ui->actionDelete->setEnabled(state);
15✔
741
  ui->actionEdit->setEnabled(state);
15✔
742
  updateGitButtonVisibility();
15✔
743
  updateOtpButtonVisibility();
15✔
744
}
15✔
745

746
/**
747
 * @brief Restores the main window geometry, state, position, size, and
748
 * tray/icon settings from saved application settings.
749
 * @example
750
 * MainWindow window;
751
 * window.restoreWindow();
752
 *
753
 * @return void - This function does not return a value.
754
 */
755
void MainWindow::restoreWindow() {
12✔
756
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
12✔
757
  restoreGeometry(geometry);
12✔
758
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
12✔
759
  restoreState(savestate);
12✔
760
  QPoint position = QtPassSettings::getPos(pos());
12✔
761
  move(position);
12✔
762
  QSize newSize = QtPassSettings::getSize(size());
12✔
763
  resize(newSize);
12✔
764
  const AppSettings s = QtPassSettings::load();
12✔
765
  if (s.maximized) {
12✔
766
    showMaximized();
×
767
  }
768

769
  if (s.alwaysOnTop) {
12✔
770
    Qt::WindowFlags flags = windowFlags();
771
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
772
    show();
×
773
  }
774

775
  if (s.useTrayIcon && m_tray == nullptr) {
12✔
776
    initTrayIcon();
×
777
    if (s.startMinimized) {
×
778
      // since we are still in constructor, can't directly hide
779
      QTimer::singleShot(10, this, SLOT(hide()));
×
780
    }
781
  } else if (!s.useTrayIcon && m_tray != nullptr) {
12✔
782
    destroyTrayIcon();
×
783
  }
784
}
24✔
785

786
/**
787
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
788
 */
789
void MainWindow::onConfig() { config(); }
×
790

791
/**
792
 * @brief Executes when the string in the search box changes, collapses the
793
 * TreeView
794
 * @param arg1
795
 */
796
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
797
  if (m_grep.inGrepMode())
×
798
    return;
799
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
800
  ui->treeView->expandAll();
×
801
  clearPanel(false);
×
802
  ui->passwordName->setText("");
×
803
  ui->actionEdit->setEnabled(false);
×
804
  ui->actionDelete->setEnabled(false);
×
805
  searchTimer.start();
×
806
}
807

808
/**
809
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
810
 * time from two keypresses is elapsed
811
 */
812
void MainWindow::onTimeoutSearch() {
×
813
  QString query = ui->lineEdit->text();
×
814

815
  if (query.isEmpty()) {
×
816
    ui->treeView->collapseAll();
×
817
    deselect();
×
818
  }
819

820
  query.replace(QStringLiteral(" "), ".*");
×
821
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
822
  if (!regExp.isValid())
×
823
    return;
824
  proxyModel.setFilterRegularExpression(regExp);
×
825
  ui->treeView->setRootIndex(
×
826
      proxyModel.rootIndexFor(QtPassSettings::getPassStore()));
×
827

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1287
  const QHash<QString, QString> prof =
1288
      QtPassSettings::getProfiles().value(name);
×
1289
  AppSettings s = QtPassSettings::load();
×
1290
  s.activeProfile = name;
×
1291
  s.passStore = prof.value("path");
×
1292
  s.passSigningKey = prof.value("signingKey");
×
1293
  QtPassSettings::save(s);
×
1294
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1295

1296
  QtPassSettings::getPass()->updateEnv();
×
1297

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

1305
/**
1306
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1307
 * support
1308
 * it
1309
 */
1310
void MainWindow::initTrayIcon() {
×
1311
  m_tray = new TrayIcon(this);
×
1312
  if (!m_tray->getIsAllocated()) {
×
1313
    destroyTrayIcon();
×
1314
  }
1315
}
×
1316

1317
/**
1318
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1319
 */
1320
void MainWindow::destroyTrayIcon() {
×
1321
  delete m_tray;
×
1322
  m_tray = nullptr;
×
1323
}
×
1324

1325
/**
1326
 * @brief MainWindow::closeEvent hide or quit
1327
 * @param event
1328
 */
1329
void MainWindow::closeEvent(QCloseEvent *event) {
×
1330
  if (QtPassSettings::isHideOnClose()) {
×
1331
    this->hide();
×
1332
    event->ignore();
1333
  } else {
1334
    m_qtPass->clearClipboard();
×
1335

1336
    QtPassSettings::setGeometry(saveGeometry());
×
1337
    QtPassSettings::setSavestate(saveState());
×
1338
    QtPassSettings::setMaximized(isMaximized());
×
1339
    if (!isMaximized()) {
×
1340
      QtPassSettings::setPos(pos());
×
1341
      QtPassSettings::setSize(size());
×
1342
    }
1343
    event->accept();
1344
    // A visible QSystemTrayIcon keeps the application alive after the last
1345
    // window closes, so quitOnLastWindowClosed never fires and the window
1346
    // merely vanishes into the tray. Quit explicitly so closing the window
1347
    // actually exits when "hide on close" is disabled.
1348
    QApplication::quit();
×
1349
  }
1350
}
×
1351

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

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

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

1409
  ui->treeView->setCurrentIndex(index);
×
1410

1411
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1412

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

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

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

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

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

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

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

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

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

1481
/**
1482
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1483
 * password window
1484
 * @param pos
1485
 */
1486
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1487
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1488
  // createStandardContextMenu() parents the menu to textBrowser, which carries
1489
  // a "background: palette(base)" stylesheet. Qt cascades that stylesheet to
1490
  // the child QMenu and breaks its opaque native background, leaving the menu
1491
  // transparent. Reparent to the main window (no stylesheet) so it paints
1492
  // solid.
1493
  contextMenu->setParent(this, contextMenu->windowFlags());
×
1494
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1495

1496
  contextMenu->exec(globalPos);
×
1497
  delete contextMenu;
×
1498
}
×
1499

1500
/**
1501
 * @brief MainWindow::openFolder open the folder in the default file manager
1502
 */
1503
void MainWindow::openFolder() {
×
1504
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1505
                             proxyModel, QtPassSettings::getPassStore());
×
1506

1507
  QString path = QDir::toNativeSeparators(dir);
×
1508
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1509
}
×
1510

1511
/**
1512
 * @brief MainWindow::addFolder add a new folder to store passwords in
1513
 */
1514
void MainWindow::addFolder() {
×
1515
  const AppSettings s = QtPassSettings::load();
×
1516
  bool ok;
1517
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1518
                             proxyModel, s.passStore);
×
1519
  QString newdir = QInputDialog::getText(
1520
      this, tr("New file"),
×
1521
      tr("New Folder: \n(Will be placed in %1 )")
×
1522
          .arg(s.passStore + Util::getDir(ui->treeView->currentIndex(), true,
×
1523
                                          model, proxyModel, s.passStore)),
1524
      QLineEdit::Normal, "", &ok);
×
1525
  if (!ok || newdir.isEmpty()) {
×
1526
    return;
1527
  }
1528
  newdir.prepend(dir);
1529
  if (!confirmPathInStore(newdir)) {
×
1530
    return;
1531
  }
1532
  if (!QDir().mkdir(newdir)) {
×
1533
    QMessageBox::warning(this, tr("Error"),
×
1534
                         tr("Failed to create folder: %1").arg(newdir));
×
1535
    return;
×
1536
  }
1537
  if (s.addGPGId) {
×
1538
    QString gpgIdFile = newdir + "/.gpg-id";
×
1539
    QFile gpgId(gpgIdFile);
×
1540
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1541
      QMessageBox::warning(
×
1542
          this, tr("Error"),
×
1543
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1544
      return;
1545
    }
1546
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1547
    for (const UserInfo &user : users) {
×
1548
      if (user.enabled) {
×
1549
        gpgId.write((user.key_id + "\n").toUtf8());
×
1550
      }
1551
    }
1552
    gpgId.close();
×
1553
    // Lock to owner-only access; see ImitatePass::writeGpgIdFile for
1554
    // rationale (NFS / USB / unusual umask scenarios). Best-effort on
1555
    // platforms where setPermissions is a no-op.
1556
    QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
×
1557
  }
×
1558
}
×
1559

1560
/**
1561
 * @brief MainWindow::renameFolder rename an existing folder
1562
 */
1563
void MainWindow::renameFolder() {
×
1564
  bool ok;
1565
  QString srcDir =
1566
      QDir::cleanPath(Util::getDir(ui->treeView->currentIndex(), false, model,
×
1567
                                   proxyModel, QtPassSettings::getPassStore()));
×
1568
  QString srcDirName = QDir(srcDir).dirName();
×
1569
  QString newName =
1570
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1571
                            QLineEdit::Normal, srcDirName, &ok);
×
1572
  if (!ok || newName.isEmpty()) {
×
1573
    return;
1574
  }
1575
  QString destDir = srcDir;
1576
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1577
  if (!confirmPathInStore(destDir)) {
×
1578
    return;
1579
  }
1580
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1581
}
1582

1583
/**
1584
 * @brief MainWindow::editPassword read password and open edit window via
1585
 * MainWindow::onEdit()
1586
 */
1587
void MainWindow::editPassword(const QString &file) {
×
1588
  if (!file.isEmpty()) {
×
1589
    const AppSettings s = QtPassSettings::load();
×
1590
    if (s.useGit && s.autoPull) {
×
1591
      onUpdate(true);
×
1592
    }
1593
    setPassword(file, false);
×
1594
  }
×
1595
}
×
1596

1597
/**
1598
 * @brief MainWindow::renamePassword rename an existing password
1599
 */
1600
void MainWindow::renamePassword() {
×
1601
  bool ok;
1602
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1603
  QString filePath = QFileInfo(file).path();
×
1604
  QString fileName = QFileInfo(file).fileName();
×
1605
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1606
    fileName.chop(4);
×
1607
  }
1608

1609
  QString newName =
1610
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1611
                            QLineEdit::Normal, fileName, &ok);
×
1612
  if (!ok || newName.isEmpty()) {
×
1613
    return;
1614
  }
1615
  QString newFile = QDir(filePath).filePath(newName);
×
1616
  if (!confirmPathInStore(newFile)) {
×
1617
    return;
1618
  }
1619
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1620
}
1621

1622
/**
1623
 * @brief Copies the password of the selected file from the tree view to the
1624
 * clipboard.
1625
 * @example
1626
 * MainWindow::copyPasswordFromTreeview();
1627
 *
1628
 * @return void - This function does not return a value.
1629
 */
1630
void MainWindow::copyPasswordFromTreeview() {
×
1631
  QFileInfo fileOrFolder =
1632
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1633

1634
  if (fileOrFolder.isFile()) {
×
1635
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1636
    // Disconnect any previous connection to avoid accumulation
1637
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1638
               &MainWindow::passwordFromFileToClipboard);
1639
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1640
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1641
            &MainWindow::passwordFromFileToClipboard, Qt::SingleShotConnection);
×
1642
#else
1643
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
1644
            &MainWindow::passwordFromFileToClipboard);
1645
#endif
1646
    QtPassSettings::getPass()->Show(file);
×
1647
  }
1648
}
×
1649

1650
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1651
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1652
  // Qt 5: no SingleShotConnection flag — disconnect manually on first fire.
1653
  disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
1654
             &MainWindow::passwordFromFileToClipboard);
1655
#endif
1656
  QStringList tokens = text.split('\n');
×
1657
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1658
}
×
1659

1660
/**
1661
 * @brief Displays message in status bar
1662
 *
1663
 * @param msg     text to be displayed
1664
 * @param timeout time for which msg shall be visible
1665
 */
1666
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
2✔
1667
  ui->statusBar->showMessage(msg, timeout);
2✔
1668
}
2✔
1669

1670
/**
1671
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1672
 * @param dir Directory path to re-encrypt
1673
 */
1674
void MainWindow::reencryptPath(const QString &dir) {
×
1675
  QDir checkDir(dir);
×
1676
  if (!checkDir.exists()) {
×
1677
    QMessageBox::critical(this, tr("Error"),
×
1678
                          tr("Directory does not exist: %1").arg(dir));
×
1679
    return;
×
1680
  }
1681

1682
  int ret = QMessageBox::question(
×
1683
      this, tr("Re-encrypt passwords"),
×
1684
      tr("Re-encrypt all passwords in %1?\n\n"
×
1685
         "This will re-encrypt ALL password files in this folder "
1686
         "using the current recipients defined in .gpg-id.\n\n"
1687
         "This may rewrite many files and cannot be undone easily.\n\n"
1688
         "Continue?")
1689
          .arg(QDir(dir).dirName()),
×
1690
      QMessageBox::Yes | QMessageBox::No);
1691

1692
  if (ret != QMessageBox::Yes)
×
1693
    return;
1694

1695
  // Disable preemptively. ImitatePass::reencryptPath emits
1696
  // startReencryptPath asynchronously and the slot would re-run this,
1697
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1698
  startReencryptPath();
×
1699

1700
  QtPassSettings::getImitatePass()->reencryptPath(
×
1701
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1702
}
×
1703

1704
/**
1705
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1706
 */
1707
void MainWindow::startReencryptPath() {
×
1708
  setUiElementsEnabled(false);
×
1709
  ui->treeView->setDisabled(true);
×
1710
}
×
1711

1712
/**
1713
 * @brief MainWindow::endReencryptPath re-enable ui elements
1714
 */
1715
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1716

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

1759
/**
1760
 * @brief MainWindow::addRecipient open the recipient management dialog for
1761
 *        the supplied directory.
1762
 * @param dir Folder whose .gpg-id should be edited.
1763
 *
1764
 * Delegates to UsersDialog so users can tick/untick keys from their
1765
 * keyring as recipients of the folder; importing a foreign key into the
1766
 * keyring still has to happen via gpg (or QtPass settings) first.
1767
 */
1768
void MainWindow::addRecipient(const QString &dir) {
×
1769
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1770
  d.exec();
×
1771
}
×
1772

1773
/**
1774
 * @brief MainWindow::showShareHelp show help about GPG sharing
1775
 */
1776
void MainWindow::showShareHelp() {
×
1777
  QMessageBox::information(
×
1778
      this, tr("Sharing Passwords with GPG"),
×
1779
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1780
         "<p>To share passwords with other users:</p>"
1781
         "<ol>"
1782
         "<li><b>Export your public key</b> and send it to teammates</li>"
1783
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1784
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1785
         "them</li>"
1786
         "</ol>"
1787
         "<p>Only people who have a matching secret key can decrypt the "
1788
         "passwords.</p>"
1789
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1790
         "<p>See the FAQ for more details.</p>"));
1791
}
×
1792

1793
void MainWindow::updateGitButtonVisibility() {
15✔
1794
  const AppSettings s = QtPassSettings::load();
15✔
1795
  if (!s.useGit || (s.gitExecutable.isEmpty() && s.passExecutable.isEmpty())) {
15✔
1796
    enableGitButtons(false);
15✔
1797
  } else {
1798
    enableGitButtons(true);
×
1799
  }
1800
}
15✔
1801

1802
void MainWindow::updateOtpButtonVisibility() {
15✔
1803
#if defined(Q_OS_WIN) || defined(__APPLE__)
1804
  ui->actionOtp->setVisible(false);
1805
#endif
1806
  if (!QtPassSettings::isUseOtp()) {
15✔
1807
    ui->actionOtp->setEnabled(false);
15✔
1808
  } else {
1809
    ui->actionOtp->setEnabled(true);
×
1810
  }
1811
}
15✔
1812

1813
void MainWindow::updateGrepButtonVisibility() {
12✔
1814
  const bool enabled = QtPassSettings::isUseGrepSearch();
12✔
1815
  ui->grepButton->setVisible(enabled);
12✔
1816
  ui->grepCaseButton->setVisible(enabled);
12✔
1817
  if (!enabled && m_grep.inGrepMode()) {
12✔
1818
    ui->grepButton->setChecked(false);
×
1819
  }
1820
}
12✔
1821

1822
void MainWindow::enableGitButtons(const bool &state) {
15✔
1823
  // Following GNOME guidelines is preferable disable buttons instead of hide
1824
  ui->actionPush->setEnabled(state);
15✔
1825
  ui->actionUpdate->setEnabled(state);
15✔
1826
}
15✔
1827

1828
/**
1829
 * @brief MainWindow::critical critical message popup wrapper.
1830
 * @param title
1831
 * @param msg
1832
 */
1833
void MainWindow::critical(const QString &title, const QString &msg) {
×
1834
  QMessageBox::critical(this, title, msg);
×
1835
}
×
1836

1837
/**
1838
 * @brief Appends processed command output to the output panel.
1839
 *
1840
 * Appends text to the process output text edit, with per-line numbering,
1841
 * optional command prefix, and color coding for errors vs. success.
1842
 * Handles auto-scrolling and line limits.
1843
 *
1844
 * @param output The raw output text from the command.
1845
 * @param isError true if this is error output (stderr).
1846
 * @param linePrefix Optional command name to prefix each line with.
1847
 */
1848
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2✔
1849
                                     const QString &linePrefix) {
1850
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
1851
    return;
1✔
1852
  }
1853

1854
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1855
  for (QString &line : lines) {
2✔
1856
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1857
    // indentation
1858
    line.remove('\r');
1✔
1859
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1860
      line.chop(1);
×
1861
    }
1862
    if (line.isEmpty()) {
1✔
1863
      continue;
×
1864
    }
1865

1866
    m_outputCounter++;
1✔
1867
    QString lineNumber = QString::number(m_outputCounter);
1✔
1868

1869
    QColor textColor =
1870
        isError ? QColor(Qt::red)
1✔
1871
                : m_processOutputEdit->palette().color(QPalette::Text);
2✔
1872
    QString colorHex = textColor.name();
1✔
1873
    // Apply the optional prefix per line so multi-line output stays
1874
    // attributed to its command (e.g. all 3 lines of a `git push` show
1875
    // "git push: ..." rather than only the first).
1876
    QString prefixed =
1877
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2✔
1878
    QString coloredOutput =
1879
        QString("<span style=\"color: %1;\">%2: %3</span>")
1✔
1880
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2✔
1881

1882
    m_processOutputEdit->append(coloredOutput);
1✔
1883
  }
1884

1885
  limitOutputLines();
1✔
1886

1887
  if (m_autoScroll) {
1✔
1888
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
1889
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
1890
  }
1891
}
1892

1893
/**
1894
 * @brief Handles process output from the Pass executor.
1895
 *
1896
 * Called when any non-sensitive process completes. Filters out password-
1897
 * related commands (pass show, insert, etc.) and delegates to
1898
 * appendProcessOutput.
1899
 *
1900
 * @param output The stdout/stderr text from the process.
1901
 * @param isError true if this is error output (stderr).
1902
 * @param pid The process ID identifying which command ran.
1903
 */
1904
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
1905
                                 Enums::PROCESS pid) {
1906
  appendProcessOutput(output, isError, getProcessName(pid));
2✔
1907
}
2✔
1908

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

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

2000
/**
2001
 * @brief Updates the visibility of the process output panel.
2002
 *
2003
 * Shows or hides the process output widget based on the user's
2004
 * showProcessOutput setting.
2005
 */
2006
void MainWindow::updateProcessOutputVisibility() {
×
2007
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
2008
}
×
2009

2010
/**
2011
 * @brief Limits the output panel to max lines, trimming old excess.
2012
 *
2013
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
2014
 * Called after each append to prevent unbounded growth.
2015
 */
2016
void MainWindow::limitOutputLines() {
1✔
2017
  QTextDocument *doc = m_processOutputEdit->document();
1✔
2018
  int excess = doc->blockCount() - MaxOutputLines;
1✔
2019
  if (excess <= 0) {
1✔
2020
    return;
1✔
2021
  }
2022

2023
  QTextCursor cursor(doc);
×
2024
  cursor.movePosition(QTextCursor::Start);
×
2025
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2026
  cursor.removeSelectedText();
×
2027
}
×
2028

2029
/**
2030
 * @brief Clears the process output panel.
2031
 *
2032
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2033
 */
2034
void MainWindow::on_clearOutputButton_clicked() {
×
2035
  m_processOutputEdit->clear();
×
2036
  m_outputCounter = 0;
×
2037
  m_autoScroll = true;
×
2038
}
×
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