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

IJHack / QtPass / 35159963146

16 Sep 2026 10:55PM UTC coverage: 72.009% (-0.3%) from 72.336%
35159963146

push

github

web-flow
One "qtpass" logging category instead of dbg() and 69 QT_DEBUG guards (#1785)

* One "qtpass" logging category instead of dbg() and 69 QT_DEBUG guards

debughelper.h promised file:line output "only in debug builds" while
src.pro already compiled qDebug out of release builds, and the #ifdef
blocks spliced control flow (an else in trayicon.cpp, an else-if in
sshauthsock.cpp) and have broken the build before (55149eca1, 0b6a80bdf).

Q_LOGGING_CATEGORY(lcQtPass, "qtpass", QtInfoMsg): every dbg() is a
qCDebug(lcQtPass), the debug-only qWarning()s are qCWarning(lcQtPass), the
guards are gone, and QT_NO_DEBUG_OUTPUT is no longer defined for release,
so a shipped build can produce a trace with
QT_LOGGING_RULES="qtpass.debug=true". FAQ entry and fixing skill updated;
tst_util checks the category is quiet until asked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* Convert the last bare qDebug(); FAQ: key IDs

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------

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

36 of 78 new or added lines in 15 files covered. (46.15%)

23 existing lines in 8 files now uncovered.

5513 of 7656 relevant lines covered (72.01%)

60.03 hits per line

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

42.68
/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
#include "qtpasslogging.h"
6

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

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

70
  m_qtPass = new QtPass(this);
33 ✔
71

72
  // register shortcut ctrl/cmd + Q to close the main window
73
  new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, this,
33 ✔
74
                &MainWindow::close);
33 ✔
75
  // register shortcut ctrl/cmd + C to copy the currently selected password
76
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
33 ✔
77
                &MainWindow::copyPasswordFromTreeview);
33 ✔
78

79
  model.setNameFilters(QStringList() << "*.gpg");
99 ✔
80
  model.setNameFilterDisables(false);
33 ✔
81

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

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

87
  proxyModel.setModelAndStore(&model, passStore);
33 ✔
88
  proxyModel.setPass(QtPassSettings::getPass());
33 ✔
89

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

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

123
  updateProfileBox();
33 ✔
124

125
  m_displayPanel = new PasswordDisplayPanel(
33 ✔
126
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
33 ✔
127
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
33 ✔
128
          &m_qtPass->clipboard(), &ClipboardManager::copyText);
33 ✔
129
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
33 ✔
130
          &QtPass::showTextAsQRCode);
33 ✔
131

132
  QtPassSettings::getPass()->updateEnv();
33 ✔
133
  clearPanelTimer.setSingleShot(true);
33 ✔
134
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
33 ✔
135

136
  searchTimer.setInterval(350);
33 ✔
137
  searchTimer.setSingleShot(true);
33 ✔
138

139
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
33 ✔
140

141
  // Install the search-box key filter once, not on every setUiElementsEnabled
142
  // call.
143
  ui->lineEdit->installEventFilter(this);
33 ✔
144
  ui->toolBar->installEventFilter(this);
33 ✔
145

146
  // Safety net: if a backend operation disables the UI but never signals
147
  // completion, re-enable after a timeout so the window can't get stuck.
148
  m_uiWatchdog.setSingleShot(true);
33 ✔
149
  m_uiWatchdog.setInterval(UiWatchdogMs);
33 ✔
150
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
33 ✔
151
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
152
    // Drop any in-flight OTP request so a late finishedShow cannot be mistaken
153
    // for the answer to it.
154
    cancelOtpRequest();
×
155
    setUiElementsEnabled(true);
×
156
  });
×
157

158
  initToolBarButtons();
33 ✔
159
  initStatusBar();
33 ✔
160
  initProcessOutputPanel();
33 ✔
161

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

183
  ui->lineEdit->setClearButtonEnabled(true);
33 ✔
184
  updateGrepButtonVisibility();
33 ✔
185

186
  setUiElementsEnabled(true);
33 ✔
187

188
  ui->lineEdit->setText(searchText);
33 ✔
189

190
  // Record whether startup configuration succeeded instead of calling
191
  // QApplication::quit() here: quit() before the event loop runs (exec() is
192
  // still ahead in main()) is a documented no-op, so a cancelled first-run
193
  // wizard used to leave the half-configured window showing anyway. main()
194
  // consults initSucceeded() and exits before show() when this is false.
195
  m_initSucceeded = m_qtPass->init();
33 ✔
196
  if (!m_initSucceeded) {
33 ✔
197
    return;
198
  }
199

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

209
MainWindow::~MainWindow() {
62 ✔
210
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
211
  // window still knows its last geometry here.
212
  saveWindowState();
33 ✔
213
  delete m_qtPass;
33 ✔
214
}
95 ✔
215

216
/**
217
 * @brief MainWindow::saveWindowState persist geometry and dock/toolbar
218
 *        layout. Idempotent, cheap, called from closeEvent() and the
219
 *        destructor.
220
 */
221
void MainWindow::saveWindowState() {
34 ✔
222
  QtPassSettings::setGeometry(saveGeometry());
34 ✔
223
  QtPassSettings::setSavestate(saveState());
34 ✔
224
}
34 ✔
225

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

264
/**
265
 * @brief MainWindow::changeEvent sets focus to the search box on activation
266
 * and re-derives palette-dependent styling on a theme change.
267
 * @param event
268
 */
269
void MainWindow::changeEvent(QEvent *event) {
49 ✔
270
  QWidget::changeEvent(event);
49 ✔
271
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
49 ✔
272
    // Desktop switched light/dark (e.g. KDE day/night). Top-level widgets
273
    // receive PaletteChange for that (ApplicationPaletteChange goes to the
274
    // QApplication object). Styling that bakes palette colours into
275
    // stylesheets must be re-derived by hand.
276
    m_displayPanel->refreshPalette();
5 ✔
277
  }
278
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
49 ✔
279
      isVisible()) {
280
    // Defer one event-loop tick so the synchronous activation dispatch
281
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
282
    // unwinds before we touch widget state — calling `focusInput()`
283
    // inline from this stack has segfaulted in past iterations because
284
    // mid-rebuild ui state isn't fully wired up yet.
285
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
8 ✔
286
                              Qt::QueuedConnection);
287
  }
288
}
49 ✔
289

290
/**
291
 * @brief First-show hook: run the initial focusInput() pulse once the
292
 *        window is actually mapped. The widget's internal data is fully
293
 *        initialised by this point, so QLineEdit::selectAll() is safe.
294
 * @param event Show event passed to the base class.
295
 */
296
void MainWindow::showEvent(QShowEvent *event) {
10 ✔
297
  QMainWindow::showEvent(event);
10 ✔
298
  if (m_firstShowCompleted) {
10 ✔
299
    return;
300
  }
301
  // Queue the focus pulse for the next event-loop tick so the platform
302
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
303
  // from the config wizard path) settle before we look up the line
304
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
305
  // *after* it actually focuses, so a transient failed lookup just
306
  // re-queues on the next show rather than silently dropping.
307
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
8 ✔
308
                            Qt::QueuedConnection);
309
}
310

311
/**
312
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
313
 */
