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

IJHack / QtPass / 36006993442

24 Sep 2026 01:37PM UTC coverage: 93.022% (+0.09%) from 92.935%
36006993442

Pull #1923

github

web-flow
Merge 16de8769a into f78497b22
Pull Request #1923: Split the 10–14 band where a function did several jobs

364 of 380 new or added lines in 10 files covered. (95.79%)

6 existing lines in 1 file now uncovered.

8878 of 9544 relevant lines covered (93.02%)

154.71 hits per line

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

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

5
#include "qtpasslogging.h"
6

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

55
MainWindow::MainWindow(const QString &searchText, QWidget *parent)
109 ✔
56
    : QMainWindow(parent), ui(new Ui::MainWindow) {
109 ✔
57
#ifdef __APPLE__
58
  // extra treatment for mac os
59
  // see https://doc.qt.io/qt-6/qkeysequence.html#qt_set_sequence_auto_mnemonic
60
  qt_set_sequence_auto_mnemonic(true);
61
#endif
62
  ui->setupUi(this);
109 ✔
63

64
  m_qtPass = new QtPass(this);
109 ✔
65
  restoreWindow();
109 ✔
66
  connectBackends();
109 ✔
67

68
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
109 ✔
69
                &MainWindow::copyPasswordFromTreeview);
109 ✔
70

71
  m_tree = new StoreTree(ui->treeView, this);
109 ✔
72
  m_tree->setStore(QtPassSettings::getPassStore(Util::findPasswordStore()));
109 ✔
73
  m_tree->setPass(QtPassSettings::getPass());
109 ✔
74
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
109 ✔
75
          &MainWindow::showContextMenu);
109 ✔
76
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
109 ✔
77
          &MainWindow::deselect);
109 ✔
78

79
  initTextBrowser();
109 ✔
80

81
  updateProfileBox();
109 ✔
82

83
  initDisplayPanel();
109 ✔
84

85
  QtPassSettings::getPass()->updateEnv();
109 ✔
86

87
  // Install the search-box key filter once, not on every setUiElementsEnabled
88
  // call.
89
  ui->lineEdit->installEventFilter(this);
109 ✔
90
  // Both live in Breeze's "tools area" and get its header palette; see the
91
  // PaletteChange branch in eventFilter().
92
  ui->toolBar->installEventFilter(this);
93
  menuBar()->installEventFilter(this);
109 ✔
94

95
  initTimers();
109 ✔
96

97
  initToolBarButtons();
109 ✔
98
  initStatusBar();
109 ✔
99
  initProcessOutput();
109 ✔
100

101
  ui->lineEdit->setClearButtonEnabled(true);
109 ✔
102
  updateGrepButtonVisibility();
109 ✔
103

104
  setUiElementsEnabled(true);
109 ✔
105

106
  ui->lineEdit->setText(searchText);
109 ✔
107

108
  // No QApplication::quit() here: before exec() it is a documented no-op.
109
  // main() checks initSucceeded() and exits before show() instead.
110
  m_qtPass->init();
109 ✔
111
  // OK is not gated on Util::configIsValid(): an accepted configuration can
112
  // still lack a .gpg-id, so ask again; only a cancel ends the loop.
113
  while (!Util::configIsValid(QtPassSettings::load())) {
110 ✔
114
    if (!config()) {
2 ✔
115
      return;
116
    }
117
  }
118
  m_freshStart = false;
108 ✔
119
  m_initSucceeded = true;
108 ✔
120

121
  // Initial focus is set in showEvent(): a 10 ms QTimer here could fire
122
  // before the window was realised (queued ActivationChange, nested exec() in
123
  // init()) and selectAll() segfaulted inside Qt (#1187, #1188).
NEW
124
}
×
125

126
void MainWindow::connectBackends() {
109 ✔
127
  connect(m_qtPass, &QtPass::outputReady, this,
109 ✔
128
          [this](const QString &html) { flashText(html, false, true); });
130 ✔
129
  connect(m_qtPass, &QtPass::operationFinished, this,
109 ✔
130
          [this]() { setUiElementsEnabled(true); });
130 ✔
131
  connect(m_qtPass, &QtPass::statusMessage, this,
109 ✔
132
          &MainWindow::showStatusMessage);
109 ✔
133
  connect(m_qtPass, &QtPass::pushRequested, this, &MainWindow::onPush);
109 ✔
134
  connect(m_qtPass, &QtPass::entryInserted, this,
109 ✔
135
          [this]() { on_treeView_clicked(getCurrentTreeViewIndex()); });
110 ✔
136
  connect(&m_qtPass->clipboard(), &ClipboardManager::statusMessage, this,
109 ✔
137
          [this](const QString &message) { showStatusMessage(message); });
117 ✔
138
  // Both backends: the active one can change through the configuration
139
  // dialog without a restart.
140
  connectPassSignals(QtPassSettings::getRealPass());
109 ✔
141
  connectPassSignals(QtPassSettings::getImitatePass());
109 ✔
142
  ImitatePass *ipass = QtPassSettings::getImitatePass();
109 ✔
143
  connect(ipass, &ImitatePass::startReencryptPath, this,
109 ✔
144
          &MainWindow::startReencryptPath);
109 ✔
145
  connect(ipass, &ImitatePass::reencryptProgress, this,
109 ✔
146
          &MainWindow::reencryptProgress);
109 ✔
147
  connect(ipass, &ImitatePass::endReencryptPath, this,
109 ✔
148
          &MainWindow::endReencryptPath);
109 ✔
149
}
109 ✔
150

151
void MainWindow::initTextBrowser() {
109 ✔
152
  {
153
    const AppSettings s = QtPassSettings::load();
109 ✔
154
    if (s.useMonospace) {
109 ✔
155
      QFont monospace("Monospace");
1 ✔
156
      monospace.setStyleHint(QFont::Monospace);
1 ✔
157
      ui->textBrowser->setFont(monospace);
1 ✔
158
    }
1 ✔
159
    if (s.noLineWrapping) {
109 ✔
160
      ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
1 ✔
161
    }
162
    clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
109 ✔
163
  }
109 ✔
164
  ui->textBrowser->setOpenExternalLinks(true);
109 ✔
165
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
109 ✔
166
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
109 ✔
167
          &MainWindow::showBrowserContextMenu);
109 ✔
168
}
109 ✔
169

170
void MainWindow::initDisplayPanel() {
109 ✔
171
  m_displayPanel = new PasswordDisplayPanel(
109 ✔
172
      ui->gridLayout, ui->verticalLayoutPassword, this, this);
109 ✔
173
  connect(m_displayPanel, &PasswordDisplayPanel::copyRequested,
109 ✔
174
          &m_qtPass->clipboard(), &ClipboardManager::copyText);
109 ✔
175
  connect(m_displayPanel, &PasswordDisplayPanel::qrRequested, m_qtPass,
109 ✔
176
          &QtPass::showTextAsQRCode);
109 ✔
177
  // Double-click on what is shown edits it, as in the tree.
178
  connect(m_displayPanel, &PasswordDisplayPanel::editRequested, this, [this] {
109 ✔
179
    if (ui->actionEdit->isEnabled()) {
2 ✔
180
      onEdit();
1 ✔
181
    }
182
  });
2 ✔
183
}
109 ✔
184

185
void MainWindow::initTimers() {
109 ✔
186
  clearPanelTimer.setSingleShot(true);
109 ✔
187
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
111 ✔
188

189
  searchTimer.setInterval(350);
109 ✔
190
  searchTimer.setSingleShot(true);
109 ✔
191

192
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
109 ✔
193

194
  // Safety net: if a backend operation disables the UI but never signals
195
  // completion, re-enable after a timeout so the window can't get stuck.
196
  m_uiWatchdog.setSingleShot(true);
109 ✔
197
  m_uiWatchdog.setInterval(UiWatchdogMs);
109 ✔
198
  connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
109 ✔
199
    showStatusMessage(tr("Operation timed out; re-enabling interface."));
×
200
    // Drop any in-flight OTP request so a late finishedShow cannot be mistaken
201
    // for the answer to it.
202
    cancelOtpRequest();
×
203
    setUiElementsEnabled(true);
×
204
  });
×
205
}
109 ✔
206

207
void MainWindow::initProcessOutput() {
109 ✔
208
  m_processOutput = new ProcessOutputPanel(this);
109 ✔
209
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
109 ✔
210
  // After addDockWidget so our preference wins over any cached state
211
  // QMainWindow applies; restoreWindow() already processed the saved layout.
212
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
109 ✔
213
  // From the setting, not isVisible(): the window is not shown yet, so no
214
  // child of it reports visible.
215
  ui->actionShowProcessOutput->setChecked(
109 ✔
216
      QtPassSettings::isShowProcessOutput());
109 ✔
217
  connect(ui->actionShowProcessOutput, &QAction::toggled, this,
109 ✔
218
          [this](bool show) {
109 ✔
219
            m_processOutput->setVisible(show);
2 ✔
220
            AppSettings s = QtPassSettings::load();
2 ✔
221
            if (s.showProcessOutput != show) {
2 ✔
222
              s.showProcessOutput = show;
2 ✔
223
              QtPassSettings::save(s);
2 ✔
224
            }
225
          });
2 ✔
226

227
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
109 ✔
228
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
109 ✔
229
            // Never route potentially-secret output through the panel:
230
            // PASS_SHOW goes to the text browser (cleared on a timer when
231
            // "Autoclear panel" is set),
232
            // PASS_GREP returns password-file lines (#252), and PASS_INSERT
233
            // is excluded defensively in case a future path uses --echo.
234
            if (ProcessOutputPanel::isSensitiveProcess(pid)) {
4 ✔
235
              return;
236
            }
237
            if (!out.isEmpty()) {
3 ✔
238
              onProcessOutput(out, false, pid);
1 ✔
239
            }
240
            if (!err.isEmpty()) {
3 ✔
241
              onProcessOutput(err, true, pid);
3 ✔
242
            }
243
          });
244
}
109 ✔
245

246
MainWindow::~MainWindow() {
214 ✔
247
  // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
248
  // window still knows its last geometry here.
249
  saveWindowState();
109 ✔
250
  delete m_qtPass;
109 ✔
251
}
323 ✔
252

