Skip to content

Instantly share code, notes, and snippets.

@BalazsGyarmati
Last active April 27, 2024 13:58
Show Gist options
  • Save BalazsGyarmati/e199080a9e47733870889626609d34c5 to your computer and use it in GitHub Desktop.
Save BalazsGyarmati/e199080a9e47733870889626609d34c5 to your computer and use it in GitHub Desktop.
Use cmd + scroll wheel to zoom inside apps in MacOS
//
// 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()
@valoiscuba
Copy link

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

@BalazsGyarmati
Copy link
Author

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.

@valoiscuba
Copy link

:( ok

@jeejee19
Copy link

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 %

@rstewa
Copy link

rstewa commented Apr 15, 2023

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_macos

@jeejee19
Copy link

Hi, 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

@jeejee19
Copy link

I think the swiftc command is functioning, though nohup not?

@jeejee19
Copy link

(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]

@karkojk
Copy link

karkojk commented May 6, 2023

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"

@karkojk
Copy link

karkojk commented May 6, 2023

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.")
            }

@dede321
Copy link

dede321 commented Jun 3, 2023

Thank you sir for sharing this. Useful!

@DvoeglazovIlya
Copy link

DvoeglazovIlya commented Jun 20, 2023

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).

image

To stop script run: launchctl unload com.example.hello.plist
If you want delete it from autostart delete file com.custom.scrollzoom.plist

@TylerWhittaker
Copy link

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment