aboutsummaryrefslogtreecommitdiff
path: root/menubar/search.swift
diff options
context:
space:
mode:
Diffstat (limited to 'menubar/search.swift')
-rw-r--r--menubar/search.swift40
1 files changed, 32 insertions, 8 deletions
diff --git a/menubar/search.swift b/menubar/search.swift
index 2bc044a..c62c612 100644
--- a/menubar/search.swift
+++ b/menubar/search.swift
@@ -157,9 +157,12 @@ struct SearchState: Equatable {
offset = max(0, min(offset, max(0, results.count - Self.visibleRows)))
}
+ var selectedRow: Row? { results.first { $0.row.paneId == selected }?.row }
+
var footer: String {
let count = results.count == rows.count ? "\(rows.count) agents" : "\(results.count) of \(rows.count)"
- return "\(count) ↑↓ select ↩ jump esc close"
+ let unread = selectedRow?.canMarkUnread == true ? " ⌘U mark unread" : ""
+ return "\(count) ↑↓ select ↩ jump\(unread) esc close"
}
/// Printed by --print-search, and compared to skip needless re-renders.
@@ -213,12 +216,14 @@ final class SearchController: NSObject, NSWindowDelegate, NSTextFieldDelegate {
private let field = NSTextField()
private let list = NSStackView()
private let onJump: (String) -> Void
+ private let onMarkUnread: (String) -> Void
private var state = SearchState(rows: [])
private var rendered: String?
private var keyMonitor: Any?
- init(onJump: @escaping (String) -> Void) {
+ init(onJump: @escaping (String) -> Void, onMarkUnread: @escaping (String) -> Void = { _ in }) {
self.onJump = onJump
+ self.onMarkUnread = onMarkUnread
panel = KeyPanel(contentRect: NSRect(x: 0, y: 0, width: searchWidth, height: 80),
styleMask: [.nonactivatingPanel, .borderless], backing: .buffered, defer: false)
super.init()
@@ -321,6 +326,14 @@ final class SearchController: NSObject, NSWindowDelegate, NSTextFieldDelegate {
onJump(paneId)
}
+ /// ⌘U: mark the selected agent unread; search stays open, and the agent
+ /// moves to the done ones as the list refreshes.
+ private func markSelectionUnread() {
+ guard let row = state.selectedRow, row.canMarkUnread else { NSSound.beep(); return }
+ log("search: mark \(row.paneId) unread")
+ onMarkUnread(row.paneId)
+ }
+
/// Debug: render `query` over `rows` offscreen to a PNG.
func snapshot(rows: [Row], query: String, down: Int, to path: String) -> Bool {
state = SearchState(rows: rows)
@@ -335,12 +348,17 @@ final class SearchController: NSObject, NSWindowDelegate, NSTextFieldDelegate {
// MARK: keys
/// ⌃N ⌃P ⌃J ⌃K move, ⌃U clears: fzf's keys, before the text field's own
- /// Emacs bindings see them.
+ /// Emacs bindings see them. ⌘U marks the selected agent unread.
private func installKeyMonitor() {
guard keyMonitor == nil else { return }
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
- guard let self, event.window === self.panel,
- event.modifierFlags.intersection(.deviceIndependentFlagsMask) == .control else { return event }
+ guard let self, event.window === self.panel else { return event }
+ let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
+ if flags == .command, event.charactersIgnoringModifiers == "u" {
+ self.markSelectionUnread()
+ return nil
+ }
+ guard flags == .control else { return event }
switch event.charactersIgnoringModifiers {
case "n", "j": self.move(1)
case "p", "k": self.move(-1)
@@ -392,9 +410,9 @@ final class SearchController: NSObject, NSWindowDelegate, NSTextFieldDelegate {
}
for match in state.visible {
let view = PanelRowView(row: match.row, title: highlightedTitle(match),
- selected: match.row.paneId == state.selected) { [weak self] paneId in
- self?.jump(to: paneId)
- }
+ selected: match.row.paneId == state.selected,
+ onClick: { [weak self] paneId in self?.jump(to: paneId) },
+ onMarkUnread: { [weak self] paneId in self?.onMarkUnread(paneId) })
list.addArrangedSubview(view)
view.widthAnchor.constraint(equalTo: list.widthAnchor).isActive = true
}
@@ -488,5 +506,11 @@ func searchSelftest() -> [String] {
scroll.move(-12)
check(scroll.offset == 0, "state: and back to the top")
check(scroll.footer.hasPrefix("25 agents"), "footer: agent count")
+ let mixed = SearchState(rows: [row("%1", "a", .idle), row("%2", "b", .done)])
+ var onDone = mixed
+ onDone.move(1)
+ check(mixed.selectedRow?.paneId == "%1" && mixed.footer.contains("⌘U mark unread"),
+ "footer: ⌘U offered for an idle agent")
+ check(onDone.selectedRow?.paneId == "%2" && !onDone.footer.contains("⌘U"), "footer: not for a done one")
return failures
}