-
-
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() |
Nice, thanks.
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
sorry, I can not help you with that - I didn't even have Mac before version Monterey :/ Mojave seems to be just too old for this.
:( ok
I would love to have this working, but somehow not getting there. compile worked, and this is what I get when command is given for running in background: (base) jj@MB documents % nohup ./scrollzoom &
[1] 60850
(base) jj@MB documents % appending output to nohup.out
[1] + trace trap nohup ./scrollzoom
(base) jj@JJMB documents %
It looks like it worked. 60850 is the PID. You can verify by opening Activity Monitor and searching for scroll_to_plusminus_macos to see if it is running.
Another option is to list the running processes inside the terminal and using grep to see if it is running:
ps -ax | grep scroll_to_plusminus_macosHi, I don't see it running in activity monitor regretfully..
Tried to run again:
(base) jj@JJMB documents % swiftc scroll_to_plusminus_macos.swift -o scrollzoom
(base) jj@JJMB documents % nohup ./scrollzoom &
[1] 44142
(base) jj@JJMB documents % appending output to nohup.out
[1] + trace trap nohup ./scrollzoom
I think the swiftc command is functioning, though nohup not?
(base) jj@JJMB documents % ./scrollzoom
scrollzoom/scroll_to_plusminus_macos.swift:247: Assertion failed: Failed to create event tap
zsh: trace trap ./scrollzoom
(base) jj@JJMB documents % exit
Saving session...
...copying shared history...
...saving history...truncating history files...
...completed.
[Process completed]
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
{
"type": "down",
"name": {"key_code": "hyphen"},
"usagePage": "7 (0x0007)",
"usage": "45 (0x002d)",
"misc": ""
}
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 "+"
guard let plusKeyCode = CGKeyCode(character: "+") else { fatalError() }
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"
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.

compile it like:
swiftc scroll_to_plusminus_macos.swift -o scrollzoom
and then run it in the background:
nohup ./scrollzoom &