314
void MainWindow::initToolBarButtons() {
33 ✔
315
  connect(ui->actionAddPassword, &QAction::triggered, this,
33 ✔
316
          &MainWindow::addPassword);
33 ✔
317
  connect(ui->actionAddFolder, &QAction::triggered, this,
33 ✔
318
          &MainWindow::addFolder);
33 ✔
319
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
33 ✔
320
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
33 ✔
321
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
33 ✔
322
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
33 ✔
323
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
33 ✔
324
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
33 ✔
325
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
33 ✔
326

327
  ui->actionAddPassword->setIcon(
33 ✔
328
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
132 ✔
329
  ui->actionAddFolder->setIcon(
33 ✔
330
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
132 ✔
331
  ui->actionEdit->setIcon(QIcon::fromTheme(
33 ✔
332
      "document-properties", QIcon(":/icons/document-properties.svg")));
99 ✔
333
  ui->actionDelete->setIcon(
33 ✔
334
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
132 ✔
335
  ui->actionPush->setIcon(
33 ✔
336
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
132 ✔
337
  ui->actionUpdate->setIcon(
33 ✔
338
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
132 ✔
339
  ui->actionUsers->setIcon(QIcon::fromTheme(
33 ✔
340
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
99 ✔
341
  ui->actionConfig->setIcon(QIcon::fromTheme(
33 ✔
342
      "applications-system", QIcon(":/icons/applications-system.svg")));
66 ✔
343
}
33 ✔
344

345
/**
346
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
347
 */
348
void MainWindow::initStatusBar() {
33 ✔
349
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
66 ✔
350

351
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
66 ✔
352
                     .scaledToHeight(statusBar()->height());
33 ✔
353
  auto *logoApp = new QLabel(statusBar());
33 ✔
354
  logoApp->setPixmap(logo);
33 ✔
355
  statusBar()->addPermanentWidget(logoApp);
33 ✔
356
}
33 ✔
357

358
/**
359
 * @brief Build the process-output panel as a bottom QDockWidget.
360
 *
361
 * The panel is constructed programmatically rather than declared in
362
 * mainwindow.ui: uic only places QMainWindow's top-level children into
363
 * the centralWidget / statusBar / menuBar / toolBars / dock-widget
364
 * slots, and the previous home (statusBar()->addPermanentWidget()) made
365
 * an 80–150 px tall QTextEdit sit inside what is otherwise a thin
366
 * status row. A QDockWidget at the bottom dock area is the conventional
367
 * place for an IDE-style output console, and it gives users
368
 * detach/move for free.
369
 */
370
void MainWindow::initProcessOutputPanel() {
33 ✔
371
  m_processOutputWidget = new QWidget;
33 ✔
372
  m_processOutputWidget->setObjectName(QStringLiteral("processOutputWidget"));
66 ✔
373
  auto *outputLayout = new QHBoxLayout(m_processOutputWidget);
33 ✔
374
  outputLayout->setObjectName(QStringLiteral("processOutputLayout"));
66 ✔
375
  outputLayout->setContentsMargins(0, 0, 0, 0);
33 ✔
376
  m_clearOutputButton = new QToolButton(m_processOutputWidget);
33 ✔
377
  m_clearOutputButton->setObjectName(QStringLiteral("clearOutputButton"));
66 ✔
378
  m_clearOutputButton->setText(tr("Clear"));
33 ✔
379
  m_clearOutputButton->setToolTip(tr("Clear output"));
33 ✔
380
  outputLayout->addWidget(m_clearOutputButton);
33 ✔
381
  m_processOutputEdit = new QTextEdit(m_processOutputWidget);
33 ✔
382
  m_processOutputEdit->setObjectName(QStringLiteral("processOutputEdit"));
66 ✔
383
  m_processOutputEdit->setReadOnly(true);
33 ✔
384
  m_processOutputEdit->setAcceptRichText(false);
33 ✔
385
  outputLayout->addWidget(m_processOutputEdit);
33 ✔
386

387
  m_processOutputDock = new QDockWidget(tr("Process Output"), this);
33 ✔
388
  m_processOutputDock->setObjectName(QStringLiteral("processOutputDock"));
66 ✔
389
  m_processOutputDock->setFeatures(QDockWidget::DockWidgetMovable |
33 ✔
390
                                   QDockWidget::DockWidgetFloatable);
391
  m_processOutputDock->setAllowedAreas(Qt::BottomDockWidgetArea |
33 ✔
392
                                       Qt::TopDockWidgetArea);
393
  m_processOutputDock->setWidget(m_processOutputWidget);
33 ✔
394
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutputDock);
33 ✔
395
  // setVisible after addDockWidget so our explicit preference wins
396
  // even if QMainWindow applies any cached state when the dock is
397
  // attached. restoreWindow() runs before this method (it's called
398
  // from the QtPass ctor, which is constructed at the top of the
399
  // MainWindow ctor), so the saved layout has already been processed
400
  // by the time we get here.
401
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
33 ✔
402

403
  connect(m_clearOutputButton, &QToolButton::clicked, this,
33 ✔
404
          &MainWindow::on_clearOutputButton_clicked);
33 ✔
405

406
  // Hysteresis: while the user is actively dragging the slider, don't
407
  // touch m_autoScroll on every tick — a brief overshoot at maximum
408
  // would silently re-arm auto-scroll without an explicit release. Only
409
  // commit on slider release. Wheel/keyboard scroll never sets
410
  // isSliderDown(), so they still update immediately.
411
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
33 ✔
412
          this, [this]() {
33 ✔
413
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
414
            if (sb->isSliderDown())
×
415
              return;
416
            m_autoScroll = sb->value() >= sb->maximum();
×
417
          });
418
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderReleased,
33 ✔
419
          this, [this]() {
33 ✔
420
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
421
            m_autoScroll = sb->value() >= sb->maximum();
×
422
          });
×
423
}
33 ✔
424

425
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1 ✔
426
  return ui->treeView->currentIndex();
1 ✔
427
}
428

429
void MainWindow::cleanKeygenDialog() {
1 ✔
430
  if (m_keyGenDialog != nullptr) {
1 ✔
431
    m_keyGenDialog->close();
×
432
  }
433
  m_keyGenDialog = nullptr;
434
}
1 ✔
435

436
/**
437
 * @brief Displays the given text in the main window text browser, optionally
438
 * marking it as an error and/or rendering it as HTML.
439
 * @example
440
 * MainWindow window;
441
 * window.flashText("Operation completed.", false, false);
442
 *
443
 * @param const QString &text - The text content to display.
444
 * @param const bool isError - If true, sets the text color to red before
445
 * displaying the text; otherwise any earlier error colour is cleared.
446
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
447
 * the existing HTML content.
448
 * @return void - No return value.
449
 */
450
void MainWindow::flashText(const QString &text, const bool isError,
12 ✔
451
                           const bool isHtml) {
452
  if (isError) {
12 ✔
453
    ui->textBrowser->setTextColor(Qt::red);
3 ✔
454
  } else {
455
    // setTextColor() merges the red foreground into the browser's current
456
    // char format, and setText()/setPlainText() re-applies that format to the
457
    // whole new document. Without clearing it, a plain-text non-error message
458
    // shown after an error would still be red. Remove the property rather
459
    // than pinning a palette colour: an explicit foreground would stop the
460
    // text from following runtime light/dark palette switches (#946).
461
    QTextCharFormat format = ui->textBrowser->currentCharFormat();
9 ✔
462
    format.clearForeground();
463
    ui->textBrowser->setCurrentCharFormat(format);
9 ✔
464
  }
465

466
  if (isHtml) {
12 ✔
467
    QString _text = text;
468
    if (!ui->textBrowser->toPlainText().isEmpty()) {
14 ✔
469
      _text = ui->textBrowser->toHtml() + _text;
4 ✔
470
    }
471
    ui->textBrowser->setHtml(_text);
7 ✔
472
  } else {
473
    ui->textBrowser->setText(text);
5 ✔
474
  }
475
}
12 ✔
476

477
/**
478
 * @brief MainWindow::config pops up the configuration screen and handles all
479
 * inter-window communication
480
 */
481
void MainWindow::applyTextBrowserSettings() {
3 ✔
482
  const AppSettings s = QtPassSettings::load();
3 ✔
483
  if (s.useMonospace) {
3 ✔
484
    QFont monospace("Monospace");
×
485
    monospace.setStyleHint(QFont::Monospace);
×
486
    ui->textBrowser->setFont(monospace);
×
487
  } else {
×
488
    ui->textBrowser->setFont(QFont());
3 ✔
489
  }
490

491
  if (s.noLineWrapping) {
3 ✔
492
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
493
  } else {
494
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
3 ✔
495
  }
496
}
3 ✔
497

498
void MainWindow::applyWindowFlagsSettings() {
43 ✔
499
  const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
43 ✔
500
  const Qt::WindowFlags flags = windowFlags();
501
  if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
43 ✔
502
    // Nothing to change. setWindowFlags() on a top-level widget rebuilds the
503
    // native window (setParent(nullptr, flags)), which used to happen on every
504
    // Settings OK because the "off" branch never compared equal — the same
505
    // teardown that left ui->lineEdit dangling (see focusInput()).
506
    return;
507
  }
508
  // Toggle just the one hint; the old "off" branch reset to Qt::Window and
509
  // discarded every other flag with it.
510
  const bool wasVisible = isVisible();
511
  setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
4 ✔
512
                           : (flags & ~Qt::WindowStaysOnTopHint));
513
  // setWindowFlags() hides a shown window; a window that was not shown yet
514
  // (first-run wizard from the constructor) is left to its normal show.
515
  if (wasVisible) {
2 ✔
516
    show();
2 ✔
517
  }
518
}
519

520
/**
521
 * @brief Opens and processes the application configuration dialog, then applies
522
 * any accepted settings.
523
 * @example
524
 * if (!config()) {
525
 *   // the user cancelled
526
 * }
527
 *
528
 * @return bool - True when the dialog was accepted, false when it was
529
 * cancelled. An accepted dialog can still leave the configuration invalid
530
 * (the OK button is not gated on Util::configIsValid()); QtPass::init() keeps
531
 * asking until the configuration is usable or this returns false.
532
 */
533
auto MainWindow::config() -> bool {
4 ✔
534
  ConfigDialog d(this);
4 ✔
535
  d.setModal(true);
4 ✔
536
  // Automatically default to pass if it's available
537
  if (m_qtPass->isFreshStart() &&
4 ✔
538
      QFile(QtPassSettings::getPassExecutable()).exists()) {
12 ✔
539
    QtPassSettings::setUsePass(true);
×
540
  }
541

542
  if (m_qtPass->isFreshStart()) {
4 ✔
543
    d.wizard(); // run initial setup wizard for first-time configuration
4 ✔
544
  }
545
  if (d.exec() != QDialog::Accepted) {
4 ✔
546
    return false;
547
  }
548

549
  applyTextBrowserSettings();
3 ✔
550
  applyWindowFlagsSettings();
3 ✔
551

552
  updateProfileBox();
3 ✔
553
  const AppSettings s = QtPassSettings::load();
3 ✔
554
  proxyModel.setStore(s.passStore);
3 ✔
555
  ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
3 ✔
556
  deselect();
3 ✔
557
  ui->treeView->setCurrentIndex(QModelIndex());
3 ✔
558

559
  Pass *activePass = QtPassSettings::getPass();
3 ✔
560
  activePass->updateEnv();
3 ✔
561
  proxyModel.setPass(activePass);
3 ✔
562
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
3 ✔
563
  m_qtPass->clipboard().setAutoclearTimer();
3 ✔
564

565
  updateGitButtonVisibility();
3 ✔
566
  updateOtpButtonVisibility();
3 ✔
567
  updateGrepButtonVisibility();
3 ✔
568
  updateProcessOutputVisibility();
3 ✔
569
  if (s.useTrayIcon && m_tray == nullptr) {
3 ✔
570
    initTrayIcon();
×
571
  } else if (!s.useTrayIcon && m_tray != nullptr) {
3 ✔
572
    destroyTrayIcon();
×
573
  }
574

575
  // Leave the fresh-start state in place while the accepted configuration is
576
  // still unusable (for example the user declined to create the store), so
577
  // the next attempt from QtPass::init() runs the first-run wizard again
578
  // instead of showing the bare dialog. The re-prompt itself lives in init():
579
  // recursing here re-ran the dialog on this stack frame and never reported
580
  // a cancel back to the caller.
581
  if (Util::configIsValid(s)) {
3 ✔
582
    m_qtPass->setFreshStart(false);
1 ✔
583
  }
584
  return true;
585
}
4 ✔
586

587
/**
588
 * @brief MainWindow::onUpdate do a git pull
589
 */
590
void MainWindow::onUpdate(bool block) {
×
591
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
592
  if (block) {
×
593
    QtPassSettings::getPass()->GitPull_b();
×
594
  } else {
595
    QtPassSettings::getPass()->GitPull();
×
596
  }
597
}
×
598

599
/**
600
 * @brief MainWindow::onPush do a git push
601
 */
602
void MainWindow::onPush() {
×
603
  if (QtPassSettings::isUseGit()) {
×
604
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
605
    QtPassSettings::getPass()->GitPush();
×
606
  }
607
}
×
608

609
/**
610
 * @brief MainWindow::getFile get the selected file path
611
 * @param index
612
 * @param forPass returns relative path without '.gpg' extension
613
 * @return path
614
 * @return
615
 */
616
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
3 ✔
617
  if (!index.isValid() ||
3 ✔
618
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
6 ✔
619
    return {};
620
  }
621
  QString filePath = model.filePath(proxyModel.mapToSource(index));
3 ✔
622
  if (forPass) {
3 ✔
623
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
9 ✔
624
    filePath.replace(Util::endsWithGpg(), "");
6 ✔
625
  }
626
  return filePath;
627
}
628

629
/**
630
 * @brief MainWindow::on_treeView_clicked read the selected password file
631
 * @param index
632
 */
633
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
2 ✔
634
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
2 ✔
635
  QString file = getFile(index, true);
2 ✔
636
  ui->passwordName->setText(file);
2 ✔
637
  if (!file.isEmpty() && !cleared) {
2 ✔
638
    // Remember what the panel is about to show, so onOtp() can tell whether the
639
    // code it can see belongs to the entry that is currently selected.
640
    m_shownFile = file;
2 ✔
641
    QtPassSettings::getPass()->Show(file);
4 ✔
642
  } else {
643
    m_shownFile.clear();
×
644
    clearPanel(false);
×
645
    ui->actionEdit->setEnabled(false);
×
646
    ui->actionDelete->setEnabled(true);
×
647
  }
