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

IJHack / QtPass / 24962433832

26 Apr 2026 05:16PM UTC coverage: 27.622%. Remained the same
24962433832

push

github

web-flow
fix: don't schedule initial focus pulse before init() completes (#1187)

Reproduced via SIGSEGV on a fresh-config launch:

  #0  QLineEdit::selectAll()
  #1  MainWindow::focusInput()
  #2  ... (timer event)
  #3  QEventLoop::exec()       ← nested loop from QDialog::exec()
  #4  ConfigDialog::wizard()
  #5  MainWindow::config()
  #6  QtPass::init()
  #7  MainWindow::MainWindow()

`MainWindow`'s constructor was scheduling
`QTimer::singleShot(10, this, SLOT(focusInput()))` *before* calling
`m_qtPass->init()`. On a fresh install (no `.gpg-id`/invalid config),
`init()` calls `MainWindow::config()` which runs the first-run wizard
via `QDialog::exec()` — i.e. a nested event loop. The 10 ms timer
fires inside that loop while the main window has not yet been shown,
`focusInput()` runs, and `QLineEdit::selectAll()` crashes inside Qt
because the line-edit's owning top-level isn't visible/realized yet.

Move the timer scheduling to *after* `init()` returns successfully,
and bail out of the constructor early when init fails. The success
path is unchanged for normal launches; the wizard path no longer
queues the focus pulse during the nested loop, and the failure path
no longer queues it before destruction.

1825 of 6607 relevant lines covered (27.62%)

26.95 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 <QMenu>
33
#include <QMessageBox>
34
#include <QScrollBar>
35
#include <QShortcut>
36
#include <QTextCursor>
37
#include <QTimer>
38
#include <QTreeWidget>
39
#include <utility>
40

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

56
  m_qtPass = new QtPass(this);
×
57

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

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

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

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

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

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

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

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

111
  updateProfileBox();
×
112

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

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

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

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

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

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

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

162
  setUiElementsEnabled(true);
×
163

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

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

172
  // Schedule the initial focus pulse only after init() has returned. init()
173
  // can run a nested event loop (e.g. ConfigDialog::wizard for first-run
174
  // setup), and a timer scheduled before that loop would fire focusInput()
175
  // against a not-yet-shown window — QLineEdit::selectAll then crashes
176
  // inside Qt because the widget hasn't been parented into a visible
177
  // top-level yet.
178
  QTimer::singleShot(10, this, SLOT(focusInput()));
179
}
×
180

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

183
/**
184
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
185
 * box and sets focus to it. Allows for easy searching, called at application
186
 * start and when receiving empty message in MainWindow::messageAvailable when
187
 * compiled with SINGLE_APP=1 (default).
188
 */
189
void MainWindow::focusInput() {
×
190
  ui->lineEdit->selectAll();
×
191
  ui->lineEdit->setFocus();
×
192
}
×
193

194
/**
195
 * @brief MainWindow::changeEvent sets focus to the search box
196
 * @param event
197
 */
198
void MainWindow::changeEvent(QEvent *event) {
×
199
  QWidget::changeEvent(event);
×
200
  if (event->type() == QEvent::ActivationChange) {
×
201
    if (isActiveWindow()) {
×
202
      focusInput();
×
203
    }
204
  }
205
}
×
206

207
/**
208
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
209
 */
210
void MainWindow::initToolBarButtons() {
×
211
  connect(ui->actionAddPassword, &QAction::triggered, this,
×
212
          &MainWindow::addPassword);
×
213
  connect(ui->actionAddFolder, &QAction::triggered, this,
×
214
          &MainWindow::addFolder);
×
215
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
×
216
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
×
217
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
×
218
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
×
219
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
×
220
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
×
221
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
×
222

223
  ui->actionAddPassword->setIcon(
×
224
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
×
225
  ui->actionAddFolder->setIcon(
×
226
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
×
227
  ui->actionEdit->setIcon(QIcon::fromTheme(
×
228
      "document-properties", QIcon(":/icons/document-properties.svg")));
×
229
  ui->actionDelete->setIcon(
×
230
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
×
231
  ui->actionPush->setIcon(
×
232
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
×
233
  ui->actionUpdate->setIcon(
×
234
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
×
235
  ui->actionUsers->setIcon(QIcon::fromTheme(
×
236
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
×
237
  ui->actionConfig->setIcon(QIcon::fromTheme(
×
238
      "applications-system", QIcon(":/icons/applications-system.svg")));
×
239
}
×
240

241
/**
242
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
243
 */
244
void MainWindow::initStatusBar() {
×
245
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
×
246

247
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
×
248
                     .scaledToHeight(statusBar()->height());
×
249
  auto *logoApp = new QLabel(statusBar());
×
250
  logoApp->setPixmap(logo);
×
251
  statusBar()->addPermanentWidget(logoApp);
×
252

253
  statusBar()->addPermanentWidget(ui->processOutputWidget);
×
254

255
  updateProcessOutputVisibility();
×
256
}
×
257

258
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
259
  return ui->treeView->currentIndex();
×
260
}
261

262
void MainWindow::cleanKeygenDialog() {
×
263
  if (this->keygenDialog != nullptr) {
×
264
    this->keygenDialog->close();
×
265
  }
266
  this->keygenDialog = nullptr;
×
267
}
×
268

269
/**
270
 * @brief Displays the given text in the main window text browser, optionally
271
 * marking it as an error and/or rendering it as HTML.
272
 * @example
273
 * MainWindow window;
274
 * window.flashText("Operation completed.", false, false);
275
 *
276
 * @param const QString &text - The text content to display.
277
 * @param const bool isError - If true, sets the text color to red before
278
 * displaying the text.
279
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
280
 * the existing HTML content.
281
 * @return void - No return value.
282
 */
283
void MainWindow::flashText(const QString &text, const bool isError,
×
284
                           const bool isHtml) {
285
  if (isError) {
×
286
    ui->textBrowser->setTextColor(Qt::red);
×
287
  }
288

289
  if (isHtml) {
×
290
    QString _text = text;
291
    if (!ui->textBrowser->toPlainText().isEmpty()) {
×
292
      _text = ui->textBrowser->toHtml() + _text;
×
293
    }
294
    ui->textBrowser->setHtml(_text);
×
295
  } else {
296
    ui->textBrowser->setText(text);
×
297
  }
298
}
×
299

300
/**
301
 * @brief MainWindow::config pops up the configuration screen and handles all
302
 * inter-window communication
303
 */
304
void MainWindow::applyTextBrowserSettings() {
×
305
  if (QtPassSettings::isUseMonospace()) {
×
306
    QFont monospace("Monospace");
×
307
    monospace.setStyleHint(QFont::Monospace);
×
308
    ui->textBrowser->setFont(monospace);
×
309
  } else {
×
310
    ui->textBrowser->setFont(QFont());
×
311
  }
312

313
  if (QtPassSettings::isNoLineWrapping()) {
×
314
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
315
  } else {
316
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
317
  }
318
}
×
319

320
void MainWindow::applyWindowFlagsSettings() {
×
321
  if (QtPassSettings::isAlwaysOnTop()) {
×
322
    Qt::WindowFlags flags = windowFlags();
323
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
324
  } else {
325
    this->setWindowFlags(Qt::Window);
×
326
  }
327
  this->show();
×
328
}
×
329

330
/**
331
 * @brief Opens and processes the application configuration dialog, then applies
332
 * any accepted settings.
333
 * @example
334
 * config();
335
 *
336
 * @return void - This function does not return a value.
337
 */
338
void MainWindow::config() {
×
339
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
340
  d->setModal(true);
×
341
  // Automatically default to pass if it's available
342
  if (m_qtPass->isFreshStart() &&
×
343
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
344
    QtPassSettings::setUsePass(true);
×
345
  }
346

347
  if (m_qtPass->isFreshStart()) {
×
348
    d->wizard(); // run initial setup wizard for first-time configuration
×
349
  }
350
  if (d->exec()) {
×
351
    if (d->result() == QDialog::Accepted) {
×
352
      applyTextBrowserSettings();
×
353
      applyWindowFlagsSettings();
×
354

355
      updateProfileBox();
×
356
      const QString passStore = QtPassSettings::getPassStore();
×
357
      proxyModel.setStore(passStore);
×
358
      ui->treeView->setRootIndex(
×
359
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
360
      deselect();
×
361
      ui->treeView->setCurrentIndex(QModelIndex());
×
362

363
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
364
        config();
×
365
      }
366
      QtPassSettings::getPass()->updateEnv();
×
367
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
368
                                  QtPassSettings::getAutoclearPanelSeconds());
×
369
      m_qtPass->setClipboardTimer();
×
370

371
      updateGitButtonVisibility();
×
372
      updateOtpButtonVisibility();
×
373
      updateGrepButtonVisibility();
×
374
      updateProcessOutputVisibility();
×
375
      if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
376
        initTrayIcon();
×
377
      } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
378
        destroyTrayIcon();
×
379
      }
380
    }
381

382
    m_qtPass->setFreshStart(false);
×
383
  }
384
}
×
385

386
/**
387
 * @brief MainWindow::onUpdate do a git pull
388
 */
389
void MainWindow::onUpdate(bool block) {
×
390
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
391
  if (block) {
×
392
    QtPassSettings::getPass()->GitPull_b();
×
393
  } else {
394
    QtPassSettings::getPass()->GitPull();
×
395
  }
396
}
×
397

398
/**
399
 * @brief MainWindow::onPush do a git push
400
 */
401
void MainWindow::onPush() {
×
402
  if (QtPassSettings::isUseGit()) {
×
403
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
404
    QtPassSettings::getPass()->GitPush();
×
405
  }
406
}
×
407

408
/**
409
 * @brief MainWindow::getFile get the selected file path
410
 * @param index
411
 * @param forPass returns relative path without '.gpg' extension
412
 * @return path
413
 * @return
414
 */
415
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
416
  if (!index.isValid() ||
×
417
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
418
    return {};
419
  }
420
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
421
  if (forPass) {
×
422
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
423
    filePath.replace(Util::endsWithGpg(), "");
×
424
  }
425
  return filePath;
426
}
427

428
/**
429
 * @brief MainWindow::on_treeView_clicked read the selected password file
430
 * @param index
431
 */
432
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
433
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
434
  currentDir =
435
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
436
  // Clear any previously cached clipped text before showing new password
437
  m_qtPass->clearClippedText();
×
438
  QString file = getFile(index, true);
×
439
  ui->passwordName->setText(file);
×
440
  if (!file.isEmpty() && !cleared) {
×
441
    QtPassSettings::getPass()->Show(file);
×
442
  } else {
443
    clearPanel(false);
×
444
    ui->actionEdit->setEnabled(false);
×
445
    ui->actionDelete->setEnabled(true);
×
446
  }
447
}
×
448

449
/**
450
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
451
 * TreeViewItem, open the edit Window
452
 * @param index
453
 */
454
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
455
  QFileInfo fileOrFolder =
456
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
457

458
  if (fileOrFolder.isFile()) {
×
459
    editPassword(getFile(index, true));
×
460
  }
461
}
×
462

