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

IJHack / QtPass / 24969809944

26 Apr 2026 11:30PM UTC coverage: 27.593%. Remained the same
24969809944

Pull #1192

github

web-flow
Merge 1dc78c093 into 312ed4ef8
Pull Request #1192: fix: build process output panel programmatically (was obscuring central UI)

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

65 existing lines in 2 files now uncovered.

1825 of 6614 relevant lines covered (27.59%)

26.92 hits per line

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

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

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

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

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

57
  m_qtPass = new QtPass(this);
×
58

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

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

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

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

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

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

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

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

112
  updateProfileBox();
×
113

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

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

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

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

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

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

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

163
  setUiElementsEnabled(true);
×
164

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1207
  QtPassSettings::setProfile(name);
×
1208

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1875
  limitOutputLines();
×
1876

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc