├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/statusicon_off.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengelbrecht/hammerspoon-config/31f07df8045b9776303ffdda5eb6ebec45e82308/assets/statusicon_off.tiff -------------------------------------------------------------------------------- /assets/statusicon_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #! 5 | 6 | 7 | 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 | --------------------------------------------------------------------------------