463
/**
464
 * @brief MainWindow::deselect clear the selection, password and copy buffer
465
 */
466
void MainWindow::deselect() {
×
467
  currentDir = "";
×
468
  m_qtPass->clearClipboard();
×
469
  ui->treeView->clearSelection();
×
470
  ui->actionEdit->setEnabled(false);
×
471
  ui->actionDelete->setEnabled(false);
×
472
  ui->passwordName->setText("");
×
473
  clearPanel(false);
×
474
}
×
475

476
void MainWindow::executeWrapperStarted() {
×
477
  clearTemplateWidgets();
×
478
  ui->textBrowser->clear();
×
479
  setUiElementsEnabled(false);
×
480
  clearPanelTimer.stop();
×
481
  if (QtPassSettings::isShowProcessOutput()) {
×
482
    ui->processOutputWidget->setVisible(true);
×
483
  }
484
}
×
485

486
/**
487
 * @brief Handles displaying parsed password entry content in the main window.
488
 * @example
489
 * void result = MainWindow::passShowHandler(p_output);
490
 * // Updates the UI with parsed fields and emits
491
 * passShowHandlerFinished(output)
492
 *
493
 * @param p_output - The raw output text containing the password entry data.
494
 * @return void - This function does not return a value.
495
 */
496
void MainWindow::passShowHandler(const QString &p_output) {
×
497
  QStringList templ = QtPassSettings::isUseTemplate()
×
498
                          ? QtPassSettings::getPassTemplate().split("\n")
×
499
                          : QStringList();
×
500
  bool allFields =
501
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
502
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
503
  QString output = p_output;
504
  QString password = fileContent.getPassword();
×
505

506
  // set clipped text
507
  m_qtPass->setClippedText(password, p_output);
×
508

509
  // first clear the current view:
510
  clearTemplateWidgets();
×
511

512
  // show what is needed:
513
  if (QtPassSettings::isHideContent()) {
×
514
    output = "***" + tr("Content hidden") + "***";
×
515
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
516
    if (!password.isEmpty()) {
×
517
      // set the password, it is hidden if needed in addToGridLayout
518
      addToGridLayout(0, tr("Password"), password);
×
519
    }
520

521
    NamedValues namedValues = fileContent.getNamedValues();
×
522
    for (int j = 0; j < namedValues.length(); ++j) {
×
523
      const NamedValue &nv = namedValues.at(j);
524
      addToGridLayout(j + 1, nv.name, nv.value);
×
525
    }
526
    if (ui->gridLayout->count() == 0) {
×
527
      ui->verticalLayoutPassword->setSpacing(0);
×
528
    } else {
529
      ui->verticalLayoutPassword->setSpacing(6);
×
530
    }
531

532
    output = fileContent.getRemainingDataForDisplay();
×
533
  }
534

535
  if (QtPassSettings::isUseAutoclearPanel()) {
×
536
    clearPanelTimer.start();
×
537
  }
538

539
  emit passShowHandlerFinished(output);
×
540
  setUiElementsEnabled(true);
×
541
}
×
542

543
/**
544
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
545
 * and updating the UI state.
546
 * @example
547
 * void MainWindow::passOtpHandler(const QString &p_output);
548
 *
549
 * @param const QString &p_output - The OTP code text to process; if empty, an
550
 * error message is shown instead.
551
 * @return void - This function does not return a value.
552
 */
553
void MainWindow::passOtpHandler(const QString &p_output) {
×
554
  if (!p_output.isEmpty()) {
×
555
    addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
×
556
    m_qtPass->copyTextToClipboard(p_output);
×
557
    showStatusMessage(tr("OTP code copied to clipboard"));
×
558
  } else {
559
    flashText(tr("No OTP code found in this password entry"), true);
×
560
  }
561
  if (QtPassSettings::isUseAutoclearPanel()) {
×
562
    clearPanelTimer.start();
×
563
  }
564
  setUiElementsEnabled(true);
×
565
}
×
566

567
/**
568
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
569
 */
570
void MainWindow::clearPanel(bool notify) {
×
571
  while (ui->gridLayout->count() > 0) {
×
572
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
573
    delete item->widget();
×
574
    delete item;
×
575
  }
576
  const bool grepWasVisible = ui->grepResultsList->isVisible();
×
577
  ui->grepResultsList->clear();
×
578
  if (grepWasVisible) {
×
579
    ui->grepResultsList->setVisible(false);
×
580
    ui->treeView->setVisible(true);
×
581
    if (m_grepMode) {
×
582
      m_grepMode = false;
×
583
      ui->grepButton->blockSignals(true);
×
584
      ui->grepButton->setChecked(false);
×
585
      ui->grepButton->blockSignals(false);
×
586
      ui->lineEdit->blockSignals(true);
×
587
      ui->lineEdit->clear();
×
588
      ui->lineEdit->blockSignals(false);
×
589
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
590
    }
591
  }
592
  if (notify) {
×
593
    QString output = "***" + tr("Password and Content hidden") + "***";
×
594
    ui->textBrowser->setHtml(output);
×
595
  } else {
596
    ui->textBrowser->setHtml("");
×
597
  }
598
}
×
599