253
// The signals of one backend that the window answers itself.
254
void MainWindow::connectPassSignals(Pass *pass) {
218 ✔
255
  // A failed decrypt never emits finishedShow; drop the pending OTP/copy
256
  // request so a later decrypt of the same entry is not taken as its answer.
257
  connect(pass, &Pass::processErrorExit, this, &MainWindow::onOperationError);
218 ✔
258
  connect(pass, &Pass::critical, this, &MainWindow::critical);
218 ✔
259
  connect(pass, &Pass::startingExecuteWrapper, this,
218 ✔
260
          &MainWindow::executeWrapperStarted);
218 ✔
261
  connect(pass, &Pass::statusMsg, this, &MainWindow::showStatusMessage);
218 ✔
262
  connect(pass, &Pass::finishedShow, this, &MainWindow::passShowHandler);
218 ✔
263
  // Both check the file against their own pending request and ignore the
264
  // rest, so they can stay connected for good.
265
  connect(pass, &Pass::finishedShow, this, &MainWindow::otpFromFileToClipboard);
218 ✔
266
  connect(pass, &Pass::finishedShow, this,
218 ✔
267
          &MainWindow::passwordFromFileToClipboard);
218 ✔
268
  connect(pass, &Pass::finishedGrep, this, &MainWindow::onGrepFinished);
218 ✔
269
}
218 ✔
270

271
// Idempotent; called from closeEvent() and the destructor.
272
void MainWindow::saveWindowState() {
111 ✔
273
  QtPassSettings::setGeometry(saveGeometry());
111 ✔
274
  QtPassSettings::setSavestate(saveState());
111 ✔
275
}
111 ✔
276

277
void MainWindow::focusInput() {
36 ✔
278
  // findChild() rather than ui->lineEdit: on a fresh-config first launch
279
  // config() calls setWindowFlags(), which rebuilds the native window, and
280
  // under Qt 6.11 the QLineEdit was destroyed while ui->lineEdit kept its old
281
  // address (SIGSEGV in QWidget::testAttribute). findChild() returns null.
282
  if (!isVisible()) {
36 ✔
283
    return;
284
  }
285
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
36 ✔
286
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
36 ✔
287
    return;
288
  }
289
  lineEdit->selectAll();
36 ✔
290
  lineEdit->setFocus();
36 ✔
291
  // Only once focus has landed: set in showEvent(), an early return above
292
  // would consume the one-shot and never retry.
293
  m_firstShowCompleted = true;
36 ✔
294
}
295

296
auto MainWindow::event(QEvent *event) -> bool {
4,717 ✔
297
  if (event->type() == QEvent::ApplicationPaletteChange) {
4,717 ✔
298
    m_themeSwitched = true;
7 ✔
299
  }
300
  return QMainWindow::event(event);
4,717 ✔
301
}
302

303
void MainWindow::changeEvent(QEvent *event) {
145 ✔
304
  QWidget::changeEvent(event);
145 ✔
305
  if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
145 ✔
306
    // Desktop switched light/dark. Top-level widgets get PaletteChange
307
    // (ApplicationPaletteChange goes to QApplication); stylesheets with
308
    // baked-in palette colours must be re-derived by hand.
309
    m_displayPanel->refreshPalette();
7 ✔
310
  }
311
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
145 ✔
312
      isVisible()) {
313
    // Defer a tick so the activation dispatch chain unwinds first: calling
314
    // focusInput() inline has segfaulted on half-rebuilt ui state.
315
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
21 ✔
316
                              Qt::QueuedConnection);
317
  }
318
}
145 ✔
319

320
void MainWindow::showEvent(QShowEvent *event) {
23 ✔
321
  QMainWindow::showEvent(event);
23 ✔
322
  if (m_firstShowCompleted) {
23 ✔
323
    return;
324
  }
325
  // Queued so the platform map and pending rebuilds (setWindowFlags on the
326
  // wizard path) settle first. focusInput() sets the latch only on success,
327
  // so a failed lookup retries on the next show.
328
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
21 ✔
329
                            Qt::QueuedConnection);
330
}
331

332
void MainWindow::initToolBarButtons() {
109 ✔
333
  connect(ui->actionAddPassword, &QAction::triggered, this,
109 ✔
334
          &MainWindow::addPassword);
109 ✔
335
  connect(ui->actionAddFolder, &QAction::triggered, this,
109 ✔
336
          &MainWindow::addFolder);
109 ✔
337
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
109 ✔
338
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
109 ✔
339
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
109 ✔
340
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
109 ✔
341
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
109 ✔
342
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
109 ✔
343
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
109 ✔
344

345
  // Menu-only. Quit ends the application, tray icon or not, as in KeePassXC,
346
  // Telegram and the KDE, GNOME and macOS guidelines (#1788). Close is
347
  // close(), a hide when "hide on close" is set, like the X button and Alt+F4.
348
  connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
109 ✔
349
  connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
109 ✔
350
#ifdef Q_OS_MACOS
351
  // The menu bar is the system's up there; nothing to hide.
352
  ui->actionShowMenuBar->setVisible(false);
353
#else
354
  // The action lives in the menu it hides, so it has to be on the window as
355
  // well for Ctrl+M to keep working while the bar is gone.
356
  addAction(ui->actionShowMenuBar);
109 ✔
357
  ui->actionShowMenuBar->setChecked(QtPassSettings::load().showMenuBar);
109 ✔
358
  menuBar()->setVisible(ui->actionShowMenuBar->isChecked());
109 ✔
359
  connect(ui->actionShowMenuBar, &QAction::toggled, this, [this](bool show) {
109 ✔
360
    menuBar()->setVisible(show);
4 ✔
361
    AppSettings s = QtPassSettings::load();
4 ✔
362
    s.showMenuBar = show;
4 ✔
363
    QtPassSettings::save(s);
4 ✔
364
  });
4 ✔
365
#endif
366
  connect(ui->actionFaq, &QAction::triggered, this, [] {
110 ✔
367
    QDesktopServices::openUrl(QUrl(QStringLiteral("https://qtpass.org/faq")));
2 ✔
368
  });
1 ✔
369
  connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
109 ✔
370
  connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
109 ✔
371

372
  ui->actionAddPassword->setIcon(
109 ✔
373
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
436 ✔
374
  ui->actionAddFolder->setIcon(
109 ✔
375
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
436 ✔
376
  ui->actionEdit->setIcon(QIcon::fromTheme(
109 ✔
377
      "document-properties", QIcon(":/icons/document-properties.svg")));
327 ✔
378
  ui->actionDelete->setIcon(
109 ✔
379
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
436 ✔
380
  ui->actionPush->setIcon(
109 ✔
381
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
436 ✔
382
  ui->actionUpdate->setIcon(
109 ✔
383
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
436 ✔
384
  ui->actionUsers->setIcon(QIcon::fromTheme(
109 ✔
385
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
327 ✔
386
  ui->actionConfig->setIcon(QIcon::fromTheme(
109 ✔
387
      "applications-system", QIcon(":/icons/applications-system.svg")));
218 ✔
388
}
109 ✔
389

390
void MainWindow::initStatusBar() {
109 ✔
391
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
218 ✔
392

393
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
218 ✔
394
                     .scaledToHeight(statusBar()->height());
109 ✔
395
  auto *logoApp = new QLabel(statusBar());
109 ✔
396
  logoApp->setPixmap(logo);
109 ✔
397
  statusBar()->addPermanentWidget(logoApp);
109 ✔
398
}
109 ✔
399

400
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
1 ✔
401
  return ui->treeView->currentIndex();
1 ✔
402
}
403

404
void MainWindow::flashText(const QString &text, const bool isError,
47 ✔
405
                           const bool isHtml) {
406
  if (isError) {
47 ✔
407
    ui->textBrowser->setTextColor(Qt::red);
7 ✔
408
  } else {
409
    // setText() re-applies the current char format, red after an error, to
410
    // the whole document. Clear the foreground rather than pin a palette
411
    // colour, so text follows runtime light/dark switches (#946).
412
    QTextCharFormat format = ui->textBrowser->currentCharFormat();
40 ✔
413
    format.clearForeground();
414
    ui->textBrowser->setCurrentCharFormat(format);
40 ✔
415
  }
416

417
  if (isHtml) {
47 ✔
418
    QString _text = text;
419
    if (!ui->textBrowser->toPlainText().isEmpty()) {
76 ✔
420
      _text = ui->textBrowser->toHtml() + _text;
28 ✔
421
    }
422
    ui->textBrowser->setHtml(_text);
38 ✔
423
  } else {
424
    ui->textBrowser->setText(text);
9 ✔
425
  }
426
}
47 ✔
427

428
void MainWindow::applyTextBrowserSettings() {
2 ✔
429
  const AppSettings s = QtPassSettings::load();
2 ✔
430
  if (s.useMonospace) {
2 ✔
431
    QFont monospace("Monospace");
1 ✔
432
    monospace.setStyleHint(QFont::Monospace);
1 ✔
433
    ui->textBrowser->setFont(monospace);
1 ✔
434
  } else {
1 ✔
435
    ui->textBrowser->setFont(QFont());
1 ✔
436
  }
437

438
  if (s.noLineWrapping) {
2 ✔
439
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
1 ✔
440
  } else {
441
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
1 ✔
442
  }
443
}
2 ✔
444

445
void MainWindow::applyWindowFlagsSettings() {
120 ✔
446
  const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
120 ✔
447
  const Qt::WindowFlags flags = windowFlags();
448
  if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
120 ✔
449
    // setWindowFlags() rebuilds the native window, the teardown that left
450
    // ui->lineEdit dangling (see focusInput()); skip it when nothing changes.
451
    return;
452
  }
453
  // Toggle just this hint; resetting to Qt::Window discarded every other flag.
454
  const bool wasVisible = isVisible();
455
  setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
4 ✔
456
                           : (flags & ~Qt::WindowStaysOnTopHint));
457
  // setWindowFlags() hides a shown window; a window that was not shown yet
458
  // (first-run wizard from the constructor) is left to its normal show.
459
  if (wasVisible) {
2 ✔
460
    show();
2 ✔
461
  }
462
}
463

464
auto MainWindow::config() -> bool {
4 ✔
465
  if (m_freshStart) {
4 ✔
466
    // No usable configuration yet: the wizard finds the programs, the key
467
    // and the store, and creates the store when it has to.
468
    FirstRunWizard wizard(this);
2 ✔
469
    if (wizard.exec() != QDialog::Accepted) {
2 ✔
470
      return false;
471
    }
472
  } else {
2 ✔
473
    ConfigDialog d(this);
2 ✔
474
    d.setModal(true);
2 ✔
475
    if (d.exec() != QDialog::Accepted) {
2 ✔
476
      return false;
477
    }
478
  }
2 ✔
479

480
  applyTextBrowserSettings();
2 ✔
481
  applyWindowFlagsSettings();
2 ✔
482

483
  updateProfileBox();
2 ✔
484
  const AppSettings s = QtPassSettings::load();
2 ✔
485
  m_tree->setStore(s.passStore);
2 ✔
486
  deselect();
2 ✔
487
  ui->treeView->setCurrentIndex(QModelIndex());
2 ✔
488

489
  Pass *activePass = QtPassSettings::getPass();
2 ✔
490
  activePass->updateEnv();
2 ✔
491
  m_tree->setPass(activePass);
2 ✔
492
  clearPanelTimer.setInterval(MS_PER_SECOND * s.autoclearPanelSeconds);
2 ✔
493
  m_qtPass->clipboard().setAutoclearTimer();
2 ✔
494

495
  updateGitButtonVisibility();
2 ✔
496
  updateOtpButtonVisibility();
2 ✔
497
  updateGrepButtonVisibility();
2 ✔
498
  updateProcessOutputVisibility();
2 ✔
499
#ifndef Q_OS_MACOS
500
  // The action's toggled handler shows or hides the bar (and saves again).
501
  ui->actionShowMenuBar->setChecked(s.showMenuBar);
2 ✔
502
#endif
503
  if (s.useTrayIcon && m_tray == nullptr) {
2 ✔
504
    initTrayIcon();
1 ✔
505
  } else if (!s.useTrayIcon && m_tray != nullptr) {
1 ✔
506
    destroyTrayIcon();
×
507
  }
508

509
  // Stay fresh-start while the configuration is unusable, so the
510
  // constructor's loop re-runs the wizard. The re-prompt lives there:
511
  // recursing here never reported a cancel back to the caller.
512
  if (Util::configIsValid(s)) {
2 ✔
513
    m_freshStart = false;
2 ✔
514
  }
515
  return true;
516
}
2 ✔
517

518
void MainWindow::onUpdate(bool block) {
2 ✔
519
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
2 ✔
520
  if (block) {
2 ✔
521
    QtPassSettings::getPass()->GitPull_b();
1 ✔
522
  } else {
523
    QtPassSettings::getPass()->GitPull();
1 ✔
524
  }
525
}
2 ✔
526

527
void MainWindow::onPush() {
1 ✔
528
  if (QtPassSettings::isUseGit()) {
1 ✔
529
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
1 ✔
530
    QtPassSettings::getPass()->GitPush();
1 ✔
531
  }
532
}
1 ✔
533

534
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
32 ✔
535
  return m_tree->fileFor(index, forPass);
32 ✔
536
}
537

538
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
10 ✔
539
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
10 ✔
540
  QString file = getFile(index, true);
10 ✔
541
  ui->passwordName->setText(file);
10 ✔
542
  if (!file.isEmpty() && !cleared) {
10 ✔
543
    // Remember what the panel is about to show, so onOtp() can tell whether the
544
    // code it can see belongs to the entry that is currently selected.
545
    m_shownFile = file;
9 ✔
546
    QtPassSettings::getPass()->Show(file);
18 ✔
547
  } else {
548
    m_shownFile.clear();
1 ✔
549
    clearPanel(false);
1 ✔
550
    ui->actionEdit->setEnabled(false);
1 ✔
551
    ui->actionDelete->setEnabled(true);
1 ✔
552
  }
553
}
10 ✔
554

555
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
3 ✔
556
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
3 ✔
557

558
  if (fileOrFolder.isFile()) {
3 ✔
559
    editPassword(getFile(index, true));
4 ✔
560
  }
561
}
3 ✔
562

563
void MainWindow::deselect() {
8 ✔
564
  m_shownFile.clear();
8 ✔
565
  cancelOtpRequest();
8 ✔
566
  m_qtPass->clipboard().clear();
8 ✔
567
  ui->treeView->clearSelection();
8 ✔
568
  ui->actionEdit->setEnabled(false);
8 ✔
569
  ui->actionDelete->setEnabled(false);
8 ✔
570
  ui->passwordName->setText("");
8 ✔
571
  clearPanel(false);
8 ✔
572
}
8 ✔
573

574
void MainWindow::executeWrapperStarted() {
27 ✔
575
  m_displayPanel->clear();
27 ✔
576
  ui->textBrowser->clear();
27 ✔
577
  setUiElementsEnabled(false);
27 ✔
578
  clearPanelTimer.stop();
27 ✔
579
  if (QtPassSettings::isShowProcessOutput()) {
27 ✔
580
    m_processOutput->setVisible(true);
22 ✔
581
  }
582
}
27 ✔
583

584
void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
17 ✔
585
  // Only the entry we most recently asked to show may repaint the panel; a
586
  // slower decrypt of an entry the user has since left is dropped.
587
  if (!file.isEmpty() && !m_shownFile.isEmpty() && file != m_shownFile) {
17 ✔
588
    return;
1 ✔
589
  }
590
  const AppSettings s = QtPassSettings::load();
16 ✔
591
  QStringList templ =
592
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
16 ✔
593
  bool allFields = s.useTemplate && s.templateAllFields;
16 ✔
594
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
16 ✔
595
  QString output = p_output;
596
  // Display variant: empty when the password line is itself an otpauth URI, so
597
  // the shared secret is neither rendered nor copied to the clipboard.
598
  QString password = fileContent.getPasswordForDisplay();
16 ✔
599

600
  // Skipped for an OTP request: the user asked for a code, and two
601
  // OleSetClipboard calls in one turn can leave the Windows clipboard empty.
602
  if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
16 ✔
603
    m_qtPass->clipboard().copyIfAlways(password, p_output);
14 ✔
604
  }
