diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 20:53:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-04 12:54:17 +0200 |
| commit | 45e5887f5b33e5f30d3c16187bb5d6e707983804 (patch) | |
| tree | 806b2a7bb0b8f914caa3db7b0eeb4eadae84bb96 | |
| parent | eb56cf5bb04771244f7bb90eadef165ccc5b9a94 (diff) | |
| download | qtmaildir-45e5887f5b33e5f30d3c16187bb5d6e707983804.tar.gz qtmaildir-45e5887f5b33e5f30d3c16187bb5d6e707983804.zip | |
feat(config): add completion_on_focus and extra_mimetypes
Mimetypes are the one completion list with no enumerator, so the user can
extend it. Entries append to the built-ins and a malformed one is skipped
with a problem recorded rather than dropping the whole list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | src/config.cpp | 32 | ||||
| -rw-r--r-- | src/config.h | 17 | ||||
| -rw-r--r-- | tests/test_config.cpp | 74 |
3 files changed, 123 insertions, 0 deletions
diff --git a/src/config.cpp b/src/config.cpp index e92c7f3..1a8f322 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -87,6 +87,38 @@ void Config::load(const QString &path) } } + // A [general] key, so no prefix, per the note at the top of load(). + m_completionOnFocus = + settings.value(QStringLiteral("completion_on_focus"), false).toBool(); + + // [completion] is an ordinary section, so this one DOES take its prefix. + // ',' separates entries and '|' separates a value from its description: + // two different characters because QSettings splits comma lists itself, + // so a description holding a comma would otherwise become two entries. + // Neither character is legal in a mimetype. + const QStringList rawMimetypes = + settings.value(QStringLiteral("completion/extra_mimetypes")).toStringList(); + for (const QString &raw : rawMimetypes) { + const QString entry = raw.trimmed(); + if (entry.isEmpty()) + continue; + + const int bar = entry.indexOf(QLatin1Char('|')); + const QString value = (bar < 0 ? entry : entry.left(bar)).trimmed(); + const QString description = + (bar < 0 ? QString() : entry.mid(bar + 1)).trimmed(); + + // Skip only the bad entry: one typo must not cost the user the rest + // of the list, and the built-ins are appended to regardless. + if (value.isEmpty()) { + addProblem(QStringLiteral("[completion] extra_mimetypes: entry '%1' " + "has no mimetype; ignoring it.") + .arg(entry)); + continue; + } + m_extraMimetypes.append({ value, description }); + } + m_syncCommand = settings.value(QStringLiteral("sync/command")).toString(); if (m_syncCommand.isEmpty()) { // Not a problem: sync is optional, and nothing the user asked for is diff --git a/src/config.h b/src/config.h index f3f1706..63fa541 100644 --- a/src/config.h +++ b/src/config.h @@ -23,6 +23,8 @@ #include <QString> #include <QStringList> +#include "completionentry.h" + /// One mail account. notmuch has no concept of accounts; it sees a single flat /// tree. An account is therefore a path prefix within that tree plus an /// identity. @@ -95,6 +97,19 @@ public: /// ever the default. Clamped by MessageView::clampZoom() on use. qreal messageZoom() const { return m_messageZoom; } + /// Whether focusing an empty query bar opens the completion popup. Off by + /// default: it is helpful when learning the query language and intrusive + /// once it is known. The manual trigger works regardless. + bool completionOnFocus() const { return m_completionOnFocus; } + + /// User-supplied mimetype completions, APPENDED to the built-in list. + /// Appending rather than replacing means a typo cannot leave completion + /// worse off than the defaults. Mimetypes are the only completion list a + /// user can extend, because they are the only one with no enumerator and + /// an open-ended set: prefixes are fixed, paths come from the configured + /// accounts, dates are closed, tags come from the database. + QList<CompletionEntry> extraMimetypes() const { return m_extraMimetypes; } + /// Every non-fatal problem, both kinds below. Shown in the status bar. QStringList warnings() const { return m_warnings; } @@ -119,6 +134,8 @@ private: QString m_syncCommand; QString m_notmuchConfig; qreal m_messageZoom = 1.0; + bool m_completionOnFocus = false; + QList<CompletionEntry> m_extraMimetypes; QString m_startupQuery = QStringLiteral("Unread"); /// Whether startup_query came from the config rather than being the diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 24f6afb..451eb43 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -39,6 +39,11 @@ private slots: void unknownStartupQueryFallsBackAndReports(); void generalSectionKeysAreActuallyRead(); void messageZoomDefaultsAndValidates(); + void completionOnFocusDefaultsToFalse(); + void completionOnFocusIsActuallyRead(); + void extraMimetypesAppendToBuiltins(); + void extraMimetypeDescriptionMayContainComma(); + void malformedExtraMimetypeIsSkipped(); }; static QString writeIni(const QTemporaryDir &dir, const QString &body) @@ -345,5 +350,74 @@ void TestConfig::messageZoomDefaultsAndValidates() } } +void TestConfig::completionOnFocusDefaultsToFalse() +{ + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n"))); + QCOMPARE(config.completionOnFocus(), false); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::completionOnFocusIsActuallyRead() +{ + // The default is false, so a test that only checks the default would pass + // just as happily against a "general/completion_on_focus" lookup that + // matches nothing. Round-tripping a true proves the key is really read. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "completion_on_focus=true\n"))); + QCOMPARE(config.completionOnFocus(), true); +} + +void TestConfig::extraMimetypesAppendToBuiltins() +{ + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[completion]\n" + "extra_mimetypes = application/epub+zip|EPUB book, message/rfc822\n"))); + + const QList<CompletionEntry> extra = config.extraMimetypes(); + QCOMPARE(extra.size(), 2); + QCOMPARE(extra.at(0).value, QStringLiteral("application/epub+zip")); + QCOMPARE(extra.at(0).description, QStringLiteral("EPUB book")); + QCOMPARE(extra.at(1).value, QStringLiteral("message/rfc822")); + QVERIFY(extra.at(1).description.isEmpty()); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::extraMimetypeDescriptionMayContainComma() +{ + // '|' separates value from description precisely so a description can + // contain a comma without QSettings tearing the entry in two. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[completion]\n" + "extra_mimetypes = \"application/epub+zip|EPUB, an ebook format\"\n"))); + + const QList<CompletionEntry> extra = config.extraMimetypes(); + QCOMPARE(extra.size(), 1); + QCOMPARE(extra.at(0).value, QStringLiteral("application/epub+zip")); + QCOMPARE(extra.at(0).description, QStringLiteral("EPUB, an ebook format")); +} + +void TestConfig::malformedExtraMimetypeIsSkipped() +{ + // One bad entry must not cost the user the rest of the list. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[completion]\n" + "extra_mimetypes = |no value here, message/rfc822\n"))); + + const QList<CompletionEntry> extra = config.extraMimetypes(); + QCOMPARE(extra.size(), 1); + QCOMPARE(extra.at(0).value, QStringLiteral("message/rfc822")); + QVERIFY(!config.problems().isEmpty()); +} + QTEST_MAIN(TestConfig) #include "test_config.moc" |
