-
Notifications
You must be signed in to change notification settings - Fork 3
/
init.lua
496 lines (425 loc) · 14 KB
/
init.lua
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
-- Load Extensions
local application = require "hs.application"
local window = require "hs.window"
local hotkey = require "hs.hotkey"
local keycodes = require "hs.keycodes"
local fnutils = require "hs.fnutils"
local alert = require "hs.alert"
local screen = require "hs.screen"
local grid = require "hs.grid"
local hints = require "hs.hints"
local appfinder = require "hs.appfinder"
local tabs = require "hs.tabs"
local definitions = nil
-- A global variable for the Hyper Mode
local hyperKey = nil
local hyperKeyStub = nil
local watchers = {}
-- Allow saving focus, then restoring it later.
auxWin = nil
function saveFocus()
auxWin = window.focusedWindow()
alert.show("Window '" .. auxWin:title() .. "' saved.")
end
function focusSaved()
if auxWin then
auxWin:focus()
end
end
--
-- Hotkeys
--
local hotkeys = {}
function createHotkeys()
bindHyperKeyStub()
for key, fun in pairs(definitions) do
if key == "." then alert.show("Key bound to `.`, which is an internal OS X shortcut for sysdiagnose.") end
local mod = hyper
-- Any definitions ending with c are cmd defs
if string.len(key) == 2 and string.sub(key,2,2) == "c" then
key = string.sub(key,1,1)
mod = {"cmd"}
-- Ending with l are ctrl
elseif string.len(key) == 2 and string.sub(key,2,2) == "l" then
key = string.sub(key,1,1)
mod = {"ctrl"}
elseif string.len(key) == 4 and string.sub(key,2,4) == "raw" then
key = string.sub(key,1,1)
mod = {}
end
-- Sierra hack
if mod == hyper then
-- Bind to the existing F17 stub
hyperKeyStub:bind({}, key, nil, fun)
else
local hk = hotkey.new(mod, key, fun)
table.insert(hotkeys, hk)
hk:enable()
end
end
end
function rebindHotkeys()
for i, hk in ipairs(hotkeys) do
hk:disable()
end
hyperKey:delete()
hyperKeyStub:delete()
hotkeys = {}
createHotkeys()
alert.show("Rebound Hotkeys")
end
function bindHyperKeyStub()
-- Create a new F17 global
hyperKeyStub = hs.hotkey.modal.new({}, "F17")
-- Enter Hyper Mode when F18 (Hyper/Capslock) is pressed
pressedF18 = function()
hyperKeyStub.triggered = false
hyperKeyStub:enter()
end
-- Leave Hyper Mode when F18 (Hyper/Capslock) is pressed,
-- send ESCAPE if no other keys are pressed.
releasedF18 = function()
hyperKeyStub:exit()
if not hyperKeyStub.triggered then
hs.eventtap.keyStroke({}, 'ESCAPE')
end
end
-- Bind the Hyper key
hyperKey = hs.hotkey.bind({}, 'F18', pressedF18, releasedF18)
end
--
-- Grid
--
-- HO function for automating moving a window to a predefined position.
local gridset = function(frame)
return function()
local win = window.focusedWindow()
if win then
grid.set(win, frame, win:screen())
else
alert.show("No focused window.")
end
end
end
function applyPlace(win, place)
local scrs = screen.allScreens()
local scr = scrs[place[1]]
grid.set(win, place[2], scr)
end
function applyLayout(layout)
return function()
alert.show("Applying Layout.")
-- Sort table, table keys last so they are sorted independently of the main app
table.sort(layout, function (op1, op2)
local type1, type2 = type(op1), type(op2)
if type1 ~= type2 then
return type1 < type2
else
return op1 < op2
end
end)
for appName, place in pairs(layout) do
-- Two types we allow: table, which is {appName, windowTitle}, or just the app itself
if type(appName) == 'table' then
local parentAppName = appName[1]
local windowPattern = appName[2]
alert(windowPattern)
local window = appfinder.windowFromWindowTitlePattern(windowPattern)
if window then
applyPlace(window, place)
end
else
alert(appName)
local app = appfinder.appFromName(appName)
if app then
for i, win in ipairs(app:allWindows()) do
applyPlace(win, place)
end
end
end
end
end
end
--
-- Conf
--
--
-- Utility
--
function reloadConfig(files)
for _,file in pairs(files) do
if file:sub(-4) == ".lua" then
hs.reload()
return
end
end
end
-- Prints out a table like a JSON object. Utility
function serializeTable(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then tmp = tmp .. name .. " = " end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
for k, v in pairs(val) do
tmp = tmp .. serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end
function string.starts(str,Start)
return string.sub(str,1,string.len(Start))==Start
end
--
-- WiFi
--
-- local home = {["Lemonparty"] = TRUE, ["Lemonparty 5GHz"] = TRUE}
-- local lastSSID = hs.wifi.currentNetwork()
-- function ssidChangedCallback()
-- newSSID = hs.wifi.currentNetwork()
-- if newSSID == lastSSID then return end
-- if home[newSSID] and not home[lastSSID] then
-- -- We just joined our home WiFi network
-- hs.audiodevice.defaultOutputDevice():setVolume(25)
-- elseif not home[newSSID] and home[lastSSID] then
-- -- We just departed our home WiFi network
-- hs.audiodevice.defaultOutputDevice():setVolume(0)
-- end
-- local messages = appfinder.appFromName('Messages')
-- messages:selectMenuItem("Log In")
-- lastSSID = newSSID
-- end
-- local wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
-- wifiWatcher:start()
--
-- Sound
--
-- Mute on jack in/out
function audioCallback(uid, eventName, eventScope, channelIdx)
if eventName == 'jack' then
alert("Jack changed, muting.", 1)
hs.audiodevice.defaultOutputDevice():setVolume(0)
end
end
-- Watch device; mute when headphones unplugged.
local defaultDevice = hs.audiodevice.defaultOutputDevice()
defaultDevice:watcherCallback(audioCallback);
defaultDevice:watcherStart();
--
-- Battery / Power
--
-- Disable spotlight indexing while on battery.
--
-- In order for this to work, add this to a file in `/etc/sudoers.d/`:
-- <username> ALL=(root) NOPASSWD: /usr/bin/mdutil -i on /
-- <username> ALL=(root) NOPASSWD: /usr/bin/mdutil -i off /
-- Verify with `sudo -l`
--
--
-- Removing this for now, appears to just churn CPU
--
-- local currentPowerSource = nil;
-- function batteryWatchUnplugged()
-- newPowerSource = hs.battery.powerSource();
-- if newPowerSource ~= currentPowerSource then
-- alert(string.format("New power source: %s", newPowerSource), 1);
-- function taskCb(code, stdout, stderr)
-- if code ~= 0 then
-- alert("Failed, check console.");
-- end
-- print("stdout: "..stdout);
-- print("stderr: "..stderr)
-- end
-- if newPowerSource == 'Battery Power' then
-- alert("Disabling spotlight.", 1);
-- hs.task.new("/usr/bin/sudo", taskCb, {"/usr/bin/mdutil", "-i", "off", "/"}):start()
-- else
-- alert("Enabling spotlight.", 1);
-- hs.task.new("/usr/bin/sudo", taskCb, {"/usr/bin/mdutil", "-i", "on", "/"}):start()
-- end
-- currentPowerSource = newPowerSource;
-- end
-- end
-- local batteryWatcher = hs.battery.watcher.new(batteryWatchUnplugged):start();
-- batteryWatchUnplugged();
--
-- Application overrides
--
--
-- Fix Slack's channel switching.
-- This rebinds ctrl-tab and ctrl-shift-tab back to switching channels,
-- which is what they did before the Teams update.
--
-- Slack only provides alt+up/down for switching channels, (and the cmd-t switcher,
-- which is buggy) and have 3 (!) shortcuts for switching teams, most of which are
-- the usual tab switching shortcuts in every other app.
--
-- This basically turns the tab switching shortcuts into LimeChat shortcuts, which very smartly
-- uses the brackets to switch any channel, and ctrl-(shift)-tab to switch unreads.
local slackKeybinds = {
hotkey.new({"ctrl"}, "tab", function()
hs.eventtap.keyStroke({"alt", "shift"}, "Down")
end),
hotkey.new({"ctrl", "shift"}, "tab", function()
hs.eventtap.keyStroke({"alt", "shift"}, "Up")
end),
hotkey.new({"cmd", "shift"}, "[", function()
hs.eventtap.keyStroke({"alt"}, "Up")
end),
hotkey.new({"cmd", "shift"}, "]", function()
hs.eventtap.keyStroke({"alt"}, "Down")
end),
-- Disables cmd-w entirely, which is so annoying on slack
hotkey.new({"cmd"}, "w", function() return end)
}
local slackWatcher = hs.application.watcher.new(function(name, eventType, app)
if eventType ~= hs.application.watcher.activated then return end
local fnName = name == "Slack" and "enable" or "disable"
for i, keybind in ipairs(slackKeybinds) do
-- Remember that lua is weird, so this is the same as keybind.enable() in JS, `this` is first param
keybind[fnName](keybind)
end
end)
slackWatcher:start()
--
-- Fix Skype's channel switching.
--
local skypeKeybinds = {
hotkey.new({"ctrl"}, "tab", function()
hs.eventtap.keyStroke({"alt", "cmd"}, "Right")
end),
hotkey.new({"ctrl", "shift"}, "tab", function()
hs.eventtap.keyStroke({"alt", "cmd"}, "Left")
end)
}
local skypeWatcher = hs.application.watcher.new(function(name, eventType, app)
if eventType ~= hs.application.watcher.activated then return end
local fnName = name == "Skype" and "enable" or "disable"
for i, keybind in ipairs(skypeKeybinds) do
-- Remember that lua is weird, so this is the same as keybind.enable() in JS, `this` is first param
keybind[fnName](keybind)
end
end)
skypeWatcher:start()
--
-- INIT!
--
function init()
-- Bind hotkeys.
createHotkeys()
-- If we hook up a keyboard, rebind.
keycodes.inputSourceChanged(rebindHotkeys)
-- Automatically reload config when it changes.
hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start()
-- Doesn't work with symlinks... so go straight to the git repo.
hs.pathwatcher.new(os.getenv("HOME") .. "/git/oss/init/hammerspoon/", reloadConfig):start()
-- Prevent system sleep, only if connected to AC power. (sleepType, shouldPrevent, batteryToo)
hs.caffeinate.set('system', true, false)
alert.show("Reloaded.", 1)
end
-- Grid config =================================
hs.window.animationDuration = 0.2;
-- hints.style = "vimperator"
-- Set grid size.
has4k = hs.screen('3840x2160')
grid.GRIDWIDTH = has4k and 32 or 4
grid.GRIDHEIGHT = has4k and 18 or 4
grid.MARGINX = 0
grid.MARGINY = 0
local gw = grid.GRIDWIDTH
local gh = grid.GRIDHEIGHT
local goMiddle = {x = gw/4, y = gh/4, w = gw/2, h = gh/2}
local goLeft = {x = 0, y = 0, w = gw/2, h = gh}
local goRight = {x = gw/2, y = 0, w = gw/2, h = gh}
local goTopLeft = {x = 0, y = 0, w = gw/2, h = gh/2}
local goTopRight = {x = gw/2, y = 0, w = gw/2, h = gh/2}
local goBottomLeft = {x = 0, y = gh/2, w = gw/2, h = gh/2}
local goBottomRight = {x = gw/2, y = gh/2, w = gw/2, h = gh/2}
local gobig = {x = 0, y = 0, w = gw, h = gh}
-- Saved layout. TODO
local layout2 = {
["Sublime Text"] = {1, {x = 0, y = 0, h = 12, w = 11}},
LimeChat = {1, {x = 0, y = 12, h = 6, w = 5}},
["Google Chrome"] = {1, {x = 11, y = 7, h = 11, w = 13}},
[{"Google Chrome", "Developer Tools.*"}] = {1, {x = 24, y = 7, h = 11, w = 8}},
Slack = {1, {x = 24, y = 0, h = 9, w = 8}},
Postbox = {1, {x = 24, y = 9, h = 9, w = 8}},
Skype = {1, {x = 6, y = 12, h = 6, w = 5}},
Telegram = {1, {x = 6, y = 12, h = 6, w = 5}},
iTerm2 = {1, {x = 11, y = 0, h = 7, w = 13}},
Messages = {1, {x = 26, y = 12, w = 6, h = 6}},
Finder = {1, {x = 22, y = 6, w = 10, h = 6}},
Postico = {1, {x = 0, y = 12, w = 6, h = 6}},
}
-- Watch out, cmd-opt-ctrl-shift-period is an actual OS X shortcut for running sysdiagose
definitions = {
r = hs.reload,
-- Not using
-- [";"] = saveFocus,
-- a = focusSaved,
-- h = gridset(godMiddle),
Left = gridset(goLeft),
Up = grid.maximizeWindow,
Right = gridset(goRight),
['1'] = gridset(goTopLeft),
['3'] = gridset(goTopRight),
['5'] = gridset(goMiddle),
['7'] = gridset(goBottomLeft),
['9'] = gridset(goBottomRight),
-- ["'"] = function() alert.show(serializeTable(grid.get(window.focusedWindow())), 30) end,
g = has4k and applyLayout(layout2) or nil,
["'"] = grid.pushWindowPrevScreen,
[";"] = grid.pushWindowNextScreen,
["\\"] = grid.show, -- way too fucked with our grid sizes
-- q = function() hs.application.find("Hammerspoon"):kill() end,
-- Shows all sublime windows
-- e = function() hints.windowHints(hs.application.find("Sublime Text"):allWindows()) end,
-- Focuses these apps
q = function() hs.application.find("Sublime Text"):mainWindow():focus() end,
w = function() hs.application.find("iTerm2"):mainWindow():focus() end,
c = function() hs.application.find("Google Chrome"):mainWindow():focus() end,
s = function() hs.application.find("Slack"):mainWindow():focus() end,
-- Show hints for all window
f = function() hints.windowHints(nil) end,
-- Shows all windows for current app
v = function() hints.windowHints(window.focusedWindow():application():allWindows()) end,
o = function() hs.execute(os.getenv("HOME") .. "/bin/subl ".. os.getenv("HOME") .."/.hammerspoon/init.lua") end,
--
-- GRID
--
-- move windows
h = grid.pushWindowLeft,
j = grid.pushWindowDown,
l = grid.pushWindowRight,
[";"] = grid.pushWindowUp,
-- resize windows
["="] = grid.resizeWindowTaller,
["-"] = grid.resizeWindowShorter,
["["] = grid.resizeWindowThinner,
["]"] = grid.resizeWindowWider,
m = grid.maximizeWindow,
n = function() grid.snap(window.focusedWindow()) end,
-- cmd+\ should run cmd-tab (keyboard symmetry)
["\\c"] = function() hs.eventtap.keyStroke({"cmd"},"tab") end
}
--
-- TABS
-- Currently crashes on sublime text.
--
-- for i=1,6 do
-- definitions[tostring(i)] = function()
-- local app = application.frontmostApplication()
-- tabs.focusTab(app,i)
-- end
-- end
init()