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

IJHack / QtPass / 27500738735

14 Jun 2026 01:43PM UTC coverage: 57.418%. First build
27500738735

Pull #1535

github

web-flow
Merge fd7a5bb80 into 09848c3f0
Pull Request #1535: refactor: extract field rendering into PasswordDisplayPanel (#1512)

76 of 100 new or added lines in 2 files covered. (76.0%)

3971 of 6916 relevant lines covered (57.42%)

36.23 hits per line

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

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

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

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

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

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

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

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

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

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

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

89
  proxyModel.setModelAndStore(&model, passStore);
12✔
90
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
12✔
91

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

108
  if (QtPassSettings::isUseMonospace()) {
12✔
109
    QFont monospace("Monospace");
×
110
    monospace.setStyleHint(QFont::Monospace);
×
111
    ui->textBrowser->setFont(monospace);
×
112
  }
×
113
  if (QtPassSettings::isNoLineWrapping()) {
12✔
114
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
115
  }
116
  ui->textBrowser->setOpenExternalLinks(true);
12✔
117
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
12✔
118
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
12✔
119
          &MainWindow::showBrowserContextMenu);
12✔
120

121
  updateProfileBox();
12✔
122

123
  m_displayPanel = new PasswordDisplayPanel(
12✔
124
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
12✔
125
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested, m_qtPass,
12✔
126
          &QtPass::copyTextToClipboard);
12✔
127
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
12✔
128
          &QtPass::showTextAsQRCode);
12✔
129

130
  QtPassSettings::getPass()->updateEnv();
12✔
131
  clearPanelTimer.setInterval(MS_PER_SECOND *
12✔
132
                              QtPassSettings::getAutoclearPanelSeconds());
24✔
133
  clearPanelTimer.setSingleShot(true);
12✔
134
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
12✔
135

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

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

141
  initToolBarButtons();
12✔
142
  initStatusBar();
12✔
143
  initProcessOutputPanel();
12✔
144

145
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
12✔
146
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
24✔
147
            // Never route potentially-secret output through the panel:
148
            // - PASS_SHOW / PASS_OTP_GENERATE go via dedicated signals to
149
            //   the main text browser (which clears on a timer).
150
            // - PASS_GREP returns lines from password files; #252 must
151
            //   not leak those into a long-lived panel.
152
            // - PASS_INSERT's stdin is the password; stdout normally
153
            //   carries gpg/git progress only, but exclude defensively
154
            //   in case a future code path uses --echo or similar.
155
            if (isSensitiveProcess(pid)) {
×
156
              return;
157
            }
158
            if (!out.isEmpty()) {
×
159
              onProcessOutput(out, false, pid);
×
160
            }
161
            if (!err.isEmpty()) {
×
162
              onProcessOutput(err, true, pid);
×
163
            }
164
          });
165

166
  ui->lineEdit->setClearButtonEnabled(true);
12✔
167
  updateGrepButtonVisibility();
12✔
168

169
  setUiElementsEnabled(true);
12✔
170

171
  ui->lineEdit->setText(searchText);
12✔
172

173
  if (!m_qtPass->init()) {
12✔
174
    // no working config so this should just quit
175
    QApplication::quit();
×
176
    return;
177
  }
178

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

188
MainWindow::~MainWindow() { delete m_qtPass; }
36✔
189

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

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

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

267
/**
268
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
269
 */
270
void MainWindow::initToolBarButtons() {
12✔
271
  connect(ui->actionAddPassword, &QAction::triggered, this,
12✔
272
          &MainWindow::addPassword);
12✔
273
  connect(ui->actionAddFolder, &QAction::triggered, this,
12✔
274
          &MainWindow::addFolder);
12✔
275
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
12✔
276
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
12✔
277
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
12✔
278
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
12✔
279
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
12✔
280
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
12✔
281
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
12✔
282

283
  ui->actionAddPassword->setIcon(
12✔
284
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
48✔
285
  ui->actionAddFolder->setIcon(
12✔
286
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
48✔
287
  ui->actionEdit->setIcon(QIcon::fromTheme(
12✔
288
      "document-properties", QIcon(":/icons/document-properties.svg")));
36✔
289
  ui->actionDelete->setIcon(
12✔
290
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
48✔
291
  ui->actionPush->setIcon(
12✔
292
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
48✔
293
  ui->actionUpdate->setIcon(
12✔
294
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
48✔
295
  ui->actionUsers->setIcon(QIcon::fromTheme(
12✔
296
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
36✔
297
  ui->actionConfig->setIcon(QIcon::fromTheme(
12✔
298
      "applications-system", QIcon(":/icons/applications-system.svg")));
24✔
299
}
12✔
300

301
/**
302
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
303
 */
304
void MainWindow::initStatusBar() {
12✔
305
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
24✔
306

307
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
24✔
308
                     .scaledToHeight(statusBar()->height());
12✔
309
  auto *logoApp = new QLabel(statusBar());
12✔
310
  logoApp->setPixmap(logo);
12✔
311
  statusBar()->addPermanentWidget(logoApp);
12✔
312
}
12✔
313

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

343
  m_processOutputDock = new QDockWidget(tr("Process Output"), this);
12✔
344
  m_processOutputDock->setObjectName(QStringLiteral("processOutputDock"));
24✔
345
  m_processOutputDock->setFeatures(QDockWidget::DockWidgetMovable |
12✔
346
                                   QDockWidget::DockWidgetFloatable);
347
  m_processOutputDock->setAllowedAreas(Qt::BottomDockWidgetArea |
12✔
348
                                       Qt::TopDockWidgetArea);
349
  m_processOutputDock->setWidget(m_processOutputWidget);
12✔
350
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutputDock);
12✔
351
  // setVisible after addDockWidget so our explicit preference wins
352
  // even if QMainWindow applies any cached state when the dock is
353
  // attached. restoreWindow() runs before this method (it's called
354
  // from the QtPass ctor, which is constructed at the top of the
355
  // MainWindow ctor), so the saved layout has already been processed
356
  // by the time we get here.
357
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
12✔
358

359
  connect(m_clearOutputButton, &QToolButton::clicked, this,
12✔
360
          &MainWindow::on_clearOutputButton_clicked);
12✔
361

362
  // Hysteresis: while the user is actively dragging the slider, don't
363
  // touch m_autoScroll on every tick — a brief overshoot at maximum
364
  // would silently re-arm auto-scroll without an explicit release. Only
365
  // commit on slider release. Wheel/keyboard scroll never sets
366
  // isSliderDown(), so they still update immediately.
367
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
12✔
368
          this, [this]() {
12✔
369
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
370
            if (sb->isSliderDown())
×
371
              return;
372
            m_autoScroll = sb->value() >= sb->maximum();
×
373
          });
374
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderReleased,
12✔
375
          this, [this]() {
12✔
376
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
377
            m_autoScroll = sb->value() >= sb->maximum();
×
378
          });
×
379
}
12✔
380

381
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
382
  return ui->treeView->currentIndex();
×
383
}
384

385
void MainWindow::cleanKeygenDialog() {
1✔
386
  if (m_keygenDialog != nullptr) {
1✔
387
    m_keygenDialog->close();
×
388
  }
389
  m_keygenDialog = nullptr;
1✔
390
}
1✔
391

392
/**
393
 * @brief Displays the given text in the main window text browser, optionally
394
 * marking it as an error and/or rendering it as HTML.
395
 * @example
396
 * MainWindow window;
397
 * window.flashText("Operation completed.", false, false);
398
 *
399
 * @param const QString &text - The text content to display.
400
 * @param const bool isError - If true, sets the text color to red before
401
 * displaying the text.
402
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
403
 * the existing HTML content.
404
 * @return void - No return value.
405
 */
406
void MainWindow::flashText(const QString &text, const bool isError,
3✔
407
                           const bool isHtml) {
408
  if (isError) {
3✔
409
    ui->textBrowser->setTextColor(Qt::red);
1✔
410
  }
411

412
  if (isHtml) {
3✔
413
    QString _text = text;
414
    if (!ui->textBrowser->toPlainText().isEmpty()) {
2✔
415
      _text = ui->textBrowser->toHtml() + _text;
2✔
416
    }
417
    ui->textBrowser->setHtml(_text);
1✔
418
  } else {
419
    ui->textBrowser->setText(text);
2✔
420
  }
421
}
3✔
422

423
/**
424
 * @brief MainWindow::config pops up the configuration screen and handles all
425
 * inter-window communication
426
 */
427
void MainWindow::applyTextBrowserSettings() {
×
428
  if (QtPassSettings::isUseMonospace()) {
×
429
    QFont monospace("Monospace");
×
430
    monospace.setStyleHint(QFont::Monospace);
×
431
    ui->textBrowser->setFont(monospace);
×
432
  } else {
×
433
    ui->textBrowser->setFont(QFont());
×
434
  }
435

436
  if (QtPassSettings::isNoLineWrapping()) {
×
437
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
438
  } else {
439
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
440
  }
441
}
×
442

443
void MainWindow::applyWindowFlagsSettings() {
×
444
  if (QtPassSettings::isAlwaysOnTop()) {
×
445
    Qt::WindowFlags flags = windowFlags();
446
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
447
  } else {
448
    this->setWindowFlags(Qt::Window);
×
449
  }
450
  this->show();
×
451
}
×
452

453
/**
454
 * @brief Opens and processes the application configuration dialog, then applies
455
 * any accepted settings.
456
 * @example
457
 * config();
458
 *
459
 * @return void - This function does not return a value.
460
 */
461
void MainWindow::config() {
×
462
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
463
  d->setModal(true);
×
464
  // Automatically default to pass if it's available
465
  if (m_qtPass->isFreshStart() &&
×
466
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
467
    QtPassSettings::setUsePass(true);
×
468
  }
469

470
  if (m_qtPass->isFreshStart()) {
×
471
    d->wizard(); // run initial setup wizard for first-time configuration
×
472
  }
473
  if (d->exec()) {
×
474
    if (d->result() == QDialog::Accepted) {
×
475
      applyTextBrowserSettings();
×
476
      applyWindowFlagsSettings();
×
477

478
      updateProfileBox();
×
479
      const QString passStore = QtPassSettings::getPassStore();
×
480
      proxyModel.setStore(passStore);
×
481
      ui->treeView->setRootIndex(
×
482
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
483
      deselect();
×
484
      ui->treeView->setCurrentIndex(QModelIndex());
×
485

486
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
487
        config();
×
488
      }
489
      QtPassSettings::getPass()->updateEnv();
×
490
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
491
                                  QtPassSettings::getAutoclearPanelSeconds());
×
492
      m_qtPass->setClipboardTimer();
×
493

494
      updateGitButtonVisibility();
×
495
      updateOtpButtonVisibility();
×
496
      updateGrepButtonVisibility();
×
497
      updateProcessOutputVisibility();
×
498
      if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
×
499
        initTrayIcon();
×
500
      } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
×
501
        destroyTrayIcon();
×
502
      }
503
    }
504

505
    m_qtPass->setFreshStart(false);
×
506
  }
