Skip to content

feat(clipboard): clear only the filtered entries - #3235

Merged
bbedward merged 4 commits into
duxlabs-dev:masterfrom
Mario-Mohar:feat/clipboard-clear-filtered
Aug 31, 2026
Merged

bbedward merged 4 commits into
duxlabs-dev:masterfrom
Mario-Mohar:feat/clipboard-clear-filtered

Conversation

@Mario-Mohar

@Mario-Mohar Mario-Mohar commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

The clear button in the clipboard modal deleted every unpinned entry regardless of the active search query or type filter, so pruning a subset meant deleting entries one at a time from the context menu (#3198).

Behaviour

  • When the recents list is filtered — search text, or a type filter other than all — the button clears exactly the entries the list is showing, and the modal stays open on that filter so the next one can be worked through. Shift+Del takes the same path.
  • With no filter, nothing changes: all unpinned entries, pinned kept, modal closes.
  • A filter that matches nothing does nothing. Without that guard, filtering to zero results and hitting clear would have fallen back to wiping the whole history — the sharpest edge in this change.
  • The tooltip reads "Clear Filtered" while a filter is active, and the confirmation names the count.
  • The saved tab and the bar widget's context-menu entry are untouched; both still clear everything, since neither is showing a filtered list.

Why a new IPC method. Deleting the filtered ids one at a time would cost a round trip and a full clipboard state broadcast per entry. clipboard.deleteEntries does it in one bolt transaction with a single updateState/notifySubscribers, and returns how many rows it removed. It skips pinned entries server-side: the UI never sends a pinned id, but a bulk delete should not be able to take out saved items.

New strings: Clear Filtered and the confirmation sentence. The term freeze is currently inactive (term_freeze.json absent) and check_term_variants.py reports no case or punctuation duplicates against the existing catalog. Catalogs are left alone per CONTRIBUTING.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Refactor / internal cleanup
  • Documentation
  • Other

Related issues

Fixes #3198

Screenshots / video

Six entries, one pinned.

Filtered to TREFFER — two of six match:

filtered list

The confirmation names the count:

confirmation dialog

After confirming, the modal stays open on the filter and the other four entries survive:

after clearing

With no filter the dialog is unchanged:

unfiltered dialog

Checklist

  • My code follows the conventions in CONTRIBUTING.md
  • I have tested my changes locally
  • New user-facing strings are wrapped in I18n.tr(), reusing existing terms where possible (the dialog title reuses Clear History?)
  • Go changes: ran make fmt, added tests, make test passes, go mod tidy is clean
  • QML changes: linted, no new warnings
  • I have opened a corresponding pull request in dlx-docs — happy to, if you want it: the clipboard page in dlx-docs documents the dms cl CLI, which this PR does not change, and the modal's clear button is not described there today. Say the word and I will add a section.

How this was verified

Six new Go tests in internal/server/clipboard: deletes the requested ids and keeps the rest, skips pinned, ignores unknown ids, empty list is a no-op, the handler reports the deleted count, and five malformed ids shapes are rejected without touching the store. go test ./... passes for the whole core.

End to end: the built dms running against this working tree in a nested Hyprland session on its own XDG dirs. Populated six entries, pinned one, filtered, cleared, and checked the result with dms cl history — the two matching entries gone, the other four including the pinned one intact. Then filtered to a term that only the pinned entry matches and pressed Shift+Del: no dialog, nothing deleted. Then cleared with no filter: only the pinned entry left, modal closed, as before.

make lint-qml needs the Quickshell tooling VFS (.qmlls.ini), which this machine does not generate outside a full DMS install, so the touched QML was checked with Qt 6 qmllint directly — no errors beyond the pre-existing unresolved-import warnings.

With a search query or type filter active, the clear button deleted
every unpinned entry regardless of what the list was showing, so
pruning a subset meant deleting entries one by one from the context
menu.

When the recents list is filtered, the button now clears exactly the
entries it shows and the modal stays open on that filter; with no
filter the previous behaviour is untouched. A filter that matches
nothing does nothing at all, rather than falling back to clearing
everything. The tooltip says "Clear Filtered" while a filter is
active, and the confirmation names the count.

Deleting one id at a time would have cost a round trip and a full
state broadcast per entry, so this adds clipboard.deleteEntries: one
bolt transaction, one state update, and it skips pinned entries
server-side so a bulk delete can never take out saved items.

Fixes duxlabs-dev#3198
@hthienloc

Copy link
Copy Markdown
Member

/claude review

iconSize: Theme.iconSize
iconColor: Theme.surfaceText
tooltipText: I18n.tr("Clear All")
tooltipText: header.clearsFilteredOnly ? I18n.tr("Clear Filtered") : I18n.tr("Clear All")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New user-facing strings need translator context per CONTRIBUTING (I18n.tr("...", "<context>")). Both strings this PR adds are missing it: "Clear Filtered" here, and "This will delete the %1 entries matching the current filter. Pinned entries are kept." in ClipboardHistoryContent.qml:150. Without context POEditor shows translators the bare string, and "Clear Filtered" is ambiguous out of context (verb vs. adjective).

Suggested change
tooltipText: header.clearsFilteredOnly ? I18n.tr("Clear Filtered") : I18n.tr("Clear All")
tooltipText: header.clearsFilteredOnly ? I18n.tr("Clear Filtered", "Clipboard history clear button tooltip when a search or type filter is active") : I18n.tr("Clear All")

handleGetEntry(conn, req, m)
case "clipboard.deleteEntry":
handleDeleteEntry(conn, req, m)
case "clipboard.deleteEntries":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new method isn't listed in the IPC help output. core/internal/server/server.go enumerates every clipboard method (clipboard.getState … clipboard.subscribe, around line 1496-1506); clipboard.deleteEntries should be added there next to clipboard.deleteEntry, otherwise dms ipc help silently omits it.

log.Info(" clipboard.deleteEntries               - Delete multiple entries by ID, skipping pinned (params: ids)")

@claude

claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude review

Solid change — the server-side pinned skip, the empty-filter guard, and the single-transaction bulk delete are all the right calls. Two minor points.

  • New I18n.tr() strings missing translator context, quickshell/Modals/Clipboard/ClipboardHeader.qml:71 (also ClipboardHistoryContent.qml:150)
  • clipboard.deleteEntries not added to the IPC help listing in core/internal/server/server.go, core/internal/server/clipboard/handlers.go:24

Checked: DeleteEntries transaction/pinned handling and toEntryID validation, clearFiltered id collection and post-clear refresh, filterActive/clearsFilteredOnly binding chain through ClipboardContent → ClipboardHeader, Shift+Del path, and that the saved tab and bar popout still clear everything. Model: claude-opus-5.

…help

Review follow-up.

The two strings this branch adds carried no translator context: the
"Clear Filtered" tooltip in ClipboardHeader and the confirmation body for
a filtered clear in ClipboardHistoryContent. Both now say where they
appear, and the second one names what %1 is, per CONTRIBUTING.

"Clear All" on the same line is deliberately left alone. It is an
existing shared term with four other call sites, so a context there would
attach a clipboard-specific comment to a term the printer tab also uses.

clipboard.deleteEntries was handled but absent from the IPC method
listing that `dms debug-srv` prints, which left it undiscoverable right
next to clipboard.deleteEntry.

Catalogs untouched.
@Mario-Mohar

Copy link
Copy Markdown
Contributor Author

Both points are addressed in 21cefbf.

Translator context — added to the two strings this PR introduces, in the style CONTRIBUTING documents:

  • ClipboardHeader.qml:71 → I18n.tr("Clear Filtered", "clipboard modal: clear button tooltip while a search filter is active")
  • ClipboardHistoryContent.qml:150 → I18n.tr("This will delete the %1 entries matching the current filter. Pinned entries are kept.", "clipboard modal: clear confirmation while a search filter is active, %1 is the number of matching entries")

I left "Clear All" on that same ternary without a context on purpose: it is an existing shared term with four other call sites (ClipboardHistoryModal, ClipboardHistoryPopout, ClipboardButton, and Settings/PrinterTab), so a clipboard-specific comment there would end up attached to the term the printer tab uses too. Happy to add a neutral one if you would rather every term in the file carry a context.

IPC help listing — clipboard.deleteEntries is now in the listing in server.go, next to deleteEntry:

 clipboard.deleteEntries               - Delete entries by ID, skipping pinned ones (params: ids)

That is the only static method listing I found; printIPCHelp() in cmd/dms/shell.go enumerates Quickshell IPC targets at runtime and does not need an entry.

Verified: gofmt clean, go build ./..., go vet ./internal/server/..., go test -count=1 ./internal/server/clipboard/... pass, go mod tidy is a no-op, and the i18n-term-freeze / i18n-term-variants / no-console-in-qml hooks pass. I ran extract_translations.py only to confirm both new terms pick up their comment and then reverted the catalogs, so en.json and template.json are untouched.

@bbedward

Copy link
Copy Markdown
Member

/claude review

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Claude review

No issues found. The two points from the previous run (translator context on the new strings, clipboard.deleteEntries in the IPC help listing) are both addressed in 21cefbf.

Checked: DeleteEntries bolt transaction (pinned skip, unknown-id skip, deleted > 0 gate on updateState/notifySubscribers, consistency with DeleteEntry/ClearHistory which likewise do no extra cleanup); toEntryID covers the float64/json.Number shapes models.Request.Params actually produces and rejects negative/fractional/wrong-type ids before touching the store; clearFiltered collects only unpinned filtered ids, guards the empty list, and refresh() plus the onClipboardStateUpdate push both preserve searchText/activeFilter so the modal stays on the filter; filterActive → clearsFilteredOnly → ClipboardContent → ClipboardHeader binding chain is reactive and ClipboardContent has a single instantiation site (modal: root); I18n.tr(term, context) comment-only context does not affect lookup (I18n._lookup falls through); Shift+Del routes through the same confirmClearAll; the saved tab (activeTab === "recents" guard) and the bar popout clearAll() still clear everything; en.json/template.json untouched and "Clear Filtered" is genuinely a new term. Model: claude-opus-5.

The lint-and-test failure on 21cefbf is an unrelated flake:
TestStoreEntry_CorruptDBReturnsErrorNotPanic died with SIGBUS in the
bbolt mmap. That test comes from master (c68b7e5) and passes there;
this branch only adds TestDeleteEntries_*/TestHandleDeleteEntries_*
and touches manager.go additively.
@Mario-Mohar

Copy link
Copy Markdown
Contributor Author

@bbedward heads up on the red lint-and-test here — and a correction to my own chore: re-trigger CI commit message, where I called it an unrelated flake. That was too quick. It reproduced on 0fb9cc1e, and in the last 30 Go CI runs the only two failures are both on this branch. So the link to this PR is real, even though the clipboard code isn't what's broken.

What dies is TestStoreEntry_CorruptDBReturnsErrorNotPanic (added in c68b7e5), and it dies with SIGBUS rather than a recovered panic:

=== RUN   TestStoreEntry_CorruptDBReturnsErrorNotPanic
unexpected fault address 0x7efcd0d48000
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x7efcd0d48000]

