diff --git a/README.md b/README.md index fbccfae..fd825a8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The project is intentionally dependency-light: Swift Package Manager, AppKit, Ca - `Command + 1` through `Command + 9` paste the numbered visible card; add `Shift` to paste that card as plain text - `Command + G` shows a filtered result back in the full clipboard history - `Shift + Command + N` creates a new collection + - `Space` previews the selected card when the focused search field is empty - Clipboard history for text, URLs with local preview thumbnails when available, images, audio, RTF/HTML rich text, PDFs, and file references - SQLite persistence with bounded history, pinned-item retention, and encrypted app-managed payloads - Search with independent token matching, structured filters such as `app:Safari`, `type:image,pdf`, `pinboard:"Client Work","Read Later"`, `date:2026-06-30`, result jump-back to full history, and optional local OCR for copied images diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index f355e92..dadac23 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -34,20 +34,21 @@ Use this checklist before a release or after changes to panel, pasteboard, setti 1. Open the panel and confirm the search field is focused. 2. Type a query and confirm results filter immediately. 3. Type a structured query such as `pinboard:"Client Work","Read Later" type:image,pdf` and confirm only clips from those collections and content types remain. -4. Use arrow keys to move selection while the search field is focused. -5. Right-click a filtered result and choose Show in Clipboard, or press `Command + G`, and confirm search clears while the same card stays selected in Most Recent. -6. Press `Esc` once with a non-empty search field and confirm search clears. -7. Press `Esc` again and confirm the panel closes. -8. Reopen the panel, change sort segments, and confirm each segment updates results. -9. Press `Shift + Command + N` or the collection rail `+`, enter `Client Work`, choose a color, and confirm a Client Work chip appears with 0 clips and an empty collection view. -10. Return to Clipboard, select a card, use its Collect button to choose Client Work, and confirm the Client Work chip count increases. -11. Select the Client Work chip and confirm the rail filters to assigned items, cards use the Client Work name/color in their headers, and the collection/color/assignment persists after quitting and reopening ClipBored. -12. Right-click the Client Work chip, choose Edit Collection..., rename it, change its color, and confirm the chip and assigned card headers update. -13. Right-click a media, file, link, PDF, audio, or text card, choose Rename..., give it a title, and confirm the card title and search results use the custom title while paste/copy still uses the original payload. -14. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry. -15. Right-click a card, use Capture Rules to ignore its source app, copy from that app again, and confirm the new item is skipped. -16. Drag an unassigned card onto the renamed collection chip and confirm the chip count increases and the card appears when that collection is selected. -17. Resize or test on a narrow display and confirm the bottom shelf switches to compact cards that still show two recent clips cleanly. +4. Clear the search field, press `Space`, and confirm the selected previewable clip opens in Quick Look instead of inserting a blank query. +5. Use arrow keys to move selection while the search field is focused. +6. Right-click a filtered result and choose Show in Clipboard, or press `Command + G`, and confirm search clears while the same card stays selected in Most Recent. +7. Press `Esc` once with a non-empty search field and confirm search clears. +8. Press `Esc` again and confirm the panel closes. +9. Reopen the panel, change sort segments, and confirm each segment updates results. +10. Press `Shift + Command + N` or the collection rail `+`, enter `Client Work`, choose a color, and confirm a Client Work chip appears with 0 clips and an empty collection view. +11. Return to Clipboard, select a card, use its Collect button to choose Client Work, and confirm the Client Work chip count increases. +12. Select the Client Work chip and confirm the rail filters to assigned items, cards use the Client Work name/color in their headers, and the collection/color/assignment persists after quitting and reopening ClipBored. +13. Right-click the Client Work chip, choose Edit Collection..., rename it, change its color, and confirm the chip and assigned card headers update. +14. Right-click a media, file, link, PDF, audio, or text card, choose Rename..., give it a title, and confirm the card title and search results use the custom title while paste/copy still uses the original payload. +15. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry. +16. Right-click a card, use Capture Rules to ignore its source app, copy from that app again, and confirm the new item is skipped. +17. Drag an unassigned card onto the renamed collection chip and confirm the chip count increases and the card appears when that collection is selected. +18. Resize or test on a narrow display and confirm the bottom shelf switches to compact cards that still show two recent clips cleanly. ## Copy And Paste diff --git a/sources/clipbored/views/ClipboardPanelController.swift b/sources/clipbored/views/ClipboardPanelController.swift index 0f08b8e..1999069 100644 --- a/sources/clipbored/views/ClipboardPanelController.swift +++ b/sources/clipbored/views/ClipboardPanelController.swift @@ -339,6 +339,16 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel removeKeyMonitor() keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in guard let self else { return event } + if self.shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: true), + self.panelView.isSearchFieldEditing, + Self.searchFieldPreviewShortcut( + forKeyCode: event.keyCode, + modifiers: event.modifierFlags, + searchText: self.panelView.searchTextForKeyboardShortcut + ) { + self.previewSelected() + return nil + } if self.shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: true), let index = Self.quickPasteIndex(forKeyCode: event.keyCode, modifiers: event.modifierFlags) { self.viewModel.pasteItem(at: index) @@ -469,6 +479,11 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel return collectionShortcuts[keyCode] } + static func searchFieldPreviewShortcut(forKeyCode keyCode: UInt16, modifiers: NSEvent.ModifierFlags, searchText: String) -> Bool { + let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask) + return keyCode == 49 && relevantModifiers.isEmpty && searchText.clipboardTrimmed.isEmpty + } + static func commandShortcutAction(forKeyCode keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> ClipboardPanelShortcutAction? { let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask) guard relevantModifiers == .command else { return nil } diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index 6d3dc33..45f699f 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -1102,6 +1102,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate { return false } + var searchTextForKeyboardShortcut: String { + searchField.stringValue + } + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { return true } diff --git a/tests/clipboredtests/ClipboardPanelControllerTests.swift b/tests/clipboredtests/ClipboardPanelControllerTests.swift index 8cbdfb4..a84737d 100644 --- a/tests/clipboredtests/ClipboardPanelControllerTests.swift +++ b/tests/clipboredtests/ClipboardPanelControllerTests.swift @@ -164,6 +164,14 @@ final class ClipboardPanelControllerTests: XCTestCase { XCTAssertNil(ClipboardPanelController.collectionShortcutMode(forKeyCode: 29, modifiers: .command)) } + func testSearchFieldSpacePreviewShortcutRequiresEmptySearchAndNoModifiers() { + XCTAssertTrue(ClipboardPanelController.searchFieldPreviewShortcut(forKeyCode: 49, modifiers: [], searchText: "")) + XCTAssertTrue(ClipboardPanelController.searchFieldPreviewShortcut(forKeyCode: 49, modifiers: [], searchText: " ")) + XCTAssertFalse(ClipboardPanelController.searchFieldPreviewShortcut(forKeyCode: 49, modifiers: [], searchText: "release note")) + XCTAssertFalse(ClipboardPanelController.searchFieldPreviewShortcut(forKeyCode: 49, modifiers: .command, searchText: "")) + XCTAssertFalse(ClipboardPanelController.searchFieldPreviewShortcut(forKeyCode: 36, modifiers: [], searchText: "")) + } + func testCommandActionShortcutsMapToSelectedClipActions() { XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 8, modifiers: .command), .copy) XCTAssertEqual(ClipboardPanelController.commandShortcutAction(forKeyCode: 5, modifiers: .command), .showInClipboard)