600
/**
601
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
602
 * elements
603
 * @param state
604
 */
605
void MainWindow::setUiElementsEnabled(bool state) {
×
606
  ui->treeView->setEnabled(state);
×
607
  ui->lineEdit->setEnabled(state);
×
608
  ui->lineEdit->installEventFilter(this);
×
609
  ui->actionAddPassword->setEnabled(state);
×
610
  ui->actionAddFolder->setEnabled(state);
×
611
  ui->actionUsers->setEnabled(state);
×
612
  ui->actionConfig->setEnabled(state);
×
613
  // is a file selected?
614
  state &= ui->treeView->currentIndex().isValid();
×
615
  ui->actionDelete->setEnabled(state);
×
616
  ui->actionEdit->setEnabled(state);
×
617
  updateGitButtonVisibility();
×
618
  updateOtpButtonVisibility();
×
619
}
×
620

621
/**
622
 * @brief Restores the main window geometry, state, position, size, and
623
 * tray/icon settings from saved application settings.
624
 * @example
625
 * MainWindow window;
626
 * window.restoreWindow();
627
 *
628
 * @return void - This function does not return a value.
629
 */
630
void MainWindow::restoreWindow() {
×
631
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
×
632
  restoreGeometry(geometry);
×
633
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
×
634
  restoreState(savestate);
×
635
  QPoint position = QtPassSettings::getPos(pos());
×
636
  move(position);
×
637
  QSize newSize = QtPassSettings::getSize(size());
×
638
  resize(newSize);
×
639
  if (QtPassSettings::isMaximized(isMaximized())) {
×
640
    showMaximized();
×
641
  }
642

643
  if (QtPassSettings::isAlwaysOnTop()) {
×
644
    Qt::WindowFlags flags = windowFlags();
645
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
646
    show();
×
647
  }
648

649
  if (QtPassSettings::isUseTrayIcon() && tray == nullptr) {
×
650
    initTrayIcon();
×
651
    if (QtPassSettings::isStartMinimized()) {
×
652
      // since we are still in constructor, can't directly hide
653
      QTimer::singleShot(10, this, SLOT(hide()));
×
654
    }
655
  } else if (!QtPassSettings::isUseTrayIcon() && tray != nullptr) {
×
656
    destroyTrayIcon();
×
657
  }
658
}
×
659

660
/**
661
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
662
 */
663
void MainWindow::onConfig() { config(); }
×
664

665
/**
666
 * @brief Executes when the string in the search box changes, collapses the
667
 * TreeView
668
 * @param arg1
669
 */
670
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
671
  if (m_grepMode)
×
672
    return;
673
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
674
  ui->treeView->expandAll();
×
675
  clearPanel(false);
×
676
  ui->passwordName->setText("");
×
677
  ui->actionEdit->setEnabled(false);
×
678
  ui->actionDelete->setEnabled(false);
×
679
  searchTimer.start();
×
680
}
681

682
/**
683
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
684
 * time from two keypresses is elapsed
685
 */
686
void MainWindow::onTimeoutSearch() {
×
687
  QString query = ui->lineEdit->text();
×
688

689
  if (query.isEmpty()) {
×
690
    ui->treeView->collapseAll();
×
691
    deselect();
×
692
  }
693

694
  query.replace(QStringLiteral(" "), ".*");
×
695
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
696
  proxyModel.setFilterRegularExpression(regExp);
×
697
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
698
      model.setRootPath(QtPassSettings::getPassStore())));
×
699

700
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
701
    selectFirstFile();
×
702
  } else {
703
    ui->actionEdit->setEnabled(false);
×
704
    ui->actionDelete->setEnabled(false);
×
705
  }
706
}
×
707

708
/**
709
 * @brief MainWindow::on_lineEdit_returnPressed get searching
710
 *
711
 * Select the first possible file in the tree
712
 */
713
void MainWindow::on_lineEdit_returnPressed() {
×
714
#ifdef QT_DEBUG
715
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
716
#endif
717

718
  if (m_grepMode) {
×
719
    const QString query = ui->lineEdit->text();
×
720
    if (!query.isEmpty()) {
×
721
      m_grepCancelled = false;
×
722
      ui->grepResultsList->clear();
×
723
      ui->statusBar->showMessage(tr("Searching…"));
×
724
      if (!m_grepBusy) {
×
725
        m_grepBusy = true;
×
726
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
727
      }
728
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
729
    } else {
730
      m_grepCancelled = true;
×
731
      if (m_grepBusy) {
×
732
        m_grepBusy = false;
×
733
        QApplication::restoreOverrideCursor();
×
734
      }
735
      ui->grepResultsList->clear();
×
736
      ui->grepResultsList->setVisible(false);
×
737
      ui->treeView->setVisible(true);
×
738
    }
739
    return;
740
  }
741

742
  if (proxyModel.rowCount() > 0) {
×
743
    selectFirstFile();
×
744
    on_treeView_clicked(ui->treeView->currentIndex());
×
745
  }
746
}
747

748
/**
749
 * @brief Toggle grep (content search) mode.
750
 */
751
void MainWindow::on_grepButton_toggled(bool checked) {
×
752
  m_grepMode = checked;
×
753
  if (checked) {
×
754
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
755
    ui->lineEdit->clear();
×
756
    searchTimer.stop();
×
757
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
758
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
759
        model.setRootPath(QtPassSettings::getPassStore())));
×
760
    ui->grepResultsList->setVisible(false);
×
761
    // Keep treeView visible until results arrive
762
  } else {
763
    if (m_grepBusy) {
×
764
      m_grepBusy = false;
×
765
      m_grepCancelled = true;
×
766
      QApplication::restoreOverrideCursor();
×
767
    }
768
    searchTimer.stop();
×
769
    ui->lineEdit->blockSignals(true);
×
770
    ui->lineEdit->clear();
×
771
    ui->lineEdit->blockSignals(false);
×
772
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
773
    ui->grepResultsList->clear();
×
774
    ui->grepResultsList->setVisible(false);
×
775
    ui->treeView->setVisible(true);
×
776
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
777
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
778
        model.setRootPath(QtPassSettings::getPassStore())));
×
779
  }
780
}
×
781

782
/**
783
 * @brief Display grep results in grepResultsList.
784
 */
785
void MainWindow::onGrepFinished(
×
786
    const QList<QPair<QString, QStringList>> &results) {
787
  if (m_grepBusy) {
×
788
    m_grepBusy = false;
×
789
    QApplication::restoreOverrideCursor();
×
790
  }
791
  if (m_grepCancelled) {
×
792
    m_grepCancelled = false;
×
793
    return;
×
794
  }
795
  setUiElementsEnabled(true);
×
796
  if (!m_grepMode)
×
797
    return;
798
  ui->grepResultsList->clear();
×
799
  if (results.isEmpty()) {
×
800
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
801
    ui->grepResultsList->setVisible(false);
×
802
    ui->treeView->setVisible(true);
×
803
    return;
×
804
  }
805
  const bool hideContent = QtPassSettings::isHideContent();
×
806
  int totalLines = 0;
807
  for (const auto &pair : results) {
×
808
    QTreeWidgetItem *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
809
    entryItem->setText(0, pair.first);
×
810
    entryItem->setData(0, Qt::UserRole, pair.first);
×
811
    for (const QString &line : pair.second) {
×
812
      QTreeWidgetItem *lineItem = new QTreeWidgetItem(entryItem);
×
813
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
814
                                       : line);
815
      lineItem->setData(0, Qt::UserRole, pair.first);
×
816
      ++totalLines;
×
817
    }
818
  }
