1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
/*
* qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
* Copyright (C) 2026 Danilo M. <danix@danix.xyz>
*
* 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.
*/
#pragma once
#include <QDate>
#include <QList>
#include <QString>
/// Builds the notmuch query strings behind the right-click search actions.
///
/// Free functions and no widget, so the whole query grammar is testable
/// without a painter, a web engine or a window. Every surface that offers a
/// search goes through here, which is what stops five surfaces growing five
/// slightly different quoting rules.
///
/// **notmuch rejects almost nothing.** `from:((((` parses cleanly and matches
/// zero, so a malformed query produces an empty result rather than an error
/// the user could act on. Correctness here cannot be checked by asking notmuch;
/// it is checked against the constructed string.
namespace SearchTerm {
/// Longest quoted value. A selection longer than this is a mis-drag rather
/// than a search, and the query bar is an editable line the user has to be
/// able to read.
inline constexpr int kMaxValueLength = 200;
/// Which of the three search operations a menu entry asked for.
///
/// Replaces the `bool extend` that carried two operations and had no room for
/// a third. A plain enum class, deliberately NOT registered as a metatype:
/// every connection carrying it is direct, within the UI thread. The `Q_ENUM`
/// trap recorded in CLAUDE.md belongs to the queued signals into
/// NotmuchWorker and does not apply here.
enum class SearchMode {
/// Replace the query bar outright.
Replace,
/// Narrow what is already there, via extend().
Narrow,
/// Narrow by everything that is not this value, via exclude().
Exclude,
};
/// Quotes an arbitrary value for use as a notmuch term.
///
/// Whitespace and newlines collapse to single spaces, embedded quotes are
/// doubled (notmuch has no backslash escape), the value is capped at
/// kMaxValueLength, and an empty or whitespace-only value yields an EMPTY
/// STRING rather than `""`. Callers
/// test for empty to decide whether to offer a menu entry at all.
QString quote(const QString &value);
/// `field:"value"`, or empty when the value is empty.
///
/// The field name is a notmuch keyword and is never translated: it is wire
/// format, not user-facing text.
QString field(const QString &name, const QString &value);
/// `date:YYYY-MM-DD..YYYY-MM-DD` for a single day, empty for an invalid date.
///
/// The day twice rather than the day and its successor: notmuch's range is
/// inclusive at both ends, so the naive `..next-day` form silently includes a
/// second day of mail.
QString onDate(const QDate &day);
/// `tag:name`, quoted only when the name needs it.
QString tag(const QString &name);
/// Narrows `existing` by `addition`, as `(existing) AND (addition)`.
///
/// **Both sides are parenthesised and that is load-bearing.** The query bar
/// may hold a hand-written disjunction, and `a or b AND c` binds as
/// `a or (b AND c)`: the result WIDENS a search the user asked to narrow, and
/// nothing reports an error. The same trap is why the post-new hook
/// parenthesises a rule's query before scoping it with `tag:new`.
///
/// An empty `existing` yields `addition` alone rather than `() AND (x)`, which
/// matches nothing; an empty `addition` leaves `existing` untouched.
QString extend(const QString &existing, const QString &addition);
/// Narrows `existing` by everything that is NOT `addition`, as
/// `(existing) AND NOT (addition)`.
///
/// **Both sides are parenthesised, for the same load-bearing reason as
/// extend().** The query bar may hold a hand-written disjunction, and
/// `a or b AND NOT c` binds as `a or (b AND NOT c)`: the exclusion would cover
/// only the second term, leaving on screen exactly the mail the user asked to
/// be rid of, with no error reported anywhere.
///
/// **An empty `existing` yields an EMPTY STRING, unlike extend().** Excluding
/// from nothing would mean the entire Maildir minus one value: a legitimate
/// query, and an implausible thing to have meant by right-clicking a value in
/// a fresh window. The menus grey the entry out when the query bar is empty;
/// this is the second layer, against a caller that forgets the guard.
///
/// An empty `addition` leaves `existing` untouched.
QString exclude(const QString &existing, const QString &addition);
/// The messages among `query` that `+add -remove` would actually change:
/// `(query) AND (not tag:a1 or ... or tag:r1 ...)`.
///
/// Used to count and to write a rule applied to existing mail (item 212), so
/// the number in the confirmation is the number of messages touched, not the
/// number matched. Built on extend(), so a disjunction in the rule cannot
/// escape the scope.
///
/// **Never empty.** An empty query or an empty change yields
/// Config::matchNothingQuery(), because notmuch reads an empty query as
/// "match everything" and this string is written to.
QString changes(const QString &query, const QStringList &add,
const QStringList &remove);
} // namespace SearchTerm
/// One entry a context menu can offer: a finished query and the text naming it.
///
/// Carried rather than rebuilt at menu-construction time, so the value a menu
/// entry searches for is the value the pane extracted, with no second parse of
/// anything already rendered.
struct SearchOffer
{
/// Shown in the menu. Already translated, and elides a long value: the
/// query keeps the full one.
QString label;
/// The finished notmuch query. Never empty in a constructed offer.
QString query;
};
|