summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
AgeCommit message (Collapse)AuthorFilesLines
6 hoursfeat(about): custom About dialog with icon, copyright and project linkDanilo M.1-7/+46
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.
6 hoursfeat: show a paperclip for threads with attachmentsDanilo M.1-1/+18
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>
6 hoursfeat: choose the startup query by nameDanilo M.1-2/+6
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>
6 hoursfeat: own the message-pane zoom and persist itDanilo M.1-0/+38
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>
6 hoursfeat: persist window, splitter and column widthsDanilo M.1-5/+71
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>
6 hoursfeat: let an account set its chip colour and labelDanilo M.1-2/+48
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.
6 hoursfeat: make every thread list column resizableDanilo M.1-7/+10
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.
6 hoursfix: let the thread list columns be resized againDanilo M.1-4/+9
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.
6 hoursfeat: show that a tag action landedDanilo M.1-0/+8
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.
6 hoursfix: keep the shortcut reference inside the screenDanilo M.1-12/+45
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.
6 hoursfeat: add menus, a toolbar and a shortcut referenceDanilo M.1-81/+179
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.
6 hoursAdd GPLv2-only license and per-file headersDanilo M.1-1/+20
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>
6 hoursfix: let key bindings win over the thread list's type-to-searchDanilo M.1-0/+9
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>
6 hoursfix: only interrupt startup for real configuration problemsDanilo M.1-4/+14
Found while walking the task 13 checklist against real mail. Item 1 ("startup shows no configuration warnings with a valid config") failed: with a perfectly valid config that simply had no [sync] command, every launch opened a blocking modal that had to be dismissed before the window could be used. Config now separates the two cases. A problem is something configured but wrong (a sync command that does not exist, an account with no maildir); those still open a dialog, as does every KeyMap warning, since each one means a binding the user wrote is being ignored. A notice is an optional feature simply not being configured; it reports to the status bar only. Nothing is broken in that case, and a modal on every launch teaches the user to dismiss dialogs unread, which defeats the ones that matter. problems() is a subset of warnings(), so callers wanting everything need only the latter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6 hoursfeat: add MainWindow wiring query, list, message, and syncDanilo M.1-0/+495
Wires the worker thread, thread list, message pane, sync process and undo stack together, and replaces the placeholder main() with real startup: custom URL schemes registered before QApplication, a libnotmuch ABI check, and config loading. Four fixes against the drafted version: - onWorkerError() only set a status label. Its own comment elsewhere claimed it reverted the optimistic update, and the spec requires that; it did not, so a rejected write left the list showing a tag the database never received. The pending change is now recorded and rolled back, and a confirmed tagsApplied clears it so a later unrelated error cannot undo a write that succeeded. - runCurrentQuery() cleared the model but left the undo stack pointing at rows that no longer exist. Undoing after a new query would have written to the database while the visible list stayed put. The stack is cleared with the model. - m_currentMessages was assigned on every thread load and never read. Removed. - buildUi() connected sync output to m_syncLog and errors to m_statusLabel before either existed. Both are constructed before the wiring now. cidPrefix generation lives here, this being its only producer in the application, and is pinned by tests: it must never contain '!' and must be distinct per message, which are the invariants the cid: namespacing rests on. A second test holds registeredActionNames() against KeyMap::knownActions(), since those two hand-maintained lists drifting either way silently breaks a user's key binding. Mutation-verified that dropping an action fails the test by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>