819
  ui->grepResultsList->expandAll();
×
820
  ui->treeView->setVisible(false);
×
821
  ui->grepResultsList->setVisible(true);
×
822
  ui->statusBar->showMessage(
×
823
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
824
          tr("in %n entr(ies).", nullptr, results.size()),
×
825
      3000);
826
  if (QtPassSettings::isUseAutoclearPanel())
×
827
    clearPanelTimer.start();
×
828
}
829

830
/**
831
 * @brief Navigate to the password entry when a grep result is clicked.
832
 */
833
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
834
                                                int /*column*/) {
835
  const QString entry = item->data(0, Qt::UserRole).toString();
×
836
  if (entry.isEmpty())
×
837
    return;
838
  const QString fullPath = QDir::cleanPath(
839
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
840
  QModelIndex srcIndex = model.index(fullPath);
×
841
  if (!srcIndex.isValid())
842
    return;
843
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
844
  if (!proxyIndex.isValid())
845
    return;
846
  ui->treeView->setCurrentIndex(proxyIndex);
×
847
  on_treeView_clicked(proxyIndex);
×
848
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
849
    ui->grepResultsList->clear();
×
850
  ui->grepResultsList->setVisible(false);
×
851
  ui->treeView->setVisible(true);
×
852
  ui->treeView->scrollTo(proxyIndex);
×
853
  ui->treeView->setFocus();
×
854
}
855

856
/**
857
 * @brief MainWindow::selectFirstFile select the first possible file in the
858
 * tree
859
 */
860
void MainWindow::selectFirstFile() {
×
861
  QModelIndex index = proxyModel.mapFromSource(
×
862
      model.setRootPath(QtPassSettings::getPassStore()));
×
863
  index = firstFile(index);
×
864
  ui->treeView->setCurrentIndex(index);
×
865
}
×
866

867
/**
868
 * @brief MainWindow::firstFile return location of first possible file
869
 * @param parentIndex
870
 * @return QModelIndex
871
 */
872
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
873
  QModelIndex index = parentIndex;
×
874
  int numRows = proxyModel.rowCount(parentIndex);
×
875
  for (int row = 0; row < numRows; ++row) {
×
876
    index = proxyModel.index(row, 0, parentIndex);
×
877
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
878
      return index;
×
879
    }
880
    if (proxyModel.hasChildren(index)) {
×
881
      return firstFile(index);
×
882
    }
883
  }
884
  return index;
×
885
}
886

887
/**
888
 * @brief MainWindow::setPassword open passworddialog
889
 * @param file which pgp file
890
 * @param isNew insert (not update)
891
 */
892
void MainWindow::setPassword(const QString &file, bool isNew) {
×
893
  PasswordDialog d(file, isNew, this);
×
894

895
  if (isNew) {
×
896
    QString storePath = QtPassSettings::getPassStore();
×
897
    QString folder =
898
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
899
    if (folder.isEmpty()) {
×
900
      folder = storePath;
×
901
    }
902
    QHash<QString, QStringList> templates = Util::readTemplates(storePath);
×
903
    if (!templates.isEmpty()) {
904
      QString defaultTemplate = Util::getFolderTemplate(folder, storePath);
×
905
      d.setAvailableTemplates(templates, defaultTemplate);
×
906
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
907
                    [&d]() { d.cycleTemplate(); });
×
908
    }
909
  }
×
910

911
  if (!d.exec()) {
×
912
    ui->treeView->setFocus();
×
913
  }
914
}
×
915

916
/**
917
 * @brief MainWindow::addPassword add a new password by showing a
918
 * number of dialogs.
919
 */
920
void MainWindow::addPassword() {
×
921
  bool ok;
922
  QString dir =
923
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
924
  QString file =
925
      QInputDialog::getText(this, tr("New file"),
×
926
                            tr("New password file: \n(Will be placed in %1 )")
×
927
                                .arg(QtPassSettings::getPassStore() +
×
928
                                     Util::getDir(ui->treeView->currentIndex(),
×
929
                                                  true, model, proxyModel)),
930
                            QLineEdit::Normal, "", &ok);
×
931
  if (!ok || file.isEmpty()) {
×
932
    return;
933
  }
934
  file = dir + file;
×
935
  setPassword(file);
×
936
}
937

938
/**
939
 * @brief MainWindow::onDelete remove password, if you are
940
 * sure.
941
 */
942
void MainWindow::onDelete() {
×
943
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
944
  if (!currentIndex.isValid()) {
945
    // This fixes https://github.com/IJHack/QtPass/issues/556
946
    // Otherwise the entire password directory would be deleted if
947
    // nothing is selected in the tree view.
948
    return;
×
949
  }
950

951
  QFileInfo fileOrFolder =
952
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
953
  QString file = "";
×
954
  bool isDir = false;
955

956
  if (fileOrFolder.isFile()) {
×
957
    file = getFile(ui->treeView->currentIndex(), true);
×
958
  } else {
959
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
960
    isDir = true;
961
  }
962

963
  QString dirMessage = tr(" and the whole content?");
964
  if (isDir) {
×
965
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
966
                    QDirIterator::Subdirectories);
×
967
    bool okDir = true;
968
    while (it.hasNext() && okDir) {
×
969
      it.next();
×
970
      if (QFileInfo(it.filePath()).isFile()) {
×
971
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
972
          okDir = false;
973
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
974
                          "there are unexpected files in the given folder, "
975
                          "check them before continue.</strong>");
976
        }
977
      }
978
    }
979
  }
×
980

981
  if (QMessageBox::question(
×
982
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
983
          tr("Are you sure you want to delete %1%2?")
×
984
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
985
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
986
    return;
987
  }
988

989
  QtPassSettings::getPass()->Remove(file, isDir);
×
990
}
×
991

992
/**
993
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
994
 */
995
void MainWindow::onOtp() {
×
996
  QString file = getFile(ui->treeView->currentIndex(), true);
×
997
  if (!file.isEmpty()) {
×
998
    if (QtPassSettings::isUseOtp()) {
×
999
      setUiElementsEnabled(false);
×
1000
      QtPassSettings::getPass()->OtpGenerate(file);
×
1001
    }
1002
  } else {
1003
    flashText(tr("No password selected for OTP generation"), true);
×
1004
  }
1005
}
×
1006

1007
/**
1008
 * @brief MainWindow::onEdit try and edit (selected) password.
1009
 */
1010
void MainWindow::onEdit() {
×
1011
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1012
  editPassword(file);
×
1013
}
×
1014

1015
/**
1016
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1017
 * @param dir folder to edit users for.
1018
 */
1019
void MainWindow::userDialog(const QString &dir) {
×
1020
  if (!dir.isEmpty()) {
×
1021
    currentDir = dir;
×
1022
  }
1023
  onUsers();
×
1024
}
×
1025

1026
/**
1027
 * @brief MainWindow::onUsers edit users for the current
1028
 * folder,
1029
 * gets lists and opens UserDialog.
1030
 */
1031
void MainWindow::onUsers() {
×
1032
  QString dir =
1033
      currentDir.isEmpty()
1034
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
1035
          : currentDir;
×
1036

1037
  UsersDialog d(dir, this);
×
1038
  if (!d.exec()) {
×
1039
    ui->treeView->setFocus();
×
1040
  }
1041
}
×
1042

1043
/**
1044
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1045
 * @param message
1046
 */
1047
void MainWindow::messageAvailable(const QString &message) {
×
1048
  if (message.isEmpty()) {
×
1049
    focusInput();
×
1050
  } else {
1051
    ui->treeView->expandAll();
×
1052
    ui->lineEdit->setText(message);
×
1053
    on_lineEdit_returnPressed();
×
1054
  }
1055
  show();
×
1056
  raise();
×
1057
}
×
1058

1059
/**
1060
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1061
 * @param batch
1062
 * @param keygenWindow
1063
 */
1064
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1065
  keygenDialog = keygenWindow;
×
1066
  emit generateGPGKeyPair(batch);
×
1067
}
×
1068

1069
/**
1070
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1071
 * select a more appropriate one to view too
1072
 */
1073
void MainWindow::updateProfileBox() {
×
1074
  QHash<QString, QHash<QString, QString>> profiles =
1075
      QtPassSettings::getProfiles();
×
1076

1077
  if (profiles.isEmpty()) {
1078
    ui->profileWidget->hide();
×
1079
  } else {
1080
    ui->profileWidget->show();
×
1081
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1082
    ui->profileBox->clear();
×
1083
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
×
1084
    while (i.hasNext()) {
×
1085
      i.next();
1086
      if (!i.key().isEmpty()) {
×
1087
        ui->profileBox->addItem(i.key());
×
1088
      }
1089
    }
1090
    ui->profileBox->model()->sort(0);
×
1091
  }
1092
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
×
1093
  if (index != -1) { //  -1 for not found
×
1094
    ui->profileBox->setCurrentIndex(index);
×
1095
  }
1096
}
×
1097

1098
/**
1099
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1100
 * correct "profile"
1101
 * @param name
1102
 */
1103
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1104
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1105
#else
1106
/**
1107
 * @brief Handles changes to the selected profile in the profile combo box.
1108
 * @details Ignores the event during a fresh start or when the selected profile
1109
 * matches the current profile. Otherwise, it clears the password field, updates
1110
 * the active profile and related settings, refreshes the environment, and
1111
 * resets the tree view and action states to reflect the newly selected profile.
1112
 *
1113
 * @param name - The newly selected profile name.
1114
 * @return void - This function does not return a value.
1115
 *
1116
 */
1117
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1118
#endif
1119
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
1120
    return;
×
1121
  }
1122

1123
  ui->lineEdit->clear();
×
1124

1125
  QtPassSettings::setProfile(name);
×
1126

1127
  QtPassSettings::setPassStore(
×
1128
      QtPassSettings::getProfiles().value(name).value("path"));
×
1129
  QtPassSettings::setPassSigningKey(
×
1130
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1131
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1132

1133
  QtPassSettings::getPass()->updateEnv();
×
1134

1135
  const QString passStore = QtPassSettings::getPassStore();
×
1136
  proxyModel.setStore(passStore);
×
1137
  ui->treeView->setRootIndex(
×
1138
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1139
  deselect();
×
1140
  ui->treeView->setCurrentIndex(QModelIndex());
×
1141
}
1142

1143
/**
1144
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1145
 * support
1146
 * it
1147
 */
1148
void MainWindow::initTrayIcon() {
×
1149
  this->tray = new TrayIcon(this);
×
1150
  // Setup tray icon
1151

1152
  if (tray == nullptr) {
1153
#ifdef QT_DEBUG
1154
    dbg() << "Allocating tray icon failed.";
1155
#endif
1156
    return;
1157
  }
1158

1159
  if (!tray->getIsAllocated()) {
×
1160
    destroyTrayIcon();
×
1161
  }
1162
}
1163

1164
/**
1165
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1166
 */
1167
void MainWindow::destroyTrayIcon() {
×
1168
  delete this->tray;
×
1169
  tray = nullptr;
×
1170
}
×
1171

1172
/**
1173
 * @brief MainWindow::closeEvent hide or quit
1174
 * @param event
1175
 */
1176
void MainWindow::closeEvent(QCloseEvent *event) {
×
1177
  if (QtPassSettings::isHideOnClose()) {
×
1178
    this->hide();
×
1179
    event->ignore();
1180
  } else {
1181
    m_qtPass->clearClipboard();
×
1182

1183
    QtPassSettings::setGeometry(saveGeometry());
×
1184
    QtPassSettings::setSavestate(saveState());
×
1185
    QtPassSettings::setMaximized(isMaximized());
×
1186
    if (!isMaximized()) {
×
1187
      QtPassSettings::setPos(pos());
×
1188
      QtPassSettings::setSize(size());
×
1189
    }
1190
    event->accept();
1191
  }
1192
}
×
1193

1194
/**
1195
 * @brief MainWindow::eventFilter filter out some events and focus the
1196
 * treeview
1197
 * @param obj
1198
 * @param event
1199
 * @return
1200
 */
1201
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
×
1202
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
×
1203
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1204
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1205
      ui->treeView->setFocus();
×
1206
    }
1207
  }
1208
  return QObject::eventFilter(obj, event);
×
1209
}
1210

1211
/**
1212
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1213
 * @param event
1214
 */
1215
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1216
  switch (event->key()) {
×
1217
  case Qt::Key_Delete:
×
1218
    onDelete();
×
1219
    break;
×
1220
  case Qt::Key_Return:
×
1221
  case Qt::Key_Enter:
1222
    if (proxyModel.rowCount() > 0) {
×
1223
      on_treeView_clicked(ui->treeView->currentIndex());
×
1224
    }
1225
    break;
1226
  case Qt::Key_Escape:
×
1227
    ui->lineEdit->clear();
×
1228
    break;
×
1229
  default:
1230
    break;
1231
  }
1232
}
×
1233

1234
/**
1235
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1236
 * menu
1237
 * @param pos
1238
 */
1239
void MainWindow::showContextMenu(const QPoint &pos) {
×
1240
  QModelIndex index = ui->treeView->indexAt(pos);
×
1241
  bool selected = true;
1242
  if (!index.isValid()) {
1243
    ui->treeView->clearSelection();
×
1244
    ui->actionDelete->setEnabled(false);
×
1245
    ui->actionEdit->setEnabled(false);
×
1246
    currentDir = "";
×
1247
    selected = false;
1248
  }
1249

1250
  ui->treeView->setCurrentIndex(index);
×
1251

1252
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1253

1254
  QFileInfo fileOrFolder =
1255
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1256

1257
  QMenu contextMenu;
×
1258
  if (!selected || fileOrFolder.isDir()) {
×
1259
    QAction *openFolder =
1260
        contextMenu.addAction(tr("Open folder with file manager"));
×
1261
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1262
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1263
    QAction *users = contextMenu.addAction(tr("Users"));
×
1264
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1265
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1266
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1267
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1268
  } else if (fileOrFolder.isFile()) {
×
1269
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1270
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1271
  }
1272
  if (selected) {
×
1273
    contextMenu.addSeparator();
×
1274
    if (fileOrFolder.isDir()) {
×
1275
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1276
      connect(renameFolder, &QAction::triggered, this,
×
1277
              &MainWindow::renameFolder);
×
1278
    } else if (fileOrFolder.isFile()) {
×
1279
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1280
      connect(renamePassword, &QAction::triggered, this,
×
1281
              &MainWindow::renamePassword);
×
1282
    }
1283
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1284
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1285
    if (fileOrFolder.isDir()) {
×
1286
      QString dirPath = QDir::cleanPath(
1287
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1288

1289
      QMenu *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1290
      contextMenu.addMenu(shareMenu);
×
1291

1292
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1293
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1294

1295
      QString exePath = QtPassSettings::isUsePass()
×
1296
                            ? QtPassSettings::getPassExecutable()
×
1297
                            : QtPassSettings::getGpgExecutable();
×
1298
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1299
                                                 QFile(exePath).exists());
×
1300

1301
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1302
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1303
      connect(reencrypt, &QAction::triggered, this,
×
1304
              [this, dirPath]() { reencryptPath(dirPath); });
×
1305

1306
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1307
      exportKey->setEnabled(gpgAvailable);
×
1308
      connect(exportKey, &QAction::triggered, this,
×
1309
              &MainWindow::exportPublicKey);
×
1310

1311
      QAction *addRecipientAction =
1312
          shareMenu->addAction(tr("Add recipient..."));
×
1313
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1314
      connect(addRecipientAction, &QAction::triggered, this,
×
1315
              [this, dirPath]() { addRecipient(dirPath); });
×
1316

1317
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1318
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1319
    }
1320
  }
1321
  contextMenu.exec(globalPos);
×
1322
}
×
1323

