| Age | Commit message (Collapse) | Author | Files | Lines |
|
Four items from the user's own notes that this document did not carry.
Item 5 gains two concrete sub-items: a star column for flagged threads,
mirroring the paperclip, and the observation that everything looks
unread. The second is recorded with a warning rather than as a defect,
because bold is already conditional on isUnread() in the model; either
the list really is mostly unread or bold leaks through some other path,
and that has to be reproduced before the font logic is touched.
Item 21, better default shortcuts, records why the defaults moved once
already: a bare letter cannot be a menu accelerator, and a bare capital
parses to a key no keystroke emits. Both still constrain a second pass,
as does the rule against testing reachability with synthetic input.
Item 22 is the translatability audit CLAUDE.md has owed since the tr()
rule was written, plus the loading machinery, which does not exist at
all: no QTranslator, no .ts files, no CMake rule. Those are separate
sizes and the item says so.
Item 23, saving a query from the UI, carries the one real design question
with it: whether the write lands in the hand-edited config or the state
file. Saved queries are user intent rather than machine state, which
argues against the split item 1 established, so the decision is flagged
rather than assumed. It also notes that item 23 may answer postponed item
10 as a side effect, since account-scoped saved queries were exactly what
item 10 proposed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Message details and automatic mark-read are both new features, so this
takes the minor slot.
The SlackBuild's MD5SUM stays at the previous value until the 0.6.0
snapshot tarball exists, which is after the tag is pushed. Update it with
sbodl before building the package.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Opening a thread left it tagged unread, so the unread count never
matched what had actually been read and the app was quietly wrong every
day it was used. Item 6 of the usability backlog.
A single-shot timer, armed when a thread is selected and restarted rather
than stacked, so arrowing down a list marks only the thread still
selected when it fires and not every one passed through. Configurable
through mark_read_delay_ms in [general], defaulting to 2000: zero marks
read at once, and any negative value disables the behaviour, which is why
the value is neither clamped nor warned about at either end.
The automatic change deliberately does NOT go on the undo stack. It
routes through sendThreadTagChange() rather than tagSelected(), because
undoing an action the user never took is worse than leaving a thread
read, and toggle_unread already gives them a direct way back. It still
funnels through the single applyTags path; what differs is only whether
the inverse is pushed, which is a window-level decision above the worker.
An explicit toggle_unread cancels any pending timer, or marking a thread
unread by hand would be reversed a moment later and the key would look
broken.
Two guards beyond the plan, both from asking what happens when a timer
outlives the thread it was armed for. Arming is skipped for a thread that
is not unread, so no write is scheduled that would change nothing, and
the handler re-checks that its thread is still selected and still unread
before writing, so a stale timer does nothing rather than tagging the
wrong thread.
The plan expected the rapid-arrow case to need a database and a manual
check. It needs neither: ThreadListModel takes threads through
appendBatch(), so the case is unit-tested. All three tests were confirmed
to fail against deliberately broken versions, one arming for read threads
and one creating a timer per selection instead of restarting one.
Item 7 is closed in the same pass. The user verified against real mail
that HTML messages already open as HTML, which is what the item asked
for, so it is recorded as done with no code changed. The prefer_html key
it floated was not added: nobody has asked to default to plain text, and
Ctrl+H already switches a thread by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
MimeParser has filled To and Cc all along and HtmlBuilder simply never
interpolated them, so both were parsed on every message and then
discarded. The header strip showed the subject and a message count and
nothing else, which is item 2 of the usability backlog.
The header now adapts to what it can say honestly. A thread holding one
message shows From, To and Cc under the subject, where every field is
unambiguous. A thread holding several keeps showing the subject and the
count alone: the recipient differs message to message, and once the user
has replied there is no single address the thread is addressed to, so
naming one would be a guess presented as a fact. Per-message detail is
what the dialog is for.
That dialog lists Subject, From, To, Cc, Date and Message-Id for every
message, numbered when there is more than one, in a read-only plain-text
widget. Plain text is the security decision, not a stylistic one: these
values come from strangers and the dialog exists to show them verbatim,
so the format that cannot interpret markup is the right one. The header
label is RichText and every value interpolated into it is escaped, since
an unescaped From injects into the application's own chrome rather than
into the sandboxed page.
Reached by a Details... button beside the subject and by Ctrl+Shift+D.
Both, because a shortcut alone restates the complaint this backlog opened
with. The binding is shifted because Ctrl+D is delete, and the
destructive action keeps the key it already had rather than being moved
to make room.
An empty Cc omits its row instead of printing a label with nothing after
it. Both header shapes were rendered to PNG and inspected, not only
asserted.
The new button also exposed a latent flaw in an older test:
attachmentButtonLabels() identified attachment buttons by excluding the
one other button's label, so it counted the details button as an
attachment as soon as one existed. It now finds the bar by object name
and reads only its children.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Item 2 (message details) gains the user's decision on where the details
go. The header adapts to what it can say honestly: a single-message
thread shows From, To, Cc and Subject, while a multi-message thread keeps
showing only subject and count. Everything else moves to a popup behind a
button on the right of the header.
An earlier draft also put a recipient line on the thread header, which
forced a choice between the union of recipients and their intersection
and would have needed real address parsing to compute either. The user
called that overcomplicating and dropped it, so no address parsing is
needed and the item is UI work over strings MimeParser already fills.
The item's own "check before building" question is answered in place: To
and Cc are parsed at mimeparser.cpp:344-345 and then dropped at the
renderer, which never interpolates them. The larger task it warned about
does not exist.
Item 10 is postponed at the user's request rather than dropped: the
complaint was real and the cheap first fix it proposes still stands, it
is simply not wanted now. Only its startup-query half ever shipped.
Items 18 and 19 come from the user's own notes and were missing here: a
visual cue for unsynced edits, and a sync-on-exit prompt with a config
option. 18 records a finding that shapes both: the QUndoStack looks like
a record of pending edits but is cleared on every query, so it cannot
drive the indicator and a separate counter is needed.
Item 20 records, unspecified, that the user's mental model of the thread
view differs from what was built. Nothing is designed there yet; the next
step is asking what they pictured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Query completion is a new feature, so this takes the minor slot rather
than a patch, the accompanying bug fixes notwithstanding.
The SlackBuild's MD5SUM is left at the 0.4.1 value on purpose: it can
only be computed once the 0.5.0 snapshot tarball exists, which is after
the tag is pushed. Update it with sbodl before the package is built.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The delegate split each row evenly between the value and its
description. That reads as fair but spends the width on the wrong
column: the values are short, the longest prefix being "attachment:",
while the descriptions are ordinary prose. Measured against the built-in
vocabulary, ten of fifteen descriptions elided at a 400px popup and the
longest, "directory below the Maildir root", needed a ~650px popup to
appear in full, all while the value's half of the row sat mostly empty.
The descriptions are the whole reason the popup teaches the query
language, so a half they cannot use is a half wasted.
Measure the value and lend the description the remainder, capped at 65%
of the row. Every built-in description now fits at ~500px instead of
~700px. The cap is what keeps a genuinely long value legible, and the
value's own reservation is capped in turn so that
"application/vnd.oasis.opendocument.text" elides itself rather than
claiming the row and silencing the column that explains what it is.
The test renders the real popup and reads the pixels back, since the
delegate is private to the .cpp and legibility is a painting question.
It asserts the painted width against the width the text needs unelided,
not merely that something was drawn: a blank-or-not check passes against
the very rule this replaces. 550px is chosen deliberately, being a width
where the new rule paints the longest description in full and an even
split cannot; against the old code it fails with "painted 261px of the
285px it needs".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The configuration block is presented as something to copy, and every
optional key in it is commented out with ';' except two. Copying the
block therefore activated whatever those two happened to say: three
extra mimetypes nobody chose, and a sync command pointing at a path
that does not exist on the reader's machine.
Neither was harmful (extra_mimetypes only appends to the built-in list,
and an unresolvable sync command just disables the button with a
tooltip), but both are config acquired by accident rather than by
decision.
Comment them out verbatim. The extra_mimetypes line in particular has
to stay legible rather than be deleted: it is the only worked example
of the '|' and ',' separators, and the reason those two characters
differ is subtle enough that the prose above it needs the illustration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Pressing Return in the query bar moved focus to the thread list and left
the query unrun, so a query typed at the keyboard could not be executed
at all.
Return is bound to open_thread as a Qt::WindowShortcut, and a shortcut is
dispatched before the focused widget ever sees the key. Qt withholds a
plain-LETTER shortcut from an editable widget, which is why every other
binding in the map was safe here, but Return is not a letter and gets no
such protection: the action fired from inside the bar, its handler called
setFocus() on the thread list, and QLineEdit::returnPressed was never
emitted.
Accept the ShortcutOverride for Return and Enter on the query bar, which
tells Qt the focused widget wants the key as ordinary input and stops the
shortcut being dispatched. Narrow by design, one widget and one key, so
open_thread keeps working everywhere else in the window.
Three earlier hypotheses were tested and disproven before this one, and
two synthetic probes wrongly reported the binding as harmless: real input
sends ShortcutOverride first and only dispatches the shortcut if nothing
claims it, while QTest::keyClick skips that round trip entirely. The new
test drives the override exchange rather than the keystroke and fails
against the old code. The comment claiming no filter was needed said the
letter rule covered this case; it did not, and it now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Pressing Tab or an arrow key with the completion popup open crashed the
application outright.
QCoreApplication::sendEvent re-runs APPLICATION-level event filters. The
navigation branch forwarded the key to the popup from inside a filter
installed on qApp, so the very same event came back to the filter that
had just sent it. The popup was still visible, the popupVisible() guard
still passed, and it forwarded again: unbounded recursion ending in a
stack overflow rather than in any diagnosable error. Reproduced at 9176
recursive QueryCompleter::eventFilter frames, with a standalone Qt probe
confirming the re-entry independently.
Guard the filter with m_forwarding, checked before the switch so it
covers every branch rather than the navigation keys alone. A key the
filter is itself redelivering now falls through to the popup instead of
being claimed a second time.
The existing tests missed this because they exercised the accept path
without ever forwarding an event. arrowNavigationDoesNotRecurse drives
Down through the grabbing popup and asserts the selection actually
moved, so it fails on a fix that merely swallows the key; against the
old code it takes the process down with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Showing the popup takes focus away from the query bar and the popup
window grabs the keyboard, so keys pressed while it is up are delivered
to the popup. An event filter installed on the line edit therefore never
ran at the one moment it had to, leaving Tab to move focus to the next
widget and Return to reach the thread list and open a message.
Install the key filter on the application instead, which sees events
before any widget receives them. It returns immediately unless our own
popup is visible, so it cannot affect keyboard handling elsewhere. The
FocusIn filter stays on the line edit, where it is correctly scoped: it
only fires with the popup down.
Accepting a completion now also reopens the popup when the caret lands
somewhere more can be offered, so taking "tag:" goes straight on to the
tag list instead of needing a second complete_query. The chain stops on
a stem that is already a complete candidate, which is what every accept
produces. The mouse path chains identically.
The previous tests passed against the broken code because they posted
events straight to the line edit, bypassing the delivery path a real
keypress takes. The new tests route keys through the active popup and
run against a real X display; offscreen does not grab the keyboard and
cannot reproduce this class of bug.
|
|
QLineEdit::setCompleter hands completion to the line edit, which then
resets the completer's completionPrefix to the widget's entire text on
every keystroke. The prefix has to be the stem, so once the query grew
past its first token the whole-line prefix matched no candidate, the
popup stopped appearing, and the two reported symptoms followed: nothing
was there for Tab to accept, and Tab fell through to focus navigation.
Attach the completer with setWidget instead, which keeps the popup
anchored without ceding control of the prefix. complete() dereferences
widget() unconditionally, so leaving it unset segfaults rather than
degrading. Opening the popup then becomes ours to do on every edit.
Extend the existing event filter to route the keys the popup needs while
it is visible, and install it unconditionally now that it does more than
the completion_on_focus case. Enter accepts a completion only while the
popup is up, so returnPressed still runs the query when it is closed.
The existing tests called acceptCompletion() directly and so never
touched the widget, which is why neither bug was caught. Add four tests
that drive the real widget path plus one covering both settings of
completion_on_focus; the first two fail against the old code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Records what shipped rather than what the plan drafted: path: is the only
prefix beyond tag:/is:/date:/mimetype: that offers values, and completion_on_focus
defaults to false.
The extra_mimetypes syntax needs both separators explained, since ',' splitting
is QSettings' own behaviour and '|' exists only because a description may contain
a comma.
|
|
Adds the complete_query action, whose registration the assertion in
registerActions() has been demanding since keymap gained the name: a
known action nothing implements would have been a silently dead binding.
Also lands the QueryCompleter half of the manual trigger, which the
keymap commit left behind: triggerCompletion() and the focus-in filter
gated on completion_on_focus.
Tags refresh at startup, after a sync, and after a mutation introduces a
tag not already known, since that is the tag most likely to be typed
again. onAllTagsReady deliberately drops the generation the signal
carries: a tag list is not an ordered query result, so a later one is
always at least as good as an earlier one and discarding on staleness
could only throw away a good list.
|
|
The free-form date hint is a footer label rather than a model row: a row
would be filtered away by the first non-matching keystroke and could be
selected and inserted, producing a query that errors.
QCompleter::setPopup takes a QAbstractItemView, so the label cannot be laid
out beside the view in a container widget. The footer sits in space reserved
with setViewportMargins inside the list view instead.
setItemDelegate must run after setPopup, not before: setPopup installs a
plain QStyledItemDelegate of its own and discards whatever was already set,
which silently drops the description column.
Accepting replaces exactly the span the tokenizer identified rather than
QCompleter's own completion prefix, which is a different span once a prefix
or a range bound is involved. Four tests drive that path directly instead of
through synthetic key events, since whether a key needs Shift is a
keyboard-layout property and could not decide the question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
tag: and is: share the tag list, since notmuch aliases them. path: offers
each account maildir in both bare and recursive forms, the latter being
what Account::scopedQuery builds and not something a user would guess.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Mimetypes are the one completion list with no enumerator, so the user can
extend it. Entries append to the built-ins and a malformed one is skipped
with a problem recorded rather than dropping the whole list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
KeyMap::defaultBindings() is the single source of truth for shortcuts:
the menus, the shortcut reference dialog and loadDefaults() all read it,
so registering here makes the binding appear in the reference and stay
rebindable from [keys] without any of them disagreeing.
Ctrl+Space is a modifier plus a named key, so it sidesteps the bare
capital trap in normalizeSequence() and needs no Shift on any layout.
Verified it parses to a single non-empty combination that round-trips to
"Ctrl+Space", and it collides with no existing default.
Note test_mainwindow now fails its everyKnownActionIsRegistered()
assertion: MainWindow does not yet implement complete_query. That wiring
is a separate task, and the assertion firing is the intended signal.
|
|
Keywords are syntax and stay untranslated; the descriptions beside them are
prose and go through tr().
The tables are free functions with no QObject to inherit tr() from, so the
file declares a VocabularyStrings context with Q_DECLARE_TR_FUNCTIONS rather
than borrowing QObject::tr, which would file every string under the QObject
context.
|
|
Query bar completion cannot offer tag names without a way to enumerate
them, and libnotmuch had no call wired up for it. Follows the existing
generation-counter pattern; the result crosses the thread boundary as a
QStringList.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
date:las..today put the caret at the end of the lower bound, where the
whole-bound span and the typed-so-far span are both 3 characters long, so
the test passed even against a tokenizer that truncates at the caret and
never sees the range separator. Moving the caret mid-bound with
date:lastweek..today separates the two: 8 under correct code, 3 under the
truncating one. Mutation-verified in both directions.
|
|
Each side of '..' is an independent value against the same model. Entries
that are themselves ranges are withheld once a range exists, since
date:1week....today is malformed.
The token is read to its full extent rather than truncated at the caret:
the separator deciding which bound is being edited can sit to the right
of the caret. Stems stay caret-bounded so matching never uses untyped
text.
|
|
subject:"foo bar has the cursor in free text, where offering keywords
would be wrong.
|
|
The replace span covers the value only, so accepting a completion never
overwrites the prefix that selected it.
|
|
Prefix completion only so far: the token under the cursor, bounded by
whitespace or an opening parenthesis rather than by the start of the line.
|
|
Twelve tasks, TDD throughout, tokenizer first: the parsing rules are where
the defects will be and they need neither a widget nor a database to test.
CompletionEntry gets its own header. Config needs it and QueryCompleter
needs Config, so declaring it in querycompleter.h would make the two headers
include each other.
Also corrects the spec: NmTags already exists in nmraii.h, it does not need
adding.
|
|
Mimetypes are the one completion list with no enumerator and an open-ended
set, so [completion] extra_mimetypes appends to the built-ins. Appending
rather than replacing means a typo or a short list cannot leave completion
worse off than the defaults.
Entries are comma-separated with an optional description after '|'. The two
separators differ because QSettings splits comma lists itself, so a
description containing a comma would otherwise be torn into two entries.
The other lists stay fixed: prefixes come from notmuch, paths from the
configured accounts, dates are closed once symbolic and relative forms are
covered, and tags are read from the database. A user-editable copy of any of
them would only drift from its source. For prefixes specifically, the upgrade
path is deriving the list from the installed notmuch, not making it editable.
|
|
Design for backlog item 17. Completion covers query prefixes, tag values,
date values, path values and mimetype values, each carrying a description
so the bar documents the query language while it is typed.
A new QueryCompleter class owns it, with the cursor-context tokenizer as a
pure function so the parsing rules are testable without a widget or a
database. NotmuchWorker gains an all-tags call, which did not exist.
Addresses for from:/to: are out of scope: libnotmuch exposes no all-addresses
call. Both prefixes still appear so the vocabulary reads complete.
Account maildirs belong to path:, not folder:. Account::scopedQuery builds
path:"<maildir>/**", and folder: is a different matcher in notmuch, against
the folder name rather than the directory path.
Also records the translatable-strings rule in CLAUDE.md. Existing code is
not yet audited against it.
|
|
Replace QMessageBox::about with a QDialog laid out in two columns: the
application icon on the left at 40%, the version, description, copyright
and AI-assistance notice on the right at 60%. A full-width row below both
carries the project URL.
The copyright line, GPLv2 notice and AI-assistance disclosure were absent
from the About window despite being present in the source headers and the
README.
|
|
The URL and checksum are taken from the published snapshot rather than
predicted: sbodl fetches it and reports "md5sum matches OK", and the
tarball is confirmed to carry the QTMAILDIR_BUILD_TESTS option, so
-DQTMAILDIR_BUILD_TESTS=OFF now takes effect where it was silently
ignored with 0.4.0.
The copy of the .info inside the v0.4.1 snapshot itself still names
0.4.0, since a snapshot cannot contain its own checksum. That copy is
not the one anyone builds from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Packaging only. The 0.4.0 tarball predated QTMAILDIR_BUILD_TESTS, so a
package built from it accepted -DQTMAILDIR_BUILD_TESTS=OFF and ignored
it. This tag is the first whose source honours the flag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Packages the application for Slackware, following SBo conventions with
two deliberate departures: the tag is _danix rather than _SBo and the
package type is txz rather than tgz, since this is not an SBo
submission. sbolint reports exactly those two as errors and nothing
else; the template comments it warned about are gone.
Written against the install layout the build really produces, checked by
staging it: one binary, one .desktop entry and one scalable icon, no
libraries and no man or info pages. The template's .la removal, man and
info compression and perllocal.pod cleanup would all act on nothing
here, so they are left out rather than carried along as dead code.
doinst.sh keeps only the desktop-database and icon-cache updates.
The download URL and checksum are verified rather than assumed: sbodl
fetches the tarball and reports "md5sum matches OK".
Adds QTMAILDIR_BUILD_TESTS, defaulting to ON so the ordinary build is
unchanged. A packaging build has no use for the test binaries, and
building them pulls in Qt6::Test to produce nothing that ships. Note the
0.4.0 tarball predates this option, so with that source the flag is
accepted but does nothing; the README says so.
notmuch is the only dependency outside Slackware. Qt6 including
WebEngine, gmime and cmake are all stock, which is what REQUIRES
reflects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The attachment bar had been an empty placeholder since it was written:
MessageView created it and added it to the layout, and nothing ever put
anything in it. MimeParser had been extracting attachments the whole
time and Attachment::saveTo() already carried the path-traversal guard,
so the backend needed calling rather than writing.
The bar holds one "Attachments (N)..." button whatever the count. One
button per file was built first and was wrong: a thread with sixteen of
them made the bar as wide as the window, pushed the splitter over and
left the thread list a few pixels wide. The button opens a dialog
listing message number, filename and size with a Save each, and a
"Save all..." when there is more than one.
Save all writes into a new subdirectory named "<date> <subject>" inside
a parent the user picks, rather than dropping sixteen files loose among
whatever is already there. Zipping was considered and rejected: Qt ships
no zip API, so a real archive meant a new build dependency or shelling
out to /usr/bin/zip at runtime, and a subdirectory answers the actual
requirement. The picker names the subfolder before the user commits to a
location.
The subject is attacker-controlled and becomes a directory name, so
attachmentFolderName() sits beside the other guards in mimeparser.cpp:
it strips separators, control characters and leading dots, caps the
length, and falls back to a generated name. Its test asserts that every
hostile subject still resolves inside the parent directory.
Two defects surfaced while using it, both silent:
saveTo() overwrites an existing file, and several messages in one thread
commonly attach the same filename. Saving that thread destroyed six of
sixteen files while reporting all sixteen as saved. The batch path now
uses saveWithoutOverwriting(), which appends " (2)" before the extension
and keeps a compound extension whole.
Qt::RFC2822Date rejects a Date header that carries a timezone comment,
"+0200 (CEST)", which is legal per RFC 5322 and common in real mail. Qt
refuses the entire string rather than ignoring the comment, so every
such message lost its date prefix. Comments are stripped before parsing.
Opening an attachment in its default application is deliberately not
included: handing a file from a stranger to xdg-open is a different
security decision from writing it where the user asked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
An attachment was only discoverable by opening the thread. A narrow
leftmost column now marks the threads that carry one.
No new worker query is involved: notmuch applies the "attachment" tag
while indexing, so ThreadSummary already holds what this needs. The
marker is a glyph rather than an icon resource, which ships no new asset
and inherits the row font, so it strikes through with a doomed thread
like every other cell. It falls back to "*" where the system font cannot
draw U+1F4CE, since an unrenderable codepoint reads as breakage rather
than as a marker.
Two silent Qt behaviours had to be handled, both found by probe:
QHeaderView::restoreState() returns true for a blob saved against fewer
columns and applies the old widths shifted one place right. Adding a
column in front would therefore have mangled every existing saved
layout with no error to detect it by. The column count is now stored
beside the blob and a mismatch discards it, so the widths reset once on
upgrade instead of landing on the wrong columns.
QHeaderView's default minimumSectionSize is 58px on this platform, and
setColumnWidth() clamps to it without reporting the smaller value back,
so the column could not be narrow at all until it was lowered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Attachments turned out to be a real gap rather than a discoverability
one. MessageView creates the attachment bar and adds it to the pane, but
nothing ever populates it: m_attachmentBar appears nowhere else in the
codebase, so it has never displayed anything. MimeParser already
extracts attachments and Attachment::saveTo() already carries the
path-traversal guard, so the backend needs calling, not writing.
Item 16 makes delete a toggle, with the open question of what a mixed
selection should do. Item 17 needs a new worker call, since there is no
way to list tags today.
Also records that this document's numbering and the user's own notes
have diverged, so a reference to "item 13" stays resolvable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The app opened whichever saved query sorted first alphabetically, which
is not a choice anyone made: [queries] is read through childKeys(), so
savedQueries().first() means "Flagged" before "Inbox" before "Unread"
rather than anything the user expressed.
[general] startup_query names the entry to open and defaults to Unread,
so a fresh install comes up on the unified unread list. Saved-query
button order is untouched and stays alphabetical.
A name matching no saved query falls back to the first one rather than
starting with an empty view. That is reported as a problem only when the
user actually wrote the name; the built-in default naming a query they
never created is not something they got wrong, and warning about it
would fire on every launch of a config that has no Unread entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Zoom was Chromium's, not the application's: the web view handled the
keys natively and never told anyone, so there was no value to save.
qtmaildir now owns it. Zoom in, out and reset are real actions, in the
View menu and rebindable through [keys], and the factor is persisted to
the UI state file. Ctrl+wheel over the body zooms and Ctrl+middle-click
resets, both filtered by ancestry from an application-level filter: the
events are delivered to an internal QQuickWidget the web view creates
lazily, so a filter on the view itself never sees them.
The factor is clamped to 0.5 - 3.0, and NaN, infinity, zero and negative
values fall back to 1.0, since a corrupt state file must not be able to
leave the pane unreadable with no visible way back.
Both risks the plan flagged turned out not to exist, verified by probe
rather than assumed. The application QAction wins over the web view's
native zoom key, so the tracked factor cannot diverge from what is on
screen. And the factor survives setHtml(), so the web view is the single
source of truth and needs no reapply per render.
A third finding is worth recording because it produced a wrong fix
first. A probe using QTest::keyClick() reported Ctrl++ as a dead
binding, and a test was written asserting that. Both were wrong: Ctrl++
is exactly what the '+' key emits on an Italian layout, confirmed
against the real keyboard, and it is the shipped default. Whether a
symbol needs Shift is a property of the layout, not of Qt, and
keyClick() reproduces neither. The test now only checks that every
default parses, and the comment in defaultBindings() says not to
re-derive this from synthetic input.
Ctrl+= is a second binding for reset, skipped when [keys] gives it to
something else.
Also fixes a pre-existing bug the new config key exposed. [general]
entries were read as "general/<key>", which matches nothing: QSettings'
INI backend treats a section literally named [general] as its own
fallback section and strips the prefix. notmuch_config had therefore
never worked. Both keys are now read without it; the file format is
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Resizing the window, the splitter or a thread-list column was undone by
the next launch. State now round-trips through a separate settings file
at ~/.local/state/qtmaildir/uistate.conf, written on close and read at
startup.
The state file is deliberately not the user's config: a base64 geometry
blob does not belong in a hand-edited file, and rewriting that file on
exit would drop its comments and key order, which QSettings does not
preserve.
Two details that are easy to get wrong:
QStandardPaths::StateLocation appends both the organization and the
application name, and both are "qtmaildir" here, so it resolves to
~/.local/state/qtmaildir/qtmaildir. The path is built from
GenericStateLocation instead, matching Config::defaultPath().
Restore runs after buildMenus() rather than at the end of buildUi():
QMainWindow::restoreState() matches toolbars by object name and silently
drops the position of one that does not exist yet.
Every restore is conditional on a non-empty blob, so a missing or
rejected state file leaves the built-in defaults instead of producing a
zero-size window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
The icon was committed in a previous session and referenced nowhere: no
qrc, no .desktop entry, no setWindowIcon. It is wired up now, as a window
icon, a desktop entry, and install rules placing both into hicolor and
share/applications.
resources.qrc belongs to the executable rather than to qtmaildir_lib. A
qrc compiled into a static library registers itself from a global
initialiser, and the linker discards that object because nothing
references it: the build succeeded, qInitResources_resources() was
present in the .a, and QFile::exists(":/icons/qtmaildir.svg") still
returned false at runtime. Verified loading at 16, 32 and 64 pixels after
the move.
Toolbar and menu actions take icons from the system theme by their
standard names, so they match the rest of the desktop rather than
shipping bespoke art. A theme lacking one leaves that action as text,
which still works.
|
|
Two optional keys in an [account.<key>] stanza. color fills the chip,
label sets its text.
Both belong to the account rather than to [tagcolors] because an account
tag is a different taxonomy: which mailbox a thread arrived in, not what
state it is in.
label is display only. "account-provider-work" is a lot of
row for one bit of information, but the notmuch tag is never renamed, so
existing queries and external tagging are unaffected. Unset falls back to
the account key, and an empty label is ignored rather than rendering a
blank chip.
|
|
Spelled out per row, tags ran to 500 pixels of largely repeated text and
took most of the thread list's width. The column is gone; tags render as
coloured chips split by what they actually mean.
An account tag says which mailbox a thread arrived in, and draws as a
chip in front of the subject. A functional tag says what state a thread
is in, and those fill one row under the message pane. One row keeps the
message area from shifting between threads with different tag counts, so
whatever does not fit collapses into a +N chip that names the rest in its
tooltip.
TagColors resolves a colour by exact tag first, then by top-level prefix,
so a single "shopping" entry covers shopping/amazon and shopping/nike
while shopping/amazon can still override its own. That matters at 96
tags. Built-in defaults cover the usual state tags, and anything left
unconfigured falls back to a hash of the name, stable so a chip does not
change colour as the list scrolls.
|
|
Subject was Stretch, which computes its own width and discards a drag, so
it alone could not be resized. Every column is Interactive now.
Nothing absorbs spare width as a consequence, so the view scrolls
horizontally instead of squeezing columns when their total exceeds the
viewport. Per-pixel, so scrolling does not jump a column at a time.
|
|
Tags, Date and From were set to ResizeToContents while fixing the
pushed-off-screen Tags column. That mode computes the width itself and
discards a drag, so the columns stopped being resizable. Verified: with
ResizeToContents a request for 250px yields 150, with Stretch 478, with
Interactive 250.
The reorder alone already fixed the original bug, since Subject stretches
and is last, so nothing can be pushed past it. Locking the other three
was unnecessary and also blocked the saved-column-widths item, which
needs widths a user can actually set.
They are Interactive again, with starting widths a drag overrides.
|
|
Selecting a thread and hitting Delete changed nothing on screen, so
there was no way to tell the action had stuck.
The tag was always applied: applyTagChange() emitted dataChanged across
the row, and the Tags column did update. But Subject was set to stretch
while Tags came after it, so Subject took all free width and pushed Tags
out of view. The feedback lived in the one column that could not be seen.
Columns are now Tags, Date, From, Subject, with Subject stretching last
so nothing can be pushed off the right edge. A thread tagged deleted or
spam fills its whole row, muted red or orange with white struck-through
text, through the background, foreground and font roles, so no cue
depends on one column remaining visible.
Strike-through accompanies the fill on purpose: it survives a theme that
overrides backgrounds and reads without colour. Bold for unread still
composes with it.
Archive adds no tag, so an archived row is left unstyled for now.
|
|
Fourteen actions in one table made a dialog taller than the display,
which pushed its own title bar off the top. The rows are split into two
columns of seven, with the closing note spanning both.
QMessageBox is replaced by a plain QDialog. The message box wraps its
text at a narrow fixed width, which broke every description into a
column of single words and was most of the height: 719x1084 before,
1426x366 after.
|
|
Actions were a QHash of std::function dispatched by an event filter,
which nothing could put in a menu. They are QActions now, bound from
KeyMap so a [keys] override reaches the menus as well as the keyboard.
Menu bar covers every action; the toolbar carries only Sync, Archive,
Delete and Undo. Help > Keyboard shortcuts is generated from the actions,
so it shows what the keys really do rather than a copy that drifts.
spam and load_remote gained defaults, having been unreachable without a
hand-written binding.
The event filter is gone. Probing showed QAction shortcuts are dispatched
before the focused widget sees the key, so they beat QAbstractItemView's
type-to-search without one, and Qt already suppresses plain-letter
shortcuts while an editable widget has focus. Dropping the filter's
blanket guard also lets Ctrl+Q work while the query bar has focus.
registeredActionNames() is derived from the actions rather than
hand-maintained, so the two drift tests it needed are replaced by checks
that a configured binding reaches its action.
No confirmation dialogs: tag mutations still answer to undo.
|
|
Typing a capital emits Shift+<key>, but QKeySequence::fromString() folds
the case of a bare letter away: "N" parsed to plain Key_N, a combination
no keystroke produces. The N, F and G defaults (toggle_unread, flag and
sync) therefore never fired, and neither would any hand-written capital
in [keys].
normalizeSequence() rewrites a bare capital to Shift+<letter> and is
shared by the defaults and the override pass. As a side effect "y" and
"Y" become distinct keys rather than a collision that dropped one.
Defaults move to modifier shortcuts throughout. A single letter cannot
be a QAction shortcut without stealing that letter from every text field
in the window, and the menus in the next commit need real accelerators.
defaultBindings() is now the one source of truth for them.
|