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

IJHack / QtPass / 35468717610

19 Sep 2026 08:52PM UTC coverage: 78.059% (-0.1%) from 78.167%
35468717610

Pull #1851

github

web-flow
Merge e377412f2 into 17bd44c92
Pull Request #1851: Store walks visit real directories only; junctions and links are not entries

84 of 129 new or added lines in 6 files covered. (65.12%)

9 existing lines in 3 files now uncovered.

6884 of 8819 relevant lines covered (78.06%)

74.73 hits per line

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

51.49
/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 "firstrunwizard.h"
13
#include "passworddialog.h"
14
#include "passworddisplaypanel.h"
15
#include "pathvalidator.h"
16
#include "processoutputpanel.h"
17
#include "qpushbuttonasqrcode.h"
18
#include "qpushbuttonshowpassword.h"
19
#include "qpushbuttonwithclipboard.h"
20
#include "qtpass.h"
21
#include "qtpasssettings.h"
22
#include "templateio.h"
23
#include "totp.h"
24
#include "trayicon.h"
25
#include "ui_mainwindow.h"
26
#include "usersdialog.h"
27
#include "util.h"
28
#include "windowstatestore.h"
29
#include <QApplication>
30
#include <QCloseEvent>
31
#include <QDate>
32
#include <QDesktopServices>
33
#include <QDialog>
34
#include <QDockWidget>
35
#include <QFileInfo>
36
#include <QHBoxLayout>
37
#include <QInputDialog>
38
#include <QLabel>
39
#include <QLineEdit>
40
#include <QMenu>
41
#include <QMessageBox>
42
#include <QProgressDialog>
43
#include <QPushButton>
44
#include <QShortcut>
45
#include <QTextCharFormat>
46
#include <QTextStream>
47
#include <QTimer>
48
#include <QTreeWidget>
49
#include <QUrl>
50
#include <algorithm>
51
#include <utility>
52

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

68
  m_qtPass = new QtPass(this);
44✔
69
  restoreWindow();
44✔
70
  connect(m_qtPass, &QtPass::outputReady, this,
44✔
71
          [this](const QString &html) { flashText(html, false, true); });
56✔
72
  connect(m_qtPass, &QtPass::operationFinished, this,
44✔
73
          [this]() { setUiElementsEnabled(true); });
56✔
74
  connect(m_qtPass, &QtPass::statusMessage, this,
44✔
75
          &MainWindow::showStatusMessage);
44✔
76
  connect(m_qtPass, &QtPass::pushRequested, this, &MainWindow::onPush);
44✔
77
  connect(m_qtPass, &QtPass::entryInserted, this,
44✔
78
          [this]() { on_treeView_clicked(getCurrentTreeViewIndex()); });
45✔
79
  connect(&m_qtPass->clipboard(), &ClipboardManager::statusMessage, this,
44✔
80
          [this](const QString &message) { showStatusMessage(message); });
51✔
81
  // Both backends: the active one can change through the configuration
82
  // dialog without a restart.
83
  connectPassSignals(QtPassSettings::getRealPass());
44✔
84
  connectPassSignals(QtPassSettings::getImitatePass());
44✔
85
  ImitatePass *ipass = QtPassSettings::getImitatePass();
44✔
86
  connect(ipass, &ImitatePass::startReencryptPath, this,
44✔
87
          &MainWindow::startReencryptPath);
44✔
88
  connect(ipass, &ImitatePass::reencryptProgress, this,
44✔
89
          &MainWindow::reencryptProgress);
44✔
90
  connect(ipass, &ImitatePass::endReencryptPath, this,
44✔
91
          &MainWindow::endReencryptPath);
44✔
92

93
  // register shortcut ctrl/cmd + C to copy the currently selected password
94
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
44✔
95
                &MainWindow::copyPasswordFromTreeview);
44✔
96

97
  m_tree = new StoreTree(ui->treeView, this);
44✔
98
  m_tree->setStore(QtPassSettings::getPassStore(Util::findPasswordStore()));
44✔
99
  m_tree->setPass(QtPassSettings::getPass());
44✔
100
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
44✔
101
          &MainWindow::showContextMenu);
44✔
102
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
44✔
103
          &MainWindow::deselect);
44✔
104

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

122
  updateProfileBox();
44✔
123

124
  m_displayPanel = new PasswordDisplayPanel(
44✔
125
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
44✔
126
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
44✔
127
          &m_qtPass->clipboard(), &ClipboardManager::copyText);
44✔
128
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
44✔
129
          &QtPass::showTextAsQRCode);
44✔
130
  // Double-click on what is shown edits it, as in the tree.
131
  connect(m_displayPanel, &PasswordDisplayPanel::editRequested, this, [this] {
44✔
132
    if (ui->actionEdit->isEnabled()) {
×
133
      onEdit();
×
134
    }
135
  });
×
136

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

141
  searchTimer.setInterval(350);
44✔
142
  searchTimer.setSingleShot(true);
44✔
143

144
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
44✔
145

146
  // Install the search-box key filter once, not on every setUiElementsEnabled
147
  // call.
148
  ui->lineEdit->installEventFilter(this);
44✔
149
  ui->toolBar->installEventFilter(this);
44✔
150

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

163
  initToolBarButtons();
44✔
164
  initStatusBar();
44✔
165
  m_processOutput = new ProcessOutputPanel(this);
44✔
166
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
44✔
167
  // setVisible after addDockWidget so our explicit preference wins even if
168
  // QMainWindow applies any cached state when the dock is attached.
169
  // restoreWindow() ran right after the QtPass construction above, so the
170
  // saved layout has already been processed.
171
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
44✔
172

173
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
44✔
174
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
88✔
175
            // Never route potentially-secret output through the panel:
176
            // - PASS_SHOW goes via a dedicated signal to
177
            //   the main text browser (which clears on a timer).
178
            // - PASS_GREP returns lines from password files; #252 must
179
            //   not leak those into a long-lived panel.
180
            // - PASS_INSERT's stdin is the password; stdout normally
181
            //   carries gpg/git progress only, but exclude defensively
182
            //   in case a future code path uses --echo or similar.
183
            if (ProcessOutputPanel::isSensitiveProcess(pid)) {
×
184
              return;
185
            }
186
            if (!out.isEmpty()) {
×
187
              onProcessOutput(out, false, pid);
×
188
            }
189
            if (!err.isEmpty()) {
×
190
              onProcessOutput(err, true, pid);
×
191
            }
192
          });
193

194
  ui->lineEdit->setClearButtonEnabled(true);
44✔
195
  updateGrepButtonVisibility();
44✔
196

197
  setUiElementsEnabled(true);
44✔
198

199
  ui->lineEdit->setText(searchText);
44✔
200

201
  // Record whether startup configuration succeeded instead of calling
202
  // QApplication::quit() here: quit() before the event loop runs (exec() is
203
  // still ahead in main()) is a documented no-op, so a cancelled first-run
204
  // wizard used to leave the half-configured window showing anyway. main()
205
  // consults initSucceeded() and exits before show() when this is false.
206
  m_qtPass->init();
44✔
207
  // Ask again until the configuration is usable or the user gives up. The
208
  // dialog's OK button is not gated on Util::configIsValid(), so an accepted
209
  // first-run configuration can still point at a store without a .gpg-id
210
  // (the user declined to create it); only a cancel ends the loop.
211
  while (!Util::configIsValid(QtPassSettings::load())) {
45✔
212
    if (!config()) {
2✔
213
      return;
214
    }
215
  }
216
  m_freshStart = false;
43✔
217
  m_initSucceeded = true;
43✔
218

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

228
MainWindow::~MainWindow() {
84✔
229
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
230
  // window still knows its last geometry here.
231
  saveWindowState();
44✔
232
  delete m_qtPass;
44✔
233
}
128✔
234

