-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
150 lines (135 loc) · 5.97 KB
/
Copy pathinit.lua
File metadata and controls
150 lines (135 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
-- ===========================================================================
-- Caps Layout Switcher — Hammerspoon config
-- ===========================================================================
-- Per-layout hotkeys for switching macOS keyboard layouts, plus a CapsLock
-- "tap" that toggles between the two most recent layouts.
--
-- • Tap CapsLock -> toggle between the two most recent layouts
-- • Hold CapsLock + <letter> -> jump straight to a specific layout
-- • Hold CapsLock + 0 -> show & copy the current layout's source ID
--
-- CapsLock is physically remapped to F18 (via hidutil + a LaunchAgent) so it
-- behaves like a normal modifier with no "caps toggle". While CapsLock is held,
-- the modal below is active. CapsLock's normal ALL-CAPS function is disabled.
--
-- >>> CONFIGURE ME <<< Edit the `layouts` table below with YOUR layouts.
-- Find your source IDs with `tools/list-input-sources` or by holding CapsLock+0
-- after the current layout is selected. See the README for details.
-- ===========================================================================
-- letter -> { human-readable name, sourceID }. Pick mnemonic letters.
local layouts = {
["e"] = { "English (ABC)", "com.apple.keylayout.ABC" },
["r"] = { "Russian (PC)", "com.apple.keylayout.RussianWin" },
["k"] = { "Korean (2-Set)", "com.apple.inputmethod.Korean.2SetKorean" },
["c"] = { "Chinese (Pinyin)", "com.apple.inputmethod.SCIM.ITABC" },
["a"] = { "Armenian (HM QWERTY)", "com.apple.keylayout.Armenian-HMQWERTY" },
["d"] = { "Austrian (German)", "com.apple.keylayout.Austrian" },
}
local function isIME(id) return id and id:find("inputmethod") ~= nil end
local function setTwice(id)
hs.keycodes.currentSourceID(id)
hs.timer.doAfter(0.05, function() hs.keycodes.currentSourceID(id) end)
end
-- Key subtlety: leaving an IME (Korean/Chinese/etc.) for a plain keyboard
-- layout. macOS updates the menu-bar icon, but the IME's input context in the
-- focused app is NOT torn down until a *different* source is selected. So we
-- first bounce through an intermediate layout (which breaks the IME), then set
-- the real target. For ordinary switches we just set the source twice (guards
-- against a race where the first set doesn't "take").
local function selectSource(targetID)
local cur = hs.keycodes.currentSourceID()
if isIME(cur) and not isIME(targetID) then
-- intermediate must differ from the target and be an ENABLED layout
local intermediate = (targetID == "com.apple.keylayout.ABC")
and "com.apple.keylayout.Austrian" or "com.apple.keylayout.ABC"
hs.keycodes.currentSourceID(intermediate)
hs.timer.doAfter(0.10, function() setTwice(targetID) end)
else
setTwice(targetID)
end
end
-- reverse index id -> name (for the on-screen labels)
local nameByID = {}
for _, def in pairs(layouts) do nameByID[def[2]] = def[1] end
-- ===== "Two most recent layouts" history =====
-- curLayout/prevLayout are updated ONLY for the final target layout, never for
-- the intermediate IME "bounce". We set a `programmatic` flag while switching
-- so the watcher below ignores those transient changes.
local curLayout = hs.keycodes.currentSourceID()
local prevLayout = nil
local programmatic = false
local function recordTarget(targetID)
if targetID ~= curLayout then
prevLayout = curLayout
curLayout = targetID
end
end
-- single entry point: remember target + set source + show a label
local function activate(targetID)
if not targetID then return end
programmatic = true
recordTarget(targetID)
selectSource(targetID)
hs.timer.doAfter(0.3, function() programmatic = false end) -- wait out the bounce
hs.alert.closeAll()
hs.alert.show(nameByID[targetID] or targetID, 0.6)
end
-- also track manual switches (via the menu bar), but ignore programmatic bounces
hs.keycodes.inputSourceChanged(function()
if programmatic then return end
local now = hs.keycodes.currentSourceID()
if now ~= curLayout then
prevLayout = curLayout
curLayout = now
end
end)
-- ===== CapsLock (=F18): hold + letter, or a short tap =====
-- While CapsLock is held the modal is active and letters switch layout.
-- A short tap with no letter toggles to the previous layout.
local capsModal = hs.hotkey.modal.new()
local tapCandidate = false -- was this press a potential "tap" (no letter)?
for key, def in pairs(layouts) do
local sourceID = def[2]
capsModal:bind({}, key, function()
tapCandidate = false -- a letter was pressed => hold-combo, not a tap
activate(sourceID)
end)
end
-- CapsLock+0 — show the current layout's source ID (handy when adding layouts)
capsModal:bind({}, "0", function()
tapCandidate = false
local id = hs.keycodes.currentSourceID()
hs.pasteboard.setContents(id) -- also copy it to the clipboard
hs.alert.show("Current source ID:\n" .. id .. "\n(copied to clipboard)", 3)
end)
-- capsWatcher is GLOBAL (no `local`), otherwise GC would kill the eventtap.
local F18 = hs.keycodes.map["f18"]
local TAP_MAX = 0.4 -- max seconds for a press to count as a "tap"
local capsDown = false
local pressTime = 0
capsWatcher = hs.eventtap.new(
{ hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp },
function(e)
local isDown = (e:getType() == hs.eventtap.event.types.keyDown)
if e:getKeyCode() == F18 then
if isDown then
capsDown = true
tapCandidate = true
pressTime = hs.timer.secondsSinceEpoch()
capsModal:enter()
else
capsDown = false
capsModal:exit()
-- short press with no letter => tap => go to the previous layout
if tapCandidate and (hs.timer.secondsSinceEpoch() - pressTime) < TAP_MAX then
activate(prevLayout)
end
end
return true -- swallow the F18 key itself
else
if capsDown and isDown then tapCandidate = false end -- any other key => not a tap
return false
end
end)
capsWatcher:start()
hs.alert.show("Caps Layout Switcher loaded", 1.2)