605

606
  m_displayPanel->clear();
16 ✔
607

608
  if (s.hideContent) {
16 ✔
609
    output = "***" + tr("Content hidden") + "***";
4 ✔
610
  } else if (!s.displayAsIs) {
14 ✔
611
    m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
28 ✔
612
                                  s.useOtp ? fileContent.getOtpUri()
14 ✔
613
                                           : QString());
614
    output = fileContent.getRemainingDataForDisplay();
28 ✔
615
  }
616

617
  if (s.useAutoclearPanel) {
16 ✔
618
    clearPanelTimer.start();
2 ✔
619
  }
620

621
  flashText(QtPass::formatOutput(output), false, true);
32 ✔
622
  setUiElementsEnabled(true);
16 ✔
623
}
32 ✔
624

625
void MainWindow::otpFromFileToClipboard(const QString &p_output,
17 ✔
626
                                        const QString &file) {
627
  // Permanently connected: only the decrypt of the entry onOtp() asked for
628
  // is ours, everything else belongs to a tree click or a Ctrl+C.
629
  if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
17 ✔
630
    return;
14 ✔
631
  }
632
  m_otpRequestFile.clear();
5 ✔
633

634
  if (p_output.isEmpty()) {
5 ✔
635
    // Distinguish "could not read the entry" from "entry has no OTP".
636
    flashText(tr("Could not decrypt this password entry"), true);
1 ✔
637
    setUiElementsEnabled(true);
1 ✔
638
    return;
1 ✔
639
  }
640

641
  // passShowHandler ran first for this finishedShow; if it rendered the OTP
642
  // row, reuse its code (as onOtp()'s fast path does). Otherwise parse:
643
  // hideContent, displayAsIs or no OTP field.
644
  const QString shown = m_displayPanel->currentOtpCode();
4 ✔
645
  if (!shown.isEmpty()) {
4 ✔
646
    m_qtPass->clipboard().copyText(shown);
1 ✔
647
    showStatusMessage(tr("OTP code copied to clipboard"));
1 ✔
648
    setUiElementsEnabled(true);
1 ✔
649
    return;
650
  }
651

652
  const AppSettings s = QtPassSettings::load();
3 ✔
653
  // Parse with the same template settings passShowHandler uses, so an OTP
654
  // field is recognised identically in both paths.
655
  const QStringList templ =
656
      s.useTemplate ? s.passTemplate.split("\n") : QStringList();
3 ✔
657
  const bool allFields = s.useTemplate && s.templateAllFields;
3 ✔
658
  const FileContent fileContent =
659
      FileContent::parse(p_output, templ, allFields);
3 ✔
660

661
  const std::optional<Totp::Settings> settings =
662
      Totp::parse(fileContent.getOtpUri());
6 ✔
663
  if (settings.has_value()) {
3 ✔
664
    m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
4 ✔
665
    showStatusMessage(tr("OTP code copied to clipboard"));
4 ✔
666
  } else {
667
    flashText(tr("No OTP code found in this password entry"), true);
2 ✔
668
  }
669
  setUiElementsEnabled(true);
3 ✔
670
}
6 ✔
671

672
void MainWindow::clearPanel(bool notify) {
29 ✔
673
  m_displayPanel->clear();
29 ✔
674
  const bool grepWasVisible = ui->grepResultsList->isVisible();
29 ✔
675
  ui->grepResultsList->clear();
29 ✔
676
  if (grepWasVisible) {
29 ✔
677
    ui->grepResultsList->setVisible(false);
2 ✔
678
    ui->treeView->setVisible(true);
2 ✔
679
    if (m_grep.inGrepMode()) {
2 ✔
680
      m_grep.clearGrepMode();
681
      ui->grepButton->blockSignals(true);
2 ✔
682
      ui->grepButton->setChecked(false);
2 ✔
683
      ui->grepButton->blockSignals(false);
2 ✔
684
      ui->lineEdit->blockSignals(true);
2 ✔
685
      ui->lineEdit->clear();
2 ✔
686
      ui->lineEdit->blockSignals(false);
2 ✔
687
      ui->lineEdit->setPlaceholderText(tr("Search password"));
4 ✔
688
    }
689
  }
690
  if (notify) {
29 ✔
691
    QString output = "***" + tr("Password and content hidden") + "***";
4 ✔
692
    ui->textBrowser->setHtml(output);
2 ✔
693
  } else {
694
    ui->textBrowser->setHtml("");
54 ✔
695
  }
696
}
29 ✔
697

