diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 16:31:34 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-03 16:31:34 +0200 |
| commit | 2c33529fb665cb54c31e54230fcb7b2491cf8565 (patch) | |
| tree | 87410a035212f0e8e5997722662803c17a3320d8 | |
| parent | 212048782be680e2f99341db6c6460e59c708e7c (diff) | |
| download | qtmaildir-2c33529fb665cb54c31e54230fcb7b2491cf8565.tar.gz qtmaildir-2c33529fb665cb54c31e54230fcb7b2491cf8565.zip | |
feat: choose the startup query by namefeature/ui-state-persistence
The app opened whichever saved query sorted first alphabetically, which
is not a choice anyone made: [queries] is read through childKeys(), so
savedQueries().first() means "Flagged" before "Inbox" before "Unread"
rather than anything the user expressed.
[general] startup_query names the entry to open and defaults to Unread,
so a fresh install comes up on the unified unread list. Saved-query
button order is untouched and stays alphabetical.
A name matching no saved query falls back to the first one rather than
starting with an empty view. That is reported as a problem only when the
user actually wrote the name; the built-in default naming a query they
never created is not something they got wrong, and warning about it
would fire on every launch of a config that has no Unread entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 15 | ||||
| -rw-r--r-- | src/config.cpp | 36 | ||||
| -rw-r--r-- | src/config.h | 18 | ||||
| -rw-r--r-- | src/mainwindow.cpp | 8 | ||||
| -rw-r--r-- | tests/test_config.cpp | 67 |
6 files changed, 148 insertions, 3 deletions
@@ -83,6 +83,10 @@ identity. ; point: once you zoom with Ctrl+wheel or Ctrl+/Ctrl-, that is remembered ; separately and this value no longer applies. ; message_zoom = 1.0 +; Optional. Which [queries] entry to open at startup, by name. Defaults to +; Unread. Falls back to the first saved query if no query by this name +; exists, and warns if you named one explicitly. +; startup_query = Unread [sync] ; Optional. Omit and the Sync button disables itself with a tooltip. @@ -125,7 +129,8 @@ k = prev_thread Saved-query buttons appear in alphabetical order rather than file order: QSettings returns keys sorted, and preserving file order would mean -hand-rolling an INI parser. +hand-rolling an INI parser. Which query opens at startup is therefore a +separate setting, `[general] startup_query`, rather than "the first one". ## Tags diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 11863a8..02bf7c4 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -392,6 +392,21 @@ stays blocked and per-render. Do not build a full account sidebar for this. Persisting the selection may resolve the complaint entirely, and it is a fraction of the work. Reassess after. +### Partly done + +**The startup query is now chosen by name**, not by sort order. `[queries]` is +read through `childKeys()`, which sorts alphabetically, so the old +`savedQueries().first()` opened whichever entry happened to sort first, which +is why the app came up on Inbox. `[general] startup_query` names the entry, +defaults to `Unread`, and falls back to the first saved query when the name +matches nothing. Only a name the user wrote is worth a warning: the built-in +default naming a query they never created is not something they got wrong. + +Neither half of item 10 proper is done: the account selection still resets on +restart, and reaching an account's inbox is still two steps. Persisting the +selection remains the next cheap step, and the reassessment the item calls for +should happen after that rather than now. + ## 11. Icon, `.desktop` file, SlackBuild Packaging, independent of everything above, and can proceed in parallel. diff --git a/src/config.cpp b/src/config.cpp index a2caaee..e92c7f3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -66,6 +66,14 @@ void Config::load(const QString &path) // is a problem, since the user asked for something and is not getting it. // The range check lives in MessageView::clampZoom(), the one place that // knows what the web view can render. + // Empty is treated as unset rather than as "a query named nothing". + const QString startup = + settings.value(QStringLiteral("startup_query")).toString().trimmed(); + if (!startup.isEmpty()) { + m_startupQuery = startup; + m_startupQueryWasSet = true; + } + const QVariant zoom = settings.value(QStringLiteral("message_zoom")); if (zoom.isValid()) { bool ok = false; @@ -149,6 +157,34 @@ void Config::load(const QString &path) for (const QString &name : settings.childKeys()) m_savedQueries.append({ name, settings.value(name).toString() }); settings.endGroup(); + + // Checked here rather than where startup_query is read: [queries] is not + // parsed until now. Only a name the user actually wrote is worth a + // problem; the built-in default naming a query they never created is not + // something they got wrong. + if (m_startupQueryWasSet && !m_savedQueries.isEmpty() + && startupSavedQuery().name.compare(m_startupQuery, + Qt::CaseInsensitive) != 0) { + addProblem(QStringLiteral("Startup query '%1' is not a saved query; " + "opening '%2' instead.") + .arg(m_startupQuery, startupSavedQuery().name)); + } +} + +SavedQuery Config::startupSavedQuery() const +{ + if (m_savedQueries.isEmpty()) + return {}; + + for (const SavedQuery &query : m_savedQueries) { + if (query.name.compare(m_startupQuery, Qt::CaseInsensitive) == 0) + return query; + } + + // Named a query that does not exist. Not worth a warning: the default is + // a name the user never wrote, so an install with no [queries] Unread + // entry would warn on every launch about a key it never set. + return m_savedQueries.first(); } Account Config::account(const QString &key) const diff --git a/src/config.h b/src/config.h index 3e94249..3b7fc48 100644 --- a/src/config.h +++ b/src/config.h @@ -78,6 +78,18 @@ public: /// Optional alternate notmuch config file. Empty means "let notmuch decide". QString notmuchConfig() const { return m_notmuchConfig; } + /// The saved query to open at startup, by name. Falls back to "Unread" + /// when unset, and to the first saved query when no query by that name + /// exists: [queries] is read through childKeys(), which sorts + /// alphabetically, so "first" would otherwise mean whatever happens to + /// sort first rather than anything the user chose. + QString startupQuery() const { return m_startupQuery; } + + /// The saved query startupQuery() names, or the first one when it names + /// nothing that exists. A default-constructed SavedQuery when there are + /// none at all. + SavedQuery startupSavedQuery() const; + /// Starting message-pane zoom for a profile with no saved UI state. Once /// the user zooms, the state file remembers that instead, so this is only /// ever the default. Clamped by MessageView::clampZoom() on use. @@ -107,6 +119,12 @@ private: QString m_syncCommand; QString m_notmuchConfig; qreal m_messageZoom = 1.0; + QString m_startupQuery = QStringLiteral("Unread"); + + /// Whether startup_query came from the config rather than being the + /// built-in default. Only a name the user wrote is worth reporting when + /// it matches no saved query. + bool m_startupQueryWasSet = false; QStringList m_warnings; QStringList m_problems; }; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c4955b6..2a79988 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -171,8 +171,12 @@ MainWindow::MainWindow(const Config &config, QWidget *parent) // modifier shortcuts such as Ctrl+Q still work there, which the old // filter blocked. - if (!m_config.savedQueries().isEmpty()) { - m_queryEdit->setText(m_config.savedQueries().first().query); + // Not savedQueries().first(): [queries] is read through childKeys(), which + // sorts alphabetically, so "first" means whatever happens to sort first + // rather than anything the user chose. Config resolves the name. + const SavedQuery startup = m_config.startupSavedQuery(); + if (!startup.query.isEmpty()) { + m_queryEdit->setText(startup.query); runCurrentQuery(); } } diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 367643c..24f6afb 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -34,6 +34,9 @@ private slots: void brokenSyncCommandIsAProblem(); void malformedAccountIsAProblem(); void validConfigHasNoProblems(); + void startupQueryDefaultsToUnread(); + void startupQueryHonoursTheConfiguredName(); + void unknownStartupQueryFallsBackAndReports(); void generalSectionKeysAreActuallyRead(); void messageZoomDefaultsAndValidates(); }; @@ -224,6 +227,70 @@ void TestConfig::validConfigHasNoProblems() QVERIFY(config.warnings().isEmpty()); } +void TestConfig::startupQueryDefaultsToUnread() +{ + // [queries] is read through childKeys(), which sorts alphabetically, so + // savedQueries().first() is "Flagged" here. The startup query must be + // chosen by name, not by sort order. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[queries]\n" + "Inbox=tag:inbox\n" + "Unread=tag:unread\n" + "Flagged=tag:flagged\n"))); + + QCOMPARE(config.savedQueries().first().name, QStringLiteral("Flagged")); + QCOMPARE(config.startupSavedQuery().name, QStringLiteral("Unread")); + QCOMPARE(config.startupSavedQuery().query, QStringLiteral("tag:unread")); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::startupQueryHonoursTheConfiguredName() +{ + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=Flagged\n" + "\n" + "[queries]\n" + "Inbox=tag:inbox\n" + "Unread=tag:unread\n" + "Flagged=tag:flagged\n"))); + + QCOMPARE(config.startupSavedQuery().name, QStringLiteral("Flagged")); + QVERIFY(config.problems().isEmpty()); +} + +void TestConfig::unknownStartupQueryFallsBackAndReports() +{ + // A name the user wrote that matches nothing is a problem: they asked for + // something and are not getting it. Startup still works, on the fallback. + QTemporaryDir dir; + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=Nonexistent\n" + "\n" + "[queries]\n" + "Inbox=tag:inbox\n"))); + + QCOMPARE(config.startupSavedQuery().name, QStringLiteral("Inbox")); + QCOMPARE(config.problems().size(), 1); + + // The built-in default naming a query the user never created is NOT a + // problem: they did not get it wrong, they simply have no Unread entry. + QTemporaryDir quiet; + Config silent; + silent.load(writeIni(quiet, QStringLiteral( + "[queries]\n" + "Inbox=tag:inbox\n"))); + + QCOMPARE(silent.startupSavedQuery().name, QStringLiteral("Inbox")); + QVERIFY(silent.problems().isEmpty()); +} + void TestConfig::generalSectionKeysAreActuallyRead() { // QSettings' INI backend treats a section literally named [general] as its |
