aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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"