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

IJHack / QtPass / 35412814380

19 Sep 2026 01:29AM UTC coverage: 77.405% (+0.05%) from 77.353%
35412814380

push

github

web-flow
A barer main window: menu bar off by default, no Git buttons without Git (#1830)

* A barer main window: menu bar off by default, no Git buttons without Git

The menu bar (#1787) is opt-in now: showMenuBar defaults to false, the
window looks as it always did, and besides Ctrl+M there is a "Show menu
bar" box on the configuration dialog's General page, which config()
applies. Push and Pull are hidden, with the separator in front of them,
whenever Git is not in use; while an operation runs they stay and are
disabled, which is what the GNOME "disable, don't hide" advice is about.
Help → FAQ opens qtpass.org/faq; the commit that made that change was
pushed to #1825 after it had been merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* Double-click on a field in the panel opens the editor

The tree already edits on double-click; the panel showing the fields did
nothing. PasswordDisplayPanel filters the label and the value widgets of
every row and emits editRequested(), which MainWindow turns into onEdit()
when editing is possible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

30 of 34 new or added lines in 4 files covered. (88.24%)

26 existing lines in 4 files now uncovered.

6646 of 8586 relevant lines covered (77.41%)

73.76 hits per line

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

81.11
/src/nativegrep.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "nativegrep.h"
4
#include "executor.h"
5
#include <QDir>
6
#include <QDirIterator>
7
#include <QElapsedTimer>
8
#include <QPointer>
9
#include <QProcess>
10
#include <QRegularExpression>
11
#include <QThread>
12
#include <utility>
13

14
NativeGrep::NativeGrep(QObject *parent) : QObject(parent) {}
76 ✔
15

16
NativeGrep::~NativeGrep() {
76 ✔
17
  static constexpr int kThreadTimeoutMs = 5000;
18
  cancel();
76 ✔
19
  QElapsedTimer elapsed;
76 ✔
20
  elapsed.start();
76 ✔
21
  for (const Worker &w : std::as_const(m_workers)) {
76 ✔
UNCOV
22
    if (w.thread && w.thread->isRunning()) {
×
23
      const int remaining =
24
          kThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
×
25
      if (remaining > 0)
×
26
        w.thread->wait(remaining);
×
27
    }
28
  }
29
}
76 ✔
30

31
/**
32
 * @brief Decrypt one .gpg file and return lines matching rx.
33
 */
34
auto NativeGrep::matchFile(const QProcessEnvironment &env,
10 ✔
35
                           const QString &gpgExe, const QString &filePath,
36
                           const QRegularExpression &rx,
37
                           const std::atomic_bool *cancel) -> QStringList {
38
  QString translatedPath = filePath;
39
  if (gpgExe.startsWith(QStringLiteral("wsl "))) {
20 ✔
40
    QString wslPath;
×
41
    const int wrc = Executor::executeBlocking(
×
42
        QStringLiteral("wsl"),
×
43
        Executor::wslExecArgs(QStringLiteral("wslpath"), {filePath}), &wslPath);
×
44
    const QString translated = wslPath.trimmed();
45
    if (wrc == 0 && !translated.isEmpty())
×
46
      translatedPath = translated;
×
47
  }
48
  QString plaintext;
10 ✔
49
  // The QProcess overload is the one that takes the cancel flag; it polls
50
  // the flag while waiting and terminates (then kills) gpg from this thread,
51
  // the one that owns the process.
52
  QProcess gpg;
10 ✔
53
  gpg.setProcessEnvironment(env);
10 ✔
54
  const int rc =
55
      Executor::executeBlocking(gpg, gpgExe,
90 ✔
56
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
57
                                 "--batch", "--use-agent", translatedPath},
58
                                QString(), &plaintext, nullptr, cancel);
10 ✔
59
  if (rc != 0 || plaintext.isEmpty())
10 ✔
60
    return {};
4 ✔
61
  QStringList matches;
6 ✔
62
  for (const QString &line : plaintext.split('\n')) {
30 ✔
63
    QString candidate = line;
64
    if (candidate.endsWith('\r'))
18 ✔
65
      candidate.chop(1);
×
66
    const QString t = candidate.trimmed();
67
    if (!t.isEmpty() && candidate.contains(rx))
18 ✔
68
      matches << t;
69
  }
70
  return matches;
71
}
20 ✔
72

73
/**
74
 * @brief Walk the store, decrypt every .gpg file, collect matches.
75
 */
76
auto NativeGrep::scanStore(const QProcessEnvironment &env,
6 ✔
77
                           const QString &gpgExe, const QString &storeDir,
78
                           const QRegularExpression &rx,
79
                           const std::atomic_bool *cancel)
80
    -> QList<QPair<QString, QStringList>> {
81
  QList<QPair<QString, QStringList>> results;
6 ✔
82
  QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
12 ✔
83
                  QDirIterator::Subdirectories);
6 ✔
84
  while (it.hasNext()) {
15 ✔
85
    if (QThread::currentThread()->isInterruptionRequested() ||
9 ✔
86
        (cancel != nullptr && cancel->load()))
9 ✔
87
      return {};
×
88
    const QString filePath = it.next();
9 ✔
89
    const QStringList matches = matchFile(env, gpgExe, filePath, rx, cancel);
9 ✔
90
    if (!matches.isEmpty()) {
9 ✔
91
      QString entry = QDir(storeDir).relativeFilePath(filePath);
6 ✔
92
      if (entry.endsWith(QLatin1String(".gpg")))
6 ✔
93
        entry.chop(4);
6 ✔
94
      results.append({entry, matches});
6 ✔
95
    }
96
  }
97
  return results;
98
}
6 ✔
99

100
/**
101
 * @brief Start a search on a worker thread.
102
 *
103
 * Results are emitted on the owner's thread via QMetaObject::invokeMethod. A
104
 * sequence counter discards results from superseded searches; the previous
105
 * search is asked to stop but not waited for, since blocking the UI thread
106
 * while gpg decrypts would freeze the interface.
107
 */
108
void NativeGrep::search(const QString &pattern, bool caseInsensitive,
6 ✔
109
                        const QString &gpgExe, const QString &storeDir,
110
                        const QProcessEnvironment &env) {
111
  cancel();
6 ✔
112

113
  // Advance the sequence before any early return so in-flight workers from the
114
  // previous query fail the seq check and cannot publish stale results.
115
  const int seq = ++m_seq;
6 ✔
116

117
  // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
118
  // regex that matches every non-empty line, which is almost never intentional
119
  // and would decrypt the entire store.
120
  //
121
  // Both early returns post finished() via Qt::QueuedConnection so that the
122
  // signal is always delivered asynchronously after search() returns, matching
123
  // the contract of the threaded path.
124
  if (pattern.trimmed().isEmpty()) {
6 ✔
125
    QMetaObject::invokeMethod(
×
126
        this,
127
        [this, seq]() {
×
128
          if (m_seq == seq)
×
129
            emit finished({});
×
130
        },
×
131
        Qt::QueuedConnection);
132
    return;
1 ✔
133
  }
134

135
  const QRegularExpression rx(
136
      pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
137
                               : QRegularExpression::PatternOptions{});
12 ✔
138
  if (!rx.isValid()) {
6 ✔
139
    QMetaObject::invokeMethod(
1 ✔
140
        this,
141
        [this, seq]() {
1 ✔
142
          if (m_seq == seq)
1 ✔
143
            emit finished({});
2 ✔
144
        },
1 ✔
145
        Qt::QueuedConnection);
146
    return;
147
  }
148
  QPointer<NativeGrep> self(this);
149

150
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
10 ✔
151
    if (!self)
5 ✔
152
      return;
153
    QMetaObject::invokeMethod(
5 ✔
154
        self,
155
        [self, seq, results = std::move(results)]() {
15 ✔
156
          if (self && self->m_seq == seq)
10 ✔
157
            emit self->finished(results);
5 ✔
158
        },
5 ✔
159
        Qt::QueuedConnection);
160
  };
5 ✔
161

162
  auto cancelFlag = std::make_shared<std::atomic_bool>(false);
5 ✔
163
  QThread *thread = QThread::create([gpgExe, storeDir, env, rx, cancelFlag,
10 ✔
164
                                     emitResults = std::move(emitResults)]() {
165
    std::move(emitResults)(
5 ✔
166
        scanStore(env, gpgExe, storeDir, rx, cancelFlag.get()));
5 ✔
167
  });
5 ✔
168

169
  m_workers.append({thread, cancelFlag});
10 ✔
170
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
5 ✔
171
  connect(thread, &QThread::finished, this, [this, thread]() {
5 ✔
172
    m_workers.removeIf(
5 ✔
173
        [thread](const Worker &w) { return w.thread == thread; });
5 ✔
174
  });
175
  thread->start();
5 ✔
176
}
11 ✔
177

178
void NativeGrep::cancel() {
158 ✔
179
  for (const Worker &w : std::as_const(m_workers)) {
159 ✔
180
    w.cancel->store(true);
181
    if (w.thread && w.thread->isRunning())
1 ✔
182
      w.thread->requestInterruption();
1 ✔
183
  }
184
}
158 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc