Skip to content
Merged
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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ final class CaptureController {
var onCapturingChanged: ((Bool) -> Void)?
private var shouldCapture: Bool = true
private var state: State = .idle
private var tapState: TapState = .idle
private var tapDisableCount: Int = 0
private let maxTapDisableCountBeforeReinstall = 3

private let eventTap = EventTap()
private let eventTap: any EventTapping
private let eventProcessor = EventProcessor()
private let pointerVisualizersManager: PointerVisualizersManager
private let keyboardVisualizer: KeyboardVisualizer
Expand All @@ -27,13 +26,18 @@ final class CaptureController {
init(
pointerVisualizersManager: PointerVisualizersManager,
keyboardVisualizer: KeyboardVisualizer,
permissionsService: any PermissionsService
permissionsService: any PermissionsService,
eventTap: any EventTapping = EventTap()
) {
self.pointerVisualizersManager = pointerVisualizersManager
self.keyboardVisualizer = keyboardVisualizer
self.permissionsService = permissionsService
self.eventTap.onOutput = { [weak self] output in
self?.handle(output)
self.eventTap = eventTap
self.eventTap.onEvent = { [weak self] event in
self?.handle(event)
}
self.eventTap.onStateChanged = { [weak self] state in
self?.handleTapStateChange(state)
}
self.eventProcessor.onItemProduced = { [keyboardVisualizer] item in
keyboardVisualizer.display(item)
Expand Down Expand Up @@ -119,7 +123,6 @@ private extension CaptureController {
// MARK: - Capture Lifecycle
private extension CaptureController {
func applyCapturing(_ capturing: Bool) {
guard !capturing || self.eventTap.isInstalled else { return }
let wasCapturing = self.isCapturing
Task { @MainActor [pointerVisualizersManager = self.pointerVisualizersManager, keyboardVisualizer = self.keyboardVisualizer] in
pointerVisualizersManager.isPresentationActive = capturing
Expand Down Expand Up @@ -153,42 +156,25 @@ private extension CaptureController {
self.stopCapture()
self.state = .blockedByTapFailure
}

func updateTapState(from state: EventTap.State) {
switch state {
case .idle:
self.tapState = .idle
case .installed:
self.tapState = .active
case .temporarilyDisabled:
self.tapState = .recovering
case .failed:
self.tapState = .failed
}
}
}

// MARK: - Event Tap Output
// MARK: - Captured Events
private extension CaptureController {
func handle(_ output: EventTap.Output) {
switch output {
case .stateChanged(let state):
self.handleTapStateChange(state)
func handle(_ event: EventTap.Event) {
guard self.isCapturing else { return }

switch event {
case .keystroke(let keystroke):
guard self.isCapturing else { return }
self.eventProcessor.processKeystroke(keystroke)

case .modifierFlags(let flags):
guard self.isCapturing else { return }
self.eventProcessor.processFlagsChanged(flags)

case .mediaKey(let mediaKey):
guard self.isCapturing, mediaKey.isRecognized else { return }
guard mediaKey.isRecognized else { return }
self.eventProcessor.processMediaKey(mediaKey)

case .mouse(let mouseEvent):
guard self.isCapturing else { return }
Task { @MainActor [pointerVisualizersManager = self.pointerVisualizersManager] in
pointerVisualizersManager.display(mouseEvent)
}
Expand All @@ -197,15 +183,18 @@ private extension CaptureController {
}

func handleTapStateChange(_ state: EventTap.State) {
self.updateTapState(from: state)

switch state {
case .idle, .installed:
case .idle:
self.tapDisableCount = 0
case .temporarilyDisabled:
case .installed:
break
case .disabled:
self.tapDisableCount += 1
guard self.tapDisableCount >= self.maxTapDisableCountBeforeReinstall else { return }
self.reinstallEventTap()
if self.tapDisableCount >= self.maxTapDisableCountBeforeReinstall {
self.reinstallEventTap()
} else {
self.eventTap.reenable()
}
case .failed:
self.handleTapFailure()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,26 @@
// SPDX-License-Identifier: BSD-3-Clause
//

import Cocoa

extension EventTap {
enum DisableReason: Equatable {
/// The system disabled the tap because the callback stopped responding quickly enough.
case timeout

/// The system disabled the tap after user input re-enabled secure or direct input handling.
case userInput

/// `nil` for any event type that is not a tap-disabled notification.
init?(eventType: CGEventType) {
switch eventType {
case .tapDisabledByTimeout:
self = .timeout
case .tapDisabledByUserInput:
self = .userInput
default:
return nil
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import Cocoa

extension EventTap {
enum Error: LocalizedError, Equatable {
/// A tap could not be created.
case creationFailed(Kind)
/// The system refused to create the tap's mach port, which is how a missing Accessibility grant surfaces.
case portCreationFailed

/// The mach port could not be attached to a run loop.
case runLoopSourceCreationFailed

var errorDescription: String? {
L10n.EventTap.creationFailed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// EventTap+Event.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension EventTap {
enum Event {
case keystroke(StandardKeyEvent)
case mouse(MouseEvent)
case mediaKey(MediaKeyEvent)
case modifierFlags(NSEvent.ModifierFlags)
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@

extension EventTap {
enum State: Equatable {
/// No taps are installed, either before the first `install()` or after `remove()`.
/// No tap is installed, either before the first `install()` or after `remove()`.
case idle

/// Both taps are installed and the system is delivering events.
/// The tap is installed and the system is delivering events.
case installed

/// Still installed, but the system turned a tap off; it is re-enabled automatically.
case temporarilyDisabled(EventTap.DisableReason)
/// Installed, but the system turned the tap off.
case disabled(EventTap.DisableReason)

/// A tap could not be created, so nothing is installed.
/// The tap could not be created, so nothing is installed.
case failed(EventTap.Error)
}
}
Loading
Loading