Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Input Source Pro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
25C5A6B52E678D86005AB80E /* PunctuationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5A6B42E678D86005AB80E /* PunctuationService.swift */; };
25C5A6B72E679DCD005AB80E /* InputMonitoringRequiredBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C5A6B62E679DCD005AB80E /* InputMonitoringRequiredBadge.swift */; };
4A093071285DACAA00232089 /* NSToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A093070285DACAA00232089 /* NSToggleView.swift */; };
4A093073285E166E00232089 /* PreferencesVM+AppKeyboardCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A093072285E166E00232089 /* PreferencesVM+AppKeyboardCache.swift */; };
4A093075285EE55A00232089 /* RulesApplicationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A093074285EE55A00232089 /* RulesApplicationPicker.swift */; };
Expand Down Expand Up @@ -174,6 +176,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
25C5A6B42E678D86005AB80E /* PunctuationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PunctuationService.swift; sourceTree = "<group>"; };
25C5A6B62E679DCD005AB80E /* InputMonitoringRequiredBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMonitoringRequiredBadge.swift; sourceTree = "<group>"; };
4A093070285DACAA00232089 /* NSToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSToggleView.swift; sourceTree = "<group>"; };
4A093072285E166E00232089 /* PreferencesVM+AppKeyboardCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesVM+AppKeyboardCache.swift"; sourceTree = "<group>"; };
4A093074285EE55A00232089 /* RulesApplicationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesApplicationPicker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -427,6 +431,7 @@
4A2A175D280BA7FA00E13249 /* Utilities */ = {
isa = PBXGroup;
children = (
25C5A6B42E678D86005AB80E /* PunctuationService.swift */,
D598C7E228B74B16004747D1 /* Indicator */,
D598C7E428B74B48004747D1 /* AppKit */,
D598C7E328B74B32004747D1 /* Accessibility */,
Expand Down Expand Up @@ -516,6 +521,7 @@
4A2A177A280BA7FA00E13249 /* Components */ = {
isa = PBXGroup;
children = (
25C5A6B62E679DCD005AB80E /* InputMonitoringRequiredBadge.swift */,
4A2A177B280BA7FA00E13249 /* IndicatorView.swift */,
4A1BD90E287BD14000E4D8C2 /* CustomizedIndicatorView.swift */,
4AC450BE281D841300DA0329 /* PreferenceSection.swift */,
Expand Down Expand Up @@ -811,6 +817,7 @@
4A881A93288BC55E00B76498 /* PreferencesVM+KeyboardConfig.swift in Sources */,
4A2A1789280BA7FA00E13249 /* TISInputSource+Extension.swift in Sources */,
4AC50D6B283A72810034E894 /* NSColor.swift in Sources */,
25C5A6B52E678D86005AB80E /* PunctuationService.swift in Sources */,
4ACC9B812D2640F70002B8CE /* AddSwitchingGroupButton.swift in Sources */,
4A2A178C280BA7FA00E13249 /* CancelBag.swift in Sources */,
4A4167302813CAE600CCC28C /* PreferencesVM+BrowserRule.swift in Sources */,
Expand Down Expand Up @@ -878,6 +885,7 @@
4A093073285E166E00232089 /* PreferencesVM+AppKeyboardCache.swift in Sources */,
4A2A179D280BA7FA00E13249 /* GeneralSettingsView.swift in Sources */,
4A5EAA6C280BF6F700A9E332 /* AppearanceSettingsView.swift in Sources */,
25C5A6B72E679DCD005AB80E /* InputMonitoringRequiredBadge.swift in Sources */,
D5CC449E2A353E81007FF839 /* BrowserRuleMenuItem.swift in Sources */,
4A2A1780280BA7FA00E13249 /* IndicatorViewController.swift in Sources */,
4A093075285EE55A00232089 /* RulesApplicationPicker.swift in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions Input Source Pro/Models/IndicatorVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class IndicatorVM: ObservableObject {
let preferencesVM: PreferencesVM
let inputSourceVM: InputSourceVM
let permissionsVM: PermissionsVM
let punctuationService: PunctuationService

let logger = ISPLogger(category: String(describing: IndicatorVM.self))

Expand Down Expand Up @@ -62,6 +63,7 @@ final class IndicatorVM: ObservableObject {
self.preferencesVM = preferencesVM
self.applicationVM = applicationVM
self.inputSourceVM = inputSourceVM
self.punctuationService = PunctuationService(preferencesVM: preferencesVM)
state = .from(
preferencesVM: preferencesVM,
inputSourceChangeReason: .system,
Expand All @@ -71,6 +73,7 @@ final class IndicatorVM: ObservableObject {

clearAppKeyboardCacheIfNeed()
watchState()
watchPunctuationRules()
}

private func clearAppKeyboardCacheIfNeed() {
Expand All @@ -92,6 +95,23 @@ final class IndicatorVM: ObservableObject {
}
.store(in: cancelBag)
}

private func watchPunctuationRules() {
applicationVM.$appKind
.compactMap { $0 }
.sink { [weak self] appKind in
guard let self = self else { return }

let app = appKind.getApp()
if self.punctuationService.shouldEnableForApp(app) {
self.logger.debug { "Enabling English punctuation for app: \(app.localizedName ?? app.bundleIdentifier ?? "Unknown")" }
self.punctuationService.enable()
} else {
self.punctuationService.disable()
}
}
.store(in: cancelBag)
}
}

extension IndicatorVM {
Expand Down
65 changes: 65 additions & 0 deletions Input Source Pro/Models/PermissionsVM.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit
import Combine
import IOKit

@MainActor
final class PermissionsVM: ObservableObject {
Expand All @@ -9,10 +10,63 @@ final class PermissionsVM: ObservableObject {
return AXIsProcessTrustedWithOptions([checkOptPrompt: prompt] as CFDictionary?)
}

@discardableResult
static func checkInputMonitoring(prompt: Bool) -> Bool {
// Multi-strategy permission checking for better reliability

// Strategy 1: IOHIDCheckAccess (most reliable)
if checkInputMonitoringViaIOHID() {
return true
}

// Strategy 2: CGEvent.tapCreate (traditional method)
if checkInputMonitoringViaCGEvent(prompt: prompt) {
return true
}

// Strategy 3: Delayed retry for timing-sensitive cases
if !prompt {
// For non-prompt calls, try a brief delay and retry
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// Retry the check after a brief delay
_ = checkInputMonitoringViaCGEvent(prompt: false)
}
}

return false
}

private static func checkInputMonitoringViaIOHID() -> Bool {
// Use IOHIDCheckAccess for reliable permission checking
let access = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent)
return access == kIOHIDAccessTypeGranted
}

private static func checkInputMonitoringViaCGEvent(prompt: Bool) -> Bool {
let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: prompt ? .defaultTap : .listenOnly,
eventsOfInterest: 1,
callback: { _, _, event, _ in
return Unmanaged.passUnretained(event)
},
userInfo: nil
)

let hasPermission = (eventTap != nil)
if let tap = eventTap {
CFMachPortInvalidate(tap)
}
return hasPermission
}

@Published var isAccessibilityEnabled = PermissionsVM.checkAccessibility(prompt: false)
@Published var isInputMonitoringEnabled = PermissionsVM.checkInputMonitoring(prompt: false)

init() {
watchAccessibilityChange()
watchInputMonitoringChange()
}

private func watchAccessibilityChange() {
Expand All @@ -25,4 +79,15 @@ final class PermissionsVM: ObservableObject {
.first()
.assign(to: &$isAccessibilityEnabled)
}

private func watchInputMonitoringChange() {
guard !isInputMonitoringEnabled else { return }

Timer
.interval(seconds: 1)
.map { _ in Self.checkInputMonitoring(prompt: false) }
.filter { $0 }
.first()
.assign(to: &$isInputMonitoringEnabled)
}
}
8 changes: 8 additions & 0 deletions Input Source Pro/Models/PreferencesVM+AppCustomization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ extension PreferencesVM {
}
}

func setForceEnglishPunctuation(_ appCustomization: AppRule?, _ forceEnglishPunctuation: Bool) {
guard let appCustomization = appCustomization else { return }

saveContext {
appCustomization.forceEnglishPunctuation = forceEnglishPunctuation
}
}

func getAppCustomization(app: NSRunningApplication) -> AppRule? {
return getAppCustomization(bundleId: app.bundleId())
}
Expand Down
4 changes: 4 additions & 0 deletions Input Source Pro/Persistence/AppRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ extension AppRule {

return InputSource.sources.first { $0.id == inputSourceId }
}

var shouldForceEnglishPunctuation: Bool {
return forceEnglishPunctuation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="doNotRestoreKeyboard" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="doRestoreKeyboard" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="forceEnglishPunctuation" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hideIndicator" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="inputSourceId" optional="YES" attributeType="String"/>
<attribute name="removed" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
Expand Down
2 changes: 2 additions & 0 deletions Input Source Pro/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
<true/>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>NSAppleEventsUsageDescription</key>
<string>Input Source Pro needs to monitor keyboard input to automatically switch between English and Chinese punctuation marks based on your app-specific rules.</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUPublicEDKey</key>
Expand Down
4 changes: 4 additions & 0 deletions Input Source Pro/Resources/Signing.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.device.input-monitoring</key>
<true/>
</dict>
</plist>
4 changes: 4 additions & 0 deletions Input Source Pro/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
"Restore Previously Used One" = "Restore Previously Used One";
"Indicator" = "Indicator";
"Hide Indicator" = "Hide Indicator";
"Punctuation" = "Punctuation";
"ASCII Punctuation" = "ASCII Punctuation";
"Force English Punctuation" = "Force English Punctuation";
"Input Monitoring Required" = "Input Monitoring Required";
"Add Running Apps" = "Add Running Apps";
"(unknown)" = "(unknown)";

Expand Down
4 changes: 4 additions & 0 deletions Input Source Pro/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
"Restore Previously Used One" = "以前使用したものを復元";
"Indicator" = "ツールチップ";
"Hide Indicator" = "ツールチップを隠す";
"Punctuation" = "句読点";
"ASCII Punctuation" = "ASCII 句読点";
"Force English Punctuation" = "英語句読点を強制使用";
"Input Monitoring Required" = "入力監視権限が必要";
"Add Running Apps" = "実行中のアプリを追加";
"(unknown)" = "(不明)";

Expand Down
4 changes: 4 additions & 0 deletions Input Source Pro/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
"Restore Previously Used One" = "이전에 사용한 것 복원";
"Indicator" = "표시기";
"Hide Indicator" = "표시기 숨기기";
"Punctuation" = "구두점";
"ASCII Punctuation" = "ASCII 구두점";
"Force English Punctuation" = "영어 구두점 강제 사용";
"Input Monitoring Required" = "입력 모니터링 권한 필요";
"Add Running Apps" = "실행 중인 앱 추가";
"(unknown)" = "(알 수 없음)";

Expand Down
4 changes: 4 additions & 0 deletions Input Source Pro/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
"Restore Previously Used One" = "恢复上次使用的输入法";
"Indicator" = "提示";
"Hide Indicator" = "隐藏输入法提示";
"Punctuation" = "标点符号";
"ASCII Punctuation" = "ASCII 标点符号";
"Force English Punctuation" = "强制使用英文标点符号";
"Input Monitoring Required" = "需要输入监控权限";
"Add Running Apps" = "添加运行中的应用";
"(unknown)" = "(未知)";

Expand Down
4 changes: 4 additions & 0 deletions Input Source Pro/Resources/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
"Restore Previously Used One" = "復原上次使用的輸入法";
"Indicator" = "提示";
"Hide Indicator" = "隱藏輸入法提示";
"Punctuation" = "標點符號";
"ASCII Punctuation" = "ASCII 標點符號";
"Force English Punctuation" = "強制使用英文標點符號";
"Input Monitoring Required" = "需要輸入監控權限";
"Add Running Apps" = "加入執行中的 Apps";
"(unknown)" = "(未知)";

Expand Down
29 changes: 29 additions & 0 deletions Input Source Pro/UI/Components/InputMonitoringRequiredBadge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import SwiftUI

struct InputMonitoringRequiredBadge: View {
@EnvironmentObject var permissionsVM: PermissionsVM

var body: some View {
Button(action: {
NSWorkspace.shared.openInputMonitoringPreferences()
}) {
Text("Input Monitoring Required".i18n())
}
.buttonStyle(InputMonitoringRequiredButtonStyle())
.opacity(permissionsVM.isInputMonitoringEnabled ? 0 : 1)
.animation(.easeInOut, value: permissionsVM.isInputMonitoringEnabled)
}
}

struct InputMonitoringRequiredButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.font(.system(size: 10))
.padding(.horizontal, 4)
.padding(.vertical, 3)
.background(Color.orange)
.foregroundColor(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 4))
.contentShape(Rectangle())
}
}
Loading