507
}
×
508

509
/**
510
 * @brief MainWindow::onUpdate do a git pull
511
 */
512
void MainWindow::onUpdate(bool block) {
×
513
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
514
  if (block) {
×
515
    QtPassSettings::getPass()->GitPull_b();
×
516
  } else {
517
    QtPassSettings::getPass()->GitPull();
×
518
  }
519
}
×
520

521
/**
522
 * @brief MainWindow::onPush do a git push
523
 */
524
void MainWindow::onPush() {
×
525
  if (QtPassSettings::isUseGit()) {
×
526
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
527
    QtPassSettings::getPass()->GitPush();
×
528
  }
529
}
×
530

531
/**
532
 * @brief MainWindow::getFile get the selected file path
533
 * @param index
534
 * @param forPass returns relative path without '.gpg' extension
535
 * @return path
536
 * @return
537
 */
538
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
539
  if (!index.isValid() ||
×
540
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
541
    return {};
542
  }
543
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
544
  if (forPass) {
×
545
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
546
    filePath.replace(Util::endsWithGpg(), "");
×
547
  }
548
  return filePath;
549
}
550

551
/**
552
 * @brief MainWindow::on_treeView_clicked read the selected password file
553
 * @param index
554
 */
555
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
556
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
557
  m_currentDir =
558
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
559
  // Clear any previously cached clipped text before showing new password
560
  m_qtPass->clearClippedText();
×
561
  QString file = getFile(index, true);
×
562
  ui->passwordName->setText(file);
×
563
  if (!file.isEmpty() && !cleared) {
×
564
    QtPassSettings::getPass()->Show(file);
×
565
  } else {
566
    clearPanel(false);
×
567
    ui->actionEdit->setEnabled(false);
×
568
    ui->actionDelete->setEnabled(true);
×
569
  }
570
}
×
571

572
/**
573
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
574
 * TreeViewItem, open the edit Window
575
 * @param index
576
 */
577
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
578
  QFileInfo fileOrFolder =
579
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
580

581
  if (fileOrFolder.isFile()) {
×
582
    editPassword(getFile(index, true));
×
583
  }
584
}
×
585

586
/**
587
 * @brief MainWindow::deselect clear the selection, password and copy buffer
588
 */
589
void MainWindow::deselect() {
1✔
590
  m_currentDir = "";
1✔
591
  m_qtPass->clearClipboard();
1✔
592
  ui->treeView->clearSelection();
1✔
593
  ui->actionEdit->setEnabled(false);
1✔
594
  ui->actionDelete->setEnabled(false);
1✔
595
  ui->passwordName->setText("");
1✔
596
  clearPanel(false);
1✔
597
}
1✔
598

599
void MainWindow::executeWrapperStarted() {
×
NEW
600
  m_displayPanel->clear();
×
601
  ui->textBrowser->clear();
×
602
  setUiElementsEnabled(false);
×
603
  clearPanelTimer.stop();
×
604
  if (QtPassSettings::isShowProcessOutput()) {
×
605
    m_processOutputDock->setVisible(true);
×
606
  }
607
}
×
608

609
/**
610
 * @brief Handles displaying parsed password entry content in the main window.
611
 * @example
612
 * void result = MainWindow::passShowHandler(p_output);
613
 * // Updates the UI with parsed fields and emits
614
 * passShowHandlerFinished(output)
615
 *
616
 * @param p_output - The raw output text containing the password entry data.
617
 * @return void - This function does not return a value.
618
 */
619
void MainWindow::passShowHandler(const QString &p_output) {
×
620
  QStringList templ = QtPassSettings::isUseTemplate()
×
621
                          ? QtPassSettings::getPassTemplate().split("\n")
×
622
                          : QStringList();
×
623
  bool allFields =
624
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
625
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
626
  QString output = p_output;
627
  QString password = fileContent.getPassword();
×
628

629
  // set clipped text
630
  m_qtPass->setClippedText(password, p_output);
×
631

632
  // first clear the current view:
NEW
633
  m_displayPanel->clear();
×
634

635
  // show what is needed:
636
  if (QtPassSettings::isHideContent()) {
×
637
    output = "***" + tr("Content hidden") + "***";
×
638
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
NEW
639
    m_displayPanel->displayFields(password, fileContent.getNamedValues());
×
640
    output = fileContent.getRemainingDataForDisplay();
×
641
  }
642

643
  if (QtPassSettings::isUseAutoclearPanel()) {
×
644
    clearPanelTimer.start();
×
645
  }
646

647
  emit passShowHandlerFinished(output);
×
648
  setUiElementsEnabled(true);
×
649
}
×
650

651
/**
652
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
653
 * and updating the UI state.
654
 * @example
655
 * void MainWindow::passOtpHandler(const QString &p_output);
656
 *
657
 * @param const QString &p_output - The OTP code text to process; if empty, an
658
 * error message is shown instead.
659
 * @return void - This function does not return a value.
660
 */
