aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md15
-rw-r--r--src/config.cpp36
-rw-r--r--src/config.h18
-rw-r--r--src/mainwindow.cpp8
-rw-r--r--tests/test_config.cpp67
6 files changed, 148 insertions, 3 deletions
diff --git a/README.md b/README.md
index 6a0ecd7..55b4f50 100644
--- a/README.md
+++ b/README.md
@@ -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