1324
/**
1325
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1326
 * password window
1327
 * @param pos
1328
 */
1329
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1330
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1331
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1332

1333
  contextMenu->exec(globalPos);
×
1334
  delete contextMenu;
×
1335
}
×
1336

1337
/**
1338
 * @brief MainWindow::openFolder open the folder in the default file manager
1339
 */
1340
void MainWindow::openFolder() {
×
1341
  QString dir =
1342
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1343

1344
  QString path = QDir::toNativeSeparators(dir);
×
1345
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1346
}
×
1347

1348
/**
1349
 * @brief MainWindow::addFolder add a new folder to store passwords in
1350
 */
1351
void MainWindow::addFolder() {
×
1352
  bool ok;
1353
  QString dir =
1354
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1355
  QString newdir =
1356
      QInputDialog::getText(this, tr("New file"),
×
1357
                            tr("New Folder: \n(Will be placed in %1 )")
×
1358
                                .arg(QtPassSettings::getPassStore() +
×
1359
                                     Util::getDir(ui->treeView->currentIndex(),
×
1360
                                                  true, model, proxyModel)),
1361
                            QLineEdit::Normal, "", &ok);
×
1362
  if (!ok || newdir.isEmpty()) {
×
1363
    return;
1364
  }
1365
  newdir.prepend(dir);
1366
  if (!QDir().mkdir(newdir)) {
×
1367
    QMessageBox::warning(this, tr("Error"),
×
1368
                         tr("Failed to create folder: %1").arg(newdir));
×
1369
    return;
×
1370
  }
1371
  if (QtPassSettings::isAddGPGId(true)) {
×
1372
    QString gpgIdFile = newdir + "/.gpg-id";
×
1373
    QFile gpgId(gpgIdFile);
×
1374
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1375
      QMessageBox::warning(
×
1376
          this, tr("Error"),
×
1377
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1378
      return;
1379
    }
1380
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1381
    for (const UserInfo &user : users) {
×
1382
      if (user.enabled) {
×
1383
        gpgId.write((user.key_id + "\n").toUtf8());
×
1384
      }
1385
    }
1386
    gpgId.close();
×
1387
  }
×
1388
}
1389

1390
/**
1391
 * @brief MainWindow::renameFolder rename an existing folder
1392
 */
1393
void MainWindow::renameFolder() {
×
1394
  bool ok;
1395
  QString srcDir = QDir::cleanPath(
1396
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1397
  QString srcDirName = QDir(srcDir).dirName();
×
1398
  QString newName =
1399
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1400
                            QLineEdit::Normal, srcDirName, &ok);
×
1401
  if (!ok || newName.isEmpty()) {
×
1402
    return;
1403
  }
1404
  QString destDir = srcDir;
1405
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
1406
  QtPassSettings::getPass()->Move(srcDir, destDir);
×
1407
}
1408

1409
/**
1410
 * @brief MainWindow::editPassword read password and open edit window via
1411
 * MainWindow::onEdit()
1412
 */
1413
void MainWindow::editPassword(const QString &file) {
×
1414
  if (!file.isEmpty()) {
×
1415
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1416
      onUpdate(true);
×
1417
    }
1418
    setPassword(file, false);
×
1419
  }
1420
}
×
1421

1422
/**
1423
 * @brief MainWindow::renamePassword rename an existing password
1424
 */
1425
void MainWindow::renamePassword() {
×
1426
  bool ok;
1427
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1428
  QString filePath = QFileInfo(file).path();
×
1429
  QString fileName = QFileInfo(file).fileName();
×
1430
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1431
    fileName.chop(4);
×
1432
  }
1433

1434
  QString newName =
1435
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1436
                            QLineEdit::Normal, fileName, &ok);
×
1437
  if (!ok || newName.isEmpty()) {
×
1438
    return;
1439
  }
1440
  QString newFile = QDir(filePath).filePath(newName);
×
1441
  QtPassSettings::getPass()->Move(file, newFile);
×
1442
}
1443

1444
/**
1445
 * @brief MainWindow::clearTemplateWidgets empty the template widget fields in
1446
 * the UI
1447
 */
1448
void MainWindow::clearTemplateWidgets() {
×
1449
  while (ui->gridLayout->count() > 0) {
×
1450
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
1451
    delete item->widget();
×
1452
    delete item;
×
1453
  }
1454
  ui->verticalLayoutPassword->setSpacing(0);
×
1455
}
×
1456

1457
/**
1458
 * @brief Copies the password of the selected file from the tree view to the
1459
 * clipboard.
1460
 * @example
1461
 * MainWindow::copyPasswordFromTreeview();
1462
 *
1463
 * @return void - This function does not return a value.
1464
 */
1465
void MainWindow::copyPasswordFromTreeview() {
×
1466
  QFileInfo fileOrFolder =
1467
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1468

1469
  if (fileOrFolder.isFile()) {
×
1470
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1471
    // Disconnect any previous connection to avoid accumulation
1472
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1473
               &MainWindow::passwordFromFileToClipboard);
1474
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1475
            &MainWindow::passwordFromFileToClipboard);
×
1476
    QtPassSettings::getPass()->Show(file);
×
1477
  }
1478
}
×
1479

1480
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1481
  QStringList tokens = text.split('\n');
×
1482
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1483
}
×
1484

1485
/**
1486
 * @brief MainWindow::addToGridLayout add a field to the template grid
1487
 * @param position
1488
 * @param field
1489
 * @param value
1490
 */
1491
void MainWindow::addToGridLayout(int position, const QString &field,
×
1492
                                 const QString &value) {
1493
  QString trimmedField = field.trimmed();
1494
  QString trimmedValue = value.trimmed();
1495

1496
  const QString buttonStyle =
1497
      "border-style: none; background: transparent; padding: 0; margin: 0; "
1498
      "icon-size: 16px; color: inherit;";
×
1499

1500
  // Combine the Copy button and the line edit in one widget
1501
  auto *frame = new QFrame();
×
1502
  QLayout *ly = new QHBoxLayout();
×
1503
  ly->setContentsMargins(5, 2, 2, 2);
×
1504
  ly->setSpacing(0);
×
1505
  frame->setLayout(ly);
×
1506
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER) {
×
1507
    auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
×
1508
    connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
×
1509
            &QtPass::copyTextToClipboard);
×
1510

1511
    fieldLabel->setStyleSheet(buttonStyle);
×
1512
    frame->layout()->addWidget(fieldLabel);
×
1513
  }
1514

1515
  if (QtPassSettings::isUseQrencode()) {
×
1516
    auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
×
1517
    connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
×
1518
            &QtPass::showTextAsQRCode);
×
1519
    qrbutton->setStyleSheet(buttonStyle);
×
1520
    frame->layout()->addWidget(qrbutton);
×
1521
  }
1522

1523
  // set the echo mode to password, if the field is "password"
1524
  const QString lineStyle =
1525
      QtPassSettings::isUseMonospace()
×
1526
          ? "border-style: none; background: transparent; font-family: "
1527
            "monospace;"
1528
          : "border-style: none; background: transparent;";
×
1529

1530
  if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
×
1531
    auto *line = new QLineEdit();
×
1532
    line->setObjectName(trimmedField);
×
1533
    line->setText(trimmedValue);
×
1534
    line->setReadOnly(true);
×
1535
    line->setStyleSheet(lineStyle);
×
1536
    line->setContentsMargins(0, 0, 0, 0);
×
1537
    line->setEchoMode(QLineEdit::Password);
×
1538
    auto *showButton = new QPushButtonShowPassword(line, this);
×
1539
    showButton->setStyleSheet(buttonStyle);
×
1540
    showButton->setContentsMargins(0, 0, 0, 0);
×
1541
    frame->layout()->addWidget(showButton);
×
1542
    frame->layout()->addWidget(line);
×
1543
  } else {
1544
    auto *line = new QTextBrowser();
×
1545
    line->setOpenExternalLinks(true);
×
1546
    line->setOpenLinks(true);
×
1547
    line->setMaximumHeight(26);
×
1548
    line->setMinimumHeight(26);
×
1549
    line->setSizePolicy(
×
1550
        QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1551
    line->setObjectName(trimmedField);
×
1552
    trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
1553
    line->setText(trimmedValue);
×
1554
    line->setReadOnly(true);
×
1555
    line->setStyleSheet(lineStyle);
×
1556
    line->setContentsMargins(0, 0, 0, 0);
×
1557
    frame->layout()->addWidget(line);
×
1558
  }
1559

1560
  frame->setStyleSheet(
×
1561
      ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1562

1563
  // set into the layout
1564
  ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
×
1565
  ui->gridLayout->addWidget(frame, position, 1);
×
1566
}
×
1567

1568
/**
1569
 * @brief Displays message in status bar
1570
 *
1571
 * @param msg     text to be displayed
1572
 * @param timeout time for which msg shall be visible
1573
 */
1574
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
×
1575
  ui->statusBar->showMessage(msg, timeout);
×
1576
}
×
1577

1578
/**
1579
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1580
 * @param dir Directory path to re-encrypt
1581
 */
1582
void MainWindow::reencryptPath(const QString &dir) {
×
1583
  QDir checkDir(dir);
×
1584
  if (!checkDir.exists()) {
×
1585
    QMessageBox::critical(this, tr("Error"),
×
1586
                          tr("Directory does not exist: %1").arg(dir));
×
1587
    return;
×
1588
  }
1589

1590
  int ret = QMessageBox::question(
×
1591
      this, tr("Re-encrypt passwords"),
×
1592
      tr("Re-encrypt all passwords in %1?\n\n"
×
1593
         "This will re-encrypt ALL password files in this folder "
1594
         "using the current recipients defined in .gpg-id.\n\n"
1595
         "This may rewrite many files and cannot be undone easily.\n\n"
1596
         "Continue?")
1597
          .arg(QDir(dir).dirName()),
×
1598
      QMessageBox::Yes | QMessageBox::No);
1599

1600
  if (ret != QMessageBox::Yes)
×
1601
    return;
1602

1603
  // Prevent double execution - use same method as startReencryptPath
1604
  setUiElementsEnabled(false);
×
1605
  ui->treeView->setDisabled(true);
×
1606

1607
  QtPassSettings::getImitatePass()->reencryptPath(
×
1608
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1609
}
×
1610

1611
/**
1612
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1613
 */
1614
void MainWindow::startReencryptPath() {
×
1615
  setUiElementsEnabled(false);
×
1616
  ui->treeView->setDisabled(true);
×
1617
}
×
1618

1619
/**
1620
 * @brief MainWindow::endReencryptPath re-enable ui elements
1621
 */
1622
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1623

1624
/**
1625
 * @brief MainWindow::exportPublicKey export the configured signing key in
1626
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1627
 *
1628
 * Falls back to a help dialog when no signing key is configured or gpg is
1629
 * unavailable, so the user still gets actionable guidance.
1630
 */
1631
void MainWindow::exportPublicKey() {
×
1632
  QString identity = QtPassSettings::getPassSigningKey();
×
1633
  if (identity.isEmpty()) {
×
1634
    QMessageBox::information(
×
1635
        this, tr("Export Public Key"),
×
1636
        tr("<h3>Export Your Public Key</h3>"
×
1637
           "<p>No signing key is configured. Set one in QtPass Settings "
1638
           "&gt; GPG keys, or run this in a terminal:</p>"
1639
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1640
           "&gt;</pre>"
1641
           "<p>Then send the file to your teammates.</p>"));
1642
    return;
×
1643
  }
1644
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
1645
  if (gpgExe.isEmpty()) {
×
1646
    gpgExe = QStringLiteral("gpg");
×
1647
  }
1648
  QStringList args = {"--armor", "--export"};
×
1649
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1650
  QString stdOut;
×
1651
  QString stdErr;
×
1652
  int exitCode =
1653
      Executor::executeBlocking(gpgExe, args, QString(), &stdOut, &stdErr);
×
1654
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1655
    QMessageBox::warning(this, tr("Export Public Key"),
×
1656
                         tr("Could not export public key for %1.\n\n%2")
×
1657
                             .arg(identity, stdErr.isEmpty()
×
1658
                                                ? tr("No output from gpg.")
×
1659
                                                : stdErr));
1660
    return;
1661
  }
1662
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1663
  dialog.exec();
×
1664
}
×
1665

1666
/**
1667
 * @brief MainWindow::addRecipient open the recipient management dialog for
1668
 *        the supplied directory.
1669
 * @param dir Folder whose .gpg-id should be edited.
1670
 *
1671
 * Delegates to UsersDialog so users can tick/untick keys from their
1672
 * keyring as recipients of the folder; importing a foreign key into the
1673
 * keyring still has to happen via gpg (or QtPass settings) first.
1674
 */
1675
void MainWindow::addRecipient(const QString &dir) {
×
1676
  UsersDialog d(dir, this);
×
1677
  d.exec();
×
1678
}
×
1679

1680
/**
1681
 * @brief MainWindow::showShareHelp show help about GPG sharing
1682
 */
1683
void MainWindow::showShareHelp() {
×
1684
  QMessageBox::information(
×
1685
      this, tr("Sharing Passwords with GPG"),
×
1686
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1687
         "<p>To share passwords with other users:</p>"
1688
         "<ol>"
1689
         "<li><b>Export your public key</b> and send it to teammates</li>"
1690
         "<li><b>Import teammates' public keys</b> to their own folders</li>"
1691
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1692
         "them</li>"
1693
         "</ol>"
1694
         "<p>Only people who have a matching secret key can decrypt the "
1695
         "passwords.</p>"
1696
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1697
         "<p>See the FAQ for more details.</p>"));
1698
}
×
1699

1700
void MainWindow::updateGitButtonVisibility() {
×
1701
  if (!QtPassSettings::isUseGit() ||
×
1702
      (QtPassSettings::getGitExecutable().isEmpty() &&
×
1703
       QtPassSettings::getPassExecutable().isEmpty())) {
×
1704
    enableGitButtons(false);
×
1705
  } else {
1706
    enableGitButtons(true);
×
1707
  }
1708
}
×
1709

1710
void MainWindow::updateOtpButtonVisibility() {
×
1711
#if defined(Q_OS_WIN) || defined(__APPLE__)
1712
  ui->actionOtp->setVisible(false);
1713
#endif
1714
  if (!QtPassSettings::isUseOtp()) {
×
1715
    ui->actionOtp->setEnabled(false);
×
1716
  } else {
1717
    ui->actionOtp->setEnabled(true);
×
1718
  }
1719
}
×
1720

1721
void MainWindow::updateGrepButtonVisibility() {
×
1722
  const bool enabled = QtPassSettings::isUseGrepSearch();
×
1723
  ui->grepButton->setVisible(enabled);
×
1724
  ui->grepCaseButton->setVisible(enabled);
×
1725
  if (!enabled && m_grepMode) {
×
1726
    ui->grepButton->setChecked(false);
×
1727
  }
1728
}
×
1729

1730
void MainWindow::enableGitButtons(const bool &state) {
×
1731
  // Following GNOME guidelines is preferable disable buttons instead of hide
1732
  ui->actionPush->setEnabled(state);
×
1733
  ui->actionUpdate->setEnabled(state);
×
1734
}
×
1735

1736
/**
1737
 * @brief MainWindow::critical critical message popup wrapper.
1738
 * @param title
1739
 * @param msg
1740
 */
1741
void MainWindow::critical(const QString &title, const QString &msg) {
×
1742
  QMessageBox::critical(this, title, msg);
×
1743
}
×
1744