661
void MainWindow::passOtpHandler(const QString &p_output) {
×
662
  if (!p_output.isEmpty()) {
×
NEW
663
    m_displayPanel->appendField(tr("OTP Code"), p_output);
×
664
    m_qtPass->copyTextToClipboard(p_output);
×
665
    showStatusMessage(tr("OTP code copied to clipboard"));
×
666
  } else {
667
    flashText(tr("No OTP code found in this password entry"), true);
×
668
  }
669
  if (QtPassSettings::isUseAutoclearPanel()) {
×
670
    clearPanelTimer.start();
×
671
  }
672
  setUiElementsEnabled(true);
×
673
}
×
674

675
/**
676
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
677
 */
678
void MainWindow::clearPanel(bool notify) {
1✔
679
  m_displayPanel->clear();
1✔
680
  const bool grepWasVisible = ui->grepResultsList->isVisible();
1✔
681
  ui->grepResultsList->clear();
1✔
682
  if (grepWasVisible) {
1✔
683
    ui->grepResultsList->setVisible(false);
×
684
    ui->treeView->setVisible(true);
×
685
    if (m_grep.inGrepMode()) {
×
686
      m_grep.clearGrepMode();
687
      ui->grepButton->blockSignals(true);
×
688
      ui->grepButton->setChecked(false);
×
689
      ui->grepButton->blockSignals(false);
×
690
      ui->lineEdit->blockSignals(true);
×
691
      ui->lineEdit->clear();
×
692
      ui->lineEdit->blockSignals(false);
×
693
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
694
    }
695
  }
696
  if (notify) {
1✔
697
    QString output = "***" + tr("Password and Content hidden") + "***";
×
698
    ui->textBrowser->setHtml(output);
×
699
  } else {
700
    ui->textBrowser->setHtml("");
2✔
701
  }
702
}
1✔
703

704
/**
705
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
706
 * elements
707
 * @param state
708
 */
709
void MainWindow::setUiElementsEnabled(bool state) {
15✔
710
  ui->treeView->setEnabled(state);
15✔
711
  ui->lineEdit->setEnabled(state);
15✔
712
  ui->lineEdit->installEventFilter(this);
15✔
713
  ui->actionAddPassword->setEnabled(state);
15✔
714
  ui->actionAddFolder->setEnabled(state);
15✔
715
  ui->actionUsers->setEnabled(state);
15✔
716
  ui->actionConfig->setEnabled(state);
15✔
717
  // is a file selected?
718
  state &= ui->treeView->currentIndex().isValid();
30✔
719
  ui->actionDelete->setEnabled(state);
15✔
720
  ui->actionEdit->setEnabled(state);
15✔
721
  updateGitButtonVisibility();
15✔
722
  updateOtpButtonVisibility();
15✔
723
}
15✔
724

725
/**
726
 * @brief Restores the main window geometry, state, position, size, and
727
 * tray/icon settings from saved application settings.
728
 * @example
729
 * MainWindow window;
730
 * window.restoreWindow();
731
 *
732
 * @return void - This function does not return a value.
733
 */
734
void MainWindow::restoreWindow() {
12✔
735
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
12✔
736
  restoreGeometry(geometry);
12✔
737
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
12✔
738
  restoreState(savestate);
12✔
739
  QPoint position = QtPassSettings::getPos(pos());
12✔
740
  move(position);
12✔
741
  QSize newSize = QtPassSettings::getSize(size());
12✔
742
  resize(newSize);
12✔
743
  if (QtPassSettings::isMaximized(isMaximized())) {
12✔
744
    showMaximized();
×
745
  }
746

747
  if (QtPassSettings::isAlwaysOnTop()) {
12✔
748
    Qt::WindowFlags flags = windowFlags();
749
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
750
    show();
×
751
  }
752

753
  if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
24✔
754
    initTrayIcon();
×
755
    if (QtPassSettings::isStartMinimized()) {
×
756
      // since we are still in constructor, can't directly hide
757
      QTimer::singleShot(10, this, SLOT(hide()));
×
758
    }
759
  } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
24✔
760
    destroyTrayIcon();
×
761
  }
762
}
12✔
763

764
/**
765
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
766
 */
767
void MainWindow::onConfig() { config(); }
×
768

769
/**
770
 * @brief Executes when the string in the search box changes, collapses the
771
 * TreeView
772
 * @param arg1
773
 */
774
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
775
  if (m_grep.inGrepMode())
×
776
    return;
777
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
778
  ui->treeView->expandAll();
×
779
  clearPanel(false);
×
780
  ui->passwordName->setText("");
×
781
  ui->actionEdit->setEnabled(false);
×
782
  ui->actionDelete->setEnabled(false);
×
783
  searchTimer.start();
×
784
}
785

786
/**
787
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
788
 * time from two keypresses is elapsed
789
 */
790
void MainWindow::onTimeoutSearch() {
×
791
  QString query = ui->lineEdit->text();
×
792

793
  if (query.isEmpty()) {
×
794
    ui->treeView->collapseAll();
×
795
    deselect();
×
796
  }
797

798
  query.replace(QStringLiteral(" "), ".*");
×
799
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
800
  proxyModel.setFilterRegularExpression(regExp);
×
801
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
802
      model.setRootPath(QtPassSettings::getPassStore())));
×
803

804
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
805
    selectFirstFile();
×
806
  } else {
807
    ui->actionEdit->setEnabled(false);
×
808
    ui->actionDelete->setEnabled(false);
×
809
  }
810
}
×
811

812
/**
813
 * @brief MainWindow::on_lineEdit_returnPressed get searching
814
 *
815
 * Select the first possible file in the tree
816
 */
817
void MainWindow::on_lineEdit_returnPressed() {
×
818
#ifdef QT_DEBUG
819
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
820
#endif
821

822
  if (m_grep.inGrepMode()) {
×
823
    const QString query = ui->lineEdit->text();
×
824
    if (!query.isEmpty()) {
×
825
      ui->grepResultsList->clear();
×
826
      ui->statusBar->showMessage(tr("Searching…"));
×
827
      if (m_grep.beginSearch()) {
828
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
829
      }
830
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
831
    } else {
832
      if (m_grep.cancelSearch()) {
833
        QApplication::restoreOverrideCursor();
×
834
      }
835
      ui->grepResultsList->clear();
×
836
      ui->grepResultsList->setVisible(false);
×
837
      ui->treeView->setVisible(true);
×
838
    }
839
    return;
840
  }
841

842
  if (proxyModel.rowCount() > 0) {
×
843
    selectFirstFile();
×
844
    on_treeView_clicked(ui->treeView->currentIndex());
×
845
  }
846
}
847

848
/**
849
 * @brief Toggle grep (content search) mode.
850
 */
851
void MainWindow::on_grepButton_toggled(bool checked) {
×
852
  if (checked) {
×
853
    m_grep.enterGrepMode();
854
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
855
    // The regex dialect depends on the backend (see Pass::Grep): the pass
856
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
857
    ui->lineEdit->setToolTip(
×
858
        QtPassSettings::isUsePass()
×
859
            ? tr("Content search uses POSIX basic regular expressions "
×
860
                 "(pass grep).")
861
            : tr("Content search uses Perl-compatible regular expressions "
862
                 "(PCRE)."));
863
    ui->lineEdit->clear();
×
864
    searchTimer.stop();
×
865
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
866
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
867
        model.setRootPath(QtPassSettings::getPassStore())));
×
868
    ui->grepResultsList->setVisible(false);
×
869
    // Keep treeView visible until results arrive
