From 441b93a75b9941d395edfe4b65ed0af2da9e0021 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 14 Aug 2026 12:42:38 +0200 Subject: feat(search): build notmuch terms for the right-click actions One place for the query grammar behind every search surface, with no widget involved so it is tested without a painter or a web engine. extend() parenthesises both sides. The query bar may hold a hand-written disjunction, and 'a or b AND c' binds as 'a or (b AND c)', which widens a search meant to narrow it and reports nothing. --- src/searchterm.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/searchterm.cpp (limited to 'src/searchterm.cpp') diff --git a/src/searchterm.cpp b/src/searchterm.cpp new file mode 100644 index 0000000..acc64c6 --- /dev/null +++ b/src/searchterm.cpp @@ -0,0 +1,93 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "searchterm.h" + +#include + +namespace SearchTerm { + +QString quote(const QString &value) +{ + // simplified() collapses every run of whitespace, newlines and tabs + // included, and trims the ends. A selection spanning paragraphs arrives + // full of newlines, which would otherwise reach the query bar verbatim. + QString cleaned = value.simplified(); + if (cleaned.isEmpty()) + return {}; + + if (cleaned.size() > kMaxValueLength) + cleaned.truncate(kMaxValueLength); + + // Backslashes first: escaping the quotes first would then escape the + // backslashes this step adds, doubling them. + cleaned.replace(QLatin1Char('\\'), QStringLiteral("\\\\")); + cleaned.replace(QLatin1Char('"'), QStringLiteral("\\\"")); + + return QLatin1Char('"') + cleaned + QLatin1Char('"'); +} + +QString field(const QString &name, const QString &value) +{ + const QString quoted = quote(value); + if (quoted.isEmpty()) + return {}; + return name + QLatin1Char(':') + quoted; +} + +QString onDate(const QDate &day) +{ + if (!day.isValid()) + return {}; + const QString text = day.toString(QStringLiteral("yyyy-MM-dd")); + return QStringLiteral("date:%1..%1").arg(text); +} + +QString tag(const QString &name) +{ + const QString trimmed = name.simplified(); + if (trimmed.isEmpty()) + return {}; + + // A tag is a token from a vocabulary the user chose, so it reads better + // unquoted in the bar they are about to edit. Quoted only when it holds + // something that would not survive. + const bool needsQuoting = + std::any_of(trimmed.cbegin(), trimmed.cend(), [](QChar ch) { + return !(ch.isLetterOrNumber() || ch == QLatin1Char('-') + || ch == QLatin1Char('_') || ch == QLatin1Char('.') + || ch == QLatin1Char('/')); + }); + + return QStringLiteral("tag:") + (needsQuoting ? quote(trimmed) : trimmed); +} + +QString extend(const QString &existing, const QString &addition) +{ + const QString left = existing.trimmed(); + const QString right = addition.trimmed(); + + if (right.isEmpty()) + return left; + if (left.isEmpty()) + return right; + + return QStringLiteral("(%1) AND (%2)").arg(left, right); +} + +} // namespace SearchTerm -- cgit v1.2.3