648
}
2 ✔
649

650
/**
651
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
652
 * TreeViewItem, open the edit Window
653
 * @param index
654
 */
655
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
1 ✔
656
  QFileInfo fileOrFolder =
657
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
1 ✔
658

659
  if (fileOrFolder.isFile()) {
1 ✔
660
    editPassword(getFile(index, true));
2 ✔
661
  }
662
}
1 ✔
663

664
/**
665
 * @brief MainWindow::deselect clear the selection, password and copy buffer
666
 */
667
void MainWindow::deselect() {
4 ✔
668
  m_shownFile.clear();
4 ✔
669
  cancelOtpRequest();
4 ✔
670
  m_qtPass->clipboard().clear();
4 ✔
671
  ui->treeView->clearSelection();
4 ✔
672
  ui->actionEdit->setEnabled(false);
4 ✔
673
  ui->actionDelete->setEnabled(false);
4 ✔
674
  ui->passwordName->setText("");
4 ✔
675
  clearPanel(false);
4 ✔
676
}
4 ✔
677

678
void MainWindow::executeWrapperStarted() {
5 ✔
679
  m_displayPanel->clear();
5 ✔
680
  ui->textBrowser->clear();
5 ✔
681
  setUiElementsEnabled(false);
5 ✔
682
  clearPanelTimer.stop();
5 ✔
683
  if (QtPassSettings::isShowProcessOutput()) {
5 ✔
684
    m_processOutputDock->setVisible(true);
×
685
  }
686
}
5 ✔
687

688
/**
689
 * @brief Handles displaying parsed password entry content in the main window.
690
 * @example
691
 * void result = MainWindow::passShowHandler(p_output);
692
 * // Updates the UI with parsed fields and emits
693
 * passShowHandlerFinished(output)
694
 *
695
 * @param p_output - The raw output text containing the password entry data.
696
 * @return void - This function does not return a value.
697
 */
698
void MainWindow::passShowHandler(const QString &p_output) {
4 ✔
699
  const AppSettings s = QtPassSettings::load();
4 ✔
700
  QStringList templ =
701
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
4 ✔
702
  bool allFields = s.useTemplate && s.templateAllFields;
4 ✔
703
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
4 ✔
704
  QString output = p_output;
705
  // Display variant: empty when the password line is itself an otpauth URI, so
706
  // the shared secret is neither rendered nor copied to the clipboard.
707
  QString password = fileContent.getPasswordForDisplay();
4 ✔
708

709
  // set clipped text
710
  //
711
  // Skipped for an OTP request: the user asked for a one-time code, not the
712
  // password, and writing both in one event-loop turn can leave the Windows
713
  // clipboard empty (two OleSetClipboard calls back to back).
714
  if (!m_otpRequestPending) {
4 ✔
715
    m_qtPass->clipboard().copyIfAlways(password, p_output);
4 ✔
716
  }
717

718
  // first clear the current view:
719
  m_displayPanel->clear();
4 ✔
720

721
  // show what is needed:
722
  if (s.hideContent) {
4 ✔
723
    output = "***" + tr("Content hidden") + "***";
×
724
  } else if (!s.displayAsIs) {
4 ✔
725
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
8 ✔
726
                                  s.useOtp ? fileContent.getOtpUri()
4 ✔
727
                                           : QString());
728
    output = fileContent.getRemainingDataForDisplay();
8 ✔
729
  }
730

731
  if (s.useAutoclearPanel) {
4 ✔
732
    clearPanelTimer.start();
×
733
  }
734

735
  emit passShowHandlerFinished(output);
4 ✔
736
  setUiElementsEnabled(true);
4 ✔
737
}
8 ✔
738

739
/**
740
 * @brief Generates a one-time password from a decrypted entry and copies it.
741
 *
742
 * Connected as a one-shot to Pass::finishedShow by onOtp(). passShowHandler is
743
 * connected first, so by the time this runs the panel has already been
744
 * repainted and the UI re-enabled.
745
 *
746
 * @param p_output - The decrypted entry content.
747
 * @return void - This function does not return a value.
748
 */
749
void MainWindow::otpFromFileToClipboard(const QString &p_output) {
×
750
  // A failed decrypt never fires finishedShow, and Qt::SingleShotConnection
751
  // only self-disconnects when it does fire, so a connection armed by an
752
  // earlier failed request can still be live here. Ignore it rather than
753
  // hijacking an unrelated entry's decrypted content.
754
  if (!m_otpRequestPending) {
×
755
    return;
×
756
  }
757
  // finishedShow carries no request identity, so make sure this decrypt is the
758
  // one we asked for and not a tree click that happened to land first.
759
  if (m_otpRequestFile != getFile(ui->treeView->currentIndex(), true)) {
×
760
    cancelOtpRequest();
×
761
    setUiElementsEnabled(true);
×
762
    return;
×
763
  }
764
  m_otpRequestPending = false;
×
765
  m_otpRequestFile.clear();
×
766

767
  if (p_output.isEmpty()) {
×
768
    // Distinguish "could not read the entry" from "entry has no OTP".
769
    flashText(tr("Could not decrypt this password entry"), true);
×
770
    setUiElementsEnabled(true);
×
771
    return;
×
772
  }
773

774
  // passShowHandler is connected first, so it has already repainted the panel
775
  // for this same finishedShow. When it rendered the OTP row the current code
776
  // is derived and cached, so reuse it instead of re-loading settings and
777
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
778
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
779
  const QString shown = m_displayPanel->currentOtpCode();
×
780
  if (!shown.isEmpty()) {
×
781
    m_qtPass->clipboard().copyText(shown);
×
782
    showStatusMessage(tr("OTP code copied to clipboard"));
×
783
    setUiElementsEnabled(true);
×
784
    return;
785
  }
786

787
  const AppSettings s = QtPassSettings::load();
×
788
  // Parse with the same template settings passShowHandler uses, so an OTP
789
  // field is recognised identically in both paths.
790
  const QStringList templ =
791
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
×
792
  const bool allFields = s.useTemplate && s.templateAllFields;
×
793
  const FileContent fileContent =
794
      FileContent::parse(p_output, templ, allFields);
×
795

796
  const std::optional<Totp::Settings> settings =
797
      Totp::parse(fileContent.getOtpUri());
×
798
  if (settings.has_value()) {
×
799
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
×
800
    showStatusMessage(tr("OTP code copied to clipboard"));
×
801
  } else {
802
    flashText(tr("No OTP code found in this password entry"), true);
×
803
  }
804
  setUiElementsEnabled(true);
×
805
}
×
806

807
/**
808
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
809
 */
810
void MainWindow::clearPanel(bool notify) {
4 ✔
811
  m_displayPanel->clear();
4 ✔
812
  const bool grepWasVisible = ui->grepResultsList->isVisible();
4 ✔
813
  ui->grepResultsList->clear();
4 ✔
814
  if (grepWasVisible) {
4 ✔
815
    ui->grepResultsList->setVisible(false);
×
816
    ui->treeView->setVisible(true);
×
817
    if (m_grep.inGrepMode()) {
×
818
      m_grep.clearGrepMode();
819
      ui->grepButton->blockSignals(true);
×
820
      ui->grepButton->setChecked(false);
×
821
      ui->grepButton->blockSignals(false);
×
822
      ui->lineEdit->blockSignals(true);
×
823
      ui->lineEdit->clear();
×
824
      ui->lineEdit->blockSignals(false);
×
825
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
826
    }
827
  }
828
  if (notify) {
4 ✔
829
    QString output = "***" + tr("Password and Content hidden") + "***";
×
830
    ui->textBrowser->setHtml(output);
×
831
  } else {
832
    ui->textBrowser->setHtml("");
8 ✔
833
  }
834
}
4 ✔
835

836
/**
837
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
838
 * elements
839
 * @param state
840
 */
841
void MainWindow::setUiElementsEnabled(bool state) {
55 ✔
842
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
843
  // the user's escape hatch and endReencryptPath() releases the interface, so
844
  // neither an unrelated completion nor the watchdog may re-enable it early.
845
  if (state && m_reencryptRunning) {
55 ✔
846
    m_uiWatchdog.stop();
1 ✔
847
    return;
1 ✔
848
  }
849
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
850
  if (state) {
851
    m_uiWatchdog.stop();
43 ✔
852
  } else {
853
    m_uiWatchdog.start();
11 ✔
854
  }
855
  ui->treeView->setEnabled(state);
54 ✔
856
  ui->lineEdit->setEnabled(state);
54 ✔
857
  ui->actionAddPassword->setEnabled(state);
54 ✔
858
  ui->actionAddFolder->setEnabled(state);
54 ✔
859
  ui->actionUsers->setEnabled(state);
54 ✔
860
  ui->actionConfig->setEnabled(state);
54 ✔
861
  // is a file selected?
862
  state &= ui->treeView->currentIndex().isValid();
108 ✔
863
  ui->actionDelete->setEnabled(state);
54 ✔
864
  ui->actionEdit->setEnabled(state);
54 ✔
865
  updateGitButtonVisibility();
54 ✔
866
  // `state` is now "UI enabled AND a file is selected", which is exactly when
867
  // generating an OTP makes sense.
868
  updateOtpButtonVisibility(state);
54 ✔
869
}
870

871
/**
872
 * @brief Restores the main window geometry, state, position, size, and
873
 * tray/icon settings from saved application settings.
874
 * @example
875
 * MainWindow window;
876
 * window.restoreWindow();
877
 *
878
 * @return void - This function does not return a value.
879
 */
880
void MainWindow::restoreWindow() {
40 ✔
881
  // saveGeometry() covers position, size, maximized state and screen; the
882
  // separate pos/size/maximized keys that used to be applied on top of it
883
  // are what discarded the restored position, and the unconditional
884
  // re-centre in main() then discarded the rest. Centre only when nothing
885
  // was saved.
886
  const QByteArray geometry = QtPassSettings::getGeometry();
80 ✔
887
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
40 ✔
888
    WindowStateStore::centreOnCursorScreen(*this);
4 ✔
889
  }
890
  restoreState(QtPassSettings::getSavestate(saveState()));
40 ✔
891
  const AppSettings s = QtPassSettings::load();
40 ✔
892

893
  applyWindowFlagsSettings();
40 ✔
894

895
  if (s.useTrayIcon && m_tray == nullptr) {
40 ✔
896
    initTrayIcon();
×
897
    if (s.startMinimized) {
×
898
      // since we are still in constructor, can't directly hide
899
      QTimer::singleShot(10, this, &MainWindow::hide);
×
900
    }
901
  } else if (!s.useTrayIcon && m_tray != nullptr) {
40 ✔
902
    destroyTrayIcon();
×
903
  }
904
}
80 ✔
905

906
/**
907
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
908
 */
909
void MainWindow::onConfig() { config(); }
×
910

911
/**
912
 * @brief Executes when the string in the search box changes, collapses the
913
 * TreeView
914
 * @param arg1
915
 */
916
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
917
  if (m_grep.inGrepMode())
×
918
    return;
919
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
920
  ui->treeView->expandAll();
×
921
  clearPanel(false);
×
922
  ui->passwordName->setText("");
×
923
  ui->actionEdit->setEnabled(false);
×
924
  ui->actionDelete->setEnabled(false);
×
925
  searchTimer.start();
×
926
}
927

928
/**
929
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
930
 * time from two keypresses is elapsed
931
 */
932
void MainWindow::onTimeoutSearch() {
×
933
  QString query = ui->lineEdit->text();
×
934

935
  if (query.isEmpty()) {
×
936
    ui->treeView->collapseAll();
×
937
    deselect();
×
938
  }
939

940
  query.replace(QStringLiteral(" "), ".*");
×
941
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
942
  if (!regExp.isValid())
×
943
    return;
944
  proxyModel.setFilterRegularExpression(regExp);
×
945
  ui->treeView->setRootIndex(
×
946
      proxyModel.rootIndexFor(QtPassSettings::getPassStore()));
×
947

948
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
949
    selectFirstFile();
×
950
  } else {
951
    ui->actionEdit->setEnabled(false);
×
952
    ui->actionDelete->setEnabled(false);
×
953
  }
954
}
×
955

956
/**
957
 * @brief MainWindow::on_lineEdit_returnPressed get searching
958
 *
959
 * Select the first possible file in the tree
960
 */
961
void MainWindow::on_lineEdit_returnPressed() {
×
NEW
962
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed" << proxyModel.rowCount();
×
963

964
  if (m_grep.inGrepMode()) {
×
965
    const QString query = ui->lineEdit->text();
×
966
    if (!query.isEmpty()) {
×
967
      ui->grepResultsList->clear();
×
968
      ui->statusBar->showMessage(tr("Searching…"));
×
969
      if (m_grep.beginSearch()) {
970
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
971
      }
972
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
973
    } else {
974
      if (m_grep.cancelSearch()) {
975
        QApplication::restoreOverrideCursor();
×
976
      }
977
      ui->grepResultsList->clear();
×
978
      ui->grepResultsList->setVisible(false);
×
979
      ui->treeView->setVisible(true);
×
980
    }
981
    return;
982
  }
983

984
  if (proxyModel.rowCount() > 0) {
×
985
    selectFirstFile();
×
986
    on_treeView_clicked(ui->treeView->currentIndex());
×
987
  }
988
}
989

990
/**
991
 * @brief Toggle grep (content search) mode.
992
 */
993
void MainWindow::on_grepButton_toggled(bool checked) {
×
994
  const AppSettings s = QtPassSettings::load();
×
995
  if (checked) {
×
996
    m_grep.enterGrepMode();
997
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
998
    // The regex dialect depends on the backend (see Pass::Grep): the pass
999
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
1000
    ui->lineEdit->setToolTip(
×
1001
        s.usePass
×
1002
            ? tr("Content search uses POSIX basic regular expressions "
×
1003
                 "(pass grep).")
1004
            : tr("Content search uses Perl-compatible regular expressions "
1005
                 "(PCRE)."));
1006
    ui->lineEdit->clear();
×
1007
    searchTimer.stop();
×
1008
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
1009
    ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
1010
    ui->grepResultsList->setVisible(false);
×
1011
    // Keep treeView visible until results arrive
1012
  } else {
1013
    if (m_grep.leaveGrepMode()) {
1014
      QApplication::restoreOverrideCursor();
×
1015
    }
1016
    searchTimer.stop();
×
1017
    ui->lineEdit->blockSignals(true);
×
1018
    ui->lineEdit->clear();
×
1019
    ui->lineEdit->blockSignals(false);
×
1020
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
1021
    ui->lineEdit->setToolTip(QString());
×
1022
    ui->grepResultsList->clear();
×
1023
    ui->grepResultsList->setVisible(false);
×
1024
    ui->treeView->setVisible(true);
×
1025
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
1026
    ui->treeView->setRootIndex(proxyModel.rootIndexFor(s.passStore));
×
1027
  }
1028
}
×
1029

1030
/**
1031
 * @brief Display grep results in grepResultsList.
1032
 */
1033
void MainWindow::onGrepFinished(
×
1034
    const QList<QPair<QString, QStringList>> &results) {
1035
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
1036
  if (outcome.restoreCursor) {
×
1037
    QApplication::restoreOverrideCursor();
×
1038
  }
1039
  // Re-enable the UI before the discard check so a cancelled search can never
1040
  // leave controls disabled.
1041
  setUiElementsEnabled(true);
×
1042
  if (outcome.discard) {
×
1043
    return;
×
1044
  }
1045
  if (!m_grep.inGrepMode())
×
1046
    return;
1047
  ui->grepResultsList->clear();
×
1048
  if (results.isEmpty()) {
×
1049
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
1050
    ui->grepResultsList->setVisible(false);
×
1051
    ui->treeView->setVisible(true);
×
1052
    return;
×
1053
  }
1054
  const AppSettings s = QtPassSettings::load();
×
1055
  const bool hideContent = s.hideContent;
×
1056
  int totalLines = 0;
1057
  for (const auto &pair : results) {
×
1058
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
1059
    entryItem->setText(0, pair.first);
×
1060
    entryItem->setData(0, Qt::UserRole, pair.first);
×
1061
    for (const QString &line : pair.second) {
×
1062
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
1063
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
1064
                                       : line);
1065
      lineItem->setData(0, Qt::UserRole, pair.first);
×
1066
      ++totalLines;
×
1067
    }
1068
  }
1069
  ui->grepResultsList->expandAll();
×
1070
  ui->treeView->setVisible(false);
×
1071
  ui->grepResultsList->setVisible(true);
×
1072
  ui->statusBar->showMessage(
×
1073
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
1074
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
1075
      3000);
1076
  if (s.useAutoclearPanel)
×
1077
    clearPanelTimer.start();
×
1078
}
×
1079

1080
/**
1081
 * @brief Navigate to the password entry when a grep result is clicked.
1082
 */
1083
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
1084
                                                int /*column*/) {
1085
  const AppSettings s = QtPassSettings::load();
×
1086
  const QString entry = item->data(0, Qt::UserRole).toString();
×
1087
  if (entry.isEmpty())
×
1088
    return;
1089
  const QString fullPath =
1090
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
×
1091
  QModelIndex srcIndex = model.index(fullPath);
×
1092
  if (!srcIndex.isValid())
1093
    return;
1094
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
1095
  if (!proxyIndex.isValid())
1096
    return;
1097
  ui->treeView->setCurrentIndex(proxyIndex);
×
1098
  on_treeView_clicked(proxyIndex);
×
1099
  if (s.hideContent || s.useAutoclearPanel)
×
1100
    ui->grepResultsList->clear();
×
1101
  ui->grepResultsList->setVisible(false);
×
1102
  ui->treeView->setVisible(true);
×
1103
  ui->treeView->scrollTo(proxyIndex);
×
1104
  ui->treeView->setFocus();
×
1105
}
×
1106

1107
/**
1108
 * @brief MainWindow::selectFirstFile select the first possible file in the
1109
 * tree
1110
 */
1111
void MainWindow::selectFirstFile() {
×
1112
  QModelIndex index = proxyModel.rootIndexFor(QtPassSettings::getPassStore());
×
1113
  index = firstFile(index);
×
1114
  ui->treeView->setCurrentIndex(index);
×
1115
}
×
1116

1117
/**
1118
 * @brief MainWindow::firstFile return location of first possible file
1119
 * @param parentIndex
1120
 * @return QModelIndex
1121
 */
1122
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
1123
  int numRows = proxyModel.rowCount(parentIndex);
×
1124
  for (int row = 0; row < numRows; ++row) {
×
1125
    QModelIndex index = proxyModel.index(row, 0, parentIndex);
×
1126
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
1127
      return index;
×
1128
    }
1129
    if (proxyModel.hasChildren(index)) {
×
1130
      QModelIndex childFile = firstFile(index);
×
1131
      if (childFile.isValid())
1132
        return childFile;
×
1133
    }
1134
  }
1135
  return QModelIndex();
1136
}
1137

1138
/**
1139
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1140
 * the password store and warn the user.
1141
 *
1142
 * Used before file/folder creation, move, and rename to stop user-typed
1143
 * names like "../../etc/passwd" or absolute paths from escaping the
1144
 * configured store root via the input dialogs.
1145
 *
1146
 * @param candidate Absolute candidate path to validate.
1147
 * @return true if the path is inside the password store; false otherwise (a
1148
 * warning dialog is shown in that case).
1149
 */
1150
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1151
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1152
    return true;
1153
  }
1154
  QMessageBox::warning(this, tr("Invalid name"),
×
1155
                       tr("That name would resolve outside the password "
×
1156
                          "store. Please choose a different name."));
1157
  return false;
×
1158
}
1159

1160
/**
1161
 * @brief MainWindow::setPassword open passworddialog
1162
 * @param file which pgp file
1163
 * @param isNew insert (not update)
1164
 */
1165
void MainWindow::setPassword(const QString &file, bool isNew) {
1 ✔
1166
  const AppSettings s = QtPassSettings::load();
1 ✔
1167
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
1 ✔
1168

1169
  if (isNew) {
1 ✔
1170
    const QString storePath = s.passStore;
1171
    QString folder = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1172
                                  proxyModel, s.passStore);
×
1173
    if (folder.isEmpty()) {
×
1174
      folder = storePath;
×
1175
    }
1176
    QHash<QString, QStringList> templates =
1177
        TemplateIO::readTemplates(storePath);
×
1178
    if (!templates.isEmpty()) {
1179
      QString defaultTemplate =
1180
          TemplateIO::getFolderTemplate(folder, storePath);
×
1181
      d.setAvailableTemplates(templates, defaultTemplate);
×
1182
    }
1183
  }
×
1184

1185
  if (!d.exec()) {
1 ✔
1186
    ui->treeView->setFocus();
×
1187
  }
1188
}
1 ✔
1189

1190
/**
1191
 * @brief MainWindow::addPassword add a new password by showing a
1192
 * number of dialogs.
1193
 */
1194
void MainWindow::addPassword() {
×
1195
  const QString passStore = QtPassSettings::load().passStore;
×
1196
  bool ok;
1197
  QString dir = Util::getDir(ui->treeView->currentIndex(), true, model,
×
1198
                             proxyModel, passStore);
×
1199
  QString file = QInputDialog::getText(
1200
      this, tr("New file"),
×
1201
      tr("New password file: \n(Will be placed in %1 )")
×
1202
          .arg(passStore + Util::getDir(ui->treeView->currentIndex(), true,
×
1203
                                        model, proxyModel, passStore)),
1204
      QLineEdit::Normal, "", &ok);
×
1205
  if (!ok || file.isEmpty()) {
×
1206
    return;
1207
  }
1208
  file = dir + file;
×
1209
  if (!confirmPathInStore(passStore + file)) {
×
1210
    return;
1211
  }
1212
  setPassword(file);
×
1213
}
1214

