aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 18:02:03 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 18:02:03 +0200
commit04e7db97ae421212614a6ed92807485f743966d3 (patch)
tree3a6cf6c1977639c0f3bfdefaa5ce7d209aead0ca
parentc4a38a5871250b9c952f3eb633ab4cf2ed3d717a (diff)
downloadqtmaildir-04e7db97ae421212614a6ed92807485f743966d3.tar.gz
qtmaildir-04e7db97ae421212614a6ed92807485f743966d3.zip
fix: show the account's startup view for a lone --account
The account dropdown deliberately does not re-run the query: by hand the user picks a filter next. A launch from another program has no next click, so `--account work` on its own moved the dropdown and changed nothing the user could see. When the account selector applies and no thread or message was given, the startup view now runs again in the new account. The constructor's startup path moves into runStartupView() and both callers use it, so the view is resolved the same way in each: a generated filter is asked for the account's own query rather than having its all-accounts query wrapped in the account's path. The test starts on the trash view because its per-account query is the account's own trash path; for a tag filter the generated and the wrapped queries are the same string, and a test on one passed against the wrap. It asserts on the generated string and on the rows, and fails with the wrap put back. anEmptySelectorSetChangesNothing also asserts that no query ran, which the bar and the dropdown alone cannot show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--src/mainwindow.cpp27
-rw-r--r--src/mainwindow.h7
-rw-r--r--tests/test_mainwindow.cpp88
3 files changed, 121 insertions, 1 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index c44e36d..3ae16e9 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -654,13 +654,24 @@ MainWindow::MainWindow(const Config &config, QWidget *parent)
m_accountBox->setCurrentIndex(index);
}
+ runStartupView();
+}
+
+void MainWindow::runStartupView()
+{
+ // The dropdown's account, which the constructor has just set from
+ // startup_account and applySelectors() from --account. Read rather than
+ // passed, because the dropdown is what runQuery() scopes with below, and
+ // two sources for one scope is how they come to disagree.
+ const QString accountKey = m_accountBox->currentData().toString();
+
// resolvedQuery(), not startup.query: a generated entry stores no query at
// all, since its text is composed from the accounts at run time. Reading
// the field directly meant a startup_query naming a built-in filter opened
// an empty bar and ran nothing.
const SavedQuery startup = m_config.startupSavedQuery();
const QString startupQuery =
- m_config.resolvedQuery(startup, startupAccount);
+ m_config.resolvedQuery(startup, accountKey);
if (!startupQuery.isEmpty()) {
m_queryEdit->setText(startupQuery);
@@ -5267,6 +5278,20 @@ void MainWindow::applySelectors(const LaunchSelectors &requested)
showTransientStatus(
tr("No account named '%1'.").arg(selectors.account));
}
+
+ // The account on its own is a request for that account's VIEW, and
+ // moving the dropdown is not one: by design the dropdown only rescopes
+ // the filter buttons and leaves the list alone until the user clicks
+ // one, and a launch from another program has no next click. So the
+ // startup view runs again, resolved for the new account.
+ //
+ // Not when a thread or message was given, whose own query is about to
+ // replace the list; running this too would race it.
+ if (index >= 0 && selectors.threadId.isEmpty()
+ && selectors.messageId.isEmpty()) {
+ runStartupView();
+ return;
+ }
}
// A message id names a message INSIDE a conversation, so it has to be
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 8167e74..d4b0975 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -582,6 +582,13 @@ private:
void runQuery(FlatResult flat,
AccountScope scope = AccountScope::Apply);
+ /// Runs the configured startup view in the dropdown's CURRENT account.
+ ///
+ /// The constructor's startup path and a lone --account selector share it,
+ /// so both resolve the view the same way: a generated filter is asked for
+ /// that account's own query, never has its all-accounts query wrapped.
+ void runStartupView();
+
/// Builds the row of saved-query buttons, the overflow menu and Sent.
///
/// Its own row since item 23: an unbounded list of buttons sharing the
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index b9bd890..e89ad69 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -189,6 +189,8 @@ public:
// went unnoticed as broken once already.
out << "[general]\n"
<< "notmuch_config=" << m_fixture.configPath() << "\n";
+ if (!m_startupQuery.isEmpty())
+ out << "startup_query=" << m_startupQuery << "\n";
if (!accountKey.isEmpty()) {
// QSettings reads `/` in a section name as a group separator,
// so the section is [account.key], never [account/key].
@@ -239,6 +241,12 @@ public:
return true;
}
+ /// Writes `startup_query` into [general]. Call before build(). For a test
+ /// that needs a startup view whose per-account query is not simply the
+ /// all-accounts one wrapped in the account's path, which is what tells a
+ /// generated scope apart from a wrapped one.
+ void setStartupQuery(const QString &name) { m_startupQuery = name; }
+
NotmuchFixture &fixture() { return m_fixture; }
const Config &config() const { return m_config; }
QString error() const { return m_error; }
@@ -250,6 +258,7 @@ private:
QString m_error;
QList<AccountSpec> m_accounts;
QString m_composeKey;
+ QString m_startupQuery;
};
/// MainWindow is mostly wiring. Cases that need a real database opt into one
@@ -320,6 +329,7 @@ private slots:
void aThreadSelectorThatIsNotHexIsRefused();
void anEmptySelectorSetChangesNothing();
void aBracketedMessageSelectorOpensThatMessage();
+ void anAccountSelectorAloneShowsThatAccountsStartupView();
void everyBuiltinFilterButtonCarriesAnIconAndItsText();
void theDraftsButtonIsAbsentWithoutADraftsFolder();
void aQueryInTheMenuCanActuallyBeRun();
@@ -18060,11 +18070,16 @@ void TestMainWindow::anEmptySelectorSetChangesNothing()
window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
QVERIFY(queryEdit);
queryEdit->setText(QStringLiteral("tag:flagged"));
+ const quint64 generation = window.currentGenerationForTesting();
window.applySelectors(LaunchSelectors());
QCOMPARE(window.selectedAccountForTesting(), QStringLiteral("work"));
QCOMPARE(queryEdit->text(), QStringLiteral("tag:flagged"));
+ // The bar and the dropdown can both survive a query that re-ran them, so
+ // those two alone pass against a raise that navigates. Every query bumps
+ // the generation, whatever it runs.
+ QCOMPARE(window.currentGenerationForTesting(), generation);
}
void TestMainWindow::aBracketedMessageSelectorOpensThatMessage()
@@ -18113,4 +18128,77 @@ void TestMainWindow::aBracketedMessageSelectorOpensThatMessage()
15000);
}
+/// Two accounts, one inbox message each, for the scope cases below. The two
+/// accounts hold DIFFERENT threads, so a query scoped to the wrong one shows a
+/// different row rather than accidentally the right one.
+static bool buildTwoAccounts(WorkerBackedWindow &backed,
+ const QString &folder = QStringLiteral("inbox"))
+{
+ // Friday and Saturday, verified with `date -d 2026-08-14 +%A` and
+ // `date -d 2026-08-15 +%A`: Qt::RFC2822Date validates the weekday.
+ if (!backed.fixture().addMessage(
+ QStringLiteral("work/") + folder, QStringLiteral("w1@example.org"),
+ QStringLiteral("Work subject"), QStringLiteral("a@example.org"),
+ QStringLiteral("Fri, 14 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Work body.")))
+ return false;
+ if (!backed.fixture().addMessage(
+ QStringLiteral("personal/") + folder, QStringLiteral("p1@example.org"),
+ QStringLiteral("Personal subject"), QStringLiteral("b@example.org"),
+ QStringLiteral("Sat, 15 Aug 2026 10:00:00 +0200"),
+ QStringLiteral("Personal body.")))
+ return false;
+
+ WorkerBackedWindow::AccountSpec work;
+ work.key = QStringLiteral("work");
+ work.maildir = QStringLiteral("work");
+ work.trash = QStringLiteral("Trash");
+ WorkerBackedWindow::AccountSpec personal;
+ personal.key = QStringLiteral("personal");
+ personal.maildir = QStringLiteral("personal");
+ personal.trash = QStringLiteral("Trash");
+ return backed.buildWithAccounts({ work, personal });
+}
+
+void TestMainWindow::anAccountSelectorAloneShowsThatAccountsStartupView()
+{
+ // --account on its own moved the dropdown and nothing else, because the
+ // dropdown deliberately does not re-run the query: by hand the user picks
+ // a filter next. From another program there is no next click, so the
+ // launch changed nothing the user could see.
+ //
+ // The startup view is the TRASH, because its per-account query is the
+ // account's own trash path. Most filters are a tag, and for a tag the
+ // generated query and the wrapped one are the same string, so a test on
+ // one of those passes against the wrap it exists to rule out.
+ WorkerBackedWindow backed;
+ backed.setStartupQuery(QStringLiteral("trash"));
+ QVERIFY2(buildTwoAccounts(backed, QStringLiteral("Trash")),
+ qPrintable(backed.error()));
+
+ MainWindow window(backed.config());
+ auto *model = window.findChild<ThreadListModel *>();
+ QVERIFY(model);
+
+ // The startup view over All accounts shows both, so the assertion below
+ // measures a change rather than a view that was already one row.
+ QTRY_COMPARE_WITH_TIMEOUT(model->rowCount(QModelIndex()), 2, 15000);
+
+ LaunchSelectors selectors;
+ selectors.account = QStringLiteral("work");
+ window.applySelectors(selectors);
+
+ // The GENERATED string for the work account, never the all-accounts query
+ // wrapped in the work scope: that wrap returns the right rows too, which
+ // is why a row count alone cannot tell the two apart.
+ const Config &config = backed.config();
+ const QString expected =
+ config.resolvedQuery(config.startupSavedQuery(), QStringLiteral("work"));
+ QVERIFY(!expected.isEmpty());
+ QCOMPARE(window.lastRunQueryForTesting(), expected);
+
+ QTRY_COMPARE_WITH_TIMEOUT(model->rowCount(QModelIndex()), 1, 15000);
+ QCOMPARE(model->threadAt(0).subject, QStringLiteral("Work subject"));
+}
+
#include "test_mainwindow.moc"