QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
mainwindow.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
9#include "mainwindow.h"
10
11#include "qtpasslogging.h"
12
13#include "configdialog.h"
14#include "enums.h"
15#include "executor.h"
17#include "filecontent.h"
18#include "firstrunwizard.h"
19#include "menubarpeek.h"
20#include "nativegrep.h"
21#include "passworddialog.h"
23#include "pathvalidator.h"
24#include "plainmessagebox.h"
25#include "processoutputpanel.h"
26#include "qpushbuttonasqrcode.h"
29#include "qtpass.h"
30#include "qtpasssettings.h"
31#include "templateio.h"
32#include "totp.h"
33#include "trayicon.h"
34#include "ui_mainwindow.h"
35#include "usersdialog.h"
36#include "util.h"
37#include "windowstatestore.h"
38#include <QApplication>
39#include <QCloseEvent>
40#include <QDate>
41#include <QDesktopServices>
42#include <QDialog>
43#include <QDockWidget>
44#include <QFileInfo>
45#include <QHBoxLayout>
46#include <QInputDialog>
47#include <QLabel>
48#include <QLineEdit>
49#include <QMenu>
50#include <QMenuBar>
51#include <QMessageBox>
52#include <QProgressDialog>
53#include <QPushButton>
54#include <QShortcut>
55#include <QTextCharFormat>
56#include <QTextStream>
57#include <QTimer>
58#include <QTreeWidget>
59#include <QUrl>
60#include <algorithm>
61#include <array>
62#include <utility>
63
64MainWindow::MainWindow(const QString &searchText, QWidget *parent)
65 : QMainWindow(parent), ui(new Ui::MainWindow) {
66#ifdef __APPLE__
67 // extra treatment for mac os
68 // see https://doc.qt.io/qt-6/qkeysequence.html#qt_set_sequence_auto_mnemonic
69 qt_set_sequence_auto_mnemonic(true);
70#endif
71 ui->setupUi(this);
72
73 m_qtPass = new QtPass(this);
76
77 new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this, this,
79
80 m_tree = new StoreTree(ui->treeView, this);
83 connect(ui->treeView, &QWidget::customContextMenuRequested, this,
85 connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
87
89
91
93
94 QtPassSettings::getPass()->updateEnv();
95
96 // Install the search-box key filter once, not on every setUiElementsEnabled
97 // call.
98 ui->lineEdit->installEventFilter(this);
99 // Both live in Breeze's "tools area" and get its header palette; see the
100 // PaletteChange branch in eventFilter().
101 ui->toolBar->installEventFilter(this);
102 menuBar()->installEventFilter(this);
103
104 initTimers();
105
109
110 ui->lineEdit->setClearButtonEnabled(true);
112
114
115 ui->lineEdit->setText(searchText);
116
117 // No QApplication::quit() here: before exec() it is a documented no-op.
118 // main() checks initSucceeded() and exits before show() instead.
119 m_qtPass->init();
120 // OK is not gated on Util::configIsValid(): an accepted configuration can
121 // still lack a .gpg-id, so ask again; only a cancel ends the loop.
123 if (!config()) {
124 return;
125 }
126 }
127 m_freshStart = false;
128 m_initSucceeded = true;
129
130 // Initial focus is set in showEvent(): a 10 ms QTimer here could fire
131 // before the window was realised (queued ActivationChange, nested exec() in
132 // init()) and selectAll() segfaulted inside Qt (#1187, #1188).
133}
134
136 connect(m_qtPass, &QtPass::outputReady, this,
137 [this](const QString &html) { flashText(html, false, true); });
138 connect(m_qtPass, &QtPass::operationFinished, this,
139 [this]() { setUiElementsEnabled(true); });
140 connect(m_qtPass, &QtPass::statusMessage, this,
143 connect(m_qtPass, &QtPass::entryInserted, this,
146 [this](const QString &message) { showStatusMessage(message); });
147 // Both backends: the active one can change through the configuration
148 // dialog without a restart.
152 connect(ipass, &ImitatePass::startReencryptPath, this,
154 connect(ipass, &ImitatePass::reencryptProgress, this,
156 connect(ipass, &ImitatePass::endReencryptPath, this,
158}
159
161 {
163 if (s.useMonospace) {
164 QFont monospace("Monospace");
165 monospace.setStyleHint(QFont::Monospace);
166 ui->textBrowser->setFont(monospace);
167 }
168 if (s.noLineWrapping) {
169 ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
170 }
172 }
173 ui->textBrowser->setOpenExternalLinks(true);
174 ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
175 connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
177}
178
181 ui->gridLayout, ui->verticalLayoutPassword, this, this);
186 connect(ui->textBrowser, &SecretTextBrowser::copied, &m_qtPass->clipboard(),
190 // Double-click on what is shown edits it, as in the tree.
192 if (ui->actionEdit->isEnabled()) {
193 onEdit();
194 }
195 });
196}
197
199 clearPanelTimer.setSingleShot(true);
200 connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
201
202 searchTimer.setInterval(350);
203 searchTimer.setSingleShot(true);
204
205 connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
206
207 // Safety net: if a backend operation disables the UI but never signals
208 // completion, re-enable after a timeout so the window can't get stuck.
209 m_uiWatchdog.setSingleShot(true);
210 m_uiWatchdog.setInterval(UiWatchdogMs);
211 connect(&m_uiWatchdog, &QTimer::timeout, this, [this]() {
212 showStatusMessage(tr("Operation timed out; re-enabling interface."));
213 // Drop any in-flight OTP request so a late finishedShow cannot be mistaken
214 // for the answer to it.
217 });
218}
219
222 addDockWidget(Qt::BottomDockWidgetArea, m_processOutput);
223 // After addDockWidget so our preference wins over any cached state
224 // QMainWindow applies; restoreWindow() already processed the saved layout.
226 // From the setting, not isVisible(): the window is not shown yet, so no
227 // child of it reports visible.
228 ui->actionShowProcessOutput->setChecked(
230 connect(ui->actionShowProcessOutput, &QAction::toggled, this,
231 [this](bool show) {
232 m_processOutput->setVisible(show);
233 AppSettings s = QtPassSettings::load();
234 if (s.showProcessOutput != show) {
235 s.showProcessOutput = show;
236 QtPassSettings::save(s);
237 }
238 });
239
241 [this](const QString &out, const QString &err, Enums::PROCESS pid) {
242 // Never route potentially-secret output through the panel:
243 // PASS_SHOW goes to the text browser (cleared on a timer when
244 // "Autoclear panel" is set),
245 // PASS_GREP would return password-file lines (#252; it is no
246 // longer started, the check stays), and PASS_INSERT
247 // is excluded defensively in case a future path uses --echo.
249 return;
250 }
251 if (!out.isEmpty()) {
252 onProcessOutput(out, false, pid);
253 }
254 if (!err.isEmpty()) {
255 onProcessOutput(err, true, pid);
256 }
257 });
258}
259
261 // Quit from the tray menu or a SIGTERM never delivers closeEvent(); the
262 // window still knows its last geometry here.
264 delete m_qtPass;
265}
266
267// The signals of one backend that the window answers itself.
269 // A failed decrypt never emits finishedShow; drop the pending OTP/copy
270 // request so a later decrypt of the same entry is not taken as its answer.
272 connect(pass, &Pass::critical, this, &MainWindow::critical);
273 connect(pass, &Pass::startingExecuteWrapper, this,
275 // The profile box comes back when the queue is done, also after the
276 // watchdog released the rest of the interface early.
277 connect(pass, &Pass::idle, this, &MainWindow::updateProfileBoxEnabled);
278 connect(pass, &Pass::statusMsg, this, &MainWindow::showStatusMessage);
279 connect(pass, &Pass::finishedShow, this, &MainWindow::passShowHandler);
280 // Both check the file against their own pending request and ignore the
281 // rest, so they can stay connected for good.
283 connect(pass, &Pass::finishedShow, this,
285 connect(pass, &Pass::finishedGrep, this, &MainWindow::onGrepFinished);
286}
287
288// Idempotent; called from closeEvent() and the destructor.
293
295 // findChild() rather than ui->lineEdit: on a fresh-config first launch
296 // config() calls setWindowFlags(), which rebuilds the native window, and
297 // under Qt 6.11 the QLineEdit was destroyed while ui->lineEdit kept its old
298 // address (SIGSEGV in QWidget::testAttribute). findChild() returns null.
299 if (!isVisible()) {
300 return;
301 }
302 auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
303 if (lineEdit == nullptr || !lineEdit->isVisible()) {
304 return;
305 }
306 lineEdit->selectAll();
307 lineEdit->setFocus();
308 // Only once focus has landed: set in showEvent(), an early return above
309 // would consume the one-shot and never retry.
311}
312
313auto MainWindow::event(QEvent *event) -> bool {
314 if (event->type() == QEvent::ApplicationPaletteChange) {
315 m_themeSwitched = true;
316 // Breeze may re-stamp the bars before this arrives (Plasma 6 does):
317 // their PaletteChange then found no switch yet, so check them now too.
318 QTimer::singleShot(0, this, &MainWindow::dropStaleToolsAreaPalettes);
319 }
320 return QMainWindow::event(event);
321}
322
324 QWidget::changeEvent(event);
325 if (event->type() == QEvent::PaletteChange && m_displayPanel != nullptr) {
326 // Desktop switched light/dark. Top-level widgets get PaletteChange
327 // (ApplicationPaletteChange goes to QApplication); stylesheets with
328 // baked-in palette colours must be re-derived by hand.
330 }
331 if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
332 isVisible()) {
333 // Defer a tick so the activation dispatch chain unwinds first: calling
334 // focusInput() inline has segfaulted on half-rebuilt ui state.
335 QMetaObject::invokeMethod(this, &MainWindow::focusInput,
336 Qt::QueuedConnection);
337 }
338}
339
340void MainWindow::showEvent(QShowEvent *event) {
341 QMainWindow::showEvent(event);
343 return;
344 }
345 // Queued so the platform map and pending rebuilds (setWindowFlags on the
346 // wizard path) settle first. focusInput() sets the latch only on success,
347 // so a failed lookup retries on the next show.
348 QMetaObject::invokeMethod(this, &MainWindow::focusInput,
349 Qt::QueuedConnection);
350}
351
353 connect(ui->actionAddPassword, &QAction::triggered, this,
355 connect(ui->actionAddFolder, &QAction::triggered, this,
357 connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
358 connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
359 connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
360 connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
361 connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
362 connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
363 connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
364
365 // Menu-only. Quit ends the application, tray icon or not, as in KeePassXC,
366 // Telegram and the KDE, GNOME and macOS guidelines (#1788). Close is
367 // close(), a hide when "hide on close" is set, like the X button and Alt+F4.
368 connect(ui->actionQuit, &QAction::triggered, qApp, &QApplication::quit);
369 connect(ui->actionClose, &QAction::triggered, this, &MainWindow::close);
370#ifdef Q_OS_MACOS
371 // The menu bar is the system's up there; nothing to hide.
372 ui->actionShowMenuBar->setVisible(false);
373#else
374 // The action lives in the menu it hides, so it has to be on the window as
375 // well for Ctrl+M to keep working while the bar is gone.
376 addAction(ui->actionShowMenuBar);
377 ui->actionShowMenuBar->setChecked(QtPassSettings::load().showMenuBar);
378 menuBar()->setVisible(ui->actionShowMenuBar->isChecked());
379 connect(ui->actionShowMenuBar, &QAction::toggled, this, [this](bool show) {
380 menuBar()->setVisible(show);
381 AppSettings s = QtPassSettings::load();
382 s.showMenuBar = show;
383 QtPassSettings::save(s);
384 });
385 new MenuBarPeek(this, ui->actionShowMenuBar);
386#endif
387 connect(ui->actionFaq, &QAction::triggered, this, [] {
388 QDesktopServices::openUrl(QUrl(QStringLiteral("https://qtpass.org/faq")));
389 });
390 connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout);
391 connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
392
393 ui->actionAddPassword->setIcon(
394 QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
395 ui->actionAddFolder->setIcon(
396 QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
397 ui->actionEdit->setIcon(QIcon::fromTheme(
398 "document-properties", QIcon(":/icons/document-properties.svg")));
399 ui->actionDelete->setIcon(
400 QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
401 ui->actionPush->setIcon(
402 QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
403 ui->actionUpdate->setIcon(
404 QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
405 ui->actionUsers->setIcon(QIcon::fromTheme(
406 "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
407 ui->actionConfig->setIcon(QIcon::fromTheme(
408 "applications-system", QIcon(":/icons/applications-system.svg")));
409}
410
412 ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
413
414 QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
415 .scaledToHeight(statusBar()->height());
416 auto *logoApp = new QLabel(statusBar());
417 logoApp->setPixmap(logo);
418 statusBar()->addPermanentWidget(logoApp);
419}
420
422 return ui->treeView->currentIndex();
423}
424
425void MainWindow::flashText(const QString &text, const bool isError,
426 const bool isHtml) {
427 if (isError) {
428 ui->textBrowser->setTextColor(Qt::red);
429 } else {
430 // setText() re-applies the current char format, red after an error, to
431 // the whole document. Clear the foreground rather than pin a palette
432 // colour, so text follows runtime light/dark switches (#946).
433 QTextCharFormat format = ui->textBrowser->currentCharFormat();
434 format.clearForeground();
435 ui->textBrowser->setCurrentCharFormat(format);
436 }
437
438 if (isHtml) {
439 QString _text = text;
440 if (!ui->textBrowser->toPlainText().isEmpty()) {
441 _text = ui->textBrowser->toHtml() + _text;
442 }
443 ui->textBrowser->setHtml(_text);
444 } else {
445 ui->textBrowser->setText(text);
446 }
447}
448
451 if (s.useMonospace) {
452 QFont monospace("Monospace");
453 monospace.setStyleHint(QFont::Monospace);
454 ui->textBrowser->setFont(monospace);
455 } else {
456 ui->textBrowser->setFont(QFont());
457 }
458
459 if (s.noLineWrapping) {
460 ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
461 } else {
462 ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
463 }
464}
465
467 const bool wantOnTop = QtPassSettings::isAlwaysOnTop();
468 const Qt::WindowFlags flags = windowFlags();
469 if (flags.testFlag(Qt::WindowStaysOnTopHint) == wantOnTop) {
470 // setWindowFlags() rebuilds the native window, the teardown that left
471 // ui->lineEdit dangling (see focusInput()); skip it when nothing changes.
472 return;
473 }
474 // Toggle just this hint; resetting to Qt::Window discarded every other flag.
475 const bool wasVisible = isVisible();
476 setWindowFlags(wantOnTop ? (flags | Qt::WindowStaysOnTopHint)
477 : (flags & ~Qt::WindowStaysOnTopHint));
478 // setWindowFlags() hides a shown window; a window that was not shown yet
479 // (first-run wizard from the constructor) is left to its normal show.
480 if (wasVisible) {
481 show();
482 }
483}
484
485auto MainWindow::config() -> bool {
486 if (m_freshStart) {
487 // No usable configuration yet: the wizard finds the programs, the key
488 // and the store, and creates the store when it has to.
489 FirstRunWizard wizard(this);
490 if (wizard.exec() != QDialog::Accepted) {
491 return false;
492 }
493 } else {
494 ConfigDialog d(this);
495 d.setModal(true);
496 if (d.exec() != QDialog::Accepted) {
497 return false;
498 }
499 }
500
503
506 m_tree->setStore(s.passStore);
507 deselect();
508 ui->treeView->setCurrentIndex(QModelIndex());
509
510 Pass *activePass = QtPassSettings::getPass();
511 activePass->updateEnv();
512 m_tree->setPass(activePass);
514 m_qtPass->clipboard().setAutoclearTimer();
515
520#ifndef Q_OS_MACOS
521 // The action's toggled handler shows or hides the bar (and saves again).
522 ui->actionShowMenuBar->setChecked(s.showMenuBar);
523#endif
524 if (s.useTrayIcon && m_tray == nullptr) {
525 initTrayIcon();
526 } else if (!s.useTrayIcon && m_tray != nullptr) {
528 }
529
530 // Stay fresh-start while the configuration is unusable, so the
531 // constructor's loop re-runs the wizard. The re-prompt lives there:
532 // recursing here never reported a cancel back to the caller.
533 if (Util::configIsValid(s)) {
534 m_freshStart = false;
535 }
536 return true;
537}
538
539void MainWindow::onUpdate(bool block) {
540 ui->statusBar->showMessage(tr("Updating password-store"), 2000);
541 if (block) {
542 QtPassSettings::getPass()->GitPull_b();
543 } else {
544 QtPassSettings::getPass()->GitPull();
545 }
546}
547
550 ui->statusBar->showMessage(tr("Updating password-store"), 2000);
551 QtPassSettings::getPass()->GitPush();
552 }
553}
554
555auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
556 return m_tree->fileFor(index, forPass);
557}
558
559void MainWindow::on_treeView_clicked(const QModelIndex &index) {
560 bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
561 QString file = getFile(index, true);
562 ui->passwordName->setText(file);
563 if (!file.isEmpty() && !cleared) {
564 // Remember what the panel is about to show, so onOtp() can tell whether the
565 // code it can see belongs to the entry that is currently selected.
566 m_shownFile = file;
567 QtPassSettings::getPass()->Show(file);
568 } else {
569 m_shownFile.clear();
570 clearPanel(false);
571 ui->actionEdit->setEnabled(false);
572 ui->actionDelete->setEnabled(true);
573 }
574}
575
576void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
577 QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
578
579 if (fileOrFolder.isFile()) {
580 editPassword(getFile(index, true));
581 }
582}
583
585 m_shownFile.clear();
587 m_qtPass->clipboard().clear();
588 ui->treeView->clearSelection();
589 ui->actionEdit->setEnabled(false);
590 ui->actionDelete->setEnabled(false);
591 ui->passwordName->setText("");
592 clearPanel(false);
593}
594
597 ui->textBrowser->clear();
599 clearPanelTimer.stop();
601 m_processOutput->setVisible(true);
602 }
603}
604
605void MainWindow::passShowHandler(const QString &p_output, const QString &file) {
606 // Only the entry we most recently asked to show may repaint the panel; a
607 // slower decrypt of an entry the user has since left is dropped, and so is
608 // one that arrives after the selection was cleared (deselect, a profile
609 // switch): nothing is asked to be shown then.
610 if (!file.isEmpty() && file != m_shownFile) {
611 // A newer show is pending and releases the interface when it lands;
612 // with none (the selection was cleared) nothing else would until the
613 // watchdog. setUiElementsEnabled() keeps a re-encryption's lock.
614 if (m_shownFile.isEmpty()) {
616 }
617 return;
618 }
620 // What a template marks secret, in the settings or the store's
621 // .templates; read per entry shown, so an edit there counts at once.
622 if (s.hidePassword) {
626 }
627 QStringList templ =
628 s.useTemplate ? TemplateIO::fieldsIn(s.passTemplate) : QStringList();
629 bool allFields = s.useTemplate && s.templateAllFields;
630 // A secret the panel hides is a field even when the template doesn't list
631 // it: left in the text below the fields, it would show in plain view.
632 FileContent fileContent = FileContent::parse(
633 p_output, templ, allFields, [this, &s](const QString &name) {
634 return m_displayPanel->isSecret(name, s);
635 });
636 QString output = p_output;
637 // Display variant: empty when the password line is itself an otpauth URI, so
638 // the shared secret is neither rendered nor copied to the clipboard.
639 QString password = fileContent.getPasswordForDisplay();
640
641 // Skipped for an OTP request: the user asked for a code, and two
642 // OleSetClipboard calls in one turn can leave the Windows clipboard empty.
643 if (m_otpRequestFile.isEmpty() || m_otpRequestFile != file) {
644 m_qtPass->clipboard().copyIfAlways(password, p_output);
645 }
646
648
649 if (s.hideContent) {
650 output = "***" + tr("Content hidden") + "***";
651 } else if (s.displayAsIs) {
652 // As it is, but not a block per line of millions.
653 output = FileContent::boundedForDisplay(p_output);
654 } else {
655 m_displayPanel->displayFields(password, fileContent.getNamedValues(), s,
656 s.useOtp ? fileContent.getOtpUri()
657 : QString());
658 output = fileContent.getRemainingDataForDisplay();
659 }
660
661 if (s.useAutoclearPanel) {
662 clearPanelTimer.start();
663 }
664
665 flashText(QtPass::formatOutput(output), false, true);
667}
668
669void MainWindow::otpFromFileToClipboard(const QString &p_output,
670 const QString &file) {
671 // Permanently connected: only the decrypt of the entry onOtp() asked for
672 // is ours, everything else belongs to a tree click or a Ctrl+C.
673 if (m_otpRequestFile.isEmpty() || file != m_otpRequestFile) {
674 return;
675 }
676 m_otpRequestFile.clear();
677
678 if (p_output.isEmpty()) {
679 // Distinguish "could not read the entry" from "entry has no OTP".
680 flashText(tr("Could not decrypt this password entry"), true);
682 return;
683 }
684
685 // passShowHandler ran first for this finishedShow; if it rendered the OTP
686 // row, reuse its code (as onOtp()'s fast path does). Otherwise parse:
687 // hideContent, displayAsIs or no OTP field.
688 const QString shown = m_displayPanel->currentOtpCode();
689 if (!shown.isEmpty()) {
690 m_qtPass->clipboard().copyText(shown);
691 showStatusMessage(tr("OTP code copied to clipboard"));
693 return;
694 }
695
697 // Parse with the same template settings passShowHandler uses, so an OTP
698 // field is recognised identically in both paths.
699 const QStringList templ =
700 s.useTemplate ? TemplateIO::fieldsIn(s.passTemplate) : QStringList();
701 const bool allFields = s.useTemplate && s.templateAllFields;
702 const FileContent fileContent =
703 FileContent::parse(p_output, templ, allFields);
704
705 const std::optional<Totp::Settings> settings =
706 Totp::parse(fileContent.getOtpUri());
707 if (settings.has_value()) {
708 m_qtPass->clipboard().copyText(Totp::generateNow(*settings));
709 showStatusMessage(tr("OTP code copied to clipboard"));
710 } else {
711 flashText(tr("No OTP code found in this password entry"), true);
712 }
714}
715
716void MainWindow::clearPanel(bool notify) {
718 const bool grepWasVisible = ui->grepResultsList->isVisible();
719 ui->grepResultsList->clear();
720 if (grepWasVisible) {
721 ui->grepResultsList->setVisible(false);
722 ui->treeView->setVisible(true);
723 if (m_grep.inGrepMode()) {
725 ui->grepButton->blockSignals(true);
726 ui->grepButton->setChecked(false);
727 ui->grepButton->blockSignals(false);
728 ui->lineEdit->blockSignals(true);
729 ui->lineEdit->clear();
730 ui->lineEdit->blockSignals(false);
731 ui->lineEdit->setPlaceholderText(tr("Search password"));
732 }
733 }
734 if (notify) {
735 QString output = "***" + tr("Password and content hidden") + "***";
736 ui->textBrowser->setHtml(output);
737 } else {
738 ui->textBrowser->setHtml("");
739 }
740}
741
743 // A running re-encryption owns the UI state: the progress dialog's Cancel is
744 // the user's escape hatch and endReencryptPath() releases the interface, so
745 // neither an unrelated completion nor the watchdog may re-enable it early.
746 if (state && m_reencryptRunning) {
747 m_uiWatchdog.stop();
748 return;
749 }
750 // Arm the watchdog while the UI is disabled; disarm once re-enabled.
751 if (state) {
752 m_uiWatchdog.stop();
753 } else {
754 m_uiWatchdog.start();
755 }
756 ui->treeView->setEnabled(state);
757 ui->lineEdit->setEnabled(state);
758 ui->actionAddPassword->setEnabled(state);
759 ui->actionAddFolder->setEnabled(state);
760 ui->actionUsers->setEnabled(state);
761 ui->actionConfig->setEnabled(state);
762 m_uiEnabled = state;
764 const bool uiEnabled = state;
765 // is a file selected?
766 state &= ui->treeView->currentIndex().isValid();
767 ui->actionDelete->setEnabled(state);
768 ui->actionEdit->setEnabled(state);
769 updateGitButtonVisibility(uiEnabled);
770 // `state` is now "UI enabled AND a file is selected", which is exactly when
771 // generating an OTP makes sense.
773}
774
776 // saveGeometry() covers position, size, maximized state and screen;
777 // anything applied on top discards it. Centre only when nothing was saved.
778 const QByteArray geometry = QtPassSettings::getGeometry();
779 if (geometry.isEmpty() || !restoreGeometry(geometry)) {
781 }
782 restoreState(QtPassSettings::getSavestate(saveState()));
784
786
787 if (s.useTrayIcon && m_tray == nullptr) {
788 initTrayIcon();
789 if (s.startMinimized) {
790 // since we are still in constructor, can't directly hide
791 QTimer::singleShot(10, this, &MainWindow::hide);
792 }
793 } else if (!s.useTrayIcon && m_tray != nullptr) {
795 }
796}
797
799
801 QMessageBox::about(
802 this, tr("About QtPass"),
803 tr("<h3>QtPass %1</h3>"
804 "<p>A multi-platform GUI for <a "
805 "href=\"https://www.passwordstore.org/\">"
806 "pass</a>, the standard Unix password manager.</p>"
807 "<p><a href=\"https://qtpass.org/\">qtpass.org</a> &middot; "
808 "<a href=\"https://github.com/IJHack/QtPass\">Source and "
809 "issues</a></p>"
810 "<p>Copyright &copy; 2014&ndash;%2 IJhack. Licensed under the "
811 "<a href=\"https://www.gnu.org/licenses/gpl-3.0.html\">GNU GPL "
812 "version 3</a> or later.</p>")
813 .arg(QStringLiteral(VERSION),
814 QString::number(QDate::currentDate().year())));
815}
816
817void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
818 if (m_grep.inGrepMode())
819 return;
820 ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
821 ui->treeView->expandAll();
822 // Drop any decrypt still pending for the entry that was shown: otherwise it
823 // keeps its right to the panel and, with "Always copy", the clipboard, and
824 // when it lands late it shows that entry's password under the new heading.
825 m_shownFile.clear();
827 clearPanel(false);
828 ui->passwordName->setText("");
829 ui->actionEdit->setEnabled(false);
830 ui->actionDelete->setEnabled(false);
831 searchTimer.start();
832}
833
835 QString query = ui->lineEdit->text();
836
837 if (query.isEmpty()) {
838 ui->treeView->collapseAll();
839 deselect();
840 }
841
842 // The words are matched literally, in order, with anything in between:
843 // "work vpn" finds work/acme/vpn. Regular expressions belong to the
844 // content search; here a typed "[" or "(" must not switch filtering off.
845 QStringList words;
846 for (const QString &word :
847 query.split(QLatin1Char(' '), Qt::SkipEmptyParts)) {
848 words << QRegularExpression::escape(word);
849 }
850 QRegularExpression regExp(words.join(QStringLiteral(".*")),
851 QRegularExpression::CaseInsensitiveOption);
852 m_tree->setFilter(regExp);
853
854 if (m_tree->proxy().rowCount() > 0 && !query.isEmpty()) {
856 } else {
857 ui->actionEdit->setEnabled(false);
858 ui->actionDelete->setEnabled(false);
859 }
860}
861
863 qCDebug(lcQtPass) << "on_lineEdit_returnPressed"
864 << m_tree->proxy().rowCount();
865
866 if (m_grep.inGrepMode()) {
867 const QString query = ui->lineEdit->text();
868 if (!query.isEmpty()) {
869 ui->grepResultsList->clear();
870 ui->statusBar->showMessage(tr("Searching…"));
871 if (m_grep.beginSearch()) {
872 QApplication::setOverrideCursor(Qt::WaitCursor);
873 }
874 QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
875 } else {
876 if (m_grep.cancelSearch()) {
877 QApplication::restoreOverrideCursor();
878 }
879 ui->grepResultsList->clear();
880 ui->grepResultsList->setVisible(false);
881 ui->treeView->setVisible(true);
882 }
883 return;
884 }
885
886 if (m_tree->proxy().rowCount() > 0) {
888 on_treeView_clicked(ui->treeView->currentIndex());
889 }
890}
891
894 if (checked) {
896 ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
897 // Both backends search through NativeGrep (QRegularExpression), so one
898 // dialect: PCRE.
899 ui->lineEdit->setToolTip(
900 tr("Content search uses Perl-compatible regular expressions (PCRE)."));
901 ui->lineEdit->clear();
902 searchTimer.stop();
903 m_tree->setFilter(QRegularExpression());
904 ui->grepResultsList->setVisible(false);
905 // Keep treeView visible until results arrive
906 } else {
907 if (m_grep.leaveGrepMode()) {
908 QApplication::restoreOverrideCursor();
909 }
910 searchTimer.stop();
911 ui->lineEdit->blockSignals(true);
912 ui->lineEdit->clear();
913 ui->lineEdit->blockSignals(false);
914 ui->lineEdit->setPlaceholderText(tr("Search password"));
915 ui->lineEdit->setToolTip(QString());
916 ui->grepResultsList->clear();
917 ui->grepResultsList->setVisible(false);
918 ui->treeView->setVisible(true);
919 m_tree->setFilter(QRegularExpression());
920 }
921}
922
924 const QList<QPair<QString, QStringList>> &results) {
926 if (outcome.restoreCursor) {
927 QApplication::restoreOverrideCursor();
928 }
929 // Re-enable the UI before the discard check so a cancelled search can never
930 // leave controls disabled.
932 if (outcome.discard) {
933 return;
934 }
935 if (!m_grep.inGrepMode())
936 return;
937 ui->grepResultsList->clear();
938 if (results.isEmpty()) {
939 ui->statusBar->showMessage(tr("No matches found."), 3000);
940 ui->grepResultsList->setVisible(false);
941 ui->treeView->setVisible(true);
942 return;
943 }
945 const bool hideContent = s.hideContent;
946 int totalLines = 0;
947 int entries = 0;
948 bool stopped = false;
949 for (const auto &pair : results) {
950 if (NativeGrep::isStoppedMarker(pair)) {
951 stopped = true;
952 continue;
953 }
954 ++entries;
955 auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
956 entryItem->setText(0, pair.first);
957 entryItem->setData(0, Qt::UserRole, pair.first);
958 for (const QString &line : pair.second) {
959 auto *lineItem = new QTreeWidgetItem(entryItem);
960 // The marker is no content: shown as it is with hidden content too,
961 // and not counted as a match.
962 if (NativeGrep::isMoreMarker(line)) {
963 lineItem->setText(0, tr("… more matches in this entry"));
964 } else {
965 lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
966 : line);
967 ++totalLines;
968 }
969 lineItem->setData(0, Qt::UserRole, pair.first);
970 }
971 }
972 ui->grepResultsList->expandAll();
973 ui->treeView->setVisible(false);
974 ui->grepResultsList->setVisible(true);
975 if (stopped) {
976 // The search stopped there; more entries may match.
977 ui->statusBar->showMessage(
978 tr("Showing the first %n match(es); search for something narrower "
979 "to see the rest.",
980 nullptr, totalLines),
981 10000);
982 } else {
983 ui->statusBar->showMessage(tr("Found %n match(es)", nullptr, totalLines) +
984 " " +
985 tr("in %n entr(ies).", nullptr, entries),
986 3000);
987 }
988 if (s.useAutoclearPanel)
989 clearPanelTimer.start();
990}
991
993 int /*column*/) {
995 const QString entry = item->data(0, Qt::UserRole).toString();
996 if (entry.isEmpty())
997 return;
998 const QString fullPath =
999 QDir::cleanPath(QDir(s.passStore).filePath(entry + ".gpg"));
1000 const QModelIndex proxyIndex = m_tree->indexFor(fullPath);
1001 if (!proxyIndex.isValid())
1002 return;
1003 ui->treeView->setCurrentIndex(proxyIndex);
1004 on_treeView_clicked(proxyIndex);
1005 if (s.hideContent || s.useAutoclearPanel)
1006 ui->grepResultsList->clear();
1007 ui->grepResultsList->setVisible(false);
1008 ui->treeView->setVisible(true);
1009 ui->treeView->scrollTo(proxyIndex);
1010 ui->treeView->setFocus();
1011}
1012
1014 QModelIndex index = firstFile(m_tree->rootIndex());
1015 ui->treeView->setCurrentIndex(index);
1016}
1017
1018auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
1019 StoreModel &proxy = m_tree->proxy();
1020 int numRows = proxy.rowCount(parentIndex);
1021 for (int row = 0; row < numRows; ++row) {
1022 QModelIndex index = proxy.index(row, 0, parentIndex);
1023 if (m_tree->fileInfo(index).isFile()) {
1024 return index;
1025 }
1026 if (proxy.hasChildren(index)) {
1027 QModelIndex childFile = firstFile(index);
1028 if (childFile.isValid())
1029 return childFile;
1030 }
1031 }
1032 return QModelIndex();
1033}
1034
1035// Guards the input dialogs (create, move, rename) against names like
1036// "../../etc/passwd" or absolute paths escaping the store root.
1037auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
1039 return true;
1040 }
1041 QMessageBox::warning(this, tr("Invalid name"),
1042 tr("That name would resolve outside the password "
1043 "store. Please choose a different name."));
1044 return false;
1045}
1046
1047void MainWindow::setPassword(const QString &file, bool isNew) {
1049 PasswordDialog d(QtPassSettings::getPass(), s, file, isNew, this);
1050 // A copy out of the editor is autocleared like one from the display.
1051 connect(&d, &PasswordDialog::copied, &m_qtPass->clipboard(),
1053
1054 if (isNew) {
1055 const QString storePath = s.passStore;
1056 QString folder = m_tree->currentDir(false);
1057 if (folder.isEmpty()) {
1058 folder = storePath;
1059 }
1060 // Folders to pick from, the tree's current one preselected. Real folders
1061 // only: a linked or junctioned one would write the entry outside the store.
1062 QStringList folders{QString()};
1063 const QStringList found = Util::directoriesUnder(storePath);
1064 for (const QString &path : found) {
1065 const QString rel = QDir(storePath).relativeFilePath(path);
1066 if (!rel.startsWith(u'.') && !rel.contains(QStringLiteral("/."))) {
1067 folders << rel;
1068 }
1069 }
1070 std::sort(folders.begin() + 1, folders.end());
1071 d.setNewEntryLocation(storePath, folders,
1072 QDir(storePath).relativeFilePath(folder));
1073 QHash<QString, QStringList> templates =
1074 TemplateIO::readTemplates(storePath);
1075 if (!templates.isEmpty()) {
1076 QString defaultTemplate =
1077 TemplateIO::getFolderTemplate(folder, storePath);
1078 d.setAvailableTemplates(templates, defaultTemplate);
1079 }
1080 }
1081
1082 if (!d.exec()) {
1083 ui->treeView->setFocus();
1084 }
1085}
1086
1088
1089auto MainWindow::confirmDeletion(const QString &file, bool isDir) -> bool {
1090 const QString path = QDir::separator() + file;
1091 if (!isDir) {
1093 this, tr("Delete password?"),
1094 tr("Are you sure you want to delete %1?").arg(path));
1095 }
1096 // Whole sentences, not a question stitched from pieces: a verb-final
1097 // language cannot put the rest of the question after the path.
1098 // Plain text, as every box quoting the store: the path is a name other
1099 // writers control, and as rich text "a b" would also show as "a b".
1100 QString question =
1101 tr("Are you sure you want to delete %1 and the whole content?").arg(path);
1102 // A link, junction or special file inside is as unexpected as a stray
1103 // plain file: the walker leaves them out of content and reports them.
1104 QStringList skipped;
1105 const QStringList content = Util::regularFilesUnder(
1106 m_tree->fileSystem().rootPath() + path, {QStringLiteral("*")}, &skipped);
1107 const bool unexpected =
1108 !skipped.isEmpty() ||
1109 std::any_of(content.cbegin(), content.cend(), [](const QString &entry) {
1110 return QFileInfo(entry).suffix() != QLatin1String("gpg");
1111 });
1112 if (unexpected) {
1113 question += QStringLiteral("\n\n") +
1114 tr("Attention: there are unexpected files in the given "
1115 "folder, check them before continuing.");
1116 }
1117 return PlainMessageBox::askYes(this, tr("Delete folder?"), question);
1118}
1119
1120auto MainWindow::confirmLinkRemoval(const QString &file) -> bool {
1121 // Only the link goes (ImitatePass::Remove unlinks it); nothing behind it
1122 // is looked at or mentioned.
1124 this, tr("Delete link?"),
1125 tr("%1 is a symbolic link or junction. Remove the link? What it "
1126 "points to is left alone.")
1127 .arg(QDir::separator() + file));
1128}
1129
1131 const QModelIndex currentIndex = ui->treeView->currentIndex();
1132 if (!currentIndex.isValid()) {
1133 // Otherwise the whole password directory would be deleted (#556).
1134 return;
1135 }
1136
1137 const bool isDir = !m_tree->fileInfo(currentIndex).isFile();
1138 const QString file =
1139 isDir ? m_tree->currentDir(true) : getFile(currentIndex, true);
1140 const QString folder =
1141 m_tree->fileSystem().rootPath() + QDir::separator() + file;
1142 const bool linkedFolder = isDir && Util::isLinkedFolder(folder);
1143
1144 if (!linkedFolder && refuseLinkedFolder(folder)) {
1145 // Behind a link: deleting would reach outside the store.
1146 return;
1147 }
1148 const bool confirmed =
1149 linkedFolder ? confirmLinkRemoval(file) : confirmDeletion(file, isDir);
1150 if (!confirmed) {
1151 return;
1152 }
1153 QtPassSettings::getPass()->Remove(file, isDir);
1154}
1155
1157 m_otpRequestFile.clear();
1158 m_copyRequestFile.clear();
1159}
1160
1163 // executeWrapperStarted() emptied the panel and the browser when the
1164 // operation started (a refusal says so too); what is left to forget is
1165 // which entry the panel was about to show.
1166 m_shownFile.clear();
1167}
1168
1169// Derives the code in-process from one decrypt, so it works with either
1170// backend on every platform; otpFromFileToClipboard() finishes the job.
1172 QString file = getFile(ui->treeView->currentIndex(), true);
1173 if (file.isEmpty()) {
1174 flashText(tr("No password selected for OTP generation"), true);
1175 return;
1176 }
1177 if (!QtPassSettings::isUseOtp()) {
1178 // Normally unreachable (the action is hidden), but never fail silently.
1179 flashText(tr("No OTP code found in this password entry"), true);
1180 return;
1181 }
1182
1183 // Fast path: the panel shows a live code, so no decrypt, and one clipboard
1184 // write (two setMimeData calls in one turn can empty the Windows
1185 // clipboard). Only for *this* entry: arrow keys and right-click move
1186 // currentIndex without QTreeView::clicked, so the code may be another's.
1187 const QString shown =
1188 (m_shownFile == file) ? m_displayPanel->currentOtpCode() : QString();
1189 if (!shown.isEmpty()) {
1190 m_qtPass->clipboard().copyText(shown);
1191 showStatusMessage(tr("OTP code copied to clipboard"));
1192 return;
1193 }
1194
1195 // Fallback: decrypt once. The request file keeps passShowHandler from
1196 // copying the password and tells otpFromFileToClipboard its answer.
1197 m_otpRequestFile = file;
1198 setUiElementsEnabled(false);
1199 // Keep the marker in step so a second request takes the fast path; safe as
1200 // executeWrapperStarted() clears the panel, so a failed decrypt shows none.
1201 m_shownFile = file;
1202 QtPassSettings::getPass()->Show(file);
1203}
1204
1206 QString file = getFile(ui->treeView->currentIndex(), true);
1207 editPassword(file);
1208}
1209
1211 const QString dir = m_tree->currentDir(false);
1212 if (refuseLinkedFolder(dir)) {
1213 return;
1214 }
1215
1217 if (!d.exec()) {
1218 ui->treeView->setFocus();
1219 }
1220}
1221
1222void MainWindow::messageAvailable(const QString &message) {
1223 show();
1224 raise();
1225 if (message.isEmpty()) {
1226 focusInput();
1227 } else {
1228 ui->treeView->expandAll();
1229 ui->lineEdit->setText(message);
1231 }
1232}
1233
1235 const Profiles profiles = QtPassSettings::getProfiles();
1236 bool activeGone = false;
1237
1238 if (profiles.isEmpty()) {
1239 ui->profileWidget->hide();
1240 } else {
1241 ui->profileWidget->show();
1242 // A refill is no choice of the user's: clearing and adding items moves
1243 // the current text, which would otherwise switch profiles.
1244 const QSignalBlocker quiet(ui->profileBox);
1245 ui->profileBox->clear();
1246 for (auto i = profiles.cbegin(); i != profiles.cend(); ++i) {
1247 if (!i.key().isEmpty()) {
1248 ui->profileBox->addItem(i.key());
1249 }
1250 }
1251 ui->profileBox->model()->sort(0);
1252 int index = ui->profileBox->findText(QtPassSettings::getProfile());
1253 if (index != -1) { // -1 for not found
1254 ui->profileBox->setCurrentIndex(index);
1255 } else if (ui->profileBox->count() > 0) {
1256 // The active profile is gone (removed in Settings): the box shows the
1257 // first one, and the settings and backend must follow it, or the
1258 // removed profile's store stays in use behind it.
1259 ui->profileBox->setCurrentIndex(0);
1260 activeGone = true;
1261 }
1262 }
1263 if (activeGone) {
1264 on_profileBox_currentTextChanged(ui->profileBox->currentText());
1265 }
1267}
1268
1270 // No profile switch while anything of the store runs or waits: the
1271 // backend's store and environment would change under it, and what is
1272 // still queued (a push after a save, say) would finish for the other
1273 // profile (#2071). Asked of the backend, not only of the interface: the
1274 // watchdog releases the interface while a slow gpg or git still runs.
1275 // Every backend: one kept from before a switch of backend may still
1276 // run what was queued on it.
1277 ui->profileBox->setEnabled(m_uiEnabled && ui->profileBox->count() > 1 &&
1279}
1280
1282 if (m_freshStart || name == QtPassSettings::getProfile()) {
1283 return;
1284 }
1285 // The box is disabled while anything runs; should a change come through
1286 // anyway, the active profile stays and the box says so again.
1288 const QSignalBlocker quiet(ui->profileBox);
1289 ui->profileBox->setCurrentText(QtPassSettings::getProfile());
1290 return;
1291 }
1292 // A content search runs outside the queue: leaving grep mode cancels it,
1293 // so its results for the old store are dropped, not shown for the new.
1294 if (m_grep.inGrepMode()) {
1295 ui->grepButton->setChecked(false);
1296 }
1297
1298 ui->lineEdit->clear();
1299
1300 const Profile prof = QtPassSettings::getProfiles().value(name);
1302 s.activeProfile = name;
1303 s.passStore = prof.path;
1304 s.passSigningKey = prof.signingKey;
1305 // A profile without its own Git flags keeps the global values.
1306 s.useGit = prof.useGit.value_or(s.useGit);
1307 s.autoPush = prof.autoPush.value_or(s.autoPush);
1308 s.autoPull = prof.autoPull.value_or(s.autoPull);
1310 ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
1311
1312 QtPassSettings::getPass()->updateEnv();
1313
1315 deselect();
1316 ui->treeView->setCurrentIndex(QModelIndex());
1317}
1318
1320 m_tray = new TrayIcon(this);
1321 if (!m_tray->getIsAllocated()) {
1323 return;
1324 }
1325#ifndef Q_OS_MACOS
1326 m_tray->addToggle(ui->actionShowMenuBar);
1327#endif
1328}
1329
1331 delete m_tray;
1332 m_tray = nullptr;
1333}
1334
1335void MainWindow::closeEvent(QCloseEvent *event) {
1336 // Save in both branches: a tray user only ever hides the window, and
1337 // used to quit through the tray menu with nothing ever written.
1340 this->hide();
1341 event->ignore();
1342 } else {
1343 m_qtPass->clipboard().clear();
1344 event->accept();
1345 // A visible QSystemTrayIcon keeps the application alive, so
1346 // quitOnLastWindowClosed never fires; quit explicitly.
1347 QApplication::quit();
1348 }
1349}
1350
1351auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
1352 if ((obj == ui->toolBar || obj == menuBar()) &&
1353 event->type() == QEvent::PaletteChange) {
1354 // Breeze gives these bars a "header" palette and, after a runtime
1355 // light/dark switch, stamps the previous theme's colours back from a
1356 // cached kdeglobals, hiding the toolbar icons. Deferred so we never
1357 // re-enter the style while it is still applying its palette.
1358 QTimer::singleShot(0, this, &MainWindow::dropStaleToolsAreaPalettes);
1359 }
1360 if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
1361 auto *key = dynamic_cast<QKeyEvent *>(event);
1362 if (key != nullptr && key->key() == Qt::Key_Down) {
1363 ui->treeView->setFocus();
1364 }
1365 }
1366 return QObject::eventFilter(obj, event);
1367}
1368
1369// Only once the application palette changed (see m_themeSwitched): a bar
1370// whose active Window lightness is far from the application's shows the
1371// other theme.
1373 if (!m_themeSwitched) {
1374 return;
1375 }
1376 constexpr int kStaleToolsAreaLightness = 64;
1377 // The active group on both sides: a bar's palette() reads the inactive
1378 // group while another window has focus (the settings app, during a theme
1379 // switch), and Breeze gives the header set different inactive colours.
1380 const int appLightness = QApplication::palette()
1381 .color(QPalette::Active, QPalette::Window)
1382 .lightness();
1383 for (QWidget *bar : {static_cast<QWidget *>(ui->toolBar),
1384 static_cast<QWidget *>(menuBar())}) {
1385 if (!bar->testAttribute(Qt::WA_SetPalette)) {
1386 continue;
1387 }
1388 const int barLightness =
1389 bar->palette().color(QPalette::Active, QPalette::Window).lightness();
1390 if (qAbs(barLightness - appLightness) <= kStaleToolsAreaLightness) {
1391 continue;
1392 }
1393 bar->setPalette(QPalette());
1394 // Both bars are transparent and the style paints the tools area from the
1395 // stale palette, so fill our own. Window, not their Button role, which is
1396 // not the window colour in either theme.
1397 bar->setBackgroundRole(QPalette::Window);
1398 bar->setAutoFillBackground(true);
1399 }
1400}
1401
1403 switch (event->key()) {
1404 case Qt::Key_Delete:
1405 onDelete();
1406 break;
1407 case Qt::Key_Return:
1408 case Qt::Key_Enter:
1409 if (m_tree->proxy().rowCount() > 0) {
1410 on_treeView_clicked(ui->treeView->currentIndex());
1411 }
1412 break;
1413 case Qt::Key_Escape:
1414 ui->lineEdit->clear();
1415 break;
1416 default:
1417 break;
1418 }
1419}
1420
1421void MainWindow::addShareMenu(QMenu &contextMenu, const AppSettings &s) {
1422 const QString dirPath = QDir::cleanPath(m_tree->currentDir(false));
1423 auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
1424 contextMenu.addMenu(shareMenu);
1425
1426 const QString gpgIdPath = Pass::getGpgIdPath(dirPath, s.passStore);
1427 const bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
1428 const QString exePath = s.usePass ? s.passExecutable : s.gpgExecutable;
1429 const bool gpgAvailable =
1430 !exePath.isEmpty() &&
1431 (Executor::parseWslCommand(exePath) || QFile(exePath).exists());
1432
1433 QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
1434 reencrypt->setEnabled(gpgIdExists && gpgAvailable);
1435 connect(reencrypt, &QAction::triggered, this,
1436 [this, dirPath]() { reencryptPath(dirPath); });
1437
1438 QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
1439 exportKey->setEnabled(gpgAvailable);
1440 connect(exportKey, &QAction::triggered, this, &MainWindow::exportPublicKey);
1441
1442 QAction *addRecipientAction = shareMenu->addAction(tr("Add recipient..."));
1443 addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
1444 connect(addRecipientAction, &QAction::triggered, this,
1445 [this, dirPath]() { addRecipient(dirPath); });
1446
1447 QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
1448 connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
1449}
1450
1451void MainWindow::showContextMenu(const QPoint &pos) {
1453 const QModelIndex index = ui->treeView->indexAt(pos);
1454 const bool selected = index.isValid();
1455 if (!selected) {
1456 ui->treeView->clearSelection();
1457 ui->actionDelete->setEnabled(false);
1458 ui->actionEdit->setEnabled(false);
1459 }
1460 ui->treeView->setCurrentIndex(index);
1461 const QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
1462 const QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
1463 const bool isDir = fileOrFolder.isDir();
1464
1465 QMenu contextMenu;
1466 if (!selected || isDir) {
1467 const std::array<std::pair<QString, void (MainWindow::*)()>, 4> onFolder{{
1468 {tr("Open folder with file manager"), &MainWindow::openFolder},
1469 {tr("Add folder"), &MainWindow::addFolder},
1470 {tr("Add password"), &MainWindow::addPassword},
1471 {tr("Users"), &MainWindow::onUsers},
1472 }};
1473 for (const auto &[text, slot] : onFolder) {
1474 connect(contextMenu.addAction(text), &QAction::triggered, this, slot);
1475 }
1476 } else if (fileOrFolder.isFile()) {
1477 connect(contextMenu.addAction(tr("Edit")), &QAction::triggered, this,
1479 }
1480 if (selected) {
1481 contextMenu.addSeparator();
1482 if (isDir) {
1483 connect(contextMenu.addAction(tr("Rename folder")), &QAction::triggered,
1485 } else if (fileOrFolder.isFile()) {
1486 connect(contextMenu.addAction(tr("Rename password")), &QAction::triggered,
1488 }
1489 connect(contextMenu.addAction(tr("Delete")), &QAction::triggered, this,
1491 if (isDir) {
1492 addShareMenu(contextMenu, s);
1493 }
1494 }
1495 contextMenu.exec(globalPos);
1496}
1497
1498void MainWindow::showBrowserContextMenu(const QPoint &pos) {
1499 QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
1500 // Reparent from textBrowser: a browser stylesheet cascades to the child
1501 // QMenu and breaks its opaque native background.
1502 contextMenu->setParent(this, contextMenu->windowFlags());
1503 QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
1504
1505 contextMenu->exec(globalPos);
1506 delete contextMenu;
1507}
1508
1510 QString dir = m_tree->currentDir(false);
1511
1512 QString path = QDir::toNativeSeparators(dir);
1513 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
1514}
1515
1518 bool ok;
1519 QString dir = m_tree->currentDir(false);
1520 QString newdir =
1521 QInputDialog::getText(this, tr("New folder"),
1522 tr("New folder: \n(Will be placed in %1 )")
1523 .arg(s.passStore + m_tree->currentDir(true)),
1524 QLineEdit::Normal, "", &ok);
1525 if (!ok || newdir.isEmpty()) {
1526 return;
1527 }
1528 // Checked and created as one and the same path: the check resolves ".."
1529 // lexically, the system after following links, so "link/../x" would pass
1530 // as the store's x and be made beside the link's target.
1531 newdir = QDir::cleanPath(newdir.prepend(dir));
1532 if (!confirmPathInStore(newdir)) {
1533 return;
1534 }
1535 // Exists already: an error, as QDir::mkdir() made it before.
1536 if (QFileInfo::exists(newdir) || !Util::mkpathForStore(newdir)) {
1537 PlainMessageBox::warning(this, tr("Error"),
1538 tr("Failed to create folder: %1").arg(newdir));
1539 return;
1540 }
1541 // With a signing key, ImitatePass::loadVerifiedRecipients and pass
1542 // (PASSWORD_STORE_SIGNING_KEY) refuse a .gpg-id without .gpg-id.sig, and
1543 // signing needs a passphrase just to make a folder. So it inherits the
1544 // parent's signed list; UsersDialog (Init) gives it its own.
1545 const bool signingConfigured = !s.passSigningKey.trimmed().isEmpty();
1546 if (s.addGPGId && !signingConfigured) {
1547 // Seed from the parent's recipients in effect, not from `enabled` keys:
1548 // that flag is only set in UsersDialog, so it left a zero-byte .gpg-id
1549 // shadowing the parent's recipients (#1682).
1550 if (!Pass::seedGpgIdFile(newdir, s.passStore)) {
1552 this, tr("Error"),
1553 tr("Failed to create .gpg-id file in: %1").arg(newdir));
1554 return;
1555 }
1556 }
1557}
1558
1560 bool ok;
1561 QString srcDir = QDir::cleanPath(m_tree->currentDir(false));
1562 QString srcDirName = QDir(srcDir).dirName();
1563 QString newName =
1564 QInputDialog::getText(this, tr("Rename folder"), tr("Rename folder to: "),
1565 QLineEdit::Normal, srcDirName, &ok);
1566 if (!ok || newName.isEmpty()) {
1567 return;
1568 }
1569 QString destDir = srcDir;
1570 destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
1571 if (!confirmPathInStore(destDir)) {
1572 return;
1573 }
1574 QtPassSettings::getPass()->Move(srcDir, destDir, false);
1575}
1576
1577void MainWindow::editPassword(const QString &file) {
1578 if (!file.isEmpty()) {
1580 if (s.useGit && s.autoPull) {
1581 onUpdate(true);
1582 }
1583 // The dialog decrypts the entry itself to fill its fields. That decrypt
1584 // lands on the same finishedShow the panel uses, so drop the panel's claim
1585 // to it: otherwise passShowHandler() repaints and, with "Always copy",
1586 // puts the entry's old password on the clipboard - replacing whatever the
1587 // user copied to paste into the dialog.
1588 m_shownFile.clear();
1589 setPassword(file, false);
1590 }
1591}
1592
1594 bool ok;
1595 QString file = getFile(ui->treeView->currentIndex(), false);
1596 QString filePath = QFileInfo(file).path();
1597 QString fileName = QFileInfo(file).fileName();
1598 if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
1599 fileName.chop(4);
1600 }
1601
1602 QString newName =
1603 QInputDialog::getText(this, tr("Rename file"), tr("Rename file to: "),
1604 QLineEdit::Normal, fileName, &ok);
1605 if (!ok || newName.isEmpty()) {
1606 return;
1607 }
1608 QString newFile = QDir(filePath).filePath(newName);
1609 if (!confirmPathInStore(newFile)) {
1610 return;
1611 }
1612 QtPassSettings::getPass()->Move(file, newFile, false);
1613}
1614
1616 QFileInfo fileOrFolder = m_tree->fileInfo(ui->treeView->currentIndex());
1617
1618 if (fileOrFolder.isFile()) {
1619 QString file = getFile(ui->treeView->currentIndex(), true);
1620 // The newest Ctrl+C wins: finishedShow names its file, so the decrypt of
1621 // an earlier request is simply not this one's.
1622 m_copyRequestFile = file;
1623 // This Show repaints the panel as well; see onOtp().
1624 m_shownFile = file;
1625 QtPassSettings::getPass()->Show(file);
1626 }
1627}
1628
1630 const QString &file) {
1631 if (m_copyRequestFile.isEmpty() || file != m_copyRequestFile) {
1632 return;
1633 }
1634 m_copyRequestFile.clear();
1635 // The first line, without a list of every line (an entry can expand into
1636 // millions of them); left(-1) is the whole text when there is no newline.
1637 const QString first = text.left(text.indexOf(QLatin1Char('\n')));
1638 // A `pass otp insert` entry's first line is the otpauth:// URI carrying the
1639 // TOTP seed; copying it would leak 2FA material. Matches the display path
1640 // (FileContent::getPasswordForDisplay / PasswordDisplayPanel).
1641 if (FileContent::isOtpUriValue(first)) {
1642 flashText(tr("This entry holds an OTP secret, not a password"), true);
1643 return;
1644 }
1645 m_qtPass->clipboard().copyText(first);
1646}
1647
1648void MainWindow::showStatusMessage(const QString &msg, int timeout) {
1649 ui->statusBar->showMessage(msg, timeout);
1650}
1651
1652void MainWindow::reencryptPath(const QString &dir) {
1653 QDir checkDir(dir);
1654 if (!checkDir.exists()) {
1655 PlainMessageBox::critical(this, tr("Error"),
1656 tr("Directory does not exist: %1").arg(dir));
1657 return;
1658 }
1659 if (refuseLinkedFolder(dir)) {
1660 return;
1661 }
1662
1663 const bool yes = PlainMessageBox::askYes(
1664 this, tr("Re-encrypt passwords"),
1665 tr("Re-encrypt all passwords in %1?\n\n"
1666 "This will re-encrypt ALL password files in this folder "
1667 "using the current recipients defined in .gpg-id.\n\n"
1668 "This may rewrite many files and cannot be undone easily.\n\n"
1669 "Continue?")
1670 .arg(QDir(dir).dirName()));
1671
1672 if (!yes)
1673 return;
1674
1675 // Disable now; the asynchronous startReencryptPath signal re-runs this
1676 // harmlessly.
1678
1679 // Under the current profile's settings: with the pass backend active, the
1680 // native one is not re-initialised on a profile switch.
1681 QtPassSettings::getImitatePass()->reencryptPath(
1682 QDir::cleanPath(QDir(dir).absolutePath()), QtPassSettings::load());
1683}
1684
1685// Idempotent: reencryptPath() calls it before ImitatePass emits the signal.
1687 m_reencryptRunning = true;
1688 setUiElementsEnabled(false);
1689 // The worker always ends with endReencryptPath(), and Cancel is available
1690 // throughout, so the generic watchdog is not needed for this operation.
1691 m_uiWatchdog.stop();
1692 ui->treeView->setDisabled(true);
1694 return;
1695 auto *progress = new QProgressDialog(tr("Re-encrypting passwords..."),
1696 tr("Cancel"), 0, 0, this);
1697 progress->setWindowTitle(tr("Re-encrypt passwords"));
1698 progress->setWindowModality(Qt::WindowModal);
1699 progress->setAutoClose(false);
1700 progress->setAutoReset(false);
1701 progress->setMinimumDuration(0);
1702 connect(progress, &QProgressDialog::canceled, this, [this]() {
1703 QtPassSettings::getImitatePass()->cancelReencryptPath();
1704 showStatusMessage(tr("Cancelling re-encryption"), 5000);
1705 });
1706 m_reencryptProgress = progress;
1707 progress->show();
1708}
1709
1710void MainWindow::reencryptProgress(int current, int total) {
1712 return;
1713 m_reencryptProgress->setMaximum(total);
1714 m_reencryptProgress->setLabelText(
1715 tr("Re-encrypting passwords: %1 of %2").arg(current).arg(total));
1716 // On a modal dialog setValue() runs processEvents(), which can deliver
1717 // endReencryptPath() and reset m_reencryptProgress: keep this last.
1718 m_reencryptProgress->setValue(current);
1719}
1720
1722 m_reencryptRunning = false;
1723 if (m_reencryptProgress) {
1724 m_reencryptProgress->hide();
1725 m_reencryptProgress->deleteLater();
1726 m_reencryptProgress = nullptr;
1727 }
1729}
1730
1733 const QString identity = s.passSigningKey;
1734 if (identity.isEmpty()) {
1735 QMessageBox::information(
1736 this, tr("Export public key"),
1737 tr("<h3>Export your public key</h3>"
1738 "<p>No signing key is configured. Set one in QtPass Settings "
1739 "&gt; GPG keys, or run this in a terminal:</p>"
1740 "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1741 "&gt;</pre>"
1742 "<p>Then send the file to your teammates.</p>"));
1743 return;
1744 }
1745 QString gpgExe = s.gpgExecutable;
1746 if (gpgExe.isEmpty()) {
1747 gpgExe = QStringLiteral("gpg");
1748 }
1749 QStringList args = {"--armor", "--export"};
1750 args.append(identity.split(' ', Qt::SkipEmptyParts));
1751 QString stdOut;
1752 QString stdErr;
1753 int exitCode = Executor::executeBlocking(gpgExe, args, &stdOut, &stdErr);
1754 if (exitCode != 0 || stdOut.isEmpty()) {
1755 PlainMessageBox::warning(this, tr("Export public key"),
1756 tr("Could not export public key for %1.\n\n%2")
1757 .arg(identity, stdErr.isEmpty()
1758 ? tr("No output from gpg.")
1759 : stdErr));
1760 return;
1761 }
1762 ExportPublicKeyDialog dialog(identity, stdOut, this);
1763 dialog.exec();
1764}
1765
1766// UsersDialog only ticks keys already in the keyring; importing a foreign
1767// key still has to happen via gpg (or QtPass settings) first.
1768void MainWindow::addRecipient(const QString &dir) {
1769 if (refuseLinkedFolder(dir)) {
1770 return;
1771 }
1773 d.exec();
1774}
1775
1776// A tree-picked folder that is, or lies behind, a symlink or junction is not
1777// a folder of the store. The store root itself may be a link (a synced
1778// folder); that is configuration, not a tree pick.
1779auto MainWindow::refuseLinkedFolder(const QString &dir) -> bool {
1780 if (!Util::isUnderLink(dir, QtPassSettings::load().passStore)) {
1781 return false;
1782 }
1784 this, tr("Not a folder of the store"),
1785 tr("%1 is, or lies behind, a symbolic link or junction. What that "
1786 "points to is not part of the password store and is left alone.")
1787 .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
1788 return true;
1789}
1790
1792 QMessageBox::information(
1793 this, tr("Sharing passwords with GPG"),
1794 tr("<h3>Sharing passwords with GPG</h3>"
1795 "<p>To share passwords with other users:</p>"
1796 "<ol>"
1797 "<li><b>Export your public key</b> and send it to teammates</li>"
1798 "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1799 "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1800 "them</li>"
1801 "</ol>"
1802 "<p>Only people who have a matching secret key can decrypt the "
1803 "passwords.</p>"
1804 "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1805 "<p>See the FAQ for more details.</p>"));
1806}
1807
1810 // Without Git there is nothing to push or pull, ever: the buttons go, not
1811 // grey out. While an operation runs they stay and are disabled.
1812 const bool usable =
1813 s.useGit && !(s.gitExecutable.isEmpty() && s.passExecutable.isEmpty());
1814 ui->actionPush->setVisible(usable);
1815 ui->actionUpdate->setVisible(usable);
1816 // The separator in front of them would otherwise sit next to the next one.
1817 const QList<QAction *> actions = ui->toolBar->actions();
1818 const qsizetype at = actions.indexOf(ui->actionPush);
1819 if (at > 0 && actions.at(at - 1)->isSeparator()) {
1820 actions.at(at - 1)->setVisible(usable);
1821 }
1822 enableGitButtons(usable && uiEnabled);
1823}
1824
1826 // No platform gating: codes are generated in-process. Visible but disabled
1827 // when OTP is off: the panel never shows an OTP field (a secret), so hiding
1828 // this too would leave no trace of the feature. Honour uiEnabled, or the
1829 // user can stack Show calls while a decrypt is in flight.
1830 ui->actionOtp->setVisible(true);
1831 ui->actionOtp->setEnabled(QtPassSettings::isUseOtp() && uiEnabled);
1832}
1833
1835 const bool enabled = QtPassSettings::isUseGrepSearch();
1836 ui->grepButton->setVisible(enabled);
1837 ui->grepCaseButton->setVisible(enabled);
1838 if (!enabled && m_grep.inGrepMode()) {
1839 ui->grepButton->setChecked(false);
1840 }
1841}
1842
1843void MainWindow::enableGitButtons(const bool &state) {
1844 ui->actionPush->setEnabled(state);
1845 ui->actionUpdate->setEnabled(state);
1846}
1847
1848void MainWindow::critical(const QString &title, const QString &msg) {
1849 // Pass's messages quote store paths; see PlainMessageBox.
1850 PlainMessageBox::critical(this, title, msg);
1851}
1852
1853void MainWindow::onProcessOutput(const QString &output, bool isError,
1854 Enums::PROCESS pid) {
1856 return;
1857 }
1858 m_processOutput->append(output, isError,
1860}
1861
1863 // The action's toggled handler shows or hides the dock.
1864 ui->actionShowProcessOutput->setChecked(
1867}
void trackCopied(const QString &text)
Track text that got onto the clipboard some other way.
void copyText(const QString &text)
Copy text into the system clipboard (or the X11 primary selection when configured) and arm the autocl...
void statusMessage(const QString &message)
Something worth showing in the status bar happened.
The ConfigDialog handles the configuration interface.
void emptyClicked()
Emitted after deselect() has cleared the selection because of a single click on empty space or on a s...
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
Definition executor.cpp:422
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
Definition executor.cpp:114
Dialog showing an ASCII-armored public GPG key for sharing.
Represents the parsed contents of a password file.
Definition filecontent.h:92
static auto boundedForDisplay(const QString &text) -> QString
text cut to kMaxDisplayLines lines, as getRemainingDataForDisplay() is: for showing an entry as it is...
auto getNamedValues() const -> NamedValues
static auto parse(const QString &fileContent, const QStringList &templateFields, bool allFields, const std::function< bool(const QString &)> &alsoField={}) -> FileContent
parse parses the given fileContent in a FileContent object. The password is accessible through getPas...
auto getRemainingDataForDisplay() const -> QString
Like getRemainingData but without data that should not be displayed (like a TOTP secret).
auto getOtpUri() const -> QString
The entry's raw one-time password configuration, if it has one.
auto getPasswordForDisplay() const -> QString
Like getPassword but empty when the password line is itself an otpauth URI.
static auto isOtpUriValue(const QString &value) -> bool
Whether a field value is an otpauth URI, and therefore a secret.
The first start of QtPass: find GnuPG (and pass and Git), make sure there is a secret key,...
void clearGrepMode()
Drop the mode flag only, without touching busy/cancelled state.
auto beginSearch() -> bool
Begin a search for a non-empty query.
auto leaveGrepMode() -> bool
Leave content-search mode (search button toggled off).
auto finishSearch() -> FinishOutcome
Record that a search finished and report required side effects.
void enterGrepMode()
Enter content-search mode (search button toggled on).
auto cancelSearch() -> bool
Cancel the current search (e.g. query cleared).
auto inGrepMode() const -> bool
Whether content-search mode is currently active.
Implementation that imitates 'pass' when the real tool is unavailable.
Definition imitatepass.h:40
void reencryptProgress(int current, int total)
Re-encryption progress: current of total files checked.
void endReencryptPath()
A reencryptPath() run is over, whether it finished, failed, was cancelled or was refused; the main wi...
void startReencryptPath()
A reencryptPath() run is about to start its worker thread.
GrepSearchController m_grep
Mode/busy/cancelled state of the content search.
Definition mainwindow.h:483
void dropStaleToolsAreaPalettes()
Drop a style-imposed menu bar or toolbar palette that belongs to the other theme (Breeze header palet...
void startReencryptPath()
Begin a re-encryption pass on the current path.
void addPassword()
Toolbar/context-menu "Add password": open PasswordDialog for a new entry in the current folder.
void closeEvent(QCloseEvent *event) override
Save window state and geometry on close.
void cancelOtpRequest()
Forget the pending OTP and copy requests.
bool m_uiEnabled
Definition mainwindow.h:542
void renameFolder()
Context menu "Rename folder": ask for a new name and Move the current folder, refusing a target outsi...
void initTextBrowser()
Font, wrapping and link handling of the text browser, per the settings.
void enableGitButtons(const bool &state)
Enable or disable the Push and Pull actions together.
void onConfig()
Toolbar "Config": config(), ignoring its result.
void saveWindowState()
void initToolBarButtons()
~MainWindow() override
Save the window state (a quit from the tray menu or a SIGTERM never delivers closeEvent()) and delete...
void endReencryptPath()
Finish a re-encryption pass on the current path.
QTimer m_uiWatchdog
Definition mainwindow.h:529
void onGrepFinished(const QList< QPair< QString, QStringList > > &results)
Handle results from a completed grep search.
void setPassword(const QString &file, bool isNew=true)
Run PasswordDialog for a new or existing entry. For a new one the dialog gets the store's real folder...
void executeWrapperStarted()
Slot called when an external process wrapper has started.
PasswordDisplayPanel * m_displayPanel
Renders decrypted fields into the grid; created in initDisplayPanel(), parented to this window.
Definition mainwindow.h:488
QTimer searchTimer
Definition mainwindow.h:523
void updateGitButtonVisibility(bool uiEnabled=true)
Show Push and Pull only when Git is in use; enable them only while the interface is.
void onDelete()
Toolbar "Delete" or the Delete key: confirm, then remove the current entry or folder....
void showEvent(QShowEvent *event) override
First-show hook used to focus the search input once the window is actually mapped (avoids races with ...
void editPassword(const QString &file)
Open PasswordDialog on an existing entry, after a blocking pull when auto-pull is on....
void changeEvent(QEvent *event) override
React to language or window state changes.
QPointer< QProgressDialog > m_reencryptProgress
Definition mainwindow.h:534
void on_profileBox_currentTextChanged(const QString &name)
The user picked another profile: save its store, key and Git flags as the active settings,...
void addFolder()
Toolbar/context-menu "Add folder": ask for a name, create the folder under the current one (refusing ...
void updateGrepButtonVisibility()
ProcessOutputPanel * m_processOutput
Definition mainwindow.h:518
void messageAvailable(const QString &message)
Handle an incoming inter-process message (single-instance mode).
MainWindow(const QString &searchText=QString(), QWidget *parent=nullptr)
Construct the main window.
void copyPasswordFromTreeview()
Ctrl+C on the tree: decrypt the current entry and let passwordFromFileToClipboard() copy its first li...
void connectPassSignals(Pass *pass)
Connect the signals of one backend that the window answers itself: errors, critical messages,...
StoreTree * m_tree
The store as shown in the tree: models, view setup, path mapping.
Definition mainwindow.h:520
void reencryptProgress(int current, int total)
Update the re-encryption progress dialog.
void keyPressEvent(QKeyEvent *event) override
Handle keyboard shortcuts.
bool m_freshStart
Definition mainwindow.h:553
void addShareMenu(QMenu &contextMenu, const AppSettings &s)
Add the Share submenu for the folder under the cursor: the entries are enabled only where a recipient...
void on_treeView_doubleClicked(const QModelIndex &index)
A double-click on a tree entry opens it for editing; folders are left to the view.
void showShareHelp()
Share menu "What is this?": the explanation of sharing with GPG.
void onTimeoutSearch()
searchTimer fired: filter the tree on the typed words (matched literally, in order,...
void onPush()
Trigger a git push operation.
void showStatusMessage(const QString &msg, int timeout=2000)
Show a message in the status bar for the given duration.
void focusInput()
Select-all and focus the search box once the window is visible; sets m_firstShowCompleted only on suc...
bool m_themeSwitched
Definition mainwindow.h:498
void on_lineEdit_returnPressed()
Enter in the search box: in content-search mode start (or, for an empty query, cancel) the grep; othe...
auto refuseLinkedFolder(const QString &dir) -> bool
Refuse, with a message, a tree-picked folder that is or lies behind a symlink or junction: what it po...
QString m_copyRequestFile
Definition mainwindow.h:511
void on_grepButton_toggled(bool checked)
The content-search toggle changed: switch the search box between name filtering and regex content sea...
QTimer clearPanelTimer
Definition mainwindow.h:523
void selectFirstFile()
Make the first entry (depth first) under the store root current.
auto confirmDeletion(const QString &file, bool isDir) -> bool
Ask before deleting an entry or a folder, naming what goes and warning when the folder holds anything...
void onOperationError()
An operation ended in an error before or instead of a result: forget the pending requests and what th...
void updateProcessOutputVisibility()
void on_grepResultsList_itemClicked(QTreeWidgetItem *item, int column)
A content-search result was clicked: select the entry in the tree, show it, and go back to the tree (...
void reencryptPath(const QString &dir)
Share menu "Re-encrypt all passwords": confirm, lock the UI via startReencryptPath() and have Imitate...
void passwordFromFileToClipboard(const QString &text, const QString &file)
Copy the first line of a decrypted entry (Ctrl+C). Ignores decrypts of anything but the entry copyPas...
auto confirmPathInStore(const QString &candidate) -> bool
Guard the input dialogs (create, move, rename) against names like ../../etc/passwd or absolute paths ...
void showContextMenu(const QPoint &pos)
Build and show the tree's context menu for the item under the cursor: folder actions (open,...
void updateProfileBoxEnabled()
Enable the profile box only when there is more than one profile, the interface is enabled and the bac...
void critical(const QString &title, const QString &msg)
Display a critical error dialog.
void applyWindowFlagsSettings()
void updateProfileBox()
Fill the profile box from the settings, the active profile selected, without switching profiles while...
void updateOtpButtonVisibility(bool uiEnabled=true)
Refresh the OTP toolbar action from settings.
void addRecipient(const QString &dir)
Share menu "Add recipient": open UsersDialog for dir (keys already in the keyring only).
bool m_firstShowCompleted
Set once focusInput() has actually focused the search box, so later show events leave the focus alone...
Definition mainwindow.h:493
void onEdit()
Toolbar "Edit" or a double-click: editPassword() on the current entry.
void restoreWindow()
Restore window geometry and state from saved settings.
QString m_otpRequestFile
Definition mainwindow.h:507
void onOtp()
Toolbar "OTP": copy the current entry's one-time code. Uses the code the panel already shows for this...
auto confirmLinkRemoval(const QString &file) -> bool
Ask before removing a folder that is a link, which takes the link and leaves what it points to.
void showBrowserContextMenu(const QPoint &pos)
Show the text browser's standard context menu, reparented to the window so the browser's stylesheet d...
QScopedPointer< Ui::MainWindow > ui
The widgets generated from mainwindow.ui.
Definition mainwindow.h:481
auto eventFilter(QObject *obj, QEvent *event) -> bool override
Filter events from watched objects.
void showAbout()
Show the About box: version, project link, licence.
static constexpr int UiWatchdogMs
Interval of m_uiWatchdog in milliseconds.
Definition mainwindow.h:531
void openFolder()
Context menu "Open folder with file manager" on the current folder.
void exportPublicKey()
Share menu "Export my public key": run gpg --armor --export for the configured signing key and show t...
QString m_shownFile
Definition mainwindow.h:503
void initTrayIcon()
Create m_tray and add the menu bar toggle to its menu; gives up (m_tray stays null) when no system tr...
void flashText(const QString &text, const bool isError, const bool isHtml=false)
Display a transient message in the text panel.
void connectBackends()
void initStatusBar()
Welcome message and the permanent logo in the status bar.
void initDisplayPanel()
The grid of decrypted fields and what its buttons ask for.
void otpFromFileToClipboard(const QString &output, const QString &file)
Generate a one-time password from decrypted content and copy it. Ignores decrypts of anything but the...
void clearPanel(bool notify=true)
Empty the display panel and the text browser, and leave content search if its results were showing....
void setUiElementsEnabled(bool state)
Enable or disable the main UI elements.
auto event(QEvent *event) -> bool override
Notice an application palette change, the theme switch that makes a style-imposed bar palette suspect...
void applyTextBrowserSettings()
auto config() -> bool
Open the configuration dialog.
auto getCurrentTreeViewIndex() -> QModelIndex
Return the currently selected index in the tree view.
bool m_initSucceeded
Definition mainwindow.h:550
void onUpdate(bool block=false)
Toolbar "Update": git pull.
void initTimers()
void deselect()
Clear the current tree view selection.
auto getFile(const QModelIndex &index, bool forPass) -> QString
StoreTree::fileFor() on the tree.
void onProcessOutput(const QString &output, bool isError, Enums::PROCESS pid=Enums::INVALID)
Handle generic process output for display in output panel.
void passShowHandler(const QString &output, const QString &file=QString())
Render a decrypted entry: fields into the display panel, the rest into the text browser,...
void on_treeView_clicked(const QModelIndex &index)
Handle a click on an item in the tree view.
void initProcessOutput()
void onUsers()
Toolbar/context-menu "Users": open UsersDialog for the current folder, unless it lies behind a link.
void destroyTrayIcon()
Delete m_tray and reset it to null.
TrayIcon * m_tray
Definition mainwindow.h:546
QtPass * m_qtPass
The UI-neutral glue to the backends (output, status, clipboard, QR codes); created in the constructor...
Definition mainwindow.h:479
auto firstFile(QModelIndex parentIndex) -> QModelIndex
Depth-first search for the first entry under a folder.
void renamePassword()
Context menu "Rename password": ask for a new name and Move the current entry within its folder,...
void on_lineEdit_textChanged(const QString &arg1)
The search box changed (name filtering only): drop the pending decrypt and the panel,...
bool m_reencryptRunning
Definition mainwindow.h:539
MenuBarPeek(QMainWindow *window, QAction *toggle)
Watch window for Alt while its menu bar is hidden.
static auto isMoreMarker(const QString &line) -> bool
Whether a line in a result is the marker for "more matches inthis entry" rather than a match.
Definition nativegrep.h:80
static auto isStoppedMarker(const QPair< QString, QStringList > &result) -> bool
Whether a result is the marker for "the search stopped atkMaxMatches, more entries were not searched"...
Definition nativegrep.h:94
Abstract base class for password store operations.
Definition pass.h:75
void startingExecuteWrapper()
Emitted each time a queued process is about to start, and by the refusals that go on to emit processE...
void finishedAnyWithPid(const QString &out, const QString &err, Enums::PROCESS pid)
A process of any kind finished successfully and its output cannot contain a secret.
static auto getGpgIdPath(const QString &for_file, const QString &passStore) -> QString
Get .gpg-id file path for a password file.
Definition pass.cpp:920
void finishedShow(const QString &out, const QString &file)
Emitted when a Show() succeeds.
void statusMsg(const QString &msg, int timeout)
Progress or a non-fatal problem worth a line in the status bar ("Creating backup commit",...
static auto seedGpgIdFile(const QString &newDir, const QString &passStore) -> bool
Seed a new folder's .gpg-id from the recipients it inherits.
Definition pass.cpp:1182
void finishedGrep(const QList< QPair< QString, QStringList > > &results)
The newest Grep() is over; an empty list means no entry matched.
void processErrorExit(int exitCode, const QString &err)
An operation ended in failure: its process exited non-zero, could not be started or crashed,...
void updateEnv()
Update environment for subprocesses.
Definition pass.cpp:631
void idle()
Nothing of the store's runs or waits any more (isIdle()).
void critical(const QString &title, const QString &message)
Something went wrong that the user has to be told about in a dialog, such as a refused path or a fail...
Dialog for creating a password entry or editing an existing one.
void setAvailableTemplates(const QHash< QString, QStringList > &templates, const QString &defaultTemplate)
Set available templates from file and select default.
void copied(const QString &text)
Text was copied out of the password, a field or the body, marked as a secret: for ClipboardManager::t...
void setNewEntryLocation(const QString &storeRoot, const QStringList &folders, const QString &currentFolder)
Let a new entry be named inside the dialog: a folder picker over the store's folders and a name field...
auto isSecret(const QString &field, const AppSettings &s) const -> bool
Whether a field is a secret "Hide password" hides: one a template marks (setMarkedSecrets()),...
void qrRequested(const QString &text)
Emitted when a field's QR button is clicked.
void refreshPalette()
Re-derive palette-dependent styling after a runtime theme change.
void setMarkedSecrets(const QStringList &names)
Set the field names a template marks as secret.
void displayFields(const QString &password, const NamedValues &namedValues, const AppSettings &s, const QString &otpConfig={})
Render the password and template fields of a decrypted entry.
void clear()
Remove all field rows and collapse the container spacing.
PasswordDisplayPanel(QGridLayout *grid, QBoxLayout *container, QWidget *widgetParent, QObject *parent=nullptr)
Construct a panel that renders into the given layouts.
void textCopied(const QString &text)
Text was selected and copied out of a field's value (Ctrl+C, the context menu, a drag,...
void copyRequested(const QString &text)
Emitted when a field's copy button is clicked.
auto currentOtpCode() const -> QString
The one-time password currently on display, if there is one.
void editRequested()
Emitted on a double-click anywhere on a field row: the user wants to change what they are looking at.
static auto isPathInStore(const QString &storeRoot, const QString &candidate) -> bool
Check that a candidate path resolves inside the password store root, after following symlinks and res...
static auto isSensitiveProcess(Enums::PROCESS pid) -> bool
Whether a process's output may carry secrets and must never reach this long-lived panel (pass show,...
ProcessOutputPanel(QWidget *parent=nullptr)
Build the console (Clear button + read-only text edit).
static auto processName(Enums::PROCESS pid) -> QString
Human-readable command label for a process id.
void append(const QString &output, bool isError, const QString &linePrefix=QString())
Append output, one numbered line per non-empty input line.
void operationFinished()
A backend operation is over, successfully or not; the interface can be enabled again.
void statusMessage(const QString &message, int timeout)
A short message for the status bar.
static auto formatOutput(QString output, const QString &prefix=QString(), const QString &postfix=QString()) -> QString
Turn a process' plain-text output into the HTML the text browser shows: http(s) URLs become links,...
Definition qtpass.cpp:142
void entryInserted()
An entry was written; the view of the current entry is stale.
void pushRequested()
The store changed and the settings ask for an automatic push.
void outputReady(const QString &html)
Formatted output (see formatOutput()) or an error rendered in colour, ready for the text browser.
auto clipboard() -> ClipboardManager &
The clipboard handling for this application instance.
Definition qtpass.h:54
void init()
Resolve the password store and the executables and migrate the settings of an older version (fresh-in...
Definition qtpass.cpp:31
QtPass(QObject *parent=nullptr)
Construct the glue and connect it to both pass backends.
Definition qtpass.cpp:22
void showTextAsQRCode(const QString &text)
Render text as a QR code with qrencode and show it in a popup at the cursor; failures are reported th...
Definition qtpass.cpp:157
static auto getPass() -> Pass *
Get currently active pass backend instance.
static auto isUseGit(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether Git integration is enabled.
static auto isUseOtp(const bool &defaultValue=true) -> bool
Check whether OTP support is enabled.
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
Get password store directory path.
static auto load() -> AppSettings
Load all flat settings into an AppSettings value object.
static auto isUseGrepSearch(const bool &defaultValue=false) -> bool
Check whether content search is enabled.
static auto getProfiles() -> Profiles
Get all configured profiles.
static auto getRealPass() -> RealPass *
Get real pass backend instance.
static auto getProfile(const QString &defaultValue=QVariant().toString()) -> QString
Get active profile name.
static auto isShowProcessOutput(const bool &defaultValue=false) -> bool
Get showProcessOutput setting.
static void setSavestate(const QByteArray &saveState)
Save window state.
static auto isAlwaysOnTop(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether main window should stay always on top.
static auto getImitatePass() -> ImitatePass *
Get imitate pass backend instance.
static auto isHideOnClose(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether closing the window hides the application.
static void save(const AppSettings &settings)
Persist an AppSettings value object to the singleton store.
static void setGeometry(const QByteArray &geometry)
Save window geometry.
static auto getGeometry(const QByteArray &defaultValue=QVariant().toByteArray()) -> QByteArray
Get saved window geometry.
static auto getSavestate(const QByteArray &defaultValue=QVariant().toByteArray()) -> QByteArray
Get saved window state.
static auto backendsIdle() -> bool
Whether no backend, the active one or one kept from before a switch, runs or waits for anything (Pass...
void copied(const QString &text)
Selected text was copied out of the browser.
QSortFilterProxyModel for filtering and displaying password store.
Definition storemodel.h:84
auto fileInfo(const QModelIndex &index) const -> QFileInfo
File system information for a view index.
Definition storetree.cpp:92
auto rootIndex() -> QModelIndex
The proxy's root index for the current store.
void setFilter(const QRegularExpression &pattern)
Filter the visible entries.
Definition storetree.cpp:49
StoreTree(QTreeView *view, QObject *parent=nullptr)
Attach the proxy model to the view and set its look: hidden header, indentation, scroll bars,...
Definition storetree.cpp:14
void setStore(const QString &path)
Point everything at path: file system root, proxy store and view root index. Also drops a search filt...
Definition storetree.cpp:26
auto fileSystem() -> QFileSystemModel &
Definition storetree.h:134
auto indexFor(const QString &path) const -> QModelIndex
The view index for an absolute path, or invalid when the file system model does not (yet) know it.
Definition storetree.cpp:96
auto proxy() -> StoreModel &
Definition storetree.h:132
void setPass(Pass *pass)
Hand the proxy the backend it drops onto.
Definition storetree.cpp:47
auto currentDir(bool forPass) const -> QString
dirFor() applied to the view's current index.
Definition storetree.cpp:71
static auto readTemplates(const QString &storePath) -> QHash< QString, QStringList >
Read templates from .templates file in password store.
static auto fieldsIn(const QString &templateText) -> QStringList
The field names of the template in the settings: one per line, with comment lines (starting with #,...
static auto getFolderTemplate(const QString &folderPath, const QString &storePath) -> QString
Get default template for a folder. Looks in folder, then parent folders up to root.
static auto readSecretFields(const QString &storePath) -> QStringList
The field names the store's .templates file marks as secret, in any of its sections (see secretFields...
static auto secretFieldsIn(const QString &templateText) -> QStringList
The field names a template marks as secret.
static auto generateNow(const Settings &settings) -> QString
Generate the code for the current wall-clock time.
Definition totp.cpp:259
static auto parse(const QString &config) -> std::optional< Settings >
Parse an OTP configuration string.
Definition totp.cpp:106
void addToggle(QAction *action)
Put one of the window's toggles in the tray menu, above Quit.
Definition trayicon.cpp:76
TrayIcon(QMainWindow *parent)
Construct a TrayIcon associated with the given main window.
Definition trayicon.cpp:16
auto getIsAllocated() -> bool
Check whether tray resources have been allocated and initialized.
Definition trayicon.cpp:38
Dialog for selecting GPG recipients for password encryption.
Definition usersdialog.h:35
static auto regularFilesUnder(const QString &dir, const QStringList &nameFilters, QStringList *skipped=nullptr, bool hiddenFiles=false) -> QStringList
Regular files under dir whose name matches one of nameFilters, found by walking only real directories...
Definition util.cpp:721
static auto isUnderLink(const QString &path, const QString &storeRoot, bool includeSelf=true) -> bool
Whether the way from storeRoot down to path passes through a symbolic link or NTFS junction: path its...
Definition util.cpp:765
static auto findPasswordStore() -> QString
Locate the password store directory.
Definition util.cpp:73
static auto configIsValid(const AppSettings &s) -> bool
Verify that the required configuration is complete.
Definition util.cpp:556
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
Definition util.cpp:438
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
Definition util.cpp:761
static auto directoriesUnder(const QString &dir) -> QStringList
Every real, visible directory under dir, walked by the same rules as regularFilesUnder(): links,...
Definition util.cpp:749
Settings dialog: every page of the configuration window and the profile table, with the validation th...
The Enums namespace: clipboard behaviour and the identifiers of the subprocess kinds QtPass queues.
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
Dialog that shows an ASCII-armored public key and lets the user copy it or save it as a ....
Parser for a decrypted password entry: password line, key: value fields, remaining text and the OTP c...
The first-run wizard and its four pages: programs, key, store and summary.
MainWindow: the main application window (tree, toolbar, search, status bar, process output dock) and ...
MenuBarPeek: shows the main window's hidden menu bar while Alt is held or tapped, and hides it again ...
PROCESS
Identifies different subprocess operations used in QtPass.
Definition enums.h:43
auto askYes(QWidget *parent, const QString &title, const QString &text) -> bool
Ask a yes/no question in plain text.
void warning(QWidget *parent, const QString &title, const QString &text)
Show a warning in plain text.
void critical(QWidget *parent, const QString &title, const QString &text)
Show an error in plain text.
void centreOnCursorScreen(QWidget &window)
Move window so its frame is centred on the screen under the mouse pointer (the primary screen when th...
NativeGrep, the content search that decrypts every entry on a worker thread instead of running pass g...
Dialog for creating or editing one password entry, including the generator, template fields,...
PasswordDisplayPanel: renders the decrypted fields of an entry (password, template fields,...
PathValidator: is a path inside the password store, and how is it spelled relative to the store root.
QMessageBox helpers that force Qt::PlainText so store-controlled names can never be read as rich-text...
ProcessOutputPanel: the dockable console that shows numbered, colour-coded git/pass/gpg output.
QMap< QString, Profile > Profiles
All profiles by name. QMap so iteration order is the name order.
Definition profile.h:81
Icon button that asks for a stored secret to be shown as a QR code.
Eye-icon button that toggles a QLineEdit between password and plain-text echo mode.
Copy-to-clipboard icon button that carries the text of the field it sits next to.
QtPass: UI-neutral glue that turns backend completion signals into formatted output,...
The "qtpass" logging category.
QtPassSettings, the QSettings singleton behind every saved setting, the profiles and the Pass backend...
Plain value-object holding all flat QtPass configuration.
Definition appsettings.h:36
bool displayAsIs
Show file content unmodified.
Definition appsettings.h:97
QString gpgExecutable
Path to the gpg/gpg2 executable.
Definition appsettings.h:66
QString passSigningKey
GPG key used to sign .gpg-id files.
Definition appsettings.h:61
bool usePass
Use the pass CLI (true) or native git/gpg.
Definition appsettings.h:59
bool hideContent
Hide the entire content panel.
Definition appsettings.h:95
bool useOtp
Enable native TOTP (OTP) support.
QString passStore
Password-store root path.
Definition appsettings.h:60
bool hidePassword
Definition appsettings.h:85
bool useAutoclearPanel
Clear the content panel after a delay.
Definition appsettings.h:81
bool noLineWrapping
Disable line wrapping in the panel.
Definition appsettings.h:98
bool showMenuBar
Show the menu bar (Ctrl+M toggles it).
QString gitExecutable
Path to the git executable.
Definition appsettings.h:65
bool autoPull
Automatically git pull on open.
int autoclearPanelSeconds
Panel autoclear delay (seconds).
Definition appsettings.h:82
bool startMinimized
Start the window minimised.
bool useTrayIcon
Show a system tray icon.
QString passExecutable
Path to the pass executable.
Definition appsettings.h:64
bool useGit
Enable git integration.
bool addGPGId
Auto-add .gpg-id files to git.
QString passTemplate
Newline-separated template field names.
bool useMonospace
Render content in a monospace font.
Definition appsettings.h:96
bool templateAllFields
Treat every key: line as a field.
QString activeProfile
Name of the active profile.
Definition appsettings.h:56
bool useTemplate
Use the password template.
Outcome of a search-completion transition.
bool discard
The finished search was cancelled; ignore results.
bool restoreCursor
A wait cursor was active and must be restored.
One named password store: where it is, how its .gpg-id is signed and, optionally, its own Git switche...
Definition profile.h:26
std::optional< bool > autoPush
Push after commits; unset = global.
Definition profile.h:30
QString path
Password store directory.
Definition profile.h:27
std::optional< bool > autoPull
Pull on start; unset = global.
Definition profile.h:31
std::optional< bool > useGit
Git on/off; unset follows the global.
Definition profile.h:29
QString signingKey
Key(s) that sign this store's .gpg-id.
Definition profile.h:28
TemplateIO: the store's .templates and .default_template files and the # secret: marker in template t...
RFC 6238 TOTP: otpauth URI parsing, code generation and canonical re-serialisation.
TrayIcon: the system tray icon and its context menu that show, hide, minimise, maximise,...
UsersDialog, the recipient picker for a folder's .gpg-id.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...
constexpr int MS_PER_SECOND
Milliseconds in a second, for timers set from a setting in seconds.
Definition util.h:25
WindowStateStore: saves and restores top-level window geometry through QtPassSettings,...