1215
/**
1216
 * @brief MainWindow::onDelete remove password, if you are
1217
 * sure.
1218
 */
1219
void MainWindow::onDelete() {
×
1220
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1221
  if (!currentIndex.isValid()) {
1222
    // This fixes https://github.com/IJHack/QtPass/issues/556
1223
    // Otherwise the entire password directory would be deleted if
1224
    // nothing is selected in the tree view.
1225
    return;
×
1226
  }
1227

1228
  QFileInfo fileOrFolder =
1229
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1230
  QString file = "";
×
1231
  bool isDir = false;
1232

1233
  if (fileOrFolder.isFile()) {
×
1234
    file = getFile(ui->treeView->currentIndex(), true);
×
1235
  } else {
1236
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel,
×
1237
                        QtPassSettings::getPassStore());
×
1238
    isDir = true;
1239
  }
1240

1241
  QString dirMessage = tr(" and the whole content?");
1242
  if (isDir) {
×
1243
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1244
                    QDirIterator::Subdirectories);
×
1245
    bool okDir = true;
1246
    while (it.hasNext() && okDir) {
×
1247
      it.next();
×
1248
      if (QFileInfo(it.filePath()).isFile()) {
×
1249
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1250
          okDir = false;
1251
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1252
                          "there are unexpected files in the given folder, "
1253
                          "check them before continue.</strong>");
1254
        }
1255
      }
1256
    }
1257
  }
×
1258

1259
  if (QMessageBox::question(
×
1260
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1261
          tr("Are you sure you want to delete %1%2?")
×
1262
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1263
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1264
    return;
1265
  }
1266

1267
  QtPassSettings::getPass()->Remove(file, isDir);
×
1268
}
×
1269

1270
/**
1271
 * @brief MainWindow::cancelOtpRequest abandon an in-flight OTP request.
1272
 *
1273
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1274
 * decrypt never emits finishedShow, and the error handler re-enables the UI —
1275
 * which stops the watchdog that was the only other thing clearing the flag.
1276
 * Left set, it permanently suppressed passShowHandler's copy-on-select and let
1277
 * the still-armed one-shot claim the next unrelated decrypt.
1278
 *
1279
 * The same failed decrypt would otherwise leave m_passwordCopyPending stuck
1280
 * (its only other reset is passwordFromFileToClipboard, which never runs when
1281
 * finishedShow is not emitted), permanently blocking Ctrl+C, so clear it here
1282
 * too. Unlike otpFromFileToClipboard, that slot has no pending-flag guard, so
1283
 * its single-shot connection must be torn down here as well — otherwise a
1284
 * connection left armed by the failed decrypt would claim the next unrelated
1285
 * finishedShow and copy the wrong entry to the clipboard. A never-fired
1286
 * Qt::SingleShotConnection persists until it emits, so disconnect here.
1287
 */
1288
void MainWindow::cancelOtpRequest() {
4 ✔
1289
  m_otpRequestPending = false;
4 ✔
1290
  m_otpRequestFile.clear();
4 ✔
1291
  disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
4 ✔
1292
             &MainWindow::passwordFromFileToClipboard);
1293
  m_passwordCopyPending = false;
4 ✔
1294
}
4 ✔
1295

1296
/**
1297
 * @brief MainWindow::onOtp generate the selected entry's OTP code and copy it.
1298
 *
1299
 * Decrypts the entry once and derives the code in-process, so this works with
1300
 * either backend and on every platform. The code itself is produced by
1301
 * otpFromFileToClipboard once Pass::finishedShow arrives.
1302
 */
1303
void MainWindow::onOtp() {
×
1304
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1305
  if (file.isEmpty()) {
×
1306
    flashText(tr("No password selected for OTP generation"), true);
×
1307
    return;
×
1308
  }
1309
  if (!QtPassSettings::isUseOtp()) {
×
1310
    // Normally unreachable (the action is hidden), but never fail silently.
1311
    flashText(tr("No OTP code found in this password entry"), true);
×
1312
    return;
×
1313
  }
1314

1315
  // Fast path: the panel already shows a live code for the selected entry, so
1316
  // there is nothing to decrypt. This also keeps the clipboard write single —
1317
  // going through Show() would let passShowHandler copy the password first, and
1318
  // two QClipboard::setMimeData calls in one event-loop turn can leave the
1319
  // Windows clipboard empty.
1320
  //
1321
  // Only when the panel is showing *this* entry: arrow-key navigation and
1322
  // right-clicking move the tree's currentIndex without emitting
1323
  // QTreeView::clicked, so the visible code can belong to a different account.
1324
  const QString shown =
1325
      (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
×
1326
  if (!shown.isEmpty()) {
×
1327
    m_qtPass->clipboard().copyText(shown);
×
1328
    showStatusMessage(tr("OTP code copied to clipboard"));
×
1329
    return;
1330
  }
1331

1332
  // Fallback: no OTP row on screen, so decrypt once. The flag stops
1333
  // passShowHandler putting the password on the clipboard for a request that
1334
  // only asked for a code.
1335
  m_otpRequestPending = true;
×
1336
  m_otpRequestFile = file;
×
1337
  setUiElementsEnabled(false);
×
1338
  connectSingleShot(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1339
                    &MainWindow::otpFromFileToClipboard);
1340
  // passShowHandler repaints the panel for this Show too, so keep the marker in
1341
  // step or a second request would decrypt again instead of taking the fast
1342
  // path. Safe to set now: executeWrapperStarted() clears the panel on every
1343
  // command, so a failed decrypt leaves currentOtpCode() empty.
1344
  m_shownFile = file;
×
1345
  QtPassSettings::getPass()->Show(file);
×
1346
}
1347

1348
/**
1349
 * @brief MainWindow::onEdit try and edit (selected) password.
1350
 */
1351
void MainWindow::onEdit() {
×
1352
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1353
  editPassword(file);
×
1354
}
×
1355

1356
/**
1357
 * @brief MainWindow::onUsers edit users for the current
1358
 * folder,
1359
 * gets lists and opens UserDialog.
1360
 */
1361
void MainWindow::onUsers() {
×
1362
  const QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1363
                                   proxyModel, QtPassSettings::getPassStore());
×
1364

1365
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1366
  if (!d.exec()) {
×
1367
    ui->treeView->setFocus();
×
1368
  }
1369
}
×
1370

1371
/**
1372
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1373
 * @param message
1374
 */
1375
void MainWindow::messageAvailable(const QString &message) {
×
1376
  show();
×
1377
  raise();
×
1378
  if (message.isEmpty()) {
×
1379
    focusInput();
×
1380
  } else {
1381
    ui->treeView->expandAll();
×
1382
    ui->lineEdit->setText(message);
×
1383
    on_lineEdit_returnPressed();
×
1384
  }
1385
}
×
1386

1387
/**
1388
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1389
 * @param batch
1390
 * @param keygenWindow
1391
 */
1392
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1393
  m_keyGenDialog = keygenWindow;
1394
  emit generateGPGKeyPair(batch);
×
1395
}
×
1396

1397
/**
1398
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1399
 * select a more appropriate one to view too
1400
 */
1401
void MainWindow::updateProfileBox() {
36 ✔
1402
  QHash<QString, QHash<QString, QString>> profiles =
1403
      QtPassSettings::getProfiles();
36 ✔
1404

1405
  if (profiles.isEmpty()) {
1406
    ui->profileWidget->hide();
36 ✔
1407
  } else {
1408
    ui->profileWidget->show();
×
1409
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1410
    ui->profileBox->clear();
×
1411
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
×
1412
    while (i.hasNext()) {
×
1413
      i.next();
1414
      if (!i.key().isEmpty()) {
×
1415
        ui->profileBox->addItem(i.key());
×
1416
      }
1417
    }
1418
    ui->profileBox->model()->sort(0);
×
1419
  }
1420
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
72 ✔
1421
  if (index != -1) { //  -1 for not found
36 ✔
1422
    ui->profileBox->setCurrentIndex(index);
×
1423
  }
1424
}
36 ✔
1425

1426
/**
1427
 * @brief Handles changes to the selected profile in the profile combo box.
1428
 * @details Ignores the event during a fresh start or when the selected profile
1429
 * matches the current profile. Otherwise, it clears the password field, updates
1430
 * the active profile and related settings, refreshes the environment, and
1431
 * resets the tree view and action states to reflect the newly selected profile.
1432
 *
1433
 * @param name - The newly selected profile name.
1434
 * @return void - This function does not return a value.
1435
 *
1436
 */
1437
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1438
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
1439
    return;
×
1440
  }
1441

1442
  ui->lineEdit->clear();
×
1443

1444
  const QHash<QString, QString> prof =
1445
      QtPassSettings::getProfiles().value(name);
×
1446
  AppSettings s = QtPassSettings::load();
×
1447
  s.activeProfile = name;
×
1448
  s.passStore = prof.value("path");
×
1449
  s.passSigningKey = prof.value("signingKey");
×
1450
  QtPassSettings::save(s);
×
1451
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1452

1453
  QtPassSettings::getPass()->updateEnv();
×
1454

1455
  const QString passStore = QtPassSettings::getPassStore();
×
1456
  proxyModel.setStore(passStore);
×
1457
  ui->treeView->setRootIndex(proxyModel.rootIndexFor(passStore));
×
1458
  deselect();
×
1459
  ui->treeView->setCurrentIndex(QModelIndex());
×
1460
}
×
1461

1462
/**
1463
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1464
 * support
1465
 * it
1466
 */
1467
void MainWindow::initTrayIcon() {
×
1468
  m_tray = new TrayIcon(this);
×
1469
  if (!m_tray->getIsAllocated()) {
×
1470
    destroyTrayIcon();
×
1471
  }
1472
}
×
1473

1474
/**
1475
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1476
 */
1477
void MainWindow::destroyTrayIcon() {
×
1478
  delete m_tray;
×
1479
  m_tray = nullptr;
×
1480
}
×
1481

1482
/**
1483
 * @brief MainWindow::closeEvent hide or quit
1484
 * @param event
1485
 */
1486
void MainWindow::closeEvent(QCloseEvent *event) {
1 ✔
1487
  // Save in both branches: a tray user only ever hides the window, and
1488
  // used to quit through the tray menu with nothing ever written.
1489
  saveWindowState();
1 ✔
1490
  if (QtPassSettings::isHideOnClose()) {
1 ✔
1491
    this->hide();
1 ✔
1492
    event->ignore();
1493
  } else {
1494
    m_qtPass->clipboard().clear();
×
1495
    event->accept();
1496
    // A visible QSystemTrayIcon keeps the application alive after the last
1497
    // window closes, so quitOnLastWindowClosed never fires and the window
1498
    // merely vanishes into the tray. Quit explicitly so closing the window
1499
    // actually exits when "hide on close" is disabled.
1500
    QApplication::quit();
×
1501
  }
1502
}
1 ✔
1503

