| Age | Commit message (Collapse) | Author | Files | Lines |
|
Follow-up to item 56. With the toolbar now following the desktop's
button style, an "icon only" desktop makes the icon the whole control,
and this style reports PM_ToolBarIconSize as 16px, which is a small
target for a button with no text beside it.
A [general] toolbar_icon_size key, 16 to 64, defaulting to 24 rather
than to the style's own metric. Setting it to 16 restores the theme's
value.
Clamped and reported, unlike message_zoom, which documents a 0.5 to 3.0
range in the README and enforces none of it. Both ends here break the
UI that would be used to fix them: too small is an invisible icon, too
large is a toolbar taller than the window.
The unenforced message_zoom range is recorded as item 58 rather than
fixed here, since it is a separate defect that predates this change.
Also documents in the README that saved-query button labels are the key
names from the user's own [queries] section, which is why the "Flagged"
button still read that way after the action was renamed: it is a user's
query name, not a string this code owns. The sample config now shows
`Important = tag:flagged` to teach the wording the UI uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Items 56 and 57, done together because both touch the action registry.
56. The complaint was inconsistency, not absence: eight of twenty-four
actions had themed icons, so two adjacent entries in one menu disagreed
and the toolbar laid out an empty slot for the other sixteen. The
themeIcons table now covers them all. The fifteen names added were
probed against a live icon theme first rather than taken from the
freedesktop spec on faith, and the existing null-icon guard still lets a
theme that lacks one fall back to text.
The second half of the note asked that buttons honour the desktop's
"Icon only" setting. They could not: the hardcoded setToolButtonStyle
overrode it whatever the user had chosen. It now reads
SH_ToolButtonStyle. Dropping the call entirely was tried and rejected,
since a bare QToolBar defaults to ToolButtonIconOnly rather than to the
platform hint, which ignores the setting just as thoroughly the other
way. This is a visible change: on a desktop set to "Icon only" the
toolbar now shows icons without text.
57. "Important" over "Starred", the user's pick; the Message menu
already has "Mark &spam", so "Starred" would have needed an accelerator
from inside the word. Changed the action text, its status tip, the undo
description and the star column's tooltip, which still read "Flagged".
The tag stays `flagged`. It is wire format that neomutt, the user's
saved queries and ThreadSummary::isFlagged() all read, and following the
label through to the tag would rewrite the mail store and desynchronise
every other tool over the same Maildir. The action name stays `flag`
too, since that is the key users write in [keys].
Four tests. everyActionCarriesAnIcon names every action missing one and
guards against passing on an empty list; it reported all sixteen before
the change. theImportantActionStillWritesTheFlaggedTag asserts on the
tag the model actually received, and mutating it to `important` fails
that test plus two pre-existing held-edit tests.
Also adds the changelog entry for the cron-sync indicator fix, which the
commit that made it omitted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Item 54. A sync fired by the user's cron carries tag edits to the mail
store exactly as a local one does, but only onSyncFinished() cleared the
pending state, so the indicator kept reporting work that had already
shipped and the exit prompt asked to sync for it. Verified against a
real cron run: 31 changes, cleared with no manual sync.
The window cannot see an external run's exit status, and /proc/locks
carries no outcome. It does not need to: mailsync.sh already ends every
run with a "RUN END ... status=OK" banner in its log, which outlives the
process that wrote it. MailSync::lastRunOutcome() reads a bounded tail
of that file and takes the last completed marker, so no change to the
script and no optimistic guessing were needed.
Only a definite OK clears anything. A failed run, a missing or
unreadable log, and a State::Unknown lock reading all leave the count
alone: over-reporting costs a redundant sync, under-reporting costs the
user their edits.
m_editedAccounts is drained in the same place, before flushHeldEdits()
and matching the local path's ordering. Item 49 uses it to choose which
mbsync channels a run syncs, and a count that reached zero while the set
stayed full would look correct and still sync the wrong channels.
The log path comes from a new optional [sync] log key, defaulting to
where the script writes, so a test never reads the developer's own log.
Two notes on the verification, both recorded in the backlog:
- A timing probe endorsed a tail read that was not happening. The
first version of the huge-log test required the call under 100 ms
and passed with the seek deleted, because reading 10 MB is fast
either way. Replaced with an assertion on content.
- Every fixture was invented and the first batch had the wrong
timestamp format, since the script uses date -Iseconds. The tests
passed anyway, because the parser keys on the prefix and the status
token. One test now builds the banner the way the script does.
The before-flushHeldEdits ordering has no test: without a held lock the
flush is a no-op, so both orderings pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
A sync ran mbsync -a regardless of what changed, so tagging mail in one
account fetched all of them. The account set was not a parameter anywhere
on the path: MailSync::start() took no arguments and the script hardcoded
-a, so nothing between a tag edit and mbsync carried which account changed.
Track which accounts have edits and pass their mbsync channels through to
the script, which now takes channel names and falls back to -a when given
none. An empty set means all accounts, per the request: a sync with nothing
pending is a fetch, and narrowing that to wherever the last edit landed
would quietly stop collecting mail everywhere else.
The channel is a new optional per-account key rather than the section key.
The two names genuinely diverge, because a QSettings section key may carry
dots that the channel does not, and mbsync treats an unknown channel as
fatal rather than skipping it, so key-as-channel would fail those accounts'
syncs outright rather than degrade. It defaults to the key, so accounts
whose two names already agree need no config change.
The edited-account set is deliberately not netted the way the pending-edit
map is: that map tracks the index, where a tag removed and re-added leaves
nothing outstanding, while this tracks the mail store, where both writes
have already renamed files that mbsync still has to propagate. It is also
snapshotted before flushHeldEdits(), which inserts into it synchronously
rather than on a queued reply, so a successful sync cannot clear accounts
whose edits it never carried.
Closes item 49.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Groundwork for item 30, the blank right pane. Nothing references these
yet; the pane itself is the next piece of work.
The pane is drawn from the user's own HTML mockup, which sets its
wordmark in Oxanium and its text in IBM Plex Sans and pulls both from
Google Fonts. That @import cannot work here: the web view's interceptor
blocks every request by default, deliberately, and neither font is
installed on the development machine either. Shipping them is what
makes the pane look the same on every machine.
Both are SIL OFL 1.1, which permits redistribution and is compatible
with the GPL, and each carries its licence text beside it. The README
records the pairing.
Both are subsets, since they are embedded into the rendered document as
data URIs and the full families would dwarf the page using them.
Oxanium is instanced to weight 800 and cut to the nine characters of
the wordmark: 43K to 1.2K. IBM Plex Sans is cut to Latin-1 plus common
punctuation rather than to the exact strings in use: 525K to 13K. The
wider cut is deliberate, because that font carries interface text that
will change, and a subset matching only today's wording would break
silently the moment a string is edited. Coverage of both was verified
against the strings they have to draw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
An action removing "unread" from every thread in the current view,
on the toolbar, the Message menu and Ctrl+Shift+U. It deliberately
ignores the selection, which makes it the one action in the window
that does, and it routes through the same funnel as every other tag
change, so it is one write rather than one per thread.
Disabled until the query reports its total. Threads arrive in batches,
so before then the model holds only what has landed, and an action
saying "all" must not silently skip the rest. A greyed control says
"not yet" without needing a dialog or a stall the user cannot see.
The state is also set at registration, since QAction starts enabled
and a window that has not run a query has nothing to act on.
Two things came out differently from the plan, both forced by existing
code. It carries a default binding, because everyActionHasAShortcut
requires every registered action to have one: an unbound action is
unreachable from the keyboard, and that invariant is deliberate, so the
action was given Ctrl+Shift+U rather than the invariant relaxed. And
only the threads that are actually unread are sent, because sending the
rest would inflate the pending-edit count with writes that change
nothing, and the quit prompt reads that count. A view with nothing
unread does nothing, pushes no command and says so: an undo entry that
restores nothing is worse than none, since it absorbs a Ctrl+Z meant
for the previous action.
undoDepthForTesting() is new and exists for a reason worth recording:
undo->isEnabled() cannot answer "was a command pushed", because the
undo QAction is always enabled and tests canUndo() when triggered. The
first version of the no-op test asserted on it and passed against a
mutant with the unread filter removed.
Closes item 43.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Four changes to the sync UI, three of them from using it.
The log pane could not be dismissed. It is hidden at construction and
shown on failure, and nothing ever hid it again, so a single failed sync
left it on screen until the application restarted. It now sits in a
container with its own Close button, and is 200px rather than 120,
because mbsync's output is wide and repetitive and the shorter pane
showed too little of it to read. It still appears only on failure, which
the user confirmed is what they want.
A sync gives no feedback while it runs. The status bar now carries an
indeterminate progress bar for the duration, and the Sync button is
disabled rather than left looking live. The bar is indeterminate on
purpose: mbsync reports no percentage and the script's output is
unstructured, so a bar filling left to right would be inventing a
fraction nobody knows. The log is also cleared at the start of each run,
since leaving the previous run's lines in place makes a stale failure
look like the current one.
The lock skip was reported as a failure. mailsync.sh exits when another
run holds the lock, and that was exit 1, which qtmaildir reads as "sync
failed": it showed the log pane and, on the exit path, told the user
their changes were still unsynced. With a cron timer every ten minutes,
a click landing inside a run is routine and none of that is true. The
script now exits 75 (EX_TEMPFAIL) and the window reports it as its own
case, saying a sync is already running. On the exit path it stays open
and says plainly that the other run is most likely carrying the changes
over but that this window cannot see it finish, rather than guessing
either way.
That last hedge is what item 27 records: the application cannot see a
sync it did not start. The user chose continuous polling of the lock file
over the narrower "only while quitting" version, and the entry notes that
the lock is already the signal, so no status file is needed, and that a
kernel lock cannot go stale where a written file can.
Verified against stub binaries: a second run while the lock is held exits
75 and says SKIPPED, while the run holding it completes at 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The obvious crontab line is the wrong one. The script writes to
~/.local/state/mailsync.log itself, so a caller appending to that same
file gets every line twice, which is what the first real run of the moved
script did.
Also records that rotation belongs to logrotate rather than the script,
and why: the two fought over the same file and the script overwrote a
compressed generation with an uncompressed one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The reference sync script lived in the companion mailctl project, which
documents that it never calls it: sync and organization are separate
there on purpose. qtmaildir is the only thing that runs it
programmatically, and two of its behaviours were live bugs here, so it
belongs in this repo where the constraints on it are.
It printed nothing a caller could see. Everything was redirected to
$LOGFILE, so MailSync captured an empty stream and the sync log pane
stayed blank, a known limitation carried since 0.1.0. It now writes to
both through tee: cron keeps its log, and the pane has something to show.
It also ended in an unconditional exit 0, discarding statuses it had
already computed. qtmaildir believes that exit code: a failed mbsync
reported success, cleared the unsynced-changes count, and would have
allowed a sync-on-exit to quit over a sync that never happened, which is
exactly the case the exit prompt exists to prevent. It now exits with
mbsync's status, or notmuch's when mbsync succeeded.
The statuses move through files rather than shell variables because the
run block is piped into tee, which puts it in a subshell where an
assignment does not survive.
Verified against stub mbsync and notmuch binaries in a fake HOME, so no
mail or database was touched: output reaches stdout, the log still gets
every line, and a failing mbsync or notmuch produces its own exit code
rather than zero.
Both properties are now recorded in CLAUDE.md, since they exist for this
application's sake and an edit that looks like a cleanup would remove
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Five hardcoded tags were the only ones reachable from the UI: archive,
delete, spam, flag and toggle_unread. For an application whose purpose is
organising mail by tag, applying any other one meant leaving for a
terminal. Item 26 of the usability backlog, raised by the user asking how
to add a tag and finding they could not.
One dialog rather than separate add and remove actions, at the user's
choice: filing something under a new tag while dropping inbox is one
thought, not two. Type tags to add or remove, comma separated, or clear a
checkbox to drop a tag already on the selection without retyping its
name.
Both fields complete against the tag list MainWindow already holds for
the query completer. Completion is a guard against typing shoppping
beside shopping, never a whitelist: inventing a tag is the entire point,
so any valid name goes through whether or not it exists yet.
Tri-state checkboxes carry the multi-thread case, and are where the risk
is. A tag on some selected threads shows partially checked, and leaving
it alone changes nothing; the opposite reading would silently tag threads
the user never looked at. A tag already on every thread and left checked
is likewise not a change and is not sent as one.
Tag names are validated before anything is applied, through a free
function so the rules are testable on their own. Empty, a leading dash
(notmuch's CLI reads it as removal, making such a tag a trap), whitespace
and control characters are refused by name and reason. Nothing is applied
until the whole set passes, since the user cannot tell which half of a
partial change landed.
TagDialog is pure UI: handed the vocabulary and the current state,
returning two lists, contacting no worker. That is what lets its fifteen
tests run without a notmuch database. Integration is a single call to the
existing tagSelected(), so undo, the optimistic model update, the
combined multi-row query and the completer refresh for a brand-new tag
all come for free.
One test assumption was wrong and the code was right: a case asserted
that QStringLiteral("null\0byte") truncates at the null and reads as
empty. It does not, so the null is caught as a control character. The
test was corrected rather than the validator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Tagging changes the notmuch index at once, but the mail store only hears
about it on the next sync, and nothing said so. Quitting with tagging
outstanding was silent. Items 18 and 19 of the usability backlog, built
together because the second needs the first's counter.
The counter cannot be QUndoStack::isClean(), which is the obvious
candidate and the wrong one: the undo stack is cleared on every query,
since its entries refer to rows the new result set discards. Tag a
thread, run any query, and the stack is empty while the change is still
unsynced. m_pendingEdits is its own count, incremented where a write is
CONFIRMED rather than where one is sent, so an optimistic update the
worker later rejects cannot leave the indicator claiming an edit that
never landed. Only a successful sync resets it: clearing on failure would
assert the changes had reached the mail store when the sync is exactly
what failed to put them there.
It is shown in the status bar, hidden entirely at zero, and described as
a lower bound rather than a guarantee, since an external notmuch run can
carry changes over without this application noticing.
On exit, sync_on_exit in [general] takes ask, always or never. Three
values rather than a bool because "prompt me", "just do it" and "do
nothing" are three behaviours and true/false expresses two; an unknown
value warns by name, since a typo there silently changes what happens to
unsynced work. The prompt offers three buttons for the same reason: a
user who hit Quit by mistake needs a way back that is not "sync". A sync
started at exit holds the window open until it finishes rather than being
killed mid-run, and a sync that FAILS does not quit, because quitting
there would discard the user's choice silently. With no sync command
configured the prompt degrades to a plain warning instead of offering a
sync that cannot run.
This is not a destructive-action confirmation of the kind CLAUDE.md
forbids. Those cover tag mutations, which keep undo instead of a dialog.
This asks about losing work at the one point where undo cannot help.
The tagsApplied lambda became a named slot, which is better structure and
also what lets a test drive it: the worker is deliberately parentless
because it moves to its own thread, so reaching it with findChild to emit
the real signal cannot work, and contorting the test to try was the wrong
instinct. Testing a modal needed its own care. A test that sends a close
event hangs forever if an unexpected dialog opens, because the modal
spins its own event loop; CloseProbe polls for activeModalWidget, closes
it and records that one appeared, turning "a dialog opened" into an
assertion rather than a hang.
Also removes a stray qDebug left in the open_thread action by the earlier
Enter-key investigation, which had reached two commits.
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>
|
|
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>
|
|
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.
|
|
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 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>
|
|
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.
|
|
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.
|
|
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>
|