summaryrefslogtreecommitdiffstats
path: root/src/threadlistmodel.cpp
AgeCommit message (Collapse)AuthorFilesLines
2026-08-09feat(ui): icons on every action, and rename Flag to ImportantDanilo M.1-1/+4
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>
2026-08-07feat(sync): sync only the accounts with unsynced editsDanilo M.1-0/+18
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>
2026-08-07feat(ui): show each thread's tags under its rowDanilo M.1-14/+83
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>
2026-08-07fix(ui): tell read threads from unread without relying on boldDanilo M.1-0/+46
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>
2026-08-04feat: show a paperclip for threads with attachmentsDanilo M.1-0/+35
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>
2026-08-04feat: render tags as chips instead of a text columnDanilo M.1-3/+20
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.
2026-08-04feat: show that a tag action landedDanilo M.1-3/+43
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.
2026-08-04Add GPLv2-only license and per-file headersDanilo M.1-0/+18
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>
2026-08-04feat: add ThreadListModel with batch appendDanilo M.1-0/+124
QAbstractTableModel over query results, appended in batches so a large query paints its first screenful immediately. Tag changes apply locally for optimistic UI; reverting a failed write means calling applyTagChange again with added and removed swapped, which the round-trip test pins. Two additions to the drafted version: - A ThreadIdRole, so a view's QModelIndex maps back to the thread id the worker speaks without every caller reaching around the model. - data() checks its own row and column bounds. Qt will not hand out an out-of-range index and invalidates persistent ones on reset, so this is unreachable defence rather than a live path; the test says so instead of pretending to cover it. Verified by mutation that the empty-batch guard, the ThreadIdRole, and the full-row dataChanged range each fail exactly one test when removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>