870
  } else {
871
    if (m_grep.leaveGrepMode()) {
872
      QApplication::restoreOverrideCursor();
×
873
    }
874
    searchTimer.stop();
×
875
    ui->lineEdit->blockSignals(true);
×
876
    ui->lineEdit->clear();
×
877
    ui->lineEdit->blockSignals(false);
×
878
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
879
    ui->lineEdit->setToolTip(QString());
×
880
    ui->grepResultsList->clear();
×
881
    ui->grepResultsList->setVisible(false);
×
882
    ui->treeView->setVisible(true);
×
883
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
884
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
885
        model.setRootPath(QtPassSettings::getPassStore())));
×
886
  }
887
}
×
888

889
/**
890
 * @brief Display grep results in grepResultsList.
891
 */
892
void MainWindow::onGrepFinished(
×
893
    const QList<QPair<QString, QStringList>> &results) {
894
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
895
  if (outcome.restoreCursor) {
×
896
    QApplication::restoreOverrideCursor();
×
897
  }
898
  // Re-enable the UI before the discard check so a cancelled search can never
899
  // leave controls disabled.
900
  setUiElementsEnabled(true);
×
901
  if (outcome.discard) {
×
902
    return;
×
903
  }
904
  if (!m_grep.inGrepMode())
×
905
    return;
906
  ui->grepResultsList->clear();
×
907
  if (results.isEmpty()) {
×
908
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
909
    ui->grepResultsList->setVisible(false);
×
910
    ui->treeView->setVisible(true);
×
911
    return;
×
912
  }
913
  const bool hideContent = QtPassSettings::isHideContent();
×
914
  int totalLines = 0;
915
  for (const auto &pair : results) {
×
916
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
917
    entryItem->setText(0, pair.first);
×
918
    entryItem->setData(0, Qt::UserRole, pair.first);
×
919
    for (const QString &line : pair.second) {
×
920
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
921
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
922
                                       : line);
923
      lineItem->setData(0, Qt::UserRole, pair.first);
×
924
      ++totalLines;
×
925
    }
926
  }
927
  ui->grepResultsList->expandAll();
×
928
  ui->treeView->setVisible(false);
×
929
  ui->grepResultsList->setVisible(true);
×
930
  ui->statusBar->showMessage(
×
931
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
932
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
933
      3000);
934
  if (QtPassSettings::isUseAutoclearPanel())
×
935
    clearPanelTimer.start();
×
936
}
937

938
/**
939
 * @brief Navigate to the password entry when a grep result is clicked.
940
 */
941
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
942
                                                int /*column*/) {
943
  const QString entry = item->data(0, Qt::UserRole).toString();
×
944
  if (entry.isEmpty())
×
945
    return;
946
  const QString fullPath = QDir::cleanPath(
947
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
948
  QModelIndex srcIndex = model.index(fullPath);
×
949
  if (!srcIndex.isValid())
950
    return;
951
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
952
  if (!proxyIndex.isValid())
953
    return;
954
  ui->treeView->setCurrentIndex(proxyIndex);
×
955
  on_treeView_clicked(proxyIndex);
×
956
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
957
    ui->grepResultsList->clear();
×
958
  ui->grepResultsList->setVisible(false);
×
959
  ui->treeView->setVisible(true);
×
960
  ui->treeView->scrollTo(proxyIndex);
×
961
  ui->treeView->setFocus();
×
962
}
963

964
/**
965
 * @brief MainWindow::selectFirstFile select the first possible file in the
966
 * tree
967
 */
968
void MainWindow::selectFirstFile() {
×
969
  QModelIndex index = proxyModel.mapFromSource(
×
970
      model.setRootPath(QtPassSettings::getPassStore()));
×
971
  index = firstFile(index);
×
972
  ui->treeView->setCurrentIndex(index);
×
973
}
×
974

975
/**
976
 * @brief MainWindow::firstFile return location of first possible file
977
 * @param parentIndex
978
 * @return QModelIndex
979
 */
980
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
981
  QModelIndex index = parentIndex;
×
982
  int numRows = proxyModel.rowCount(parentIndex);
×
983
  for (int row = 0; row < numRows; ++row) {
×
984
    index = proxyModel.index(row, 0, parentIndex);
×
985
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
986
      return index;
×
987
    }
988
    if (proxyModel.hasChildren(index)) {
×
989
      return firstFile(index);
×
990
    }
991
  }
992
  return index;
×
993
}
994

995
/**
996
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
997
 * the password store and warn the user.
998
 *
999
 * Used before file/folder creation, move, and rename to stop user-typed
1000
 * names like "../../etc/passwd" or absolute paths from escaping the
1001
 * configured store root via the input dialogs.
1002
 *
1003
 * @param candidate Absolute candidate path to validate.
1004
 * @return true if the path is inside the password store; false otherwise (a
1005
 * warning dialog is shown in that case).
1006
 */
1007
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
1008
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1009
    return true;
1010
  }
1011
  QMessageBox::warning(this, tr("Invalid name"),
×
1012
                       tr("That name would resolve outside the password "
×
1013
                          "store. Please choose a different name."));
1014
  return false;
×
1015
}
1016

1017
/**
1018
 * @brief MainWindow::setPassword open passworddialog
1019
 * @param file which pgp file
1020
 * @param isNew insert (not update)
1021
 */
1022
void MainWindow::setPassword(const QString &file, bool isNew) {
×
1023
  PasswordDialog d(file, isNew, this);
×
1024

1025
  if (isNew) {
×
1026
    QString storePath = QtPassSettings::getPassStore();
×
1027
    QString folder =
1028
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1029
    if (folder.isEmpty()) {
×
1030
      folder = storePath;
×
1031
    }
1032
    QHash<QString, QStringList> templates =
1033
        TemplateIO::readTemplates(storePath);
×
1034
    if (!templates.isEmpty()) {
1035
      QString defaultTemplate =
1036
          TemplateIO::getFolderTemplate(folder, storePath);
×
1037
      d.setAvailableTemplates(templates, defaultTemplate);
×
1038
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
1039
                    [&d]() { d.cycleTemplate(); });
×
1040
    }
1041
  }
×
1042

1043
  if (!d.exec()) {
×
1044
    ui->treeView->setFocus();
×
1045
  }
1046
}
×
1047

1048
/**
1049
 * @brief MainWindow::addPassword add a new password by showing a
1050
 * number of dialogs.
1051
 */
1052
void MainWindow::addPassword() {
×
1053
  bool ok;
1054
  QString dir =
1055
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1056
  QString file =
1057
      QInputDialog::getText(this, tr("New file"),
×
1058
                            tr("New password file: \n(Will be placed in %1 )")
×
1059
                                .arg(QtPassSettings::getPassStore() +
×
1060
                                     Util::getDir(ui->treeView->currentIndex(),
×
1061
                                                  true, model, proxyModel)),
1062
                            QLineEdit::Normal, "", &ok);
×
1063
  if (!ok || file.isEmpty()) {
×
1064
    return;
1065
  }
1066
  file = dir + file;
×
1067
  if (!confirmPathInStore(QtPassSettings::getPassStore() + file)) {
×
1068
    return;
1069
  }
1070
  setPassword(file);
×
1071
}
1072

1073
/**
1074
 * @brief MainWindow::onDelete remove password, if you are
1075
 * sure.
1076
 */
1077
void MainWindow::onDelete() {
×
1078
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1079
  if (!currentIndex.isValid()) {
1080
    // This fixes https://github.com/IJHack/QtPass/issues/556
1081
    // Otherwise the entire password directory would be deleted if
1082
    // nothing is selected in the tree view.
1083
    return;
×
1084
  }
1085

1086
  QFileInfo fileOrFolder =
1087
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1088
  QString file = "";
×
1089
  bool isDir = false;
1090

1091
  if (fileOrFolder.isFile()) {
×
1092
    file = getFile(ui->treeView->currentIndex(), true);
×
1093
  } else {
1094
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1095
    isDir = true;
1096
  }
1097

1098
  QString dirMessage = tr(" and the whole content?");
1099
  if (isDir) {
×
1100
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1101
                    QDirIterator::Subdirectories);
×
1102
    bool okDir = true;
1103
    while (it.hasNext() && okDir) {
×
1104
      it.next();
×
1105
      if (QFileInfo(it.filePath()).isFile()) {
×
1106
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1107
          okDir = false;
1108
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1109
                          "there are unexpected files in the given folder, "
1110
                          "check them before continue.</strong>");
1111
        }
1112
      }
