aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-18 15:09:09 +0200
committerDanilo M. <danix@danix.xyz>2026-09-18 15:09:09 +0200
commit729fd1465718ef1da5f61452bcb7eb23d02c9c9a (patch)
tree49df1e831e90d012685e829b8eb52c5d5fd1a3dc
parent3303c7855cb22a6bb4f36aa1a17707187320f5d2 (diff)
downloadqtmaildir-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.cpp36
-rw-r--r--src/config.h14
-rw-r--r--tests/test_config.cpp100
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"