Keyboard redefined - part 2

the consequences of using a PC keyboard for my Mac

Saturday, September 19, 2020

This is part 2 of my “Keyboard redefined” post. In part 1 I wrote about the tools and native ways I encountered to customize my keyboard.

I bought this Qisan Magicforce 68 mechanical keyboard, with a PC layout. A nice compact PC keyboard with proper arrow-keys. The main goal was to swap the Window (❖)- and Alt- keys for the Command (⌘)- and Option (⌥) keys.

The keyboard has only 5 rows, the main reason to combine three keys into one Escape key. By default the function of this combined Escape-key can be modified with fn+q. Functional but not convenient. I prefer to have a dedicated key for grave accent (`) and tilde (~).

Escape

I looked at the macOS native options, invested some time exploring the available tools and decided to use Karabiner-Elements, Goku and Hammerspoon.

There is overlap in functionality between Karabiner-Elements and Hammerspoon. At first I used Karabiner-Elements only to swap the left-option (⌥) and left-command (⌘) key and a hyperkey for Hammerspoon. That didn’t work as nicely as I expected. In my current setup I use:

  • Karabiner-Elements to manage the keyboard customizations,
  • Goku to generate the Karabiner-Elements configuration file and
  • Hammerspoon to do some automation tasks

Karabiner-Elements

Karabiner-Elements logo

My keyboard customizations are divided in three parts:

  • Specific for the compact PC keyboard in my case the Qisan Magicforce 68
  • Specific for the native Apple keyboards
  • Generic shortcuts

Before we dive into Goku and the keyboard customizations first the application preferences.

Preferences

The preferences/configuration screens of my Karabiner-Elements setup.

Simple modifications

In part 1 an example was shown that swapped the left Option (⌥) and left Command (⌘) keys. My current configuration has no simple modifications. Karabiner GUI with Simple modifications

Complex modifications

Initially the “Complex modifications”-tab is empty. After running “Goku” this tab will be filled.

Devices

Under “Basic configuration” make sure your keyboard is selected (☑️).

Profiles

The “Default” profile is a requirement for Goku. In some cases you need to create the ‘Default’ profile in other cases you need to rename the profile to ‘Default’.

Karabiner Profiles GUI

Misc

My menu bar is already filled with icons so I unchecked the option “Show icon in menu bar …”

Karabiner Misc GUI

Debugging

Karabiner-Elements comes with a very nice debugging tool, Karabiner-Eventviewer. When you work with Goku or directly with Karabiner-Elements this tool shows all keyboard events. An essential tool!

Karabiner-Eventviewer logo

It happened to me a couple of times that the GUI of Karabiner-Elements or Karabiner-Eventviewer would not display. A work-around is to open the application from the commandline.

Open Karabiner-Elements from the CLI:

open -a "Karabiner-Elements"

or the Karabiner-Eventviewer

open -a "Karabiner-Eventviewer"

Goku

Goku generates the Karabiner-Elements configuration file ‘karabiner.json’ from the file ‘karabiner.edn’. The default location for this file is ‘.config’ directory.

~/.config$ tree
.
├── karabiner
│   ├── assets
│   │   └── complex_modifications
│   ├── automatic_backups
│   └── karabiner.json
└── karabiner.edn

The Goku/Karabiner file below is fully documented, including the specific workarounds for the Qisan “PC” keyboard. I created a cleaned version, without the “PC” keyboard mappings, for my colleague.

karabiner.edn

Some rules are commented-out, their purpose is either as alternative option for an existing command or as showcase of what is possible.

{
    ;; !  | means mandatory -   modifier(s) alone when pressend change behavior
    ;; #  | means optional  -   modifiers are optional (but atleast one necessary)

    ;; :!Ca is keycode :a and prefix a with !C

    ;; C  | left_command
    ;; T  | left_control
    ;; O  | left_option
    ;; S  | left_shift
    ;; F  | fn
    ;; Q  | right_command
    ;; W  | right_control
    ;; E  | right_option
    ;; R  | right_shift

    ;; ## | optional any
    ;; !! | command + control + optional + shift (hyper)

    ;; Install via brew: `brew install yqrashawn/goku/goku`

    ;; karabiner docs: ;; https://pqrs.org/osx/karabiner/json.html#complex_modifications-manipulator-definition
    ;; <other options> includs ~to_if_alone~, ~to_if_held_down~,~to_after_key_up~, ~to_delayed_action~ and ~parameters~.

    ;; (custom variables) & modifiers -> Advanced
    ;; https://github.com/yqrashawn/GokuRakuJoudo/blob/master/examples.org#custom-variable

    ;; Examples: https://github.com/yqrashawn/GokuRakuJoudo/blob/master/examples.org
    ;; https://gist.github.com/kaushikgopal/ff7a92bbc887e59699c804b59074a126
    ;; https://github.com/kchen0x/k-goku/blob/master/karabiner.edn

:profiles {
    :Default { :default true
        :sim     50     ;; simultaneous_threshold_milliseconds (def: 50)
                        ;; keys need to be pressed within this threshold to be considered simultaneous
        :delay   500    ;; to_delayed_action_delay_milliseconds (def: 500)
                        ;; basically it means time after which the key press is count delayed
        :alone   500    ;; to_if_alone_timeout_milliseconds (def: 1000)
                        ;; hold for 995s and single tap registered; hold for 1005s and seen as modifier
        :held    1000   ;; to_if_held_down_threshold_milliseconds (def: 500)
                        ;; key is fired twice when 500 ms is elapsed (otherwise seen as a hold command)
    }
} ;; profiles

:templates {
    :open "open -a '%s'"                                ;; :open "Safari"
    :openApp "open \"%s\""                              ;; :open "/Applications/Safari.app"
    :applescript "osascript '/Users/albert/Library/Mobile Documents/com~apple~ScriptEditor2/Documents/%s'"
                                                        ;; Full path to user's 'iCloud Drive/Script Editor'
                                                        ;; You can't use '~' for the users home directory
    :type "osascript -e 'tell application \"System Events\" to keystroke \"%s\" as text'"
    :beep "osascript -e 'beep'"                         ;; Play default beep
    :purr "afplay /System/Library/Sounds/Purr.aiff"     ;; Play the Purr sound
    :tink "afplay /System/Library/Sounds/Tink.aiff"     ;; Play the Tink sound
} ;; templates


:applications {                                         ;; Get the app id: `osascript -e 'id of app "VSCodium"'`
    :chrome ["^com\\.google\\.Chrome$"]
    :safari ["^com\\.apple\\.Safari$"]
    :firefox ["^org\\.mozilla\\.firefoxdeveloperedition$"]
    :finder ["^com\\.apple\\.finder$"]
    :code ["^com\\.visualstudio\\.code\\.oss$"
           "^com\\.microsoft\\.VSCode(Insiders)?$"
           "^com\\.microsoft\\.VSCode?"]
}

;; layers are basically "hyper" keys
;; press this in combination with something else and depending on your rule, make things happen
:layers {
    :hyper-mode {:key :caps_lock :alone {:key :escape}} ;; make the caps_lock key the hyper key
                                                        ;; In "System Preferences/Keyboard/Modifier Keys..."
                                                        ;; the Caps Lock Key is set to "No Action".
} ;; layers

;; simlayers are basically "hyper" keys
;; layers works too, but only recommended for non-typing keys like . or tab
;; or keys like z which are used less often
:simlayers {
    :e-mode {:key :e}                                   ;; make the e-key a simlayer key
    :o-mode {:key :o}                                   ;; make the o-key a simlayer key
    :space-mode {:key :spacebar}                        ;; make the spacebar a simlayer key
} ;; simlayers

;; Define different kinds of keyboards
:devices {
    :kb-apple [
        {:vendor_id 1452}                               ;; product_id 636 for MacBook Pro (15-inch, 2018) internal keyboard
        {:vendor_id 76}
    ]
    :magicforce68 [                                     ;; Qisan Magicforce 68 keyboard without backlight
        {:vendor_id 1241,
         :product_id 41208 }]
    :magicforce68-backlight                             ;; Qisan Magicforce 68 keyboard with backlight
       [{:vendor_id 1241,
         :product_id 36 }]

    :kb-all [                                           ;; Group with all keyboards
        {:vendor_id 1452}
        {:vendor_id 76}
        {:vendor_id 1241, :product_id 36 }
        {:vendor_id 1241, :product_id 41208 }
    ]

    :kb-magicforce68 [                                  ;; Group with Magicforce 68-key keyboards
        {:vendor_id 1241, :product_id 36 }
        {:vendor_id 1241, :product_id 41208 }
    ]
} ;; devices

;; main contains multiple manipulators
:main [
    ;; each manipulator has a description and multiple rules

    ;;{:des "..."                                       ;; -> description
    ;; :rules[
    ;;          [<from>  <to>  <conditions>]            ;; -> rule 1
    ;;          [<from>  <to>  <conditions>]            ;; -> rule 2
    ;; ]}

    ;; rule [:period ["period-mode" 1] nil {:afterup ["period-mode" 0] :alone :period}]
    ;;       |_____| |_______________| |_| |_________________________________________|
    ;;        <from>    <to>      <conditions>         <other options>

    ;; Tab is set to F18 as hyperkey for Hammerspoon, alone it acts as tab.
    {:des "Tab to F18/Tab, trigger for Hyperspoon, ehh Hammerspoon"
     :rules [
        [:##tab :f18 nil {:alone :tab}]
     ]
    }

    ;; The Magicforce keyboards have a PC layout, I swapped the Windows- and Alt-key on the left side.
    ;; Swapping the physical keys and virtually in karabiner-elements make the layout behave like a Mac keyboard.
    ;; It does require extra rules for combinations with other modifier keys.
    ;; This is the simplified version of the keycombinations below, so without the 'alone' functionality.
    ; {:des "Magicforce68 - swap left-option and left-command"
    ;  :rules [ :kb-magicforce68
    ;     [:##left_command :left_option]
    ;     [:##left_option :left_command]
    ;     [:!Sleft_option :!Sleft_command]                ;; fix for shift_option
    ;     [:!Sleft_command :!Sleft_option]                ;; fix for shift_command
    ;     [:!Tleft_option :!Tleft_command]                ;; fix for control_option
    ;     [:!Tleft_command :!Tleft_option]                ;; fix for control_command
    ;  ]
    ; }

    ;; The Magicforce keyboards have a PC layout, I swapped the Windows- and Alt-key on the left side.
    ;; Swapping the physical keys and virtually in karabiner-elements make the layout behave like a Mac keyboard.
    ;; Additional shortcut for command+tab to switch between the current and previous application
    ;; Additional shortcut for command+tilde to cycle through the open windows of an application
    ;; It does require extra rules for combinations with other modifier keys.
    {:des "Magicforce68 - swap left-option and -command and add cmd-tilde and cmd-tab to the standalone keys"
     :rules [ :kb-magicforce68
        [:left_command :left_option nil {:alone [:!Cgrave_accent_and_tilde]}]
        [:left_option :left_command nil {:alone [:!Ctab]}]
        [:!Sleft_option :!Sleft_command]                ;; fix for shift_option
        [:!Sleft_command :!Sleft_option]                ;; fix for shift_command
        [:!Tleft_option :!Tleft_command]                ;; fix for control_option
        [:!Tleft_command :!Tleft_option]                ;; fix for control_command
     ]
    }

    ;; The Magicforce is a 68 key keyboard with a combined escape_grave_accent_and_tilde key.
    ;; Capslock alone is set to behave like the Escape-key, with this setting the key is
    ;; forced to tilde and grave-accent.
    ;; Additional rule required for command-tilde behaviour.
    ;; Note: On the backlight version there is an override with Fn-Q.
    {:des "Magicforce68 - set escape to grave_accent_and_tilde"
     :rules [ :kb-magicforce68
        [:escape :grave_accent_and_tilde]
        [:!Cescape :!Cgrave_accent_and_tilde]
     ]
    }

    ;; The function key on Apple keyboards can be used in Karabiner,
    ;; it does not work for the function key on my Qisan-Magicfoce keyboard.
    {:des "Apple keyboard - fn + l to Lock-screen, fn + s to Sleep"
     :rules [ :kb-apple
        [:!Fl [:!CTq]]                                  ;; Function + l to Lock Screen (Command-Control-Q)
        [:!Fs [:!COeject]]                              ;; Function + s to Sleep (Command-Option-Eject)
     ]
    }

    ;; Shortcut `command` for command+tab to switch between the current and previous application
    ;; Shortcut `option` for command+tilde to cycle through the open windows of an application
    ;; Shortcut `control` for control+tab to go to the next tab in most applications
    ;; Due to the switched left-option and -command on the Magicforce keyboard, two rules are excluded.
    ;; Functonality for the left-option and left-command is handled in the Magicforce specific rules.
    ;; Command-tab s not implemented in the same way across applications. This results either in looping
    ;; through the tabs or toggling between two tabs. To create the same experience everywhere specific
    ;; sequences need to be build. For VSCodium I experimented with with Ctrl+tab twice.
    {:des "Change command to cmd+tab, option to cmd+tilde and control to ctrl+tab when pressed `alone`"
     :rules [
        [:##left_command :left_command [:!kb-magicforce68] {:alone [:!Ctab]}]
        [:##left_option :left_option [:!kb-magicforce68] {:alone [:!Cgrave_accent_and_tilde]}]
        [:##left_control :left_control [:!code] {:alone [:!Ttab]}]
        [:##left_control :left_control [:code] {:alone [:!Ttab :!Ttab]}]
        [:##right_command :right_command nil {:alone [:!Ctab]}]
        [:##right_option :right_option nil {:alone [:!Cgrave_accent_and_tilde]}]
        [:##right_control :right_control [:!code] {:alone [:!Wtab]}]
        [:##right_control :right_control [:code] {:alone [:!Wtab :!Wtab]}]
     ]
    }

    ;; Use the left and right shift-key to move the cursor one word left or right.
    {:des "Left- and right- shiftkeys: move the cursor one word"
     :rules [
        [:##left_shift :left_shift nil {:alone :!Oleft_arrow }]
        [:##right_shift :right_shift nil {:alone :!Eright_arrow}]
     ]
    }

    ;; Use e-key key to launch apps or actions
    {:des "e-key shortcuts"                             ; Hold 'e' + extra key
     :rules [ :e-mode
        [:m [:!TCspacebar]]                             ; 'm' - Emoji picker
     ]
    }

    ;; Use o-key to launch apps or actions
    ;; Mostly examples
    {:des "o-key shortcuts (example)"                   ; Hold 'o' + extra key
     :rules [ :o-mode
        [:s [:open "Safari"]]                           ; 's' - Open Safari
        [:t [[:type "Moi,oet Grunn!"]]]                 ; 't' - Type some text
        [:b [:beep]]                                    ; 'b' - beep
        [:p [:purr]]                                    ; 'p' - play "Purr"
        [:k [:tink]]                                    ; 'k' - play "Tink"
     ]
    }

    ;; Use spacebar to launch apps or actions
    ;; Mostly as example
    {:des "Spacebar shortcuts (example)"                ; Hold 'space' + extra key
     :rules [ :space-mode
        [:slash [:open "Finder"]]                       ; '/' - Open Finder
        [:p [:open "Preview"]]                          ; 'p' - Open Preview
     ]
    }

    ;; Use caps_lock key to launch apps or actions
    {:des "Hyper-mode shortcuts"                        ; Hold 'caps_lock' + extra key
     :rules [ :hyper-mode
        [:l [:!CTq]]                                    ; 'l' - Lock Screen
        [:m [:!TCspacebar]]                             ; 'm' - Emoji picker
        [:slash [:open "Finder"]]                       ; '/' - Open Finder
        [:p [:open "Preview"]]                          ; 'p' - Open Preview
        [:s [:open "Safari"]]                           ; 's' - Open Safari
        [:f [:open "Firefox Developer Edition"]]        ; 'f' - Open Firefox
        [:k [:open "Kubernetic"]]                       ; 'k' - Open Kubernetic
        [:g [:open "Google Chrome"]]                    ; 'g' - Open Google Chrome
        [:t [:open "iTerm"] [:!code]]                   ; 't' - Open iTerm 'console' or 'commandline'
        [:y [:open "Typora"]]                           ; 'y' - Open Typora
        [:v [:open "VSCodium"]]                         ; 'v' - Open VSCodium
        [:4 [:!TSC4]]                                   ; '4' - Screenshot to clipboard
        [:5 [:!SC5]]                                    ; '5' - Screenshot and recording options
     ]
    }

    ;; Borrowed and modified from kchen0x/k-goku, interesting feature, not sure how and when to use it.
    ;; Hyperkey (caps_lock) + arrow key to move the cursor
    ;; Hyperkey (caps_lock) + shift + arrow key to move the cursor at half the speed
    ;; Hyperkey (caps_lock) + left_shift + left_control + arrow key to move the cursor at quarter the speed
    ;; Hyperkey + return for mouse button 1
    ;; Hyperkey + command + return for mouse button 2
    {:des "Hyper Mode mousekey movement (left_shift or left_shift + left_control reduce the speed)"
     :rules [ :hyper-mode
        [:down_arrow {:mkey {:y 1200}}]
        [:up_arrow {:mkey {:y -1200}}]
        [:left_arrow {:mkey {:x -1200}}]
        [:right_arrow {:mkey {:x 1200}}]
        [:!Sdown_arrow {:mkey {:y 600}}]
        [:!Sup_arrow {:mkey {:y -600}}]
        [:!Sleft_arrow {:mkey {:x -600}}]
        [:!Sright_arrow {:mkey {:x 600}}]
        [:!TSdown_arrow {:mkey {:y 300}}]
        [:!TSup_arrow {:mkey {:y -300}}]
        [:!TSleft_arrow {:mkey {:x -300}}]
        [:!TSright_arrow {:mkey {:x 300}}]
        [:return_or_enter {:pkey :button1}]
        [:!Creturn_or_enter {:pkey :button2}]
     ]
    }

    ;; Borrowed from kchen0x/k-goku, interesting feature, not sure if I want to use it.
    ; {:des "Quit application by pressing command-q twice"
    ;  :rules [[:!C#Pq [:!Cq ["command-q" 0]] ["command-q" 1]]
    ;          [:!C#Pq ["command-q" 1] nil {:delayed {:invoked ["command-q" 0] :canceled ["command-q" 0]}}]
    ;  ]
    ; }

    ;; Attempt to do a double tap with the left-control key. Double tap works but control-c fails.
    ;; Double tap the left-control to bring up the emoji-picker.
    ; {:des "Open emoji-picker by pressing left-control twice"
    ;  :rules [
    ;     [:##left_control [:!TCspacebar  ["left_control_twice" 0]] ["left_control_twice" 1]]
    ;     [:##left_control ["left_control_twice" 1] ["left_control_twice" 0]
    ;       {:delayed {:invoked [:left_control ["left_control_twice" 0]] :canceled ["left_control_twice" 0]} :held [:##left_control]}
    ;     ]
    ;  ]
    ; }

    ;; Browser specific: save all tabs to a markdown file.
    ;; Option + 'o' saves all tabs to a MarkDown file
    ;; Info on the used AppleScript: "https://tisgoud.nl/2020/04/saving-the-yak-browser-trail/"
    ;; and some fun examples
    {:des "Safari: run script to save tabs to markdown (left-option + 'o')"
     :rules [ :safari
        [:!Os [:applescript "SaveSafariTabsToMarkdown.scpt"]]
                                                        ;; Examples of other possible functions
        [:!Oa "say 'now we are in Safari'"]
        [:!Of ["osascript -e 'tell app \"Finder\" to make new Finder window'"]]
        [:!Oh ["osascript -e 'display dialog \"Hello from Karabiner-elements\" with title \"This must be Safari!\"'"]]
     ]
    }

    ;; Browser specific: save all tabs to a markdown file.
    ;; Option + 'o' saves all tabs to a MarkDown file
    ;; Info on the used AppleScript: "https://tisgoud.nl/2020/04/saving-the-yak-browser-trail/"
    {:des "Google Chrome: run script to save tabs to markdown (left-option + 'o')"
     :rules [ :chrome
        [:!Os [:applescript "SaveChromeTabsToMarkdown.scpt"]]
     ]
    }

    ;; Finder specific: open selected file or directory in VSCodium.
    ;; Info on the used AppleScript: "https://tisgoud.nl/2019/09/open-in-vscodium/"
    {:des "Finder: run script to open selected file or directory in VSCodium (right-option + 'o')"
     :rules [ :finder
        [:!Eo [:applescript "Open in VSCodium.scpt"]]   ;; right_option + 'o' to open the file or folder in VSCodium
     ]
    }

    ;; Use caps_lock key to launch apps or actions in VSCode/VSCodium
    ;; When the same keycode is used as general rule/shortcut, '!code' is required on the original rule
    {:des "VSCode/VSCodium shortcuts"                   ; Hold 'caps_lock' + extra key
     :rules [ :code :hyper-mode
        [:t [:!TSgrave_accent_and_tilde]]               ; 't' - Open terminal
     ]
    }

] ;; main

} ;; EOF

With the following command the new ‘karabiner.json’ file is generated.

$ goku
Done!

Karabiner-Elements reloads the configuration automatically and all rules show up in Karabiner-Elements under the “complex modifications” tab.

Karabiner GUI with Complex modifications

The latest version of the ‘karabiner.edn’ file can be found on github/tisGoud/goku.

Hammerspoon

Hammerspoon logo

After instalation of Hammerspoon you need to create the configuration file ‘init.lua’ in the ‘~/.hammerspoon’ directory. It’s possible to put everything in one large file but I chose to separate functionality per file and use the ‘require’ option to load the different .lua files.

Hammerspoon directory structure

The Hammerspoon directory with the separation of functionality per .lua file:

~/.hammerspoon$ tree
.
├── hyper.lua
├── init.lua
├── showmousepointer.lua
├── Spoons
│   └── MouseCircle.spoon
├── wifinotifier.lua
└── windowmanagement.lua

In the next paragraphs I will explain the functionality per file.

init.lua

The init file loads four different .lua files and watches the configuration directory for changes.

If something in the configuration changes, the configuration is reloaded and a notification is displayed.

-----------------------------------------------
-- Init.lua
-----------------------------------------------
require "hyper"
require "showmousepointer"
require "windowmanagement"
require "wifinotifier"

-----------------------------------------------
-- Reload config on write
-----------------------------------------------
local function reload_config()
  hs.reload()
end

hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reload_config):start()
hs.alert.show("Hammerspoon config (re)loaded")

hyper.lua

The Hyper-file creates a “hyper-key” in Hammerspoon.

Within Karabiner-Elements the Tab key is bound to fn18, a non-existing key on my keyboard.

The ‘hyper’ variable can now be used to bind a key to the ‘hyper’ key to trigger an action.

Many hyper-key implementations in Hammerspoon use the ‘hyper:exit()’ function to send a keystroke when the hyper-key was not triggered. This smells like a bad entanglement.

To keep the separation as clean as possible the “non-triggering” is handled by Karabiner-Elements.

With just two lines of code, a very clean hyper-key implementation.

-----------------------------------------------
-- Hyper.lua
-----------------------------------------------

-- The clean modal keybindings are made possible due to a comment from Steve Kehlet (github/skehlet),
-- he mentioned that there is no need to assign a key to the modal key.
-- With that a much cleaner piece of code.
-- Sending a keystroke when the hyper-key is not triggered is handled by Karabiner-Elements.
-- In Karabiner-Elements the 'tab'-key is set to f18/escape.

hyper = hs.hotkey.modal.new()
f18 = hs.hotkey.bind({}, "f18", function () hyper:enter() end, function () hyper:exit() end)

showmousepointer.lua

The Showmousepointer-file is an example of how a Spoon can be called from a .lua file. Download the MouseCircle Spoon from the repository and unzip it in the “Spoons” directory.

The spoon draws a circle around the mouse pointer.

I tried to bind the hyper key with the key ’m' but I could not get that working. Therefore Control + m.

-----------------------------------------------
-- Showmousepointer.lua
-----------------------------------------------

hs.loadSpoon("MouseCircle")

-- 'red' is the default color
spoon.MouseCircle.color = hs.drawing.color.hammerspoon.green

spoon.MouseCircle:bindHotkeys({
  show = { "ctrl" , "m" }
})

wifinotifier.lua

The WifiNotifier-file is an example of an automation not triggered by a keybinding but by a change of the WiFi network. On change it displays a message with the name of the network.

As example I used the hs.notify as well as the hs.alert APIs.

This might become the base of an automated VPN setup triggered by an untrusted network.

-----------------------------------------------
-- WiFinotifier.lua
-----------------------------------------------

-- Current functionality is just a notification with the network name
-- Would be nice to turn on a VPN on an untrusted WiFi network

wifiWatcher = nil
homeSSID = "tisgoud"
workSSID = ""
lastSSID = hs.wifi.currentNetwork()

function ssidChangedCallback()
    newSSID = hs.wifi.currentNetwork()

    if newSSID == homeSSID and lastSSID ~= homeSSID then
        -- We just joined our home WiFi network
        hs.notify.new({title="WiFI", informativeText=string.format("Connected to %q", homeSSID)}):send()
    elseif newSSID ~= homeSSID and lastSSID == homeSSID then
        -- We just departed our home WiFi network
        hs.alert.show(string.format("WiFi connected to %q", newSSID), 3)
    end

    lastSSID = newSSID
end

wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()

windowmanagement.lua

The WindowManagement file enables you to move and resize windows with your keyboard. From all the possibilities my Hammerspoon configuration offers this is the most used automation.

I came across this already modified file and modified it a bit more, there is also a Spoon for Window management but I haven’t tried it.

-----------------------------------------------
-- WindowManagement.lua
-----------------------------------------------
-- Original Work Copyright (c) 2016 Miro Mannino
-- Modified Work Copyright (c) 2017 Rosco kalis
--   Modifications:
-- - Made original functionality work with Hyper Mode
-- - Added next/previous monitor functionality
-- - Added Fullscreen toggle functionality
-- Modified Work Copyright (c) 2020 Albert W. Alberts
--   Modifications:
-- - Removed 'left' + 'right' and 'up' + 'down' key combinations
--   to change size to full width or heighth.
-- - Added extra 'size'-value to step to full width or heighth

-- Permission is hereby granted, free of charge, to any person obtaining a copy of this
-- software and associated documentation files (the "Software"), to deal in the Software
-- without restriction, including without limitation the rights to use, copy, modify, merge,
-- publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
-- to whom the Software is furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

hs.window.animationDuration = 0.0

-----------------------------------------------
-- Functionality
-----------------------------------------------

local sizes = {2, 3, 3/2, 1/4}
local fullScreenSizes = {1, 4/3, 2}

local GRID = {w = 24, h = 24}
hs.grid.setGrid(GRID.w .. 'x' .. GRID.h)
hs.grid.MARGINX = 0
hs.grid.MARGINY = 0

local pressed = {
  up = false,
  down = false,
  left = false,
  right = false
}

function nextStep(dim, offs, cb)
  if hs.window.focusedWindow() then
    local axis = dim == 'w' and 'x' or 'y'
    local oppDim = dim == 'w' and 'h' or 'w'
    local oppAxis = dim == 'w' and 'y' or 'x'
    local win = hs.window.frontmostWindow()
    local id = win:id()
    local screen = win:screen()

    cell = hs.grid.get(win, screen)

    local nextSize = sizes[1]
    for i=1,#sizes do
      if cell[dim] == GRID[dim] / sizes[i] and
        (cell[axis] + (offs and cell[dim] or 0)) == (offs and GRID[dim] or 0)
        then
          nextSize = sizes[(i % #sizes) + 1]
        break
      end
    end

    cb(cell, nextSize)
    if cell[oppAxis] ~= 0 and cell[oppAxis] + cell[oppDim] ~= GRID[oppDim] then
      cell[oppDim] = GRID[oppDim]
      cell[oppAxis] = 0
    end

    hs.grid.set(win, cell, screen)
  end
end

function nextFullScreenStep()
  if hs.window.focusedWindow() then
    local win = hs.window.frontmostWindow()
    local id = win:id()
    local screen = win:screen()

    cell = hs.grid.get(win, screen)

    local nextSize = fullScreenSizes[1]
    for i=1,#fullScreenSizes do
      if cell.w == GRID.w / fullScreenSizes[i] and
         cell.h == GRID.h / fullScreenSizes[i] and
         cell.x == (GRID.w - GRID.w / fullScreenSizes[i]) / 2 and
         cell.y == (GRID.h - GRID.h / fullScreenSizes[i]) / 2 then
        nextSize = fullScreenSizes[(i % #fullScreenSizes) + 1]
        break
      end
    end

    cell.w = GRID.w / nextSize
    cell.h = GRID.h / nextSize
    cell.x = (GRID.w - GRID.w / nextSize) / 2
    cell.y = (GRID.h - GRID.h / nextSize) / 2

    hs.grid.set(win, cell, screen)
  end
end

function fullDimension(dim)
  if hs.window.focusedWindow() then
    local win = hs.window.frontmostWindow()
    local id = win:id()
    local screen = win:screen()
    cell = hs.grid.get(win, screen)

    if (dim == 'x') then
      cell = '0,0 ' .. GRID.w .. 'x' .. GRID.h
    else
      cell[dim] = GRID[dim]
      cell[dim == 'w' and 'x' or 'y'] = 0
    end

    hs.grid.set(win, cell, screen)
  end
end

function prevMonitor()
  local win = hs.window.focusedWindow()
  local nextScreen = win:screen():previous()
  win:moveToScreen(nextScreen)
end

function nextMonitor()
  local win = hs.window.focusedWindow()
  local nextScreen = win:screen():next()
  win:moveToScreen(nextScreen)
end

-----------------------------------------------
-- Shortcut bindings
-----------------------------------------------

hyper:bind({}, "down", function()
  pressed.down = true
  nextStep('h', true, function(cell, nextSize)
    cell.y = GRID.h - GRID.h / nextSize
    cell.h = GRID.h / nextSize
  end)
end, function()
  pressed.down = false
end)

hyper:bind({}, "right", function()
  pressed.right = true
  nextStep('w', true, function(cell, nextSize)
    cell.x = GRID.w - GRID.w / nextSize
    cell.w = GRID.w / nextSize
  end)
end, function()
  pressed.right = false
end)

hyper:bind({}, "left", function()
  pressed.left = true
  nextStep('w', false, function(cell, nextSize)
    cell.x = 0
    cell.w = GRID.w / nextSize
  end)
end, function()
  pressed.left = false
end)

hyper:bind({}, "up", function()
  pressed.up = true
  nextStep('h', false, function(cell, nextSize)
    cell.y = 0
    cell.h = GRID.h / nextSize
  end)
end, function ()
  pressed.up = false
end)

hyper:bind({}, "f", function()
  nextFullScreenStep()
end)

hyper:bind({"shift"}, "left", function()
  prevMonitor()
end)

hyper:bind({"shift"}, "right", function()
  nextMonitor()
end)

hyper:bind({}, "return", function()
  local win = hs.window.frontmostWindow()
  win:setFullscreen(not win:isFullscreen())
end)

Hammerspoon tip

Working with Karabiner-Elements and Hypersoon can be confusing due to the naming convention used for the keys. From the Hammerspoon console the following command can be called to display all the keycodes:

hs.inspect(hs.keycodes.map)

Shortcuts, remappings and automations

The following sections give an overview of the shortcuts, mappings and automations

Qisan Magicforce68 keyboard specific settings

The specific settings for my compact Qisan Magicforce68 keyboard

Shortcut Keystroke Function
left-option left-⌘ swapping option and command
left-command left-⌥ swapping command and option
Escape ` Escape to `
Shift+Escape ~ Shift + Escape to ~
right-Command ⌘ + Tab Toggle current and previous app
right-Option ⌘ + ~ Cycle app windows

Apple keyboard specific settings

The specific Apple keyboard setting involve the use of the ‘fn’ or function key. I was unable to address the function key on my Qisan keyboard.

Shortcut Keystroke Function
fn + l ⌘ + ⌃ + q Lock screen
fn + s ⌘ + ⌥ + Eject Sleep

Generic shortcuts

You might wonder why there are 5 hyper-keys, yes it is to much but I’m still figuring out which hyperkey location is most convenient. The position of your hands above the keyboard is an important factor in this. Using the capslock as hyper key is also a good option due to the size of the key.

Shortcut Keystroke Function
Capslock Escape hyper-key
Tab Tab / fn18 Hammerspoon hyper-key
e e hyper-key
Command (l/r) ⌘ + Tab Toggle current and previous app
Option (l/r) ⌘ + ~ Cycle app windows
Control (l/r) ⌃ + Tab Cycle app tabs
left-Shift ⌥ + left-Arrow Move cursor one word left
right-Shift ⌥ + right-Arrow Move cursor one word right
e + m ⌘ + ⌃ + Space Open Emoji picker
Capslock + l ⌘ + ⌃ + q Lock screen
Capslock + m ⌘ + ⌃ + Space Open Emoji picker
Capslock + \ Open Finder
Capslock + s Open Safari
Capslock + f Open Firefox
Capslock + k Open Kubernetic
Capslock + g Open Google Chrome
Capslock + t Open iTerm
Capslock + y Open Typora
Capslock + v Open VSCodium
Capslock + 4 Screenshot to clipboard
Capslock + 5 Screenshot and recording options
Capslock + arrow Move mouse fast
left-Option + s Safari: Save tabs to markdown
left-Option + s Chrome: Save tabs to markdown
right-Option + o Finder: Open in VSCodium
Capslock + t VSCodium: Open terminal pane

Example shortcuts

Some shortcuts to test stuff.

Shortcut Keystroke Function
o o hyper-key
Spacebar Spacebar hyper-key
o + s Open Safari (example)
o + t Type some text (example)
o + b Play beep sound (example)
o + p Play Purr sound (example)
o + k Play Tink sound (example)
e + m ⌘ + ⌃ + Space Open Emoji picker (example)
Spacebar + \ Open Finder (example)
Spacebar + p Open Preview (example)

Hammerspoon shortcuts and automations

Shortcut Function
Control + m Draw circle around mousepointer
Tab + Arrow Window: Move and Resize (repeat)
Tab + f Window: Next full screen step (repeat)
Tab + Return Window: Toggle full screen
n.a. Wifi notifier

Recap

I enjoyed generating the Karabiner-config with Goku and these tools became part of my standard configuration.

Hammerspoon, is for me still in a test phase. I only scratched the surface of it’s possibilities but I have my doubts, so far my most used function is the windowmanagement and there is a commandline tool yabai that could do just that or one of the many other windowmanagement tools.

Keyboard Maestro might be a good automation alternative but not right now. First I want to use the current configuration for a while before jumping on that bandwagon.

Playing around with these tools was fun, I learned a lot but it also turned out to be very time-consuming. Let’s hope it was worth the investment 😉