corruptInlineBucketRoot deliberately writes an out-of-range pgid — bbolt reports it as Page expected to be: 16, but self identifies as 282584257676671. bbolt recovers from its own assertion panics, but it can't recover from a fault when that offset lands on a page inside the mapping that has no file behind it. That's a SIGBUS, and the Go runtime turns it into a fatal error that takes the whole test binary with it.

Which means whether it faults is a property of the process memory layout, not of the code under test. Two things point that way: the bogus pgid is byte-identical across runs (282584257676671), while the fault address is not — 0x7f56ceb73000 on the first run, 0x7efcd0d48000 on the second. It tracks the mmap base, i.e. ASLR.

This PR inserts six test functions into manager_test.go at line 345, ahead of the corrupt-DB test in the same binary, which is enough to shift that layout. So I don't think this is my branch being special — the test looks latently unsafe on master, and any PR that adds tests to this package before it can trip the same way.

For what it's worth, the tests this PR adds all pass; the binary dies further down the run, after they've gone green.

Happy to send a separate PR against master for it. Options as I see them:

  • have corruptInlineBucketRoot write a bogus-but-in-bounds pgid, so the failure stays an assertion panic bbolt can actually recover from
  • run that one test in a subprocess and assert on the exit status
  • drop it and lean on the TestOpenDB_Heals* cases, which already cover the salvage path without dereferencing a wild pointer

First option is the smallest and still covers what #3232 was about. Tell me which you'd rather have and I'll open it.

@bbedward
bbedward merged commit 9eaaed3 into duxlabs-dev:master Aug 31, 2026
3 checks passed
@Mario-Mohar
Mario-Mohar deleted the feat/clipboard-clear-filtered branch September 2, 2026 13:21
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.

Clear filtered clipboard entries (not just all)

3 participants