1745
/**
1746
 * @brief Appends processed command output to the output panel.
1747
 *
1748
 * Appends text to the process output text edit, with per-line numbering,
1749
 * optional command prefix, and color coding for errors vs. success.
1750
 * Handles auto-scrolling and line limits.
1751
 *
1752
 * @param output The raw output text from the command.
1753
 * @param isError true if this is error output (stderr).
1754
 * @param linePrefix Optional command name to prefix each line with.
1755
 */
1756
void MainWindow::appendProcessOutput(const QString &output, bool isError,
×
1757
                                     const QString &linePrefix) {
1758
  if (!QtPassSettings::isShowProcessOutput()) {
×
1759
    return;
×
1760
  }
1761

1762
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
×
1763
  for (QString &line : lines) {
×
1764
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1765
    // indentation
1766
    line.remove('\r');
×
1767
    while (!line.isEmpty() && line.back().isSpace()) {
×
1768
      line.chop(1);
×
1769
    }
1770
    if (line.isEmpty()) {
×
1771
      continue;
×
1772
    }
1773

1774
    m_outputCounter++;
×
1775
    QString lineNumber = QString::number(m_outputCounter);
×
1776

1777
    QColor textColor =
1778
        isError ? QColor(Qt::red)
×
1779
                : ui->processOutputEdit->palette().color(QPalette::Text);
×
1780
    QString colorHex = textColor.name();
×
1781
    // Apply the optional prefix per line so multi-line output stays
1782
    // attributed to its command (e.g. all 3 lines of a `git push` show
1783
    // "git push: ..." rather than only the first).
1784
    QString prefixed =
1785
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
×
1786
    QString coloredOutput =
1787
        QString("<span style=\"color: %1;\">%2: %3</span>")
×
1788
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
×
1789

1790
    ui->processOutputEdit->append(coloredOutput);
×
1791
  }
1792

1793
  limitOutputLines();
×
1794

1795
  if (m_autoScroll) {
×
1796
    ui->processOutputEdit->verticalScrollBar()->setValue(
×
1797
        ui->processOutputEdit->verticalScrollBar()->maximum());
×
1798
  }
1799
}
1800

1801
/**
1802
 * @brief Handles process output from the Pass executor.
1803
 *
1804
 * Called when any non-sensitive process completes. Filters out password-
1805
 * related commands (pass show, insert, etc.) and delegates to
1806
 * appendProcessOutput.
1807
 *
1808
 * @param output The stdout/stderr text from the process.
1809
 * @param isError true if this is error output (stderr).
1810
 * @param pid The process ID identifying which command ran.
1811
 */
1812
void MainWindow::onProcessOutput(const QString &output, bool isError,
×
1813
                                 Enums::PROCESS pid) {
1814
  appendProcessOutput(output, isError, getProcessName(pid));
×
1815
}
×
1816

1817
/**
1818
 * @brief Maps a process ID to its human-readable command name.
1819
 *
1820
 * Returns static strings for git/pass commands that appear in output.
1821
 * Password-related commands return empty (they are filtered).
1822
 *
1823
 * @param pid The process ID to look up.
1824
 * @return QString with command name, or empty if filtered.
1825
 */
1826
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
×
1827
  switch (pid) {
×
1828
  case Enums::GIT_INIT:
×
1829
    return QStringLiteral("git init"); // no-tr
×
1830
  case Enums::GIT_ADD:
×
1831
    return QStringLiteral("git add"); // no-tr
×
1832
  case Enums::GIT_COMMIT:
×
1833
    return QStringLiteral("git commit"); // no-tr
×
1834
  case Enums::GIT_RM:
×
1835
    return QStringLiteral("git rm"); // no-tr
×
1836
  case Enums::GIT_PULL:
×
1837
    return QStringLiteral("git pull"); // no-tr
×
1838
  case Enums::GIT_PUSH:
×
1839
    return QStringLiteral("git push"); // no-tr
×
1840
  case Enums::GIT_MOVE:
×
1841
    return QStringLiteral("git mv"); // no-tr
×
1842
  case Enums::GIT_COPY:
×
1843
    return QStringLiteral("git cp"); // no-tr
×
1844
  case Enums::PASS_INSERT:
×
1845
    return QStringLiteral("pass insert"); // no-tr
×
1846
  case Enums::PASS_REMOVE:
×
1847
    return QStringLiteral("pass rm"); // no-tr
×
1848
  case Enums::PASS_INIT:
×
1849
    return QStringLiteral("pass init"); // no-tr
×
1850
  case Enums::PASS_MOVE:
×
1851
    return QStringLiteral("pass mv"); // no-tr
×
1852
  case Enums::PASS_COPY:
×
1853
    return QStringLiteral("pass cp"); // no-tr
×
1854
  case Enums::PASS_GREP:
×
1855
    return QStringLiteral("pass grep"); // no-tr
×
1856
  case Enums::GPG_GENKEYS:
×
1857
    return QStringLiteral("gpg --gen-key"); // no-tr
×
1858
  case Enums::PASS_SHOW:
1859
  case Enums::PASS_OTP_GENERATE:
1860
  case Enums::PROCESS_COUNT:
1861
  case Enums::INVALID:
1862
    break;
1863
  }
1864
  return QString();
1865
}
1866

1867
/**
1868
 * @brief Checks if a process ID represents a sensitive operation whose
1869
 * output should not be shown in the process output panel.
1870
 *
1871
 * Password-related commands (pass show, OTP generate, grep, insert)
1872
 * display their output in other UI areas, so we skip them here.
1873
 *
1874
 * @param pid The process ID to check.
1875
 * @return true if the process is sensitive and should be filtered.
1876
 */
1877
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
1878
  switch (pid) {
×
1879
  case Enums::PASS_SHOW:
1880
  case Enums::PASS_OTP_GENERATE:
1881
  case Enums::PASS_GREP:
1882
  case Enums::PASS_INSERT:
1883
    return true;
1884
  case Enums::GIT_INIT:
1885
  case Enums::GIT_ADD:
1886
  case Enums::GIT_COMMIT:
1887
  case Enums::GIT_RM:
1888
  case Enums::GIT_PULL:
1889
  case Enums::GIT_PUSH:
1890
  case Enums::GIT_MOVE:
1891
  case Enums::GIT_COPY:
1892
  case Enums::PASS_REMOVE:
1893
  case Enums::PASS_INIT:
1894
  case Enums::PASS_MOVE:
1895
  case Enums::PASS_COPY:
1896
  case Enums::GPG_GENKEYS:
1897
  case Enums::PROCESS_COUNT:
1898
  case Enums::INVALID:
1899
    break;
1900
  }
1901
  return false;
×
1902
}
1903

1904
/**
1905
 * @brief Updates the visibility of the process output panel.
1906
 *
1907
 * Shows or hides the process output widget based on the user's
1908
 * showProcessOutput setting.
1909
 */
1910
void MainWindow::updateProcessOutputVisibility() {
×
1911
  ui->processOutputWidget->setVisible(QtPassSettings::isShowProcessOutput());
×
1912
}
×
1913

1914
/**
1915
 * @brief Limits the output panel to max lines, trimming old excess.
1916
 *
1917
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
1918
 * Called after each append to prevent unbounded growth.
1919
 */
1920
void MainWindow::limitOutputLines() {
×
1921
  QTextDocument *doc = ui->processOutputEdit->document();
×
1922
  int excess = doc->blockCount() - MaxOutputLines;
×
1923
  if (excess <= 0) {
×
1924
    return;
×
1925
  }
1926

1927
  QTextCursor cursor(doc);
×
1928
  cursor.movePosition(QTextCursor::Start);
×
1929
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
1930
  cursor.removeSelectedText();
×
1931
}
×
1932

1933
/**
1934
 * @brief Clears the process output panel.
1935
 *
1936
 * Clears all output, resets the line counter, and re-enables auto-scroll.
1937
 */
1938
void MainWindow::on_clearOutputButton_clicked() {
×
1939
  ui->processOutputEdit->clear();
×
1940
  m_outputCounter = 0;
×
1941
  m_autoScroll = true;
×
1942
}
×
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