235
/**
236
 * @brief Connect the signals of one backend that the window answers itself:
237
 * decrypt results, errors, status text and the grep results.
238
 * @param pass The backend.
239
 */
240
void MainWindow::connectPassSignals(Pass *pass) {
88✔
241
  // A failed decrypt never emits finishedShow; drop the pending OTP/copy
242
  // request so a later decrypt of the same entry is not taken as its answer.
243
  connect(pass, &Pass::processErrorExit, this, &MainWindow::cancelOtpRequest);
88✔
244
  connect(pass, &Pass::critical, this, &MainWindow::critical);
88✔
245
  connect(pass, &Pass::startingExecuteWrapper, this,
88✔
246
          &MainWindow::executeWrapperStarted);
88✔
247
  connect(pass, &Pass::statusMsg, this, &MainWindow::showStatusMessage);
88✔
248
  connect(pass, &Pass::finishedShow, this, &MainWindow::passShowHandler);
88✔
249
  // Both check the file against their own pending request and ignore the
250
  // rest, so they can stay connected for good.
251
  connect(pass, &Pass::finishedShow, this, &MainWindow::otpFromFileToClipboard);
88✔
252
  connect(pass, &Pass::finishedShow, this,
88✔
253
          &MainWindow::passwordFromFileToClipboard);
88✔
254
  connect(pass, &Pass::finishedGrep, this, &MainWindow::onGrepFinished);
88✔
255
}
88✔
256

257
/**
258
 * @brief MainWindow::saveWindowState persist geometry and dock/toolbar
259
 *        layout. Idempotent, cheap, called from closeEvent() and the
260
 *        destructor.
261
 */
262
void MainWindow::saveWindowState() {
46✔
263
  QtPassSettings::setGeometry(saveGeometry());
46✔
264
  QtPassSettings::setSavestate(saveState());
46✔
265
}
46✔
266

267
/**
268
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
269
 * box and sets focus to it. Allows for easy searching, called at application
270
 * start and when receiving empty message in MainWindow::messageAvailable when
271
 * compiled with SINGLE_APP=1 (default).
272
 */
273
void MainWindow::focusInput() {
14✔
274
  // Resolve the QLineEdit through the live widget tree rather than the
275
  // cached `ui->lineEdit` pointer.
276
  //
277
  // On a fresh-config first launch the constructor calls
278
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
279
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
280
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
281
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
282
  // we observed the QLineEdit attached to the centralWidget gets
283
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
284
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
285
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
286
  // walks the current hierarchy and returns null cleanly when the
287
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
288
  // use-after-free.
289
  if (!isVisible()) {
14✔
290
    return;
291
  }
292
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
14✔
293
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
14✔
294
    return;
295
  }
296
  lineEdit->selectAll();
14✔
297
  lineEdit->setFocus();
14✔
298
  // Only mark the first-show focus pulse as done once it's actually
299
  // landed; setting it eagerly in showEvent() would consume the
300
  // one-shot if focusInput returned early (mid-rebuild widget state)
301
  // and we'd never retry.
302
  m_firstShowCompleted = true;
14✔
303
}
304

305
/**
306
 * @brief MainWindow::changeEvent sets focus to the search box on activation
307
 * and re-derives palette-dependent styling on a theme change.
308
 * @param event
309
 */
310
void MainWindow::changeEvent(QEvent *event) {
63✔
311
  QWidget::changeEvent(event);
63✔
312
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
63✔
313
    // Desktop switched light/dark (e.g. KDE day/night). Top-level widgets
314
    // receive PaletteChange for that (ApplicationPaletteChange goes to the
315
    // QApplication object). Styling that bakes palette colours into
316
    // stylesheets must be re-derived by hand.
317
    m_displayPanel->refreshPalette();
5✔
318
  }
319
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
63✔
320
      isVisible()) {
321
    // Defer one event-loop tick so the synchronous activation dispatch
322
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
323
    // unwinds before we touch widget state — calling `focusInput()`
324
    // inline from this stack has segfaulted in past iterations because
325
    // mid-rebuild ui state isn't fully wired up yet.
326
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
10✔
327
                              Qt::QueuedConnection);
328
  }
329
}
63✔
330

331
/**
332
 * @brief First-show hook: run the initial focusInput() pulse once the
333
 *        window is actually mapped. The widget's internal data is fully
334
 *        initialised by this point, so QLineEdit::selectAll() is safe.
335
 * @param event Show event passed to the base class.
336
 */
337
void MainWindow::showEvent(QShowEvent *event) {
12✔
338
  QMainWindow::showEvent(event);
12✔
339
  if (m_firstShowCompleted) {
12✔
340
    return;
341
  }
342
  // Queue the focus pulse for the next event-loop tick so the platform
343
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
344
  // from the config wizard path) settle before we look up the line
345
  // edit. The `m_firstShowCompleted` latch is set inside focusInput()
346
  // *after* it actually focuses, so a transient failed lookup just
347
  // re-queues on the next show rather than silently dropping.
348
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
10✔
349
                            Qt::QueuedConnection);
350
}
351

352
/**
353
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
354
 */
355
void MainWindow::initToolBarButtons() {
44✔
356
  connect(ui->actionAddPassword, &QAction::triggered, this,
44✔
357
          &MainWindow::addPassword);
44✔
358
  connect(ui->actionAddFolder, &QAction::triggered, this,
44✔
359
          &MainWindow::addFolder);
44✔
360
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
44✔
361
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
44✔
362
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
44✔
363
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
44✔
364
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
44✔
365
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
44✔
366
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
44✔
367

368
  // Menu-only actions. Quit ends the application, as Ctrl+Q/Cmd+Q does in
369
  // KeePassXC, Telegram and per the KDE, GNOME and macOS guidelines, tray
370
  // icon or not (#1788). Close window is what the old bare Ctrl+Q shortcut
371
  // did: close(), which closeEvent() turns into a hide when "hide on close"
372
  // is set. The X button and Alt+F4 are close() as well.
373
  connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
44✔
374
  connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
44✔
375
#ifdef Q_OS_MACOS
376
  // The menu bar is the system's up there; nothing to hide.
377
  ui->actionShowMenuBar->setVisible(false);
378
#else
379
  // The action lives in the menu it hides, so it has to be on the window as
380
  // well for Ctrl+M to keep working while the bar is gone.
381
  addAction(ui->actionShowMenuBar);
44✔
382
  ui->actionShowMenuBar->setChecked(QtPassSettings::load().showMenuBar);
44✔
383
  menuBar()->setVisible(ui->actionShowMenuBar->isChecked());
44✔
384
  connect(ui->actionShowMenuBar, &QAction::toggled, this, [this](bool show) {
44✔
385
    menuBar()->setVisible(show);
3✔
386
    AppSettings s = QtPassSettings::load();
3✔
387
    s.showMenuBar = show;
3✔
388
    QtPassSettings::save(s);
3✔
389
  });
3✔
390
#endif
391
  connect(ui->actionFaq, &QAction::triggered, this, [] {
44✔
392
    QDesktopServices::openUrl(QUrl(QStringLiteral("https://qtpass.org/faq")));
×
393
  });
×
394
  connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
44✔
395
  connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
44✔
396

397
  ui->actionAddPassword->setIcon(
44✔
398
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
176✔
399
  ui->actionAddFolder->setIcon(
44✔
400
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
176✔
401
  ui->actionEdit->setIcon(QIcon::fromTheme(
44✔
402
      "document-properties", QIcon(":/icons/document-properties.svg")));
132✔
403
  ui->actionDelete->setIcon(
44✔
404
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
176✔
405
  ui->actionPush->setIcon(
44✔
406
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
176✔
407
  ui->actionUpdate->setIcon(
44✔
408
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
176✔
409
  ui->actionUsers->setIcon(QIcon::fromTheme(
44✔
410
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
132✔
411
  ui->actionConfig->setIcon(QIcon::fromTheme(
44✔
412
      "applications-system", QIcon(":/icons/applications-system.svg")));
88✔
413
}
44✔
414

415
/**
416
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
417
 */
418
void MainWindow::initStatusBar() {
44✔
419
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
88✔
420

421
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
88✔
422
                     .scaledToHeight(statusBar()->height());
44✔
423
  auto *logoApp = new QLabel(statusBar());
44✔
424
  logoApp->setPixmap(logo);
44✔
425
  statusBar()->addPermanentWidget(logoApp);
44✔
426
}
44✔
427

428
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1✔
429
  return ui->treeView->currentIndex();
1✔
430
}
431

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

462
  if (isHtml) {
24✔
463
    QString _text = text;
464
    if (!ui->textBrowser->toPlainText().isEmpty()) {
38✔
465
      _text = ui->textBrowser->toHtml() + _text;
16✔
466
    }
467
    ui->textBrowser->setHtml(_text);
19✔
468
  } else {
469
    ui->textBrowser->setText(text);
5✔
470
  }
471
}
24✔
472

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

487
  if (s.noLineWrapping) {
1✔
488
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
489
  } else {
490
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
1✔
491
  }
492
}
1✔
493

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

516
/**
517
 * @brief Opens the configuration dialog (or, before the first usable
518
 * configuration, the first-run wizard) and applies what was accepted.
519
 * @example
520
 * if (!config()) {
521
 *   // the user cancelled
522
 * }
523
 *
524
 * @return bool - True when the dialog was accepted, false when it was
525
 * cancelled. An accepted dialog can still leave the configuration invalid
526
 * (the OK button is not gated on Util::configIsValid()); the constructor
527
 * keeps asking until the configuration is usable or this returns false.
528
 */
529
auto MainWindow::config() -> bool {
2✔
530
  if (m_freshStart) {
2✔
531
    // No usable configuration yet: the wizard finds the programs, the key
532
    // and the store, and creates the store when it has to.
533
    FirstRunWizard wizard(this);
2✔
534
    if (wizard.exec() != QDialog::Accepted) {
2✔
535
      return false;
536
    }
537
  } else {
2✔
538
    ConfigDialog d(this);
×
539
    d.setModal(true);
×
540
    if (d.exec() != QDialog::Accepted) {
×
541
      return false;
542
    }
543
  }
×
544

545
  applyTextBrowserSettings();
1✔
546
  applyWindowFlagsSettings();
1✔
547

548
  updateProfileBox();
1✔
549
  const AppSettings s = QtPassSettings::load();
1✔
550
  m_tree->setStore(s.passStore);
1✔
551
  deselect();
1✔
552
  ui->treeView->setCurrentIndex(QModelIndex());
1✔
553

554
  Pass *activePass = QtPassSettings::getPass();
1✔
555
  activePass->updateEnv();
1✔
556
  m_tree->setPass(activePass);
1✔
557
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
1✔
558
  m_qtPass->clipboard().setAutoclearTimer();
1✔
559

560
  updateGitButtonVisibility();
1✔
561
  updateOtpButtonVisibility();
1✔
562
  updateGrepButtonVisibility();
1✔
563
  updateProcessOutputVisibility();
1✔
564
#ifndef Q_OS_MACOS
565
  // The action's toggled handler shows or hides the bar (and saves again).
566
  ui->actionShowMenuBar->setChecked(s.showMenuBar);
1✔
567
#endif
568
  if (s.useTrayIcon && m_tray == nullptr) {
1✔
569
    initTrayIcon();
×
570
  } else if (!s.useTrayIcon && m_tray != nullptr) {
1✔
571
    destroyTrayIcon();
×
572
  }
573

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

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

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

608
/**
609
 * @brief MainWindow::getFile get the selected file path
610
 * @param index
611
 * @param forPass returns relative path without '.gpg' extension
612
 * @return path
613
 * @return
614
 */
615
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
12✔
616
  return m_tree->fileFor(index, forPass);
12✔
617
}
618

619
/**
620
 * @brief MainWindow::on_treeView_clicked read the selected password file
621
 * @param index
622
 */
623
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
3✔
624
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
3✔
625
  QString file = getFile(index, true);
3✔
626
  ui->passwordName->setText(file);
3✔
627
  if (!file.isEmpty() && !cleared) {
3✔
628
    // Remember what the panel is about to show, so onOtp() can tell whether the
629
    // code it can see belongs to the entry that is currently selected.
630
    m_shownFile = file;
3✔
631
    QtPassSettings::getPass()->Show(file);
6✔
632
  } else {
633
    m_shownFile.clear();
×
634
    clearPanel(false);
×
635
    ui->actionEdit->setEnabled(false);
×
636
    ui->actionDelete->setEnabled(true);
×
637
  }
638
}
3✔
639

640
/**
641
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
642
 * TreeViewItem, open the edit Window
643
 * @param index
644
 */
645
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
1✔
646
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
1✔
647

648
  if (fileOrFolder.isFile()) {
1✔
649
    editPassword(getFile(index, true));
2✔
650
  }
651
}
1✔
652

653
/**
654
 * @brief MainWindow::deselect clear the selection, password and copy buffer
655
 */
656
void MainWindow::deselect() {
2✔
657
  m_shownFile.clear();
2✔
658
  cancelOtpRequest();
2✔
659
  m_qtPass->clipboard().clear();
2✔
660
  ui->treeView->clearSelection();
2✔
661
  ui->actionEdit->setEnabled(false);
2✔
662
  ui->actionDelete->setEnabled(false);
2✔
663
  ui->passwordName->setText("");
2✔
664
  clearPanel(false);
2✔
665
}
2✔
666

667
void MainWindow::executeWrapperStarted() {
13✔
668
  m_displayPanel->clear();
13✔
669
  ui->textBrowser->clear();
13✔
670
  setUiElementsEnabled(false);
13✔
671
  clearPanelTimer.stop();
13✔
672
  if (QtPassSettings::isShowProcessOutput()) {
13✔
673
    m_processOutput->setVisible(true);
8✔
674
  }
675
}
13✔
676

677
/**
678
 * @brief Handles displaying parsed password entry content in the main window:
679
 * the panel shows the parsed fields, the text browser what is left over.
680
 * @param p_output - The raw output text containing the password entry data.
681
 * @param file - The entry the output belongs to; anything but the entry most
682
 * recently asked for is dropped.
683
 */
684
void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
6✔
685
  // Only the entry we most recently asked to show may repaint the panel; a
686
  // slower decrypt of an entry the user has since left is dropped.
687
  if (!file.isEmpty() && !m_shownFile.isEmpty() && file != m_shownFile) {
6✔
688
    return;
×
689
  }
690
  const AppSettings s = QtPassSettings::load();
6✔
691
  QStringList templ =
692
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
6✔
693
  bool allFields = s.useTemplate && s.templateAllFields;
6✔
694
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
6✔
695
  QString output = p_output;
696
  // Display variant: empty when the password line is itself an otpauth URI, so
697
  // the shared secret is neither rendered nor copied to the clipboard.
698
  QString password = fileContent.getPasswordForDisplay();
6✔
699

700
  // set clipped text
701
  //
702
  // Skipped for an OTP request: the user asked for a one-time code, not the
703
  // password, and writing both in one event-loop turn can leave the Windows
704
  // clipboard empty (two OleSetClipboard calls back to back).
705
  if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
6✔
706
    m_qtPass->clipboard().copyIfAlways(password, p_output);
5✔
707
  }
708

709
  // first clear the current view:
710
  m_displayPanel->clear();
6✔
711

712
  // show what is needed:
713
  if (s.hideContent) {
6✔
714
    output = "***" + tr("Content hidden") + "***";
×
715
  } else if (!s.displayAsIs) {
6✔
716
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
12✔
717
                                  s.useOtp ? fileContent.getOtpUri()
6✔
718
                                           : QString());
719
    output = fileContent.getRemainingDataForDisplay();
12✔
720
  }