1504
/**
1505
 * @brief MainWindow::eventFilter filter out some events and focus the
1506
 * treeview
1507
 * @param obj
1508
 * @param event
1509
 * @return
1510
 */
1511
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
1,057 ✔
1512
  if (obj == ui->toolBar && event->type() == QEvent::PaletteChange) {
1,057 ✔
1513
    // KDE's Breeze style gives top toolbars a "header" palette of its own.
1514
    // After a runtime light/dark switch it recomputes that palette from a
1515
    // cached kdeglobals and stamps the previous theme's colours back on, so
1516
    // the toolbar stays dark on a light window (or vice versa) with matching
1517
    // invisible icons. A legitimate header tint is close to the window
1518
    // colour; a stale theme is not. When the two disagree, drop the imposed
1519
    // palette so the toolbar follows the application palette again. Deferred
1520
    // so we never re-enter the style while it is still applying its palette.
1521
    QTimer::singleShot(0, this, &MainWindow::dropStaleToolBarPalette);
6 ✔
1522
  }
1523
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
1,057 ✔
1524
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1525
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1526
      ui->treeView->setFocus();
×
1527
    }
1528
  }
1529
  return QObject::eventFilter(obj, event);
1,057 ✔
1530
}
1531

1532
/**
1533
 * @brief Reset the toolbar palette when a style left it in the wrong theme.
1534
 *
1535
 * Compares the lightness of the toolbar's Window colour with the application
1536
 * palette; a difference above kStaleToolBarLightness means the toolbar shows
1537
 * the other theme. See the PaletteChange branch in eventFilter().
1538
 */
1539
void MainWindow::dropStaleToolBarPalette() {
3 ✔
1540
  if (!ui->toolBar->testAttribute(Qt::WA_SetPalette)) {
3 ✔
1541
    return;
1542
  }
1543
  constexpr int kStaleToolBarLightness = 64;
1544
  const int barLightness =
1545
      ui->toolBar->palette().color(QPalette::Window).lightness();
4 ✔
1546
  const int appLightness =
1547
      QApplication::palette().color(QPalette::Window).lightness();
4 ✔
1548
  if (qAbs(barLightness - appLightness) > kStaleToolBarLightness) {
2 ✔
1549
    ui->toolBar->setPalette(QPalette());
1 ✔
1550
    // The style also paints the tools-area background on the main window
1551
    // from the same stale palette, and the toolbar is transparent by default;
1552
    // paint our own background so the reset is actually visible.
1553
    ui->toolBar->setAutoFillBackground(true);
1 ✔
1554
  }
1555
}
1556

1557
/**
1558
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1559
 * @param event
1560
 */
1561
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1562
  switch (event->key()) {
×
1563
  case Qt::Key_Delete:
×
1564
    onDelete();
×
1565
    break;
×
1566
  case Qt::Key_Return:
×
1567
  case Qt::Key_Enter:
1568
    if (proxyModel.rowCount() > 0) {
×
1569
      on_treeView_clicked(ui->treeView->currentIndex());
×
1570
    }
1571
    break;
1572
  case Qt::Key_Escape:
×
1573
    ui->lineEdit->clear();
×
1574
    break;
×
1575
  default:
1576
    break;
1577
  }
1578
}
×
1579

1580
/**
1581
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1582
 * menu
1583
 * @param pos
1584
 */
1585
void MainWindow::showContextMenu(const QPoint &pos) {
×
1586
  const AppSettings s = QtPassSettings::load();
×
1587
  QModelIndex index = ui->treeView->indexAt(pos);
×
1588
  bool selected = true;
1589
  if (!index.isValid()) {
1590
    ui->treeView->clearSelection();
×
1591
    ui->actionDelete->setEnabled(false);
×
1592
    ui->actionEdit->setEnabled(false);
×
1593
    selected = false;
1594
  }
1595

1596
  ui->treeView->setCurrentIndex(index);
×
1597

1598
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1599

1600
  QFileInfo fileOrFolder =
1601
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1602

1603
  QMenu contextMenu;
×
1604
  if (!selected || fileOrFolder.isDir()) {
×
1605
    QAction *openFolder =
1606
        contextMenu.addAction(tr("Open folder with file manager"));
×
1607
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1608
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1609
    QAction *users = contextMenu.addAction(tr("Users"));
×
1610
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1611
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1612
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1613
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1614
  } else if (fileOrFolder.isFile()) {
×
1615
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1616
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1617
  }
1618
  if (selected) {
×
1619
    contextMenu.addSeparator();
×
1620
    if (fileOrFolder.isDir()) {
×
1621
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1622
      connect(renameFolder, &QAction::triggered, this,
×
1623
              &MainWindow::renameFolder);
×
1624
    } else if (fileOrFolder.isFile()) {
×
1625
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1626
      connect(renamePassword, &QAction::triggered, this,
×
1627
              &MainWindow::renamePassword);
×
1628
    }
1629
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1630
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1631
    if (fileOrFolder.isDir()) {
×
1632
      QString dirPath = QDir::cleanPath(Util::getDir(
×
1633
          ui->treeView->currentIndex(), false, model, proxyModel, s.passStore));
×
1634

1635
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1636
      contextMenu.addMenu(shareMenu);
×
1637

1638
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1639
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1640

1641
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1642
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1643
                                                 QFile(exePath).exists());
×
1644

1645
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1646
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1647
      connect(reencrypt, &QAction::triggered, this,
×
1648
              [this, dirPath]() { reencryptPath(dirPath); });
×
1649

1650
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1651
      exportKey->setEnabled(gpgAvailable);
×
1652
      connect(exportKey, &QAction::triggered, this,
×
1653
              &MainWindow::exportPublicKey);
×
1654

1655
      QAction *addRecipientAction =
1656
          shareMenu->addAction(tr("Add recipient..."));
×
1657
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1658
      connect(addRecipientAction, &QAction::triggered, this,
×
1659
              [this, dirPath]() { addRecipient(dirPath); });
×
1660

1661
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1662
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1663
    }
1664
  }
1665
  contextMenu.exec(globalPos);
×
1666
}
×
1667

1668
/**
1669
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1670
 * password window
1671
 * @param pos
1672
 */
1673
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1674
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1675
  // createStandardContextMenu() parents the menu to textBrowser. A stylesheet
1676
  // on the browser (it used to carry "background: palette(base)") cascades to
1677
  // the child QMenu and breaks its opaque native background. Reparent to the
1678
  // main window so the menu paints solid regardless of browser styling.
1679
  contextMenu->setParent(this, contextMenu->windowFlags());
×
1680
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1681

1682
  contextMenu->exec(globalPos);
×
1683
  delete contextMenu;
×
1684
}
×
1685

1686
/**
1687
 * @brief MainWindow::openFolder open the folder in the default file manager
1688
 */
1689
void MainWindow::openFolder() {
×
1690
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1691
                             proxyModel, QtPassSettings::getPassStore());
×
1692

1693
  QString path = QDir::toNativeSeparators(dir);
×
1694
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1695
}
×
1696

1697
/**
1698
 * @brief MainWindow::addFolder add a new folder to store passwords in
1699
 */
1700
void MainWindow::addFolder() {
×
1701
  const AppSettings s = QtPassSettings::load();
×
1702
  bool ok;
1703
  QString dir = Util::getDir(ui->treeView->currentIndex(), false, model,
×
1704
                             proxyModel, s.passStore);
×
1705
  QString newdir = QInputDialog::getText(
1706
      this, tr("New folder"),
×
1707
      tr("New Folder: \n(Will be placed in %1 )")
×
1708
          .arg(s.passStore + Util::getDir(ui->treeView->currentIndex(), true,
×
1709
                                          model, proxyModel, s.passStore)),
1710
      QLineEdit::Normal, "", &ok);
×
1711
  if (!ok || newdir.isEmpty()) {
×
1712
    return;
1713
  }
1714
  newdir.prepend(dir);
1715
  if (!confirmPathInStore(newdir)) {
×
1716
    return;
1717
  }
1718
  if (!QDir().mkdir(newdir)) {
×
1719
    QMessageBox::warning(this, tr("Error"),
×
1720
                         tr("Failed to create folder: %1").arg(newdir));
×
1721
    return;
×
1722
  }
1723
  // A .gpg-id only counts when it is signed once a signing key is configured:
1724
  // ImitatePass::verifyGpgIdFile and pass (PASSWORD_STORE_SIGNING_KEY) both
1725
  // refuse to encrypt into a folder whose .gpg-id has no matching
1726
  // .gpg-id.sig. Signing here would need the signing secret key and a
1727
  // passphrase prompt just to create a folder, so in that case leave the
1728
  // folder without its own .gpg-id: it inherits the parent's signed list,
1729
  // which encrypts to the same recipients. UsersDialog (Init) is the place to
1730
  // give it a distinct, signed list.
1731
  const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
×
1732
  if (s.addGPGId && !signingConfigured) {
×
1733
    // Seed the new folder's .gpg-id from the recipients that are currently
1734
    // in effect for its parent. The previous implementation walked
1735
    // listKeys("", true) and wrote every key whose `enabled` flag was set,
1736
    // but that flag is only toggled inside UsersDialog, so the loop wrote
1737
    // nothing and left a zero-byte .gpg-id that shadowed the parent's
1738
    // recipients (see #1682).
1739
    if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
×
1740
      QMessageBox::warning(
×
1741
          this, tr("Error"),
×
1742
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1743
      return;
×
1744
    }
1745
  }
1746
}
×
1747

1748
/**
1749
 * @brief MainWindow::renameFolder rename an existing folder
1750
 */
1751
void MainWindow::renameFolder() {
×
1752
  bool ok;
1753
  QString srcDir =
1754
      QDir::cleanPath(Util::getDir(ui->treeView->currentIndex(), false, model,
×
1755
                                   proxyModel, QtPassSettings::getPassStore()));
×
1756
  QString srcDirName = QDir(srcDir).dirName();
×
1757
  QString newName =
1758
      QInputDialog::getText(this, tr("Rename folder"), tr("Rename Folder To: "),
×
1759
                            QLineEdit::Normal, srcDirName, &ok);
×
1760
  if (!ok || newName.isEmpty()) {
×
1761
    return;
1762
  }
1763
  QString destDir = srcDir;
1764
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1765
  if (!confirmPathInStore(destDir)) {
×
1766
    return;
1767
  }
1768
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1769
}
1770

1771
/**
1772
 * @brief MainWindow::editPassword read password and open edit window via
1773
 * MainWindow::onEdit()
1774
 */
