README 및 install.sh, razer-tilt.swift 파일 업데이트: 권한 설정 및 자동 재시작 로직 개선

This commit is contained in:
jsahn
2026-09-28 01:30:25 +09:00
parent a6bdd85385
commit f29845d981
3 changed files with 63 additions and 13 deletions
+50 -11
View File
@@ -9,8 +9,15 @@ func log(_ message: String) {
fflush(stdout)
}
struct ReportSource: Hashable {
let device: Int
let reportID: UInt32
}
final class State {
var buffers: [IOHIDDevice: UnsafeMutablePointer<UInt8>] = [:]
var lastBits: [ReportSource: UInt8] = [:]
var lastFired: [UInt8: TimeInterval] = [:]
var opened = false
var lastStatus = ""
}
@@ -30,16 +37,25 @@ func sendCommandKey(_ keyCode: CGKeyCode) {
up.post(tap: .cgSessionEventTap)
}
let tiltMask: UInt8 = 0x60
// Ignore a second press of the same tilt within this window (switch bounce).
let debounceInterval: TimeInterval = 0.12
// Preserve the supplied Razer report layout and Command+[ / Command+] mapping.
// The mouse repeats the tilt bit in every report while held, so fire only on the press edge.
let reportCallback: IOHIDReportCallback = {
_, result, _, _, _, report, reportLength in
guard result == kIOReturnSuccess, reportLength >= 1 else { return }
let value = report[0]
if value & 0x20 != 0 {
sendCommandKey(33)
}
if value & 0x40 != 0 {
sendCommandKey(30)
context, result, sender, _, reportID, report, reportLength in
guard result == kIOReturnSuccess, reportLength >= 1, let context = context else { return }
let state = Unmanaged<State>.fromOpaque(context).takeUnretainedValue()
let source = ReportSource(device: Int(bitPattern: sender), reportID: reportID)
let value = report[0] & tiltMask
let pressed = value & ~(state.lastBits[source] ?? 0)
state.lastBits[source] = value
let now = ProcessInfo.processInfo.systemUptime
for (mask, keyCode) in [(UInt8(0x20), CGKeyCode(33)), (UInt8(0x40), CGKeyCode(30))] where pressed & mask != 0 {
if let last = state.lastFired[mask], now - last < debounceInterval { continue }
state.lastFired[mask] = now
sendCommandKey(keyCode)
}
}
@@ -60,6 +76,8 @@ let deviceCallback: IOHIDDeviceCallback = { context, result, _, device in
let removalCallback: IOHIDDeviceCallback = { context, _, _, device in
guard let context = context else { return }
let state = Unmanaged<State>.fromOpaque(context).takeUnretainedValue()
let deviceKey = Int(bitPattern: Unmanaged.passUnretained(device).toOpaque())
state.lastBits = state.lastBits.filter { $0.key.device != deviceKey }
if let buffer = state.buffers.removeValue(forKey: device) {
IOHIDDeviceRegisterInputReportCallback(device, buffer, 0, nil, nil)
buffer.deallocate()
@@ -77,14 +95,22 @@ IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.de
log("Razer Tilt Mapper started. LEFT -> Command+[; RIGHT -> Command+]")
// macOS presents its own permission dialogs. The user grants access in Settings.
if !AXIsProcessTrusted() {
// Launchd restarts this app while permissions are missing, so prompt only once per build.
let buildStamp = (try? FileManager.default.attributesOfItem(atPath: CommandLine.arguments[0])[.modificationDate] as? Date)
.map { String($0.timeIntervalSince1970) } ?? ""
if !AXIsProcessTrusted() && UserDefaults.standard.string(forKey: "promptedBuild") != buildStamp {
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
_ = AXIsProcessTrustedWithOptions(options as CFDictionary)
UserDefaults.standard.set(buildStamp, forKey: "promptedBuild")
}
if IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeUnknown {
_ = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent)
}
func accessGranted() -> Bool {
IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted && CGPreflightPostEventAccess()
}
func updateAccess() {
let inputAllowed = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted
let outputAllowed = CGPreflightPostEventAccess()
@@ -102,6 +128,19 @@ func updateAccess() {
}
updateAccess()
// Recheck after the initial permission grant without requiring a manual restart.
let accessTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in updateAccess() }
// A running process may not see a new grant, so exit while access is missing and let
// launchd (KeepAlive) start a fresh process that picks it up.
let accessTimer: Timer? = accessGranted() ? nil : {
let started = Date()
return Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
if accessGranted() {
log("Permissions granted; restarting to apply them.")
exit(0)
}
if Date().timeIntervalSince(started) >= 20 {
log("Permissions still missing; restarting to recheck.")
exit(0)
}
}
}()
CFRunLoopRun()