summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp93
-rw-r--r--src/mainwindow.h33
-rw-r--r--src/notmuchworker.cpp42
-rw-r--r--src/notmuchworker.h17
-rw-r--r--src/types.h14
5 files changed, 199 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 3f8a53d..0ce94a8 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -288,6 +288,7 @@ MainWindow::MainWindow(const Config &config, QWidget *parent)
qRegisterMetaType<ThreadSummary>();
qRegisterMetaType<MessageRef>();
qRegisterMetaType<TagChange>();
+ qRegisterMetaType<DatabaseStats>();
qRegisterMetaType<QVector<ThreadSummary>>();
qRegisterMetaType<QVector<MessageRef>>();
@@ -885,6 +886,13 @@ void MainWindow::buildMenus()
auto *shortcuts = helpMenu->addAction(tr("&Keyboard shortcuts"));
connect(shortcuts, &QAction::triggered,
this, &MainWindow::showShortcutReference);
+ // A dialog the user asks for, per item 34: counting every message is not
+ // free on a large database, so this must not be anything that refreshes on
+ // its own.
+ auto *maildirInfo = helpMenu->addAction(tr("&Maildir overview"));
+ maildirInfo->setObjectName(QStringLiteral("maildirOverview"));
+ connect(maildirInfo, &QAction::triggered,
+ this, &MainWindow::showMaildirOverview);
auto *about = helpMenu->addAction(tr("&About"));
connect(about, &QAction::triggered, this, &MainWindow::showAbout);
@@ -1031,6 +1039,89 @@ void MainWindow::showShortcutReference()
dialog.exec();
}
+void MainWindow::showMaildirOverview()
+{
+ auto *dialog = new QDialog(this);
+ dialog->setWindowTitle(tr("Maildir overview"));
+ dialog->setObjectName(QStringLiteral("maildirOverviewDialog"));
+ // Deleted on close, which is what makes m_overviewCounts a QPointer: the
+ // worker's reply can arrive after the user has dismissed it.
+ dialog->setAttribute(Qt::WA_DeleteOnClose);
+
+ auto *counts = new QLabel(dialog);
+ counts->setObjectName(QStringLiteral("maildirCounts"));
+ counts->setTextFormat(Qt::RichText);
+ // Shown as pending rather than as zero. The dialog opens before the answer
+ // arrives, and a zero would read as "no mail", which is a claim rather than
+ // an absence of one.
+ counts->setText(tr("<b>Counting...</b>"));
+ m_overviewCounts = counts;
+
+ // From config, never from notmuch, which does not model accounts at all.
+ // That is the whole reason per-account subdirectories are configured.
+ QString accountText;
+ const QList<Account> accounts = m_config.accounts();
+ accountText += tr("<b>%n account(s)</b>", "", int(accounts.size()));
+ if (!accounts.isEmpty()) {
+ accountText += QStringLiteral("<ul>");
+ for (const Account &account : accounts) {
+ // Account names are user-written config, and this label is rich
+ // text, so they are escaped like any other untrusted value.
+ const QString label = account.label.isEmpty() ? account.key
+ : account.label;
+ accountText += QStringLiteral("<li>%1</li>")
+ .arg(label.toHtmlEscaped());
+ }
+ accountText += QStringLiteral("</ul>");
+ }
+
+ auto *accountLabel = new QLabel(accountText, dialog);
+ accountLabel->setObjectName(QStringLiteral("maildirAccounts"));
+ accountLabel->setTextFormat(Qt::RichText);
+
+ auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, dialog);
+ connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
+
+ auto *layout = new QVBoxLayout(dialog);
+ layout->addWidget(counts);
+ layout->addWidget(accountLabel);
+ layout->addStretch();
+ layout->addWidget(buttons);
+
+ // Asked for when the dialog opens and never on a timer: counting every
+ // message in a large database is not free, which is the constraint that
+ // made this a dialog rather than a status-bar field.
+ QMetaObject::invokeMethod(m_worker, "requestDatabaseStats",
+ Qt::QueuedConnection,
+ Q_ARG(quint64, ++m_statsGeneration));
+
+ dialog->show();
+}
+
+void MainWindow::onDatabaseStatsReady(const DatabaseStats &stats,
+ quint64 generation)
+{
+ // Closed and reopened while the count ran: this answer belongs to the old
+ // dialog. The QPointer covers "closed", this covers "closed and reopened".
+ if (generation != m_statsGeneration)
+ return;
+
+ if (!m_overviewCounts)
+ return;
+
+ // A field notmuch could not answer stays unknown. Printing 0 would say the
+ // database is empty, which is the opposite of "we could not tell".
+ const auto number = [](int value) {
+ return value < 0 ? tr("unknown") : QLocale().toString(value);
+ };
+
+ m_overviewCounts->setText(
+ tr("<b>%1</b> messages in <b>%2</b> threads<br>"
+ "<b>%3</b> tags")
+ .arg(number(stats.messages), number(stats.threads),
+ number(stats.tags)));
+}
+
void MainWindow::showAbout()
{
QDialog dialog(this);
@@ -1098,6 +1189,8 @@ void MainWindow::wireWorker()
this, &MainWindow::onAllTagsReady);
connect(m_worker, &NotmuchWorker::countsReady,
this, &MainWindow::onCountsReady);
+ connect(m_worker, &NotmuchWorker::databaseStatsReady,
+ this, &MainWindow::onDatabaseStatsReady);
// A confirmed write clears the pending revert: without this, a later
// unrelated error would roll back a change that actually succeeded.
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 8043727..579597c 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -20,6 +20,7 @@
#include <QHash>
#include <QMainWindow>
+#include <QPointer>
#include <QThread>
#include <QUndoCommand>
#include <QUndoStack>
@@ -133,6 +134,13 @@ public:
/// stale, so a test standing in for the worker has to know the current one.
quint64 currentGenerationForTesting() const { return m_generation; }
+ /// The generation a database-stats reply must carry to be accepted.
+ ///
+ /// A test seam, for the same reason as the one above: onDatabaseStatsReady
+ /// discards a reply belonging to a dialog that has since been closed and
+ /// reopened, so a test standing in for the worker needs the current value.
+ quint64 statsGenerationForTesting() const { return m_statsGeneration; }
+
protected:
void closeEvent(QCloseEvent *event) override;
@@ -189,6 +197,10 @@ private slots:
/// requestPlaceholderCounts() asked for them.
void onCountsReady(const QVector<int> &counts, quint64 generation);
+ /// Fills in the overview dialog's counts when the worker answers. Does
+ /// nothing if the dialog has since been closed.
+ void onDatabaseStatsReady(const DatabaseStats &stats, quint64 generation);
+
/// Runs a query the user clicked on the placeholder pane.
void onPlaceholderQueryRequested(const QString &query);
@@ -225,6 +237,16 @@ private:
void showShortcutReference();
void showAbout();
+ /// The Maildir overview (item 34): what notmuch knows about the database,
+ /// plus the account list, which comes from config since notmuch does not
+ /// model accounts at all.
+ ///
+ /// Opens immediately showing the counts as pending and fills them in when
+ /// the worker answers, rather than blocking: counting every message is not
+ /// free on a large database and a dialog that hangs first is worse than one
+ /// that populates.
+ void showMaildirOverview();
+
/// Creates a QAction, binds it to the sequence KeyMap holds for `name`,
/// and registers it. `name` is the action name used in [keys].
QAction *addAction(const QString &name, const QString &text,
@@ -422,6 +444,17 @@ private:
/// attention, so it must survive until the next successful run.
bool m_lastSyncFailed = false;
+ /// The overview dialog's counts label while that dialog is open, null
+ /// otherwise. A QPointer because the dialog is deleted on close and the
+ /// worker's reply can arrive afterwards: a raw pointer would dangle for
+ /// exactly as long as the count takes on a large database, which is
+ /// precisely when the user is most likely to close it first.
+ QPointer<QLabel> m_overviewCounts;
+
+ /// Discriminates a stats reply from a dialog that has since been closed
+ /// and reopened, so an old answer cannot fill in a newer dialog.
+ quint64 m_statsGeneration = 0;
+
/// Holds the sync log and its close button, so the pane can be dismissed.
QWidget *m_syncLogPane = nullptr;
QPlainTextEdit *m_syncLog = nullptr;
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp
index 973d110..752a52c 100644
--- a/src/notmuchworker.cpp
+++ b/src/notmuchworker.cpp
@@ -363,6 +363,48 @@ void NotmuchWorker::requestAllTags(quint64 generation)
emit allTagsReady(result, generation);
}
+void NotmuchWorker::requestDatabaseStats(quint64 generation)
+{
+ if (!openReadOnly())
+ return;
+
+ DatabaseStats stats;
+
+ // "*" is notmuch's match-everything query. Counting messages and threads
+ // needs two calls on it: the numbers differ by the reply depth of the
+ // database and there is no single call that yields both.
+ NmQuery all(notmuch_query_create(m_db, "*"));
+ if (all) {
+ unsigned int messages = 0;
+ if (notmuch_query_count_messages(all.get(), &messages)
+ == NOTMUCH_STATUS_SUCCESS) {
+ stats.messages = static_cast<int>(messages);
+ }
+ }
+
+ // A second query object rather than reusing the one above: notmuch caches
+ // results on a query, and counting both ways from one has bitten people.
+ NmQuery allThreads(notmuch_query_create(m_db, "*"));
+ if (allThreads) {
+ unsigned int threads = 0;
+ if (notmuch_query_count_threads(allThreads.get(), &threads)
+ == NOTMUCH_STATUS_SUCCESS) {
+ stats.threads = static_cast<int>(threads);
+ }
+ }
+
+ // Already enumerated for the completer, so this costs nothing extra.
+ NmTags tags(notmuch_database_get_all_tags(m_db));
+ if (tags) {
+ int count = 0;
+ for (; notmuch_tags_valid(tags.get()); notmuch_tags_move_to_next(tags.get()))
+ ++count;
+ stats.tags = count;
+ }
+
+ emit databaseStatsReady(stats, generation);
+}
+
void NotmuchWorker::requestCounts(const QStringList &queries, quint64 generation)
{
if (!openReadOnly())
diff --git a/src/notmuchworker.h b/src/notmuchworker.h
index 05c1d3a..7d6a587 100644
--- a/src/notmuchworker.h
+++ b/src/notmuchworker.h
@@ -83,6 +83,19 @@ public slots:
/// refreshing one nobody is looking at is work for nothing.
void requestCounts(const QStringList &queries, quint64 generation);
+ /// Database-level facts for the Maildir overview (item 34): total messages,
+ /// total threads, and the number of tags.
+ ///
+ /// **Messages, not threads**, which is what distinguishes this from
+ /// requestCounts above. That one answers "how many rows will this query
+ /// produce" and counts threads to match the list; this one describes the
+ /// database, where the message total is the number a user means by "how
+ /// much mail is in here".
+ ///
+ /// Counting every message is not free on a large database, so this is
+ /// called when the dialog is opened and never on a timer.
+ void requestDatabaseStats(quint64 generation);
+
signals:
void threadsReady(const QVector<ThreadSummary> &threads, quint64 generation);
void queryFinished(int totalThreads, quint64 generation);
@@ -95,6 +108,10 @@ signals:
/// positional correspondence the caller relies on always holds.
void countsReady(const QVector<int> &counts, quint64 generation);
+ /// Fields left at -1 are ones notmuch could not answer, which the dialog
+ /// renders as unknown rather than as zero.
+ void databaseStatsReady(const DatabaseStats &stats, quint64 generation);
+
void errorOccurred(const QString &message);
private:
diff --git a/src/types.h b/src/types.h
index 59d1d06..999d3f3 100644
--- a/src/types.h
+++ b/src/types.h
@@ -78,6 +78,20 @@ struct TagChange
}
};
+/// Database-level facts for the Maildir overview.
+///
+/// Every field is -1 until answered, so a dialog opened against a database that
+/// cannot be read shows "unknown" rather than a confident zero. A zero is a
+/// claim, and "no mail at all" is exactly the wrong thing to tell someone whose
+/// index failed to open.
+struct DatabaseStats
+{
+ int messages = -1; ///< Every message notmuch has indexed.
+ int threads = -1; ///< Every thread. Differs from messages by reply depth.
+ int tags = -1; ///< Distinct tag names in the database.
+};
+
Q_DECLARE_METATYPE(ThreadSummary)
Q_DECLARE_METATYPE(MessageRef)
Q_DECLARE_METATYPE(TagChange)
+Q_DECLARE_METATYPE(DatabaseStats)