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
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ protocol PointerVisualizer: AnyObject {
var isPresentationActive: Bool { get set }

/// Handles a captured mouse event and updates the visualizer if needed.
func noteMouseEvent(_ mouseEvent: MouseEvent)
func display(_ mouseEvent: MouseEvent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ final class PointerVisualizersManager {
icon = PointerIconVisualizer(settings: pointerIconSettings)
}

func noteMouseEvent(_ mouseEvent: MouseEvent) {
func display(_ mouseEvent: MouseEvent) {
guard self.isPresentationActive else { return }
for visualizer in all where visualizer.isEnabled {
visualizer.noteMouseEvent(mouseEvent)
visualizer.display(mouseEvent)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ extension PointerIconVisualizer {

// MARK: - PointerVisualizer
extension PointerIconVisualizer: PointerVisualizer {
func noteMouseEvent(_ mouseEvent: MouseEvent) {
func display(_ mouseEvent: MouseEvent) {
guard self.isEnabled else { return }
self.pointerContentView.handle(mouseEvent: mouseEvent)
self.syncPresentation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public final class PointerRingVisualizer {
// MARK: - PointerVisualizer

extension PointerRingVisualizer: PointerVisualizer {
public func noteMouseEvent(_ mouseEvent: MouseEvent) {
public func display(_ mouseEvent: MouseEvent) {
guard self.isEnabled else { return }
if mouseEvent.type == .scrollWheel { return }
self.visualizerWindow?.update(with: mouseEvent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ final class CaptureController {
self.pointerVisualizersManager = pointerVisualizersManager
self.keyboardVisualizer = keyboardVisualizer
self.permissionsService = permissionsService
self.eventTap.delegate = self
self.eventTap.onOutput = { [weak self] output in
self?.handle(output)
}
self.eventProcessor.onItemProduced = { [keyboardVisualizer] item in
keyboardVisualizer.display(item)
}
Expand Down Expand Up @@ -166,9 +168,35 @@ private extension CaptureController {
}
}

// MARK: - EventTapDelegate
extension CaptureController: EventTapDelegate {
func eventTap(_ tap: EventTap, didChangeState state: EventTap.State) {
// MARK: - Event Tap Output
private extension CaptureController {
func handle(_ output: EventTap.Output) {
switch output {
case .stateChanged(let state):
self.handleTapStateChange(state)

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 }
self.eventProcessor.processMediaKey(mediaKey)

case .mouse(let mouseEvent):
guard self.isCapturing else { return }
Task { @MainActor [pointerVisualizersManager = self.pointerVisualizersManager] in
pointerVisualizersManager.display(mouseEvent)
}
self.eventProcessor.processMouseEvent(mouseEvent)
}
}

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

switch state {
Expand All @@ -182,27 +210,4 @@ extension CaptureController: EventTapDelegate {
self.handleTapFailure()
}
}

func eventTap(_ tap: EventTap, noteKeystroke keystroke: StandardKeyEvent) {
guard self.isCapturing else { return }
self.eventProcessor.processKeystroke(keystroke)
}

func eventTap(_ tap: EventTap, noteFlagsChanged flags: NSEvent.ModifierFlags) {
guard self.isCapturing else { return }
self.eventProcessor.processFlagsChanged(flags)
}

func eventTap(_ tap: EventTap, noteMediaKey mediaKey: MediaKeyEvent) {
guard self.isCapturing, mediaKey.isRecognized else { return }
self.eventProcessor.processMediaKey(mediaKey)
}

func eventTap(_ tap: EventTap, noteMouseEvent mouseEvent: MouseEvent) {
guard self.isCapturing else { return }
Task { @MainActor [pointerVisualizersManager = self.pointerVisualizersManager] in
pointerVisualizersManager.noteMouseEvent(mouseEvent)
}
self.eventProcessor.processMouseEvent(mouseEvent)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@ import Cocoa

extension EventTap {
enum Error: LocalizedError, Equatable {
case keyTapCreationFailed
case mouseAndFlagsTapCreationFailed
/// A tap could not be created.
case creationFailed(Kind)

var errorDescription: String? {
switch self {
case .keyTapCreationFailed:
return L10n.EventTap.keyTapCreationFailed
case .mouseAndFlagsTapCreationFailed:
return L10n.EventTap.mouseAndFlagsTapCreationFailed
}
L10n.EventTap.creationFailed
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// EventTap+InstalledTap.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import Cocoa

extension EventTap {
/// A mach port and its run loop source, kept together so they are torn down together.
struct InstalledTap {
let machPort: CFMachPort
let runLoopSource: CFRunLoopSource
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// EventTap+Kind.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import Cocoa

extension EventTap {
enum Kind: CaseIterable {
case key
case mouseAndFlags
}
}

extension EventTap.Kind {
var eventsOfInterest: CGEventMask {
switch self {
case .key:
return Self.mask(.keyDown, .keyUp, .systemDefined)
case .mouseAndFlags:
return Self.mask(.leftMouseDown, .leftMouseUp, .rightMouseDown, .rightMouseUp,
.leftMouseDragged, .rightMouseDragged,
.otherMouseDown, .otherMouseUp, .otherMouseDragged,
.scrollWheel, .flagsChanged)
}
}

private static func mask(_ types: CGEventType...) -> CGEventMask {
types.reduce(0) { $0 | CGEventMask(1 << $1.rawValue) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// EventTap+Output.swift
// Keyty
//
// SPDX-FileCopyrightText: 2026 Serhii Bykov
// SPDX-License-Identifier: BSD-3-Clause
//

import AppKit

extension EventTap {
/// Everything an installed tap reports to its owner.
///
/// Captured input and lifecycle changes share one channel so that a consumer
/// handles them in a single ordered switch rather than across several callbacks.
enum Output {
case keystroke(StandardKeyEvent)
case mouse(MouseEvent)
case mediaKey(MediaKeyEvent)
case modifierFlags(NSEvent.ModifierFlags)
case stateChanged(EventTap.State)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@

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

/// Both taps are 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)

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

This file was deleted.

Loading
Loading