721

722
  if (s.useAutoclearPanel) {
6✔
723
    clearPanelTimer.start();
×
724
  }
725

726
  flashText(QtPass::formatOutput(output), false, true);
12✔
727
  setUiElementsEnabled(true);
6✔
728
}
12✔
729

730
/**
731
 * @brief Generates a one-time password from a decrypted entry and copies it.
732
 *
733
 * Connected to Pass::finishedShow for good; reacts only to the entry onOtp()
734
 * asked for. passShowHandler is connected first, so by the time this runs the
735
 * panel has already been repainted and the UI re-enabled.
736
 *
737
 * @param p_output - The decrypted entry content.
738
 * @return void - This function does not return a value.
739
 */
740
void MainWindow::otpFromFileToClipboard(const QString &p_output,
9✔
741
                                        const QString &file) {
742
  // Permanently connected: only the decrypt of the entry onOtp() asked for
743
  // is ours, everything else belongs to a tree click or a Ctrl+C.
744
  if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
9✔
745
    return;
8✔
746
  }
747
  m_otpRequestFile.clear();
2✔
748

749
  if (p_output.isEmpty()) {
2✔
750
    // Distinguish "could not read the entry" from "entry has no OTP".
751
    flashText(tr("Could not decrypt this password entry"), true);
×
752
    setUiElementsEnabled(true);
×
753
    return;
×
754
  }
755

756
  // passShowHandler is connected first, so it has already repainted the panel
757
  // for this same finishedShow. When it rendered the OTP row the current code
758
  // is derived and cached, so reuse it instead of re-loading settings and
759
  // re-parsing p_output (mirrors onOtp()'s fast path). Falls through to a fresh
760
  // parse when no OTP row is shown (hideContent / displayAsIs / no OTP field).
761
  const QString shown = m_displayPanel->currentOtpCode();
2✔
762
  if (!shown.isEmpty()) {
2✔
763
    m_qtPass->clipboard().copyText(shown);
1✔
764
    showStatusMessage(tr("OTP code copied to clipboard"));
1✔
765
    setUiElementsEnabled(true);
1✔
766
    return;
767
  }
768

769
  const AppSettings s = QtPassSettings::load();
1✔
770
  // Parse with the same template settings passShowHandler uses, so an OTP
771
  // field is recognised identically in both paths.
772
  const QStringList templ =
773
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
1✔
774
  const bool allFields = s.useTemplate && s.templateAllFields;
1✔
775
  const FileContent fileContent =
776
      FileContent::parse(p_output, templ, allFields);
1✔
777

778
  const std::optional<Totp::Settings> settings =
779
      Totp::parse(fileContent.getOtpUri());
2✔
780
  if (settings.has_value()) {
1✔
781
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
2✔
782
    showStatusMessage(tr("OTP code copied to clipboard"));
2✔
783
  } else {
784
    flashText(tr("No OTP code found in this password entry"), true);
×
785
  }
786
  setUiElementsEnabled(true);
1✔
787
}
2✔
788

789
/**
790
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
791
 */
792
void MainWindow::clearPanel(bool notify) {
5✔
793
  m_displayPanel->clear();
5✔
794
  const bool grepWasVisible = ui->grepResultsList->isVisible();
5✔
795
  ui->grepResultsList->clear();
5✔
796
  if (grepWasVisible) {
5✔
797
    ui->grepResultsList->setVisible(false);
×
798
    ui->treeView->setVisible(true);
×
799
    if (m_grep.inGrepMode()) {
×
800
      m_grep.clearGrepMode();
801
      ui->grepButton->blockSignals(true);
×
802
      ui->grepButton->setChecked(false);
×
803
      ui->grepButton->blockSignals(false);
×
804
      ui->lineEdit->blockSignals(true);
×
805
      ui->lineEdit->clear();
×
806
      ui->lineEdit->blockSignals(false);
×
807
      ui->lineEdit->setPlaceholderText(tr("Search password"));
×
808
    }
809
  }
810
  if (notify) {
5✔
811
    QString output = "***" + tr("Password and content hidden") + "***";
×
812
    ui->textBrowser->setHtml(output);
×
813
  } else {
814
    ui->textBrowser->setHtml("");
10✔
815
  }
816
}
5✔
817

818
/**
819
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
820
 * elements
821
 * @param state
822
 */
823
void MainWindow::setUiElementsEnabled(bool state) {
96✔
824
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
825
  // the user's escape hatch and endReencryptPath() releases the interface, so
826
  // neither an unrelated completion nor the watchdog may re-enable it early.
827
  if (state && m_reencryptRunning) {
96✔
828
    m_uiWatchdog.stop();
1✔
829
    return;
1✔
830
  }
831
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
832
  if (state) {
833
    m_uiWatchdog.stop();
70✔
834
  } else {
835
    m_uiWatchdog.start();
25✔
836
  }
837
  ui->treeView->setEnabled(state);
95✔
838
  ui->lineEdit->setEnabled(state);
95✔
839
  ui->actionAddPassword->setEnabled(state);
95✔
840
  ui->actionAddFolder->setEnabled(state);
95✔
841
  ui->actionUsers->setEnabled(state);
95✔
842
  ui->actionConfig->setEnabled(state);
95✔
843
  const bool uiEnabled = state;
844
  // is a file selected?
845
  state &= ui->treeView->currentIndex().isValid();
190✔
846
  ui->actionDelete->setEnabled(state);
95✔
847
  ui->actionEdit->setEnabled(state);
95✔
848
  updateGitButtonVisibility(uiEnabled);
95✔
849
  // `state` is now "UI enabled AND a file is selected", which is exactly when
850
  // generating an OTP makes sense.
851
  updateOtpButtonVisibility(state);
95✔
852
}
853

854
/**
855
 * @brief Restores the main window geometry, state, position, size, and
856
 * tray/icon settings from saved application settings.
857
 * @example
858
 * MainWindow window;
859
 * window.restoreWindow();
860
 *
861
 * @return void - This function does not return a value.
862
 */
863
void MainWindow::restoreWindow() {
51✔
864
  // saveGeometry() covers position, size, maximized state and screen; the
865
  // separate pos/size/maximized keys that used to be applied on top of it
866
  // are what discarded the restored position, and the unconditional
867
  // re-centre in main() then discarded the rest. Centre only when nothing
868
  // was saved.
869
  const QByteArray geometry = QtPassSettings::getGeometry();
102✔
870
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
51✔
871
    WindowStateStore::centreOnCursorScreen(*this);
4✔
872
  }
873
  restoreState(QtPassSettings::getSavestate(saveState()));
51✔
874
  const AppSettings s = QtPassSettings::load();
51✔
875

876
  applyWindowFlagsSettings();
51✔
877

878
  if (s.useTrayIcon && m_tray == nullptr) {
51✔
879
    initTrayIcon();
×
880
    if (s.startMinimized) {
×
881
      // since we are still in constructor, can't directly hide
882
      QTimer::singleShot(10, this, &MainWindow::hide);
×
883
    }
884
  } else if (!s.useTrayIcon && m_tray != nullptr) {
51✔
885
    destroyTrayIcon();
×
886
  }
887
}
102✔
888

889
/**
890
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
891
 */
892
void MainWindow::onConfig() { config(); }
×
893

894
/**
895
 * @brief MainWindow::showAbout version, link and licence in a standard box.
896
 */
897
void MainWindow::showAbout() {
×
898
  QMessageBox::about(
×
899
      this, tr("About QtPass"),
×
900
      tr("<h3>QtPass %1</h3>"
×
901
         "<p>A multi-platform GUI for <a "
902
         "href=\"https://www.passwordstore.org/\">"
903
         "pass</a>, the standard Unix password manager.</p>"
904
         "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
905
         "<a href=\"https://github.com/IJHack/QtPass\">Source and "
906
         "issues</a></p>"
907
         "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
908
         "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
909
         "version 3</a> or later.</p>")
910
          .arg(QStringLiteral(VERSION),
×
911
               QString::number(QDate::currentDate().year())));
×
912
}
×
913

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

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

938
  if (query.isEmpty()) {
3✔
939
    ui->treeView->collapseAll();
×
940
    deselect();
×
941
  }
942

943
  // The words are matched literally, in order, with anything in between:
944
  // "work vpn" finds work/acme/vpn. Regular expressions belong to the
945
  // content search; here a typed "[" or "(" must not switch filtering off.
946
  QStringList words;
3✔
947
  for (const QString &word :
948
       query.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
10✔
949
    words << QRegularExpression::escape(word);
4✔
950
  }
951
  QRegularExpression regExp(words.join(QStringLiteral(".*")),
6✔
952
                            QRegularExpression::CaseInsensitiveOption);
3✔
953
  m_tree->setFilter(regExp);
3✔
954

955
  if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
3✔
956
    selectFirstFile();
2✔
957
  } else {
958
    ui->actionEdit->setEnabled(false);
1✔
959
    ui->actionDelete->setEnabled(false);
1✔
960
  }
961
}
6✔
962

963
/**
964
 * @brief MainWindow::on_lineEdit_returnPressed get searching
965
 *
966
 * Select the first possible file in the tree
967
 */
968
void MainWindow::on_lineEdit_returnPressed() {
×
969
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
×
970
                    << m_tree->proxy().rowCount();
×
971

972
  if (m_grep.inGrepMode()) {
×
973
    const QString query = ui->lineEdit->text();
×
974
    if (!query.isEmpty()) {
×
975
      ui->grepResultsList->clear();
×
976
      ui->statusBar->showMessage(tr("Searching…"));
×
977
      if (m_grep.beginSearch()) {
978
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
979
      }
980
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
981
    } else {
982
      if (m_grep.cancelSearch()) {
983
        QApplication::restoreOverrideCursor();
×
984
      }
985
      ui->grepResultsList->clear();
×
986
      ui->grepResultsList->setVisible(false);
×
987
      ui->treeView->setVisible(true);
×
988
    }
989
    return;
990
  }
991

992
  if (m_tree->proxy().rowCount() > 0) {
×
993
    selectFirstFile();
×
994
    on_treeView_clicked(ui->treeView->currentIndex());
×
995
  }
996
}
997

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

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

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

1110
/**
1111
 * @brief MainWindow::selectFirstFile select the first possible file in the
1112
 * tree
1113
 */
1114
void MainWindow::selectFirstFile() {
2✔
1115
  QModelIndex index = firstFile(m_tree->rootIndex());
2✔
1116
  ui->treeView->setCurrentIndex(index);
2✔
1117
}
2✔
1118

1119
/**
1120
 * @brief MainWindow::firstFile return location of first possible file
1121
 * @param parentIndex
1122
 * @return QModelIndex
1123
 */
1124
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
4✔
1125
  StoreModel &proxy = m_tree->proxy();
4✔
1126
  int numRows = proxy.rowCount(parentIndex);
4✔
1127
  for (int row = 0; row < numRows; ++row) {
4✔
1128
    QModelIndex index = proxy.index(row, 0, parentIndex);
4✔
1129
    if (m_tree->fileInfo(index).isFile()) {
4✔
1130
      return index;
4✔
1131
    }
1132
    if (proxy.hasChildren(index)) {
2✔
1133
      QModelIndex childFile = firstFile(index);
2✔
1134
      if (childFile.isValid())
1135
        return childFile;
2✔
1136
    }
1137
  }
1138
  return QModelIndex();
1139
}
1140

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

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

1172
  if (isNew) {
1✔
1173
    const QString storePath = s.passStore;
1174
    QString folder = m_tree->currentDir(false);
×
1175
    if (folder.isEmpty()) {
×
1176
      folder = storePath;
×
1177
    }
1178
    // The dialog names the entry itself: every folder of the store to pick
1179
    // from, the tree's current one preselected.
1180
    // Real folders of the store only: a linked or junctioned folder would
1181
    // offer (and write the new entry to) somewhere outside it.
UNCOV
1182
    QStringList folders{QString()};
×
NEW
1183
    const QStringList found = Util::directoriesUnder(storePath);
×
NEW
1184
    for (const QString &path : found) {
×
NEW
1185
      const QString rel = QDir(storePath).relativeFilePath(path);
×
UNCOV
1186
      if (!rel.startsWith(u'.') && !rel.contains(QStringLiteral("/."))) {
×
1187
        folders << rel;
1188
      }
1189
    }
1190
    std::sort(folders.begin() + 1, folders.end());
×
1191
    d.setNewEntryLocation(storePath, folders,
×
1192
                          QDir(storePath).relativeFilePath(folder));
×
1193
    QHash<QString, QStringList> templates =
1194
        TemplateIO::readTemplates(storePath);
×
1195
    if (!templates.isEmpty()) {
1196
      QString defaultTemplate =
1197
          TemplateIO::getFolderTemplate(folder, storePath);
×
1198
      d.setAvailableTemplates(templates, defaultTemplate);
×
1199
    }
1200
  }
×
1201

1202
  if (!d.exec()) {
1✔
1203
    ui->treeView->setFocus();
×
1204
  }
1205
}
1✔
1206

1207
/**
1208
 * @brief MainWindow::addPassword add a new password: one dialog with the
1209
 * folder, the name and the content.
1210
 */
1211
void MainWindow::addPassword() { setPassword(QString()); }
×
1212

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

1226
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
×
1227
  QString file = "";
×
1228
  bool isDir = false;
1229

1230
  if (fileOrFolder.isFile()) {
×
1231
    file = getFile(ui->treeView->currentIndex(), true);
×
1232
  } else {
1233
    file = m_tree->currentDir(true);
×
1234
    isDir = true;
1235
  }
1236

1237
  QString dirMessage = tr(" and the whole content?");
1238
  const QString folder =
NEW
1239
      m_tree->fileSystem().rootPath() + QDir::separator() + file;
×
NEW
1240
  if (!(isDir && Util::isLinkedFolder(folder)) && refuseLinkedFolder(folder)) {
×
1241
    // Behind a link: deleting would reach outside the store.
1242
    return;
1243
  }
NEW
1244
  if (isDir && Util::isLinkedFolder(folder)) {
×
1245
    // Only the link goes (ImitatePass::Remove unlinks it); nothing behind it
1246
    // is looked at or mentioned.
NEW
1247
    if (QMessageBox::question(
×
NEW
1248
            this, tr("Delete link?"),
×
NEW
1249
            tr("%1 is a symbolic link or junction. Remove the link? What it "
×
1250
               "points to is left alone.")
NEW
1251
                .arg(QDir::separator() + file),
×
1252
            QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1253
      return;
1254
    }
NEW
1255
    QtPassSettings::getPass()->Remove(file, isDir);
×
NEW
1256
    return;
×
1257
  }
1258
  if (isDir) {
×
1259
    // A link, junction or special file inside is as unexpected as a stray
1260
    // plain file: the walker leaves them out of content and reports them.
NEW
1261
    QStringList skipped;
×
1262
    const QStringList content =
NEW
1263
        Util::regularFilesUnder(folder, {QStringLiteral("*")}, &skipped);
×
1264
    const bool unexpected =
NEW
1265
        !skipped.isEmpty() ||
×
NEW
1266
        std::any_of(content.cbegin(), content.cend(), [](const QString &path) {
×
NEW
1267
          return QFileInfo(path).suffix() != QLatin1String("gpg");
×
1268
        });
1269
    if (unexpected) {
NEW
1270
      dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1271
                      "there are unexpected files in the given folder, "
1272
                      "check them before continue.</strong>");
1273
    }
1274
  }
1275

1276
  if (QMessageBox::question(
×
1277
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1278
          tr("Are you sure you want to delete %1%2?")
×
1279
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1280
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1281
    return;
1282
  }
1283

1284
  QtPassSettings::getPass()->Remove(file, isDir);
×
1285
}
×
1286

1287
/**
1288
 * @brief MainWindow::cancelOtpRequest forget the pending OTP and copy requests.
1289
 *
1290
 * Connected to Pass::processErrorExit, and called from deselect(). A failed
1291
 * decrypt never emits finishedShow; if the request stayed pending, the next
1292
 * successful decrypt of that same entry (the user clicking it again) would be
1293
 * taken as the answer and copy something the user no longer asked for.
1294
 */
1295
void MainWindow::cancelOtpRequest() {
12✔
1296
  m_otpRequestFile.clear();
12✔
1297
  m_copyRequestFile.clear();
12✔
1298
}
12✔
1299

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

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

1336
  // Fallback: no OTP row on screen, so decrypt once. The request file stops
1337
  // passShowHandler putting the password on the clipboard for a request that
1338
  // only asked for a code, and tells otpFromFileToClipboard which decrypt is
1339
  // its answer.
1340
  m_otpRequestFile = file;
3✔
1341
  setUiElementsEnabled(false);
3✔
1342
  // passShowHandler repaints the panel for this Show too, so keep the marker in
1343
  // step or a second request would decrypt again instead of taking the fast
1344
  // path. Safe to set now: executeWrapperStarted() clears the panel on every
1345
  // command, so a failed decrypt leaves currentOtpCode() empty.
1346
  m_shownFile = file;
3✔
1347
  QtPassSettings::getPass()->Show(file);
6✔
1348
}
1349

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

1358
/**
1359
 * @brief MainWindow::onUsers edit users for the current
1360
 * folder,
1361
 * gets lists and opens UserDialog.
1362
 */
1363
void MainWindow::onUsers() {
×
1364
  const QString dir = m_tree->currentDir(false);
×
NEW
1365
  if (refuseLinkedFolder(dir)) {
×
1366
    return;
1367
  }
1368

1369
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
1370
  if (!d.exec()) {
×
1371
    ui->treeView->setFocus();
×
1372
  }
1373
}
×
1374

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

1391
/**
1392
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1393
 * select a more appropriate one to view too
1394
 */
1395
void MainWindow::updateProfileBox() {
45✔
1396
  const Profiles profiles = QtPassSettings::getProfiles();
45✔
1397

1398
  if (profiles.isEmpty()) {
×
1399
    ui->profileWidget->hide();
45✔
1400
  } else {
1401
    ui->profileWidget->show();
×
1402
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1403
    ui->profileBox->clear();
×
1404
    for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
×
1405
      if (!i.key().isEmpty()) {
×
1406
        ui->profileBox->addItem(i.key());
×
1407
      }
1408
    }
1409
    ui->profileBox->model()->sort(0);
×
1410
  }
1411
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
90✔
1412
  if (index != -1) { //  -1 for not found
45✔
1413
    ui->profileBox->setCurrentIndex(index);
×
1414
  }
1415
}
45✔
1416

1417
/**
1418
 * @brief Handles changes to the selected profile in the profile combo box.
1419
 * @details Ignores the event during a fresh start or when the selected profile
1420
 * matches the current profile. Otherwise, it clears the password field, updates
1421
 * the active profile and related settings, refreshes the environment, and
1422
 * resets the tree view and action states to reflect the newly selected profile.
1423
 *
1424
 * @param name - The newly selected profile name.
1425
 * @return void - This function does not return a value.
1426
 *
1427
 */
1428
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1429
  if (m_freshStart || name == QtPassSettings::getProfile()) {
×
1430
    return;
×
1431
  }
1432

1433
  ui->lineEdit->clear();
×
1434

1435
  const Profile prof = QtPassSettings::getProfiles().value(name);
×
1436
  AppSettings s = QtPassSettings::load();
×
1437
  s.activeProfile = name;
×
1438
  s.passStore = prof.path;
×
1439
  s.passSigningKey = prof.signingKey;
×
1440
  // A profile without its own Git flags keeps the global values.
1441
  s.useGit = prof.useGit.value_or(s.useGit);
×
1442
  s.autoPush = prof.autoPush.value_or(s.autoPush);
×
1443
  s.autoPull = prof.autoPull.value_or(s.autoPull);
×
1444
  QtPassSettings::save(s);
×
1445
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1446

1447
  QtPassSettings::getPass()->updateEnv();
×
1448

1449
  m_tree->setStore(QtPassSettings::getPassStore());
×
1450
  deselect();
×
1451
  ui->treeView->setCurrentIndex(QModelIndex());
×
1452
}
×
1453

1454
/**
1455
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1456
 * support
1457
 * it
1458
 */
1459
void MainWindow::initTrayIcon() {
×
1460
  m_tray = new TrayIcon(this);
×
1461
  if (!m_tray->getIsAllocated()) {
×
1462
    destroyTrayIcon();
×
1463
  }
1464
}
×
1465

1466
/**
1467
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1468
 */
1469
void MainWindow::destroyTrayIcon() {
×
1470
  delete m_tray;
×
1471
  m_tray = nullptr;
×
1472
}
×
1473

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

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

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

1549
/**
1550
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1551
 * @param event
1552
 */
1553
void MainWindow::keyPressEvent(QKeyEvent *event) {
1✔
1554
  switch (event->key()) {
1✔
1555
  case Qt::Key_Delete:
×
1556
    onDelete();
×
1557
    break;
×
1558
  case Qt::Key_Return:
×
1559
  case Qt::Key_Enter:
1560
    if (m_tree->proxy().rowCount() > 0) {
×
1561
      on_treeView_clicked(ui->treeView->currentIndex());
×
1562
    }
1563
    break;
1564
  case Qt::Key_Escape:
×
1565
    ui->lineEdit->clear();
×
1566
    break;
×
1567
  default:
1568
    break;
1569
  }
1570
}
1✔
1571

1572
/**
1573
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1574
 * menu
1575
 * @param pos
1576
 */
1577
void MainWindow::showContextMenu(const QPoint &pos) {
×
1578
  const AppSettings s = QtPassSettings::load();
×
1579
  QModelIndex index = ui->treeView->indexAt(pos);
×
1580
  bool selected = true;
1581
  if (!index.isValid()) {
1582
    ui->treeView->clearSelection();
×
1583
    ui->actionDelete->setEnabled(false);
×
1584
    ui->actionEdit->setEnabled(false);
×
1585
    selected = false;
1586
  }
1587

1588
  ui->treeView->setCurrentIndex(index);
×
1589

1590
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1591

1592
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
×
1593

1594
  QMenu contextMenu;
×
1595
  if (!selected || fileOrFolder.isDir()) {
×
1596
    QAction *openFolder =
1597
        contextMenu.addAction(tr("Open folder with file manager"));
×
1598
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1599
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1600
    QAction *users = contextMenu.addAction(tr("Users"));
×
1601
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1602
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1603
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1604
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1605
  } else if (fileOrFolder.isFile()) {
×
1606
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1607
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1608
  }
1609
  if (selected) {
×
1610
    contextMenu.addSeparator();
×
1611
    if (fileOrFolder.isDir()) {
×
1612
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1613
      connect(renameFolder, &QAction::triggered, this,
×
1614
              &MainWindow::renameFolder);
×
1615
    } else if (fileOrFolder.isFile()) {
×
1616
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1617
      connect(renamePassword, &QAction::triggered, this,
×
1618
              &MainWindow::renamePassword);
×
1619
    }
1620
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1621
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1622
    if (fileOrFolder.isDir()) {
×
1623
      QString dirPath = QDir::cleanPath(m_tree->currentDir(false));
×
1624

1625
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1626
      contextMenu.addMenu(shareMenu);
×
1627

1628
      QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
×
1629
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1630

1631
      const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
×
1632
      bool gpgAvailable =
1633
          !exePath.isEmpty() &&
×
1634
          (Executor::parseWslCommand(exePath) || QFile(exePath).exists());
×
1635

1636
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1637
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1638
      connect(reencrypt, &QAction::triggered, this,
×
1639
              [this, dirPath]() { reencryptPath(dirPath); });
×
1640

1641
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1642
      exportKey->setEnabled(gpgAvailable);
×
1643
      connect(exportKey, &QAction::triggered, this,
×
1644
              &MainWindow::exportPublicKey);
×
1645

1646
      QAction *addRecipientAction =
1647
          shareMenu->addAction(tr("Add recipient..."));
×
1648
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1649
      connect(addRecipientAction, &QAction::triggered, this,
×
1650
              [this, dirPath]() { addRecipient(dirPath); });
×
1651

1652
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1653
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1654
    }
1655
  }
1656
  contextMenu.exec(globalPos);
×
1657
}
×
1658

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

