diff --git a/Info.plist b/Info.plist index a2944e5..ee7475d 100644 --- a/Info.plist +++ b/Info.plist @@ -26,6 +26,8 @@ 13.0 LSUIElement + NSSupportsAppNap + NSHighResolutionCapable NSMicrophoneUsageDescription diff --git a/package.sh b/package.sh index 73682a6..5443d45 100755 --- a/package.sh +++ b/package.sh @@ -13,7 +13,7 @@ rm -rf "$APP_BUNDLE" # 1. Compile Swift sources (Pure Native Swift) swiftc -O -target arm64-apple-macosx13.0 \ - -framework Cocoa -framework AVFoundation -framework Carbon \ + -framework Cocoa -framework AVFoundation -framework Carbon -framework AppIntents \ "$PROJECT_DIR"/src/*.swift \ -o "$BUILD_DIR/SonioxVoice" @@ -29,6 +29,10 @@ cp "$BUILD_DIR/SonioxVoice" "$APP_BUNDLE/Contents/MacOS/SonioxVoice" cp "$PROJECT_DIR/Info.plist" "$APP_BUNDLE/Contents/Info.plist" cp "$PROJECT_DIR/resources/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/AppIcon.icns" +if [ -d "$PROJECT_DIR/resources/Metadata.appintents" ]; then + cp -R "$PROJECT_DIR/resources/Metadata.appintents" "$APP_BUNDLE/Contents/Resources/" +fi + # 3. Ad-hoc Codesign codesign --force --deep --sign - "$APP_BUNDLE" diff --git a/resources/Metadata.appintents/extract.actionsdata b/resources/Metadata.appintents/extract.actionsdata new file mode 100644 index 0000000..4b12228 --- /dev/null +++ b/resources/Metadata.appintents/extract.actionsdata @@ -0,0 +1 @@ +{"actions": {"StartRecordingIntent": {"actionConfiguration": {"actionSummary": {"wrapper": {"otherParameterIdentifiers": []}}}, "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas": [], "authenticationPolicy": 0, "availabilityAnnotations": {"LNPlatformNameIOS": {"introducedVersion": "16.0"}, "LNPlatformNameMACOS": {"introducedVersion": "13.0"}, "LNPlatformNameWildcard": {"introducedVersion": "*"}}, "customIntentClassName": "StartRecordingIntent", "descriptionMetadata": {"descriptionText": {"alternatives": [], "key": "Starts voice recording and real-time word-by-word streaming."}, "searchKeywords": []}, "effectiveBundleIdentifiers": [], "fullyQualifiedTypeName": "SonioxVoice.StartRecordingIntent", "identifier": "StartRecordingIntent", "isAuthPolExplicit": false, "isDiscoverable": true, "mangledTypeName": "11SonioxVoice20StartRecordingIntentV", "mangledTypeNameByBundleIdentifier": {}, "mangledTypeNameByBundleIdentifierV2": {}, "mangledTypeNameV2": "11SonioxVoice20StartRecordingIntentV", "openAppWhenRun": false, "outputFlags": 0, "parameters": [], "presentationStyle": 0, "requiredCapabilities": [], "supportedModes": 1, "systemProtocolMetadata": [], "systemProtocolMetadataV2": [], "systemProtocols": [], "title": {"alternatives": [], "key": "Start Recording"}, "typeSpecificMetadata": [], "visibilityMetadata": {"assistantOnly": false, "isDiscoverable": true}}, "StopRecordingIntent": {"actionConfiguration": {"actionSummary": {"wrapper": {"otherParameterIdentifiers": []}}}, "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas": [], "authenticationPolicy": 0, "availabilityAnnotations": {"LNPlatformNameIOS": {"introducedVersion": "16.0"}, "LNPlatformNameMACOS": {"introducedVersion": "13.0"}, "LNPlatformNameWildcard": {"introducedVersion": "*"}}, "customIntentClassName": "StopRecordingIntent", "descriptionMetadata": {"descriptionText": {"alternatives": [], "key": "Stops active recording and finalizes text paste."}, "searchKeywords": []}, "effectiveBundleIdentifiers": [], "fullyQualifiedTypeName": "SonioxVoice.StopRecordingIntent", "identifier": "StopRecordingIntent", "isAuthPolExplicit": false, "isDiscoverable": true, "mangledTypeName": "11SonioxVoice19StopRecordingIntentV", "mangledTypeNameByBundleIdentifier": {}, "mangledTypeNameByBundleIdentifierV2": {}, "mangledTypeNameV2": "11SonioxVoice19StopRecordingIntentV", "openAppWhenRun": false, "outputFlags": 0, "parameters": [], "presentationStyle": 0, "requiredCapabilities": [], "supportedModes": 1, "systemProtocolMetadata": [], "systemProtocolMetadataV2": [], "systemProtocols": [], "title": {"alternatives": [], "key": "Stop Recording"}, "typeSpecificMetadata": [], "visibilityMetadata": {"assistantOnly": false, "isDiscoverable": true}}, "ToggleRecordingIntent": {"actionConfiguration": {"actionSummary": {"wrapper": {"otherParameterIdentifiers": []}}}, "assistantDefinedSchemaTraits": [], "assistantDefinedSchemas": [], "authenticationPolicy": 0, "availabilityAnnotations": {"LNPlatformNameIOS": {"introducedVersion": "16.0"}, "LNPlatformNameMACOS": {"introducedVersion": "13.0"}, "LNPlatformNameWildcard": {"introducedVersion": "*"}}, "customIntentClassName": "ToggleRecordingIntent", "descriptionMetadata": {"descriptionText": {"alternatives": [], "key": "Toggles voice recording on or off."}, "searchKeywords": []}, "effectiveBundleIdentifiers": [], "fullyQualifiedTypeName": "SonioxVoice.ToggleRecordingIntent", "identifier": "ToggleRecordingIntent", "isAuthPolExplicit": false, "isDiscoverable": true, "mangledTypeName": "11SonioxVoice21ToggleRecordingIntentV", "mangledTypeNameByBundleIdentifier": {}, "mangledTypeNameByBundleIdentifierV2": {}, "mangledTypeNameV2": "11SonioxVoice21ToggleRecordingIntentV", "openAppWhenRun": false, "outputFlags": 0, "parameters": [], "presentationStyle": 0, "requiredCapabilities": [], "supportedModes": 1, "systemProtocolMetadata": [], "systemProtocolMetadataV2": [], "systemProtocols": [], "title": {"alternatives": [], "key": "Toggle Recording"}, "typeSpecificMetadata": [], "visibilityMetadata": {"assistantOnly": false, "isDiscoverable": true}}}, "assistantEntities": [], "assistantIntentNegativePhrases": [], "assistantIntents": [], "autoShortcutProviderMangledName": "", "autoShortcuts": [], "entities": {}, "enums": [], "generator": {"name": "xcode-tools", "version": "17C52"}, "negativePhrases": [], "queries": {}, "shortcutTileColor": 14, "version": 1} \ No newline at end of file diff --git a/resources/Metadata.appintents/version.json b/resources/Metadata.appintents/version.json new file mode 100644 index 0000000..d072006 --- /dev/null +++ b/resources/Metadata.appintents/version.json @@ -0,0 +1,4 @@ +{ + "version" : "3.0", + "toolsVersion" : "17C52" +} diff --git a/src/HotkeyManager.swift b/src/HotkeyManager.swift index 1f6d103..c8056ce 100644 --- a/src/HotkeyManager.swift +++ b/src/HotkeyManager.swift @@ -74,6 +74,10 @@ public final class HotkeyManager { private var isCapsLockPhysicallyDown = false private var isF5PhysicallyDown = false private var isFnPhysicallyDown = false + + public var isPhysicalKeyDown: Bool { + return isOptionPhysicallyDown || isCapsLockPhysicallyDown || isF5PhysicallyDown || isFnPhysicallyDown + } private var lastFnPressTime: TimeInterval = 0 private var lastTriggerTime: TimeInterval = 0 diff --git a/src/Intents.swift b/src/Intents.swift new file mode 100644 index 0000000..e879aa0 --- /dev/null +++ b/src/Intents.swift @@ -0,0 +1,85 @@ +import Foundation +import AppIntents + +@available(macOS 13.0, *) +public struct StartRecordingIntent: AppIntent { + public static var title: LocalizedStringResource = "Start Recording" + public static var description = IntentDescription("Starts voice recording with Soniox Voice") + public static var openAppWhenRun: Bool = false + + public init() {} + + @MainActor + public func perform() async throws -> some IntentResult { + print("StartRecordingIntent: Executing startRecording via AppIntent...") + + // 1. In-process direct execution if running in the main app + if let app = AppDelegate.shared { + app.startRecording() + } + + // 2. Out-of-process IPC notification (for when Shortcuts runner executes this) + DistributedNotificationCenter.default().postNotificationName( + NSNotification.Name("com.soniox.voice.start"), + object: nil, + userInfo: nil, + deliverImmediately: true + ) + + return .result() + } +} + +@available(macOS 13.0, *) +public struct StopRecordingIntent: AppIntent { + public static var title: LocalizedStringResource = "Stop Recording" + public static var description = IntentDescription("Stops active voice recording and finalizes text paste") + public static var openAppWhenRun: Bool = false + + public init() {} + + @MainActor + public func perform() async throws -> some IntentResult { + print("StopRecordingIntent: Executing stopRecording via AppIntent...") + + if let app = AppDelegate.shared { + app.stopRecording() + } + + DistributedNotificationCenter.default().postNotificationName( + NSNotification.Name("com.soniox.voice.stop"), + object: nil, + userInfo: nil, + deliverImmediately: true + ) + + return .result() + } +} + +@available(macOS 13.0, *) +public struct ToggleRecordingIntent: AppIntent { + public static var title: LocalizedStringResource = "Toggle Recording" + public static var description = IntentDescription("Toggles voice recording on or off") + public static var openAppWhenRun: Bool = false + + public init() {} + + @MainActor + public func perform() async throws -> some IntentResult { + print("ToggleRecordingIntent: Executing toggleRecording via AppIntent...") + + if let app = AppDelegate.shared { + app.toggleRecording() + } + + DistributedNotificationCenter.default().postNotificationName( + NSNotification.Name("com.soniox.voice.toggle"), + object: nil, + userInfo: nil, + deliverImmediately: true + ) + + return .result() + } +}