From c6b4d8d8f6c0f2013d19605f5e4c24fa48c902ad Mon Sep 17 00:00:00 2001 From: dupenodi Date: Tue, 31 Mar 2026 17:25:26 +0530 Subject: [PATCH] feat: add min-length filter and history deduplication (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two configurable history filters to address noise from modal editors: - Minimum text length: entries shorter than the threshold (default: 1, configurable 1–20) are silently dropped before being written to history. - Deduplication: when enabled, copying an identical inline text entry promotes the existing item to the top instead of inserting a duplicate. Both settings are exposed in a new "History" section in Preferences and persisted via UserDefaults through SettingsManager. Co-Authored-By: Claude Sonnet 4.6 --- Services/ClipboardStore.swift | 14 +++++++- Services/ClipboardWatcher.swift | 3 ++ Services/SettingsManager.swift | 22 ++++++++---- Views/SettingsView.swift | 63 ++++++++++++++++++++++++++------- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/Services/ClipboardStore.swift b/Services/ClipboardStore.swift index 47375ea..0e833bb 100644 --- a/Services/ClipboardStore.swift +++ b/Services/ClipboardStore.swift @@ -47,7 +47,19 @@ class ClipboardStore: ObservableObject { private func performAdd(_ item: ClipboardItem) { print("[Buffer] Store: Adding item, current count: \(items.count)") - + + // Deduplication: if an identical inline text entry already exists, promote it + // to the top instead of adding a duplicate. File-backed large text is already + // deduplicated consecutively via hash in ClipboardWatcher. + if SettingsManager.shared.deduplicateHistory, + item.type == .text, + let newText = item.textContent, + !item.isFileBacked, + let existing = items.first(where: { $0.type == .text && !$0.isFileBacked && $0.textContent == newText }) { + moveToTop(existing) + return + } + // Insert at beginning (newest first) items.insert(item, at: 0) diff --git a/Services/ClipboardWatcher.swift b/Services/ClipboardWatcher.swift index e20e66e..9125a8c 100644 --- a/Services/ClipboardWatcher.swift +++ b/Services/ClipboardWatcher.swift @@ -84,6 +84,9 @@ class ClipboardWatcher: ObservableObject { // Try to capture text first if let text = pasteboard.string(forType: .string), !text.isEmpty { + // Drop entries that are shorter than the configured minimum length + guard text.count >= SettingsManager.shared.minTextLength else { return } + let textSize = text.utf8.count // Use prefix hash for large text to avoid expensive full-string hashing diff --git a/Services/SettingsManager.swift b/Services/SettingsManager.swift index 1542cb5..6b3de00 100644 --- a/Services/SettingsManager.swift +++ b/Services/SettingsManager.swift @@ -11,36 +11,46 @@ class SettingsManager: ObservableObject { // Keys private let hotkeyModifiersKey = "hotkeyModifiers" private let hotkeyKeyCodeKey = "hotkeyKeyCode" - + private let minTextLengthKey = "minTextLength" + private let deduplicateHistoryKey = "deduplicateHistory" + @Published var hotkeyModifiers: HotkeyModifiers @Published var hotkeyKeyCode: UInt16 @Published var launchAtLogin: Bool = false - + @Published var minTextLength: Int + @Published var deduplicateHistory: Bool + private init() { // Initialize with defaults first, then load saved values let defaultMods = HotkeyModifiers(shift: true, command: true, option: false, control: false) let defaultKeyCode: UInt16 = 9 // V key - + // Load saved modifiers or use default if let savedMods = defaults.array(forKey: hotkeyModifiersKey) as? [String] { self.hotkeyModifiers = HotkeyModifiers(from: savedMods) } else { self.hotkeyModifiers = defaultMods } - + // Load saved keycode or use default (V key) let savedKeyCode = defaults.integer(forKey: hotkeyKeyCodeKey) self.hotkeyKeyCode = savedKeyCode > 0 ? UInt16(savedKeyCode) : defaultKeyCode - + // Load launch at login status if #available(macOS 13.0, *) { self.launchAtLogin = SMAppService.mainApp.status == .enabled } + + // Load history filter settings + self.minTextLength = defaults.object(forKey: minTextLengthKey) as? Int ?? 1 + self.deduplicateHistory = defaults.bool(forKey: deduplicateHistoryKey) } - + func save() { defaults.set(hotkeyModifiers.toArray(), forKey: hotkeyModifiersKey) defaults.set(Int(hotkeyKeyCode), forKey: hotkeyKeyCodeKey) + defaults.set(minTextLength, forKey: minTextLengthKey) + defaults.set(deduplicateHistory, forKey: deduplicateHistoryKey) } func toggleLaunchAtLogin(_ enabled: Bool) { diff --git a/Views/SettingsView.swift b/Views/SettingsView.swift index 6fd7cce..4d70384 100644 --- a/Views/SettingsView.swift +++ b/Views/SettingsView.swift @@ -79,34 +79,63 @@ struct SettingsView: View { } Divider() - + + // History section + VStack(alignment: .leading, spacing: 12) { + Text("History") + .font(.system(size: 13, weight: .medium)) + .foregroundColor(.secondary) + + HStack { + Text("Ignore entries shorter than") + .font(.system(size: 12)) + Spacer() + Stepper( + settings.minTextLength == 1 + ? "1 character" + : "\(settings.minTextLength) characters", + value: $settings.minTextLength, + in: 1...20 + ) + .font(.system(size: 12)) + .onChange(of: settings.minTextLength) { _ in settings.save() } + } + + Toggle("Deduplicate history", isOn: $settings.deduplicateHistory) + .font(.system(size: 12)) + .onChange(of: settings.deduplicateHistory) { _ in settings.save() } + .toggleStyle(.switch) + } + + Divider() + // System section VStack(alignment: .leading, spacing: 12) { Text("System") .font(.system(size: 13, weight: .medium)) .foregroundColor(.secondary) - + Toggle("Launch at Login", isOn: $settings.launchAtLogin) .font(.system(size: 12)) .onChange(of: settings.launchAtLogin) { newValue in SettingsManager.shared.toggleLaunchAtLogin(newValue) - // Sync back state in case toggle fails + // Sync back state in case toggle fails DispatchQueue.main.async { settings.launchAtLogin = SettingsManager.shared.launchAtLogin } } .toggleStyle(.switch) } - + Spacer() - + // Footer Text("Changes take effect after restarting Buffer") .font(.system(size: 10)) .foregroundColor(.secondary) } .padding(20) - .frame(width: 320, height: 280) + .frame(width: 320, height: 400) .background(KeyRecorder(isRecording: $isRecording) { keyCode, modifiers in settings.hotkeyKeyCode = keyCode settings.hotkeyModifiers = modifiers @@ -187,11 +216,13 @@ class SettingsViewModel: ObservableObject { @Published var hotkeyModifiers: HotkeyModifiers @Published var hotkeyKeyCode: UInt16 @Published var launchAtLogin: Bool - + @Published var minTextLength: Int + @Published var deduplicateHistory: Bool + private let defaults = UserDefaults.standard private let hotkeyModifiersKey = "hotkeyModifiers" private let hotkeyKeyCodeKey = "hotkeyKeyCode" - + init() { // Load modifiers if let savedMods = defaults.array(forKey: hotkeyModifiersKey) as? [String] { @@ -199,23 +230,29 @@ class SettingsViewModel: ObservableObject { } else { self.hotkeyModifiers = HotkeyModifiers(shift: true, command: true, option: false, control: false) } - + // Load keycode (default to V = 9) let savedKeyCode = defaults.integer(forKey: hotkeyKeyCodeKey) self.hotkeyKeyCode = savedKeyCode > 0 ? UInt16(savedKeyCode) : 9 - + // Load launch at login status from manager natively via SMAppService self.launchAtLogin = SettingsManager.shared.launchAtLogin + + // Load history filter settings + self.minTextLength = SettingsManager.shared.minTextLength + self.deduplicateHistory = SettingsManager.shared.deduplicateHistory } - + func save() { defaults.set(hotkeyModifiers.toArray(), forKey: hotkeyModifiersKey) defaults.set(Int(hotkeyKeyCode), forKey: hotkeyKeyCodeKey) - + SettingsManager.shared.hotkeyModifiers = hotkeyModifiers SettingsManager.shared.hotkeyKeyCode = hotkeyKeyCode + SettingsManager.shared.minTextLength = minTextLength + SettingsManager.shared.deduplicateHistory = deduplicateHistory SettingsManager.shared.save() - + NotificationCenter.default.post(name: .bufferHotkeyChanged, object: nil) } }