diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-18 15:09:09 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-18 15:09:09 +0200 |
| commit | 729fd1465718ef1da5f61452bcb7eb23d02c9c9a (patch) | |
| tree | 49df1e831e90d012685e829b8eb52c5d5fd1a3dc | |
| parent | 3303c7855cb22a6bb4f36aa1a17707187320f5d2 (diff) | |
| download | qtmaildir-729fd1465718ef1da5f61452bcb7eb23d02c9c9a.tar.gz qtmaildir-729fd1465718ef1da5f61452bcb7eb23d02c9c9a.zip | |
feat: add the contacts_dir config key
Adds Config::contactsDir(), the [general] key Task 5 will read to locate
the ContactStore. Empty means the feature is off.
The key is read WITHOUT the general/ prefix, like notmuch_config, because
QSettings' INI backend strips a section literally named [general]. Absent
or empty is silent; a set path that does not exist is reported through
addProblem(). A leading ~ is expanded by a local helper, since config.cpp
expands no other path and this is the first one to need it.
| -rw-r--r-- | src/config.cpp | 36 | ||||
| -rw-r--r-- | src/config.h | 14 | ||||
| -rw-r--r-- | tests/test_config.cpp | 100 |
3 files changed, 150 insertions, 0 deletions
diff --git a/src/config.cpp b/src/config.cpp index 184c756..3cd842b 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -94,6 +94,21 @@ bool generatorIsFlat(const QString &generator) || generator == QStringLiteral("drafts"); } +/// Expands a leading "~" to the home directory. +/// +/// No other key in config.cpp expands one; contacts_dir is the first, and only +/// because the README documents its usual value as +/// "~/.local/share/vdirsyncer/contacts/". A shared helper would serve one +/// caller, so this stays local until a second one exists. +QString expandTilde(const QString &path) +{ + if (path == QLatin1String("~")) + return QDir::homePath(); + if (path.startsWith(QLatin1String("~/"))) + return QDir::homePath() + path.mid(1); + return path; +} + } // namespace QString Account::scopedQuery(const QString &query) const @@ -236,6 +251,27 @@ void Config::load(const QString &path) m_notmuchConfig = settings.value(QStringLiteral("notmuch_config")).toString(); + // [general], so no prefix, per the note above. Tilde is expanded for the + // README's documented value, "~/.local/share/vdirsyncer/contacts/", which + // would otherwise name a literal "~" directory that does not exist. + // + // Absent or empty is silent: the feature is simply off, which is an + // ordinary machine rather than a misconfiguration. A path that is set and + // missing is reported, because there the user asked for something and is + // not getting it. The value is KEPT rather than cleared: a vdirsyncer + // target may not exist until its first run, and ContactStore's walk treats + // a missing directory as empty anyway, so the warning is the whole report. + const QString contactsDir = + settings.value(QStringLiteral("contacts_dir")).toString().trimmed(); + if (!contactsDir.isEmpty()) { + m_contactsDir = expandTilde(contactsDir); + if (!QFileInfo::exists(m_contactsDir)) { + addProblem(tr("Contacts directory '%1' does not exist; contact " + "completion will find no contacts.") + .arg(m_contactsDir)); + } + } + // Absent is fine and silent: the default is 1.0. Present but unparseable // is a problem, since the user asked for something and is not getting it. // The range is enforced by MessageView::clampZoom(), the one place that diff --git a/src/config.h b/src/config.h index 5a43d8c..c6f1cae 100644 --- a/src/config.h +++ b/src/config.h @@ -404,6 +404,19 @@ public: /// Optional alternate notmuch config file. Empty means "let notmuch decide". QString notmuchConfig() const { return m_notmuchConfig; } + /// The vdirsyncer contacts directory (vCard 3.0 files), or empty to turn + /// contact completion off. + /// + /// A [general] key, read WITHOUT the "general/" prefix like notmuch_config + /// above. There is no default: guessing a vdir would make the application + /// read a directory the user never named, and a machine with no address + /// book is an ordinary machine. A leading "~" is expanded, because the + /// README documents the usual value in that form. A set path that does not + /// exist is reported through addProblem(), because the user asked for + /// something and is not getting it; an absent key is silent, because that + /// is simply the feature being off rather than a misconfiguration. + QString contactsDir() const { return m_contactsDir; } + /// Matches every configured account's sent mail, or empty when no account /// configures one. /// @@ -578,6 +591,7 @@ private: QString m_syncStatus; int m_toolbarIconSize = 24; QString m_notmuchConfig; + QString m_contactsDir; QString m_dateFormat; QStringList m_forwardPrefixes; QString m_language; diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 70e3705..05ddbee 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -144,6 +144,11 @@ private slots: void theSignatureKeysAreRead(); void anAccountSignatureOverridesTheComposeDefault(); void aMalformedSignaturePositionIsReportedAndFallsBack(); + void contactsDirIsEmptyWithoutTheKey(); + void contactsDirIsEmptyWhenBlank(); + void contactsDirIsActuallyRead(); + void contactsDirExpandsATilde(); + void contactsDirThatDoesNotExistIsReported(); }; static QString writeIni(const QTemporaryDir &dir, const QString &body) @@ -2761,5 +2766,100 @@ void TestConfig::aMalformedSignaturePositionIsReportedAndFallsBack() "an unrecognised signature_position was accepted silently"); } +void TestConfig::contactsDirIsEmptyWithoutTheKey() +{ + // Absent is the feature off, and silently so: a machine with no address + // book is an ordinary machine, not a misconfiguration. A modal on every + // launch about a key the user never wrote would train them to dismiss + // dialogs without reading them. Only problems() is asserted, not + // warnings(): a bare [general] also carries the "no sync command" notice. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n"))); + + QVERIFY(config.contactsDir().isEmpty()); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::contactsDirIsEmptyWhenBlank() +{ + // A key written with no value is the same "off" as an absent one, not a + // problem: the user named nothing, so nothing is missing. Trimmed too, so + // a stray space cannot become a path that does not exist and warn. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "contacts_dir = \n"))); + + QVERIFY(config.contactsDir().isEmpty()); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::contactsDirIsActuallyRead() +{ + // [general] keys are read WITHOUT the general/ prefix, per the note at the + // top of Config::load(). A "general/contacts_dir" lookup matches nothing + // and would leave the accessor empty, which looks exactly like the feature + // being off: the whole key would be silently inert. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "contacts_dir=%1\n") + .arg(dir.path()))); + + QCOMPARE(config.contactsDir(), dir.path()); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::contactsDirExpandsATilde() +{ + // The README documents the usual value as + // "~/.local/share/vdirsyncer/contacts/", so a leading "~" has to reach + // QDir::homePath() or the user gets a literal "~" directory that does not + // exist. config.cpp expands no other path key; this is the first, which is + // why the expansion lives here rather than in a shared helper nothing else + // calls. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "contacts_dir=~\n"))); + + QCOMPARE(config.contactsDir(), QDir::homePath()); + QVERIFY(config.problems().isEmpty()); + + // Under a subpath, which is the form the README actually names. The + // directory deliberately does not exist, which also pins the missing-path + // decision: the value is KEPT, not cleared, and the warning is what + // reports it. A vdirsyncer target may not exist until its first run. + QTemporaryDir other; + Config nested; + const QString sub = QStringLiteral(".qtmaildir-test-no-such-contacts"); + nested.load(writeIni(other, QStringLiteral("[general]\n" + "contacts_dir=~/%1\n") + .arg(sub))); + QCOMPARE(nested.contactsDir(), + QDir::homePath() + QLatin1Char('/') + sub); +} + +void TestConfig::contactsDirThatDoesNotExistIsReported() +{ + // Present and not there is a problem, the same line message_zoom and + // forward_prefixes draw: the user asked for something and is not getting + // it. The value is kept so the accessor still says what was asked for. + QTemporaryDir dir; + const QString missing = dir.filePath(QStringLiteral("no-such-contacts")); + Config config; + config.load(writeIni(dir, QStringLiteral("[general]\n" + "contacts_dir=%1\n") + .arg(missing))); + + QVERIFY2(!config.problems().isEmpty(), + "a configured but missing contacts directory must be reported"); + const QString joined = config.problems().join(QLatin1Char('\n')); + QVERIFY2(joined.contains(missing), + "the warning must name the path the user wrote"); + QCOMPARE(config.contactsDir(), missing); +} + QTEST_MAIN(TestConfig) #include "test_config.moc" |