1113
    }
1114
  }
×
1115

1116
  if (QMessageBox::question(
×
1117
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1118
          tr("Are you sure you want to delete %1%2?")
×
1119
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1120
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1121
    return;
1122
  }
1123

1124
  QtPassSettings::getPass()->Remove(file, isDir);
×
1125
}
×
1126

1127
/**
1128
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
1129
 */
1130
void MainWindow::onOtp() {
×
1131
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1132
  if (!file.isEmpty()) {
×
1133
    if (QtPassSettings::isUseOtp()) {
×
1134
      setUiElementsEnabled(false);
×
1135
      QtPassSettings::getPass()->OtpGenerate(file);
×
1136
    }
1137
  } else {
1138
    flashText(tr("No password selected for OTP generation"), true);
×
1139
  }
1140
}
×
1141

1142
/**
1143
 * @brief MainWindow::onEdit try and edit (selected) password.
1144
 */
1145
void MainWindow::onEdit() {
×
1146
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1147
  editPassword(file);
×
1148
}
×
1149

1150
/**
1151
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1152
 * @param dir folder to edit users for.
1153
 */
1154
void MainWindow::userDialog(const QString &dir) {
×
1155
  if (!dir.isEmpty()) {
×
1156
    m_currentDir = dir;
×
1157
  }
1158
  onUsers();
×
1159
}
×
1160

1161
/**
1162
 * @brief MainWindow::onUsers edit users for the current
1163
 * folder,
1164
 * gets lists and opens UserDialog.
1165
 */
1166
void MainWindow::onUsers() {
×
1167
  QString dir =
1168
      m_currentDir.isEmpty()
1169
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
1170
          : m_currentDir;
×
1171

1172
  UsersDialog d(dir, this);
×
1173
  if (!d.exec()) {
×
1174
    ui->treeView->setFocus();
×
1175
  }
1176
}
×
1177

1178
/**
1179
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1180
 * @param message
1181
 */
1182
void MainWindow::messageAvailable(const QString &message) {
×
1183
  show();
×
1184
  raise();
×
1185
  if (message.isEmpty()) {
×
1186
    focusInput();
×
1187
  } else {
1188
    ui->treeView->expandAll();
×
1189
    ui->lineEdit->setText(message);
×
1190
    on_lineEdit_returnPressed();
×
1191
  }
1192
}
×
1193

1194
/**
1195
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1196
 * @param batch
1197
 * @param keygenWindow
1198
 */
1199
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1200
  m_keygenDialog = keygenWindow;
×
1201
  emit generateGPGKeyPair(batch);
×
1202
}
×
1203

1204
/**
1205
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1206
 * select a more appropriate one to view too
1207
 */
1208
void MainWindow::updateProfileBox() {
12✔
1209
  QHash<QString, QHash<QString, QString>> profiles =
1210
      QtPassSettings::getProfiles();
12✔
1211

1212
  if (profiles.isEmpty()) {
1213
    ui->profileWidget->hide();
×
1214
  } else {
1215
    ui->profileWidget->show();
12✔
1216
    ui->profileBox->setEnabled(profiles.size() > 1);
24✔
1217
    ui->profileBox->clear();
12✔
1218
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
12✔
1219
    while (i.hasNext()) {
12✔
1220
      i.next();
1221
      if (!i.key().isEmpty()) {
12✔
1222
        ui->profileBox->addItem(i.key());
12✔
1223
      }
1224
    }
1225
    ui->profileBox->model()->sort(0);
12✔
1226
  }
1227
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
24✔
1228
  if (index != -1) { //  -1 for not found
12✔
1229
    ui->profileBox->setCurrentIndex(index);
×
1230
  }
1231
}
12✔
1232

1233
/**
1234
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1235
 * correct "profile"
1236
 * @param name
1237
 */
1238
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1239
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1240
#else
1241
/**
1242
 * @brief Handles changes to the selected profile in the profile combo box.
1243
 * @details Ignores the event during a fresh start or when the selected profile
1244
 * matches the current profile. Otherwise, it clears the password field, updates
1245
 * the active profile and related settings, refreshes the environment, and
1246
 * resets the tree view and action states to reflect the newly selected profile.
1247
 *
1248
 * @param name - The newly selected profile name.
1249
 * @return void - This function does not return a value.
1250
 *
1251
 */
1252
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
12✔
1253
#endif
1254
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
12✔
1255
    return;
12✔
1256
  }
1257

1258
  ui->lineEdit->clear();
×
1259

1260
  QtPassSettings::setProfile(name);
×
1261

1262
  QtPassSettings::setPassStore(
×
1263
      QtPassSettings::getProfiles().value(name).value("path"));
×
1264
  QtPassSettings::setPassSigningKey(
×
1265
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1266
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1267

1268
  QtPassSettings::getPass()->updateEnv();
×
1269

1270
  const QString passStore = QtPassSettings::getPassStore();
×
1271
  proxyModel.setStore(passStore);
×
1272
  ui->treeView->setRootIndex(
×
1273
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1274
  deselect();
×
1275
  ui->treeView->setCurrentIndex(QModelIndex());
×
1276
}
1277

1278
/**
1279
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1280
 * support
1281
 * it
1282
 */
1283
void MainWindow::initTrayIcon() {
×
1284
  m_tray = new TrayIcon(this);
×
1285
  // Setup tray icon
1286

1287
  if (m_tray == nullptr) {
1288
#ifdef QT_DEBUG
1289
    dbg() << "Allocating tray icon failed.";
1290
#endif
1291
    return;
1292
  }
1293

1294
  if (!m_tray->getIsAllocated()) {
×
1295
    destroyTrayIcon();
×
1296
  }
1297
}
1298

1299
/**
1300
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1301
 */
1302
void MainWindow::destroyTrayIcon() {
×
1303
  delete m_tray;
×
1304
  m_tray = nullptr;
×
1305
}
×
1306

1307
/**
1308
 * @brief MainWindow::closeEvent hide or quit
1309
 * @param event
1310
 */
1311
void MainWindow::closeEvent(QCloseEvent *event) {
×
1312
  if (QtPassSettings::isHideOnClose()) {
×
1313
    this->hide();
×
1314
    event->ignore();
1315
  } else {
1316
    m_qtPass->clearClipboard();
×
1317

1318
    QtPassSettings::setGeometry(saveGeometry());
×
1319
    QtPassSettings::setSavestate(saveState());
×
1320
    QtPassSettings::setMaximized(isMaximized());
×
1321
    if (!isMaximized()) {
×
1322
      QtPassSettings::setPos(pos());
×
1323
      QtPassSettings::setSize(size());
×
1324
    }
1325
    event->accept();
1326
  }
1327
}
×
1328

1329
/**
1330
 * @brief MainWindow::eventFilter filter out some events and focus the
1331
 * treeview
1332
 * @param obj
1333
 * @param event
1334
 * @return
1335
 */
1336
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
3✔
1337
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
3✔
1338
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1339
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1340
      ui->treeView->setFocus();
×
1341
    }
1342
  }
1343
  return QObject::eventFilter(obj, event);
3✔
1344
}
1345

1346
/**
1347
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1348
 * @param event
1349
 */
1350
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1351
  switch (event->key()) {
×
1352
  case Qt::Key_Delete:
×
1353
    onDelete();
×
1354
    break;
×
1355
  case Qt::Key_Return:
×
1356
  case Qt::Key_Enter:
1357
    if (proxyModel.rowCount() > 0) {
×
1358
      on_treeView_clicked(ui->treeView->currentIndex());
×
1359
    }
1360
    break;
1361
  case Qt::Key_Escape:
×
1362
    ui->lineEdit->clear();
×
1363
    break;
×
1364
  default:
1365
    break;
1366
  }
