summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 20:53:49 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:54:17 +0200
commit45e5887f5b33e5f30d3c16187bb5d6e707983804 (patch)
tree806b2a7bb0b8f914caa3db7b0eeb4eadae84bb96
parenteb56cf5bb04771244f7bb90eadef165ccc5b9a94 (diff)
downloadqtmaildir-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.cpp32
-rw-r--r--src/config.h17
-rw-r--r--tests/test_config.cpp74
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"