1775
void MainWindow::editPassword(const QString &file) {
1 ✔
1776
  if (!file.isEmpty()) {
1 ✔
1777
    const AppSettings s = QtPassSettings::load();
1 ✔
1778
    if (s.useGit && s.autoPull) {
1 ✔
1779
      onUpdate(true);
×
1780
    }
1781
    setPassword(file, false);
1 ✔
1782
  }
1 ✔
1783
}
1 ✔
1784

1785
/**
1786
 * @brief MainWindow::renamePassword rename an existing password
1787
 */
1788
void MainWindow::renamePassword() {
×
1789
  bool ok;
1790
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1791
  QString filePath = QFileInfo(file).path();
×
1792
  QString fileName = QFileInfo(file).fileName();
×
1793
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1794
    fileName.chop(4);
×
1795
  }
1796

1797
  QString newName =
1798
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1799
                            QLineEdit::Normal, fileName, &ok);
×
1800
  if (!ok || newName.isEmpty()) {
×
1801
    return;
1802
  }
1803
  QString newFile = QDir(filePath).filePath(newName);
×
1804
  if (!confirmPathInStore(newFile)) {
×
1805
    return;
1806
  }
1807
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1808
}
1809

1810
/**
1811
 * @brief Copies the password of the selected file from the tree view to the
1812
 * clipboard.
1813
 * @example
1814
 * MainWindow::copyPasswordFromTreeview();
1815
 *
1816
 * @return void - This function does not return a value.
1817
 */
1818
void MainWindow::copyPasswordFromTreeview() {
×
1819
  QFileInfo fileOrFolder =
1820
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1821

1822
  if (fileOrFolder.isFile()) {
×
1823
    // finishedShow carries no request identity, so allow only one copy request
1824
    // in flight: otherwise a second Ctrl+C on a different entry would re-arm
1825
    // the single-shot slot and the first decrypt to finish would copy the wrong
1826
    // entry while the later request is silently dropped. See
1827
    // m_passwordCopyPending.
1828
    if (m_passwordCopyPending) {
×
1829
      return;
×
1830
    }
1831
    m_passwordCopyPending = true;
×
1832
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1833
    connectSingleShot(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1834
                      &MainWindow::passwordFromFileToClipboard);
1835
    // This Show repaints the panel as well; see onOtp().
1836
    m_shownFile = file;
×
1837
    QtPassSettings::getPass()->Show(file);
×
1838
  }
1839
}
×
1840

1841
void MainWindow::passwordFromFileToClipboard(const QString &text) {
2 ✔
1842
  m_passwordCopyPending = false;
2 ✔
1843
  const QStringList tokens = text.split('\n');
2 ✔
1844
  if (tokens.isEmpty()) {
2 ✔
1845
    return;
1846
  }
1847
  // An entry created by `pass otp insert` has the otpauth:// URI as its first
1848
  // line. That URI carries the shared TOTP secret (the seed, not a code), so
1849
  // copying it to the clipboard would leak 2FA material. Skip it, matching the
1850
  // display path (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1851
  if (FileContent::isOtpUriValue(tokens[0])) {
2 ✔
1852
    flashText(tr("This entry holds an OTP secret, not a password"), true);
1 ✔
1853
    return;
1 ✔
1854
  }
1855
  m_qtPass->clipboard().copyText(tokens[0]);
1 ✔
1856
}
1857

1858
/**
1859
 * @brief Displays message in status bar
1860
 *
1861
 * @param msg     text to be displayed
1862
 * @param timeout time for which msg shall be visible
1863
 */
1864
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
5 ✔
1865
  ui->statusBar->showMessage(msg, timeout);
5 ✔
1866
}
5 ✔
1867

1868
/**
1869
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1870
 * @param dir Directory path to re-encrypt
1871
 */
1872
void MainWindow::reencryptPath(const QString &dir) {
×
1873
  QDir checkDir(dir);
×
1874
  if (!checkDir.exists()) {
×
1875
    QMessageBox::critical(this, tr("Error"),
×
1876
                          tr("Directory does not exist: %1").arg(dir));
×
1877
    return;
×
1878
  }
1879

1880
  int ret = QMessageBox::question(
×
1881
      this, tr("Re-encrypt passwords"),
×
1882
      tr("Re-encrypt all passwords in %1?\n\n"
×
1883
         "This will re-encrypt ALL password files in this folder "
1884
         "using the current recipients defined in .gpg-id.\n\n"
1885
         "This may rewrite many files and cannot be undone easily.\n\n"
1886
         "Continue?")
1887
          .arg(QDir(dir).dirName()),
×
1888
      QMessageBox::Yes | QMessageBox::No);
1889

1890
  if (ret != QMessageBox::Yes)
×
1891
    return;
1892

1893
  // Disable preemptively. ImitatePass::reencryptPath emits
1894
  // startReencryptPath asynchronously and the slot would re-run this,
1895
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1896
  startReencryptPath();
×
1897

1898
  QtPassSettings::getImitatePass()->reencryptPath(
×
1899
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1900
}
×
1901

1902
/**
1903
 * @brief MainWindow::startReencryptPath disable ui elements and treeview and
1904
 * show the progress dialog. Idempotent: MainWindow::reencryptPath calls it
1905
 * before ImitatePass emits startReencryptPath.
1906
 */
1907
void MainWindow::startReencryptPath() {
3 ✔
1908
  m_reencryptRunning = true;
3 ✔
1909
  setUiElementsEnabled(false);
3 ✔
1910
  // The worker always ends with endReencryptPath(), and Cancel is available
1911
  // throughout, so the generic watchdog is not needed for this operation.
1912
  m_uiWatchdog.stop();
3 ✔
1913
  ui->treeView->setDisabled(true);
3 ✔
1914
  if (m_reencryptProgress)
3 ✔
1915
    return;
1916
  auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
2 ✔
1917
                                       tr("Cancel"), 0, 0, this);
4 ✔
1918
  progress->setWindowTitle(tr("Re-encrypt passwords"));
2 ✔
1919
  progress->setWindowModality(Qt::WindowModal);
2 ✔
1920
  progress->setAutoClose(false);
2 ✔
1921
  progress->setAutoReset(false);
2 ✔
1922
  progress->setMinimumDuration(0);
2 ✔
1923
  connect(progress, &QProgressDialog::canceled, this, [this]() {
2 ✔
1924
    QtPassSettings::getImitatePass()->cancelReencryptPath();
×
1925
    showStatusMessage(tr("Cancelling re-encryption"), 5000);
×
1926
  });
×
1927
  m_reencryptProgress = progress;
1928
  progress->show();
2 ✔
1929
}
1930

1931
/**
1932
 * @brief MainWindow::reencryptProgress show how many files were checked
1933
 * @param current files checked so far
1934
 * @param total files found under the folder
1935
 */
1936
void MainWindow::reencryptProgress(int current, int total) {
4 ✔
1937
  if (!m_reencryptProgress)
4 ✔
1938
    return;
1939
  m_reencryptProgress->setMaximum(total);
3 ✔
1940
  m_reencryptProgress->setLabelText(
3 ✔
1941
      tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
6 ✔
1942
  // On a modal QProgressDialog setValue() calls processEvents(), which can
1943
  // deliver the queued completion: endReencryptPath() then hides the dialog
1944
  // and resets m_reencryptProgress under our feet. Keep it last and touch
1945
  // nothing afterwards.
1946
  m_reencryptProgress->setValue(current);
3 ✔
1947
}
1948

1949
/**
1950
 * @brief MainWindow::endReencryptPath re-enable ui elements
1951
 */
1952
void MainWindow::endReencryptPath() {
2 ✔
1953
  m_reencryptRunning = false;
2 ✔
1954
  if (m_reencryptProgress) {
2 ✔
1955
    m_reencryptProgress->hide();
2 ✔
1956
    m_reencryptProgress->deleteLater();
2 ✔
1957
    m_reencryptProgress = nullptr;
1958
  }
1959
  setUiElementsEnabled(true);
2 ✔
1960
}
2 ✔
1961

1962
/**
1963
 * @brief MainWindow::exportPublicKey export the configured signing key in
1964
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1965
 *
1966
 * Falls back to a help dialog when no signing key is configured or gpg is
1967
 * unavailable, so the user still gets actionable guidance.
1968
 */
1969
void MainWindow::exportPublicKey() {
×
1970
  const AppSettings s = QtPassSettings::load();
×
1971
  const QString identity = s.passSigningKey;
1972
  if (identity.isEmpty()) {
×
1973
    QMessageBox::information(
×
1974
        this, tr("Export Public Key"),
×
1975
        tr("<h3>Export Your Public Key</h3>"
×
1976
           "<p>No signing key is configured. Set one in QtPass Settings "
1977
           "&gt; GPG keys, or run this in a terminal:</p>"
1978
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1979
           "&gt;</pre>"
1980
           "<p>Then send the file to your teammates.</p>"));
1981
    return;
×
1982
  }
1983
  QString gpgExe = s.gpgExecutable;
1984
  if (gpgExe.isEmpty()) {
×
1985
    gpgExe = QStringLiteral("gpg");
×
1986
  }
1987
  QStringList args = {"--armor", "--export"};
×
1988
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1989
  QString stdOut;
×
1990
  QString stdErr;
×
1991
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
×
1992
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1993
    QMessageBox::warning(this, tr("Export Public Key"),
×
1994
                         tr("Could not export public key for %1.\n\n%2")
×
1995
                             .arg(identity, stdErr.isEmpty()
×
1996
                                                ? tr("No output from gpg.")
×
1997
                                                : stdErr));
1998
    return;
1999
  }
2000
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
2001
  dialog.exec();
×
2002
}
×
2003

2004
/**
2005
 * @brief MainWindow::addRecipient open the recipient management dialog for
2006
 *        the supplied directory.
2007
 * @param dir Folder whose .gpg-id should be edited.
2008
 *
2009
 * Delegates to UsersDialog so users can tick/untick keys from their
2010
 * keyring as recipients of the folder; importing a foreign key into the
2011
 * keyring still has to happen via gpg (or QtPass settings) first.
2012
 */
2013
void MainWindow::addRecipient(const QString &dir) {
×
2014
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
2015
  d.exec();
×
2016
}
×
2017

2018
/**
2019
 * @brief MainWindow::showShareHelp show help about GPG sharing
2020
 */
2021
void MainWindow::showShareHelp() {
×
2022
  QMessageBox::information(
×
2023
      this, tr("Sharing Passwords with GPG"),
×
2024
      tr("<h3>Sharing Passwords with GPG</h3>"
×
2025
         "<p>To share passwords with other users:</p>"
2026
         "<ol>"
2027
         "<li><b>Export your public key</b> and send it to teammates</li>"
2028
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
2029
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
2030
         "them</li>"
2031
         "</ol>"
2032
         "<p>Only people who have a matching secret key can decrypt the "
2033
         "passwords.</p>"
2034
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
2035
         "<p>See the FAQ for more details.</p>"));
2036
}
×
2037

2038
void MainWindow::updateGitButtonVisibility() {
57 ✔
2039
  const AppSettings s = QtPassSettings::load();
57 ✔
2040
  if (!s.useGit || (s.gitExecutable.isEmpty() && s.passExecutable.isEmpty())) {
57 ✔
2041
    enableGitButtons(false);
57 ✔
2042
  } else {
2043
    enableGitButtons(true);
×
2044
  }
2045
}
57 ✔
2046

2047
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
57 ✔
2048
  // No platform gating any more: codes are generated in-process, so this works
2049
  // on Windows and macOS and with either backend. It used to be hidden there
2050
  // because the pass-otp extension is Unix-only.
2051
  //
2052
  // Kept visible but disabled when OTP is off, as it was before: hiding it
2053
  // removed the only discoverable trace of the feature, and since an OTP field
2054
  // is suppressed from the panel unconditionally (its value is a secret), a
2055
  // user with the setting off would see nothing at all where their OTP data
2056
  // used to be.
2057
  //
2058
  // Enablement must honour uiEnabled: this is called from
2059
  // setUiElementsEnabled(), and ignoring the argument re-enabled the action
2060
  // while a decrypt was in flight, letting the user stack Show calls.
2061
  ui->actionOtp->setVisible(true);
57 ✔
2062
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
57 ✔
2063
}
57 ✔
2064

2065
void MainWindow::updateGrepButtonVisibility() {
36 ✔
2066
  const bool enabled = QtPassSettings::isUseGrepSearch();
36 ✔
2067
  ui->grepButton->setVisible(enabled);
36 ✔
2068
  ui->grepCaseButton->setVisible(enabled);
36 ✔
2069
  if (!enabled && m_grep.inGrepMode()) {
36 ✔
2070
    ui->grepButton->setChecked(false);
×
2071
  }
2072
}
36 ✔
2073

2074
void MainWindow::enableGitButtons(const bool &state) {
57 ✔
2075
  // Following GNOME guidelines is preferable disable buttons instead of hide
2076
  ui->actionPush->setEnabled(state);
57 ✔
2077
  ui->actionUpdate->setEnabled(state);
57 ✔
2078
}
57 ✔
2079

2080
/**
2081
 * @brief MainWindow::critical critical message popup wrapper.
2082
 * @param title
2083
 * @param msg
2084
 */
2085
void MainWindow::critical(const QString &title, const QString &msg) {
×
2086
  QMessageBox::critical(this, title, msg);
×
2087
}
×
2088

2089
/**
2090
 * @brief Appends processed command output to the output panel.
2091
 *
2092
 * Appends text to the process output text edit, with per-line numbering,
2093
 * optional command prefix, and color coding for errors vs. success.
2094
 * Handles auto-scrolling and line limits.
2095
 *
2096
 * @param output The raw output text from the command.
2097
 * @param isError true if this is error output (stderr).
2098
 * @param linePrefix Optional command name to prefix each line with.
2099
 */
2100
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2 ✔
2101
                                     const QString &linePrefix) {
2102
  if (!QtPassSettings::isShowProcessOutput()) {
2 ✔
2103
    return;
1 ✔
2104
  }
2105

2106
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1 ✔
2107
  for (QString &line : lines) {
2 ✔
2108
    // Right-trim only: remove trailing CR and whitespace, preserve leading
2109
    // indentation
2110
    line.remove('\r');
1 ✔
2111
    while (!line.isEmpty() && line.back().isSpace()) {
2 ✔
2112
      line.chop(1);
×
2113
    }
2114
    if (line.isEmpty()) {
1 ✔
2115
      continue;
×
2116
    }
2117

2118
    m_outputCounter++;
1 ✔
2119
    QString lineNumber = QString::number(m_outputCounter);
1 ✔
2120

2121
    QColor textColor =
2122
        isError ? QColor(Qt::red)
1 ✔
2123
                : m_processOutputEdit->palette().color(QPalette::Text);
2 ✔
2124
    QString colorHex = textColor.name();
1 ✔
2125
    // Apply the optional prefix per line so multi-line output stays
2126
    // attributed to its command (e.g. all 3 lines of a `git push` show
2127
    // "git push: ..." rather than only the first).
2128
    QString prefixed =
2129
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2 ✔
2130
    QString coloredOutput =
2131
        QString("<span style=\"color: %1;\">%2: %3</span>")
1 ✔
2132
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2 ✔
2133

2134
    m_processOutputEdit->append(coloredOutput);
1 ✔
2135
  }
2136

2137
  limitOutputLines();
1 ✔
2138

2139
  if (m_autoScroll) {
1 ✔
2140
    m_processOutputEdit->verticalScrollBar()->setValue(
2 ✔
2141
        m_processOutputEdit->verticalScrollBar()->maximum());
1 ✔
2142
  }
2143
}
2144

2145
/**
2146
 * @brief Handles process output from the Pass executor.
2147
 *
2148
 * Called when any non-sensitive process completes. Filters out password-
2149
 * related commands (pass show, insert, etc.) and delegates to
2150
 * appendProcessOutput.
2151
 *
2152
 * @param output The stdout/stderr text from the process.
2153
 * @param isError true if this is error output (stderr).
2154
 * @param pid The process ID identifying which command ran.
2155
 */
2156
void MainWindow::onProcessOutput(const QString &output, bool isError,
2 ✔
2157
                                 Enums::PROCESS pid) {
2158
  appendProcessOutput(output, isError, getProcessName(pid));
2 ✔
2159
}
2 ✔
2160

2161
/**
2162
 * @brief Maps a process ID to its human-readable command name.
2163
 *
2164
 * Returns static strings for git/pass commands that appear in output.
2165
 * Password-related commands return empty (they are filtered).
2166
 *
2167
 * @param pid The process ID to look up.
2168
 * @return QString with command name, or empty if filtered.
2169
 */
2170
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
2 ✔
2171
  switch (pid) {
2 ✔
2172
  case Enums::GIT_INIT:
×
2173
    return QStringLiteral("git init"); // no-tr
×
2174
  case Enums::GIT_ADD:
×
2175
    return QStringLiteral("git add"); // no-tr
×
2176
  case Enums::GIT_COMMIT:
×
2177
    return QStringLiteral("git commit"); // no-tr
×
2178
  case Enums::GIT_RM:
×
2179
    return QStringLiteral("git rm"); // no-tr
×
2180
  case Enums::GIT_PULL:
×
2181
    return QStringLiteral("git pull"); // no-tr
×
2182
  case Enums::GIT_PUSH:
×
2183
    return QStringLiteral("git push"); // no-tr
×
2184
  case Enums::GIT_MOVE:
×
2185
    return QStringLiteral("git mv"); // no-tr
×
2186
  case Enums::GIT_COPY:
×
2187
    // ImitatePass::Copy literally invokes `git cp` (a git-extras
2188
    // subcommand), so the label matches what's run. Stock-git users
2189
    // without git-extras will see the underlying "'cp' is not a git
2190
    // command" failure surfaced in the process output panel.
2191
    return QStringLiteral("git cp"); // no-tr
×
2192
  case Enums::PASS_INSERT:
×
2193
    return QStringLiteral("pass insert"); // no-tr
×
2194
  case Enums::PASS_REMOVE:
×
2195
    return QStringLiteral("pass rm"); // no-tr
×
2196
  case Enums::PASS_INIT:
×
2197
    return QStringLiteral("pass init"); // no-tr
×
2198
  case Enums::PASS_MOVE:
×
2199
    return QStringLiteral("pass mv"); // no-tr
×
2200
  case Enums::PASS_COPY:
×
2201
    return QStringLiteral("pass cp"); // no-tr
×
2202
  case Enums::PASS_GREP:
×
2203
    return QStringLiteral("pass grep"); // no-tr
×
2204
  case Enums::GPG_GENKEYS:
×
2205
    return QStringLiteral("gpg --gen-key"); // no-tr
×
2206
  case Enums::PASS_SHOW:
2207
  case Enums::PROCESS_COUNT:
2208
  case Enums::INVALID:
2209
    break;
2210
  }
2211
  return {};
2212
}
2213

2214
/**
2215
 * @brief Checks if a process ID represents a sensitive operation whose
2216
 * output should not be shown in the process output panel.
2217
 *
2218
 * Password-related commands (pass show, grep, insert)
2219
 * display their output in other UI areas, so we skip them here.
2220
 *
2221
 * @param pid The process ID to check.
2222
 * @return true if the process is sensitive and should be filtered.
2223
 */
2224
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
2225
  switch (pid) {
×
2226
  case Enums::PASS_SHOW:
2227
  case Enums::PASS_GREP:
2228
  case Enums::PASS_INSERT:
2229
    return true;
2230
  case Enums::GIT_INIT:
2231
  case Enums::GIT_ADD:
2232
  case Enums::GIT_COMMIT:
2233
  case Enums::GIT_RM:
2234
  case Enums::GIT_PULL:
2235
  case Enums::GIT_PUSH:
2236
  case Enums::GIT_MOVE:
2237
  case Enums::GIT_COPY:
2238
  case Enums::PASS_REMOVE:
2239
  case Enums::PASS_INIT:
2240
  case Enums::PASS_MOVE:
2241
  case Enums::PASS_COPY:
2242
  case Enums::GPG_GENKEYS:
2243
  case Enums::PROCESS_COUNT:
2244
  case Enums::INVALID:
2245
    break;
2246
  }
2247
  return false;
×
2248
}
2249

2250
/**
2251
 * @brief Updates the visibility of the process output panel.
2252
 *
2253
 * Shows or hides the process output widget based on the user's
2254
 * showProcessOutput setting.
2255
 */
2256
void MainWindow::updateProcessOutputVisibility() {
3 ✔
2257
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
3 ✔
2258
}
3 ✔
2259

2260
/**
2261
 * @brief Limits the output panel to max lines, trimming old excess.
2262
 *
2263
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
2264
 * Called after each append to prevent unbounded growth.
2265
 */
2266
void MainWindow::limitOutputLines() {
1 ✔
2267
  QTextDocument *doc = m_processOutputEdit->document();
1 ✔
2268
  int excess = doc->blockCount() - MaxOutputLines;
1 ✔
2269
  if (excess <= 0) {
1 ✔
2270
    return;
1 ✔
2271
  }
2272

2273
  QTextCursor cursor(doc);
×
2274
  cursor.movePosition(QTextCursor::Start);
×
2275
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2276
  cursor.removeSelectedText();
×
2277
}
×
2278

2279
/**
2280
 * @brief Clears the process output panel.
2281
 *
2282
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2283
 */
2284
void MainWindow::on_clearOutputButton_clicked() {
×
2285
  m_processOutputEdit->clear();
×
2286
  m_outputCounter = 0;
×
2287
  m_autoScroll = true;
×
2288
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc