Skip to content

Instantly share code, notes, and snippets.

@busla
Created December 3, 2025 08:01
Show Gist options
  • Select an option

  • Save busla/2983f87c5c3b3c76ed32fe53fdb74ad5 to your computer and use it in GitHub Desktop.

Select an option

Save busla/2983f87c5c3b3c76ed32fe53fdb74ad5 to your computer and use it in GitHub Desktop.
hammerspoon per-app keyboard input switcher

⌨️ Automatic Keyboard Layout Switching

Hammerspoon + macOS Fix for perfectly consistent keyboard layout switching.

This setup combines:

  1. Hammerspoon — dynamically switches keyboard layout when specific apps become active
  2. macOS system override — disables Apple's per-app keyboard memory to avoid conflicts
  3. A small Bash utility — applies the required macOS preference fix

This eliminates layout flickering, unexpected switches, and the fight between macOS and Hammerspoon.


🚀 Features

  • Automatically switches to U.S. English when opening coding/terminal apps
  • Automatically switches to Icelandic when opening communication apps
  • Configurable lists for both languages
  • On-screen alerts showing the active layout
  • Reload config hotkey: Cmd + Alt + Ctrl + R
  • macOS keyboard memory disabled to prevent interference

🛠️ Installation

1. Install Hammerspoon

Download from: https://www.hammerspoon.org

Then allow permissions under:
System Settings → Privacy & Security → Accessibility / Automation


📂 Hammerspoon Configuration

Place the following file at ~/.hammerspoon/init.lua:

-- ⌨️ Automatic Keyboard Switching for Applications

-- CONFIGURATION
local englishApps = {
    "Ghostty",
    "Code",
    "Terminal",
    "iTerm2",
    "Visual Studio Code",
}

local icelandicApps = {
    "Slack",
    "Mail",
    "Messages",
    "Discord",
}

-- Helper function
local function isInArray(array, value)
    local lowerValue = string.lower(value)
    for _, item in ipairs(array) do
        if string.lower(item) == lowerValue then
            return true
        end
    end
    return false
end

-- Stop existing watcher if reloading
if appWatcher then
    appWatcher:stop()
    appWatcher = nil
end

-- Create watcher (GLOBAL to prevent garbage collection)
appWatcher = hs.application.watcher.new(function(appName, eventType, appObject)
    if eventType == hs.application.watcher.activated and appName then
        if isInArray(englishApps, appName) then
            hs.keycodes.setLayout("U.S.")
            hs.alert.show("🇺🇸 English", 0.5)
        elseif isInArray(icelandicApps, appName) then
            hs.keycodes.setLayout("Icelandic")
            hs.alert.show("🇮🇸 Íslenska", 0.5)
        end
    end
end)

appWatcher:start()

-- Reload config: Cmd+Alt+Ctrl+R
hs.hotkey.bind({ "cmd", "alt", "ctrl" }, "R", function()
    hs.reload()
end)

hs.alert.show(string.format("⌨️ Auto-Keyboard: %d EN, %d IS apps", #englishApps, #icelandicApps), 1.5)

🧰 Disable macOS Per-App Keyboard Memory (Required)

macOS tries to remember the keyboard layout per application, which conflicts with Hammerspoon trying to enforce a layout.

Disable it with the following script:

disable-macos-keyboard-memory.sh

#!/bin/bash

###############################################
# Disable macOS's built-in per-app keyboard   #
# memory. Without it, you'd have two systems  #
# fighting each other:                        #
# 1. macOS trying to restore the last         #
#    keyboard layout you used in each app     #
# 2. Hammerspoon trying to set the layout     #
#    based on your config                     #
#                                             #
# This conflict can cause flickering or       #
# unexpected behavior.                        #
###############################################

defaults write ~/Library/Preferences/com.apple.HIToolbox.plist AppleGlobalTextInputProperties -dict TextInputGlobalPropertyPerContextInput -int 0

killall cfprefsd

Run it:

chmod +x disable-macos-keyboard-memory.sh
./disable-macos-keyboard-memory.sh

This takes effect instantly.


🔧 Customization

Add/Remove Apps

Modify the app arrays in init.lua:

local englishApps = { ... }
local icelandicApps = { ... }

Use the exact application name as macOS reports it (case-insensitive).

Change Layouts

Update the layout names:

hs.keycodes.setLayout("U.S.")
hs.keycodes.setLayout("Icelandic")

List available layouts in the Hammerspoon console:

hs.keycodes.layouts()

🧪 Testing

  1. Reload config: Cmd + Alt + Ctrl + R
  2. Switch between apps listed in the config
  3. You should see:
    • An alert (🇺🇸 English or 🇮🇸 Íslenska)
    • The keyboard input menu switching instantly

If layouts flicker, re-run the macOS memory script.


✔️ Summary

You now have:

  • Automatic layout switching based on app focus
  • No macOS interference
  • Clean and fast UX for Icelandic + English workflows
-- ⌨️ Automatic Keyboard Switching for Applications
-- CONFIGURATION
local englishApps = {
"Ghostty",
"Code",
"Terminal",
"iTerm2",
"Visual Studio Code",
}
local icelandicApps = {
"Slack",
"Mail",
"Messages",
"Discord",
}
-- Helper function
local function isInArray(array, value)
local lowerValue = string.lower(value)
for _, item in ipairs(array) do
if string.lower(item) == lowerValue then
return true
end
end
return false
end
-- Stop existing watcher if reloading
if appWatcher then
appWatcher:stop()
appWatcher = nil
end
-- Create watcher (GLOBAL to prevent garbage collection)
appWatcher = hs.application.watcher.new(function(appName, eventType, appObject)
if eventType == hs.application.watcher.activated and appName then
if isInArray(englishApps, appName) then
hs.keycodes.setLayout("U.S.")
hs.alert.show("🇺🇸 English", 0.5)
elseif isInArray(icelandicApps, appName) then
hs.keycodes.setLayout("Icelandic")
hs.alert.show("🇮🇸 Íslenska", 0.5)
end
end
end)
appWatcher:start()
-- Reload config: Cmd+Alt+Ctrl+R
hs.hotkey.bind({ "cmd", "alt", "ctrl" }, "R", function()
hs.reload()
end)
hs.alert.show(string.format("⌨️ Auto-Keyboard: %d EN, %d IS apps", #englishApps, #icelandicApps), 1.5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment