├── Icon.png ├── Icon-dark.png ├── .gitignore ├── Screenshots ├── 01.png ├── 02.png └── 03.png ├── TextGrabber2 ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── AppIcon.icon │ │ ├── Assets │ │ │ ├── Inner.svg │ │ │ └── Outer.svg │ │ └── icon.json │ ├── Services │ │ ├── zh-Hans.json │ │ ├── zh-Hant.json │ │ └── en-US.json │ ├── AppShortcuts.xcstrings │ └── Localizable.xcstrings ├── Sources │ ├── Extensions │ │ ├── Array+Extension.swift │ │ ├── SMAppService+Extension.swift │ │ ├── NSMenuItem+Extension.swift │ │ ├── NSWorkspace+Extension.swift │ │ ├── Bundle+Extension.swift │ │ ├── URL+Extension.swift │ │ ├── NSControl+Extension.swift │ │ ├── NSImage+Extension.swift │ │ ├── NSMenu+Extension.swift │ │ ├── String+Extension.swift │ │ ├── NSObject+Extension.swift │ │ ├── NSPasteboard+Extension.swift │ │ └── NSAlert+Extension.swift │ ├── CopyObserver.swift │ ├── Logger.swift │ ├── Preferences.swift │ ├── Detector.swift │ ├── Translator.swift │ ├── Services.swift │ ├── Recognizer.swift │ ├── Intents.swift │ ├── Updater.swift │ ├── Resources.swift │ └── App.swift ├── main.swift └── Info.entitlements ├── TextGrabber2.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── TextGrabber2.xcscheme │ │ ├── TextGrabber2 (en).xcscheme │ │ └── TextGrabber2 (zh-Hans).xcscheme └── project.pbxproj ├── Build.xcconfig ├── .github └── workflows │ └── build.yml ├── LICENSE └── README.md /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TextGrabber2-app/TextGrabber2/HEAD/Icon.png -------------------------------------------------------------------------------- /Icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TextGrabber2-app/TextGrabber2/HEAD/Icon-dark.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## System 2 | *.DS_Store 3 | 4 | ## User settings 5 | xcuserdata/ 6 | Local.xcconfig 7 | -------------------------------------------------------------------------------- /Screenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TextGrabber2-app/TextGrabber2/HEAD/Screenshots/01.png -------------------------------------------------------------------------------- /Screenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TextGrabber2-app/TextGrabber2/HEAD/Screenshots/02.png -------------------------------------------------------------------------------- /Screenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TextGrabber2-app/TextGrabber2/HEAD/Screenshots/03.png -------------------------------------------------------------------------------- /TextGrabber2/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TextGrabber2.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/Array+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/8/30. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | var hasValue: Bool { 12 | !isEmpty 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TextGrabber2.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TextGrabber2/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | 10 | NSMenu.swizzleIsUpdatedExcludingContentTypesOnce 11 | 12 | let app = NSApplication.shared 13 | let delegate = App() 14 | 15 | app.delegate = delegate 16 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 17 | -------------------------------------------------------------------------------- /Build.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Build.xcconfig 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | // https://help.apple.com/xcode/#/dev745c5c974 8 | 9 | CODE_SIGN_IDENTITY = - 10 | DEVELOPMENT_TEAM = 11 | PRODUCT_BUNDLE_IDENTIFIER = app.cyan.textgrabber2-dev 12 | 13 | MARKETING_VERSION = 1.8.0 14 | CURRENT_PROJECT_VERSION = 12 15 | 16 | #include? "Local.xcconfig" 17 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/SMAppService+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMAppService+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import ServiceManagement 9 | 10 | extension SMAppService { 11 | var isEnabled: Bool { 12 | status == .enabled 13 | } 14 | 15 | func toggle() throws { 16 | try (isEnabled ? unregister() : register()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TextGrabber2/Info.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSMenuItem+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMenuItem+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSMenuItem { 11 | convenience init(title: String) { 12 | self.init(title: title, action: nil, keyEquivalent: "") 13 | } 14 | 15 | func setOn(_ on: Bool) { 16 | state = on ? .on : .off 17 | } 18 | 19 | func toggle() { 20 | state = state == .on ? .off : .on 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSWorkspace+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSWorkspace+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSWorkspace { 11 | @discardableResult 12 | func safelyOpenURL(string: String) -> Bool { 13 | guard let url = URL(string: string) else { 14 | Logger.assertFail("Failed to create the URL: \(string)") 15 | return false 16 | } 17 | 18 | return open(url) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/Bundle+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | var bundleName: String? { 12 | infoDictionary?[kCFBundleNameKey as String] as? String 13 | } 14 | 15 | var shortVersionString: String { 16 | guard let version = infoDictionary?["CFBundleShortVersionString"] as? String else { 17 | Logger.assertFail("Missing CFBundleShortVersionString in bundle \(self)") 18 | return "1.0.0" 19 | } 20 | 21 | return version 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | types: [synchronize, opened, reopened, ready_for_review] 9 | 10 | jobs: 11 | textgrabber2: 12 | name: TextGrabber2 13 | runs-on: macos-26 14 | if: github.event.pull_request.draft == false 15 | env: 16 | DEVELOPER_DIR: /Applications/Xcode_26.0.app/Contents/Developer 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | - name: Build TextGrabber2 21 | run: | 22 | xcodebuild build -scheme TextGrabber2 -destination 'platform=macOS' CODE_SIGN_IDENTITY="" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO 23 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/AppIcon.icon/Assets/Inner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/Services/zh-Hans.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "serviceName": "Look Up in Dictionary", 4 | "displayName": "在词典中查询" 5 | }, 6 | { 7 | "serviceName": "Search With %WebSearchProvider@", 8 | "displayName": "在 Safari 中搜索" 9 | }, 10 | { 11 | "serviceName": "SEARCH_WITH_SPOTLIGHT", 12 | "displayName": "用 Spotlight 搜索" 13 | }, 14 | { 15 | "serviceName": "Convert Text from Simplified to Traditional Chinese", 16 | "displayName": "转换为繁体中文" 17 | }, 18 | { 19 | "serviceName": "Convert Text from Traditional to Simplified Chinese", 20 | "displayName": "转换为简体中文" 21 | }, 22 | { 23 | "serviceName": "New TextEdit Window Containing Selection", 24 | "displayName": "在 TextEdit 新建文件" 25 | }, 26 | { 27 | "serviceName": "Make Sticky", 28 | "displayName": "创建新便笺条" 29 | }, 30 | { 31 | "serviceName": "Summarize", 32 | "displayName": "摘要" 33 | } 34 | ] -------------------------------------------------------------------------------- /TextGrabber2/Resources/Services/zh-Hant.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "serviceName": "Look Up in Dictionary", 4 | "displayName": "在辭典中查詢" 5 | }, 6 | { 7 | "serviceName": "Search With %WebSearchProvider@", 8 | "displayName": "在 Safari 中搜尋" 9 | }, 10 | { 11 | "serviceName": "SEARCH_WITH_SPOTLIGHT", 12 | "displayName": "用 Spotlight 搜尋" 13 | }, 14 | { 15 | "serviceName": "Convert Text from Simplified to Traditional Chinese", 16 | "displayName": "轉換為繁體中文" 17 | }, 18 | { 19 | "serviceName": "Convert Text from Traditional to Simplified Chinese", 20 | "displayName": "轉換為簡體中文" 21 | }, 22 | { 23 | "serviceName": "New TextEdit Window Containing Selection", 24 | "displayName": "在 TextEdit 新建檔案" 25 | }, 26 | { 27 | "serviceName": "Make Sticky", 28 | "displayName": "建立新便條紙" 29 | }, 30 | { 31 | "serviceName": "Summarize", 32 | "displayName": "摘要" 33 | } 34 | ] -------------------------------------------------------------------------------- /TextGrabber2/Sources/CopyObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CopyObserver.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/12/6. 6 | // 7 | 8 | import AppKit 9 | 10 | @MainActor 11 | final class CopyObserver { 12 | static let `default` = CopyObserver() 13 | 14 | func changes(interval: Duration = .seconds(1.0)) -> AsyncStream { 15 | AsyncStream { continuation in 16 | var lastCount = NSPasteboard.general.changeCount 17 | let mainTask = Task { @MainActor in 18 | while !Task.isCancelled { 19 | try await Task.sleep(for: interval) 20 | let newCount = NSPasteboard.general.changeCount 21 | if newCount != lastCount { 22 | lastCount = newCount 23 | continuation.yield() 24 | } 25 | } 26 | } 27 | 28 | continuation.onTermination = { _ in 29 | mainTask.cancel() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/Services/en-US.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "serviceName": "Look Up in Dictionary", 4 | "displayName": "Look Up in Dictionary" 5 | }, 6 | { 7 | "serviceName": "Search With %WebSearchProvider@", 8 | "displayName": "Search in Safari" 9 | }, 10 | { 11 | "serviceName": "SEARCH_WITH_SPOTLIGHT", 12 | "displayName": "Search with Spotlight" 13 | }, 14 | { 15 | "serviceName": "Convert Text from Simplified to Traditional Chinese", 16 | "displayName": "Convert to Traditional Chinese" 17 | }, 18 | { 19 | "serviceName": "Convert Text from Traditional to Simplified Chinese", 20 | "displayName": "Convert to Simplified Chinese" 21 | }, 22 | { 23 | "serviceName": "New TextEdit Window Containing Selection", 24 | "displayName": "New File in TextEdit" 25 | }, 26 | { 27 | "serviceName": "Make Sticky", 28 | "displayName": "Make New Sticky Note" 29 | }, 30 | { 31 | "serviceName": "Summarize", 32 | "displayName": "Summarize" 33 | } 34 | ] -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/URL+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/10/30. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | static var previewingDirectory: URL { 12 | let directory = temporaryDirectory.appendingPathComponent("QuickLook") 13 | let fileManager = FileManager.default 14 | 15 | if !fileManager.directoryExists(at: directory) { 16 | do { 17 | try fileManager.createDirectory(at: directory, withIntermediateDirectories: false) 18 | } catch { 19 | Logger.log(.error, "Failed to create previewing directory: \(error.localizedDescription)") 20 | } 21 | } 22 | 23 | return directory 24 | } 25 | } 26 | 27 | // MARK: - Private 28 | 29 | private extension FileManager { 30 | func directoryExists(at url: URL) -> Bool { 31 | var isDirectory: ObjCBool = false 32 | let fileExists = fileExists(atPath: url.path, isDirectory: &isDirectory) 33 | return fileExists && isDirectory.boolValue 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/21. 6 | // 7 | 8 | import Foundation 9 | import os.log 10 | 11 | enum Logger { 12 | static func log(_ level: OSLogType, _ message: @autoclosure @escaping () -> String, file: StaticString = #file, line: UInt = #line, function: StaticString = #function) { 13 | var file: String = "\(file)" 14 | if let url = URL(string: file) { 15 | file = url.lastPathComponent 16 | } 17 | 18 | os_logger.log(level: level, "\(file):\(line), \(function) -> \(message())") 19 | } 20 | 21 | static func assertFail(_ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { 22 | assertionFailure(message(), file: file, line: line) 23 | } 24 | 25 | static func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { 26 | if !condition() { 27 | assertionFailure(message(), file: file, line: line) 28 | } 29 | } 30 | } 31 | 32 | private let os_logger = os.Logger() 33 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Preferences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preferences.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/6/10. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | UserDefaults wrapper with handy getters and setters. 12 | */ 13 | enum Preferences {} 14 | 15 | @MainActor 16 | @propertyWrapper 17 | struct Storage { 18 | private let key: String 19 | private let defaultValue: T 20 | 21 | init(key: String, defaultValue: T) { 22 | self.key = key 23 | self.defaultValue = defaultValue 24 | } 25 | 26 | var wrappedValue: T { 27 | get { 28 | guard let data = UserDefaults.standard.object(forKey: key) as? Data else { 29 | return defaultValue 30 | } 31 | 32 | let value = try? Coders.decoder.decode(T.self, from: data) 33 | return value ?? defaultValue 34 | } 35 | set { 36 | let data = try? Coders.encoder.encode(newValue) 37 | UserDefaults.standard.set(data, forKey: key) 38 | } 39 | } 40 | } 41 | 42 | private enum Coders { 43 | static let encoder = JSONEncoder() 44 | static let decoder = JSONDecoder() 45 | } 46 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSControl+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSControl+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | 10 | /** 11 | Closure-based handlers to replace target-action. 12 | */ 13 | protocol ClosureActionable: AnyObject { 14 | var target: AnyObject? { get set } 15 | var action: Selector? { get set } 16 | } 17 | 18 | extension ClosureActionable { 19 | func addAction(_ action: @escaping () -> Void) { 20 | let target = Handler(action) 21 | objc_setAssociatedObject( 22 | self, 23 | UUID().uuidString, 24 | target, 25 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN 26 | ) 27 | 28 | self.target = target 29 | self.action = #selector(Handler.invoke) 30 | } 31 | } 32 | 33 | extension NSMenuItem: ClosureActionable {} 34 | 35 | // MARK: - Private 36 | 37 | private class Handler: NSObject { 38 | private let action: () -> Void 39 | 40 | init(_ action: @escaping () -> Void) { 41 | self.action = action 42 | } 43 | 44 | @objc func invoke() { 45 | action() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TextGrabber2.app 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Detector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Detector.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | https://developer.apple.com/documentation/foundation/nsdatadetector 12 | */ 13 | enum Detector { 14 | static func matches(in text: String) -> [String] { 15 | guard let detector = try? NSDataDetector(types: types) else { 16 | Logger.assertFail("Failed to create NSDataDetector") 17 | return [] 18 | } 19 | 20 | let range = NSRange(text.startIndex.. -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSImage { 11 | var cgImage: CGImage? { 12 | var rect = CGRect(origin: .zero, size: size) 13 | return cgImage(forProposedRect: &rect, context: .current, hints: nil) 14 | } 15 | 16 | var pngData: Data? { 17 | guard let tiffData = tiffRepresentation else { 18 | Logger.log(.error, "Failed to get the tiff data") 19 | return nil 20 | } 21 | 22 | guard let bitmapRep = NSBitmapImageRep(data: tiffData) else { 23 | Logger.log(.error, "Failed to get the bitmap representation") 24 | return nil 25 | } 26 | 27 | guard let pngData = bitmapRep.representation(using: .png, properties: [:]) else { 28 | Logger.log(.error, "Failed to get the png data") 29 | return nil 30 | } 31 | 32 | return pngData 33 | } 34 | 35 | static func with( 36 | symbolName: String, 37 | pointSize: Double, 38 | weight: NSFont.Weight = .regular, 39 | accessibilityLabel: String? = nil 40 | ) -> NSImage { 41 | let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityLabel) 42 | let config = NSImage.SymbolConfiguration(pointSize: pointSize, weight: weight) 43 | 44 | guard let image = image?.withSymbolConfiguration(config) else { 45 | Logger.assertFail("Failed to create image with symbol \"\(symbolName)\"") 46 | return NSImage() 47 | } 48 | 49 | return image 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSMenu+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSMenu+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSMenu { 11 | /** 12 | Hook this method to work around the **Populating a menu window that is already visible** crash. 13 | */ 14 | static let swizzleIsUpdatedExcludingContentTypesOnce: () = { 15 | NSMenu.exchangeInstanceMethods( 16 | originalSelector: sel_getUid("_isUpdatedExcludingContentTypes:"), 17 | swizzledSelector: #selector(swizzled_isUpdatedExcludingContentTypes(_:)) 18 | ) 19 | }() 20 | 21 | @discardableResult 22 | func addItem(withTitle string: String, action selector: Selector? = nil) -> NSMenuItem { 23 | addItem(withTitle: string, action: selector, keyEquivalent: "") 24 | } 25 | 26 | @discardableResult 27 | func addItem(withTitle string: String, action: @escaping () -> Void) -> NSMenuItem { 28 | let item = addItem(withTitle: string, action: nil) 29 | item.addAction(action) 30 | return item 31 | } 32 | 33 | func removeItems(where: (NSMenuItem) -> Bool) { 34 | items.filter { `where`($0) }.forEach { 35 | removeItem($0) 36 | } 37 | } 38 | } 39 | 40 | // MARK: - Private 41 | 42 | private extension NSMenu { 43 | @objc func swizzled_isUpdatedExcludingContentTypes(_ contentTypes: Int) -> Bool { 44 | // The original implementation contains an invalid assertion that causes a crash. 45 | // Based on testing, it would return false anyway, so we simply return false to bypass the assertion. 46 | false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/5/14. 6 | // 7 | 8 | import AppKit 9 | 10 | extension String { 11 | /** 12 | Returns a single-line version of the string by replacing newlines with spaces. 13 | */ 14 | var singleLine: String { 15 | components(separatedBy: .newlines).joined(separator: " ") 16 | } 17 | 18 | /** 19 | Returns a truncated string that fits the desired drawing width, with specified font. 20 | */ 21 | func truncatedToFit(width: Double, font: NSFont) -> String { 22 | // Early return if it already fits 23 | if self.width(using: font) <= width { 24 | return self 25 | } 26 | 27 | // Binary search to find truncation point 28 | var low = 0 29 | var high = count 30 | var truncated = self 31 | 32 | while low < high { 33 | let mid = (low + high) / 2 34 | let test = "\(prefix(mid))\(Constants.suffix)" 35 | if test.width(using: font) <= width { 36 | low = mid + 1 37 | truncated = test 38 | } else { 39 | high = mid 40 | } 41 | } 42 | 43 | return truncated 44 | } 45 | } 46 | 47 | // MARK: - Private 48 | 49 | private extension String { 50 | enum Constants { 51 | // Half-width space and horizontal ellipsis 52 | static let suffix = "\u{2009}\u{2026}" 53 | } 54 | 55 | func width(using font: NSFont) -> Double { 56 | NSAttributedString( 57 | string: self, 58 | attributes: [NSAttributedString.Key.font: font] 59 | ).size().width 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSObject+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NSObject { 11 | /// Exchange two instance methods during runtime. 12 | static func exchangeInstanceMethods(originalSelector: Selector, swizzledSelector: Selector) { 13 | exchangeImplementations( 14 | originalSelector: originalSelector, 15 | originalMethod: class_getInstanceMethod(Self.self, originalSelector), 16 | swizzledSelector: swizzledSelector, 17 | swizzledMethod: class_getInstanceMethod(Self.self, swizzledSelector) 18 | ) 19 | } 20 | } 21 | 22 | // MARK: - Private 23 | 24 | private extension NSObject { 25 | /// Exchange two implementations during runtime. 26 | static func exchangeImplementations( 27 | originalSelector: Selector, 28 | originalMethod: Method?, 29 | swizzledSelector: Selector, 30 | swizzledMethod: Method? 31 | ) { 32 | let type = Self.self 33 | guard let originalMethod else { 34 | Logger.assertFail("Failed to swizzle: \(type), missing original method") 35 | return 36 | } 37 | 38 | guard let swizzledMethod else { 39 | Logger.assertFail("Failed to swizzle: \(type), missing swizzled method") 40 | return 41 | } 42 | 43 | if class_addMethod(type, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) { 44 | class_replaceMethod(type, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) 45 | } else { 46 | method_exchangeImplementations(originalMethod, swizzledMethod) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Translator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Translator.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/11/30. 6 | // 7 | 8 | import AppKit 9 | 10 | @MainActor 11 | enum Translator { 12 | static func showWindow(text: String) { 13 | NSApp.activate(ignoringOtherApps: true) 14 | contentVC?.setValue(NSAttributedString(string: text), forKey: "text") 15 | windowController.window?.center() 16 | windowController.showWindow(nil) 17 | } 18 | 19 | // MARK: - Private 20 | 21 | private static var contentVC = controllerClass?.init() 22 | private static var windowController = { 23 | let window = NSWindow(contentViewController: contentVC ?? NSViewController()) 24 | window.styleMask = [.closable, .titled] 25 | window.title = "" 26 | window.isReleasedWhenClosed = false 27 | return NSWindowController(window: window) 28 | }() 29 | } 30 | 31 | // MARK: - Private 32 | 33 | private extension Translator { 34 | static var controllerClass: NSViewController.Type? { 35 | loadBundle() 36 | 37 | // LTUITranslationViewController 38 | let className = "LTUI" + "TranslationViewController" 39 | return NSClassFromString(className) as? NSViewController.Type 40 | } 41 | 42 | static func loadBundle() { 43 | // Joined as: /System/Library/PrivateFrameworks/TranslationUIServices.framework 44 | let path = [ 45 | "", 46 | "System", 47 | "Library", 48 | "PrivateFrameworks", 49 | "TranslationUIServices.framework", 50 | ].joined(separator: "/") 51 | 52 | guard let bundle = Bundle(path: path) else { 53 | return Logger.assertFail("Missing TranslationUIServices") 54 | } 55 | 56 | guard !bundle.isLoaded else { 57 | return 58 | } 59 | 60 | bundle.load() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Services.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Services.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/4/4. 6 | // 7 | 8 | import AppKit 9 | 10 | /** 11 | https://support.apple.com/guide/mac-help/mchlp1012/mac. 12 | 13 | Definition file is located at ~/Library/Containers/app.cyan.textgrabber2/Data/Documents/ 14 | */ 15 | enum Services { 16 | struct Item: Decodable { 17 | let serviceName: String 18 | let displayName: String? 19 | } 20 | 21 | static var fileURL: URL { 22 | URL.documentsDirectory.appending( 23 | path: Constants.fileName, 24 | directoryHint: .notDirectory 25 | ) 26 | } 27 | 28 | static var items: [Item] { 29 | guard let data = try? Data(contentsOf: fileURL) else { 30 | Logger.log(.error, "Missing \(Constants.fileName)") 31 | return [] 32 | } 33 | 34 | guard let items = try? JSONDecoder().decode([Item].self, from: data) else { 35 | Logger.log(.error, "Failed to decode the file") 36 | return [] 37 | } 38 | 39 | return items 40 | } 41 | 42 | static func initialize() { 43 | guard !FileManager.default.fileExists(atPath: fileURL.path()) else { 44 | return Logger.log(.info, "\(Constants.fileName) was created before") 45 | } 46 | 47 | guard let sourceURL = Bundle.main.url(forResource: "Services/\(Localized.languageIdentifier)", withExtension: "json") else { 48 | return Logger.assertFail("Missing source file to copy from") 49 | } 50 | 51 | do { 52 | try FileManager.default.copyItem(at: sourceURL, to: fileURL) 53 | } catch { 54 | Logger.log(.error, "\(error)") 55 | } 56 | } 57 | } 58 | 59 | // MARK: - Private 60 | 61 | private extension Services { 62 | enum Constants { 63 | static let fileName = "services.json" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/AppShortcuts.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "Extract text from copied image using ${applicationName}" : { 5 | "extractionState" : "extracted_with_value", 6 | "localizations" : { 7 | "en" : { 8 | "stringSet" : { 9 | "state" : "new", 10 | "values" : [ 11 | "Extract text from copied image using ${applicationName}" 12 | ] 13 | } 14 | }, 15 | "zh-Hans" : { 16 | "stringSet" : { 17 | "state" : "translated", 18 | "values" : [ 19 | "使用 ${applicationName} 从复制的图片中提取文本。" 20 | ] 21 | } 22 | }, 23 | "zh-Hant" : { 24 | "stringSet" : { 25 | "state" : "translated", 26 | "values" : [ 27 | "使用 ${applicationName} 從拷貝的圖像中提取文字。" 28 | ] 29 | } 30 | } 31 | } 32 | }, 33 | "Preview copied image using ${applicationName}" : { 34 | "extractionState" : "extracted_with_value", 35 | "localizations" : { 36 | "en" : { 37 | "stringSet" : { 38 | "state" : "new", 39 | "values" : [ 40 | "Preview copied image using ${applicationName}" 41 | ] 42 | } 43 | }, 44 | "zh-Hans" : { 45 | "stringSet" : { 46 | "state" : "translated", 47 | "values" : [ 48 | "使用 ${applicationName} 预览复制的图片。" 49 | ] 50 | } 51 | }, 52 | "zh-Hant" : { 53 | "stringSet" : { 54 | "state" : "translated", 55 | "values" : [ 56 | "使用 ${applicationName} 預覽拷貝的圖像。" 57 | ] 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "version" : "1.1" 64 | } -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSPasteboard+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSPasteboard+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/21. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSPasteboard { 11 | var isEmpty: Bool { 12 | pasteboardItems?.isEmpty ?? true 13 | } 14 | 15 | var string: String? { 16 | get { 17 | string(forType: .string) 18 | } 19 | set { 20 | guard let newValue else { 21 | return 22 | } 23 | 24 | declareTypes([.string], owner: nil) 25 | setString(newValue, forType: .string) 26 | } 27 | } 28 | 29 | var image: NSImage? { 30 | // Copied file 31 | if let data = data(forType: .fileURL), 32 | let string = String(data: data, encoding: .utf8), 33 | let url = URL(string: string) { 34 | return NSImage(contentsOf: url) 35 | } 36 | 37 | // Copied tiff or png 38 | if let data = data(forType: .tiff) ?? data(forType: .png) { 39 | return NSImage(data: data) 40 | } 41 | 42 | // Fallback 43 | return (readObjects(forClasses: [NSImage.self]) as? [NSImage])?.first 44 | } 45 | 46 | var hasLimitedAccess: Bool { 47 | guard #available(macOS 15.4, *) else { 48 | return false 49 | } 50 | 51 | return accessBehavior != .alwaysAllow 52 | } 53 | 54 | var hasFullAccess: Bool { 55 | !hasLimitedAccess 56 | } 57 | 58 | @MainActor 59 | func saveImageAsFile() { 60 | NSApp.activate() 61 | 62 | let savePanel = NSSavePanel() 63 | savePanel.allowedContentTypes = [.png] 64 | savePanel.isExtensionHidden = false 65 | savePanel.titlebarAppearsTransparent = true 66 | 67 | guard let pngData = image?.pngData, savePanel.runModal() == .OK, let url = savePanel.url else { 68 | return 69 | } 70 | 71 | do { 72 | try pngData.write(to: url, options: .atomic) 73 | } catch { 74 | Logger.log(.error, "Failed to save the image") 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Recognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Recognizer.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/21. 6 | // 7 | 8 | import AppKit 9 | @preconcurrency import Vision 10 | 11 | /** 12 | https://developer.apple.com/documentation/vision/recognizing_text_in_images 13 | */ 14 | enum Recognizer { 15 | struct ResultData: Equatable { 16 | let candidates: [String] 17 | 18 | init(candidates: [String]) { 19 | var seen = Set(candidates) 20 | let aggregated = candidates + candidates.flatMap { 21 | Detector.matches(in: $0) 22 | }.filter { 23 | seen.insert($0).inserted 24 | } 25 | 26 | self.candidates = aggregated.filter { !$0.isEmpty } 27 | } 28 | 29 | var spacesJoined: String { 30 | candidates.joined(separator: " ") 31 | } 32 | 33 | var lineBreaksJoined: String { 34 | candidates.joined(separator: "\n") 35 | } 36 | 37 | var directlyJoined: String { 38 | candidates.joined() 39 | } 40 | } 41 | 42 | static func detect(image: CGImage?, level: VNRequestTextRecognitionLevel) async -> ResultData? { 43 | guard let image else { 44 | return ResultData(candidates: []) 45 | } 46 | 47 | return await withCheckedContinuation { continuation in 48 | let request = VNRecognizeTextRequest { request, error in 49 | let candidates = request.results? 50 | .compactMap { $0 as? VNRecognizedTextObservation } 51 | .compactMap { $0.topCandidates(1).first?.string } 52 | 53 | DispatchQueue.main.async { 54 | guard error == nil, let candidates else { 55 | return continuation.resume(returning: nil) 56 | } 57 | 58 | continuation.resume(returning: ResultData(candidates: candidates)) 59 | } 60 | } 61 | 62 | request.recognitionLevel = level 63 | request.usesLanguageCorrection = level == .accurate 64 | request.automaticallyDetectsLanguage = level == .accurate 65 | 66 | DispatchQueue.global(qos: .userInitiated).async { 67 | do { 68 | try VNImageRequestHandler(cgImage: image).perform([request]) 69 | } catch { 70 | Logger.log(.error, "\(error)") 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Extensions/NSAlert+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAlert+Extension.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/5/1. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSAlert { 11 | /** 12 | Drop-in replacement for `informativeText` with Markdown support. 13 | */ 14 | var markdownBody: String? { 15 | get { 16 | objc_getAssociatedObject(self, &AssociatedObjects.markdownBody) as? String 17 | } 18 | set { 19 | objc_setAssociatedObject( 20 | self, 21 | &AssociatedObjects.markdownBody, 22 | newValue, 23 | objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC 24 | ) 25 | 26 | updateAccessoryView(with: newValue ?? "") 27 | } 28 | } 29 | 30 | static func runModal(message: String, style: Style = .critical) { 31 | NSApp.activate() 32 | 33 | let alert = Self() 34 | alert.alertStyle = style 35 | alert.messageText = message 36 | alert.runModal() 37 | } 38 | } 39 | 40 | // MARK: - Private 41 | 42 | private extension NSAlert { 43 | private enum AssociatedObjects { 44 | @MainActor static var markdownBody: UInt8 = 0 45 | } 46 | 47 | private enum Constants { 48 | static let fontSize: Double = 11 49 | static let contentWidth: Double = 220 50 | static let contentPadding: Double = 10 51 | } 52 | 53 | func updateAccessoryView(with markdown: String) { 54 | let textView = NSTextView() 55 | textView.font = .systemFont(ofSize: Constants.fontSize) 56 | textView.drawsBackground = false 57 | textView.isEditable = false 58 | 59 | if let data = markdown.data(using: .utf8), let string = try? NSAttributedString(markdown: data, options: .init(allowsExtendedAttributes: true, interpretedSyntax: .inlineOnlyPreservingWhitespace)) { 60 | textView.textStorage?.setAttributedString(string) 61 | } else { 62 | textView.string = markdown 63 | } 64 | 65 | textView.textStorage?.addAttribute( 66 | .foregroundColor, 67 | value: NSColor.labelColor, 68 | range: NSRange(location: 0, length: textView.attributedString().length) 69 | ) 70 | 71 | let contentSize = CGSize(width: Constants.contentWidth, height: 0) 72 | textView.frame = CGRect(origin: CGPoint(x: Constants.contentPadding, y: 0), size: contentSize) 73 | textView.sizeToFit() 74 | 75 | let wrapper = NSView(frame: textView.frame.insetBy(dx: -Constants.contentPadding, dy: 0)) 76 | wrapper.addSubview(textView) 77 | accessoryView = wrapper 78 | layout() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /TextGrabber2.xcodeproj/xcshareddata/xcschemes/TextGrabber2.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /TextGrabber2.xcodeproj/xcshareddata/xcschemes/TextGrabber2 (en).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /TextGrabber2.xcodeproj/xcshareddata/xcschemes/TextGrabber2 (zh-Hans).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Intents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Intents.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2025/10/30. 6 | // 7 | 8 | import AppKit 9 | import AppIntents 10 | 11 | struct IntentProvider: AppShortcutsProvider { 12 | static var appShortcuts: [AppShortcut] { 13 | return [ 14 | AppShortcut( 15 | intent: ExtractIntent(), 16 | phrases: [ 17 | "Extract text from copied image using \(.applicationName)", 18 | ], 19 | shortTitle: "\(ExtractIntent.title)", 20 | systemImageName: "text.viewfinder" 21 | ), 22 | AppShortcut( 23 | intent: PreviewIntent(), 24 | phrases: [ 25 | "Preview copied image using \(.applicationName)", 26 | ], 27 | shortTitle: "\(PreviewIntent.title)", 28 | systemImageName: "eye" 29 | ), 30 | ] 31 | } 32 | } 33 | 34 | struct ExtractIntent: AppIntent { 35 | struct ResultEntity: AppEntity { 36 | struct DummyQuery: EntityQuery { 37 | func entities(for identifiers: [ResultEntity.ID]) async throws -> [ResultEntity] { [] } 38 | func suggestedEntities() async throws -> [ResultEntity] { [] } 39 | } 40 | 41 | static let defaultQuery = DummyQuery() 42 | static var typeDisplayRepresentation: TypeDisplayRepresentation { "Result" } 43 | 44 | var id: String { title } 45 | var title: String 46 | var subtitle: String? 47 | 48 | var displayRepresentation: DisplayRepresentation { 49 | DisplayRepresentation( 50 | title: "\(title)", 51 | subtitle: subtitle.map { "\($0)" } 52 | ) 53 | } 54 | } 55 | 56 | static let title: LocalizedStringResource = "Extract Text from Copied Image" 57 | static let description = IntentDescription( 58 | "Extract text from copied image using TextGrabber2.", 59 | searchKeywords: ["TextGrabber2"], 60 | ) 61 | 62 | func perform() async throws -> some ReturnsValue<[ResultEntity]> { 63 | let image = NSPasteboard.general.image?.cgImage 64 | let text = NSPasteboard.general.string 65 | 66 | let result = await Recognizer.detect(image: image, level: .accurate) 67 | let candidates = (result?.candidates ?? []) + [text].compactMap { $0 } 68 | 69 | if let first = candidates.first, !first.isEmpty && image != nil { 70 | NSPasteboard.general.string = first 71 | } 72 | 73 | let entities = candidates.map { 74 | ResultEntity(title: $0, subtitle: nil) 75 | } 76 | 77 | if entities.count > 1 { 78 | return .result(value: entities + [ 79 | ResultEntity( 80 | title: candidates.joined(separator: " "), 81 | subtitle: Localized.menuTitleJoinWithSpaces 82 | ), 83 | ResultEntity( 84 | title: candidates.joined(separator: "\n"), 85 | subtitle: Localized.menuTitleJoinWithLineBreaks 86 | ), 87 | ResultEntity( 88 | title: candidates.joined(), 89 | subtitle: Localized.menuTitleJoinDirectly 90 | ), 91 | ]) 92 | } 93 | 94 | return .result(value: entities) 95 | } 96 | } 97 | 98 | struct PreviewIntent: AppIntent { 99 | static let title: LocalizedStringResource = "Preview Copied Image" 100 | static let description = IntentDescription( 101 | "Preview copied image using TextGrabber2.", 102 | searchKeywords: ["TextGrabber2"], 103 | ) 104 | 105 | func perform() async throws -> some IntentResult { 106 | Task { @MainActor in 107 | (NSApp.delegate as? App)?.previewCopiedImage() 108 | } 109 | 110 | return .result() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Updater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Updater.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/12/3. 6 | // 7 | 8 | import AppKit 9 | 10 | enum Updater { 11 | /** 12 | [GitHub Releases API](https://api.github.com/repos/TextGrabber2-app/TextGrabber2/releases/latest) 13 | */ 14 | fileprivate struct Version: Decodable { 15 | let name: String 16 | let body: String 17 | let htmlUrl: String 18 | } 19 | 20 | private enum Constants { 21 | static let endpoint = "https://api.github.com/repos/TextGrabber2-app/TextGrabber2/releases/latest" 22 | static let decoder = { 23 | let decoder = JSONDecoder() 24 | decoder.keyDecodingStrategy = .convertFromSnakeCase 25 | return decoder 26 | }() 27 | } 28 | 29 | static func checkForUpdates() async { 30 | guard let url = URL(string: Constants.endpoint) else { 31 | return Logger.assertFail("Failed to create the URL: \(Constants.endpoint)") 32 | } 33 | 34 | guard let (data, response) = try? await URLSession.shared.data(from: url) else { 35 | return Logger.log(.error, "Failed to reach out to the server") 36 | } 37 | 38 | guard let status = (response as? HTTPURLResponse)?.statusCode, status == 200 else { 39 | return Logger.log(.error, "Failed to get the update") 40 | } 41 | 42 | guard let version = try? Constants.decoder.decode(Version.self, from: data) else { 43 | return Logger.log(.error, "Failed to decode the data") 44 | } 45 | 46 | DispatchQueue.main.async { 47 | presentUpdate(newVersion: version) 48 | } 49 | } 50 | } 51 | 52 | // MARK: - Private 53 | 54 | @MainActor 55 | private extension Updater { 56 | static func presentUpdate(newVersion: Version) { 57 | let currentVersion = Bundle.main.shortVersionString 58 | 59 | // Check if the new version was skipped 60 | guard !Preferences.Updater.skippedVersions.contains(newVersion.name) else { 61 | return 62 | } 63 | 64 | // Check if the version is different 65 | guard newVersion.name != currentVersion else { 66 | return 67 | } 68 | 69 | let alert = NSAlert() 70 | alert.messageText = String(format: Localized.Updater.newVersionAvailableTitle, newVersion.name) 71 | alert.markdownBody = newVersion.body 72 | alert.addButton(withTitle: Localized.Updater.learnMore) 73 | alert.addButton(withTitle: Localized.Updater.remindMeLater) 74 | alert.addButton(withTitle: Localized.Updater.skipThisVersion) 75 | 76 | switch alert.runModal() { 77 | case .alertFirstButtonReturn: // Learn More 78 | NSWorkspace.shared.safelyOpenURL(string: newVersion.htmlUrl) 79 | case .alertThirdButtonReturn: // Skip This Version 80 | Preferences.Updater.skippedVersions.insert(newVersion.name) 81 | default: 82 | break 83 | } 84 | } 85 | } 86 | 87 | // MARK: - Private 88 | 89 | private extension Localized { 90 | enum Updater { 91 | static let newVersionAvailableTitle = String(localized: "TextGrabber2 %@ is available!", comment: "Title for new version available") 92 | static let learnMore = String(localized: "Learn More", comment: "Title for the \"Learn More\" button") 93 | static let remindMeLater = String(localized: "Remind Me Later", comment: "Title for the \"Remind Me Later\" button") 94 | static let skipThisVersion = String(localized: "Skip This Version", comment: "Title for the \"Skip This Version\" button") 95 | } 96 | } 97 | 98 | private extension Preferences { 99 | enum Updater { 100 | @Storage(key: "updater.skipped-versions", defaultValue: Set()) 101 | static var skippedVersions: Set 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /TextGrabber2/Sources/Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resources.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | To make localization work, always use `String(localized:comment:)` directly and add to this file. 12 | 13 | Besides, we use `string catalogs` to do the translation work: 14 | https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog 15 | */ 16 | enum Localized { 17 | static let languageIdentifier = String(localized: "en-US", comment: "Identifier used to locate localized resources") 18 | static let failedToRun = String(localized: "Failed to run \"%@\".", comment: "Error message when a system service failed") 19 | static let menuTitleHintCapture = String(localized: "Capture Screen to Detect", comment: "[Menu] Hint for capturing the screen") 20 | static let menuTitleHintCopy = String(localized: "Click to Copy", comment: "[Menu] Hint for copying text") 21 | static let menuTitleHintLimitedAccess = String(localized: "Limited Access", comment: "[Menu] Hint for limited access") 22 | static let menuTitleHowToCapture = String(localized: "How to Capture?", comment: "[Menu] How to use the app") 23 | static let menuTitleHowToSetUp = String(localized: "How to Set Up?", comment: "[Menu] How to set up the app") 24 | static let menuTitleServices = String(localized: "Services", comment: "[Menu] System services menu") 25 | static let menuTitleConfigure = String(localized: "Configure", comment: "[Menu] Configure system services") 26 | static let menuTitleDocumentation = String(localized: "Documentation", comment: "[Menu] Open the wiki for system services") 27 | static let menuTitleClipboard = String(localized: "Clipboard", comment: "[Menu] Clipboard options") 28 | static let menuTitleTranslate = String(localized: "Translate", comment: "[Menu] Translate text using the system service") 29 | static let menuTitleQuickLook = String(localized: "Quick Look", comment: "[Menu] Open the clipboard in Quick Look") 30 | static let menuTitleSaveAsFile = String(localized: "Save as File", comment: "[Menu] Save the clipboard as file") 31 | static let menuTitleCopyAll = String(localized: "Copy All", comment: "[Menu] Copy all text at once") 32 | static let menuTitleJoinWithSpaces = String(localized: "Join with Spaces", comment: "[Menu] Join all text with spaces and copy them") 33 | static let menuTitleJoinWithLineBreaks = String(localized: "Join with Line Breaks", comment: "[Menu] Join all text with line breaks and copy them") 34 | static let menuTitleJoinDirectly = String(localized: "Join Directly", comment: "[Menu] Join all text directly") 35 | static let menuTitleClearContents = String(localized: "Clear Contents", comment: "[Menu] Clear the clipboard") 36 | static let menuTitleObserveChanges = String(localized: "Observe Changes", comment: "[Menu] Observe changes to the clipboard") 37 | static let menuTitleGitHub = String(localized: "GitHub", comment: "[Menu] Open the TextGrabber2 repository on GitHub") 38 | static let menuTitleLaunchAtLogin = String(localized: "Launch at Login", comment: "[Menu] Automatically start the app at login") 39 | static let menuTitleVersion = String(localized: "Version", comment: "[Menu] Version number label") 40 | static let menuTitleQuitTextGrabber2 = String(localized: "Quit TextGrabber2", comment: "[Menu] Quit the app") 41 | } 42 | 43 | // Icon set used in the app: https://developer.apple.com/sf-symbols/ 44 | // 45 | // Note: double check availability and deployment target before adding new icons 46 | enum Icons { 47 | static let handRaisedSlash = "hand.raised.slash" 48 | static let textViewFinder = "text.viewfinder" 49 | } 50 | 51 | enum Links { 52 | static let github = "https://github.com/TextGrabber2-app/TextGrabber2" 53 | static let releases = "\(github)/releases" 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | # TextGrabber2 8 | 9 | [![](https://img.shields.io/badge/Platform-macOS_15.0+-blue?color=007bff)](https://github.com/TextGrabber2-app/TextGrabber2/releases/latest) [![](https://github.com/TextGrabber2-app/TextGrabber2/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/TextGrabber2-app/TextGrabber2/actions/workflows/build.yml) 10 | 11 | TextGrabber2 is a free and **open-source** macOS menu bar app that efficiently detects text from copied images. This eliminates the need to save images as files and then delete them solely for the purpose of text detection. 12 | 13 | Screenshot 01 Screenshot 02 14 | 15 | For example, press `Control-Shift-Command-4` to capture a portion of the screen and then open TextGrabber2 from the menu bar. 16 | 17 | It also functions effectively for any form of image copying. 18 | 19 | In macOS Tahoe and later, you can directly extract text from copied images using Spotlight, through the action called `Extract Text from Copied Image`. 20 | 21 | > Note that keyboard shortcuts can be remapped (and it's recommended since pressing 4 keys is a bit clunky). Please check out Apple's [documentation](https://support.apple.com/guide/mac-help/mchlp2271/mac) for details. 22 | > 23 | > Learn more [here](https://github.com/TextGrabber2-app/TextGrabber2/wiki#capture-screen-on-mac). 24 | > 25 | > For information on pasteboard access issues in macOS 15.4 and later, please refer to our [wiki](https://github.com/TextGrabber2-app/TextGrabber2/wiki#limited-access). 26 | 27 | > [!TIP] 28 | > Discover our other free and open-source apps at [libremac.github.io](https://libremac.github.io/). 29 | 30 | ## Installation 31 | 32 | Get `TextGrabber2.dmg` from the latest release, open it and drag `TextGrabber2.app` to `Applications`. 33 | 34 | Install TextGrabber2 35 | 36 | > TextGrabber2 checks for updates automatically. However, it's worth noting that updates will likely be infrequent, typically limited to bug fixes. 37 | > 38 | > Older builds: [macos-13](https://github.com/TextGrabber2-app/TextGrabber2/releases/tag/macos-13), [macos-14](https://github.com/TextGrabber2-app/TextGrabber2/releases/tag/macos-14). 39 | 40 | ## Why TextGrabber2 41 | 42 | TextGrabber2 is NOT a screenshot tool, meaning it doesn't require access like `Screen Recording` or `Accessibility`. It relies on the keyboard shortcuts you use daily. 43 | 44 | TextGrabber2 utilizes the built-in [Vision](https://developer.apple.com/documentation/vision/) framework, which is on-device, secure, fast, accurate, and **free**. In fact, it's often superior to many paid services. 45 | 46 | TextGrabber2 connects to [system services](https://github.com/TextGrabber2-app/TextGrabber2/wiki#connect-to-system-services), you can easily integrate your workflows. 47 | 48 | TextGrabber2 does NOT have any settings; it works magically until something goes wrong. 49 | 50 | It's simple, privacy-oriented, brutal and beautiful. 51 | 52 | ## Where is TextGrabber1 53 | 54 | TextGrabber1 does not exist; the "2" in TextGrabber2 does not indicate a version number. 55 | 56 | Here's the thing, there was a discontinued app called TextGrabber that I used a decade ago, I quite liked it. 57 | 58 | When initiating this project, I couldn't think of a better name than TextGrabber, so I decided to name it: 59 | 60 | **TextGrabber** *"too"*. 61 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/AppIcon.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill-specializations" : [ 3 | { 4 | "value" : "system-light" 5 | }, 6 | { 7 | "appearance" : "dark", 8 | "value" : "system-dark" 9 | } 10 | ], 11 | "groups" : [ 12 | { 13 | "blur-material-specializations" : [ 14 | { 15 | "value" : null 16 | }, 17 | { 18 | "appearance" : "tinted", 19 | "value" : null 20 | } 21 | ], 22 | "hidden-specializations" : [ 23 | { 24 | "idiom" : "macOS", 25 | "value" : false 26 | } 27 | ], 28 | "layers" : [ 29 | { 30 | "blend-mode-specializations" : [ 31 | { 32 | "value" : "normal" 33 | }, 34 | { 35 | "appearance" : "tinted", 36 | "value" : "normal" 37 | } 38 | ], 39 | "fill-specializations" : [ 40 | { 41 | "value" : { 42 | "linear-gradient" : [ 43 | "srgb:0.00000,0.75294,0.90980,1.00000", 44 | "srgb:0.00000,0.53333,1.00000,1.00000" 45 | ] 46 | } 47 | }, 48 | { 49 | "appearance" : "dark", 50 | "value" : { 51 | "linear-gradient" : [ 52 | "srgb:0.23529,0.82745,0.99608,1.00000", 53 | "srgb:0.00000,0.56863,1.00000,1.00000" 54 | ] 55 | } 56 | }, 57 | { 58 | "appearance" : "tinted", 59 | "value" : { 60 | "solid" : "srgb:1.00000,1.00000,1.00000,1.00000" 61 | } 62 | } 63 | ], 64 | "glass" : true, 65 | "image-name" : "Outer.svg", 66 | "name" : "Outer", 67 | "opacity-specializations" : [ 68 | { 69 | "appearance" : "tinted", 70 | "value" : 0.66 71 | } 72 | ] 73 | }, 74 | { 75 | "blend-mode" : "normal", 76 | "fill-specializations" : [ 77 | { 78 | "value" : { 79 | "linear-gradient" : [ 80 | "extended-gray:0.00000,1.00000", 81 | "extended-gray:0.20105,1.00000" 82 | ] 83 | } 84 | }, 85 | { 86 | "appearance" : "dark", 87 | "value" : { 88 | "linear-gradient" : [ 89 | "extended-gray:1.00000,1.00000", 90 | "extended-gray:0.80000,1.00000" 91 | ] 92 | } 93 | }, 94 | { 95 | "appearance" : "tinted", 96 | "value" : { 97 | "solid" : "display-p3:1.00000,1.00000,1.00000,1.00000" 98 | } 99 | } 100 | ], 101 | "glass-specializations" : [ 102 | { 103 | "value" : true 104 | }, 105 | { 106 | "appearance" : "tinted", 107 | "value" : true 108 | } 109 | ], 110 | "image-name" : "Inner.svg", 111 | "name" : "Inner", 112 | "opacity-specializations" : [ 113 | { 114 | "value" : 1 115 | }, 116 | { 117 | "appearance" : "dark", 118 | "value" : 1 119 | }, 120 | { 121 | "appearance" : "tinted", 122 | "value" : 0.66 123 | } 124 | ] 125 | } 126 | ], 127 | "lighting" : "individual", 128 | "name" : "Default", 129 | "shadow-specializations" : [ 130 | { 131 | "value" : { 132 | "kind" : "layer-color", 133 | "opacity" : 0.66 134 | } 135 | }, 136 | { 137 | "appearance" : "dark", 138 | "value" : { 139 | "kind" : "layer-color", 140 | "opacity" : 0.33 141 | } 142 | }, 143 | { 144 | "appearance" : "tinted", 145 | "value" : { 146 | "kind" : "none", 147 | "opacity" : 1 148 | } 149 | } 150 | ], 151 | "specular" : true, 152 | "translucency-specializations" : [ 153 | { 154 | "value" : { 155 | "enabled" : true, 156 | "value" : 0.5 157 | } 158 | }, 159 | { 160 | "appearance" : "tinted", 161 | "value" : { 162 | "enabled" : true, 163 | "value" : 0.5 164 | } 165 | } 166 | ] 167 | } 168 | ], 169 | "supported-platforms" : { 170 | "squares" : [ 171 | "macOS" 172 | ] 173 | } 174 | } -------------------------------------------------------------------------------- /TextGrabber2/Sources/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App.swift 3 | // TextGrabber2 4 | // 5 | // Created by cyan on 2024/3/20. 6 | // 7 | 8 | import AppKit 9 | import QuickLookUI 10 | import ServiceManagement 11 | 12 | @MainActor 13 | final class App: NSObject, NSApplicationDelegate { 14 | private var copyObserver: Task? 15 | private var currentResult: Recognizer.ResultData? 16 | private var silentDetectCount = 0 17 | 18 | private var previewingFileURL: URL { 19 | .previewingDirectory.appendingPathComponent("TextGrabber2.png") 20 | } 21 | 22 | private var isMenuVisible = false { 23 | didSet { 24 | statusItem.button?.highlight(isMenuVisible) 25 | } 26 | } 27 | 28 | private var userClickCount: Int { 29 | get { 30 | UserDefaults.standard.integer(forKey: DefaultKeys.userClickCount) 31 | } 32 | set { 33 | UserDefaults.standard.set(newValue, forKey: DefaultKeys.userClickCount) 34 | } 35 | } 36 | 37 | private lazy var statusItem: NSStatusItem = { 38 | let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 39 | item.behavior = .terminationOnRemoval 40 | item.autosaveName = Bundle.main.bundleName 41 | 42 | item.button?.image = .with(symbolName: Icons.textViewFinder, pointSize: 15) 43 | item.button?.setAccessibilityLabel("TextGrabber2") 44 | 45 | return item 46 | }() 47 | 48 | private lazy var mainMenu: NSMenu = { 49 | let menu = NSMenu() 50 | menu.delegate = self 51 | 52 | menu.addItem(hintItem) 53 | menu.addItem(howToItem) 54 | menu.addItem(.separator()) 55 | menu.addItem(servicesItem) 56 | menu.addItem(clipboardItem) 57 | menu.addItem(.separator()) 58 | menu.addItem(launchAtLoginItem) 59 | 60 | menu.addItem({ 61 | let item = NSMenuItem(title: Localized.menuTitleGitHub) 62 | item.toolTip = Links.github 63 | item.addAction { 64 | NSWorkspace.shared.safelyOpenURL(string: Links.github) 65 | } 66 | 67 | return item 68 | }()) 69 | 70 | menu.addItem(.separator()) 71 | menu.addItem({ 72 | let item = NSMenuItem(title: "\(Localized.menuTitleVersion) \(Bundle.main.shortVersionString)") 73 | item.toolTip = Links.releases 74 | item.addAction { 75 | NSWorkspace.shared.safelyOpenURL(string: Links.releases) 76 | } 77 | 78 | return item 79 | }()) 80 | 81 | menu.addItem({ 82 | let item = NSMenuItem(title: Localized.menuTitleQuitTextGrabber2, action: nil, keyEquivalent: "q") 83 | item.keyEquivalentModifierMask = .command 84 | item.addAction { 85 | NSApp.terminate(nil) 86 | } 87 | 88 | return item 89 | }()) 90 | 91 | return menu 92 | }() 93 | 94 | private let hintItem: NSMenuItem = { 95 | let item = NSMenuItem() 96 | if NSPasteboard.general.hasLimitedAccess { 97 | item.image = NSImage(systemSymbolName: Icons.handRaisedSlash, accessibilityDescription: nil) 98 | } 99 | 100 | return item 101 | }() 102 | 103 | private lazy var howToItem: NSMenuItem = { 104 | let item = NSMenuItem(title: NSPasteboard.general.hasLimitedAccess ? Localized.menuTitleHowToSetUp : Localized.menuTitleHowToCapture) 105 | item.addAction { [weak self] in 106 | let section = NSPasteboard.general.hasLimitedAccess ? "limited-access" : "capture-screen-on-mac" 107 | NSWorkspace.shared.safelyOpenURL(string: "\(Links.github)/wiki#\(section)") 108 | self?.increaseUserClickCount() 109 | } 110 | 111 | return item 112 | }() 113 | 114 | private lazy var servicesItem: NSMenuItem = { 115 | let menu = NSMenu() 116 | menu.addItem(.separator()) 117 | 118 | menu.addItem(withTitle: Localized.menuTitleConfigure) { 119 | NSWorkspace.shared.open(Services.fileURL) 120 | } 121 | 122 | menu.addItem(withTitle: Localized.menuTitleDocumentation) { 123 | NSWorkspace.shared.safelyOpenURL(string: "\(Links.github)/wiki#connect-to-system-services") 124 | } 125 | 126 | let item = NSMenuItem(title: Localized.menuTitleServices) 127 | item.submenu = menu 128 | return item 129 | }() 130 | 131 | private lazy var clipboardItem: NSMenuItem = { 132 | let menu = NSMenu() 133 | menu.autoenablesItems = false 134 | 135 | menu.addItem(translateItem) 136 | menu.addItem(quickLookItem) 137 | menu.addItem(saveImageItem) 138 | menu.addItem(.separator()) 139 | menu.addItem(copyAllItem) 140 | menu.addItem(clearContentsItem) 141 | 142 | if NSPasteboard.general.hasFullAccess { 143 | menu.addItem(.separator()) 144 | menu.addItem(observeChangesItem) 145 | } 146 | 147 | let item = NSMenuItem(title: Localized.menuTitleClipboard) 148 | item.submenu = menu 149 | return item 150 | }() 151 | 152 | private lazy var translateItem: NSMenuItem = { 153 | let item = NSMenuItem(title: Localized.menuTitleTranslate) 154 | item.addAction { [weak self] in 155 | Translator.showWindow(text: self?.currentResult?.spacesJoined ?? "") 156 | } 157 | 158 | return item 159 | }() 160 | 161 | private lazy var quickLookItem: NSMenuItem = { 162 | let item = NSMenuItem(title: Localized.menuTitleQuickLook) 163 | item.addAction { [weak self] in 164 | self?.previewCopiedImage() 165 | } 166 | 167 | return item 168 | }() 169 | 170 | private let saveImageItem: NSMenuItem = { 171 | let item = NSMenuItem(title: Localized.menuTitleSaveAsFile) 172 | item.addAction { 173 | NSPasteboard.general.saveImageAsFile() 174 | } 175 | 176 | return item 177 | }() 178 | 179 | private lazy var copyAllItem: NSMenuItem = { 180 | let menu = NSMenu() 181 | menu.addItem(withTitle: Localized.menuTitleJoinWithSpaces) { 182 | NSPasteboard.general.string = self.currentResult?.spacesJoined 183 | } 184 | 185 | menu.addItem(withTitle: Localized.menuTitleJoinWithLineBreaks) { 186 | NSPasteboard.general.string = self.currentResult?.lineBreaksJoined 187 | } 188 | 189 | menu.addItem(withTitle: Localized.menuTitleJoinDirectly) { 190 | NSPasteboard.general.string = self.currentResult?.directlyJoined 191 | } 192 | 193 | let item = NSMenuItem(title: Localized.menuTitleCopyAll) 194 | item.submenu = menu 195 | return item 196 | }() 197 | 198 | private let clearContentsItem: NSMenuItem = { 199 | let item = NSMenuItem(title: Localized.menuTitleClearContents) 200 | item.addAction { 201 | NSPasteboard.general.clearContents() 202 | } 203 | 204 | return item 205 | }() 206 | 207 | private lazy var observeChangesItem: NSMenuItem = { 208 | let cacheKey = DefaultKeys.observeChanges 209 | UserDefaults.standard.register(defaults: [cacheKey: true]) 210 | 211 | let item = NSMenuItem(title: Localized.menuTitleObserveChanges) 212 | item.addAction { [weak self, weak item] in 213 | let isOn = item?.state == .off 214 | UserDefaults.standard.set(isOn, forKey: cacheKey) 215 | 216 | item?.setOn(isOn) 217 | self?.updateObserver(isEnabled: isOn) 218 | } 219 | 220 | item.setOn(UserDefaults.standard.bool(forKey: cacheKey)) 221 | return item 222 | }() 223 | 224 | private let launchAtLoginItem: NSMenuItem = { 225 | let item = NSMenuItem(title: Localized.menuTitleLaunchAtLogin) 226 | item.addAction { [weak item] in 227 | do { 228 | try SMAppService.mainApp.toggle() 229 | } catch { 230 | Logger.log(.error, "\(error)") 231 | } 232 | 233 | item?.toggle() 234 | } 235 | 236 | item.setOn(SMAppService.mainApp.isEnabled) 237 | return item 238 | }() 239 | } 240 | 241 | // MARK: - Life Cycle 242 | 243 | extension App { 244 | func applicationDidFinishLaunching(_ notification: Notification) { 245 | Services.initialize() 246 | statusItem.isVisible = true 247 | 248 | // Observe pasteboard changes to detect silently 249 | if NSPasteboard.general.hasFullAccess && observeChangesItem.state == .on { 250 | updateObserver(isEnabled: true) 251 | } 252 | 253 | // Handle quit action manually since we don't have a window anymore 254 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 255 | let keyCode = event.keyCode 256 | let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) 257 | 258 | // Cmd-Q 259 | if keyCode == 0x0C && flags == .command { 260 | NSApp.terminate(nil) 261 | return nil 262 | } 263 | 264 | // Cmd-W 265 | if keyCode == 0x0D && flags == .command { 266 | NSApp.keyWindow?.close() 267 | return nil 268 | } 269 | 270 | return event 271 | } 272 | 273 | // Observe clicks on the status item 274 | NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in 275 | guard event.window == self?.statusItem.button?.window else { 276 | // The click was outside the status window 277 | return event 278 | } 279 | 280 | guard !event.modifierFlags.contains(.command) else { 281 | // Holding the command key usually means the icon is being dragged 282 | return event 283 | } 284 | 285 | self?.statusItemClicked() 286 | return nil 287 | } 288 | 289 | // Observe clicks outside the app 290 | NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] _ in 291 | guard self?.isMenuVisible == true, let menu = self?.mainMenu else { 292 | return 293 | } 294 | 295 | // This is needed because menuDidClose isn't reliably called 296 | self?.menuDidClose(menu) 297 | } 298 | 299 | let silentlyCheckUpdates: @Sendable () -> Void = { 300 | Task { 301 | await Updater.checkForUpdates() 302 | } 303 | } 304 | 305 | // Check for updates on launch with a delay 306 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: silentlyCheckUpdates) 307 | 308 | // Check for updates on a weekly basis, for users who never quit apps 309 | Timer.scheduledTimer(withTimeInterval: 7 * 24 * 60 * 60, repeats: true) { _ in 310 | silentlyCheckUpdates() 311 | } 312 | } 313 | 314 | func applicationWillTerminate(_ notification: Notification) { 315 | try? FileManager.default.removeItem(at: .previewingDirectory) 316 | } 317 | } 318 | 319 | // MARK: - NSMenuDelegate 320 | 321 | extension App: NSMenuDelegate { 322 | func statusItemClicked() { 323 | // Hide the user guide after the user becomes familiar 324 | if userClickCount > 3 { 325 | howToItem.isHidden = true 326 | } 327 | 328 | // Update the services menu 329 | servicesItem.submenu?.removeItems { $0 is ServiceItem } 330 | for service in Services.items.reversed() { 331 | let serviceName = service.serviceName 332 | let displayName = service.displayName ?? serviceName 333 | let item = ServiceItem(title: displayName) 334 | item.addAction { 335 | NSPasteboard.general.string = self.currentResult?.spacesJoined 336 | 337 | if !NSPerformService(serviceName, .general) { 338 | NSAlert.runModal(message: String(format: Localized.failedToRun, displayName)) 339 | } 340 | } 341 | 342 | servicesItem.submenu?.insertItem(item, at: 0) 343 | } 344 | 345 | // Rely on this instead of mutating items in menuWillOpen 346 | isMenuVisible = true 347 | startDetection(userInitiated: true) 348 | } 349 | 350 | func menuDidClose(_ menu: NSMenu) { 351 | isMenuVisible = false 352 | } 353 | } 354 | 355 | // MARK: - QLPreviewPanelDataSource 356 | 357 | extension App: @preconcurrency QLPreviewPanelDataSource { 358 | func numberOfPreviewItems(in panel: QLPreviewPanel?) -> Int { 359 | 1 360 | } 361 | 362 | func previewPanel(_ panel: QLPreviewPanel?, previewItemAt index: Int) -> (any QLPreviewItem)? { 363 | previewingFileURL as NSURL 364 | } 365 | 366 | func previewCopiedImage() { 367 | guard let pngData = NSPasteboard.general.image?.pngData else { 368 | return Logger.log(.info, "No image for preview") 369 | } 370 | 371 | NSApp.activate() 372 | try? pngData.write(to: self.previewingFileURL) 373 | 374 | let previewPanel = QLPreviewPanel.shared() 375 | previewPanel?.dataSource = self 376 | previewPanel?.reloadData() 377 | previewPanel?.makeKeyAndOrderFront(nil) 378 | } 379 | } 380 | 381 | // MARK: - Private 382 | 383 | private extension App { 384 | class ResultItem: NSMenuItem { /* Just a sub-class to be identifiable */ } 385 | class ServiceItem: NSMenuItem { /* Just a sub-class to be identifiable */ } 386 | 387 | enum DefaultKeys { 388 | static let userClickCount = "general.user-click-count" 389 | static let observeChanges = "pasteboard.observe-changes" 390 | } 391 | 392 | func clearMenuItems() { 393 | hintItem.title = NSPasteboard.general.hasLimitedAccess ? Localized.menuTitleHintLimitedAccess : Localized.menuTitleHintCapture 394 | mainMenu.removeItems { $0 is ResultItem } 395 | } 396 | 397 | func startDetection(userInitiated: Bool = false) { 398 | let newCount = NSPasteboard.general.changeCount 399 | if userInitiated && silentDetectCount == newCount { 400 | Logger.log(.debug, "Presenting previously detected results") 401 | return presentMainMenu() 402 | } 403 | 404 | currentResult = nil 405 | clearMenuItems() 406 | 407 | translateItem.isEnabled = false 408 | quickLookItem.isEnabled = false 409 | saveImageItem.isEnabled = false 410 | copyAllItem.isEnabled = false 411 | clearContentsItem.isEnabled = !NSPasteboard.general.isEmpty 412 | 413 | let image = NSPasteboard.general.image?.cgImage 414 | let text = NSPasteboard.general.string 415 | 416 | Task { 417 | if let result = await Recognizer.detect(image: image, level: .accurate) { 418 | updateResult(result, textCopied: text, in: mainMenu) 419 | } else { 420 | Logger.log(.error, "Failed to detect text from image") 421 | } 422 | 423 | if userInitiated { 424 | Logger.log(.debug, "Presenting newly detected results") 425 | presentMainMenu() 426 | } else { 427 | Logger.log(.debug, "Silently detected and cached") 428 | silentDetectCount = newCount 429 | } 430 | } 431 | } 432 | 433 | func updateObserver(isEnabled: Bool) { 434 | copyObserver?.cancel() 435 | 436 | if isEnabled { 437 | copyObserver = Task { @MainActor in 438 | for await _ in CopyObserver.default.changes() { 439 | startDetection() 440 | } 441 | } 442 | 443 | startDetection() 444 | } 445 | } 446 | 447 | func updateResult(_ imageResult: Recognizer.ResultData, textCopied: String?, in menu: NSMenu) { 448 | // Combine recognized items and copied text 449 | let allItems = imageResult.candidates + [textCopied].compactMap { $0 } 450 | let resultData = type(of: imageResult).init(candidates: allItems) 451 | 452 | guard currentResult != resultData else { 453 | #if DEBUG 454 | Logger.log(.debug, "No change in result data") 455 | #endif 456 | return 457 | } 458 | 459 | currentResult = resultData 460 | translateItem.isEnabled = resultData.candidates.hasValue 461 | quickLookItem.isEnabled = imageResult.candidates.hasValue 462 | saveImageItem.isEnabled = imageResult.candidates.hasValue 463 | copyAllItem.isEnabled = resultData.candidates.hasValue 464 | 465 | if NSPasteboard.general.hasLimitedAccess { 466 | hintItem.title = Localized.menuTitleHintLimitedAccess 467 | } else { 468 | hintItem.title = resultData.candidates.isEmpty ? Localized.menuTitleHintCapture : Localized.menuTitleHintCopy 469 | } 470 | 471 | let separator = NSMenuItem.separator() 472 | menu.insertItem(separator, at: menu.index(of: howToItem) + 1) 473 | 474 | for text in resultData.candidates.reversed() { 475 | let item = ResultItem(title: text.singleLine.truncatedToFit(width: 320, font: .menuFont(ofSize: 0))) 476 | menu.insertItem(item, at: menu.index(of: separator) + 1) 477 | 478 | item.addAction { [weak self] in 479 | NSPasteboard.general.string = text 480 | self?.increaseUserClickCount() 481 | } 482 | 483 | if item.title != text { 484 | item.toolTip = text 485 | } 486 | } 487 | } 488 | 489 | func presentMainMenu() { 490 | let location: CGPoint = { 491 | if #available(macOS 26.0, *) { 492 | return CGPoint(x: -8, y: 0) 493 | } 494 | 495 | return CGPoint(x: -8, y: (statusItem.button?.frame.height ?? 0) + 4) 496 | }() 497 | 498 | mainMenu.appearance = NSApp.effectiveAppearance 499 | mainMenu.popUp(positioning: nil, at: location, in: statusItem.button) 500 | } 501 | 502 | func increaseUserClickCount() { 503 | userClickCount += 1 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /TextGrabber2/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "%@" : { 5 | "localizations" : { 6 | "zh-Hans" : { 7 | "stringUnit" : { 8 | "state" : "translated", 9 | "value" : "%@" 10 | } 11 | }, 12 | "zh-Hant" : { 13 | "stringUnit" : { 14 | "state" : "translated", 15 | "value" : "%@" 16 | } 17 | } 18 | } 19 | }, 20 | "Capture Screen to Detect" : { 21 | "comment" : "[Menu] Hint for capturing the screen", 22 | "localizations" : { 23 | "zh-Hans" : { 24 | "stringUnit" : { 25 | "state" : "translated", 26 | "value" : "捕捉屏幕来检测" 27 | } 28 | }, 29 | "zh-Hant" : { 30 | "stringUnit" : { 31 | "state" : "translated", 32 | "value" : "擷取螢幕來檢測" 33 | } 34 | } 35 | } 36 | }, 37 | "Clear Contents" : { 38 | "comment" : "[Menu] Clear the clipboard", 39 | "localizations" : { 40 | "zh-Hans" : { 41 | "stringUnit" : { 42 | "state" : "translated", 43 | "value" : "清除内容" 44 | } 45 | }, 46 | "zh-Hant" : { 47 | "stringUnit" : { 48 | "state" : "translated", 49 | "value" : "清除內容" 50 | } 51 | } 52 | } 53 | }, 54 | "Click to Copy" : { 55 | "comment" : "[Menu] Hint for copying text", 56 | "localizations" : { 57 | "zh-Hans" : { 58 | "stringUnit" : { 59 | "state" : "translated", 60 | "value" : "点击来复制内容" 61 | } 62 | }, 63 | "zh-Hant" : { 64 | "stringUnit" : { 65 | "state" : "translated", 66 | "value" : "點選來拷貝內容" 67 | } 68 | } 69 | } 70 | }, 71 | "Clipboard" : { 72 | "comment" : "[Menu] Clipboard options", 73 | "localizations" : { 74 | "zh-Hans" : { 75 | "stringUnit" : { 76 | "state" : "translated", 77 | "value" : "剪贴板" 78 | } 79 | }, 80 | "zh-Hant" : { 81 | "stringUnit" : { 82 | "state" : "translated", 83 | "value" : "剪貼簿" 84 | } 85 | } 86 | } 87 | }, 88 | "Configure" : { 89 | "comment" : "[Menu] Configure system services", 90 | "localizations" : { 91 | "zh-Hans" : { 92 | "stringUnit" : { 93 | "state" : "translated", 94 | "value" : "配置" 95 | } 96 | }, 97 | "zh-Hant" : { 98 | "stringUnit" : { 99 | "state" : "translated", 100 | "value" : "配置" 101 | } 102 | } 103 | } 104 | }, 105 | "Copy All" : { 106 | "comment" : "[Menu] Copy all text at once", 107 | "localizations" : { 108 | "zh-Hans" : { 109 | "stringUnit" : { 110 | "state" : "translated", 111 | "value" : "复制全部" 112 | } 113 | }, 114 | "zh-Hant" : { 115 | "stringUnit" : { 116 | "state" : "translated", 117 | "value" : "拷貝全部" 118 | } 119 | } 120 | } 121 | }, 122 | "Documentation" : { 123 | "comment" : "[Menu] Open the wiki for system services", 124 | "localizations" : { 125 | "zh-Hans" : { 126 | "stringUnit" : { 127 | "state" : "translated", 128 | "value" : "文档" 129 | } 130 | }, 131 | "zh-Hant" : { 132 | "stringUnit" : { 133 | "state" : "translated", 134 | "value" : "文檔" 135 | } 136 | } 137 | } 138 | }, 139 | "en-US" : { 140 | "comment" : "Identifier used to locate localized resources", 141 | "localizations" : { 142 | "zh-Hans" : { 143 | "stringUnit" : { 144 | "state" : "translated", 145 | "value" : "zh-Hans" 146 | } 147 | }, 148 | "zh-Hant" : { 149 | "stringUnit" : { 150 | "state" : "translated", 151 | "value" : "zh-Hant" 152 | } 153 | } 154 | } 155 | }, 156 | "Extract Text from Copied Image" : { 157 | "localizations" : { 158 | "zh-Hans" : { 159 | "stringUnit" : { 160 | "state" : "translated", 161 | "value" : "从复制的图片中提取文本" 162 | } 163 | }, 164 | "zh-Hant" : { 165 | "stringUnit" : { 166 | "state" : "translated", 167 | "value" : "從拷貝的圖像中提取文字" 168 | } 169 | } 170 | } 171 | }, 172 | "Extract text from copied image using TextGrabber2." : { 173 | "localizations" : { 174 | "zh-Hans" : { 175 | "stringUnit" : { 176 | "state" : "translated", 177 | "value" : "使用 TextGrabber2 从复制的图片中提取文本。" 178 | } 179 | }, 180 | "zh-Hant" : { 181 | "stringUnit" : { 182 | "state" : "translated", 183 | "value" : "使用 TextGrabber2 從拷貝的圖像中提取文字。" 184 | } 185 | } 186 | } 187 | }, 188 | "Failed to run \"%@\"." : { 189 | "comment" : "Error message when a system service failed", 190 | "localizations" : { 191 | "zh-Hans" : { 192 | "stringUnit" : { 193 | "state" : "translated", 194 | "value" : "“%@”运行失败。" 195 | } 196 | }, 197 | "zh-Hant" : { 198 | "stringUnit" : { 199 | "state" : "translated", 200 | "value" : "“%@”執行失敗。" 201 | } 202 | } 203 | } 204 | }, 205 | "GitHub" : { 206 | "comment" : "[Menu] Open the TextGrabber2 repository on GitHub", 207 | "localizations" : { 208 | "zh-Hans" : { 209 | "stringUnit" : { 210 | "state" : "translated", 211 | "value" : "GitHub" 212 | } 213 | }, 214 | "zh-Hant" : { 215 | "stringUnit" : { 216 | "state" : "translated", 217 | "value" : "GitHub" 218 | } 219 | } 220 | } 221 | }, 222 | "How to Capture?" : { 223 | "comment" : "[Menu] How to use the app", 224 | "localizations" : { 225 | "zh-Hans" : { 226 | "stringUnit" : { 227 | "state" : "translated", 228 | "value" : "如何捕捉?" 229 | } 230 | }, 231 | "zh-Hant" : { 232 | "stringUnit" : { 233 | "state" : "translated", 234 | "value" : "如何捕捉?" 235 | } 236 | } 237 | } 238 | }, 239 | "How to Set Up?" : { 240 | "comment" : "[Menu] How to set up the app", 241 | "localizations" : { 242 | "zh-Hans" : { 243 | "stringUnit" : { 244 | "state" : "translated", 245 | "value" : "如何设置?" 246 | } 247 | }, 248 | "zh-Hant" : { 249 | "stringUnit" : { 250 | "state" : "translated", 251 | "value" : "如何設定?" 252 | } 253 | } 254 | } 255 | }, 256 | "Join Directly" : { 257 | "comment" : "[Menu] Join all text directly", 258 | "localizations" : { 259 | "zh-Hans" : { 260 | "stringUnit" : { 261 | "state" : "translated", 262 | "value" : "直接合并" 263 | } 264 | }, 265 | "zh-Hant" : { 266 | "stringUnit" : { 267 | "state" : "translated", 268 | "value" : "直接合併" 269 | } 270 | } 271 | } 272 | }, 273 | "Join with Line Breaks" : { 274 | "comment" : "[Menu] Join all text with line breaks and copy them", 275 | "localizations" : { 276 | "zh-Hans" : { 277 | "stringUnit" : { 278 | "state" : "translated", 279 | "value" : "使用换行合并" 280 | } 281 | }, 282 | "zh-Hant" : { 283 | "stringUnit" : { 284 | "state" : "translated", 285 | "value" : "使用換行合併" 286 | } 287 | } 288 | } 289 | }, 290 | "Join with Spaces" : { 291 | "comment" : "[Menu] Join all text with spaces and copy them", 292 | "localizations" : { 293 | "zh-Hans" : { 294 | "stringUnit" : { 295 | "state" : "translated", 296 | "value" : "使用空格合并" 297 | } 298 | }, 299 | "zh-Hant" : { 300 | "stringUnit" : { 301 | "state" : "translated", 302 | "value" : "使用空格合併" 303 | } 304 | } 305 | } 306 | }, 307 | "Launch at Login" : { 308 | "comment" : "[Menu] Automatically start the app at login", 309 | "localizations" : { 310 | "zh-Hans" : { 311 | "stringUnit" : { 312 | "state" : "translated", 313 | "value" : "登录时启动" 314 | } 315 | }, 316 | "zh-Hant" : { 317 | "stringUnit" : { 318 | "state" : "translated", 319 | "value" : "登入時啟動" 320 | } 321 | } 322 | } 323 | }, 324 | "Learn More" : { 325 | "comment" : "Title for the \"Learn More\" button", 326 | "localizations" : { 327 | "zh-Hans" : { 328 | "stringUnit" : { 329 | "state" : "translated", 330 | "value" : "了解更多" 331 | } 332 | }, 333 | "zh-Hant" : { 334 | "stringUnit" : { 335 | "state" : "translated", 336 | "value" : "瞭解更多" 337 | } 338 | } 339 | } 340 | }, 341 | "Limited Access" : { 342 | "comment" : "[Menu] Hint for limited access", 343 | "localizations" : { 344 | "zh-Hans" : { 345 | "stringUnit" : { 346 | "state" : "translated", 347 | "value" : "访问受限" 348 | } 349 | }, 350 | "zh-Hant" : { 351 | "stringUnit" : { 352 | "state" : "translated", 353 | "value" : "訪問受限" 354 | } 355 | } 356 | } 357 | }, 358 | "Observe Changes" : { 359 | "comment" : "[Menu] Observe changes to the clipboard", 360 | "localizations" : { 361 | "zh-Hans" : { 362 | "stringUnit" : { 363 | "state" : "translated", 364 | "value" : "监听剪贴板变化" 365 | } 366 | }, 367 | "zh-Hant" : { 368 | "stringUnit" : { 369 | "state" : "translated", 370 | "value" : "監聽剪貼簿變化" 371 | } 372 | } 373 | } 374 | }, 375 | "Preview Copied Image" : { 376 | "localizations" : { 377 | "zh-Hans" : { 378 | "stringUnit" : { 379 | "state" : "translated", 380 | "value" : "预览复制的图片" 381 | } 382 | }, 383 | "zh-Hant" : { 384 | "stringUnit" : { 385 | "state" : "translated", 386 | "value" : "預覽拷貝的圖像" 387 | } 388 | } 389 | } 390 | }, 391 | "Preview copied image using TextGrabber2." : { 392 | "localizations" : { 393 | "zh-Hans" : { 394 | "stringUnit" : { 395 | "state" : "translated", 396 | "value" : "使用 TextGrabber2 预览复制的图片。" 397 | } 398 | }, 399 | "zh-Hant" : { 400 | "stringUnit" : { 401 | "state" : "translated", 402 | "value" : "使用 TextGrabber2 預覽拷貝的圖像。" 403 | } 404 | } 405 | } 406 | }, 407 | "Quick Look" : { 408 | "comment" : "[Menu] Open the clipboard in Quick Look", 409 | "localizations" : { 410 | "zh-Hans" : { 411 | "stringUnit" : { 412 | "state" : "translated", 413 | "value" : "快速查看" 414 | } 415 | }, 416 | "zh-Hant" : { 417 | "stringUnit" : { 418 | "state" : "translated", 419 | "value" : "快速查看" 420 | } 421 | } 422 | } 423 | }, 424 | "Quit TextGrabber2" : { 425 | "comment" : "[Menu] Quit the app", 426 | "localizations" : { 427 | "zh-Hans" : { 428 | "stringUnit" : { 429 | "state" : "translated", 430 | "value" : "退出 TextGrabber2" 431 | } 432 | }, 433 | "zh-Hant" : { 434 | "stringUnit" : { 435 | "state" : "translated", 436 | "value" : "退出 TextGrabber2" 437 | } 438 | } 439 | } 440 | }, 441 | "Remind Me Later" : { 442 | "comment" : "Title for the \"Remind Me Later\" button", 443 | "localizations" : { 444 | "zh-Hans" : { 445 | "stringUnit" : { 446 | "state" : "translated", 447 | "value" : "稍后提醒我" 448 | } 449 | }, 450 | "zh-Hant" : { 451 | "stringUnit" : { 452 | "state" : "translated", 453 | "value" : "稍後提醒我" 454 | } 455 | } 456 | } 457 | }, 458 | "Result" : { 459 | "localizations" : { 460 | "zh-Hans" : { 461 | "stringUnit" : { 462 | "state" : "translated", 463 | "value" : "结果" 464 | } 465 | }, 466 | "zh-Hant" : { 467 | "stringUnit" : { 468 | "state" : "translated", 469 | "value" : "結果" 470 | } 471 | } 472 | } 473 | }, 474 | "Save as File" : { 475 | "comment" : "[Menu] Save the clipboard as file", 476 | "localizations" : { 477 | "zh-Hans" : { 478 | "stringUnit" : { 479 | "state" : "translated", 480 | "value" : "保存为文件" 481 | } 482 | }, 483 | "zh-Hant" : { 484 | "stringUnit" : { 485 | "state" : "translated", 486 | "value" : "儲存為檔案" 487 | } 488 | } 489 | } 490 | }, 491 | "Services" : { 492 | "comment" : "[Menu] System services menu", 493 | "localizations" : { 494 | "zh-Hans" : { 495 | "stringUnit" : { 496 | "state" : "translated", 497 | "value" : "服务" 498 | } 499 | }, 500 | "zh-Hant" : { 501 | "stringUnit" : { 502 | "state" : "translated", 503 | "value" : "服務" 504 | } 505 | } 506 | } 507 | }, 508 | "Skip This Version" : { 509 | "comment" : "Title for the \"Skip This Version\" button", 510 | "localizations" : { 511 | "zh-Hans" : { 512 | "stringUnit" : { 513 | "state" : "translated", 514 | "value" : "跳过这个版本" 515 | } 516 | }, 517 | "zh-Hant" : { 518 | "stringUnit" : { 519 | "state" : "translated", 520 | "value" : "跳過這個版本" 521 | } 522 | } 523 | } 524 | }, 525 | "TextGrabber2" : { 526 | "localizations" : { 527 | "zh-Hans" : { 528 | "stringUnit" : { 529 | "state" : "translated", 530 | "value" : "TextGrabber2" 531 | } 532 | }, 533 | "zh-Hant" : { 534 | "stringUnit" : { 535 | "state" : "translated", 536 | "value" : "TextGrabber2" 537 | } 538 | } 539 | } 540 | }, 541 | "TextGrabber2 %@ is available!" : { 542 | "comment" : "Title for new version available", 543 | "localizations" : { 544 | "zh-Hans" : { 545 | "stringUnit" : { 546 | "state" : "translated", 547 | "value" : "TextGrabber2 %@ 已发布!" 548 | } 549 | }, 550 | "zh-Hant" : { 551 | "stringUnit" : { 552 | "state" : "translated", 553 | "value" : "TextGrabber2 %@ 已釋出!" 554 | } 555 | } 556 | } 557 | }, 558 | "Translate" : { 559 | "comment" : "[Menu] Translate text using the system service", 560 | "localizations" : { 561 | "zh-Hans" : { 562 | "stringUnit" : { 563 | "state" : "translated", 564 | "value" : "翻译" 565 | } 566 | }, 567 | "zh-Hant" : { 568 | "stringUnit" : { 569 | "state" : "translated", 570 | "value" : "翻譯" 571 | } 572 | } 573 | } 574 | }, 575 | "Version" : { 576 | "comment" : "[Menu] Version number label", 577 | "localizations" : { 578 | "zh-Hans" : { 579 | "stringUnit" : { 580 | "state" : "translated", 581 | "value" : "版本" 582 | } 583 | }, 584 | "zh-Hant" : { 585 | "stringUnit" : { 586 | "state" : "translated", 587 | "value" : "版本" 588 | } 589 | } 590 | } 591 | } 592 | }, 593 | "version" : "1.0" 594 | } -------------------------------------------------------------------------------- /TextGrabber2.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 870826EE2EDBFF16004D7899 /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870826ED2EDBFF14004D7899 /* Translator.swift */; }; 11 | 871AFFF62E633B5C007BE57D /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 871AFFF52E633B59007BE57D /* Array+Extension.swift */; }; 12 | 8730CD062EE459B40058CDCF /* CopyObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730CD052EE459B40058CDCF /* CopyObserver.swift */; }; 13 | 8737A24A2BBEC27700042E83 /* Services in Resources */ = {isa = PBXBuildFile; fileRef = 8737A2492BBEC27700042E83 /* Services */; }; 14 | 8737A24C2BBECB8800042E83 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8737A24B2BBECB8800042E83 /* Services.swift */; }; 15 | 874C0E692DF80959006AB61C /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874C0E662DF80959006AB61C /* Updater.swift */; }; 16 | 874C0E6C2DF80A48006AB61C /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874C0E6B2DF80A48006AB61C /* Preferences.swift */; }; 17 | 875364A42BE1D6FC00611579 /* NSAlert+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875364A32BE1D6FC00611579 /* NSAlert+Extension.swift */; }; 18 | 8772FAE92BAC743D00DBEAA0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8772FAE82BAC743D00DBEAA0 /* Localizable.xcstrings */; }; 19 | 8775E08B2EB3138F0036B9BA /* Intents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8775E08A2EB3138E0036B9BA /* Intents.swift */; }; 20 | 87A7715E2E704F3800E9EE72 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 87A7715D2E704F3800E9EE72 /* AppIcon.icon */; }; 21 | 87AEC3F92EC065A900C6CD12 /* AppShortcuts.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 87AEC3F82EC065A900C6CD12 /* AppShortcuts.xcstrings */; }; 22 | 87AEC5802EC07C2600C6CD12 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AEC57F2EC07C2600C6CD12 /* URL+Extension.swift */; }; 23 | 87BB08C22DD417EC0016D061 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BB08C12DD417E70016D061 /* String+Extension.swift */; }; 24 | 87E192C32BAADE1200A87A4E /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192C22BAADE1200A87A4E /* App.swift */; }; 25 | 87E192C72BAADE1300A87A4E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87E192C62BAADE1300A87A4E /* Assets.xcassets */; }; 26 | 87E192D52BAAE12600A87A4E /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192D42BAAE12600A87A4E /* Bundle+Extension.swift */; }; 27 | 87E192D82BAAE17200A87A4E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192D72BAAE17200A87A4E /* main.swift */; }; 28 | 87E192DA2BAAE25500A87A4E /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192D92BAAE25500A87A4E /* NSImage+Extension.swift */; }; 29 | 87E192DE2BAAE38700A87A4E /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192DB2BAAE38700A87A4E /* NSMenuItem+Extension.swift */; }; 30 | 87E192DF2BAAE38700A87A4E /* NSControl+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192DC2BAAE38700A87A4E /* NSControl+Extension.swift */; }; 31 | 87E192E02BAAE38700A87A4E /* NSMenu+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192DD2BAAE38700A87A4E /* NSMenu+Extension.swift */; }; 32 | 87E192E52BAAFB1800A87A4E /* NSWorkspace+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192E42BAAFB1800A87A4E /* NSWorkspace+Extension.swift */; }; 33 | 87E192E72BAAFBC900A87A4E /* SMAppService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192E62BAAFBC900A87A4E /* SMAppService+Extension.swift */; }; 34 | 87E192E92BAAFF0200A87A4E /* Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192E82BAAFF0200A87A4E /* Resources.swift */; }; 35 | 87E192F12BABB6EA00A87A4E /* Recognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192F02BABB6EA00A87A4E /* Recognizer.swift */; }; 36 | 87E192F32BABB71D00A87A4E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192F22BABB71D00A87A4E /* NSPasteboard+Extension.swift */; }; 37 | 87E192F52BABC8FD00A87A4E /* Detector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192F42BABC8FD00A87A4E /* Detector.swift */; }; 38 | 87E192F72BABD18900A87A4E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192F62BABD18900A87A4E /* Logger.swift */; }; 39 | 87E192FB2BAC044E00A87A4E /* NSObject+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E192FA2BAC044E00A87A4E /* NSObject+Extension.swift */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 870826ED2EDBFF14004D7899 /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = ""; }; 44 | 871AFFF52E633B59007BE57D /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 45 | 8730CD052EE459B40058CDCF /* CopyObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyObserver.swift; sourceTree = ""; }; 46 | 8737A2492BBEC27700042E83 /* Services */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Services; sourceTree = ""; }; 47 | 8737A24B2BBECB8800042E83 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; }; 48 | 874C0E662DF80959006AB61C /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; 49 | 874C0E6B2DF80A48006AB61C /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 50 | 875364A32BE1D6FC00611579 /* NSAlert+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAlert+Extension.swift"; sourceTree = ""; }; 51 | 8772FAE82BAC743D00DBEAA0 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 52 | 8775E08A2EB3138E0036B9BA /* Intents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Intents.swift; sourceTree = ""; }; 53 | 87A7715D2E704F3800E9EE72 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 54 | 87AEC3F82EC065A900C6CD12 /* AppShortcuts.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = AppShortcuts.xcstrings; sourceTree = ""; }; 55 | 87AEC57F2EC07C2600C6CD12 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; 56 | 87BB08C12DD417E70016D061 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 57 | 87E192BF2BAADE1200A87A4E /* TextGrabber2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TextGrabber2.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 87E192C22BAADE1200A87A4E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 59 | 87E192C62BAADE1300A87A4E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 60 | 87E192CB2BAADE1400A87A4E /* Info.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Info.entitlements; sourceTree = ""; }; 61 | 87E192D12BAADEFE00A87A4E /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; 62 | 87E192D42BAAE12600A87A4E /* Bundle+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = ""; }; 63 | 87E192D72BAAE17200A87A4E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 64 | 87E192D92BAAE25500A87A4E /* NSImage+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = ""; }; 65 | 87E192DB2BAAE38700A87A4E /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; 66 | 87E192DC2BAAE38700A87A4E /* NSControl+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSControl+Extension.swift"; sourceTree = ""; }; 67 | 87E192DD2BAAE38700A87A4E /* NSMenu+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMenu+Extension.swift"; sourceTree = ""; }; 68 | 87E192E42BAAFB1800A87A4E /* NSWorkspace+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Extension.swift"; sourceTree = ""; }; 69 | 87E192E62BAAFBC900A87A4E /* SMAppService+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SMAppService+Extension.swift"; sourceTree = ""; }; 70 | 87E192E82BAAFF0200A87A4E /* Resources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resources.swift; sourceTree = ""; }; 71 | 87E192F02BABB6EA00A87A4E /* Recognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recognizer.swift; sourceTree = ""; }; 72 | 87E192F22BABB71D00A87A4E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; 73 | 87E192F42BABC8FD00A87A4E /* Detector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Detector.swift; sourceTree = ""; }; 74 | 87E192F62BABD18900A87A4E /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 75 | 87E192FA2BAC044E00A87A4E /* NSObject+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+Extension.swift"; sourceTree = ""; }; 76 | /* End PBXFileReference section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | 87E192BC2BAADE1200A87A4E /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | /* End PBXFrameworksBuildPhase section */ 87 | 88 | /* Begin PBXGroup section */ 89 | 8772FAE72BAC73F100DBEAA0 /* Resources */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 8737A2492BBEC27700042E83 /* Services */, 93 | 87E192C62BAADE1300A87A4E /* Assets.xcassets */, 94 | 87A7715D2E704F3800E9EE72 /* AppIcon.icon */, 95 | 8772FAE82BAC743D00DBEAA0 /* Localizable.xcstrings */, 96 | 87AEC3F82EC065A900C6CD12 /* AppShortcuts.xcstrings */, 97 | ); 98 | path = Resources; 99 | sourceTree = ""; 100 | }; 101 | 87E192B62BAADE1200A87A4E = { 102 | isa = PBXGroup; 103 | children = ( 104 | 87E192C12BAADE1200A87A4E /* TextGrabber2 */, 105 | 87E192C02BAADE1200A87A4E /* Products */, 106 | 87E192D12BAADEFE00A87A4E /* Build.xcconfig */, 107 | ); 108 | sourceTree = ""; 109 | }; 110 | 87E192C02BAADE1200A87A4E /* Products */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 87E192BF2BAADE1200A87A4E /* TextGrabber2.app */, 114 | ); 115 | name = Products; 116 | sourceTree = ""; 117 | }; 118 | 87E192C12BAADE1200A87A4E /* TextGrabber2 */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 87E192D62BAAE16200A87A4E /* Sources */, 122 | 8772FAE72BAC73F100DBEAA0 /* Resources */, 123 | 87E192D22BAAE0CD00A87A4E /* Supporting Files */, 124 | ); 125 | path = TextGrabber2; 126 | sourceTree = ""; 127 | }; 128 | 87E192D22BAAE0CD00A87A4E /* Supporting Files */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 87E192CB2BAADE1400A87A4E /* Info.entitlements */, 132 | 87E192D72BAAE17200A87A4E /* main.swift */, 133 | ); 134 | name = "Supporting Files"; 135 | sourceTree = ""; 136 | }; 137 | 87E192D32BAAE11500A87A4E /* Extensions */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 871AFFF52E633B59007BE57D /* Array+Extension.swift */, 141 | 87E192D42BAAE12600A87A4E /* Bundle+Extension.swift */, 142 | 87AEC57F2EC07C2600C6CD12 /* URL+Extension.swift */, 143 | 875364A32BE1D6FC00611579 /* NSAlert+Extension.swift */, 144 | 87E192DC2BAAE38700A87A4E /* NSControl+Extension.swift */, 145 | 87E192D92BAAE25500A87A4E /* NSImage+Extension.swift */, 146 | 87E192DD2BAAE38700A87A4E /* NSMenu+Extension.swift */, 147 | 87E192DB2BAAE38700A87A4E /* NSMenuItem+Extension.swift */, 148 | 87E192FA2BAC044E00A87A4E /* NSObject+Extension.swift */, 149 | 87E192F22BABB71D00A87A4E /* NSPasteboard+Extension.swift */, 150 | 87E192E42BAAFB1800A87A4E /* NSWorkspace+Extension.swift */, 151 | 87E192E62BAAFBC900A87A4E /* SMAppService+Extension.swift */, 152 | 87BB08C12DD417E70016D061 /* String+Extension.swift */, 153 | ); 154 | path = Extensions; 155 | sourceTree = ""; 156 | }; 157 | 87E192D62BAAE16200A87A4E /* Sources */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 87E192D32BAAE11500A87A4E /* Extensions */, 161 | 87E192C22BAADE1200A87A4E /* App.swift */, 162 | 87E192F42BABC8FD00A87A4E /* Detector.swift */, 163 | 87E192F62BABD18900A87A4E /* Logger.swift */, 164 | 8730CD052EE459B40058CDCF /* CopyObserver.swift */, 165 | 87E192F02BABB6EA00A87A4E /* Recognizer.swift */, 166 | 87E192E82BAAFF0200A87A4E /* Resources.swift */, 167 | 8737A24B2BBECB8800042E83 /* Services.swift */, 168 | 870826ED2EDBFF14004D7899 /* Translator.swift */, 169 | 8775E08A2EB3138E0036B9BA /* Intents.swift */, 170 | 874C0E662DF80959006AB61C /* Updater.swift */, 171 | 874C0E6B2DF80A48006AB61C /* Preferences.swift */, 172 | ); 173 | path = Sources; 174 | sourceTree = ""; 175 | }; 176 | /* End PBXGroup section */ 177 | 178 | /* Begin PBXNativeTarget section */ 179 | 87E192BE2BAADE1200A87A4E /* TextGrabber2 */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = 87E192CE2BAADE1400A87A4E /* Build configuration list for PBXNativeTarget "TextGrabber2" */; 182 | buildPhases = ( 183 | 87E192BB2BAADE1200A87A4E /* Sources */, 184 | 87E192BC2BAADE1200A87A4E /* Frameworks */, 185 | 87E192BD2BAADE1200A87A4E /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | ); 191 | name = TextGrabber2; 192 | productName = TextGrabber2; 193 | productReference = 87E192BF2BAADE1200A87A4E /* TextGrabber2.app */; 194 | productType = "com.apple.product-type.application"; 195 | }; 196 | /* End PBXNativeTarget section */ 197 | 198 | /* Begin PBXProject section */ 199 | 87E192B72BAADE1200A87A4E /* Project object */ = { 200 | isa = PBXProject; 201 | attributes = { 202 | BuildIndependentTargetsInParallel = 1; 203 | LastSwiftUpdateCheck = 1530; 204 | LastUpgradeCheck = 2620; 205 | TargetAttributes = { 206 | 87E192BE2BAADE1200A87A4E = { 207 | CreatedOnToolsVersion = 15.3; 208 | }; 209 | }; 210 | }; 211 | buildConfigurationList = 87E192BA2BAADE1200A87A4E /* Build configuration list for PBXProject "TextGrabber2" */; 212 | compatibilityVersion = "Xcode 14.0"; 213 | developmentRegion = en; 214 | hasScannedForEncodings = 0; 215 | knownRegions = ( 216 | en, 217 | Base, 218 | "zh-Hans", 219 | "zh-Hant", 220 | ); 221 | mainGroup = 87E192B62BAADE1200A87A4E; 222 | productRefGroup = 87E192C02BAADE1200A87A4E /* Products */; 223 | projectDirPath = ""; 224 | projectRoot = ""; 225 | targets = ( 226 | 87E192BE2BAADE1200A87A4E /* TextGrabber2 */, 227 | ); 228 | }; 229 | /* End PBXProject section */ 230 | 231 | /* Begin PBXResourcesBuildPhase section */ 232 | 87E192BD2BAADE1200A87A4E /* Resources */ = { 233 | isa = PBXResourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 8737A24A2BBEC27700042E83 /* Services in Resources */, 237 | 87A7715E2E704F3800E9EE72 /* AppIcon.icon in Resources */, 238 | 8772FAE92BAC743D00DBEAA0 /* Localizable.xcstrings in Resources */, 239 | 87AEC3F92EC065A900C6CD12 /* AppShortcuts.xcstrings in Resources */, 240 | 87E192C72BAADE1300A87A4E /* Assets.xcassets in Resources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | /* End PBXResourcesBuildPhase section */ 245 | 246 | /* Begin PBXSourcesBuildPhase section */ 247 | 87E192BB2BAADE1200A87A4E /* Sources */ = { 248 | isa = PBXSourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 8737A24C2BBECB8800042E83 /* Services.swift in Sources */, 252 | 8775E08B2EB3138F0036B9BA /* Intents.swift in Sources */, 253 | 87E192DF2BAAE38700A87A4E /* NSControl+Extension.swift in Sources */, 254 | 875364A42BE1D6FC00611579 /* NSAlert+Extension.swift in Sources */, 255 | 8730CD062EE459B40058CDCF /* CopyObserver.swift in Sources */, 256 | 87E192F32BABB71D00A87A4E /* NSPasteboard+Extension.swift in Sources */, 257 | 87E192E72BAAFBC900A87A4E /* SMAppService+Extension.swift in Sources */, 258 | 87E192F12BABB6EA00A87A4E /* Recognizer.swift in Sources */, 259 | 87E192D52BAAE12600A87A4E /* Bundle+Extension.swift in Sources */, 260 | 87E192FB2BAC044E00A87A4E /* NSObject+Extension.swift in Sources */, 261 | 874C0E6C2DF80A48006AB61C /* Preferences.swift in Sources */, 262 | 87E192DE2BAAE38700A87A4E /* NSMenuItem+Extension.swift in Sources */, 263 | 87E192E52BAAFB1800A87A4E /* NSWorkspace+Extension.swift in Sources */, 264 | 87BB08C22DD417EC0016D061 /* String+Extension.swift in Sources */, 265 | 870826EE2EDBFF16004D7899 /* Translator.swift in Sources */, 266 | 87E192D82BAAE17200A87A4E /* main.swift in Sources */, 267 | 87E192DA2BAAE25500A87A4E /* NSImage+Extension.swift in Sources */, 268 | 87E192F52BABC8FD00A87A4E /* Detector.swift in Sources */, 269 | 874C0E692DF80959006AB61C /* Updater.swift in Sources */, 270 | 87E192E02BAAE38700A87A4E /* NSMenu+Extension.swift in Sources */, 271 | 87E192F72BABD18900A87A4E /* Logger.swift in Sources */, 272 | 871AFFF62E633B5C007BE57D /* Array+Extension.swift in Sources */, 273 | 87E192C32BAADE1200A87A4E /* App.swift in Sources */, 274 | 87AEC5802EC07C2600C6CD12 /* URL+Extension.swift in Sources */, 275 | 87E192E92BAAFF0200A87A4E /* Resources.swift in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | /* End PBXSourcesBuildPhase section */ 280 | 281 | /* Begin XCBuildConfiguration section */ 282 | 87E192CC2BAADE1400A87A4E /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | baseConfigurationReference = 87E192D12BAADEFE00A87A4E /* Build.xcconfig */; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 288 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 289 | CLANG_ANALYZER_NONNULL = YES; 290 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 291 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 292 | CLANG_ENABLE_MODULES = YES; 293 | CLANG_ENABLE_OBJC_ARC = YES; 294 | CLANG_ENABLE_OBJC_WEAK = YES; 295 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 296 | CLANG_WARN_BOOL_CONVERSION = YES; 297 | CLANG_WARN_COMMA = YES; 298 | CLANG_WARN_CONSTANT_CONVERSION = YES; 299 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 300 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 301 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 302 | CLANG_WARN_EMPTY_BODY = YES; 303 | CLANG_WARN_ENUM_CONVERSION = YES; 304 | CLANG_WARN_INFINITE_RECURSION = YES; 305 | CLANG_WARN_INT_CONVERSION = YES; 306 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 308 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 310 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 311 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 312 | CLANG_WARN_STRICT_PROTOTYPES = YES; 313 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 314 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 315 | CLANG_WARN_UNREACHABLE_CODE = YES; 316 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 317 | COPY_PHASE_STRIP = NO; 318 | DEAD_CODE_STRIPPING = YES; 319 | DEBUG_INFORMATION_FORMAT = dwarf; 320 | ENABLE_STRICT_OBJC_MSGSEND = YES; 321 | ENABLE_TESTABILITY = YES; 322 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 323 | GCC_C_LANGUAGE_STANDARD = gnu17; 324 | GCC_DYNAMIC_NO_PIC = NO; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_OPTIMIZATION_LEVEL = 0; 327 | GCC_PREPROCESSOR_DEFINITIONS = ( 328 | "DEBUG=1", 329 | "$(inherited)", 330 | ); 331 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 332 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 333 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 334 | GCC_WARN_UNDECLARED_SELECTOR = YES; 335 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 336 | GCC_WARN_UNUSED_FUNCTION = YES; 337 | GCC_WARN_UNUSED_VARIABLE = YES; 338 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 339 | MACOSX_DEPLOYMENT_TARGET = 15.0; 340 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 341 | MTL_FAST_MATH = YES; 342 | ONLY_ACTIVE_ARCH = YES; 343 | SDKROOT = macosx; 344 | STRING_CATALOG_GENERATE_SYMBOLS = YES; 345 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 346 | SWIFT_EMIT_LOC_STRINGS = YES; 347 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 348 | SWIFT_STRICT_CONCURRENCY = complete; 349 | }; 350 | name = Debug; 351 | }; 352 | 87E192CD2BAADE1400A87A4E /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | baseConfigurationReference = 87E192D12BAADEFE00A87A4E /* Build.xcconfig */; 355 | buildSettings = { 356 | ALWAYS_SEARCH_USER_PATHS = NO; 357 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 358 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 359 | CLANG_ANALYZER_NONNULL = YES; 360 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 361 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 362 | CLANG_ENABLE_MODULES = YES; 363 | CLANG_ENABLE_OBJC_ARC = YES; 364 | CLANG_ENABLE_OBJC_WEAK = YES; 365 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 366 | CLANG_WARN_BOOL_CONVERSION = YES; 367 | CLANG_WARN_COMMA = YES; 368 | CLANG_WARN_CONSTANT_CONVERSION = YES; 369 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 370 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 371 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 381 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 382 | CLANG_WARN_STRICT_PROTOTYPES = YES; 383 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 384 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 385 | CLANG_WARN_UNREACHABLE_CODE = YES; 386 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 387 | COPY_PHASE_STRIP = NO; 388 | DEAD_CODE_STRIPPING = YES; 389 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 390 | ENABLE_NS_ASSERTIONS = NO; 391 | ENABLE_STRICT_OBJC_MSGSEND = YES; 392 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 393 | GCC_C_LANGUAGE_STANDARD = gnu17; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 396 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 397 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 398 | GCC_WARN_UNDECLARED_SELECTOR = YES; 399 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 400 | GCC_WARN_UNUSED_FUNCTION = YES; 401 | GCC_WARN_UNUSED_VARIABLE = YES; 402 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 403 | MACOSX_DEPLOYMENT_TARGET = 15.0; 404 | MTL_ENABLE_DEBUG_INFO = NO; 405 | MTL_FAST_MATH = YES; 406 | SDKROOT = macosx; 407 | STRING_CATALOG_GENERATE_SYMBOLS = YES; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | SWIFT_EMIT_LOC_STRINGS = YES; 410 | SWIFT_STRICT_CONCURRENCY = complete; 411 | }; 412 | name = Release; 413 | }; 414 | 87E192CF2BAADE1400A87A4E /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = 87E192D12BAADEFE00A87A4E /* Build.xcconfig */; 417 | buildSettings = { 418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 419 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 420 | CODE_SIGN_ENTITLEMENTS = TextGrabber2/Info.entitlements; 421 | CODE_SIGN_IDENTITY = "$(inherited)"; 422 | CODE_SIGN_STYLE = Automatic; 423 | COMBINE_HIDPI_IMAGES = YES; 424 | CURRENT_PROJECT_VERSION = "$(inherited)"; 425 | DEAD_CODE_STRIPPING = YES; 426 | DEVELOPMENT_TEAM = "$(inherited)"; 427 | ENABLE_HARDENED_RUNTIME = YES; 428 | GENERATE_INFOPLIST_FILE = YES; 429 | INFOPLIST_KEY_CFBundleDisplayName = TextGrabber2; 430 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 431 | INFOPLIST_KEY_LSUIElement = YES; 432 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 433 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 434 | LD_RUNPATH_SEARCH_PATHS = ( 435 | "$(inherited)", 436 | "@executable_path/../Frameworks", 437 | ); 438 | MARKETING_VERSION = "$(inherited)"; 439 | PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)"; 440 | PRODUCT_NAME = "$(TARGET_NAME)"; 441 | SWIFT_EMIT_LOC_STRINGS = YES; 442 | SWIFT_VERSION = 5.0; 443 | }; 444 | name = Debug; 445 | }; 446 | 87E192D02BAADE1400A87A4E /* Release */ = { 447 | isa = XCBuildConfiguration; 448 | baseConfigurationReference = 87E192D12BAADEFE00A87A4E /* Build.xcconfig */; 449 | buildSettings = { 450 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 451 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 452 | CODE_SIGN_ENTITLEMENTS = TextGrabber2/Info.entitlements; 453 | CODE_SIGN_IDENTITY = "$(inherited)"; 454 | CODE_SIGN_STYLE = Automatic; 455 | COMBINE_HIDPI_IMAGES = YES; 456 | CURRENT_PROJECT_VERSION = "$(inherited)"; 457 | DEAD_CODE_STRIPPING = YES; 458 | DEVELOPMENT_TEAM = "$(inherited)"; 459 | ENABLE_HARDENED_RUNTIME = YES; 460 | GENERATE_INFOPLIST_FILE = YES; 461 | INFOPLIST_KEY_CFBundleDisplayName = TextGrabber2; 462 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 463 | INFOPLIST_KEY_LSUIElement = YES; 464 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 465 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 466 | LD_RUNPATH_SEARCH_PATHS = ( 467 | "$(inherited)", 468 | "@executable_path/../Frameworks", 469 | ); 470 | MARKETING_VERSION = "$(inherited)"; 471 | PRODUCT_BUNDLE_IDENTIFIER = "$(inherited)"; 472 | PRODUCT_NAME = "$(TARGET_NAME)"; 473 | SWIFT_EMIT_LOC_STRINGS = YES; 474 | SWIFT_VERSION = 5.0; 475 | }; 476 | name = Release; 477 | }; 478 | /* End XCBuildConfiguration section */ 479 | 480 | /* Begin XCConfigurationList section */ 481 | 87E192BA2BAADE1200A87A4E /* Build configuration list for PBXProject "TextGrabber2" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | 87E192CC2BAADE1400A87A4E /* Debug */, 485 | 87E192CD2BAADE1400A87A4E /* Release */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | 87E192CE2BAADE1400A87A4E /* Build configuration list for PBXNativeTarget "TextGrabber2" */ = { 491 | isa = XCConfigurationList; 492 | buildConfigurations = ( 493 | 87E192CF2BAADE1400A87A4E /* Debug */, 494 | 87E192D02BAADE1400A87A4E /* Release */, 495 | ); 496 | defaultConfigurationIsVisible = 0; 497 | defaultConfigurationName = Release; 498 | }; 499 | /* End XCConfigurationList section */ 500 | }; 501 | rootObject = 87E192B72BAADE1200A87A4E /* Project object */; 502 | } 503 | --------------------------------------------------------------------------------