698
void MainWindow::setUiElementsEnabled(bool state) {
213 ✔
699
  // A running re-encryption owns the UI state: the progress dialog's Cancel is
700
  // the user's escape hatch and endReencryptPath() releases the interface, so
701
  // neither an unrelated completion nor the watchdog may re-enable it early.
702
  if (state && m_reencryptRunning) {
213 ✔
703
    m_uiWatchdog.stop();
1 ✔
704
    return;
1 ✔
705
  }
706
  // Arm the watchdog while the UI is disabled; disarm once re-enabled.
707
  if (state) {
708
    m_uiWatchdog.stop();
166 ✔
709
  } else {
710
    m_uiWatchdog.start();
46 ✔
711
  }
712
  ui->treeView->setEnabled(state);
212 ✔
713
  ui->lineEdit->setEnabled(state);
212 ✔
714
  ui->actionAddPassword->setEnabled(state);
212 ✔
715
  ui->actionAddFolder->setEnabled(state);
212 ✔
716
  ui->actionUsers->setEnabled(state);
212 ✔
717
  ui->actionConfig->setEnabled(state);
212 ✔
718
  const bool uiEnabled = state;
719
  // is a file selected?
720
  state &= ui->treeView->currentIndex().isValid();
424 ✔
721
  ui->actionDelete->setEnabled(state);
212 ✔
722
  ui->actionEdit->setEnabled(state);
212 ✔
723
  updateGitButtonVisibility(uiEnabled);
212 ✔
724
  // `state` is now "UI enabled AND a file is selected", which is exactly when
725
  // generating an OTP makes sense.
726
  updateOtpButtonVisibility(state);
212 ✔
727
}
728

729
void MainWindow::restoreWindow() {
118 ✔
730
  // saveGeometry() covers position, size, maximized state and screen;
731
  // anything applied on top discards it. Centre only when nothing was saved.
732
  const QByteArray geometry = QtPassSettings::getGeometry();
236 ✔
733
  if (geometry.isEmpty() || !restoreGeometry(geometry)) {
118 ✔
734
    WindowStateStore::centreOnCursorScreen(*this);
4 ✔
735
  }
736
  restoreState(QtPassSettings::getSavestate(saveState()));
118 ✔
737
  const AppSettings s = QtPassSettings::load();
118 ✔
738

739
  applyWindowFlagsSettings();
118 ✔
740

741
  if (s.useTrayIcon && m_tray == nullptr) {
118 ✔
742
    initTrayIcon();
1 ✔
743
    if (s.startMinimized) {
1 ✔
744
      // since we are still in constructor, can't directly hide
745
      QTimer::singleShot(10, this, &MainWindow::hide);
2 ✔
746
    }
747
  } else if (!s.useTrayIcon && m_tray != nullptr) {
117 ✔
748
    destroyTrayIcon();
×
749
  }
750
}
236 ✔
751

752
void MainWindow::onConfig() { config(); }
1 ✔
753

754
void MainWindow::showAbout() {
1 ✔
755
  QMessageBox::about(
1 ✔
756
      this, tr("About QtPass"),
1 ✔
757
      tr("<h3>QtPass %1</h3>"
1 ✔
758
         "<p>A multi-platform GUI for <a "
759
         "href=\"https://www.passwordstore.org/\">"
760
         "pass</a>, the standard Unix password manager.</p>"
761
         "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
762
         "<a href=\"https://github.com/IJHack/QtPass\">Source and "
763
         "issues</a></p>"
764
         "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
765
         "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
766
         "version 3</a> or later.</p>")
767
          .arg(QStringLiteral(VERSION),
2 ✔
768
               QString::number(QDate::currentDate().year())));
2 ✔
769
}
1 ✔
770

771
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
23 ✔
772
  if (m_grep.inGrepMode())
23 ✔
773
    return;
774
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
36 ✔
775
  ui->treeView->expandAll();
18 ✔
776
  clearPanel(false);
18 ✔
777
  ui->passwordName->setText("");
36 ✔
778
  ui->actionEdit->setEnabled(false);
18 ✔
779
  ui->actionDelete->setEnabled(false);
18 ✔
780
  searchTimer.start();
18 ✔
781
}
782

783
void MainWindow::onTimeoutSearch() {
17 ✔
784
  QString query = ui->lineEdit->text();
17 ✔
785

786
  if (query.isEmpty()) {
17 ✔
787
    ui->treeView->collapseAll();
1 ✔
788
    deselect();
1 ✔
789
  }
790

791
  // The words are matched literally, in order, with anything in between:
792
  // "work vpn" finds work/acme/vpn. Regular expressions belong to the
793
  // content search; here a typed "[" or "(" must not switch filtering off.
794
  QStringList words;
17 ✔
795
  for (const QString &word :
796
       query.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
51 ✔
797
    words << QRegularExpression::escape(word);
17 ✔
798
  }
799
  QRegularExpression regExp(words.join(QStringLiteral(".*")),
34 ✔
800
                            QRegularExpression::CaseInsensitiveOption);
17 ✔
801
  m_tree->setFilter(regExp);
17 ✔
802

803
  if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
17 ✔
804
    selectFirstFile();
14 ✔
805
  } else {
806
    ui->actionEdit->setEnabled(false);
3 ✔
807
    ui->actionDelete->setEnabled(false);
3 ✔
808
  }
809
}
34 ✔
810

811
void MainWindow::on_lineEdit_returnPressed() {
6 ✔
812
  qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
6 ✔
813
                    << m_tree->proxy().rowCount();
×
814

815
  if (m_grep.inGrepMode()) {
6 ✔
816
    const QString query = ui->lineEdit->text();
4 ✔
817
    if (!query.isEmpty()) {
4 ✔
818
      ui->grepResultsList->clear();
3 ✔
819
      ui->statusBar->showMessage(tr("Searching…"));
6 ✔
820
      if (m_grep.beginSearch()) {
821
        QApplication::setOverrideCursor(Qt::WaitCursor);
3 ✔
822
      }
823
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
6 ✔
824
    } else {
825
      if (m_grep.cancelSearch()) {
826
        QApplication::restoreOverrideCursor();
1 ✔
827
      }
828
      ui->grepResultsList->clear();
1 ✔
829
      ui->grepResultsList->setVisible(false);
1 ✔
830
      ui->treeView->setVisible(true);
1 ✔
831
    }
832
    return;
833
  }
834

835
  if (m_tree->proxy().rowCount() > 0) {
2 ✔
836
    selectFirstFile();
2 ✔
837
    on_treeView_clicked(ui->treeView->currentIndex());
2 ✔
838
  }
839
}
840

841
void MainWindow::on_grepButton_toggled(bool checked) {
10 ✔
842
  const AppSettings s = QtPassSettings::load();
10 ✔
843
  if (checked) {
10 ✔
844
    m_grep.enterGrepMode();
845
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
12 ✔
846
    // The regex dialect depends on the backend (see Pass::Grep): the pass
847
    // backend uses POSIX BRE via `pass grep`, the native backend uses PCRE.
848
    ui->lineEdit->setToolTip(
6 ✔
849
        s.usePass
6 ✔
850
            ? tr("Content search uses POSIX basic regular expressions "
12 ✔
851
                 "(pass grep).")
852
            : tr("Content search uses Perl-compatible regular expressions "
853
                 "(PCRE)."));
854
    ui->lineEdit->clear();
6 ✔
855
    searchTimer.stop();
6 ✔
856
    m_tree->setFilter(QRegularExpression());
6 ✔
857
    ui->grepResultsList->setVisible(false);
6 ✔
858
    // Keep treeView visible until results arrive
859
  } else {
860
    if (m_grep.leaveGrepMode()) {
861
      QApplication::restoreOverrideCursor();
1 ✔
862
    }
863
    searchTimer.stop();
4 ✔
864
    ui->lineEdit->blockSignals(true);
4 ✔
865
    ui->lineEdit->clear();
4 ✔
866
    ui->lineEdit->blockSignals(false);
4 ✔
867
    ui->lineEdit->setPlaceholderText(tr("Search password"));
8 ✔
868
    ui->lineEdit->setToolTip(QString());
8 ✔
869
    ui->grepResultsList->clear();
4 ✔
870
    ui->grepResultsList->setVisible(false);
4 ✔
871
    ui->treeView->setVisible(true);
4 ✔
872
    m_tree->setFilter(QRegularExpression());
4 ✔
873
  }
874
}
10 ✔
875

876
void MainWindow::onGrepFinished(
6 ✔
877
    const QList<QPair<QString, QStringList>> &results) {
878
  const GrepSearchController::FinishOutcome outcome = m_grep.finishSearch();
879
  if (outcome.restoreCursor) {
6 ✔
880
    QApplication::restoreOverrideCursor();
1 ✔
881
  }
882
  // Re-enable the UI before the discard check so a cancelled search can never
883
  // leave controls disabled.
884
  setUiElementsEnabled(true);
6 ✔
885
  if (outcome.discard) {
6 ✔
886
    return;
3 ✔
887
  }
888
  if (!m_grep.inGrepMode())
4 ✔
889
    return;
890
  ui->grepResultsList->clear();
4 ✔
891
  if (results.isEmpty()) {
4 ✔
892
    ui->statusBar->showMessage(tr("No matches found."), 3000);
2 ✔
893
    ui->grepResultsList->setVisible(false);
1 ✔
894
    ui->treeView->setVisible(true);
1 ✔
895
    return;
1 ✔
896
  }
897
  const AppSettings s = QtPassSettings::load();
3 ✔
898
  const bool hideContent = s.hideContent;
3 ✔
899
  int totalLines = 0;
900
  for (const auto &pair : results) {
23 ✔
901
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
20 ✔
902
    entryItem->setText(0, pair.first);
20 ✔
903
    entryItem->setData(0, Qt::UserRole, pair.first);
20 ✔
904
    for (const QString &line : pair.second) {
40 ✔
905
      auto *lineItem = new QTreeWidgetItem(entryItem);
20 ✔
906
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
60 ✔
907
                                       : line);
908
      lineItem->setData(0, Qt::UserRole, pair.first);
20 ✔
909
      ++totalLines;
20 ✔
910
    }
911
  }
912
  ui->grepResultsList->expandAll();
3 ✔
913
  ui->treeView->setVisible(false);
3 ✔
914
  ui->grepResultsList->setVisible(true);