1673
  contextMenu->exec(globalPos);
×
1674
  delete contextMenu;
×
1675
}
×
1676

1677
/**
1678
 * @brief MainWindow::openFolder open the folder in the default file manager
1679
 */
1680
void MainWindow::openFolder() {
×
1681
  QString dir = m_tree->currentDir(false);
×
1682

1683
  QString path = QDir::toNativeSeparators(dir);
×
1684
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1685
}
×
1686

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

1736
/**
1737
 * @brief MainWindow::renameFolder rename an existing folder
1738
 */
1739
void MainWindow::renameFolder() {
×
1740
  bool ok;
1741
  QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
×
1742
  QString srcDirName = QDir(srcDir).dirName();
×
1743
  QString newName =
1744
      QInputDialog::getText(this, tr("Rename folder"), tr("Rename folder to: "),
×
1745
                            QLineEdit::Normal, srcDirName, &ok);
×
1746
  if (!ok || newName.isEmpty()) {
×
1747
    return;
1748
  }
1749
  QString destDir = srcDir;
1750
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1751
  if (!confirmPathInStore(destDir)) {
×
1752
    return;
1753
  }
1754
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1755
}
1756

1757
/**
1758
 * @brief MainWindow::editPassword read password and open edit window via
1759
 * MainWindow::onEdit()
1760
 */
1761
void MainWindow::editPassword(const QString &file) {
1✔
1762
  if (!file.isEmpty()) {
1✔
1763
    const AppSettings s = QtPassSettings::load();
1✔
1764
    if (s.useGit && s.autoPull) {
1✔
1765
      onUpdate(true);
×
1766
    }
1767
    setPassword(file, false);
1✔
1768
  }
1✔
1769
}
1✔
1770

1771
/**
1772
 * @brief MainWindow::renamePassword rename an existing password
1773
 */
1774
void MainWindow::renamePassword() {
×
1775
  bool ok;
1776
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1777
  QString filePath = QFileInfo(file).path();
×
1778
  QString fileName = QFileInfo(file).fileName();
×
1779
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1780
    fileName.chop(4);
×
1781
  }
1782

1783
  QString newName =
1784
      QInputDialog::getText(this, tr("Rename file"), tr("Rename file to: "),
×
1785
                            QLineEdit::Normal, fileName, &ok);
×
1786
  if (!ok || newName.isEmpty()) {
×
1787
    return;
1788
  }
1789
  QString newFile = QDir(filePath).filePath(newName);
×
1790
  if (!confirmPathInStore(newFile)) {
×
1791
    return;
1792
  }
1793
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1794
}
1795

1796
/**
1797
 * @brief Copies the password of the selected file from the tree view to the
1798
 * clipboard.
1799
 * @example
1800
 * MainWindow::copyPasswordFromTreeview();
1801
 *
1802
 * @return void - This function does not return a value.
1803
 */
1804
void MainWindow::copyPasswordFromTreeview() {
4✔
1805
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
4✔
1806

1807
  if (fileOrFolder.isFile()) {
4✔
1808
    QString file = getFile(ui->treeView->currentIndex(), true);
4✔
1809
    // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
1810
    // an earlier request is simply not this one's.
1811
    m_copyRequestFile = file;
4✔
1812
    // This Show repaints the panel as well; see onOtp().
1813
    m_shownFile = file;
4✔
1814
    QtPassSettings::getPass()->Show(file);
8✔
1815
  }
1816
}
4✔
1817

1818
void MainWindow::passwordFromFileToClipboard(const QString &text,
9✔
1819
                                             const QString &file) {
1820
  if (m_copyRequestFile.isEmpty() || file != m_copyRequestFile) {
9✔
1821
    return;
7✔
1822
  }
1823
  m_copyRequestFile.clear();
3✔
1824
  const QStringList tokens = text.split('\n');
3✔
1825
  if (tokens.isEmpty()) {
3✔
1826
    return;
1827
  }
1828
  // An entry created by `pass otp insert` has the otpauth:// URI as its first
1829
  // line. That URI carries the shared TOTP secret (the seed, not a code), so
1830
  // copying it to the clipboard would leak 2FA material. Skip it, matching the
1831
  // display path (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1832
  if (FileContent::isOtpUriValue(tokens[0])) {
3✔
1833
    flashText(tr("This entry holds an OTP secret, not a password"), true);
1✔
1834
    return;
1✔
1835
  }
1836
  m_qtPass->clipboard().copyText(tokens[0]);
2✔
1837
}
1838

1839
/**
1840
 * @brief Displays message in status bar
1841
 *
1842
 * @param msg     text to be displayed
1843
 * @param timeout time for which msg shall be visible
1844
 */
1845
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
13✔
1846
  ui->statusBar->showMessage(msg, timeout);
13✔
1847
}
13✔
1848

1849
/**
1850
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1851
 * @param dir Directory path to re-encrypt
1852
 */
1853
void MainWindow::reencryptPath(const QString &dir) {
×
1854
  QDir checkDir(dir);
×
1855
  if (!checkDir.exists()) {
×
1856
    QMessageBox::critical(this, tr("Error"),
×
1857
                          tr("Directory does not exist: %1").arg(dir));
×
1858
    return;
×
1859
  }
NEW
1860
  if (refuseLinkedFolder(dir)) {
×
1861
    return;
1862
  }
1863

1864
  int ret = QMessageBox::question(
×
1865
      this, tr("Re-encrypt passwords"),
×
1866
      tr("Re-encrypt all passwords in %1?\n\n"
×
1867
         "This will re-encrypt ALL password files in this folder "
1868
         "using the current recipients defined in .gpg-id.\n\n"
1869
         "This may rewrite many files and cannot be undone easily.\n\n"
1870
         "Continue?")
1871
          .arg(QDir(dir).dirName()),
×
1872
      QMessageBox::Yes | QMessageBox::No);
1873

1874
  if (ret != QMessageBox::Yes)
×
1875
    return;
1876

1877
  // Disable preemptively. ImitatePass::reencryptPath emits
1878
  // startReencryptPath asynchronously and the slot would re-run this,
1879
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1880
  startReencryptPath();
×
1881

1882
  QtPassSettings::getImitatePass()->reencryptPath(
×
1883
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1884
}
×
1885

1886
/**
1887
 * @brief MainWindow::startReencryptPath disable ui elements and treeview and
1888
 * show the progress dialog. Idempotent: MainWindow::reencryptPath calls it
1889
 * before ImitatePass emits startReencryptPath.
1890
 */
1891
void MainWindow::startReencryptPath() {
3✔
1892
  m_reencryptRunning = true;
3✔
1893
  setUiElementsEnabled(false);
3✔
1894
  // The worker always ends with endReencryptPath(), and Cancel is available
1895
  // throughout, so the generic watchdog is not needed for this operation.
1896
  m_uiWatchdog.stop();
3✔
1897
  ui->treeView->setDisabled(true);
3✔
1898
  if (m_reencryptProgress)
3✔
1899
    return;
1900
  auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
2✔
1901
                                       tr("Cancel"), 0, 0, this);
4✔
1902
  progress->setWindowTitle(tr("Re-encrypt passwords"));
2✔
1903
  progress->setWindowModality(Qt::WindowModal);
2✔
1904
  progress->setAutoClose(false);
2✔
1905
  progress->setAutoReset(false);
2✔
1906
  progress->setMinimumDuration(0);
2✔
1907
  connect(progress, &QProgressDialog::canceled, this, [this]() {
2✔
1908
    QtPassSettings::getImitatePass()->cancelReencryptPath();
×
1909
    showStatusMessage(tr("Cancelling re-encryption"), 5000);
×
1910
  });
×
1911
  m_reencryptProgress = progress;
1912
  progress->show();
2✔
1913
}
1914

