├── .gitignore
├── LICENSE
├── README.md
├── assets
├── make.sh
├── statusicon_off.svg
├── statusicon_off.tiff
├── statusicon_on.svg
└── statusicon_on.tiff
├── clipboard.lua
└── init.lua
/.gitignore:
--------------------------------------------------------------------------------
1 | assets/node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2015 Markus Engelbrecht
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | hammerspoon-config
2 | ==================
3 |
4 | This is a configuration for [Hammerspoon](http://www.hammerspoon.org)
5 |
6 | ## License
7 |
8 | Released under the [MIT License](LICENSE)
9 |
--------------------------------------------------------------------------------
/assets/make.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Requirements
4 | # brew install node imagemagick
5 | # npm install svgexport -g
6 |
7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
8 |
9 | makeIcon() {
10 | input="$1"
11 | output="$2"
12 | npx svgexport "${input}" "${input}_32x32.png" 32:32
13 | npx svgexport "${input}" "${input}_16x16.png" 16:16
14 | convert -units PixelsPerInch -density 144 "${input}_32x32.png" "${input}_32x32.tiff"
15 | convert -units PixelsPerInch -density 72 "${input}_16x16.png" "${input}_16x16.tiff"
16 | convert "${input}_32x32.tiff" "${input}_16x16.tiff" "${output}"
17 | rm -f "${input}_32x32.png" "${input}_16x16.png" "${input}_32x32.tiff" "${input}_16x16.tiff"
18 | }
19 |
20 | makeIcon "${DIR}/statusicon_on.svg" "${DIR}/statusicon_on.tiff"
21 | makeIcon "${DIR}/statusicon_off.svg" "${DIR}/statusicon_off.tiff"
22 |
23 |
--------------------------------------------------------------------------------
/assets/statusicon_off.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/assets/statusicon_off.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mengelbrecht/hammerspoon-config/31f07df8045b9776303ffdda5eb6ebec45e82308/assets/statusicon_off.tiff
--------------------------------------------------------------------------------
/assets/statusicon_on.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/assets/statusicon_on.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mengelbrecht/hammerspoon-config/31f07df8045b9776303ffdda5eb6ebec45e82308/assets/statusicon_on.tiff
--------------------------------------------------------------------------------
/clipboard.lua:
--------------------------------------------------------------------------------
1 | -- ClipboardManager
2 | -- based on TextClipboardHistory.spoon by Diego Zamboni
3 | -- https://github.com/Hammerspoon/Spoons/blob/28c3aa65e2de1b12a23659544693f06dd4dc9836/Source/TextClipboardHistory.spoon/init.lua
4 | -- License: MIT - https://opensource.org/licenses/MIT
5 | --
6 | local clipboard = {}
7 |
8 | clipboard.frequency = 0.8
9 | clipboard.maxItems = 100
10 |
11 | clipboard.ignoredIdentifiers = {
12 | ["org.nspasteboard.TransientType"] = true,
13 | ["org.nspasteboard.ConcealedType"] = true,
14 | ["org.nspasteboard.AutoGeneratedType"] = true
15 | }
16 |
17 | clipboard.ignoredApplications = {
18 | ["com.markmcguill.strongbox.mac"] = true
19 | }
20 |
21 | clipboard.emptyHistoryItem = {
22 | text = "《Clipboard is empty》",
23 | image = hs.image.imageFromName("NSCaution")
24 | }
25 |
26 | clipboard.clearHistoryItem = {
27 | text = "《Clear Clipboard History》",
28 | action = "clear",
29 | image = hs.image.imageFromName("NSTrashFull")
30 | }
31 |
32 | ----------------------------------------------------------------------
33 |
34 | local itemSetting = "clipboard.items"
35 | local clipboardHistory
36 | local chooser
37 | local previousFocusedWindow
38 |
39 | local function getItem(content)
40 | return {
41 | text = content:gsub("\n", " "):gsub("%s+", " "),
42 | subText = #content .. " characters",
43 | data = content
44 | }
45 | end
46 |
47 | local function dedupeAndResize(list)
48 | local result = {}
49 | local hashes = {}
50 | for _, v in ipairs(list) do
51 | if #result >= clipboard.maxItems then
52 | break
53 | end
54 |
55 | local hash = hs.hash.MD5(v)
56 | if not hashes[hash] then
57 | table.insert(result, v)
58 | hashes[hash] = true
59 | end
60 | end
61 | return result
62 | end
63 |
64 | local function addContentToClipboardHistory(content)
65 | table.insert(clipboardHistory, 1, content)
66 | clipboardHistory = dedupeAndResize(clipboardHistory)
67 | end
68 |
69 | local function processSelectedItem(value)
70 | local actions = {
71 | clear = clipboard.clearAll
72 | }
73 |
74 | if previousFocusedWindow ~= nil then
75 | previousFocusedWindow:focus()
76 | end
77 |
78 | if value == nil or type(value) ~= "table" then
79 | return
80 | end
81 |
82 | if value.action and actions[value.action] then
83 | actions[value.action]()
84 | elseif value.data then
85 | addContentToClipboardHistory(value.data)
86 | hs.pasteboard.setContents(value.data)
87 | hs.eventtap.keyStroke({ "cmd" }, "v")
88 | end
89 | end
90 |
91 | local function populateChooser()
92 | local menuData = hs.fnutils.imap(clipboardHistory, getItem)
93 | table.insert(menuData, #menuData == 0 and clipboard.emptyHistoryItem or clipboard.clearHistoryItem)
94 | return menuData
95 | end
96 |
97 | local function shouldIgnoreLatestPasteboardEntry()
98 | if hs.fnutils.some(hs.pasteboard.pasteboardTypes(), function(v) return clipboard.ignoredIdentifiers[v] end) then
99 | return true
100 | end
101 |
102 | if hs.fnutils.some(hs.pasteboard.contentTypes(), function(v) return clipboard.ignoredIdentifiers[v] end) then
103 | return true
104 | end
105 |
106 | if clipboard.ignoredApplications[hs.application.frontmostApplication():bundleID()] then
107 | return true
108 | end
109 |
110 | return false
111 | end
112 |
113 | local function handleNewPasteboardContent(content)
114 | if shouldIgnoreLatestPasteboardEntry() or content == nil then
115 | return
116 | end
117 |
118 | addContentToClipboardHistory(content)
119 | end
120 |
121 | function clipboard.start()
122 | clipboardHistory = {}
123 |
124 | chooser = hs.chooser.new(processSelectedItem)
125 | chooser:choices(populateChooser)
126 |
127 | hs.pasteboard.watcher.interval(clipboard.frequency)
128 | pasteboardWatcher = hs.pasteboard.watcher.new(handleNewPasteboardContent)
129 | pasteboardWatcher:start()
130 | end
131 |
132 | function clipboard.clearAll()
133 | hs.pasteboard.clearContents()
134 | clipboardHistory = {}
135 | end
136 |
137 | function clipboard.toggleClipboard()
138 | if chooser:isVisible() then
139 | chooser:hide()
140 | return
141 | end
142 |
143 | chooser:refreshChoicesCallback()
144 | previousFocusedWindow = hs.window.focusedWindow()
145 | chooser:show()
146 | end
147 |
148 | return clipboard
149 |
--------------------------------------------------------------------------------
/init.lua:
--------------------------------------------------------------------------------
1 | ----------------------------------------------------------------------------------------------------
2 | -- Settings
3 | ----------------------------------------------------------------------------------------------------
4 | hs.autoLaunch(true)
5 | hs.automaticallyCheckForUpdates(true)
6 | hs.consoleOnTop(true)
7 | hs.dockIcon(false)
8 | hs.menuIcon(false)
9 | hs.uploadCrashData(false)
10 |
11 | hs.window.animationDuration = 0
12 |
13 | configWatcher = hs.pathwatcher.new(hs.configdir, hs.reload)
14 | configWatcher:start()
15 |
16 | local maximizeMode = true
17 |
18 | ----------------------------------------------------------------------------------------------------
19 | -- Utilities
20 | ----------------------------------------------------------------------------------------------------
21 |
22 | local modifier = {
23 | cmd = "cmd",
24 | shift = "shift",
25 | ctrl = "ctrl",
26 | option = "alt",
27 | }
28 |
29 | local modifiers = {
30 | hyper = { modifier.shift, modifier.ctrl, modifier.option, modifier.cmd },
31 | window = { modifier.ctrl, modifier.option },
32 | clipboard = { modifier.ctrl, modifier.cmd }
33 | }
34 |
35 | local bundleID = {
36 | activityMonitor = "com.apple.ActivityMonitor",
37 | finder = "com.apple.finder",
38 | firefox = "org.mozilla.firefox",
39 | googleChrome = "com.google.Chrome",
40 | intellij = "com.jetbrains.intellij",
41 | iterm = "com.googlecode.iterm2",
42 | outlook = "com.microsoft.Outlook",
43 | reeder = "com.reederapp.5.macOS",
44 | safari = "com.apple.Safari",
45 | teams = "com.microsoft.teams2",
46 | vsCode = "com.microsoft.VSCode",
47 | other = "other",
48 | }
49 |
50 | local font = {
51 | monospace = "Iosevka Code"
52 | }
53 |
54 | local function maximizeCurrentWindow() hs.window.focusedWindow():maximize() end
55 |
56 | local function centerCurrentWindow() hs.window.focusedWindow():centerOnScreen() end
57 |
58 | local function moveCurrentWindowToLeftHalf()
59 | local win = hs.window.focusedWindow()
60 | local screenFrame = win:screen():frame()
61 | local newFrame = hs.geometry.rect(screenFrame.x, screenFrame.y, screenFrame.w / 2, screenFrame.h)
62 | win:setFrame(newFrame)
63 | end
64 |
65 | local function moveCurrentWindowToRightHalf()
66 | local win = hs.window.focusedWindow()
67 | local screenFrame = win:screen():frame()
68 | local newFrame = hs.geometry.rect(screenFrame.x + screenFrame.w / 2, screenFrame.y, screenFrame.w / 2, screenFrame.h)
69 | win:setFrame(newFrame)
70 | end
71 |
72 | local function moveCurentWindowToNextScreen()
73 | local win = hs.window.focusedWindow()
74 | win:moveToScreen(win:screen():next())
75 | win:maximize()
76 | end
77 |
78 | local function moveMouseToWindowCenter()
79 | local windowCenter = hs.window.frontmostWindow():frame().center
80 | hs.mouse.absolutePosition(windowCenter)
81 | end
82 |
83 | local function moveMouseToUpperLeft()
84 | local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame()
85 | local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w / 6, screenFrame.y + screenFrame.h / 6)
86 | hs.mouse.absolutePosition(newPoint)
87 | end
88 |
89 | local function moveMouseToUpperRight()
90 | local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame()
91 | local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w * 5 / 6, screenFrame.y + screenFrame.h / 6)
92 | hs.mouse.absolutePosition(newPoint)
93 | end
94 |
95 | local function moveMouseToLowerLeft()
96 | local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame()
97 | local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w / 6, screenFrame.y + screenFrame.h * 5 / 6)
98 | hs.mouse.absolutePosition(newPoint)
99 | end
100 |
101 | local function moveMouseToLowerRight()
102 | local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame()
103 | local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w * 5 / 6, screenFrame.y + screenFrame.h * 5 / 6)
104 | hs.mouse.absolutePosition(newPoint)
105 | end
106 |
107 | ----------------------------------------------------------------------------------------------------
108 | -- Menu
109 | ----------------------------------------------------------------------------------------------------
110 |
111 | local function menuItems()
112 | return {
113 | {
114 | title = "Hammerspoon " .. hs.processInfo.version,
115 | disabled = true
116 | },
117 | { title = "-" },
118 | {
119 | title = "Maximize Mode",
120 | checked = maximizeMode,
121 | fn = function() maximizeMode = not maximizeMode end
122 | },
123 | { title = "-" },
124 | {
125 | title = "Reload",
126 | fn = hs.reload
127 | },
128 | {
129 | title = "Console...",
130 | fn = hs.openConsole
131 | },
132 | { title = "-" },
133 | {
134 | title = "Quit",
135 | fn = function() hs.application.get(hs.processInfo.processID):kill() end
136 | }
137 | }
138 | end
139 |
140 | menu = hs.menubar.new()
141 | menu:setMenu(menuItems)
142 | menu:setIcon(hs.configdir .. "/assets/statusicon_on.tiff")
143 |
144 | ----------------------------------------------------------------------------------------------------
145 | -- Window Management
146 | ----------------------------------------------------------------------------------------------------
147 |
148 | function timedAllWindows()
149 | local appTimes = {}
150 | for _, app in ipairs(hs.application.runningApplications()) do
151 | local name = app:name()
152 | if not hs.window.filter.ignoreAlways[name] then
153 | local start = hs.timer.absoluteTime()
154 | app:allWindows()
155 | appTimes[name] = (appTimes[name] or 0) + hs.timer.absoluteTime() - start
156 | end
157 | end
158 | for appName, time in hs.fnutils.sortByKeys(appTimes) do
159 | local timeMillis = time / 1000 / 1000
160 | if timeMillis >= 50 then
161 | print(string.format('took %.0fms for "%s"', timeMillis, appName))
162 | end
163 | end
164 | end
165 |
166 | hs.window.filter.ignoreAlways = {
167 | ["Familie"] = true,
168 | ["Mail-Webinhalt"] = true,
169 | ["Reeder Web Content"] = true,
170 | ["Safari-Webinhalt"] = true,
171 | ["Safari-Webinhalt (im Cache)"] = true,
172 | }
173 |
174 | maximizeWindows = {
175 | "App Store",
176 | "Brave Browser",
177 | "Code",
178 | "Firefox",
179 | "Fork",
180 | "Fotos",
181 | "Google Chrome",
182 | "IntelliJ IDEA",
183 | "iTerm2",
184 | "Kalender",
185 | "Mail",
186 | "Microsoft Outlook",
187 | "Microsoft Teams",
188 | "Music",
189 | "Musik",
190 | "Photos",
191 | "Reeder",
192 | "Safari",
193 | "Spotify",
194 | }
195 |
196 | windowFilter = hs.window.filter.new()
197 | windowFilter:subscribe({ hs.window.filter.windowCreated, hs.window.filter.windowFocused }, function(window)
198 | if maximizeMode and
199 | window ~= nil and
200 | window:isStandard() and
201 | window:frame().h > 500 and
202 | hs.fnutils.contains(maximizeWindows, window:application():name())
203 | then
204 | window:maximize()
205 | end
206 | end)
207 |
208 | ----------------------------------------------------------------------------------------------------
209 | -- Keyboard Shortcuts
210 | ----------------------------------------------------------------------------------------------------
211 |
212 | hs.hotkey.bind(modifiers.window, hs.keycodes.map.left, moveCurrentWindowToLeftHalf)
213 | hs.hotkey.bind(modifiers.window, hs.keycodes.map.right, moveCurrentWindowToRightHalf)
214 | hs.hotkey.bind(modifiers.window, hs.keycodes.map.down, moveCurentWindowToNextScreen)
215 | hs.hotkey.bind(modifiers.window, hs.keycodes.map.up, maximizeCurrentWindow)
216 | hs.hotkey.bind(modifiers.window, "c", centerCurrentWindow)
217 |
218 | hs.hotkey.bind(modifiers.hyper, "n", moveCurrentWindowToLeftHalf)
219 | hs.hotkey.bind(modifiers.hyper, "i", moveCurrentWindowToRightHalf)
220 | hs.hotkey.bind(modifiers.hyper, "e", moveCurentWindowToNextScreen)
221 | hs.hotkey.bind(modifiers.hyper, "o", maximizeCurrentWindow)
222 | hs.hotkey.bind(modifiers.hyper, "8", centerCurrentWindow)
223 | hs.hotkey.bind(modifiers.hyper, "l", moveMouseToUpperLeft)
224 | hs.hotkey.bind(modifiers.hyper, "p", moveMouseToUpperRight)
225 | hs.hotkey.bind(modifiers.hyper, "r", moveMouseToLowerLeft)
226 | hs.hotkey.bind(modifiers.hyper, "t", moveMouseToLowerRight)
227 | hs.hotkey.bind(modifiers.hyper, "s", moveMouseToWindowCenter)
228 | hs.hotkey.bind(modifiers.hyper, "x", function() hs.application.launchOrFocusByBundleID(bundleID.teams) end)
229 | hs.hotkey.bind(modifiers.hyper, "u", function() hs.application.launchOrFocusByBundleID(bundleID.iterm) end)
230 | hs.hotkey.bind(modifiers.hyper, "f", function() hs.application.launchOrFocusByBundleID(bundleID.safari) end)
231 | hs.hotkey.bind(modifiers.hyper, "m", function() hs.application.launchOrFocusByBundleID(bundleID.intellij) end)
232 | hs.hotkey.bind(modifiers.hyper, "a", function() hs.application.launchOrFocusByBundleID(bundleID.vsCode) end)
233 | hs.hotkey.bind(modifiers.hyper, "h", function() hs.application.launchOrFocusByBundleID(bundleID.firefox) end)
234 | hs.hotkey.bind(modifiers.hyper, "z", function() hs.application.launchOrFocusByBundleID(bundleID.outlook) end)
235 |
236 | ----------------------------------------------------------------------------------------------------
237 | -- Mouse Shortcuts
238 | ----------------------------------------------------------------------------------------------------
239 |
240 | local function tap_key(mods, key)
241 | return false, {
242 | hs.eventtap.event.newKeyEvent(mods, key, true),
243 | hs.eventtap.event.newKeyEvent(mods, key, false),
244 | }
245 | end
246 |
247 | local mouseBindings = {
248 | [2] = {
249 | -- Safari: Close tab
250 | [bundleID.safari] = function() return tap_key({ modifier.cmd }, "w") end,
251 | -- Google Chrome: Close tab
252 | [bundleID.googleChrome] = function() return tap_key({ modifier.cmd }, "w") end,
253 | -- Firefox: Close tab
254 | [bundleID.firefox] = function() return tap_key({ modifier.cmd }, "w") end,
255 | -- Teams: End call
256 | [bundleID.teams] = function() return tap_key({ modifier.cmd, modifier.shift }, "h") end,
257 | -- Other: Close window
258 | [bundleID.other] = function() return tap_key({ modifier.cmd }, "w") end,
259 | },
260 | [3] = {
261 | -- Safari: Back
262 | [bundleID.safari] = function() return tap_key({ modifier.cmd }, 41) end,
263 | -- Google Chrome: Back
264 | [bundleID.googleChrome] = function() return tap_key({ modifier.cmd }, 41) end,
265 | -- Firefox: Back
266 | [bundleID.firefox] = function() return tap_key({ modifier.cmd }, "left") end,
267 | -- Teams: Toggle mute
268 | [bundleID.teams] = function() return tap_key({ modifier.cmd, modifier.shift }, "m") end,
269 | -- Reeder: Open in Safari
270 | [bundleID.reeder] = function() return tap_key({}, "b") end,
271 | -- Other: Copy to clipboard
272 | [bundleID.other] = function() return tap_key({ "cmd" }, "c") end,
273 | },
274 | [4] = {
275 | -- Safari: Forward
276 | [bundleID.safari] = function() return tap_key({ modifier.cmd }, 39) end,
277 | -- Google Chrome: Forward
278 | [bundleID.googleChrome] = function() return tap_key({ modifier.cmd }, 39) end,
279 | -- Firefox: Forward
280 | [bundleID.firefox] = function() return tap_key({ modifier.cmd }, "right") end,
281 | -- Teams: Toggle video
282 | [bundleID.teams] = function() return tap_key({ modifier.cmd, modifier.shift }, "o") end,
283 | -- Reeder: Mark all as read
284 | [bundleID.reeder] = function() return tap_key({}, "a") end,
285 | -- Other: Paste from clipboard
286 | [bundleID.other] = function() return tap_key({ modifier.cmd }, "v") end,
287 | },
288 | }
289 |
290 | mouseTap = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDown }, function(event)
291 | for i = 2, 4 do
292 | if event:getButtonState(i) then
293 | local action = mouseBindings[i][hs.application.frontmostApplication():bundleID()]
294 | or mouseBindings[i][bundleID.other]
295 | or (function() return true end)
296 | return action()
297 | end
298 | end
299 | return true
300 | end)
301 | mouseTap:start()
302 |
303 | ----------------------------------------------------------------------------------------------------
304 | -- Clipboard Manager
305 | ----------------------------------------------------------------------------------------------------
306 |
307 | clipboard = require("clipboard")
308 | clipboard:start()
309 |
310 | hs.hotkey.bind(modifiers.clipboard, "v", function() clipboard:toggleClipboard() end)
311 | hs.hotkey.bind(modifiers.clipboard, hs.keycodes.map.delete, function() clipboard:clearAll() end)
312 |
313 | ----------------------------------------------------------------------------------------------------
314 | -- Hints
315 | ----------------------------------------------------------------------------------------------------
316 |
317 | hs.hints.fontName = font.monospace
318 | hs.hints.fontSize = 16.0
319 | hs.hints.showTitleThresh = 7
320 | hs.hints.style = "vimperator"
321 |
322 | hs.hotkey.bind(modifiers.window, hs.keycodes.map["return"], function() hs.hints.windowHints(windowFilter:getWindows()) end)
323 | hs.hotkey.bind(modifiers.hyper, hs.keycodes.map["return"], function() hs.hints.windowHints(windowFilter:getWindows()) end)
324 |
--------------------------------------------------------------------------------