-
-
Save BalazsGyarmati/e199080a9e47733870889626609d34c5 to your computer and use it in GitHub Desktop.
// | |
// main.swift | |
// scroll_to_plusminus | |
// | |
// Created by uniqueidentifier on 2021-01-08. | |
// Modified by alex on 2022-07-08 to use modifiers for scrolling | |
// Modified by BalazsGyarmati on 2023-01-04 to use command instead of control + respect any keyboard layout for + and - | |
// | |
import Foundation | |
import CoreGraphics | |
// | |
// https://jjrscott.com/how-to-convert-ascii-character-to-cgkeycode/ | |
// CGKeyCodeInitializers.swift START | |
// | |
// Created by John Scott on 09/02/2022. | |
// | |
import AppKit | |
extension CGKeyCode { | |
public init?(character: String) { | |
if let keyCode = Initializers.shared.characterKeys[character] { | |
self = keyCode | |
} else { | |
return nil | |
} | |
} | |
public init?(modifierFlag: NSEvent.ModifierFlags) { | |
if let keyCode = Initializers.shared.modifierFlagKeys[modifierFlag] { | |
self = keyCode | |
} else { | |
return nil | |
} | |
} | |
public init?(specialKey: NSEvent.SpecialKey) { | |
if let keyCode = Initializers.shared.specialKeys[specialKey] { | |
self = keyCode | |
} else { | |
return nil | |
} | |
} | |
private struct Initializers { | |
let specialKeys: [NSEvent.SpecialKey:CGKeyCode] | |
let characterKeys: [String:CGKeyCode] | |
let modifierFlagKeys: [NSEvent.ModifierFlags:CGKeyCode] | |
static let shared = Initializers() | |
init() { | |
var specialKeys = [NSEvent.SpecialKey:CGKeyCode]() | |
var characterKeys = [String:CGKeyCode]() | |
var modifierFlagKeys = [NSEvent.ModifierFlags:CGKeyCode]() | |
for keyCode in (0..<128).map({ CGKeyCode($0) }) { | |
guard let cgevent = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(keyCode), keyDown: true) else { continue } | |
guard let nsevent = NSEvent(cgEvent: cgevent) else { continue } | |
var hasHandledKeyCode = false | |
if nsevent.type == .keyDown { | |
if let specialKey = nsevent.specialKey { | |
hasHandledKeyCode = true | |
specialKeys[specialKey] = keyCode | |
} else if let characters = nsevent.charactersIgnoringModifiers, !characters.isEmpty && characters != "\u{0010}" { | |
hasHandledKeyCode = true | |
characterKeys[characters] = keyCode | |
} | |
} else if nsevent.type == .flagsChanged, let modifierFlag = nsevent.modifierFlags.first(.capsLock, .shift, .control, .option, .command, .help, .function) { | |
hasHandledKeyCode = true | |
modifierFlagKeys[modifierFlag] = keyCode | |
} | |
if !hasHandledKeyCode { | |
#if DEBUG | |
print("Unhandled keycode \(keyCode): \(nsevent)") | |
#endif | |
} | |
} | |
self.specialKeys = specialKeys | |
self.characterKeys = characterKeys | |
self.modifierFlagKeys = modifierFlagKeys | |
} | |
} | |
} | |
extension NSEvent.ModifierFlags: Hashable { } | |
extension OptionSet { | |
public func first(_ options: Self.Element ...) -> Self.Element? { | |
for option in options { | |
if contains(option) { | |
return option | |
} | |
} | |
return nil | |
} | |
} | |
// CGKeyCodeInitializers.swift END | |
func KeyPress(_ key: CGKeyCode, _ down: Bool, _ command: Bool) { | |
if | |
let source = CGEventSource( stateID: .privateState ), | |
let event = CGEvent( keyboardEventSource: source, virtualKey: key, keyDown: down ) { | |
if(command) { | |
event.flags = CGEventFlags.maskCommand | |
} | |
event.type = down ? .keyDown : .keyUp | |
event.post( tap: .cghidEventTap ) | |
} | |
} | |
/* | |
enum Key: CGKeyCode { | |
case left = 123, right = 124, minus = 27, plus = 24, command = 55 | |
func press(_ down:Bool, _ command:Bool) { | |
if | |
let source = CGEventSource( stateID: .privateState ), | |
let event = CGEvent( keyboardEventSource: source, virtualKey: rawValue, keyDown: down ) { | |
if(command) { | |
event.flags = CGEventFlags.maskCommand | |
} | |
event.type = down ? .keyDown : .keyUp | |
event.post( tap: .cghidEventTap ) | |
} | |
} | |
} | |
*/ | |
class EventTap { | |
static var rloop_source: CFRunLoopSource! = nil | |
class func create(){ | |
if rloop_source != nil | |
{ EventTap.remove() } | |
let tap = CGEventTap.create( callback: tap_callback )! | |
rloop_source | |
= CFMachPortCreateRunLoopSource( kCFAllocatorDefault, tap, CFIndex(0) ) | |
CFRunLoopAddSource( CFRunLoopGetCurrent(),rloop_source, .commonModes ) | |
CGEvent.tapEnable( tap: tap, enable: true ) | |
CFRunLoopRun() | |
} | |
class func remove(){ | |
if rloop_source != nil { | |
CFRunLoopRemoveSource( CFRunLoopGetCurrent(), rloop_source, .commonModes ) | |
rloop_source = nil | |
} | |
} | |
@objc class func handle_event( proxy: CGEventTapProxy, type: CGEventType, | |
event immutable_event: CGEvent!, refcon: UnsafeMutableRawPointer? ) -> CGEvent? { | |
guard let event = immutable_event else { return nil } | |
switch type { | |
case .scrollWheel: | |
let delta_y = event.getIntegerValueField(.scrollWheelEventDeltaAxis1) | |
guard let plusKeyCode = CGKeyCode(character: "+") else { fatalError() } | |
guard let minusKeyCode = CGKeyCode(character: "-") else { fatalError() } | |
let key : CGKeyCode = (delta_y > 0) ? plusKeyCode : ( (delta_y < 0) ? minusKeyCode : 55 ) | |
let flagsP: CGEventFlags = event.flags; | |
if ((flagsP.contains(CGEventFlags.maskCommand))) { | |
/* | |
if (delta_y != 0) { | |
print("cmd+scroll",delta_y) | |
} | |
*/ | |
KeyPress(key, true, true) | |
KeyPress(key, false, true) | |
return nil | |
} | |
//print("scroll",delta_y) | |
return event | |
case .keyDown, .keyUp: | |
//let code = event.getIntegerValueField(.keyboardEventKeycode) | |
//print("Caught a keydown: \(code)! Flags: \(event.flags)") | |
return event | |
// fallthrough | |
default: | |
//let code1 = event.getIntegerValueField(.keyboardEventKeycode) | |
//print("Caught a \(type): \(code1)!") | |
//defer { exit_program() } | |
return event | |
} | |
} | |
} | |
func exit_program(){ | |
EventTap.remove() | |
exit(0) | |
} | |
private typealias CGEventTap = CFMachPort | |
extension CGEventTap { | |
fileprivate class func create( | |
callback: @escaping CGEventTapCallBack | |
) -> CGEventTap? { | |
/* | |
leftMouseDown = 1 | |
leftMouseUp = 2 | |
rightMouseDown = 3 | |
rightMouseUp = 4 | |
mouseMoved = 5 | |
leftMouseDragged = 6 | |
rightMouseDragged = 7 | |
keyDown = 10 | |
keyUp = 11 | |
flagsChanged = 12 | |
scrollWheel = 22 | |
tabletPointer = 23 | |
tabletProximity = 24 | |
otherMouseDown = 25 | |
otherMouseUp = 26 | |
otherMouseDragged = 27 | |
*/ | |
let mask: UInt32 | |
= (1 << 1) | (1 << 3) | (1 << 6) | (1 << 7) | (1 << 10) | (1 << 22) | (1 << 25) | (1 << 27) | |
let tap: CFMachPort! = CGEvent.tapCreate( | |
tap: .cgSessionEventTap /*.cghidEventTap*/, | |
place: .headInsertEventTap, | |
options: .defaultTap, | |
// eventsOfInterest: CGEventMask( 1 << CGEventType.scrollWheel.rawValue ), | |
eventsOfInterest: CGEventMask(mask), | |
callback: callback, | |
userInfo: nil | |
) | |
assert( tap != nil, "Failed to create event tap" ) | |
return tap | |
} | |
} | |
let tap_callback: CGEventTapCallBack = { | |
proxy, type, event, refcon in | |
guard let event = EventTap.handle_event( proxy: proxy, type:type, event:event, refcon:refcon ) | |
else { return nil } | |
return Unmanaged<CGEvent>.passRetained( event ) | |
} | |
EventTap.create() |
I was unable to insert a working character from the main keyboard, but this works for me
let plusKeyCode = CGKeyCode(0x1B)
This place quad
let plusKeyCode = CGKeyCode(0x1B)
if minusKeyCode == 0 {
fatalError("Failed to initialize plusKeyCode with valid key code.")
}
Thank you sir for sharing this. Useful!
Thanks, works on macos venture.
If you want it to autostart for all users yon can make it like this:
Go to "/Library/LaunchAgents" (from root) and create file for example I named it like "com.custom.scrollzoom.plist"
Add this code to it:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.custom.scrollzoom</string>
<key>ProgramArguments</key>
<array>
<string>/opt/scrollzoom/scrollzoom</string>
</array>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
Where "/opt/scrollzoom/scrollzoom" is you script location.
Go to "/Library/LaunchAgents" and start script: launchctl load com.custom.scrollzoom.plist
Then in "System Settings" -> "Privacy & Security" -> "Accessibility" enable your script (see screenshot).
To stop script run: launchctl unload com.example.hello.plist
If you want delete it from autostart delete file com.custom.scrollzoom.plist
When a I compile in mojave, I got "error: the compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions = (1 << 1) | (1 << 3) | (1 << 6) | (1 << 7) | (1 << 10) | (1 << 22) | (1 << 25) | (1 << 27) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~" :( please help
MuhammedZakir posted a solution in the original thread. The compiler suggestion is right; you just have to split the line into a few subexpressions. I'm kind of surprised that the Swift compiler struggles with this small number of bitwise operations. It knows it's a UInt32, and you're doing just doing a few shifts and ORs -- why is it struggling with type checking this expression "in a reasonable time"?
Solution from the original thread (replace the original mask line 236-237 with the following):
// Split the expression due to compilation error.
let _mask1: UInt32 = (1 << 1) | (1 << 3) | (1 << 6)
let _mask2: UInt32 = _mask1 | (1 << 7) | (1 << 10)
let mask: UInt32 = _mask2 | (1 << 22) | (1 << 25) | (1 << 27)
@BalazsGyarmati What if I want it bind right/left arrow key ? i.e scroll up => right arrow, scroll down => left arrow. And without command key ? The app will run under specific condition.
I just don't know the numerical value.
I'm working on Windows and now trying out a MacBook, so thanks for this code.
I have a problem with the "WIN/CMD + =" key code though, I'm using a classic Logitech keyboard via the USB monitor the MacBook is running on.
Shrinking "WIN/CMD + -" on the numeric keypad runs fine on the mouse, but on the classic keyboard "WIN/CMD + =" runs fine but doesn't work on the mouse.
I tried using the Karabiner EventViewer program to respond to a keystroke
I test using the mouse application "Logi Options" when pressing "WIN/CMD + =" it shows "WIN/CMD + -"
So I'm looking for a way to get the right character here instead of the "+"
I'm also looking for a substitution for this case of WIN/CMD -> CTRL.
I'm using both WIN and MAC and I keep getting "overwritten"