1367
}
×
1368

1369
/**
1370
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1371
 * menu
1372
 * @param pos
1373
 */
1374
void MainWindow::showContextMenu(const QPoint &pos) {
×
1375
  QModelIndex index = ui->treeView->indexAt(pos);
×
1376
  bool selected = true;
1377
  if (!index.isValid()) {
1378
    ui->treeView->clearSelection();
×
1379
    ui->actionDelete->setEnabled(false);
×
1380
    ui->actionEdit->setEnabled(false);
×
1381
    m_currentDir = "";
×
1382
    selected = false;
1383
  }
1384

1385
  ui->treeView->setCurrentIndex(index);
×
1386

1387
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1388

1389
  QFileInfo fileOrFolder =
1390
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1391

1392
  QMenu contextMenu;
×
1393
  if (!selected || fileOrFolder.isDir()) {
×
1394
    QAction *openFolder =
1395
        contextMenu.addAction(tr("Open folder with file manager"));
×
1396
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1397
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1398
    QAction *users = contextMenu.addAction(tr("Users"));
×
1399
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1400
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1401
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1402
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1403
  } else if (fileOrFolder.isFile()) {
×
1404
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1405
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1406
  }
1407
  if (selected) {
×
1408
    contextMenu.addSeparator();
×
1409
    if (fileOrFolder.isDir()) {
×
1410
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1411
      connect(renameFolder, &QAction::triggered, this,
×
1412
              &MainWindow::renameFolder);
×
1413
    } else if (fileOrFolder.isFile()) {
×
1414
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1415
      connect(renamePassword, &QAction::triggered, this,
×
1416
              &MainWindow::renamePassword);
×
1417
    }
1418
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1419
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1420
    if (fileOrFolder.isDir()) {
×
1421
      QString dirPath = QDir::cleanPath(
1422
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1423

1424
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1425
      contextMenu.addMenu(shareMenu);
×
1426

1427
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1428
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1429

1430
      QString exePath = QtPassSettings::isUsePass()
×
1431
                            ? QtPassSettings::getPassExecutable()
×
1432
                            : QtPassSettings::getGpgExecutable();
×
1433
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1434
                                                 QFile(exePath).exists());
×
1435

1436
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1437
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1438
      connect(reencrypt, &QAction::triggered, this,
×
1439
              [this, dirPath]() { reencryptPath(dirPath); });
×
1440

1441
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1442
      exportKey->setEnabled(gpgAvailable);
×
1443
      connect(exportKey, &QAction::triggered, this,
×
1444
              &MainWindow::exportPublicKey);
×
1445

1446
      QAction *addRecipientAction =
1447
          shareMenu->addAction(tr("Add recipient..."));
×
1448
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1449
      connect(addRecipientAction, &QAction::triggered, this,
×
1450
              [this, dirPath]() { addRecipient(dirPath); });
×
1451

1452
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1453
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1454
    }
1455
  }
1456
  contextMenu.exec(globalPos);
×
1457
}
×
1458

1459
/**
1460
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1461
 * password window
1462
 * @param pos
1463
 */
1464
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1465
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1466
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1467

1468
  contextMenu->exec(globalPos);
×
1469
  delete contextMenu;
×
1470
}
×
1471

1472
/**
1473
 * @brief MainWindow::openFolder open the folder in the default file manager
1474
 */
1475
void MainWindow::openFolder() {
×
1476
  QString dir =
1477
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1478

1479
  QString path = QDir::toNativeSeparators(dir);
×
1480
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1481
}
×
1482

1483
/**
1484
 * @brief MainWindow::addFolder add a new folder to store passwords in
1485
 */
1486
void MainWindow::addFolder() {
×
1487
  bool ok;
1488
  QString dir =
1489
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1490
  QString newdir =
1491
      QInputDialog::getText(this, tr("New file"),
×
1492
                            tr("New Folder: \n(Will be placed in %1 )")
×
1493
                                .arg(QtPassSettings::getPassStore() +
×
1494
                                     Util::getDir(ui->treeView->currentIndex(),
×
1495
                                                  true, model, proxyModel)),
1496
                            QLineEdit::Normal, "", &ok);
×
1497
  if (!ok || newdir.isEmpty()) {
×
1498
    return;
1499
  }
1500
  newdir.prepend(dir);
1501
  if (!confirmPathInStore(newdir)) {
×
1502
    return;
1503
  }
1504
  if (!QDir().mkdir(newdir)) {
×
1505
    QMessageBox::warning(this, tr("Error"),
×
1506
                         tr("Failed to create folder: %1").arg(newdir));
×
1507
    return;
×
1508
  }
1509
  if (QtPassSettings::isAddGPGId(true)) {
×
1510
    QString gpgIdFile = newdir + "/.gpg-id";
×
1511
    QFile gpgId(gpgIdFile);
×
1512
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1513
      QMessageBox::warning(
×
1514
          this, tr("Error"),
×
1515
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1516
      return;
1517
    }
1518
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1519
    for (const UserInfo &user : users) {
×
1520
      if (user.enabled) {
×
1521
        gpgId.write((user.key_id + "\n").toUtf8());
×
1522
      }
1523
    }
1524
    gpgId.close();
×
1525
    // Lock to owner-only access; see ImitatePass::writeGpgIdFile for
1526
    // rationale (NFS / USB / unusual umask scenarios). Best-effort on
1527
    // platforms where setPermissions is a no-op.
1528
    QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
×
1529
  }
×
1530
}
1531

1532
/**
1533
 * @brief MainWindow::renameFolder rename an existing folder
1534
 */
1535
void MainWindow::renameFolder() {
×
1536
  bool ok;
1537
  QString srcDir = QDir::cleanPath(
1538
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1539
  QString srcDirName = QDir(srcDir).dirName();
×
1540
  QString newName =
1541
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1542
                            QLineEdit::Normal, srcDirName, &ok);
×
1543
  if (!ok || newName.isEmpty()) {
×
1544
    return;
1545
  }
1546
  QString destDir = srcDir;
1547
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1548
  if (!confirmPathInStore(destDir)) {
×
1549
    return;
1550
  }
1551
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
×
1552
}
1553

1554
/**
1555
 * @brief MainWindow::editPassword read password and open edit window via
1556
 * MainWindow::onEdit()
1557
 */
1558
void MainWindow::editPassword(const QString &file) {
×
1559
  if (!file.isEmpty()) {
×
1560
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1561
      onUpdate(true);
×
1562
    }
1563
    setPassword(file, false);
×
1564
  }
1565
}
×
1566

1567
/**
1568
 * @brief MainWindow::renamePassword rename an existing password
1569
 */
1570
void MainWindow::renamePassword() {
×
1571
  bool ok;
1572
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1573
  QString filePath = QFileInfo(file).path();
×
1574
  QString fileName = QFileInfo(file).fileName();
×
1575
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1576
    fileName.chop(4);
×
1577
  }
1578

1579
  QString newName =
1580
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1581
                            QLineEdit::Normal, fileName, &ok);
×
1582
  if (!ok || newName.isEmpty()) {
×
1583
    return;
1584
  }
1585
  QString newFile = QDir(filePath).filePath(newName);
×
1586
  if (!confirmPathInStore(newFile)) {
×
1587
    return;
1588
  }
1589
  QtPassSettings::getPass()->Move(file, newFile, false);
×
1590
}
1591

1592
/**
1593
 * @brief Copies the password of the selected file from the tree view to the
1594
 * clipboard.
1595
 * @example
1596
 * MainWindow::copyPasswordFromTreeview();
1597
 *
1598
 * @return void - This function does not return a value.
1599
 */
1600
void MainWindow::copyPasswordFromTreeview() {
×
1601
  QFileInfo fileOrFolder =
1602
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1603

1604
  if (fileOrFolder.isFile()) {
×
1605
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1606
    // Disconnect any previous connection to avoid accumulation
1607
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1608
               &MainWindow::passwordFromFileToClipboard);
1609
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1610
            &MainWindow::passwordFromFileToClipboard);
×
1611
    QtPassSettings::getPass()->Show(file);
×
1612
  }
1613
}
×
1614

1615
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1616
  QStringList tokens = text.split('\n');
×
1617
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1618
}
×
1619

1620
/**
1621
 * @brief Displays message in status bar
1622
 *
1623
 * @param msg     text to be displayed
1624
 * @param timeout time for which msg shall be visible
1625
 */
1626
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
2✔
1627
  ui->statusBar->showMessage(msg, timeout);
2✔
1628
}
2✔
1629

1630
/**
1631
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1632
 * @param dir Directory path to re-encrypt
1633
 */
1634
void MainWindow::reencryptPath(const QString &dir) {
×
1635
  QDir checkDir(dir);
×
1636
  if (!checkDir.exists()) {
×
1637
    QMessageBox::critical(this, tr("Error"),
×
1638
                          tr("Directory does not exist: %1").arg(dir));
×
1639
    return;
×
1640
  }
1641

1642
  int ret = QMessageBox::question(
×
1643
      this, tr("Re-encrypt passwords"),
×
1644
      tr("Re-encrypt all passwords in %1?\n\n"
×
1645
         "This will re-encrypt ALL password files in this folder "
1646
         "using the current recipients defined in .gpg-id.\n\n"
1647
         "This may rewrite many files and cannot be undone easily.\n\n"
1648
         "Continue?")
1649
          .arg(QDir(dir).dirName()),
×
1650
      QMessageBox::Yes | QMessageBox::No);
1651

1652
  if (ret != QMessageBox::Yes)
×
1653
    return;
1654

1655
  // Disable preemptively. ImitatePass::reencryptPath emits
1656
  // startReencryptPath asynchronously and the slot would re-run this,
1657
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1658
  startReencryptPath();
×
1659

1660
  QtPassSettings::getImitatePass()->reencryptPath(
×
1661
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1662
}
×
1663

1664
/**
1665
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1666
 */
1667
void MainWindow::startReencryptPath() {
×
1668
  setUiElementsEnabled(false);
×
1669
  ui->treeView->setDisabled(true);
×
1670
}
×
1671

1672
/**
1673
 * @brief MainWindow::endReencryptPath re-enable ui elements
1674
 */
1675
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1676

1677
/**
1678
 * @brief MainWindow::exportPublicKey export the configured signing key in
1679
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1680
 *
1681
 * Falls back to a help dialog when no signing key is configured or gpg is
1682
 * unavailable, so the user still gets actionable guidance.
1683
 */
1684
void MainWindow::exportPublicKey() {
×
1685
  QString identity = QtPassSettings::getPassSigningKey();
×
1686
  if (identity.isEmpty()) {
×
1687
    QMessageBox::information(
×
1688
        this, tr("Export Public Key"),
×
1689
        tr("<h3>Export Your Public Key</h3>"
×
1690
           "<p>No signing key is configured. Set one in QtPass Settings "
1691
           "&gt; GPG keys, or run this in a terminal:</p>"
1692
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1693
           "&gt;</pre>"
1694
           "<p>Then send the file to your teammates.</p>"));
1695
    return;
×
1696
  }
1697
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
1698
  if (gpgExe.isEmpty()) {
×
1699
    gpgExe = QStringLiteral("gpg");
×
1700
  }
1701
  QStringList args = {"--armor", "--export"};
×
1702
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1703
  QString stdOut;
×
1704
  QString stdErr;
×
1705
  int exitCode =
1706
      Executor::executeBlocking(gpgExe, args, QString(), &stdOut, &stdErr);
×
1707
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1708
    QMessageBox::warning(this, tr("Export Public Key"),
×
1709
                         tr("Could not export public key for %1.\n\n%2")
×
1710
                             .arg(identity, stdErr.isEmpty()
×
1711
                                                ? tr("No output from gpg.")
×
1712
                                                : stdErr));
1713
    return;
1714
  }
1715
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1716
  dialog.exec();
×
1717
}
×
1718

1719
/**
1720
 * @brief MainWindow::addRecipient open the recipient management dialog for
1721
 *        the supplied directory.
1722
 * @param dir Folder whose .gpg-id should be edited.
1723
 *
1724
 * Delegates to UsersDialog so users can tick/untick keys from their
1725
 * keyring as recipients of the folder; importing a foreign key into the
1726
 * keyring still has to happen via gpg (or QtPass settings) first.
1727
 */
1728
void MainWindow::addRecipient(const QString &dir) {
×
1729
  UsersDialog d(dir, this);
×
1730
  d.exec();
×
1731
}
×
1732

1733
/**
1734
 * @brief MainWindow::showShareHelp show help about GPG sharing
1735
 */
1736
void MainWindow::showShareHelp() {
×
1737
  QMessageBox::information(
×
1738
      this, tr("Sharing Passwords with GPG"),
×
1739
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1740
         "<p>To share passwords with other users:</p>"
1741
         "<ol>"
1742
         "<li><b>Export your public key</b> and send it to teammates</li>"
1743
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1744
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1745
         "them</li>"
1746
         "</ol>"
1747
         "<p>Only people who have a matching secret key can decrypt the "
1748
         "passwords.</p>"
1749
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1750
         "<p>See the FAQ for more details.</p>"));
1751
}
×
1752

1753
void MainWindow::updateGitButtonVisibility() {
15✔
1754
  if (!QtPassSettings::isUseGit() ||
30✔
1755
      (QtPassSettings::getGitExecutable().isEmpty() &&
15✔
1756
       QtPassSettings::getPassExecutable().isEmpty())) {
15✔
1757
    enableGitButtons(false);
15✔
1758
  } else {
1759
    enableGitButtons(true);
×
1760
  }
1761
}
15✔
1762

1763
void MainWindow::updateOtpButtonVisibility() {
15✔
1764
#if defined(Q_OS_WIN) || defined(__APPLE__)
1765
  ui->actionOtp->setVisible(false);
1766
#endif
1767
  if (!QtPassSettings::isUseOtp()) {
15✔
1768
    ui->actionOtp->setEnabled(false);
15✔
1769
  } else {
1770
    ui->actionOtp->setEnabled(true);
×
1771
  }
1772
}
15✔
1773

1774
void MainWindow::updateGrepButtonVisibility() {
12✔
1775
  const bool enabled = QtPassSettings::isUseGrepSearch();
12✔
1776
  ui->grepButton->setVisible(enabled);
12✔
1777
  ui->grepCaseButton->setVisible(enabled);
12✔
1778
  if (!enabled && m_grep.inGrepMode()) {
12✔
1779
    ui->grepButton->setChecked(false);
×
1780
  }
1781
}
12✔
1782

1783
void MainWindow::enableGitButtons(const bool &state) {
15✔
1784
  // Following GNOME guidelines is preferable disable buttons instead of hide
1785
  ui->actionPush->setEnabled(state);
15✔
1786
  ui->actionUpdate->setEnabled(state);
15✔
1787
}
15✔
1788

1789
/**
1790
 * @brief MainWindow::critical critical message popup wrapper.
1791
 * @param title
1792
 * @param msg
1793
 */
1794
void MainWindow::critical(const QString &title, const QString &msg) {
×
1795
  QMessageBox::critical(this, title, msg);
×
1796
}
×
1797

1798
/**
1799
 * @brief Appends processed command output to the output panel.
1800
 *
1801
 * Appends text to the process output text edit, with per-line numbering,
1802
 * optional command prefix, and color coding for errors vs. success.
1803
 * Handles auto-scrolling and line limits.
1804
 *
1805
 * @param output The raw output text from the command.
1806
 * @param isError true if this is error output (stderr).
1807
 * @param linePrefix Optional command name to prefix each line with.
1808
 */
