diff --git a/TypeaheadAI.xcodeproj/project.pbxproj b/TypeaheadAI.xcodeproj/project.pbxproj index 69afee1..d17ef73 100644 --- a/TypeaheadAI.xcodeproj/project.pbxproj +++ b/TypeaheadAI.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 2B8B95202A9C099E00FB9EA9 /* GoogleChromeURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B8B951F2A9C099E00FB9EA9 /* GoogleChromeURLHandler.swift */; }; + 2B8B95252A9C1F7B00FB9EA9 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B8B95242A9C1F7B00FB9EA9 /* ScriptingBridge.framework */; }; + 2B8B95272A9C2E0D00FB9EA9 /* GoogleChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B8B95262A9C2E0D00FB9EA9 /* GoogleChrome.swift */; }; + 2B8B95292A9C2E6400FB9EA9 /* GoogleChromeScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B8B95282A9C2E6400FB9EA9 /* GoogleChromeScripting.swift */; }; 2BA7F0792A9ABBA8003D38BA /* TypeaheadAIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA7F0782A9ABBA8003D38BA /* TypeaheadAIApp.swift */; }; 2BA7F07D2A9ABBA8003D38BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BA7F07C2A9ABBA8003D38BA /* Assets.xcassets */; }; 2BA7F0802A9ABBA8003D38BA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BA7F07F2A9ABBA8003D38BA /* Preview Assets.xcassets */; }; @@ -42,6 +46,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 2B8B951E2A9C08DC00FB9EA9 /* GoogleChrome.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoogleChrome.h; sourceTree = ""; }; + 2B8B951F2A9C099E00FB9EA9 /* GoogleChromeURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleChromeURLHandler.swift; sourceTree = ""; }; + 2B8B95222A9C1A2E00FB9EA9 /* GoogleChrome-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GoogleChrome-Bridging-Header.h"; sourceTree = ""; }; + 2B8B95242A9C1F7B00FB9EA9 /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; }; + 2B8B95262A9C2E0D00FB9EA9 /* GoogleChrome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleChrome.swift; sourceTree = ""; }; + 2B8B95282A9C2E6400FB9EA9 /* GoogleChromeScripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleChromeScripting.swift; sourceTree = ""; }; 2BA7F0752A9ABBA8003D38BA /* TypeaheadAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TypeaheadAI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2BA7F0782A9ABBA8003D38BA /* TypeaheadAIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeaheadAIApp.swift; sourceTree = ""; }; 2BA7F07C2A9ABBA8003D38BA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -67,6 +77,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2B8B95252A9C1F7B00FB9EA9 /* ScriptingBridge.framework in Frameworks */, 2BA7F0AE2A9ABC47003D38BA /* KeyboardShortcuts in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -88,6 +99,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2B8B95232A9C1F7B00FB9EA9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2B8B95242A9C1F7B00FB9EA9 /* ScriptingBridge.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 2BA7F06C2A9ABBA7003D38BA = { isa = PBXGroup; children = ( @@ -95,6 +114,7 @@ 2BA7F08E2A9ABBA8003D38BA /* TypeaheadAITests */, 2BA7F0982A9ABBA8003D38BA /* TypeaheadAIUITests */, 2BA7F0762A9ABBA8003D38BA /* Products */, + 2B8B95232A9C1F7B00FB9EA9 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +141,11 @@ 2BA7F0A82A9ABBE2003D38BA /* ClientManager.swift */, 2BA7F0AA2A9ABC20003D38BA /* Constants.swift */, 2BA7F0B42A9ABCD7003D38BA /* PromptManager.swift */, + 2B8B951E2A9C08DC00FB9EA9 /* GoogleChrome.h */, + 2B8B951F2A9C099E00FB9EA9 /* GoogleChromeURLHandler.swift */, + 2B8B95222A9C1A2E00FB9EA9 /* GoogleChrome-Bridging-Header.h */, + 2B8B95262A9C2E0D00FB9EA9 /* GoogleChrome.swift */, + 2B8B95282A9C2E6400FB9EA9 /* GoogleChromeScripting.swift */, ); path = TypeaheadAI; sourceTree = ""; @@ -300,11 +325,14 @@ 2BA7F0B32A9ABCBF003D38BA /* MenuView.swift in Sources */, 2BA7F0B52A9ABCD7003D38BA /* PromptManager.swift in Sources */, 2BA7F0792A9ABBA8003D38BA /* TypeaheadAIApp.swift in Sources */, + 2B8B95292A9C2E6400FB9EA9 /* GoogleChromeScripting.swift in Sources */, 2BA7F0B72A9ABD61003D38BA /* SettingsView.swift in Sources */, + 2B8B95272A9C2E0D00FB9EA9 /* GoogleChrome.swift in Sources */, 2BA7F0852A9ABBA8003D38BA /* TypeaheadAI.xcdatamodeld in Sources */, 2BA7F0A92A9ABBE2003D38BA /* ClientManager.swift in Sources */, 2BA7F0B12A9ABCA5003D38BA /* MenuPromptView.swift in Sources */, 2BA7F0AB2A9ABC20003D38BA /* Constants.swift in Sources */, + 2B8B95202A9C099E00FB9EA9 /* GoogleChromeURLHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -484,6 +512,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TypeaheadAI/GoogleChrome-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Debug; @@ -518,6 +547,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "TypeaheadAI/GoogleChrome-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist b/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist index d5ea02b..202fb4c 100644 --- a/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,23 @@ 0 + SuppressBuildableAutocreation + + 2BA7F0742A9ABBA8003D38BA + + primary + + + 2BA7F08A2A9ABBA8003D38BA + + primary + + + 2BA7F0942A9ABBA8003D38BA + + primary + + + diff --git a/TypeaheadAI/GoogleChrome-Bridging-Header.h b/TypeaheadAI/GoogleChrome-Bridging-Header.h new file mode 100644 index 0000000..ffed750 --- /dev/null +++ b/TypeaheadAI/GoogleChrome-Bridging-Header.h @@ -0,0 +1,13 @@ +// +// GoogleChrome-Bridging-Header.h +// TypeaheadAI +// +// Created by Jeff Hara on 8/27/23. +// + +#ifndef GoogleChrome_Bridging_Header_h +#define GoogleChrome_Bridging_Header_h + +#import "GoogleChrome.h" + +#endif /* GoogleChrome_Bridging_Header_h */ diff --git a/TypeaheadAI/GoogleChrome.h b/TypeaheadAI/GoogleChrome.h new file mode 100644 index 0000000..4a6d8b2 --- /dev/null +++ b/TypeaheadAI/GoogleChrome.h @@ -0,0 +1,130 @@ +/* + * GoogleChrome.h + * + * Generated by `sdef /Applications/Google\ Chrome.app | sdp -fh --basename GoogleChrome` + */ + +#import +#import + + +@class GoogleChromeApplication, GoogleChromeWindow, GoogleChromeTab, GoogleChromeBookmarkFolder, GoogleChromeBookmarkItem; + +@protocol GoogleChromeGenericMethods + +- (void) saveIn:(NSURL *)in_ as:(NSString *)as; // Save an object. +- (void) close; // Close a window. +- (void) delete; // Delete an object. +- (SBObject *) duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location. +- (SBObject *) moveTo:(SBObject *)to; // Move object(s) to a new location. +- (void) print; // Print an object. +- (void) reload; // Reload a tab. +- (void) goBack; // Go Back (If Possible). +- (void) goForward; // Go Forward (If Possible). +- (void) selectAll; // Select all. +- (void) cutSelection; // Cut selected text (If Possible). +- (void) copySelection NS_RETURNS_NOT_RETAINED; // Copy text. +- (void) pasteSelection; // Paste text (If Possible). +- (void) undo; // Undo the last change. +- (void) redo; // Redo the last change. +- (void) stop; // Stop the current tab from loading. +- (void) viewSource; // View the HTML source of the tab. +- (id) executeJavascript:(NSString *)javascript; // Execute a piece of javascript. + +@end + + + +/* + * Standard Suite + */ + +// The application's top-level scripting object. +@interface GoogleChromeApplication : SBApplication + +- (SBElementArray *) windows; + +@property (copy, readonly) NSString *name; // The name of the application. +@property (readonly) BOOL frontmost; // Is this the frontmost (active) application? +@property (copy, readonly) NSString *version; // The version of the application. + +- (void) open:(NSArray *)x; // Open a document. +- (void) quit; // Quit the application. +- (BOOL) exists:(id)x; // Verify if an object exists. + +@end + +// A window. +@interface GoogleChromeWindow : SBObject + +- (SBElementArray *) tabs; + +@property (copy) NSString *givenName; // The given name of the window. +@property (copy, readonly) NSString *name; // The full title of the window. +- (NSString *) id; // The unique identifier of the window. +@property NSInteger index; // The index of the window, ordered front to back. +@property NSRect bounds; // The bounding rectangle of the window. +@property (readonly) BOOL closeable; // Whether the window has a close box. +@property (readonly) BOOL minimizable; // Whether the window can be minimized. +@property BOOL minimized; // Whether the window is currently minimized. +@property (readonly) BOOL resizable; // Whether the window can be resized. +@property BOOL visible; // Whether the window is currently visible. +@property (readonly) BOOL zoomable; // Whether the window can be zoomed. +@property BOOL zoomed; // Whether the window is currently zoomed. +@property (copy, readonly) GoogleChromeTab *activeTab; // Returns the currently selected tab +@property (copy) NSString *mode; // Represents the mode of the window which can be 'normal' or 'incognito', can be set only once during creation of the window. +@property NSInteger activeTabIndex; // The index of the active tab. + + +@end + + + +/* + * Chromium Suite + */ + +// The application's top-level scripting object. +@interface GoogleChromeApplication (ChromiumSuite) + +- (SBElementArray *) bookmarkFolders; + +@property (copy, readonly) GoogleChromeBookmarkFolder *bookmarksBar; // The bookmarks bar bookmark folder. +@property (copy, readonly) GoogleChromeBookmarkFolder *otherBookmarks; // The other bookmarks bookmark folder. + +@end + +// A tab. +@interface GoogleChromeTab : SBObject + +- (NSString *) id; // Unique ID of the tab. +@property (copy, readonly) NSString *title; // The title of the tab. +@property (copy) NSString *URL; // The url visible to the user. +@property (readonly) BOOL loading; // Is loading? + + +@end + +// A bookmarks folder that contains other bookmarks folder and bookmark items. +@interface GoogleChromeBookmarkFolder : SBObject + +- (SBElementArray *) bookmarkFolders; +- (SBElementArray *) bookmarkItems; + +- (NSString *) id; // Unique ID of the bookmark folder. +@property (copy) NSString *title; // The title of the folder. +@property (copy, readonly) NSNumber *index; // Returns the index with respect to its parent bookmark folder. + + +@end + +// An item consists of an URL and the title of a bookmark +@interface GoogleChromeBookmarkItem : SBObject + +- (NSString *) id; // Unique ID of the bookmark item. +@property (copy) NSString *title; // The title of the bookmark item. +@property (copy) NSString *URL; // The URL of the bookmark. +@property (copy, readonly) NSNumber *index; // Returns the index with respect to its parent bookmark folder. + + +@end diff --git a/TypeaheadAI/GoogleChrome.swift b/TypeaheadAI/GoogleChrome.swift new file mode 100644 index 0000000..85372b7 --- /dev/null +++ b/TypeaheadAI/GoogleChrome.swift @@ -0,0 +1,117 @@ +// +// GoogleChrome.swift +// TypeaheadAI +// +// Created by Jeff Hara on 8/27/23. +// + +import AppKit +import ScriptingBridge + +@objc public protocol SBObjectProtocol: NSObjectProtocol { + func get() -> Any! +} + +@objc public protocol SBApplicationProtocol: SBObjectProtocol { + func activate() + var delegate: SBApplicationDelegate! { get set } + var isRunning: Bool { get } +} + +// MARK: GoogleChromeGenericMethods +@objc public protocol GoogleChromeGenericMethods { + @objc optional func saveIn(_ in_: URL!, as: String!) // Save an object. + @objc optional func close() // Close a window. + @objc optional func delete() // Delete an object. + @objc optional func duplicateTo(_ to: SBObject!, withProperties: [AnyHashable : Any]!) -> SBObject // Copy object(s) and put the copies at a new location. + @objc optional func moveTo(_ to: SBObject!) -> SBObject // Move object(s) to a new location. + @objc optional func print() // Print an object. + @objc optional func reload() // Reload a tab. + @objc optional func goBack() // Go Back (If Possible). + @objc optional func goForward() // Go Forward (If Possible). + @objc optional func selectAll() // Select all. + @objc optional func cutSelection() // Cut selected text (If Possible). + @objc optional func copySelection() // Copy text. + @objc optional func pasteSelection() // Paste text (If Possible). + @objc optional func undo() // Undo the last change. + @objc optional func redo() // Redo the last change. + @objc optional func stop() // Stop the current tab from loading. + @objc optional func viewSource() // View the HTML source of the tab. + @objc optional func executeJavascript(_ javascript: String!) -> Any // Execute a piece of javascript. +} + +// MARK: GoogleChromeApplication +@objc public protocol GoogleChromeApplication: SBApplicationProtocol { + @objc optional func windows() -> SBElementArray + @objc optional var name: String { get } // The name of the application. + @objc optional var frontmost: Bool { get } // Is this the frontmost (active) application? + @objc optional var version: String { get } // The version of the application. + @objc optional func `open`(_ x: [URL]!) // Open a document. + @objc optional func quit() // Quit the application. + @objc optional func exists(_ x: Any!) -> Bool // Verify if an object exists. + @objc optional func bookmarkFolders() -> SBElementArray + @objc optional var bookmarksBar: GoogleChromeBookmarkFolder { get } // The bookmarks bar bookmark folder. + @objc optional var otherBookmarks: GoogleChromeBookmarkFolder { get } // The other bookmarks bookmark folder. +} +extension SBApplication: GoogleChromeApplication {} + +// MARK: GoogleChromeWindow +@objc public protocol GoogleChromeWindow: SBObjectProtocol, GoogleChromeGenericMethods { + @objc optional func tabs() -> SBElementArray + @objc optional var givenName: String { get } // The given name of the window. + @objc optional var name: String { get } // The full title of the window. + @objc optional func id() -> String // The unique identifier of the window. + @objc optional var index: Int { get } // The index of the window, ordered front to back. + @objc optional var bounds: NSRect { get } // The bounding rectangle of the window. + @objc optional var closeable: Bool { get } // Whether the window has a close box. + @objc optional var minimizable: Bool { get } // Whether the window can be minimized. + @objc optional var minimized: Bool { get } // Whether the window is currently minimized. + @objc optional var resizable: Bool { get } // Whether the window can be resized. + @objc optional var visible: Bool { get } // Whether the window is currently visible. + @objc optional var zoomable: Bool { get } // Whether the window can be zoomed. + @objc optional var zoomed: Bool { get } // Whether the window is currently zoomed. + @objc optional var activeTab: GoogleChromeTab { get } // Returns the currently selected tab + @objc optional var mode: String { get } // Represents the mode of the window which can be 'normal' or 'incognito', can be set only once during creation of the window. + @objc optional var activeTabIndex: Int { get } // The index of the active tab. + @objc optional func setGivenName(_ givenName: String!) // The given name of the window. + @objc optional func setIndex(_ index: Int) // The index of the window, ordered front to back. + @objc optional func setBounds(_ bounds: NSRect) // The bounding rectangle of the window. + @objc optional func setMinimized(_ minimized: Bool) // Whether the window is currently minimized. + @objc optional func setVisible(_ visible: Bool) // Whether the window is currently visible. + @objc optional func setZoomed(_ zoomed: Bool) // Whether the window is currently zoomed. + @objc optional func setMode(_ mode: String!) // Represents the mode of the window which can be 'normal' or 'incognito', can be set only once during creation of the window. + @objc optional func setActiveTabIndex(_ activeTabIndex: Int) // The index of the active tab. +} +extension SBObject: GoogleChromeWindow {} + +// MARK: GoogleChromeTab +@objc public protocol GoogleChromeTab: SBObjectProtocol, GoogleChromeGenericMethods { + @objc optional func id() -> String // Unique ID of the tab. + @objc optional var title: String { get } // The title of the tab. + @objc optional var URL: String { get } // The url visible to the user. + @objc optional var loading: Bool { get } // Is loading? + @objc optional func setURL(_ URL: String!) // The url visible to the user. +} +extension SBObject: GoogleChromeTab {} + +// MARK: GoogleChromeBookmarkFolder +@objc public protocol GoogleChromeBookmarkFolder: SBObjectProtocol, GoogleChromeGenericMethods { + @objc optional func bookmarkFolders() -> SBElementArray + @objc optional func bookmarkItems() -> SBElementArray + @objc optional func id() -> String // Unique ID of the bookmark folder. + @objc optional var title: String { get } // The title of the folder. + @objc optional var index: NSNumber { get } // Returns the index with respect to its parent bookmark folder. + @objc optional func setTitle(_ title: String!) // The title of the folder. +} +extension SBObject: GoogleChromeBookmarkFolder {} + +// MARK: GoogleChromeBookmarkItem +@objc public protocol GoogleChromeBookmarkItem: SBObjectProtocol, GoogleChromeGenericMethods { + @objc optional func id() -> String // Unique ID of the bookmark item. + @objc optional var title: String { get } // The title of the bookmark item. + @objc optional var URL: String { get } // The URL of the bookmark. + @objc optional var index: NSNumber { get } // Returns the index with respect to its parent bookmark folder. + @objc optional func setTitle(_ title: String!) // The title of the bookmark item. + @objc optional func setURL(_ URL: String!) // The URL of the bookmark. +} +extension SBObject: GoogleChromeBookmarkItem {} diff --git a/TypeaheadAI/GoogleChromeScripting.swift b/TypeaheadAI/GoogleChromeScripting.swift new file mode 100644 index 0000000..ab94712 --- /dev/null +++ b/TypeaheadAI/GoogleChromeScripting.swift @@ -0,0 +1,14 @@ +// +// GoogleChromeScripting.swift +// TypeaheadAI +// +// Created by Jeff Hara on 8/27/23. +// + +public enum GoogleChromeScripting: String { + case application = "application" + case bookmarkFolder = "bookmark folder" + case bookmarkItem = "bookmark item" + case tab = "tab" + case window = "window" +} diff --git a/TypeaheadAI/GoogleChromeURLHandler.swift b/TypeaheadAI/GoogleChromeURLHandler.swift new file mode 100644 index 0000000..00ec077 --- /dev/null +++ b/TypeaheadAI/GoogleChromeURLHandler.swift @@ -0,0 +1,80 @@ +// +// GoogleChromeURLHandler.swift +// TypeaheadAI +// +// Created by Jeff Hara on 8/27/23. +// + +import Cocoa +import Foundation +import os.log + +class GoogleChromeURLHandler { + private let logger = Logger( + subsystem: "ai.typeahead.TypeaheadAI", + category: "GoogleChromeURLHandler" + ) + + func getCurrentURLFromChrome() -> String? { + return getChromeActiveURL() + + guard let chrome = SBApplication.init(bundleIdentifier: "com.google.Chrome") as GoogleChromeApplication? else { + self.logger.error("Failed to access Google Chrome") + return nil + } + + // Loop through the windows to find the frontmost one + if let windows = chrome.windows?() { + for i in 0.. String? { + let source = """ + tell application "Google Chrome" + return URL of active tab of front window + end tell + """ + + var error: NSDictionary? + if let script = NSAppleScript(source: source) { + if let output = script.executeAndReturnError(&error).stringValue { + self.logger.debug("Successfully retrieved URL from Chrome: \(output)") + return output + } else { + self.logger.debug("Failed to retrieve URL from Chrome: \(error)") + return nil + } + } + + if let error = error { + self.logger.debug("Error retrieving URL from Chrome: \(error)") + } + + return nil + } +} diff --git a/TypeaheadAI/TypeaheadAI.entitlements b/TypeaheadAI/TypeaheadAI.entitlements index b637592..2c988d1 100644 --- a/TypeaheadAI/TypeaheadAI.entitlements +++ b/TypeaheadAI/TypeaheadAI.entitlements @@ -2,11 +2,6 @@ - com.apple.security.temporary-exception.apple-events -com.apple.security.temporary-exception.apple-events - - com.google.Chrome - com.apple.security.app-sandbox com.apple.security.automation.apple-events @@ -14,6 +9,9 @@ com.apple.security.temporary-exception.apple-events com.apple.security.network.client com.apple.security.scripting-targets - + + com.google.Chrome + + diff --git a/TypeaheadAI/TypeaheadAIApp.swift b/TypeaheadAI/TypeaheadAIApp.swift index 9532cff..5c4dbc9 100644 --- a/TypeaheadAI/TypeaheadAIApp.swift +++ b/TypeaheadAI/TypeaheadAIApp.swift @@ -26,6 +26,7 @@ final class AppState: ObservableObject { category: "AppState" ) private let clientManager = ClientManager() + private let googleChromeURLHandler = GoogleChromeURLHandler() init() { checkAndRequestAccessibilityPermissions() @@ -69,6 +70,9 @@ final class AppState: ObservableObject { let commandKeyUsed = event.modifierFlags.contains(.command) if event.keyCode == 8 && commandKeyUsed { // 'C' key // Get the latest string content from the pasteboard + + let _ = self.googleChromeURLHandler.getCurrentURLFromChrome() + if let _ = NSPasteboard.general.string(forType: .string) { self.logger.debug("copy detected") }