| Age | Commit message (Collapse) | Author | Files | Lines |
|
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>
|
|
The empty message pane now carries the application's own identity and the
counts worth knowing, and Escape finally does what it does everywhere else.
Collects the placeholder pane, the Maildir overview, the Escape change,
and the thread-list work that had been sitting unreleased since 0.10.0.
|
|
The thread list was uniform and cramped: every row one line tall, with
nothing to say what a thread was about before opening it. Rows are now
roughly double height, carrying a strip of tag chips beneath the text,
with alternating row colours and a star column for flagged threads
beside the existing paperclip.
The strip is painted by the VIEW rather than by a delegate, which is
why ThreadListView exists. A delegate is handed one cell's rectangle
and cannot paint outside its column, so a strip drawn from the subject
column stops at that column's edge, losing the last tags of a
well-tagged thread, and starts at its left edge, putting the chips
under the subject instead of under the row.
Tags the row already shows another way are left out: inbox as
structure, unread as the dimming, flagged as the star, attachment as
the paperclip, and the account as the chip in the subject cell. Sorted,
since notmuch's order is not guaranteed stable and a row whose chips
reordered between repaints would flicker.
Six defects were introduced and fixed on the way here, all of them one
consequence: a QTableView paints per cell, and a row-wide strip is not
a cell. SubjectDelegate installed view-wide drew the account chip into
every column, since AccountLabelRole belongs to the row; it is split
into RowStyleDelegate for every column and SubjectDelegate for the
subject alone, with a Q_ASSERT guarding that. Row height returned from
sizeHint did nothing, because a table takes one height per row. The
strip painted from x=0 over the marker columns, via a protected
viewportMargins() that returns 0. Measuring the text band and the strip
with one font put the pills over the date. Alternating colours and the
selection are per-cell too, so the band showed bare viewport background
until the view filled it, honouring the model's own BackgroundRole
first so a deleted row is not cut in half. And that fill spanned the
full width, cutting the centred marker glyphs at their midpoint.
Closes item 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Bold was unread's only cue, and it renders identically to regular on the
user's system: confirmed by eye against a bare QTableView holding a
plain QStandardItemModel, with no code from this project involved. The
fault is in Qt or fontconfig, below this application, and nothing in the
model could ever have reached it. Read and unread mail looked exactly
alike.
The emphasis is inverted instead. Unread rows keep the palette's own
text colour and read rows are dimmed toward the background, so the cue
rides on Qt::ForegroundRole, which the delegate already honours, and
costs no column. It also suits the real ratio, measured at 99 unread
against 4220 read: dimming the bulk is calmer than highlighting it. The
dim colour is derived from the palette, never hardcoded, per the rule
item 12 established. Bold is kept for systems where it works, but
nothing depends on it now.
That exposed a second defect, visible the moment it shipped. Qt resolves
ForegroundRole into the palette and then prefers it over
HighlightedText, so a model-supplied colour wins on a SELECTED row too.
The dim is blended against the unselected background, so a selected read
row painted grey on the selection colour, near unreadable.
SubjectDelegate::initStyleOption now reverses that, and the delegate is
installed view-wide rather than on the subject column alone, so every
column gets the same handling instead of three of them keeping Qt's
ordering.
The guarding tests state the property rather than the mechanism: strip
the font from the model's answer and the two states must still differ.
A test asserting only that bold is set passes on a system where bold
paints like regular, which is exactly how this survived. The selection
test renders two rows identical but for the unread tag, selects both,
and requires zero differing pixels.
Part of item 5; the density work and the star column remain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The stylesheet hardcoded #bbb, #555, #000, #666, #ddd and #4a6f8a, and
set no background at all, so on a dark desktop plain-text mail rendered
as black on white inside a dark window and the web view's own default
showed through.
The colours now come from a Palette struct derived from QPalette, and
are passed into the builder rather than read from qApp inside it, so
the stylesheet can be tested against a known palette with no running
application. Base and Text rather than Window and WindowText: the pane
is a content surface like a text edit, and on many themes those differ.
The secondary colours are blends of text and background, not fixed
greys. That is the part that makes it work both ways round, since a
#555 chosen to read as subtle on white is nearly invisible on #2b2b2b.
The quote colour keeps its hue, because "this is quoted" is carried by
being a different colour rather than a dimmer one, but is pulled toward
the background so it stays readable instead of glowing on dark.
A sender's own HTML is deliberately left alone, and a test asserts that
so it cannot drift: rewriting a sender's styling would break layouts
that depend on it, and a newsletter setting a white background is
entitled to stay white. This themes the plain-text render and the
chrome around messages, nothing more.
MessageView passes its own widget palette rather than the
application's, since a style sheet or a themed parent can give the pane
different colours from qApp, and re-renders on PaletteChange: the
document's colours are baked into its stylesheet at build time, so
unlike a widget it does not restyle itself when the desktop theme
changes.
The load-bearing test asserts the negative, that no hex colour appears
in the style block which the palette did not supply. A test checking
only that the palette's colours are present passes with a leftover
literal still there, and one leftover literal is the whole defect.
Confirmed by mutation.
Closes item 12.
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>
|
|
Tag edits made during a background sync are held and sent when it finishes,
rather than stalling the worker and freezing the message pane. Sync is one
control on the toolbar instead of two that behaved differently, and the query
bar gains a clear button and the saved queries beside it.
Minor rather than patch: a visible control was removed, the query row was
rearranged, and when a tag edit reaches the database changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Corrections from using 0.8.0: net unsynced count, Delete as a toggle,
expiring status messages, Sync disabled during a background sync, Esc to
blank the pane, and a named default on the quit prompt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Multi-select discoverability and awareness of background syncs.
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>
|
|
Arbitrary tagging from the UI and the unsynced-edits work are both new
features, so this takes the minor slot.
The SlackBuild's MD5SUM stays at the previous value until the 0.7.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>
|
|
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>
|
|
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>
|
|
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>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
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.
|
|
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.
|
|
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>
|