Skip to content

Select several inbox messages with shift+arrows and mark them read - #39

Draft
williammartin wants to merge 2 commits into
maxbeizer:mainfrom
williammartin:williammartin-mail-multiselect-mark-read
Draft

williammartin wants to merge 2 commits into
maxbeizer:mainfrom
williammartin:williammartin-mail-multiselect-mark-read

Conversation

@williammartin

@williammartin williammartin commented Aug 4, 2026

Copy link
Copy Markdown

Reviewer Notes

You better believe this was vibe coded and I haven't read anything. It does seem to work in my local jamming. Unfortunately, it's not that useful for me without server side mark as read, but might be a useful feature for something else?

On place I found an issue just now:

Given I have used shift+down to select an item in multi select mode
When I press shift+up
Then it should unselect that item

Right now it doesn't unselect at all. I'm also not sure exactly on the behaviour of shift+modifier in this mode - it selects the existing item not the one you're navigating to. That feels wrong.

I'm happy to work on these if you want this feature but otherwise I'll just leave it here as an artifact.

Why

Clearing a morning inbox is one message at a time: move, r, move, r. There is no way to say "these five, mark them all read".

What

shift+↑ / shift+↓ select messages in the interactive inbox, and r marks the whole selection read.

Shift is a momentary modifier, not a mode. Each shift-arrow selects the message the cursor is on and then moves, so holding shift and repeating an arrow builds up a run. A plain arrow moves without selecting, which passes over a message and leaves it out while keeping everything selected so far — so a non-contiguous set needs no extra keys.

Selecting four inbox messages with shift and arrow keys, passing over one, then marking the selection read.

In the recording: four messages are selected, "Workplace Team" is deliberately passed over with a plain , and r marks the four read while the skipped message keeps its orange NEW marker.

Selected rows carry a and the header shows the count. r clears the selection after applying; esc, tab, and R clear it without acting; archiving prunes the archived message from it. With nothing selected, r still toggles the current row exactly as before. J / K are bound to the same actions for terminals that do not send modified arrow keys.

Read state stays local to the running TUI, matching the existing r binding — the WorkIQ broker exposes no mark-as-read action, so there is nothing to write back to Microsoft 365. Bulk mark-as-read against Graph would need PATCH /me/messages/{id}, and do_action is POST-only.

Also fixed

Recording the demo surfaced rows wrapping onto a second line, dropping the received time onto its own line and breaking the column alignment. Rows were sized to listWidth() — the same width handed to the panel style — but Lip Gloss counts a style's horizontal padding inside that width, so a full-width row was two columns too wide and the panel wrapped it. contentWidth() now reserves those columns for everything drawn inside the panel.

This predates the feature (the new mark gutter does not change row totals), but it is the exact width math this PR touches, so it is fixed here rather than left to make the recording look broken.

Acceptance criteria

Each of these is an executable test in internal/tui/multiselect_test.go.

# Given When Then
1 the inbox is loaded and the cursor is on message 1 I press shift+↓ message 1 is selected and the cursor is on message 2
2 the cursor is on message 3 and nothing is selected I press shift+↑ message 3 is selected and the cursor is on message 2
3 messages 1 and 2 are selected and the cursor is on message 3 I press then shift+↓ messages 1, 2 and 4 are selected, message 3 is not, and the cursor is on message 5
4 messages 1 and 3 are selected I press twice messages 1 and 3 remain selected and nothing further is selected
5 the cursor is on the last message I press shift+↓ the last message is selected and the cursor stays put
6 the cursor is on the first message I press shift+↑ the first message is selected and the cursor stays put
7 message 1 is already selected and the cursor is on message 1 I press shift+↓ message 1 is selected exactly once and the cursor is on message 2
8 messages 1 and 3 are selected and both are unread I press r both are read, the status reads marked 2 as read, and the selection is empty
9 message 1 is unread, message 3 is already read, both selected I press r both messages are read (a set, not a toggle)
10 nothing is selected and the cursor is on an unread message I press r that message is read and no other message changes
11 messages 1 and 2 are selected I press esc nothing is selected and no read state changed
12 messages 1 and 2 are selected I press tab to calendar and tab back nothing is selected
13 messages 1 and 2 are selected I press R nothing is selected
14 messages 1 and 2 are selected and the cursor is on message 1 I press a and the archive completes message 1 is gone and only message 2 remains selected
15 messages 1 and 3 are selected the inbox renders rows 1 and 3 show the mark glyph, row 2 does not, and the title includes 2 selected
16 the calendar view is active I press shift+↓ the cursor does not move and no selection is created
17 the recorded demo it plays a selection is built with a skipped message, r is applied, the selected messages lose their NEW marker, and the skipped message keeps its own

Notes for the reviewer

The recording is scripted through expect, not VHS keystrokes. VHS 0.11 cannot send shift-modified arrows: Shift+Down is a parse error, and the Escape + Type "[1;2B" workaround arrives as six separate keystrokes that Bubble Tea parses individually. I confirmed both failure modes with a throwaway key probe before settling on this. docs/multiselect.tape therefore runs docs/multiselect.exp, which spawns the TUI in a pty and writes the raw CSI bytes, so the recording exercises the real bindings rather than a stand-in. The script uses an expect-based pause instead of sleep, because sleep stops draining the pty and freezes the TUI mid-frame.

williammartin and others added 2 commits August 4, 2026 11:09
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Rows inside the list panel were sized to listWidth(), the same width the
panel style is given. Lip Gloss counts a style's horizontal padding inside
that width, so a row that filled its budget was two columns too wide and got
wrapped, dropping the received time onto a line of its own. contentWidth()
now reserves those two columns for everything drawn inside the panel.

Found while recording the multi-select demo; the scripts that produce that
recording live in the pull request description rather than the tree.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@williammartin
williammartin force-pushed the williammartin-mail-multiselect-mark-read branch from b42a5af to 3cb6f25 Compare August 4, 2026 09:16
@williammartin
williammartin marked this pull request as ready for review August 4, 2026 09:19
@williammartin
williammartin marked this pull request as draft August 4, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant