fix: expose Caps remap permissions and diagnostics
This commit is contained in:
parent
394ff78431
commit
643a466e5c
5 changed files with 272 additions and 18 deletions
48
README.md
48
README.md
|
|
@ -6,8 +6,14 @@ Small native macOS menu-bar app. Physical Caps Lock is HID-remapped to F19, whic
|
||||||
|
|
||||||
- macOS 14 or newer and Apple Command Line Tools (install with xcode-select --install).
|
- macOS 14 or newer and Apple Command Line Tools (install with xcode-select --install).
|
||||||
- Task (Taskfile runner).
|
- Task (Taskfile runner).
|
||||||
- Accessibility permission under **System Settings -> Privacy & Security -> Accessibility**.
|
- Grant **Accessibility** under **System Settings -> Privacy & Security** to
|
||||||
- Disable **Keyboard -> Text Input -> Edit -> Use Caps Lock to switch to/from ABC**.
|
let the app suppress F19. On some macOS setups you may also need
|
||||||
|
**Input Monitoring**. The menu shows both permissions separately and
|
||||||
|
provides a grant action; active CGEventTap may work with Accessibility alone.
|
||||||
|
- **Important for users coming from the stock Caps switch:** Disable
|
||||||
|
**Keyboard -> Text Input -> Edit -> Use Caps Lock to switch to/from ABC**
|
||||||
|
while using Capswitch. That macOS shortcut must not compete with our own
|
||||||
|
input-source selector. Capswitch does not change this setting automatically.
|
||||||
- Keep the Caps Lock key set to its normal role in **Keyboard -> Keyboard Shortcuts -> Modifier Keys**, not "No Action".
|
- Keep the Caps Lock key set to its normal role in **Keyboard -> Keyboard Shortcuts -> Modifier Keys**, not "No Action".
|
||||||
- No Xcode project, root helper, Karabiner or third-party driver is needed.
|
- No Xcode project, root helper, Karabiner or third-party driver is needed.
|
||||||
|
|
||||||
|
|
@ -22,7 +28,24 @@ task bundle
|
||||||
open build/Capswitch.app
|
open build/Capswitch.app
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
Grant Accessibility permission and restart Capswitch if macOS prompts you to do so.
|
After opening Capswitch, grant **Accessibility** from the status-bar menu.
|
||||||
|
If the event tap is refused or no F19 events are received, also grant
|
||||||
|
**Input Monitoring** from the menu. Restart Capswitch after granting
|
||||||
|
permissions if macOS prompts you to do so.
|
||||||
|
|
||||||
|
If you previously used macOS's built-in Caps-to-input-source switching, open
|
||||||
|
System Settings -> Keyboard -> Text Input -> Edit and uncheck
|
||||||
|
"Use Caps Lock to switch to and from [last used Latin input source]".
|
||||||
|
Keep Caps assigned to the normal Caps Lock action in Modifier Keys.
|
||||||
|
|
||||||
|
Open the Capswitch menu and confirm:
|
||||||
|
- Accessibility: Granted
|
||||||
|
- Input Monitoring: Granted (or not granted if the active tap works)
|
||||||
|
- Event tap: Enabled
|
||||||
|
- HID Caps -> F19: configured
|
||||||
|
|
||||||
|
These confirm permissions and the global configuration, **not** yet that the
|
||||||
|
physical keyboard is actually sending F19. Use the counters to verify it.
|
||||||
|
|
||||||
## Behavior
|
## Behavior
|
||||||
|
|
||||||
|
|
@ -38,7 +61,23 @@ Open the status-bar menu while testing quick typing. It displays:
|
||||||
|
|
||||||
F19 ↓N ↑N · switches N
|
F19 ↓N ↑N · switches N
|
||||||
|
|
||||||
No typed text is recorded. If the F19 counts do not change, the HID remap is not producing F19 events. If they change but the switch count does not, inspect input source selection.
|
No typed text is recorded. If the F19 counts do not change, the HID remap is not producing F19 events
|
||||||
|
or the event tap is not receiving them. First check both permissions, the
|
||||||
|
event tap status and the native Caps switch preference. Then use the
|
||||||
|
"Reapply Caps -> F19 mapping" menu action and repeat the test.
|
||||||
|
|
||||||
|
If F19 counts increase but switches do not, investigate TIS input-source
|
||||||
|
selection. If the stock Caps input switch still operates while Capswitch
|
||||||
|
is active, the HID remap may not be applied to your physical keyboard.
|
||||||
|
|
||||||
|
To gather diagnostics without logging any typed text:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
task doctor
|
||||||
|
~~~
|
||||||
|
|
||||||
|
This prints the native Caps preference, global UserKeyMapping and the keyboard
|
||||||
|
service-specific mapping; these can differ even when hidutil exits successfully.
|
||||||
|
|
||||||
Caps+Option through IOKit and its LED behavior still require verification on an actual Mac.
|
Caps+Option through IOKit and its LED behavior still require verification on an actual Mac.
|
||||||
|
|
||||||
|
|
@ -69,6 +108,7 @@ task build
|
||||||
task bundle
|
task bundle
|
||||||
task clean
|
task clean
|
||||||
task verify
|
task verify
|
||||||
|
task doctor
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
Linux RSB runs host-independent static checks only. SwiftPM builds, XCTest, IOKit and actual keyboard input behavior must be verified on macOS with the Apple SDK.
|
Linux RSB runs host-independent static checks only. SwiftPM builds, XCTest, IOKit and actual keyboard input behavior must be verified on macOS with the Apple SDK.
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ final class HIDRemappingController {
|
||||||
// Adopt a rule left behind after Capswitch crashed. A pre-existing
|
// Adopt a rule left behind after Capswitch crashed. A pre-existing
|
||||||
// user-managed Caps -> F19 rule has no marker and is never removed.
|
// user-managed Caps -> F19 rule has no marker and is never removed.
|
||||||
ownsMapping = ownsMapping || UserDefaults.standard.bool(forKey: markerKey)
|
ownsMapping = ownsMapping || UserDefaults.standard.bool(forKey: markerKey)
|
||||||
|
if ownsMapping && !isInstalled {
|
||||||
|
// Reapply once after a crash/relaunch. Global UserKeyMapping
|
||||||
|
// may be present while a keyboard service still has no mapping.
|
||||||
|
try writeMappings(current)
|
||||||
|
}
|
||||||
isInstalled = true
|
isInstalled = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +69,26 @@ final class HIDRemappingController {
|
||||||
isInstalled = true
|
isInstalled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reapplyIfOwned() throws {
|
||||||
|
guard ownsMapping || UserDefaults.standard.bool(forKey: markerKey) else {
|
||||||
|
throw HIDRemapError.conflictingMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = try readMappings()
|
||||||
|
guard current.contains(ourMapping) else {
|
||||||
|
try ensureInstalled()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-broadcast the complete mapping table, never an empty replacement.
|
||||||
|
try writeMappings(current)
|
||||||
|
guard try readMappings().contains(ourMapping) else {
|
||||||
|
throw HIDRemapError.verificationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
isInstalled = true
|
||||||
|
}
|
||||||
|
|
||||||
func restoreIfOwned() throws {
|
func restoreIfOwned() throws {
|
||||||
guard ownsMapping || UserDefaults.standard.bool(forKey: markerKey) else {
|
guard ownsMapping || UserDefaults.standard.bool(forKey: markerKey) else {
|
||||||
isInstalled = false
|
isInstalled = false
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@ import Foundation
|
||||||
|
|
||||||
enum InputControllerError: LocalizedError {
|
enum InputControllerError: LocalizedError {
|
||||||
case accessibilityRequired
|
case accessibilityRequired
|
||||||
|
case inputMonitoringRequired
|
||||||
case eventTapUnavailable
|
case eventTapUnavailable
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .accessibilityRequired:
|
case .accessibilityRequired:
|
||||||
return "Accessibility permission is required to intercept Caps Lock."
|
return "Accessibility permission is required. Open the Capswitch menu to grant it."
|
||||||
|
case .inputMonitoringRequired:
|
||||||
|
return "Keyboard tap refused. Grant Input Monitoring from the Capswitch menu."
|
||||||
case .eventTapUnavailable:
|
case .eventTapUnavailable:
|
||||||
return "macOS did not allow the global keyboard event tap to start."
|
return "macOS did not allow the keyboard event tap. Check Accessibility and Input Monitoring permissions."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +163,36 @@ final class MacOSInputController {
|
||||||
var onStateChanged: (() -> Void)?
|
var onStateChanged: (() -> Void)?
|
||||||
|
|
||||||
var isRunning: Bool {
|
var isRunning: Bool {
|
||||||
eventTap != nil && remapper.isInstalled
|
isTapEnabled && remapper.isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTapEnabled: Bool {
|
||||||
|
guard let eventTap else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return CFMachPortIsValid(eventTap) && CGEvent.tapIsEnabled(tap: eventTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputMonitoringTrusted: Bool {
|
||||||
|
CGPreflightListenEventAccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
var hidMappingConfigured: Bool {
|
||||||
|
remapper.isInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
// This key is an undocumented macOS preference; missing means unknown,
|
||||||
|
// not "off". The UI never changes the user's global keyboard settings.
|
||||||
|
var nativeCapsSwitchState: String {
|
||||||
|
let value = UserDefaults.standard.object(forKey: "TISRomanSwitchState")
|
||||||
|
?? UserDefaults(suiteName: "com.apple.HIToolbox")?
|
||||||
|
.object(forKey: "TISRomanSwitchState")
|
||||||
|
|
||||||
|
guard let number = value as? NSNumber else {
|
||||||
|
return "Unknown (check Keyboard settings)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return number.intValue == 0 ? "Off" : "On - disable for Capswitch"
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentInputSourceName: String {
|
var currentInputSourceName: String {
|
||||||
|
|
@ -188,6 +220,9 @@ final class MacOSInputController {
|
||||||
throw InputControllerError.accessibilityRequired
|
throw InputControllerError.accessibilityRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Active .defaultTap filtering is authorized through Accessibility
|
||||||
|
// on many macOS versions. Try that first; Input Monitoring is an
|
||||||
|
// additional diagnostic/grant path if tap creation is refused.
|
||||||
// Enumerate input sources before entering the event tap callback.
|
// Enumerate input sources before entering the event tap callback.
|
||||||
inputSources.refresh()
|
inputSources.refresh()
|
||||||
|
|
||||||
|
|
@ -204,7 +239,9 @@ final class MacOSInputController {
|
||||||
callback: Self.eventTapCallback,
|
callback: Self.eventTapCallback,
|
||||||
userInfo: refcon
|
userInfo: refcon
|
||||||
) else {
|
) else {
|
||||||
throw InputControllerError.eventTapUnavailable
|
throw inputMonitoringTrusted
|
||||||
|
? InputControllerError.eventTapUnavailable
|
||||||
|
: InputControllerError.inputMonitoringRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
eventTap = tap
|
eventTap = tap
|
||||||
|
|
@ -284,6 +321,60 @@ final class MacOSInputController {
|
||||||
inputSources.refresh()
|
inputSources.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// macOS can silently disable taps after a timeout or permission change.
|
||||||
|
// Detect that before considering the HID mapping healthy.
|
||||||
|
func repairEventTapIfNeeded() {
|
||||||
|
guard let eventTap else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !isTapEnabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if CFMachPortIsValid(eventTap), accessibilityTrusted {
|
||||||
|
CGEvent.tapEnable(tap: eventTap, enable: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isTapEnabled {
|
||||||
|
lastError = "The keyboard event tap is disabled. Recheck macOS permissions."
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reapplyHIDMapping() {
|
||||||
|
guard !isPaused, eventTap != nil, isTapEnabled else {
|
||||||
|
lastError = "The keyboard event tap must be active before reapplying Caps → F19."
|
||||||
|
onStateChanged?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try remapper.reapplyIfOwned()
|
||||||
|
lastError = nil
|
||||||
|
} catch {
|
||||||
|
lastError = error.localizedDescription
|
||||||
|
}
|
||||||
|
onStateChanged?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestInputMonitoringPermission() {
|
||||||
|
_ = CGRequestListenEventAccess()
|
||||||
|
if let url = URL(
|
||||||
|
string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"
|
||||||
|
) {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openKeyboardSettings() {
|
||||||
|
if let url = URL(
|
||||||
|
string: "x-apple.systempreferences:com.apple.preference.keyboard"
|
||||||
|
) {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func repairMappingIfNeeded() {
|
func repairMappingIfNeeded() {
|
||||||
guard !isPaused, eventTap != nil else {
|
guard !isPaused, eventTap != nil else {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,36 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
action: nil,
|
action: nil,
|
||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
|
private let tapStatusItem = NSMenuItem(
|
||||||
|
title: "",
|
||||||
|
action: nil,
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
private let hidMappingItem = NSMenuItem(
|
||||||
|
title: "",
|
||||||
|
action: nil,
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
private let reapplyMappingItem = NSMenuItem(
|
||||||
|
title: "Reapply Caps → F19 mapping",
|
||||||
|
action: #selector(reapplyMapping),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
private let permissionItem = NSMenuItem(
|
private let permissionItem = NSMenuItem(
|
||||||
title: "",
|
title: "",
|
||||||
action: #selector(openAccessibilitySettings),
|
action: #selector(openAccessibilitySettings),
|
||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
|
private let inputMonitoringItem = NSMenuItem(
|
||||||
|
title: "",
|
||||||
|
action: #selector(openInputMonitoringSettings),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
private let systemSwitchItem = NSMenuItem(
|
||||||
|
title: "",
|
||||||
|
action: #selector(openKeyboardSettings),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
private let pauseItem = NSMenuItem(
|
private let pauseItem = NSMenuItem(
|
||||||
title: "",
|
title: "",
|
||||||
action: #selector(togglePaused),
|
action: #selector(togglePaused),
|
||||||
|
|
@ -77,10 +102,15 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
menu.delegate = self
|
menu.delegate = self
|
||||||
menu.addItem(currentLayoutItem)
|
menu.addItem(currentLayoutItem)
|
||||||
menu.addItem(diagnosticsItem)
|
menu.addItem(diagnosticsItem)
|
||||||
|
menu.addItem(tapStatusItem)
|
||||||
|
menu.addItem(hidMappingItem)
|
||||||
menu.addItem(errorItem)
|
menu.addItem(errorItem)
|
||||||
|
menu.addItem(reapplyMappingItem)
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
menu.addItem(pauseItem)
|
menu.addItem(pauseItem)
|
||||||
menu.addItem(permissionItem)
|
menu.addItem(permissionItem)
|
||||||
|
menu.addItem(inputMonitoringItem)
|
||||||
|
menu.addItem(systemSwitchItem)
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
let quitItem = NSMenuItem(
|
let quitItem = NSMenuItem(
|
||||||
|
|
@ -90,7 +120,14 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
)
|
)
|
||||||
menu.addItem(quitItem)
|
menu.addItem(quitItem)
|
||||||
|
|
||||||
for item in [pauseItem, permissionItem, quitItem] {
|
for item in [
|
||||||
|
pauseItem,
|
||||||
|
permissionItem,
|
||||||
|
inputMonitoringItem,
|
||||||
|
systemSwitchItem,
|
||||||
|
reapplyMappingItem,
|
||||||
|
quitItem,
|
||||||
|
] {
|
||||||
item.target = self
|
item.target = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,10 +138,13 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
private func refreshController() {
|
private func refreshController() {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
if !inputController.accessibilityTrusted && inputController.isRunning {
|
if !inputController.accessibilityTrusted,
|
||||||
|
inputController.isTapEnabled {
|
||||||
inputController.stop()
|
inputController.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputController.repairEventTapIfNeeded()
|
||||||
|
|
||||||
if inputController.accessibilityTrusted,
|
if inputController.accessibilityTrusted,
|
||||||
!inputController.isPaused,
|
!inputController.isPaused,
|
||||||
!inputController.isRunning,
|
!inputController.isRunning,
|
||||||
|
|
@ -140,6 +180,15 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
diagnosticsItem.title = inputController.diagnosticsSummary
|
diagnosticsItem.title = inputController.diagnosticsSummary
|
||||||
diagnosticsItem.isEnabled = false
|
diagnosticsItem.isEnabled = false
|
||||||
|
|
||||||
|
tapStatusItem.title = "Event tap: \(inputController.isTapEnabled ? "Enabled" : "Inactive")"
|
||||||
|
tapStatusItem.isEnabled = false
|
||||||
|
|
||||||
|
hidMappingItem.title = inputController.hidMappingConfigured
|
||||||
|
? "HID Caps → F19: configured (verify physical keyboard)"
|
||||||
|
: "HID Caps → F19: not configured"
|
||||||
|
hidMappingItem.isEnabled = false
|
||||||
|
reapplyMappingItem.isEnabled = inputController.isRunning
|
||||||
|
|
||||||
errorItem.isHidden = inputController.lastError == nil
|
errorItem.isHidden = inputController.lastError == nil
|
||||||
errorItem.title = "Error: \(inputController.lastError ?? "")"
|
errorItem.title = "Error: \(inputController.lastError ?? "")"
|
||||||
errorItem.toolTip = inputController.lastError
|
errorItem.toolTip = inputController.lastError
|
||||||
|
|
@ -150,15 +199,26 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
: "Pause switching (restore Caps)"
|
: "Pause switching (restore Caps)"
|
||||||
pauseItem.state = inputController.isPaused ? .on : .off
|
pauseItem.state = inputController.isPaused ? .on : .off
|
||||||
|
|
||||||
if inputController.accessibilityTrusted {
|
permissionItem.title = inputController.accessibilityTrusted
|
||||||
permissionItem.title = inputController.isRunning
|
? "Accessibility: Granted"
|
||||||
? "Accessibility: Granted · Caps → F19 active"
|
: "Grant Accessibility permission…"
|
||||||
: "Accessibility: Granted · switching inactive"
|
|
||||||
} else {
|
|
||||||
permissionItem.title = "Grant Accessibility permission…"
|
|
||||||
}
|
|
||||||
|
|
||||||
for item in [currentLayoutItem, permissionItem, pauseItem] {
|
inputMonitoringItem.title = inputController.inputMonitoringTrusted
|
||||||
|
? "Input Monitoring: Granted"
|
||||||
|
: "Input Monitoring: Not granted (may be needed)…"
|
||||||
|
|
||||||
|
systemSwitchItem.title =
|
||||||
|
"System Caps input switch: \(inputController.nativeCapsSwitchState)"
|
||||||
|
systemSwitchItem.toolTip =
|
||||||
|
"Keyboard → Text Input → Edit → disable 'Use Caps Lock to switch input sources'."
|
||||||
|
|
||||||
|
for item in [
|
||||||
|
currentLayoutItem,
|
||||||
|
permissionItem,
|
||||||
|
inputMonitoringItem,
|
||||||
|
systemSwitchItem,
|
||||||
|
pauseItem,
|
||||||
|
] {
|
||||||
item.target = self
|
item.target = self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,6 +242,18 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
||||||
inputController.openAccessibilitySettings()
|
inputController.openAccessibilitySettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func openInputMonitoringSettings() {
|
||||||
|
inputController.requestInputMonitoringPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func openKeyboardSettings() {
|
||||||
|
inputController.openKeyboardSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func reapplyMapping() {
|
||||||
|
inputController.reapplyHIDMapping()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func quit() {
|
@objc private func quit() {
|
||||||
NSApp.terminate(nil)
|
NSApp.terminate(nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
Taskfile.yml
26
Taskfile.yml
|
|
@ -69,11 +69,37 @@ tasks:
|
||||||
- test -f Tests/CapswitchCoreTests/HIDMappingCodecTests.swift
|
- test -f Tests/CapswitchCoreTests/HIDMappingCodecTests.swift
|
||||||
- grep -q "CGEvent.tapCreate" Sources/CapswitchApp/MacOSInputController.swift
|
- grep -q "CGEvent.tapCreate" Sources/CapswitchApp/MacOSInputController.swift
|
||||||
- grep -q "CGEvent.tapEnable" Sources/CapswitchApp/MacOSInputController.swift
|
- grep -q "CGEvent.tapEnable" Sources/CapswitchApp/MacOSInputController.swift
|
||||||
|
- grep -q "CGPreflightListenEventAccess" Sources/CapswitchApp/MacOSInputController.swift
|
||||||
|
- grep -q "CGRequestListenEventAccess" Sources/CapswitchApp/MacOSInputController.swift
|
||||||
|
- grep -q "CGEvent.tapIsEnabled" Sources/CapswitchApp/MacOSInputController.swift
|
||||||
- test "$(grep -c "CGEventTapCreate" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0
|
- test "$(grep -c "CGEventTapCreate" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0
|
||||||
- test "$(grep -c "CGEventTapEnable" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0
|
- test "$(grep -c "CGEventTapEnable" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0
|
||||||
- grep -q "TISSelectInputSource" Sources/CapswitchApp/MacOSInputController.swift
|
- grep -q "TISSelectInputSource" Sources/CapswitchApp/MacOSInputController.swift
|
||||||
- grep -q "switchToNextInputSource" Sources/Capswitch/CapsGestureMachine.swift
|
- grep -q "switchToNextInputSource" Sources/Capswitch/CapsGestureMachine.swift
|
||||||
|
|
||||||
|
doctor:
|
||||||
|
desc: Inspect macOS Caps preferences, remapping and running Capswitch processes
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
if [ "$(uname -s)" != "Darwin" ]; then
|
||||||
|
echo "SKIP: HID diagnostics require macOS."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "=== macOS ==="
|
||||||
|
sw_vers
|
||||||
|
echo "=== Native Caps input-source setting (1=enabled, 0=disabled) ==="
|
||||||
|
defaults read -g TISRomanSwitchState 2>/dev/null || true
|
||||||
|
defaults read com.apple.HIToolbox TISRomanSwitchState 2>/dev/null || true
|
||||||
|
echo "=== Global HID remappings ==="
|
||||||
|
hidutil property --get UserKeyMapping
|
||||||
|
echo "=== HID keyboard services (may vary by keyboard) ==="
|
||||||
|
hidutil property --matching '{"PrimaryUsagePage":1,"PrimaryUsage":6}' --get UserKeyMapping || true
|
||||||
|
echo "=== Running Capswitch processes ==="
|
||||||
|
pgrep -fl Capswitch || true
|
||||||
|
echo "=== Privacy permissions ==="
|
||||||
|
echo "Inspect Accessibility and Input Monitoring in the Capswitch status menu."
|
||||||
|
echo "If Input Monitoring is not granted, choose 'Grant Input Monitoring permission'."
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
desc: Remove build outputs
|
desc: Remove build outputs
|
||||||
cmds:
|
cmds:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue