WIP: add card capture rules

This commit is contained in:
Akshay Kolli
2026-06-30 02:49:35 -07:00
parent 1b1ca3936c
commit a278675db0
6 changed files with 194 additions and 5 deletions

View File

@@ -595,6 +595,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
self?.viewModel.selectItem(at: selected)
self?.viewModel.assignSelected(to: collectionName)
}
card.onIgnoreSourceApp = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
self?.viewModel.ignoreSelectedSourceApp()
}
card.onIgnoreKind = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
self?.viewModel.ignoreSelectedKind()
}
card.onDelete = { [weak self] selected in
self?.viewModel.selectItem(at: selected)
self?.viewModel.deleteSelected()
@@ -710,7 +718,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
return .ready
}
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") {
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") || lower.hasPrefix("ignored") {
return .action
}
if lower.hasPrefix("error") || lower.contains("failed") {
@@ -1014,6 +1022,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
cardViews.first?.debugCollectionMenuTitles ?? []
}
var debugFirstCardCaptureRuleMenuTitles: [String] {
cardViews.first?.debugCaptureRuleMenuTitles ?? []
}
var debugFirstCardVisibleActionLabels: [String] {
cardViews.first?.debugVisibleActionLabels ?? []
}
@@ -1388,6 +1400,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
var onReveal: (Int) -> Void = { _ in }
var onTogglePin: (Int) -> Void = { _ in }
var onAssignCollection: (Int, String?) -> Void = { _, _ in }
var onIgnoreSourceApp: (Int) -> Void = { _ in }
var onIgnoreKind: (Int) -> Void = { _ in }
var onDelete: (Int) -> Void = { _ in }
private let index: Int
@@ -1395,6 +1409,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private let itemIsPinned: Bool
private let itemIsStacked: Bool
private let stackCount: Int
private let itemSourceAppName: String?
private let itemSourceAppBundleID: String?
private let itemCollectionName: String?
private let collectionNames: [String]
private let contentView = NSView()
@@ -1422,6 +1438,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
self.itemIsPinned = item.isPinned
self.itemIsStacked = isStacked
self.stackCount = stackCount
self.itemSourceAppName = Self.presentSourceText(item.sourceApp)
self.itemSourceAppBundleID = Self.presentSourceText(item.sourceAppBundleId)
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
super.init(frame: .zero)
@@ -1551,6 +1569,13 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
return collectionMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
}
var debugCaptureRuleMenuTitles: [String] {
guard let rulesMenu = contextMenu().items.first(where: { $0.title == "Capture Rules" })?.submenu else {
return []
}
return rulesMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
}
var debugVisibleActionLabels: [String] {
actionRail.isHidden ? [] : actionRailButtons.map { $0.toolTip ?? "" }
}
@@ -1598,6 +1623,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
}
addMenuItem(itemIsPinned ? "Unpin" : "Pin", action: #selector(togglePinFromMenu), to: menu)
addCollectionMenu(to: menu)
addCaptureRulesMenu(to: menu)
menu.addItem(NSMenuItem.separator())
let open = addMenuItem("Open", action: #selector(openFromMenu), to: menu)
open.isEnabled = canOpen
@@ -1639,6 +1665,48 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
menu.setSubmenu(submenu, for: parent)
}
private func addCaptureRulesMenu(to menu: NSMenu) {
let parent = NSMenuItem(title: "Capture Rules", action: nil, keyEquivalent: "")
let submenu = NSMenu(title: "Capture Rules")
submenu.autoenablesItems = false
let ignoreSource = NSMenuItem(
title: ignoreSourceTitle(),
action: #selector(ignoreSourceAppFromMenu),
keyEquivalent: ""
)
ignoreSource.target = self
ignoreSource.isEnabled = itemSourceAppName != nil || itemSourceAppBundleID != nil
submenu.addItem(ignoreSource)
let ignoreKind = NSMenuItem(
title: "Ignore \(kindLabel(for: itemKind)) Items",
action: #selector(ignoreKindFromMenu),
keyEquivalent: ""
)
ignoreKind.target = self
ignoreKind.isEnabled = true
submenu.addItem(ignoreKind)
menu.addItem(parent)
menu.setSubmenu(submenu, for: parent)
}
private func ignoreSourceTitle() -> String {
if let itemSourceAppName {
return "Ignore \(itemSourceAppName)"
}
if let itemSourceAppBundleID {
return "Ignore \(itemSourceAppBundleID)"
}
return "Ignore Source App"
}
private static func presentSourceText(_ value: String?) -> String? {
guard let text = value?.clipboardTrimmed, !text.isEmpty else { return nil }
return text
}
private func availableCollectionNames() -> [String] {
var names: [String] = []
var seen = Set<String>()
@@ -1876,6 +1944,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
onAssignCollection(index, nil)
}
@objc private func ignoreSourceAppFromMenu() {
onIgnoreSourceApp(index)
}
@objc private func ignoreKindFromMenu() {
onIgnoreKind(index)
}
@objc private func deleteFromMenu() {
onDelete(index)
}

View File

@@ -426,6 +426,34 @@ final class ClipboardPanelViewModel {
}
}
func ignoreSelectedSourceApp() {
guard let item = selectedItem else { return }
guard let rule = sourceIgnoreRule(for: item) else {
statusMessage = "Source app unavailable"
return
}
let existing = settings.ignoredApps.map { $0.clipboardTrimmed.lowercased() }
guard !existing.contains(rule.value.lowercased()) else {
statusMessage = "\(rule.displayName) is already ignored"
return
}
settings.ignoredApps.append(rule.value)
statusMessage = "Ignored \(rule.displayName) for future captures"
}
func ignoreSelectedKind() {
guard let item = selectedItem else { return }
guard !settings.ignoredItemKindsRaw.contains(item.kind.rawValue) else {
statusMessage = "\(Self.statusKindName(item.kind)) items are already ignored"
return
}
settings.ignoredItemKindsRaw.append(item.kind.rawValue)
statusMessage = "Ignored \(Self.statusKindName(item.kind)) items for future captures"
}
func selectCollection(named name: String) {
guard let normalizedName = ClipboardCollectionDefaults.normalizedName(name) else { return }
isStackFilterSelected = false
@@ -791,6 +819,25 @@ final class ClipboardPanelViewModel {
return min(lhs, rhs)
}
private func sourceIgnoreRule(for item: ClipboardItem) -> (value: String, displayName: String)? {
if let bundleID = item.sourceAppBundleId?.clipboardTrimmed, !bundleID.isEmpty {
let sourceApp = item.sourceApp?.clipboardTrimmed
let display = sourceApp?.isEmpty == false ? sourceApp ?? bundleID : bundleID
return (bundleID, display)
}
if let sourceApp = item.sourceApp?.clipboardTrimmed, !sourceApp.isEmpty {
return (sourceApp, sourceApp)
}
return nil
}
private static func statusKindName(_ kind: ClipboardItemKind) -> String {
let name = kind.displayName
return name == name.uppercased() ? name : name.capitalized
}
private func searchTokens(from query: String) -> [String] {
query
.split { character in