| Age | Commit message (Collapse) | Author | Files | Lines |
|
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.
|
|
Collects the items found while actually using 0.1.0. Two clusters dominate:
state that does not survive restart (splitter, zoom, account selection) and
actions reachable only by memorized keys.
The plan is open by design rather than a fixed release scope. Numbering is
stable so notes referring to an item keep meaning the same thing.
Decisions recorded while triaging:
- UI state goes to its own file, not qtmaildir.conf. That file is hand-edited
and rewriting it on exit would eat comments QSettings does not preserve.
- Auto-mark-read stays off the undo stack. An explicit toggle_unread action
already exists, so Ctrl+Z need not undo an action the user never took.
- Zoom is Chromium's, not ours. No setZoomFactor call exists in src/, so
persisting it means taking ownership of zoom first.
Icon is a tag rather than an envelope, since tagging is the core interaction
and an envelope would not distinguish it from any other mail client. Verified
legible at 16x16, which is where it is mostly seen.
|
|
The manual verification notes named the real threads used to test against a
live mailbox. Most of it was incidental, but one was not: the automated
sender identified in items 12, 13 and 16-18 is a healthcare provider's
patient-notification service, and naming it disclosed a medical
relationship. Also removed a correspondent's username, a locatable GitHub
thread id, a real maildir name in the spec's example config, and absolute
home paths.
Every finding is restated generically and none lost substance: "a message
with 3 inline cid: parts" carries the same technical weight as naming the
sender did. Counts and timings are kept deliberately, since those are the
evidence behind the claims rather than anything identifying.
Commit history and the v0.1.0 tag were checked and were already clean, so no
history rewrite is needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
README covers what the project deliberately does not do (no POP, IMAP or
SMTP, no send in v1, no confirmation dialogs), the verified dependency
versions, build and test commands, the full config format with the reasons
behind its two surprises, the keybinding table, and the security posture of
the message view.
Two facts in it were checked rather than assumed: the `spam` and
`load_remote` actions are registered but have no default binding, so they
are documented as unreachable until bound; and the attachment path guard
does exist as described. The Release build was also verified to pass all 11
test binaries, which matters because Q_ASSERT compiles out there and one of
the cid: invariants leans on an assertion in debug.
CHANGELOG.md follows Keep a Changelog and records 0.1.0 along with the
current known limitations. docs/RELEASING.md records what semver means for
a mail client (the config format and action names are the public interface)
and the steps, including that the version is bumped in exactly one place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The version was declared in the project() call and used nowhere. It is now
generated into version.h from that single declaration, so nothing repeats
the literal, and it reaches the places it is actually wanted: --version,
--help, the window title, and QApplication.
--version and --help are answered before the web engine schemes are
registered and before QApplication is constructed. Printing one line does
not need a GUI, and both must work on a machine where the GUI cannot open.
Staying at 0.1.0 rather than calling this 1.0.0: under semver, 0.x is where
the interface may still change, and for this project the interface is the
config file format and the bindable action names. Both are one manual
verification pass old. 1.0.0 becomes a deliberate decision to stop changing
them under users.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Confirmed with the maintainer as v2-only rather than v2-or-later. LICENSE is
the official text from gnu.org. Every file under src/ and tests/ carries the
matching notice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Tag mutation (16-18) verified against three low-stakes 2023 notifications,
with the index inspected before and after every step and the mailbox
returned to its exact starting state. Bulk archive resolved two threads in
one combined query, and undo re-resolved thread ids while those threads were
outside the displayed result set, which is the case that design exists for.
Sync (19-20) verified with the real mailsync.sh. start() does not block, a
concurrent run is refused, and a run colliding with the flock returns exit 1
in 2 ms without corrupting anything.
One caveat recorded rather than fixed: the script redirects its output to
its own logfile, so MailSync captures nothing and the log pane stays empty
even on failure. qtmaildir shows what the command emits and this command
emits nothing; piping instead of redirecting in mailsync.sh would populate
the pane with no change here.
Twenty-one of twenty-two items now verified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Eleven of twenty-two items verified. Items 16-18 remain deferred until the
tag-mutation path can be run with someone watching, 19-20 need a sync
script, and 13 is unreachable with this mailbox and covered by a unit test
instead.
Records what the pass was worth: three defects, each living in the gap
between components that were individually tested and correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
With the thread list focused, 'h' jumped to the next thread whose subject
began with "h" instead of toggling HTML, and j, k, a, d, N, F, u and G were
swallowed the same way. The keymap only worked when focus happened to be
somewhere else.
installEventFilter(this) was on the MainWindow, and a window-level filter
only sees key presses the focused child did not consume. QAbstractItemView
consumes plain letters for its type-to-search feature, so it took them
first. The filter is now installed on the thread view as well, which puts
the keymap ahead of that search. The existing query-bar guard in
eventFilter() still keeps ordinary typing working there.
Found by the maintainer while walking task 13 item 14, and confirmed fixed
on screen.
No regression test: a QTest::keyClick attempt passed both with and without
the fix, because synthetic key posting does not reproduce the focus and
consumption path that causes the bug. A test that cannot fail is worse than
none, so it was dropped rather than kept for appearances.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|