3 ✔
915
  ui->statusBar->showMessage(
6 ✔
916
      tr("Found %n match(es)", nullptr, totalLines) + " " +
6 ✔
917
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
3 ✔
918
      3000);
919
  if (s.useAutoclearPanel)
3 ✔
920
    clearPanelTimer.start();
1 ✔
921
}
3 ✔
922

923
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
1 ✔
924
                                                int /*column*/) {
925
  const AppSettings s = QtPassSettings::load();
1 ✔
926
  const QString entry = item->data(0, Qt::UserRole).toString();
1 ✔
927
  if (entry.isEmpty())
1 ✔
928
    return;
929
  const QString fullPath =
930
      QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
2 ✔
931
  const QModelIndex proxyIndex = m_tree->indexFor(fullPath);
1 ✔
932
  if (!proxyIndex.isValid())
933
    return;
934
  ui->treeView->setCurrentIndex(proxyIndex);
1 ✔
935
  on_treeView_clicked(proxyIndex);
1 ✔
936
  if (s.hideContent || s.useAutoclearPanel)
1 ✔
937
    ui->grepResultsList->clear();
1 ✔
938
  ui->grepResultsList->setVisible(false);
1 ✔
939
  ui->treeView->setVisible(true);
1 ✔
940
  ui->treeView->scrollTo(proxyIndex);
1 ✔
941
  ui->treeView->setFocus();
1 ✔
942
}
1 ✔
943

944
void MainWindow::selectFirstFile() {
16 ✔
945
  QModelIndex index = firstFile(m_tree->rootIndex());
16 ✔
946
  ui->treeView->setCurrentIndex(index);
16 ✔
947
}
16 ✔
948

949
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
24 ✔
950
  StoreModel &proxy = m_tree->proxy();
24 ✔
951
  int numRows = proxy.rowCount(parentIndex);
24 ✔
952
  for (int row = 0; row < numRows; ++row) {
34 ✔
953
    QModelIndex index = proxy.index(row, 0, parentIndex);
24 ✔
954
    if (m_tree->fileInfo(index).isFile()) {
24 ✔
955
      return index;
14 ✔
956
    }
957
    if (proxy.hasChildren(index)) {
15 ✔
958
      QModelIndex childFile = firstFile(index);
8 ✔
959
      if (childFile.isValid())
960
        return childFile;
5 ✔
961
    }
962
  }
963
  return QModelIndex();
964
}
965

966
// Guards the input dialogs (create, move, rename) against names like
967
// "../../etc/passwd" or absolute paths escaping the store root.
968
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
8 ✔
969
  if (PathValidator::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
16 ✔
970
    return true;
971
  }
972
  QMessageBox::warning(this, tr("Invalid name"),
6 ✔
973
                       tr("That name would resolve outside the password "
3 ✔
974
                          "store. Please choose a different name."));
975
  return false;
3 ✔
976
}
977

978
void MainWindow::setPassword(const QString &file, bool isNew) {
4 ✔
979
  const AppSettings s = QtPassSettings::load();
4 ✔
980
  PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
4 ✔
981

982
  if (isNew) {
4 ✔
983
    const QString storePath = s.passStore;
984
    QString folder = m_tree->currentDir(false);
1 ✔
985
    if (folder.isEmpty()) {
1 ✔
986
      folder = storePath;
×
987
    }
988
    // Folders to pick from, the tree's current one preselected. Real folders
989
    // only: a linked or junctioned one would write the entry outside the store.
990
    QStringList folders{QString()};
2 ✔
991
    const QStringList found = Util::directoriesUnder(storePath);
1 ✔
992
    for (const QString &path : found) {
15 ✔
993
      const QString rel = QDir(storePath).relativeFilePath(path);
14 ✔
994
      if (!rel.startsWith(u'.') && !rel.contains(QStringLiteral("/."))) {
42 ✔
995
        folders << rel;
996
      }
997
    }
998
    std::sort(folders.begin() + 1, folders.end());
1 ✔
999
    d.setNewEntryLocation(storePath, folders,
1 ✔
1000
                          QDir(storePath).relativeFilePath(folder));
2 ✔
1001
    QHash<QString, QStringList> templates =
1002
        TemplateIO::readTemplates(storePath);
1 ✔
1003
    if (!templates.isEmpty()) {
1004
      QString defaultTemplate =
1005
          TemplateIO::getFolderTemplate(folder, storePath);
1 ✔
1006
      d.setAvailableTemplates(templates, defaultTemplate);
1 ✔
1007
    }
1008
  }
1 ✔
1009

1010
  if (!d.exec()) {
4 ✔
1011
    ui->treeView->setFocus();
3 ✔
1012
  }
1013
}
4 ✔
1014

1015
void MainWindow::addPassword() { setPassword(QString()); }
2 ✔
1016

1017
auto MainWindow::confirmDeletion(const QString &file, bool isDir) -> bool {
4 ✔
1018
  const QString path = QDir::separator() + file;
4 ✔
1019
  if (!isDir) {
4 ✔
1020
    return QMessageBox::question(
4 ✔
1021
               this, tr("Delete password?"),
2 ✔
1022
               tr("Are you sure you want to delete %1?").arg(path),
4 ✔
1023
               QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
2 ✔
1024
  }
1025
  // Whole sentences, not a question stitched from pieces: a verb-final
1026
  // language cannot put the rest of the question after the path.
1027
  QString question =
1028
      tr("Are you sure you want to delete %1 and the whole content?").arg(path);
2 ✔
1029
  // A link, junction or special file inside is as unexpected as a stray
1030
  // plain file: the walker leaves them out of content and reports them.
1031
  QStringList skipped;
2 ✔
1032
  const QStringList content = Util::regularFilesUnder(
2 ✔
1033
      m_tree->fileSystem().rootPath() + path, {QStringLiteral("*")}, &skipped);
8 ✔
1034
  const bool unexpected =
1035
      !skipped.isEmpty() ||
4 ✔
1036
      std::any_of(content.cbegin(), content.cend(), [](const QString &entry) {
4 ✔
1037
        return QFileInfo(entry).suffix() != QLatin1String("gpg");
4 ✔
1038
      });
1039
  if (unexpected) {
1040
    question += QStringLiteral("<br><strong>") +
2 ✔
1041
                tr("Attention: there are unexpected files in the given "
1 ✔
1042
                   "folder, check them before continuing.") +
1 ✔
1043
                QStringLiteral("</strong>");
1 ✔
1044
  }
1045
  return QMessageBox::question(this, tr("Delete folder?"), question,
4 ✔
1046
                               QMessageBox::Yes | QMessageBox::No) ==
1047
         QMessageBox::Yes;
2 ✔
1048
}
1049

1050
auto MainWindow::confirmLinkRemoval(const QString &file) -> bool {
1 ✔
1051
  // Only the link goes (ImitatePass::Remove unlinks it); nothing behind it
1052
  // is looked at or mentioned.
1053
  return QMessageBox::question(
2 ✔
1054
             this, tr("Delete link?"),
1 ✔
1055
             tr("%1 is a symbolic link or junction. Remove the link? What it "
1 ✔
1056
                "points to is left alone.")
1057
                 .arg(QDir::separator() + file),
2 ✔
1058
             QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes;
2 ✔
1059
}
1060

1061
void MainWindow::onDelete() {
8 ✔
1062
  const QModelIndex currentIndex = ui->treeView->currentIndex();
8 ✔
1063
  if (!currentIndex.isValid()) {
1064
    // Otherwise the whole password directory would be deleted (#556).
1065
    return;
5 ✔
1066
  }
1067

1068
  const bool isDir = !m_tree->fileInfo(currentIndex).isFile();
6 ✔
1069
  const QString file =
1070
      isDir ? m_tree->currentDir(true) : getFile(currentIndex, true);
6 ✔
1071
  const QString folder =
1072
      m_tree->fileSystem().rootPath() + QDir::separator() + file;
6 ✔
1073
  const bool linkedFolder = isDir && Util::isLinkedFolder(folder);
6 ✔
1074

1075
  if (!linkedFolder && refuseLinkedFolder(folder)) {
5 ✔
1076
    // Behind a link: deleting would reach outside the store.
1077
    return;
1078
  }
1079
  const bool confirmed =
1080
      linkedFolder ? confirmLinkRemoval(file) : confirmDeletion(file, isDir);
5 ✔
1081
  if (!confirmed) {
5 ✔
1082
    return;
1083
  }
1084
  QtPassSettings::getPass()->Remove(file, isDir);
6 ✔
1085
}
1086

1087
void MainWindow::cancelOtpRequest() {
25 ✔
1088
  m_otpRequestFile.clear();
25 ✔
1089
  m_copyRequestFile.clear();
25 ✔
1090
}
25 ✔
1091

1092
void MainWindow::onOperationError() {
16 ✔
1093
  cancelOtpRequest();
16 ✔
1094
  // executeWrapperStarted() emptied the panel and the browser when the
1095
  // operation started (a refusal says so too); what is left to forget is
1096
  // which entry the panel was about to show.
1097
  m_shownFile.clear();
16 ✔
1098
}
16 ✔
1099

1100
// Derives the code in-process from one decrypt, so it works with either
1101
// backend on every platform; otpFromFileToClipboard() finishes the job.
1102
void MainWindow::onOtp() {
10 ✔
1103
  QString file = getFile(ui->treeView->currentIndex(), true);
10 ✔
1104
  if (file.isEmpty()) {
10 ✔
1105
    flashText(tr("No password selected for OTP generation"), true);
1 ✔
1106
    return;
1 ✔
1107
  }
1108
  if (!QtPassSettings::isUseOtp()) {
9 ✔
1109
    // Normally unreachable (the action is hidden), but never fail silently.
1110
    flashText(tr("No OTP code found in this password entry"), true);
1 ✔
1111
    return;
1 ✔
1112
  }
1113

1114
  // Fast path: the panel shows a live code, so no decrypt, and one clipboard
1115
  // write (two setMimeData calls in one turn can empty the Windows
1116
  // clipboard). Only for *this* entry: arrow keys and right-click move
1117
  // currentIndex without QTreeView::clicked, so the code may be another's.
1118
  const QString shown =
1119
      (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
8 ✔
1120
  if (!shown.isEmpty()) {
8 ✔
1121
    m_qtPass->clipboard().copyText(shown);
1 ✔
1122
    showStatusMessage(tr("OTP code copied to clipboard"));
2 ✔
1123
    return;
1124
  }
1125

1126
  // Fallback: decrypt once. The request file keeps passShowHandler from
1127
  // copying the password and tells otpFromFileToClipboard its answer.
1128
  m_otpRequestFile = file;
7 ✔
1129
  setUiElementsEnabled(false);
7 ✔
1130
  // Keep the marker in step so a second request takes the fast path; safe as
1131
  // executeWrapperStarted() clears the panel, so a failed decrypt shows none.
1132
  m_shownFile = file;
7 ✔
1133
  QtPassSettings::getPass()->Show(file);
14 ✔
1134
}
1135

1136
void MainWindow::onEdit() {
1 ✔
1137
  QString file = getFile(ui->treeView->currentIndex(), true);
1 ✔
1138
  editPassword(file);
1 ✔
1139
}
1 ✔
1140

1141
void MainWindow::onUsers() {
2 ✔
1142
  const QString dir = m_tree->currentDir(false);
2 ✔
1143
  if (refuseLinkedFolder(dir)) {
2 ✔
1144
    return;
1145
  }
1146

1147
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
1 ✔
1148
  if (!d.exec()) {
1 ✔
1149
    ui->treeView->setFocus();
1 ✔
1150
  }
1151
}
1 ✔
1152

1153
void MainWindow::messageAvailable(const QString &message) {
2 ✔
1154
  show();
2 ✔
1155
  raise();
2 ✔
1156
  if (message.isEmpty()) {
2 ✔
1157
    focusInput();
1 ✔
1158
  } else {
1159
    ui->treeView->expandAll();
1 ✔
1160
    ui->lineEdit->setText(message);
1 ✔
1161
    on_lineEdit_returnPressed();
1 ✔
1162
  }
1163
}
2 ✔
1164

1165
void MainWindow::updateProfileBox() {
111 ✔
1166
  const Profiles profiles = QtPassSettings::getProfiles();
111 ✔
1167

1168
  if (profiles.isEmpty()) {
1 ✔
1169
    ui->profileWidget->hide();
110 ✔
1170
  } else {
1171
    ui->profileWidget->show();
1 ✔
1172
    ui->profileBox->setEnabled(profiles.size() > 1);
2 ✔
1173
    ui->profileBox->clear();
1 ✔
1174
    for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
3 ✔
1175
      if (!i.key().isEmpty()) {
2 ✔
1176
        ui->profileBox->addItem(i.key());
2 ✔
1177
      }
1178
    }
1179
    ui->profileBox->model()->sort(0);
1 ✔
1180
  }
1181
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
222 ✔
1182
  if (index != -1) { //  -1 for not found
111 ✔
1183
    ui->profileBox->setCurrentIndex(index);
1 ✔
1184
  }
1185
}
111 ✔
1186

1187
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
3 ✔
1188
  if (m_freshStart || name == QtPassSettings::getProfile()) {
9 ✔
1189
    return;
2 ✔
1190
  }
1191

1192
  ui->lineEdit->clear();
1 ✔
1193

1194
  const Profile prof = QtPassSettings::getProfiles().value(name);
2 ✔
1195
  AppSettings s = QtPassSettings::load();
1 ✔
1196
  s.activeProfile = name;
1 ✔
1197
  s.passStore = prof.path;
1 ✔
1198
  s.passSigningKey = prof.signingKey;
1 ✔
1199
  // A profile without its own Git flags keeps the global values.
1200
  s.useGit = prof.useGit.value_or(s.useGit);
1 ✔
1201
  s.autoPush = prof.autoPush.value_or(s.autoPush);
1 ✔
1202
  s.autoPull = prof.autoPull.value_or(s.autoPull);
1 ✔
1203
  QtPassSettings::save(s);
1 ✔
1204
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
2 ✔
1205

1206
  QtPassSettings::getPass()->updateEnv();
1 ✔
1207

1208
  m_tree->setStore(QtPassSettings::getPassStore());
2 ✔
1209
  deselect();
1 ✔
1210
  ui->treeView->setCurrentIndex(QModelIndex());
1 ✔
1211
}
1 ✔
1212

1213
void MainWindow::initTrayIcon() {
2 ✔
1214
  m_tray = new TrayIcon(this);
2 ✔
1215
  if (!m_tray->getIsAllocated()) {
2 ✔
1216
    destroyTrayIcon();
2 ✔
1217
  }
1218
}
2 ✔
1219

1220
void MainWindow::destroyTrayIcon() {
2 ✔
1221
  delete m_tray;
2 ✔
1222
  m_tray = nullptr;
2 ✔
1223
}
2 ✔
1224

1225
void MainWindow::closeEvent(QCloseEvent *event) {
2 ✔
1226
  // Save in both branches: a tray user only ever hides the window, and
1227
  // used to quit through the tray menu with nothing ever written.
1228
  saveWindowState();
2 ✔
1229
  if (QtPassSettings::isHideOnClose()) {
2 ✔
1230
    this->hide();
2 ✔
1231
    event->ignore();
1232
  } else {
1233
    m_qtPass->clipboard().clear();
×
1234
    event->accept();
1235
    // A visible QSystemTrayIcon keeps the application alive, so
1236
    // quitOnLastWindowClosed never fires; quit explicitly.
1237
    QApplication::quit();
×
1238
  }
1239
}
2 ✔
1240

1241
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
4,471 ✔
1242
  if ((obj == ui->toolBar || obj == menuBar()) &&
4,471 ✔
1243
      event->type() == QEvent::PaletteChange) {
1244
    // Breeze gives these bars a "header" palette and, after a runtime
1245
    // light/dark switch, stamps the previous theme's colours back from a
1246
    // cached kdeglobals, hiding the toolbar icons. Deferred so we never
1247
    // re-enter the style while it is still applying its palette.
1248
    QTimer::singleShot(0, this, &MainWindow::dropStaleToolsAreaPalettes);
24 ✔
1249
  }
1250
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
4,471 ✔
1251
    auto *key = dynamic_cast<QKeyEvent *>(event);
1 ✔
1252
    if (key != nullptr && key->key() == Qt::Key_Down) {
1 ✔
1253
      ui->treeView->setFocus();
1 ✔
1254
    }
1255
  }
1256
  return QObject::eventFilter(obj, event);
4,471 ✔
1257
}
1258

1259
// Only once the application palette changed (see m_themeSwitched): a bar
1260
// whose active Window lightness is far from the application's shows the
1261
// other theme.
1262
void MainWindow::dropStaleToolsAreaPalettes() {
16 ✔
1263
  if (!m_themeSwitched) {
16 ✔
1264
    return;
1265
  }
1266
  constexpr int kStaleToolsAreaLightness = 64;
1267
  // The active group on both sides: a bar's palette() reads the inactive
1268
  // group while another window has focus (the settings app, during a theme
1269
  // switch), and Breeze gives the header set different inactive colours.
1270
  const int appLightness = QApplication::palette()
28 ✔
1271
                               .color(QPalette::Active, QPalette::Window)
1272
                               .lightness();
14 ✔
1273
  for (QWidget *bar : {static_cast<QWidget *>(ui->toolBar),
56 ✔
1274
                       static_cast<QWidget *>(menuBar())}) {
42 ✔
1275
    if (!bar->testAttribute(Qt::WA_SetPalette)) {
28 ✔
1276
      continue;
24 ✔
1277
    }
1278
    const int barLightness =
1279
        bar->palette().color(QPalette::Active, QPalette::Window).lightness();
8 ✔
1280
    if (qAbs(barLightness - appLightness) <= kStaleToolsAreaLightness) {
4 ✔
1281
      continue;
2 ✔
1282
    }
1283
    bar->setPalette(QPalette());
2 ✔
1284
    // Both bars are transparent and the style paints the tools area from the
1285
    // stale palette, so fill our own. Window, not their Button role, which is
1286
    // not the window colour in either theme.
1287
    bar->setBackgroundRole(QPalette::Window);
2 ✔
1288
    bar->setAutoFillBackground(true);
2 ✔
1289
  }
1290
}
1291

1292
void MainWindow::keyPressEvent(QKeyEvent *event) {
5 ✔
1293
  switch (event->key()) {
5 ✔
1294
  case Qt::Key_Delete:
1 ✔
1295
    onDelete();
1 ✔
1296
    break;
1 ✔
1297
  case Qt::Key_Return:
1 ✔
1298
  case Qt::Key_Enter:
1299
    if (m_tree->proxy().rowCount() > 0) {
1 ✔
1300
      on_treeView_clicked(ui->treeView->currentIndex());
1 ✔
1301
    }
1302
    break;
1303
  case Qt::Key_Escape:
1 ✔
1304
    ui->lineEdit->clear();
1 ✔
1305
    break;
1 ✔
1306
  default:
1307
    break;
1308
  }
1309
}
5 ✔
1310

1311
void MainWindow::addShareMenu(QMenu &contextMenu, const AppSettings &s) {
9 ✔
1312
  const QString dirPath = QDir::cleanPath(m_tree->currentDir(false));
9 ✔
1313
  auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
9 ✔
1314
  contextMenu.addMenu(shareMenu);
9 ✔
1315

1316
  const QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
9 ✔
1317
  const bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
9 ✔
1318
  const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
9 ✔
1319
  const bool gpgAvailable =
1320
      !exePath.isEmpty() &&
18 ✔
1321
      (Executor::parseWslCommand(exePath) || QFile(exePath).exists());
18 ✔
1322

1323
  QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
9 ✔
1324
  reencrypt->setEnabled(gpgIdExists && gpgAvailable);
9 ✔
1325
  connect(reencrypt, &QAction::triggered, this,
9 ✔
1326
          [this, dirPath]() { reencryptPath(dirPath); });
30 ✔
1327

1328
  QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
9 ✔
1329
  exportKey->setEnabled(gpgAvailable);
9 ✔
1330
  connect(exportKey, &QAction::triggered, this, &MainWindow::exportPublicKey);
9 ✔
1331

1332
  QAction *addRecipientAction = shareMenu->addAction(tr("Add recipient..."));
9 ✔
1333
  addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
9 ✔
1334
  connect(addRecipientAction, &QAction::triggered, this,
9 ✔
1335
          [this, dirPath]() { addRecipient(dirPath); });
29 ✔
1336

1337
  QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
9 ✔
1338
  connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
9 ✔
1339
}
9 ✔
1340

1341
void MainWindow::showContextMenu(const QPoint &pos) {
11 ✔
1342
  const AppSettings s = QtPassSettings::load();
11 ✔
1343
  const QModelIndex index = ui->treeView->indexAt(pos);
11 ✔
1344
  const bool selected = index.isValid();
1345
  if (!selected) {
1346
    ui->treeView->clearSelection();
1 ✔
1347
    ui->actionDelete->setEnabled(false);
1 ✔
1348
    ui->actionEdit->setEnabled(false);
1 ✔
1349
  }
1350
  ui->treeView->setCurrentIndex(index);
11 ✔
1351
  const QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
11 ✔
1352
  const QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
11 ✔
1353
  const bool isDir = fileOrFolder.isDir();
11 ✔
1354

1355
  QMenu contextMenu;
11 ✔
1356
  if (!selected || isDir) {
11 ✔
1357
    const std::array<std::pair<QString, void (MainWindow::*)()>, 4> onFolder{{
1358
        {tr("Open folder with file manager"), &MainWindow::openFolder},
10 ✔
1359
        {tr("Add folder"), &MainWindow::addFolder},
10 ✔
1360
        {tr("Add password"), &MainWindow::addPassword},
10 ✔
1361
        {tr("Users"), &MainWindow::onUsers},
10 ✔
1362
    }};
×
1363
    for (const auto &[text, slot] : onFolder) {
50 ✔
1364
      connect(contextMenu.addAction(text), &QAction::triggered, this, slot);
40 ✔
1365
    }
1366
  } else if (fileOrFolder.isFile()) {
1 ✔
1367
    connect(contextMenu.addAction(tr("Edit")), &QAction::triggered, this,
1 ✔
1368
            &MainWindow::onEdit);
2 ✔
1369
  }
1370
  if (selected) {
11 ✔
1371
    contextMenu.addSeparator();
10 ✔
1372
    if (isDir) {
10 ✔
1373
      connect(contextMenu.addAction(tr("Rename folder")), &QAction::triggered,
9 ✔
1374
              this, &MainWindow::renameFolder);
18 ✔
1375
    } else if (fileOrFolder.isFile()) {
1 ✔
1376
      connect(contextMenu.addAction(tr("Rename password")), &QAction::triggered,
1 ✔
1377
              this, &MainWindow::renamePassword);
2 ✔
1378
    }
1379
    connect(contextMenu.addAction(tr("Delete")), &QAction::triggered, this,
10 ✔
1380
            &MainWindow::onDelete);
10 ✔
1381
    if (isDir) {
10 ✔
1382
      addShareMenu(contextMenu, s);
9 ✔
1383
    }
1384
  }
1385
  contextMenu.exec(globalPos);
11 ✔
1386
}
11 ✔
1387

1388
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
1 ✔
1389
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
1 ✔
1390
  // Reparent from textBrowser: a browser stylesheet cascades to the child
1391
  // QMenu and breaks its opaque native background.
1392
  contextMenu->setParent(this, contextMenu->windowFlags());
1 ✔
1393
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
1 ✔
1394

1395
  contextMenu->exec(globalPos);
1 ✔
1396
  delete contextMenu;
1 ✔
1397
}
1 ✔
1398

1399
void MainWindow::openFolder() {
1 ✔
1400
  QString dir = m_tree->currentDir(false);
1 ✔
1401

1402
  QString path = QDir::toNativeSeparators(dir);
1 ✔
1403
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
1 ✔
1404
}
1 ✔
1405

1406
void MainWindow::addFolder() {
5 ✔
1407
  const AppSettings s = QtPassSettings::load();
5 ✔
1408
  bool ok;
1409
  QString dir = m_tree->currentDir(false);
5 ✔
1410
  QString newdir =
1411
      QInputDialog::getText(this, tr("New folder"),
5 ✔
1412
                            tr("New folder: \n(Will be placed in %1 )")
5 ✔
1413
                                .arg(s.passStore + m_tree->currentDir(true)),
10 ✔
1414
                            QLineEdit::Normal, "", &ok);
10 ✔
1415
  if (!ok || newdir.isEmpty()) {
5 ✔
1416
    return;
1417
  }
1418
  newdir.prepend(dir);
1419
  if (!confirmPathInStore(newdir)) {
4 ✔
1420
    return;
1421
  }
1422
  if (!QDir().mkdir(newdir)) {
6 ✔
1423
    QMessageBox::warning(this, tr("Error"),
2 ✔
1424
                         tr("Failed to create folder: %1").arg(newdir));
2 ✔
1425
    return;
1 ✔
1426
  }
1427
  // With a signing key, ImitatePass::loadVerifiedRecipients and pass
1428
  // (PASSWORD_STORE_SIGNING_KEY) refuse a .gpg-id without .gpg-id.sig, and
1429
  // signing needs a passphrase just to make a folder. So it inherits the
1430
  // parent's signed list; UsersDialog (Init) gives it its own.
1431
  const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
2 ✔
1432
  if (s.addGPGId && !signingConfigured) {
2 ✔
1433
    // Seed from the parent's recipients in effect, not from `enabled` keys:
1434
    // that flag is only set in UsersDialog, so it left a zero-byte .gpg-id
1435
    // shadowing the parent's recipients (#1682).
1436
    if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
2 ✔
1437
      QMessageBox::warning(
1 ✔
1438
          this, tr("Error"),
1 ✔
1439
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
2 ✔
1440
      return;
1 ✔
1441
    }
1442
  }
1443
}
5 ✔
1444

1445
void MainWindow::renameFolder() {
3 ✔
1446
  bool ok;
1447
  QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
3 ✔
1448
  QString srcDirName = QDir(srcDir).dirName();
3 ✔
1449
  QString newName =
1450
      QInputDialog::getText(this, tr("Rename folder"), tr("Rename folder to: "),
3 ✔
1451
                            QLineEdit::Normal, srcDirName, &ok);
3 ✔
1452
  if (!ok || newName.isEmpty()) {
3 ✔
1453
    return;
1454
  }
1455
  QString destDir = srcDir;
1456
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
2 ✔
1457
  if (!confirmPathInStore(destDir)) {
2 ✔
1458
    return;
1459
  }
1460
  QtPassSettings::getPass()->Move(srcDir, destDir, false);
3 ✔
1461
}
1462

1463
void MainWindow::editPassword(const QString &file) {
3 ✔
1464
  if (!file.isEmpty()) {
3 ✔
1465
    const AppSettings s = QtPassSettings::load();
3 ✔
1466
    if (s.useGit && s.autoPull) {
3 ✔
1467
      onUpdate(true);
1 ✔
1468
    }
1469
    setPassword(file, false);
3 ✔
1470
  }
3 ✔
1471
}
3 ✔
1472

1473
void MainWindow::renamePassword() {
2 ✔
1474
  bool ok;
1475
  QString file = getFile(ui->treeView->currentIndex(), false);
2 ✔
1476
  QString filePath = QFileInfo(file).path();
2 ✔
1477
  QString fileName = QFileInfo(file).fileName();
2 ✔
1478
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
4 ✔
1479
    fileName.chop(4);
2 ✔
1480
  }
1481

1482
  QString newName =
1483
      QInputDialog::getText(this, tr("Rename file"), tr("Rename file to: "),
2 ✔
1484
                            QLineEdit::Normal, fileName, &ok);
2 ✔
1485
  if (!ok || newName.isEmpty()) {
2 ✔
1486
    return;
1487
  }
1488
  QString newFile = QDir(filePath).filePath(newName);
2 ✔
1489
  if (!confirmPathInStore(newFile)) {
2 ✔
1490
    return;
1491
  }
1492
  QtPassSettings::getPass()->Move(file, newFile, false);
3 ✔
1493
}
1494

1495
void MainWindow::copyPasswordFromTreeview() {
4 ✔
1496
  QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
4 ✔
1497

1498
  if (fileOrFolder.isFile()) {
4 ✔
1499
    QString file = getFile(ui->treeView->currentIndex(), true);
4 ✔
1500
    // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
1501
    // an earlier request is simply not this one's.
1502
    m_copyRequestFile = file;
4 ✔
1503
    // This Show repaints the panel as well; see onOtp().
1504
    m_shownFile = file;
4 ✔
1505
    QtPassSettings::getPass()->Show(file);
8 ✔
1506
  }
1507
}
4 ✔
1508

1509
void MainWindow::passwordFromFileToClipboard(const QString &text,
14 ✔
1510
                                             const QString &file) {
1511
  if (m_copyRequestFile.isEmpty() || file != m_copyRequestFile) {
14 ✔
1512
    return;
12 ✔
1513
  }
1514
  m_copyRequestFile.clear();
3 ✔
1515
  const QStringList tokens = text.split('\n');
3 ✔
1516
  if (tokens.isEmpty()) {
3 ✔
1517
    return;
1518
  }
1519
  // A `pass otp insert` entry's first line is the otpauth:// URI carrying the
1520
  // TOTP seed; copying it would leak 2FA material. Matches the display path
1521
  // (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1522
  if (FileContent::isOtpUriValue(tokens[0])) {
3 ✔
1523
    flashText(tr("This entry holds an OTP secret, not a password"), true);
1 ✔
1524
    return;
1 ✔
1525
  }
1526
  m_qtPass->clipboard().copyText(tokens[0]);
2 ✔
1527
}
1528

1529
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
19 ✔
1530
  ui->statusBar->showMessage(msg, timeout);
19 ✔
1531
}
19 ✔
1532

1533
void MainWindow::reencryptPath(const QString &dir) {
3 ✔
1534
  QDir checkDir(dir);
3 ✔
1535
  if (!checkDir.exists()) {
3 ✔
1536
    QMessageBox::critical(this, tr("Error"),
2 ✔
1537
                          tr("Directory does not exist: %1").arg(dir));
2 ✔
1538
    return;
1 ✔
1539
  }
1540
  if (refuseLinkedFolder(dir)) {
2 ✔
1541
    return;
1542
  }
1543

1544
  int ret = QMessageBox::question(
4 ✔
1545
      this, tr("Re-encrypt passwords"),
2 ✔
1546
      tr("Re-encrypt all passwords in %1?\n\n"
2 ✔
1547
         "This will re-encrypt ALL password files in this folder "
1548
         "using the current recipients defined in .gpg-id.\n\n"
1549
         "This may rewrite many files and cannot be undone easily.\n\n"
1550
         "Continue?")
1551
          .arg(QDir(dir).dirName()),
4 ✔
1552
      QMessageBox::Yes | QMessageBox::No);
1553

1554
  if (ret != QMessageBox::Yes)
2 ✔
1555
    return;
1556

1557
  // Disable now; the asynchronous startReencryptPath signal re-runs this
1558
  // harmlessly.
1559
  startReencryptPath();
1 ✔
1560

1561
  QtPassSettings::getImitatePass()->reencryptPath(
2 ✔
1562
      QDir::cleanPath(QDir(dir).absolutePath()));
2 ✔
1563
}
3 ✔
1564

1565
// Idempotent: reencryptPath() calls it before ImitatePass emits the signal.
1566
void MainWindow::startReencryptPath() {
6 ✔
1567
  m_reencryptRunning = true;
6 ✔
1568
  setUiElementsEnabled(false);
6 ✔
1569
  // The worker always ends with endReencryptPath(), and Cancel is available
1570
  // throughout, so the generic watchdog is not needed for this operation.
1571
  m_uiWatchdog.stop();
6 ✔
1572
  ui->treeView->setDisabled(true);
6 ✔
1573
  if (m_reencryptProgress)
6 ✔
1574
    return;
1575
  auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
4 ✔
1576
                                       tr("Cancel"), 0, 0, this);
8 ✔
1577
  progress->setWindowTitle(tr("Re-encrypt passwords"));
4 ✔
1578
  progress->setWindowModality(Qt::WindowModal);
4 ✔
1579
  progress->setAutoClose(false);
4 ✔
1580
  progress->setAutoReset(false);
4 ✔
1581
  progress->setMinimumDuration(0);
4 ✔
1582
  connect(progress, &QProgressDialog::canceled, this, [this]() {
4 ✔
1583
    QtPassSettings::getImitatePass()->cancelReencryptPath();
1 ✔
1584
    showStatusMessage(tr("Cancelling re-encryption"), 5000);
1 ✔
1585
  });
1 ✔
1586
  m_reencryptProgress = progress;
1587
  progress->show();
4 ✔
1588
}
1589

1590
void MainWindow::reencryptProgress(int current, int total) {
5 ✔
1591
  if (!m_reencryptProgress)
5 ✔
1592
    return;
1593
  m_reencryptProgress->setMaximum(total);
4 ✔
1594
  m_reencryptProgress->setLabelText(
4 ✔
1595
      tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
8 ✔
1596
  // On a modal dialog setValue() runs processEvents(), which can deliver
1597
  // endReencryptPath() and reset m_reencryptProgress: keep this last.
1598
  m_reencryptProgress->setValue(current);
4 ✔
1599
}
1600

1601
void MainWindow::endReencryptPath() {
4 ✔
1602
  m_reencryptRunning = false;
4 ✔
1603
  if (m_reencryptProgress) {
4 ✔
1604
    m_reencryptProgress->hide();
4 ✔
1605
    m_reencryptProgress->deleteLater();
4 ✔
1606
    m_reencryptProgress = nullptr;
1607
  }
1608
  setUiElementsEnabled(true);
4 ✔
1609
}
4 ✔
1610

1611
void MainWindow::exportPublicKey() {
3 ✔
1612
  const AppSettings s = QtPassSettings::load();
3 ✔
1613
  const QString identity = s.passSigningKey;
1614
  if (identity.isEmpty()) {
3 ✔
1615
    QMessageBox::information(
1 ✔
1616
        this, tr("Export public key"),
1 ✔
1617
        tr("<h3>Export your public key</h3>"
×
1618
           "<p>No signing key is configured. Set one in QtPass Settings "
1619
           "&gt; GPG keys, or run this in a terminal:</p>"
1620
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1621
           "&gt;</pre>"
1622
           "<p>Then send the file to your teammates.</p>"));
1623
    return;
1 ✔
1624
  }
1625
  QString gpgExe = s.gpgExecutable;
1626
  if (gpgExe.isEmpty()) {
2 ✔
1627
    gpgExe = QStringLiteral("gpg");
×
1628
  }
1629
  QStringList args = {"--armor", "--export"};
6 ✔
1630
  args.append(identity.split(' ', Qt::SkipEmptyParts));
2 ✔
1631
  QString stdOut;
2 ✔
1632
  QString stdErr;
2 ✔
1633
  int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
2 ✔
1634
  if (exitCode != 0 || stdOut.isEmpty()) {
2 ✔
1635
    QMessageBox::warning(this, tr("Export public key"),
2 ✔
1636
                         tr("Could not export public key for %1.\n\n%2")
1 ✔
1637
                             .arg(identity, stdErr.isEmpty()
2 ✔
1638
                                                ? tr("No output from gpg.")
2 ✔
1639
                                                : stdErr));
1640
    return;
1641
  }
1642
  ExportPublicKeyDialog dialog(identity, stdOut, this);
1 ✔
1643
  dialog.exec();
1 ✔
1644
}
6 ✔
1645

1646
// UsersDialog only ticks keys already in the keyring; importing a foreign
1647
// key still has to happen via gpg (or QtPass settings) first.
1648
void MainWindow::addRecipient(const QString &dir) {
2 ✔
1649
  if (refuseLinkedFolder(dir)) {
2 ✔
1650
    return;
1 ✔
1651
  }
1652
  UsersDialog d(QtPassSettings::getPass(), QtPassSettings::load(), dir, this);
1 ✔
1653
  d.exec();
1 ✔
1654
}
1 ✔
1655

1656
// A tree-picked folder that is, or lies behind, a symlink or junction is not
1657
// a folder of the store. The store root itself may be a link (a synced
1658
// folder); that is configuration, not a tree pick.
1659
auto MainWindow::refuseLinkedFolder(const QString &dir) -> bool {
11 ✔
1660
  if (!Util::isUnderLink(dir, QtPassSettings::load().passStore)) {
11 ✔
1661
    return false;
1662
  }
1663
  QMessageBox::critical(
3 ✔
1664
      this, tr("Not a folder of the store"),
3 ✔
1665
      tr("%1 is, or lies behind, a symbolic link or junction. What that "
3 ✔
1666
         "points to is not part of the password store and is left alone.")
1667
          .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
6 ✔
1668
  return true;
3 ✔
1669
}
1670

1671
void MainWindow::showShareHelp() {
1 ✔
1672
  QMessageBox::information(
1 ✔
1673
      this, tr("Sharing passwords with GPG"),
1 ✔
1674
      tr("<h3>Sharing passwords with GPG</h3>"
1 ✔
1675
         "<p>To share passwords with other users:</p>"
1676
         "<ol>"
1677
         "<li><b>Export your public key</b> and send it to teammates</li>"
1678
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1679
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1680
         "them</li>"
1681
         "</ol>"
1682
         "<p>Only people who have a matching secret key can decrypt the "
1683
         "passwords.</p>"
1684
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1685
         "<p>See the FAQ for more details.</p>"));
1686
}
1 ✔
1687

1688
void MainWindow::updateGitButtonVisibility(bool uiEnabled) {
214 ✔
1689
  const AppSettings s = QtPassSettings::load();
214 ✔
1690
  // Without Git there is nothing to push or pull, ever: the buttons go, not
1691
  // grey out. While an operation runs they stay and are disabled.
1692
  const bool usable =
1693
      s.useGit && !(s.gitExecutable.isEmpty() && s.passExecutable.isEmpty());
214 ✔
1694
  ui->actionPush->setVisible(usable);
214 ✔
1695
  ui->actionUpdate->setVisible(usable);
214 ✔
1696
  // The separator in front of them would otherwise sit next to the next one.
1697
  const QList<QAction *> actions = ui->toolBar->actions();
214 ✔
1698
  const qsizetype at = actions.indexOf(ui->actionPush);
214 ✔
1699
  if (at > 0 && actions.at(at - 1)->isSeparator()) {
214 ✔
1700
    actions.at(at - 1)->setVisible(usable);
214 ✔
1701
  }
1702
  enableGitButtons(usable && uiEnabled);
214 ✔
1703
}
214 ✔
1704

1705
void MainWindow::updateOtpButtonVisibility(bool uiEnabled) {
214 ✔
1706
  // No platform gating: codes are generated in-process. Visible but disabled
1707
  // when OTP is off: the panel never shows an OTP field (a secret), so hiding
1708
  // this too would leave no trace of the feature. Honour uiEnabled, or the
1709
  // user can stack Show calls while a decrypt is in flight.
1710
  ui->actionOtp->setVisible(true);
214 ✔
1711
  ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
214 ✔
1712
}
214 ✔
1713

1714
void MainWindow::updateGrepButtonVisibility() {
111 ✔
1715
  const bool enabled = QtPassSettings::isUseGrepSearch();
111 ✔
1716
  ui->grepButton->setVisible(enabled);
111 ✔
1717
  ui->grepCaseButton->setVisible(enabled);
111 ✔
1718
  if (!enabled && m_grep.inGrepMode()) {
111 ✔
1719
    ui->grepButton->setChecked(false);
1 ✔
1720
  }
1721
}
111 ✔
1722

1723
void MainWindow::enableGitButtons(const bool &state) {
214 ✔
1724
  ui->actionPush->setEnabled(state);
214 ✔
1725
  ui->actionUpdate->setEnabled(state);
214 ✔
1726
}
214 ✔
1727

1728
void MainWindow::critical(const QString &title, const QString &msg) {
2 ✔
1729
  QMessageBox::critical(this, title, msg);
2 ✔
1730
}
2 ✔
1731

1732
void MainWindow::onProcessOutput(const QString &output, bool isError,
6 ✔
1733
                                 Enums::PROCESS pid) {
1734
  if (!QtPassSettings::isShowProcessOutput()) {
6 ✔
1735
    return;
1736
  }
1737
  m_processOutput->append(output, isError,
5 ✔
1738
                          ProcessOutputPanel::processName(pid));
10 ✔
1739
}
1740

1741
void MainWindow::updateProcessOutputVisibility() {
2 ✔
1742
  // The action's toggled handler shows or hides the dock.
1743
  ui->actionShowProcessOutput->setChecked(
2 ✔
1744
      QtPassSettings::isShowProcessOutput());
2 ✔
1745
  m_processOutput->setVisible(QtPassSettings::isShowProcessOutput());
2 ✔
1746
}
2 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc