mirror of
https://github.com/toothbrush/debounce-mac.git
synced 2024-09-11 05:15:45 +03:00
153 lines
4.0 KiB
Objective-C
153 lines
4.0 KiB
Objective-C
// compile and run from the command-line with:
|
|
// clang -fobjc-arc -framework Cocoa ./debounce.m -o debounce
|
|
// sudo ./debounce
|
|
|
|
/*
|
|
* Keyboard debouncer, main audience is users of flaky mechanical
|
|
* keyboards. Script heavily inspired by Brad Allred's answer on
|
|
* StackOverflow:
|
|
* <http://stackoverflow.com/questions/19646108/modify-keydown-output>.
|
|
*/
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import <AppKit/NSEvent.h>
|
|
|
|
#define DEBOUNCE_DELAY 100
|
|
#define SYNTHETIC_KB_ID 666
|
|
|
|
typedef CFMachPortRef EventTap;
|
|
|
|
@interface KeyChanger : NSObject
|
|
{
|
|
@private
|
|
EventTap _eventTap;
|
|
CFRunLoopSourceRef _runLoopSource;
|
|
long long lastKeytime;
|
|
UInt16 lastKeycode;
|
|
}
|
|
@end
|
|
|
|
CGEventRef _tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, KeyChanger* listener);
|
|
|
|
@implementation KeyChanger
|
|
|
|
- (BOOL)tapEvents
|
|
{
|
|
if (!_eventTap) {
|
|
NSLog(@"Initializing an event tap.");
|
|
|
|
_eventTap = CGEventTapCreate(kCGSessionEventTap,
|
|
kCGTailAppendEventTap,
|
|
kCGEventTapOptionDefault,
|
|
CGEventMaskBit(kCGEventKeyDown),
|
|
(CGEventTapCallBack)_tapCallback,
|
|
(__bridge void *)(self));
|
|
if (!_eventTap) {
|
|
NSLog(@"Unable to create event tap. Must run as root or add Accessibility privileges to this app.");
|
|
return NO;
|
|
}
|
|
}
|
|
CGEventTapEnable(_eventTap, TRUE);
|
|
|
|
return [self isTapActive];
|
|
}
|
|
|
|
- (BOOL)isTapActive
|
|
{
|
|
return CGEventTapIsEnabled(_eventTap);
|
|
}
|
|
|
|
- (void)listen
|
|
{
|
|
if (!_runLoopSource) {
|
|
if (_eventTap) { // Don't use [self tapActive]
|
|
_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault,
|
|
_eventTap, 0);
|
|
// Add to the current run loop.
|
|
CFRunLoopAddSource(CFRunLoopGetCurrent(), _runLoopSource,
|
|
kCFRunLoopCommonModes);
|
|
|
|
NSLog(@"Registering event tap as run loop source.");
|
|
CFRunLoopRun();
|
|
}else{
|
|
NSLog(@"No Event tap in place! You will need to call listen after tapEvents to get events.");
|
|
}
|
|
}
|
|
}
|
|
|
|
- (CGEventRef)processEvent:(CGEventRef)cgEvent
|
|
{
|
|
NSEvent* event = [NSEvent eventWithCGEvent:cgEvent];
|
|
|
|
long long currentKeytime = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
|
|
UInt16 currentKeycode = [event keyCode];
|
|
BOOL debounce = NO;
|
|
long long keyboard_id = CGEventGetIntegerValueField(cgEvent, kCGKeyboardEventKeyboardType);
|
|
|
|
if (keyboard_id != SYNTHETIC_KB_ID &&
|
|
currentKeycode == lastKeycode &&
|
|
![event isARepeat] &&
|
|
(currentKeytime - lastKeytime) < DEBOUNCE_DELAY) {
|
|
|
|
NSLog(@"BOUNCE detected!!! Character: %@",
|
|
event.characters);
|
|
NSLog(@"Time between keys: %lldms (limit <%dms)",
|
|
(currentKeytime - lastKeytime),
|
|
DEBOUNCE_DELAY);
|
|
|
|
// Cancel keypress event
|
|
debounce = YES;
|
|
}
|
|
|
|
if(debounce) {
|
|
return NULL;
|
|
}
|
|
|
|
lastKeytime = currentKeytime;
|
|
lastKeycode = currentKeycode;
|
|
|
|
return cgEvent;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
if (_runLoopSource){
|
|
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _runLoopSource, kCFRunLoopCommonModes);
|
|
CFRelease(_runLoopSource);
|
|
}
|
|
if (_eventTap){
|
|
|
|
// Kill the event tap
|
|
CGEventTapEnable(_eventTap, FALSE);
|
|
CFRelease(_eventTap);
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
CGEventRef _tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, KeyChanger* listener) {
|
|
// Do not make the NSEvent here.
|
|
// NSEvent will throw an exception if we try to make an event from the tap timeout type
|
|
@autoreleasepool {
|
|
if(type == kCGEventTapDisabledByTimeout) {
|
|
NSLog(@"Event tap has timed out, re-enabling tap.");
|
|
[listener tapEvents];
|
|
return nil;
|
|
}
|
|
if (type != kCGEventTapDisabledByUserInput) {
|
|
return [listener processEvent:event];
|
|
}
|
|
}
|
|
return event;
|
|
}
|
|
|
|
int main(int argc, const char * argv[])
|
|
{
|
|
@autoreleasepool {
|
|
KeyChanger* keyChanger = [KeyChanger new];
|
|
[keyChanger tapEvents];
|
|
[keyChanger listen]; // This is a blocking call.
|
|
}
|
|
return 0;
|
|
}
|