From 5321352184e6c38ff660a512cfc26385632b1b38 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Tue, 12 Aug 2025 14:52:03 +0530 Subject: [PATCH] Feature: Favourites --- DynamicIsland/DynamicIslandApp.swift | 63 +-- DynamicIsland/Localizable.xcstrings | 33 +- .../Clipboard/ClipboardWindow.swift | 363 ++++++++++++++++++ .../components/Settings/SettingsView.swift | 21 +- DynamicIsland/managers/ClipboardManager.swift | 82 ++++ .../managers/ClipboardWindowManager.swift | 104 +++++ DynamicIsland/models/Constants.swift | 1 + 7 files changed, 631 insertions(+), 36 deletions(-) create mode 100644 DynamicIsland/components/Clipboard/ClipboardWindow.swift create mode 100644 DynamicIsland/managers/ClipboardWindowManager.swift diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 613c2cd..5e09975 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -445,38 +445,45 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Only open clipboard if the feature is enabled guard Defaults[.enableClipboardManager] else { return } - // Find the appropriate view model based on mouse location - let mouseLocation = NSEvent.mouseLocation - var viewModel = self.vm - - if Defaults[.showOnAllDisplays] { - for screen in NSScreen.screens { - if screen.frame.contains(mouseLocation) { - if let screenViewModel = self.viewModels[screen] { - viewModel = screenViewModel - break - } - } - } - } - - // Open the notch if it's closed - if viewModel.notchState == .closed { - viewModel.open() - - // Wait a bit for the notch to open before toggling clipboard - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self.coordinator.toggleClipboardPopover() - } - } else { - // If notch is already open, toggle immediately - self.coordinator.toggleClipboardPopover() - } - // Start clipboard monitoring if not already running if !ClipboardManager.shared.isMonitoring { ClipboardManager.shared.startMonitoring() } + + // Check if window mode is enabled + if Defaults[.clipboardWindowMode] { + // Use window interface + ClipboardWindowManager.shared.toggleClipboardWindow() + } else { + // Use traditional popover interface + // Find the appropriate view model based on mouse location + let mouseLocation = NSEvent.mouseLocation + var viewModel = self.vm + + if Defaults[.showOnAllDisplays] { + for screen in NSScreen.screens { + if screen.frame.contains(mouseLocation) { + if let screenViewModel = self.viewModels[screen] { + viewModel = screenViewModel + break + } + } + } + } + + // Open the notch if it's closed + if viewModel.notchState == .closed { + viewModel.open() + + // Wait a bit for the notch to open before toggling clipboard + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.coordinator.toggleClipboardPopover() + } + } else { + // If notch is already open, toggle immediately + self.coordinator.toggleClipboardPopover() + } + } } if !Defaults[.showOnAllDisplays] { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 8ec7b0f..4563a06 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -271,9 +271,15 @@ }, "Clear Clipboard History" : { + }, + "Clear clipboard history removes recent copies. Clear pinned items removes your favorites. Both actions are permanent." : { + }, "Clear Data" : { + }, + "Clear Pinned Items" : { + }, "Clipboard" : { @@ -783,9 +789,15 @@ }, "No extension installed" : { + }, + "No favorites" : { + }, "No Graphs Enabled" : { + }, + "No results found" : { + }, "Non-notch display height" : { @@ -852,6 +864,12 @@ } } } + }, + "Pin items from history to save them here" : { + + }, + "Pinned Items" : { + }, "Plugged In" : { @@ -920,6 +938,9 @@ }, "Running" : { + }, + "Search clipboard..." : { + }, "seconds" : { "extractionState" : "stale", @@ -1083,15 +1104,9 @@ }, "System features" : { - }, - "The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history." : { - }, "TheDynamicIsland" : { - }, - "This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header." : { - }, "Time to Full Charge: %lld min" : { @@ -1122,6 +1137,9 @@ }, "Transform your notch truly yours" : { + }, + "Try adjusting your search terms" : { + }, "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : { @@ -1178,6 +1196,9 @@ }, "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { + }, + "Window Mode shows clipboard in a moveable window like Raycast with tabs for history and favorites. When disabled, clipboard appears in a popover attached to the Dynamic Island." : { + }, "With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : { diff --git a/DynamicIsland/components/Clipboard/ClipboardWindow.swift b/DynamicIsland/components/Clipboard/ClipboardWindow.swift new file mode 100644 index 0000000..b727067 --- /dev/null +++ b/DynamicIsland/components/Clipboard/ClipboardWindow.swift @@ -0,0 +1,363 @@ +// +// ClipboardWindow.swift +// DynamicIsland +// +// Created by GitHub Copilot on 12/08/25. +// + +import SwiftUI +import Defaults + +enum ClipboardTab: String, CaseIterable { + case history = "History" + case favorites = "Favorites" + + var icon: String { + switch self { + case .history: return "clock" + case .favorites: return "heart.fill" + } + } +} + +struct ClipboardWindow: View { + @ObservedObject var clipboardManager = ClipboardManager.shared + @State private var selectedTab: ClipboardTab = .history + @State private var searchText = "" + + var body: some View { + VStack(spacing: 0) { + // Header with tabs and search + ClipboardWindowHeader(selectedTab: $selectedTab, searchText: $searchText) + + Divider() + .background(Color.gray.opacity(0.3)) + + // Content area + ClipboardWindowContent(selectedTab: selectedTab, searchText: searchText) + } + .frame(width: 400, height: 300) + .background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10) + } +} + +struct ClipboardWindowHeader: View { + @Binding var selectedTab: ClipboardTab + @Binding var searchText: String + @ObservedObject var clipboardManager = ClipboardManager.shared + + var body: some View { + VStack(spacing: 0) { + // Title and controls + HStack { + Image(systemName: "doc.on.clipboard") + .foregroundColor(.primary) + .font(.system(size: 16, weight: .medium)) + + Text("Clipboard Manager") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.primary) + + Spacer() + + // Clear button + Button(action: { + if selectedTab == .history { + clipboardManager.clearHistory() + } else { + // Clear favorites + clipboardManager.pinnedItems.removeAll() + clipboardManager.savePinnedItemsToDefaults() + } + }) { + Image(systemName: "trash") + .foregroundColor(.red) + .font(.system(size: 12)) + } + .buttonStyle(PlainButtonStyle()) + .disabled(selectedTab == .history ? clipboardManager.clipboardHistory.isEmpty : clipboardManager.pinnedItems.isEmpty) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + // Tab selector + HStack(spacing: 0) { + ForEach(ClipboardTab.allCases, id: \.self) { tab in + ClipboardTabButton(tab: tab, isSelected: selectedTab == tab) { + withAnimation(.easeInOut(duration: 0.2)) { + selectedTab = tab + } + } + } + + Spacer() + } + .padding(.horizontal, 16) + .padding(.bottom, 8) + + // Search bar + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.secondary) + .font(.system(size: 12)) + + TextField("Search clipboard...", text: $searchText) + .textFieldStyle(PlainTextFieldStyle()) + .font(.system(size: 12)) + + if !searchText.isEmpty { + Button(action: { searchText = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + .font(.system(size: 10)) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.gray.opacity(0.1)) + .cornerRadius(6) + .padding(.horizontal, 16) + .padding(.bottom, 8) + } + } +} + +struct ClipboardTabButton: View { + let tab: ClipboardTab + let isSelected: Bool + let action: () -> Void + @ObservedObject var clipboardManager = ClipboardManager.shared + + var itemCount: Int { + switch tab { + case .history: + return clipboardManager.regularHistory.count + case .favorites: + return clipboardManager.pinnedItems.count + } + } + + var body: some View { + Button(action: action) { + HStack(spacing: 6) { + Image(systemName: tab.icon) + .font(.system(size: 11)) + + Text(tab.rawValue) + .font(.system(size: 11, weight: .medium)) + + if itemCount > 0 { + Text("\(itemCount)") + .font(.system(size: 9, weight: .bold)) + .foregroundColor(isSelected ? .white : .secondary) + .padding(.horizontal, 4) + .padding(.vertical, 1) + .background( + Capsule() + .fill(isSelected ? Color.white.opacity(0.3) : Color.gray.opacity(0.2)) + ) + } + } + .foregroundColor(isSelected ? .white : .secondary) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isSelected ? Color.blue : Color.clear) + ) + } + .buttonStyle(PlainButtonStyle()) + } +} + +struct ClipboardWindowContent: View { + let selectedTab: ClipboardTab + let searchText: String + @ObservedObject var clipboardManager = ClipboardManager.shared + + var filteredItems: [ClipboardItem] { + let items = selectedTab == .history ? clipboardManager.regularHistory : clipboardManager.pinnedItems + + if searchText.isEmpty { + return items + } else { + return items.filter { item in + item.preview.localizedCaseInsensitiveContains(searchText) || + item.type.displayName.localizedCaseInsensitiveContains(searchText) + } + } + } + + var body: some View { + if filteredItems.isEmpty { + ClipboardEmptyState(tab: selectedTab, hasSearch: !searchText.isEmpty) + } else { + ScrollView { + LazyVStack(spacing: 1) { + ForEach(filteredItems) { item in + ClipboardWindowItemRow(item: item, tab: selectedTab) + } + } + .padding(.vertical, 8) + } + } + } +} + +struct ClipboardEmptyState: View { + let tab: ClipboardTab + let hasSearch: Bool + + var body: some View { + VStack(spacing: 12) { + Image(systemName: hasSearch ? "magnifyingglass" : tab.icon) + .font(.system(size: 32)) + .foregroundColor(.secondary) + + if hasSearch { + Text("No results found") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary) + + Text("Try adjusting your search terms") + .font(.system(size: 12)) + .foregroundColor(.secondary) + } else { + Text(tab == .history ? "No clipboard history" : "No favorites") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.primary) + + Text(tab == .history ? "Copy something to get started" : "Pin items from history to save them here") + .font(.system(size: 12)) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct ClipboardWindowItemRow: View { + let item: ClipboardItem + let tab: ClipboardTab + @ObservedObject var clipboardManager = ClipboardManager.shared + @State private var isHovering = false + + var isPinned: Bool { + item.isPinned || clipboardManager.pinnedItems.contains(where: { $0.id == item.id }) + } + + var body: some View { + HStack(alignment: .top, spacing: 12) { + // Type icon + Image(systemName: item.type.icon) + .font(.system(size: 14)) + .foregroundColor(.blue) + .frame(width: 20) + + // Content + VStack(alignment: .leading, spacing: 4) { + Text(item.preview) + .font(.system(size: 12)) + .foregroundColor(.primary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + HStack { + Text(item.type.displayName) + .font(.system(size: 10)) + .foregroundColor(.secondary) + + Spacer() + + Text(timeAgoString(from: item.timestamp)) + .font(.system(size: 10)) + .foregroundColor(.secondary) + } + } + + Spacer() + + // Action buttons (shown on hover) + if isHovering { + HStack(spacing: 6) { + // Pin/Unpin button + Button(action: { + clipboardManager.togglePin(for: item) + }) { + Image(systemName: isPinned ? "heart.fill" : "heart") + .font(.system(size: 11)) + .foregroundColor(isPinned ? .red : .gray) + } + .buttonStyle(PlainButtonStyle()) + + // Copy button + Button(action: { + clipboardManager.copyToClipboard(item) + }) { + Image(systemName: "doc.on.doc") + .font(.system(size: 11)) + .foregroundColor(.green) + } + .buttonStyle(PlainButtonStyle()) + + // Delete button + Button(action: { + if isPinned { + clipboardManager.unpinItem(item) + } else { + clipboardManager.deleteItem(item) + } + }) { + Image(systemName: "trash") + .font(.system(size: 11)) + .foregroundColor(.red) + } + .buttonStyle(PlainButtonStyle()) + } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isHovering ? Color.gray.opacity(0.1) : Color.clear) + ) + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.2)) { + isHovering = hovering + } + } + .onTapGesture { + clipboardManager.copyToClipboard(item) + } + } + + private func timeAgoString(from date: Date) -> String { + let interval = Date().timeIntervalSince(date) + + if interval < 60 { + return "Just now" + } else if interval < 3600 { + let minutes = Int(interval / 60) + return "\(minutes)m ago" + } else if interval < 86400 { + let hours = Int(interval / 3600) + return "\(hours)h ago" + } else { + let days = Int(interval / 86400) + return "\(days)d ago" + } + } +} + +#Preview { + ClipboardWindow() + .frame(width: 400, height: 300) + .background(Color.gray.opacity(0.3)) +} diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 3103c72..dde6ea5 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1589,6 +1589,7 @@ struct ClipboardSettings: View { @Default(.enableClipboardManager) var enableClipboardManager @Default(.clipboardHistorySize) var clipboardHistorySize @Default(.showClipboardIcon) var showClipboardIcon + @Default(.clipboardWindowMode) var clipboardWindowMode var body: some View { Form { @@ -1611,6 +1612,8 @@ struct ClipboardSettings: View { Section { Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) + Defaults.Toggle("Window Mode", key: .clipboardWindowMode) + HStack { Text("History Size") Spacer() @@ -1631,6 +1634,13 @@ struct ClipboardSettings: View { .foregroundColor(.secondary) } + HStack { + Text("Pinned Items") + Spacer() + Text("\(clipboardManager.pinnedItems.count)") + .foregroundColor(.secondary) + } + HStack { Text("Monitoring Status") Spacer() @@ -1640,7 +1650,7 @@ struct ClipboardSettings: View { } header: { Text("Settings") } footer: { - Text("The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history.") + Text("Window Mode shows clipboard in a moveable window like Raycast with tabs for history and favorites. When disabled, clipboard appears in a popover attached to the Dynamic Island.") } Section { @@ -1649,10 +1659,17 @@ struct ClipboardSettings: View { } .foregroundColor(.red) .disabled(clipboardManager.clipboardHistory.isEmpty) + + Button("Clear Pinned Items") { + clipboardManager.pinnedItems.removeAll() + clipboardManager.savePinnedItemsToDefaults() + } + .foregroundColor(.red) + .disabled(clipboardManager.pinnedItems.isEmpty) } header: { Text("Actions") } footer: { - Text("This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header.") + Text("Clear clipboard history removes recent copies. Clear pinned items removes your favorites. Both actions are permanent.") } if !clipboardManager.clipboardHistory.isEmpty { diff --git a/DynamicIsland/managers/ClipboardManager.swift b/DynamicIsland/managers/ClipboardManager.swift index e4b4e7f..43269c4 100644 --- a/DynamicIsland/managers/ClipboardManager.swift +++ b/DynamicIsland/managers/ClipboardManager.swift @@ -18,6 +18,7 @@ struct ClipboardItem: Identifiable, Codable { let type: ClipboardItemType let timestamp: Date let preview: String + var isPinned: Bool = false // Store different types of data - avoid large binary data in UserDefaults let stringData: String? @@ -90,6 +91,14 @@ struct ClipboardItem: Identifiable, Codable { return try? Data(contentsOf: fileURL) } + // Helper to check if this item has the same content as another + func isSameContent(as other: ClipboardItem) -> Bool { + return stringData == other.stringData && + imageFileName == other.imageFileName && + fileURLs == other.fileURLs && + type == other.type + } + static func generatePreview(stringData: String, type: ClipboardItemType) -> String { switch type { case .text: @@ -149,6 +158,7 @@ class ClipboardManager: ObservableObject { static let shared = ClipboardManager() @Published var clipboardHistory: [ClipboardItem] = [] + @Published var pinnedItems: [ClipboardItem] = [] @Published var isMonitoring: Bool = false private var timer: Timer? @@ -159,6 +169,15 @@ class ClipboardManager: ObservableObject { return Defaults[.clipboardHistorySize] } + // Computed properties for filtered lists + var regularHistory: [ClipboardItem] { + clipboardHistory.filter { !$0.isPinned } + } + + var pinnedHistory: [ClipboardItem] { + pinnedItems + } + // Directory for storing clipboard data files static let clipboardDataDirectory: URL = { let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! @@ -254,6 +273,58 @@ class ClipboardManager: ObservableObject { saveHistoryToDefaults() } + func pinItem(_ item: ClipboardItem) { + // Update the item to be pinned + var pinnedItem = item + pinnedItem.isPinned = true + + // Remove from regular history if it exists there + clipboardHistory.removeAll { $0.id == item.id } + + // Add to pinned items if not already there + if !pinnedItems.contains(where: { $0.id == item.id }) { + pinnedItems.append(pinnedItem) + } + + saveHistoryToDefaults() + savePinnedItemsToDefaults() + } + + func unpinItem(_ item: ClipboardItem) { + // Remove from pinned items + pinnedItems.removeAll { $0.id == item.id } + + // Update the item to be unpinned and add back to regular history + var unpinnedItem = item + unpinnedItem.isPinned = false + + // Add back to regular history at the top + clipboardHistory.insert(unpinnedItem, at: 0) + + // Maintain history size limit + if clipboardHistory.count > maxHistoryItems { + let itemsToDelete = Array(clipboardHistory.dropFirst(maxHistoryItems)) + for oldItem in itemsToDelete { + if let fileName = oldItem.imageFileName { + let fileURL = ClipboardManager.clipboardDataDirectory.appendingPathComponent(fileName) + try? FileManager.default.removeItem(at: fileURL) + } + } + clipboardHistory = Array(clipboardHistory.prefix(maxHistoryItems)) + } + + saveHistoryToDefaults() + savePinnedItemsToDefaults() + } + + func togglePin(for item: ClipboardItem) { + if item.isPinned || pinnedItems.contains(where: { $0.id == item.id }) { + unpinItem(item) + } else { + pinItem(item) + } + } + // MARK: - Private Methods private func checkClipboard() { @@ -438,10 +509,21 @@ class ClipboardManager: ObservableObject { } } + func savePinnedItemsToDefaults() { + if let encoded = try? JSONEncoder().encode(pinnedItems) { + UserDefaults.standard.set(encoded, forKey: "ClipboardPinnedItems") + } + } + private func loadHistoryFromDefaults() { if let data = UserDefaults.standard.data(forKey: "ClipboardHistory"), let history = try? JSONDecoder().decode([ClipboardItem].self, from: data) { clipboardHistory = history } + + if let data = UserDefaults.standard.data(forKey: "ClipboardPinnedItems"), + let pinned = try? JSONDecoder().decode([ClipboardItem].self, from: data) { + pinnedItems = pinned + } } } diff --git a/DynamicIsland/managers/ClipboardWindowManager.swift b/DynamicIsland/managers/ClipboardWindowManager.swift new file mode 100644 index 0000000..bb7834a --- /dev/null +++ b/DynamicIsland/managers/ClipboardWindowManager.swift @@ -0,0 +1,104 @@ +// +// ClipboardWindowManager.swift +// DynamicIsland +// +// Created by GitHub Copilot on 12/08/25. +// + +import SwiftUI +import AppKit + +class ClipboardWindowManager: ObservableObject { + static let shared = ClipboardWindowManager() + + private var clipboardWindow: NSWindow? + + private init() {} + + func showClipboardWindow() { + if let existingWindow = clipboardWindow { + existingWindow.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + return + } + + // Create the window + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 400, height: 300), + styleMask: [.titled, .closable, .resizable], + backing: .buffered, + defer: false + ) + + window.title = "Clipboard Manager" + window.isMovableByWindowBackground = true + window.level = .floating + window.isReleasedWhenClosed = false + window.hasShadow = true + window.titlebarAppearsTransparent = true + window.styleMask.insert(.fullSizeContentView) + + // Set minimum and maximum sizes + window.minSize = NSSize(width: 350, height: 250) + window.maxSize = NSSize(width: 600, height: 500) + + // Center the window + if let screen = NSScreen.main { + let screenFrame = screen.visibleFrame + let windowFrame = window.frame + let x = (screenFrame.width - windowFrame.width) / 2 + screenFrame.minX + let y = (screenFrame.height - windowFrame.height) / 2 + screenFrame.minY + window.setFrameOrigin(NSPoint(x: x, y: y)) + } + + // Set the content view + let contentView = ClipboardWindow() + let hostingView = NSHostingView(rootView: contentView) + window.contentView = hostingView + + // Handle window closing + window.delegate = WindowDelegate { [weak self] in + self?.clipboardWindow = nil + } + + self.clipboardWindow = window + window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + + func hideClipboardWindow() { + clipboardWindow?.close() + } + + func toggleClipboardWindow() { + if let window = clipboardWindow, window.isVisible { + hideClipboardWindow() + } else { + showClipboardWindow() + } + } + + var isWindowVisible: Bool { + return clipboardWindow?.isVisible ?? false + } +} + +private class WindowDelegate: NSObject, NSWindowDelegate { + private let onClose: () -> Void + + init(onClose: @escaping () -> Void) { + self.onClose = onClose + super.init() + } + + func windowWillClose(_ notification: Notification) { + onClose() + } + + func windowShouldClose(_ sender: NSWindow) -> Bool { + // Hide instead of close when user clicks close button + sender.orderOut(nil) + onClose() + return false + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index a97a13b..0f98e94 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -170,6 +170,7 @@ extension Defaults.Keys { static let enableClipboardManager = Key("enableClipboardManager", default: true) static let clipboardHistorySize = Key("clipboardHistorySize", default: 3) static let showClipboardIcon = Key("showClipboardIcon", default: true) + static let clipboardWindowMode = Key("clipboardWindowMode", default: true) // MARK: Keyboard Shortcuts static let enableShortcuts = Key("enableShortcuts", default: true)