1915
/**
1916
 * @brief MainWindow::reencryptProgress show how many files were checked
1917
 * @param current files checked so far
1918
 * @param total files found under the folder
1919
 */
1920
void MainWindow::reencryptProgress(int current, int total) {
4✔
1921
  if (!m_reencryptProgress)
4✔
1922
    return;
1923
  m_reencryptProgress->setMaximum(total);
3✔
1924
  m_reencryptProgress->setLabelText(
3✔
1925
      tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
6✔
1926
  // On a modal QProgressDialog setValue() calls processEvents(), which can
1927
  // deliver the queued completion: endReencryptPath() then hides the dialog
1928
  // and resets m_reencryptProgress under our feet. Keep it last and touch
1929
  // nothing afterwards.
1930
  m_reencryptProgress->setValue(current);
3✔
1931
}
1932

1933
/**
1934
 * @brief MainWindow::endReencryptPath re-enable ui elements
1935
 */
1936
void MainWindow::endReencryptPath() {
2✔
1937
  m_reencryptRunning = false;
2✔
1938
  if (m_reencryptProgress) {
2✔
1939
    m_reencryptProgress->hide();
2✔
1940
    m_reencryptProgress->deleteLater();
2✔
1941
    m_reencryptProgress = nullptr;
1942
  }
1943
  setUiElementsEnabled(true);
2✔
1944
}
2✔
1945

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

1988
/**
1989
 * @brief MainWindow::addRecipient open the recipient management dialog for
1990
 *        the supplied directory.
1991
 * @param dir Folder whose .gpg-id should be edited.
1992
 *
1993
 * Delegates to UsersDialog so users can tick/untick keys from their
1994
 * keyring as recipients of the folder; importing a foreign key into the
1995
 * keyring still has to happen via gpg (or QtPass settings) first.
1996
 */
1997
void MainWindow::addRecipient(const QString &dir) {
×
NEW
1998
  if (refuseLinkedFolder(dir)) {
×
NEW
1999
    return;
×
2000
  }
2001
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
×
2002
  d.exec();
×
2003
}
×
2004

2005
/**
2006
 * @brief A folder picked in the tree that is a symlink or junction is not a
2007
 *        folder of the store: say so instead of re-encrypting, re-keying or
2008
 *        listing whatever it points to. The store root itself may be a link
2009
 *        (a synced folder); that is configuration, not a tree pick.
2010
 * @param dir Folder as the tree names it.
2011
 * @return true when the operation must not go ahead.
2012
 */
NEW
2013
auto MainWindow::refuseLinkedFolder(const QString &dir) -> bool {
×
NEW
2014
  if (!Util::isUnderLink(dir, QtPassSettings::load().passStore)) {
×
2015
    return false;
2016
  }
NEW
2017
  QMessageBox::critical(
×
NEW
2018
      this, tr("Not a folder of the store"),
×
NEW
2019
      tr("%1 is, or lies behind, a symbolic link or junction. What that "
×
2020
         "points to is not part of the password store and is left alone.")
NEW
2021
          .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
×
NEW
2022
  return true;
×
2023
}
2024

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

2045
void MainWindow::updateGitButtonVisibility(bool uiEnabled) {
96✔
2046
  const AppSettings s = QtPassSettings::load();
96✔
2047
  // Without Git there is nothing to push or pull, ever: the buttons go, not
2048
  // grey out. While an operation runs they stay and are disabled.
2049
  const bool usable =
2050
      s.useGit && !(s.gitExecutable.isEmpty() && s.passExecutable.isEmpty());
96✔
2051
  ui->actionPush->setVisible(usable);
96✔
2052
  ui->actionUpdate->setVisible(usable);
96✔
2053
  // The separator in front of them would otherwise sit next to the next one.
2054
  const QList<QAction *> actions = ui->toolBar->actions();
96✔
2055
  const qsizetype at = actions.indexOf(ui->actionPush);
96✔
2056
  if (at > 0 && actions.at(at - 1)->isSeparator()) {
96✔
2057
    actions.at(at - 1)->setVisible(usable);
96✔
2058
  }
2059
  enableGitButtons(usable && uiEnabled);
96✔
2060
}
96✔
2061

2062
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
96✔
2063
  // No platform gating any more: codes are generated in-process, so this works
2064
  // on Windows and macOS and with either backend. It used to be hidden there
2065
  // because the pass-otp extension is Unix-only.
2066
  //
2067
  // Kept visible but disabled when OTP is off, as it was before: hiding it
2068
  // removed the only discoverable trace of the feature, and since an OTP field
2069
  // is suppressed from the panel unconditionally (its value is a secret), a
2070
  // user with the setting off would see nothing at all where their OTP data
2071
  // used to be.
2072
  //
2073
  // Enablement must honour uiEnabled: this is called from
2074
  // setUiElementsEnabled(), and ignoring the argument re-enabled the action
2075
  // while a decrypt was in flight, letting the user stack Show calls.
2076
  ui->actionOtp->setVisible(true);
96✔
2077
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
96✔
2078
}
96✔
2079

2080
void MainWindow::updateGrepButtonVisibility() {
45✔
2081
  const bool enabled = QtPassSettings::isUseGrepSearch();
45✔
2082
  ui->grepButton->setVisible(enabled);
45✔
2083
  ui->grepCaseButton->setVisible(enabled);
45✔
2084
  if (!enabled && m_grep.inGrepMode()) {
45✔
2085
    ui->grepButton->setChecked(false);
×
2086
  }
2087
}
45✔
2088

2089
void MainWindow::enableGitButtons(const bool &state) {
96✔
2090
  ui->actionPush->setEnabled(state);
96✔
2091
  ui->actionUpdate->setEnabled(state);
96✔
2092
}
96✔
2093

2094
/**
2095
 * @brief MainWindow::critical critical message popup wrapper.
2096
 * @param title
2097
 * @param msg
2098
 */
2099
void MainWindow::critical(const QString &title, const QString &msg) {
×
2100
  QMessageBox::critical(this, title, msg);
×
2101
}
×
2102

2103
/**
2104
 * @brief Route a process's output to the console, unless the panel is
2105
 *        switched off in the settings.
2106
 * @param output The stdout/stderr text from the process.
2107
 * @param isError true if this is error output (stderr).
2108
 * @param pid The process that ran, for the line prefix.
2109
 */
2110
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
2111
                                 Enums::PROCESS pid) {
2112
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
2113
    return;
2114
  }
2115
  m_processOutput->append(output, isError,
1✔
2116
                          ProcessOutputPanel::processName(pid));
2✔
2117
}
2118

2119
/**
2120
 * @brief Show or hide the console per the showProcessOutput setting.
2121
 */
2122
void MainWindow::updateProcessOutputVisibility() {
1✔
2123
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
1✔
2124
}
1✔
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