1809
void MainWindow::appendProcessOutput(const QString &output, bool isError,
2✔
1810
                                     const QString &linePrefix) {
1811
  if (!QtPassSettings::isShowProcessOutput()) {
2✔
1812
    return;
1✔
1813
  }
1814

1815
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
1✔
1816
  for (QString &line : lines) {
2✔
1817
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1818
    // indentation
1819
    line.remove('\r');
1✔
1820
    while (!line.isEmpty() && line.back().isSpace()) {
2✔
1821
      line.chop(1);
×
1822
    }
1823
    if (line.isEmpty()) {
1✔
1824
      continue;
×
1825
    }
1826

1827
    m_outputCounter++;
1✔
1828
    QString lineNumber = QString::number(m_outputCounter);
1✔
1829

1830
    QColor textColor =
1831
        isError ? QColor(Qt::red)
1✔
1832
                : m_processOutputEdit->palette().color(QPalette::Text);
2✔
1833
    QString colorHex = textColor.name();
1✔
1834
    // Apply the optional prefix per line so multi-line output stays
1835
    // attributed to its command (e.g. all 3 lines of a `git push` show
1836
    // "git push: ..." rather than only the first).
1837
    QString prefixed =
1838
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
2✔
1839
    QString coloredOutput =
1840
        QString("<span style=\"color: %1;\">%2: %3</span>")
1✔
1841
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
2✔
1842

1843
    m_processOutputEdit->append(coloredOutput);
1✔
1844
  }
1845

1846
  limitOutputLines();
1✔
1847

1848
  if (m_autoScroll) {
1✔
1849
    m_processOutputEdit->verticalScrollBar()->setValue(
2✔
1850
        m_processOutputEdit->verticalScrollBar()->maximum());
1✔
1851
  }
1852
}
1853

1854
/**
1855
 * @brief Handles process output from the Pass executor.
1856
 *
1857
 * Called when any non-sensitive process completes. Filters out password-
1858
 * related commands (pass show, insert, etc.) and delegates to
1859
 * appendProcessOutput.
1860
 *
1861
 * @param output The stdout/stderr text from the process.
1862
 * @param isError true if this is error output (stderr).
1863
 * @param pid The process ID identifying which command ran.
1864
 */
1865
void MainWindow::onProcessOutput(const QString &output, bool isError,
2✔
1866
                                 Enums::PROCESS pid) {
1867
  appendProcessOutput(output, isError, getProcessName(pid));
2✔
1868
}
2✔
1869

1870
/**
1871
 * @brief Maps a process ID to its human-readable command name.
1872
 *
1873
 * Returns static strings for git/pass commands that appear in output.
1874
 * Password-related commands return empty (they are filtered).
1875
 *
1876
 * @param pid The process ID to look up.
1877
 * @return QString with command name, or empty if filtered.
1878
 */
1879
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
2✔
1880
  switch (pid) {
2✔
1881
  case Enums::GIT_INIT:
×
1882
    return QStringLiteral("git init"); // no-tr
×
1883
  case Enums::GIT_ADD:
×
1884
    return QStringLiteral("git add"); // no-tr
×
1885
  case Enums::GIT_COMMIT:
×
1886
    return QStringLiteral("git commit"); // no-tr
×
1887
  case Enums::GIT_RM:
×
1888
    return QStringLiteral("git rm"); // no-tr
×
1889
  case Enums::GIT_PULL:
×
1890
    return QStringLiteral("git pull"); // no-tr
×
1891
  case Enums::GIT_PUSH:
×
1892
    return QStringLiteral("git push"); // no-tr
×
1893
  case Enums::GIT_MOVE:
×
1894
    return QStringLiteral("git mv"); // no-tr
×
1895
  case Enums::GIT_COPY:
×
1896
    // ImitatePass::Copy literally invokes `git cp` (a git-extras
1897
    // subcommand), so the label matches what's run. Stock-git users
1898
    // without git-extras will see the underlying "'cp' is not a git
1899
    // command" failure surfaced in the process output panel.
1900
    return QStringLiteral("git cp"); // no-tr
×
1901
  case Enums::PASS_INSERT:
×
1902
    return QStringLiteral("pass insert"); // no-tr
×
1903
  case Enums::PASS_REMOVE:
×
1904
    return QStringLiteral("pass rm"); // no-tr
×
1905
  case Enums::PASS_INIT:
×
1906
    return QStringLiteral("pass init"); // no-tr
×
1907
  case Enums::PASS_MOVE:
×
1908
    return QStringLiteral("pass mv"); // no-tr
×
1909
  case Enums::PASS_COPY:
×
1910
    return QStringLiteral("pass cp"); // no-tr
×
1911
  case Enums::PASS_GREP:
×
1912
    return QStringLiteral("pass grep"); // no-tr
×
1913
  case Enums::GPG_GENKEYS:
×
1914
    return QStringLiteral("gpg --gen-key"); // no-tr
×
1915
  case Enums::PASS_SHOW:
1916
  case Enums::PASS_OTP_GENERATE:
1917
  case Enums::PROCESS_COUNT:
1918
  case Enums::INVALID:
1919
    break;
1920
  }
1921
  return {};
1922
}
1923

1924
/**
1925
 * @brief Checks if a process ID represents a sensitive operation whose
1926
 * output should not be shown in the process output panel.
1927
 *
1928
 * Password-related commands (pass show, OTP generate, grep, insert)
1929
 * display their output in other UI areas, so we skip them here.
1930
 *
1931
 * @param pid The process ID to check.
1932
 * @return true if the process is sensitive and should be filtered.
1933
 */
1934
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
1935
  switch (pid) {
×
1936
  case Enums::PASS_SHOW:
1937
  case Enums::PASS_OTP_GENERATE:
1938
  case Enums::PASS_GREP:
1939
  case Enums::PASS_INSERT:
1940
    return true;
1941
  case Enums::GIT_INIT:
1942
  case Enums::GIT_ADD:
1943
  case Enums::GIT_COMMIT:
1944
  case Enums::GIT_RM:
1945
  case Enums::GIT_PULL:
1946
  case Enums::GIT_PUSH:
1947
  case Enums::GIT_MOVE:
1948
  case Enums::GIT_COPY:
1949
  case Enums::PASS_REMOVE:
1950
  case Enums::PASS_INIT:
1951
  case Enums::PASS_MOVE:
1952
  case Enums::PASS_COPY:
1953
  case Enums::GPG_GENKEYS:
1954
  case Enums::PROCESS_COUNT:
1955
  case Enums::INVALID:
1956
    break;
1957
  }
1958
  return false;
×
1959
}
1960

1961
/**
1962
 * @brief Updates the visibility of the process output panel.
1963
 *
1964
 * Shows or hides the process output widget based on the user's
1965
 * showProcessOutput setting.
1966
 */
1967
void MainWindow::updateProcessOutputVisibility() {
×
1968
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
1969
}
×
1970

1971
/**
1972
 * @brief Limits the output panel to max lines, trimming old excess.
1973
 *
1974
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
1975
 * Called after each append to prevent unbounded growth.
1976
 */
1977
void MainWindow::limitOutputLines() {
1✔
1978
  QTextDocument *doc = m_processOutputEdit->document();
1✔
1979
  int excess = doc->blockCount() - MaxOutputLines;
1✔
1980
  if (excess <= 0) {
1✔
1981
    return;
1✔
1982
  }
1983

1984
  QTextCursor cursor(doc);
×
1985
  cursor.movePosition(QTextCursor::Start);
×
1986
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
1987
  cursor.removeSelectedText();
×
1988
}
×
1989

1990
/**
1991
 * @brief Clears the process output panel.
1992
 *
1993
 * Clears all output, resets the line counter, and re-enables auto-scroll.
1994
 */
1995
void MainWindow::on_clearOutputButton_clicked() {
×
1996
  m_processOutputEdit->clear();
×
1997
  m_outputCounter = 0;
×
1998
  m_autoScroll = true;
×
1999
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc