diff --git a/hosts/daedalus/hammerspoon.lua b/hosts/daedalus/hammerspoon.lua index 37c06b50..6f2fe8bf 100644 --- a/hosts/daedalus/hammerspoon.lua +++ b/hosts/daedalus/hammerspoon.lua @@ -1,3 +1,4 @@ + ---------------------------------------------------------------------------------------------------- -- Settings ---------------------------------------------------------------------------------------------------- @@ -10,71 +11,246 @@ hs.uploadCrashData(false) hs.window.animationDuration = 0 -local log = hs.logger.new('init', 'debug') +configWatcher = hs.pathwatcher.new(hs.configdir, hs.reload) +configWatcher:start() -configWatcher = hs.pathwatcher.new(hs.configdir, hs.reload):start() +local moonlanderMode = false +local maximizeMode = true +---------------------------------------------------------------------------------------------------- +-- Utilities +---------------------------------------------------------------------------------------------------- + +local modifier = { + cmd = "cmd", + shift = "shift", + ctrl = "ctrl", + option = "alt", +} + +local modifiers = { + hyper = { modifier.cmd, modifier.shift, modifier.ctrl, modifier.option }, + window = { modifier.ctrl, modifier.option }, + clipboard = { modifier.ctrl, modifier.cmd } +} + +local bundleID = { + activityMonitor = "com.apple.ActivityMonitor", + finder = "com.apple.finder", + firefox = "org.mozilla.firefox", + emacs = "org.gnu.emacs", + iterm = "com.googlecode.iterm2", + safari = "com.apple.Safari", + safariTechnologyPreview = "com.apple.SafariTechnologyPreview", + spotify = "com.spotify.client", + bitwarden = "com.bitwarden.desktop", + teams = "com.microsoft.teams", + timeular = "com.timeular.zei", + logseq = "com.electron.logseq" +} + +local usbDevice = { + moonlander = "Moonlander Mark I" +} + +local function languageIsGerman() return hs.host.locale.preferredLanguages()[1]:sub(0, 2) == "de" end + +local function maximizeCurrentWindow() hs.window.focusedWindow():maximize() end + +local function centerCurrentWindow() hs.window.focusedWindow():centerOnScreen() end + +local function moveCurrentWindowToLeftHalf() + local win = hs.window.focusedWindow() + local screenFrame = win:screen():frame() + local newFrame = hs.geometry.rect(screenFrame.x, screenFrame.y, screenFrame.w / 2, screenFrame.h) + win:setFrame(newFrame) +end + +local function moveCurrentWindowToRightHalf() + local win = hs.window.focusedWindow() + local screenFrame = win:screen():frame() + local newFrame = hs.geometry.rect(screenFrame.x + screenFrame.w / 2, screenFrame.y, screenFrame.w / 2, screenFrame.h) + win:setFrame(newFrame) +end + +local function moveCurentWindowToNextScreen() + local win = hs.window.focusedWindow() + win:moveToScreen(win:screen():next()) +end + +local function moveMouseToWindowCenter() + local windowCenter = hs.window.frontmostWindow():frame().center + hs.mouse.absolutePosition(windowCenter) +end + +local function moveMouseToUpperLeft() + local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame() + local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w / 4, screenFrame.y + screenFrame.h / 4) + hs.mouse.absolutePosition(newPoint) +end + +local function moveMouseToUpperRight() + local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame() + local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w * 3 / 4, screenFrame.y + screenFrame.h / 4) + hs.mouse.absolutePosition(newPoint) +end + +local function moveMouseToLowerLeft() + local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame() + local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w / 4, screenFrame.y + screenFrame.h * 3 / 4) + hs.mouse.absolutePosition(newPoint) +end + +local function moveMouseToLowerRight() + local screenFrame = (hs.window.focusedWindow():screen() or hs.screen.primaryScreen()):frame() + local newPoint = hs.geometry.point(screenFrame.x + screenFrame.w * 3 / 4, screenFrame.y + screenFrame.h * 3 / 4) + hs.mouse.absolutePosition(newPoint) +end + +---------------------------------------------------------------------------------------------------- +-- Menu +---------------------------------------------------------------------------------------------------- + +local function menuItems() + return { + { + title = "Hammerspoon " .. hs.processInfo.version, + disabled = true + }, + { title = "-" }, + { + title = "Moonlander Mode", + checked = moonlanderMode, + fn = function() moonlanderDetected(not moonlanderMode) end + }, + { + title = "Maximize Mode", + checked = maximizeMode, + fn = function() maximizeMode = not maximizeMode end + }, + { title = "-" }, + { + title = "Reload", + fn = hs.reload + }, + { + title = "Console...", + fn = hs.openConsole + }, + { title = "-" }, + { + title = "Quit", + fn = function() hs.application.get(hs.processInfo.processID):kill() end + } + } +end + +menu = hs.menubar.new() +menu:setMenu(menuItems) ---------------------------------------------------------------------------------------------------- -- Moonlander Detection ---------------------------------------------------------------------------------------------------- -function isDeviceMoonlander(device) - return device.productName == "Moonlander Mark I" -end +local moonlanderModeConfig = { + [false] = { + keyboardLayout = "Colemak DH ISO copy", + icon = hs.configdir .. "/assets/statusicon_off.tiff" + }, + [true] = { + keyboardLayout = "EurKEY v1.2", + icon = hs.configdir .. "/assets/statusicon_on.tiff" + } +} + +local function isDeviceMoonlander(device) return device.productName == usbDevice.moonlander end function moonlanderDetected(connected) - if connected then - hs.keycodes.setLayout("EurKEY v1.2") - else - hs.keycodes.setLayout("Colemak DH ISO copy") - end + moonlanderMode = connected + hs.keycodes.setLayout(moonlanderModeConfig[connected].keyboardLayout) + menu:setIcon(moonlanderModeConfig[connected].icon) end -function searchMoonlander() - local usbDevices = hs.usb.attachedDevices() - local moonlanderConnected = hs.fnutils.find(usbDevices, isDeviceMoonlander) ~= nil - - moonlanderDetected(moonlanderConnected) +local function searchMoonlander() + local usbDevices = hs.usb.attachedDevices() + local moonlanderConnected = hs.fnutils.find(usbDevices, isDeviceMoonlander) ~= nil + + moonlanderDetected(moonlanderConnected) end searchMoonlander() usbWatcher = hs.usb.watcher.new(function(event) - if event.productName == "Moonlander Mark I" then - moonlanderDetected(event.eventType == "added") - end -end):start() + if event.productName == usbDevice.moonlander then + moonlanderDetected(event.eventType == "added") + end +end) +usbWatcher:start() caffeinateWatcher = hs.caffeinate.watcher.new(function(event) - if event == hs.caffeinate.watcher.systemDidWake then - searchMoonlander() - end -end):start() - + if event == hs.caffeinate.watcher.systemDidWake then + searchMoonlander() + end +end) +caffeinateWatcher:start() ---------------------------------------------------------------------------------------------------- --- mount ds9 via tailscale +-- Window Management ---------------------------------------------------------------------------------------------------- --- kaputti ---function mountDS9() --- local ssid = hs.wifi.currentNetwork() --- if ssid ~= nil and ssid ~= 'vim' then -- not at home --- if os.execute("mount | grep //ragon@ds9._smb._tcp.local/data") == nil then -- check if mounted via mdns --- os.execute("diskutil umount /Volumes/data") -- umount share if it exists --- hs.osascript.applescript('mount volume "smb://ragon@ds9.ragon000.github.beta.tailscale.net/data"') -- mount share via tailscale --- end --- end ---end --- ---mountDS9() --- ---hs.wifi.watcher.new(function(watcher, message, interface) --- mountDS9() ---end):start() + +hs.window.filter.ignoreAlways = { + ["Mail Web Content"] = true, + ["Mail-Webinhalt"] = true, + ["QLPreviewGenerationExtension (Finder)"] = true, + ["Reeder Web Content"] = true, + ["Reeder-Webinhalt"] = true, + ["Safari Web Content (Cached)"] = true, + ["Safari Web Content (Prewarmed)"] = true, + ["Safari Web Content"] = true, + ["Safari Technology Preview Web Content (Cached)"] = true, + ["Safari Technology Preview Web Content (Prewarmed)"] = true, + ["Safari Technology Preview Web Content"] = true, + ["Safari-Webinhalt (im Cache)"] = true, + ["Safari-Webinhalt (vorgeladen)"] = true, + ["Safari-Webinhalt"] = true, + ["Strongbox (Safari)"] = true, +} +windowFilter = hs.window.filter.new({ + "App Store", + "Code", + "DataGrip", + "Firefox", + "Fork", + "Fotos", + "Google Chrome", + "Vivaldi", + "IntelliJ IDEA", + "Mail", + "Emacs", + "Microsoft Outlook", + "Microsoft Teams", + "Music", + "Musik", + "Photos", + "Postman", + "Reeder", + "Safari", + "Safari Technology Preview", + "Spotify", + "Strongbox", + "BitWarden", + "Logseq", + "Timeular", + "Tower", +}) +windowFilter:subscribe({ hs.window.filter.windowCreated, hs.window.filter.windowFocused }, function(window) + if maximizeMode and window ~= nil and window:isStandard() and window:frame().h > 500 then + window:maximize() + end +end) ---------------------------------------------------------------------------------------------------- --- Scratchpad +-- Keyboard Shortcuts ---------------------------------------------------------------------------------------------------- function showHideBundleId(bundleId) @@ -87,14 +263,177 @@ function showHideBundleId(bundleId) end end -local hyperModifier = {"cmd", "shift", "ctrl", "alt"} -local cmdShiftModifier = {"cmd", "shift"} -hs.hotkey.bind(hyperModifier, "b", function() showHideBundleId("com.bitwarden.desktop") end) -hs.hotkey.bind(hyperModifier, "p", function() showHideBundleId("com.timeular.zei") end) -hs.hotkey.bind(hyperModifier, "l", function() showHideBundleId("com.electron.logseq") end) -hs.hotkey.bind(hyperModifier, "e", function() hs.task.new("@myEmacs@/bin/emacsclient", nil, function() return false end, {"-c"}):start() end) -hs.hotkey.bind(hyperModifier, "i", function() hs.task.new("@myEmacs@/bin/emacsclient", nil, function() return false end, {"--eval", "(emacs-everywhere)"}):start() end) +hs.hotkey.bind(modifiers.window, hs.keycodes.map.left, moveCurrentWindowToLeftHalf) +hs.hotkey.bind(modifiers.window, hs.keycodes.map.right, moveCurrentWindowToRightHalf) +hs.hotkey.bind(modifiers.window, hs.keycodes.map.down, moveCurentWindowToNextScreen) +hs.hotkey.bind(modifiers.window, hs.keycodes.map["return"], maximizeCurrentWindow) +hs.hotkey.bind(modifiers.window, "c", centerCurrentWindow) + +hs.hotkey.bind(modifiers.hyper, "[", moveMouseToWindowCenter) +hs.hotkey.bind(modifiers.hyper, "m", moveMouseToUpperLeft) +hs.hotkey.bind(modifiers.hyper, "o", moveMouseToUpperRight) +hs.hotkey.bind(modifiers.hyper, hs.keycodes.map.up, moveMouseToLowerLeft) +hs.hotkey.bind(modifiers.hyper, hs.keycodes.map.down, moveMouseToLowerRight) +hs.hotkey.bind(modifiers.hyper, hs.keycodes.map.delete, function() hs.caffeinate.lockScreen() end) +hs.hotkey.bind(modifiers.hyper, "a", function() showHideBundleId(bundleID.activityMonitor) end) +hs.hotkey.bind(modifiers.hyper, "c", function() showHideBundleId(bundleID.safari) end) +hs.hotkey.bind(modifiers.hyper, "f", function() showHideBundleId(bundleID.finder) end) +hs.hotkey.bind(modifiers.hyper, "e", function() hs.task.new("@myEmacs@/bin/emacsclient", nil, function() return false end, {"-c"}):start() end) +hs.hotkey.bind(modifiers.hyper, "i", function() hs.task.new("@myEmacs@/bin/emacsclient", nil, function() return false end, {"--eval", "(emacs-everywhere)"}):start() end) +hs.hotkey.bind(modifiers.hyper, "p", function() showHideBundleId(bundleID.timeular) end) +hs.hotkey.bind(modifiers.hyper, "b", function() showHideBundleId(bundleID.bitwarden) end) +hs.hotkey.bind(modifiers.hyper, "t", function() showHideBundleId(bundleID.iterm) end) +hs.hotkey.bind({ modifier.cmd }, "\\", function() + local application = hs.application.frontmostApplication() + + if application:bundleID() == bundleID.bitwarden then + application:hide() + else + hs.application.launchOrFocusByBundleID(bundleID.bitwarden) + end +end) ---------------------------------------------------------------------------------------------------- --- Tiling +-- Mouse Shortcuts ---------------------------------------------------------------------------------------------------- + +local function handleMouse2() + local application = hs.application.frontmostApplication() + + -- Safari: Close tab + if application:bundleID() == bundleID.safari then + hs.eventtap.keyStroke({ modifier.cmd }, "w") + + -- Safari Technology Preview: Close tab + elseif application:bundleID() == bundleID.safariTechnologyPreview then + hs.eventtap.keyStroke({ modifier.cmd }, "w") + + -- Google Chrome: Close tab + elseif application:bundleID() == bundleID.googleChrome then + hs.eventtap.keyStroke({ modifier.cmd }, "w") + + -- Firefox: Close tab + elseif application:bundleID() == bundleID.firefox then + hs.eventtap.keyStroke({ modifier.cmd }, "w") + + -- Teams: End call + elseif application:bundleID() == bundleID.teams then + hs.eventtap.keyStroke({ modifier.cmd, modifier.shift }, "h") + + -- Spotify: Toggle play + elseif application:bundleID() == bundleID.spotify then + hs.eventtap.keyStroke({}, "space") + end +end + +local function handleMouse3() + local application = hs.application.frontmostApplication() + + -- Safari: Back + if application:bundleID() == bundleID.safari then + if languageIsGerman() then + application:selectMenuItem({ "Verlauf", "Zurück" }) + else + application:selectMenuItem({ "History", "Back" }) + end + + -- Safari Technology Preview: Back + elseif application:bundleID() == bundleID.safariTechnologyPreview then + application:selectMenuItem({ "History", "Back" }) + + -- Google Chrome: Back + elseif application:bundleID() == bundleID.googleChrome then + if languageIsGerman() then + application:selectMenuItem({ "Verlauf", "Zurück" }) + else + application:selectMenuItem({ "History", "Back" }) + end + + -- Firefox: Back + elseif application:bundleID() == bundleID.firefox then + hs.eventtap.keyStroke({ modifier.cmd }, "left") + + -- Teams: Toggle mute + elseif application:bundleID() == bundleID.teams then + hs.eventtap.keyStroke({ modifier.cmd, modifier.shift }, "m") + + -- Spotify: Next + elseif application:bundleID() == bundleID.spotify then + hs.eventtap.keyStroke({ modifier.cmd }, "right") + + -- Reeder: Open in Safari + elseif application:bundleID() == bundleID.reeder then + hs.eventtap.keyStroke({}, "b") + + -- Other: Copy to clipboard + else + hs.eventtap.keyStroke({ "cmd" }, "c") + end +end + +local function handleMouse4() + local application = hs.application.frontmostApplication() + + -- Safari: Forward + if application:bundleID() == bundleID.safari then + if languageIsGerman() then + application:selectMenuItem({ "Verlauf", "Vorwärts" }) + else + application:selectMenuItem({ "History", "Forward" }) + end + + -- Safari Technology Preview: Forward + elseif application:bundleID() == bundleID.safariTechnologyPreview then + application:selectMenuItem({ "History", "Forward" }) + + -- Google Chrome: Forward + elseif application:bundleID() == bundleID.googleChrome then + if languageIsGerman() then + application:selectMenuItem({ "Verlauf", "Vorwärts" }) + else + application:selectMenuItem({ "History", "Forward" }) + end + + -- Firefox: Forward + elseif application:bundleID() == bundleID.firefox then + hs.eventtap.keyStroke({ modifier.cmd }, "right") + + -- Teams: Toggle video + elseif application:bundleID() == bundleID.teams then + hs.eventtap.keyStroke({ modifier.cmd, modifier.shift }, "o") + + -- Spotify: Previous + elseif application:bundleID() == bundleID.spotify then + hs.eventtap.keyStroke({ modifier.cmd }, "left") + + -- Reeder: Mark all as read + elseif application:bundleID() == bundleID.reeder then + hs.eventtap.keyStroke({}, "a") + + -- Other: Paste from clipboard + else + hs.eventtap.keyStroke({ modifier.cmd }, "v") + end +end + +mouseTap = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDown }, function(event) + if event:getButtonState(2) then + handleMouse2() + elseif event:getButtonState(3) then + handleMouse3() + elseif event:getButtonState(4) then + handleMouse4() + end + return true +end) +mouseTap:start() + +---------------------------------------------------------------------------------------------------- +-- Clipboard Manager +---------------------------------------------------------------------------------------------------- + +-- clipboard = require("clipboard") +-- clipboard:start() +-- +-- hs.hotkey.bind(modifiers.clipboard, "v", function() clipboard:toggleClipboard() end) +-- hs.hotkey.bind(modifiers.clipboard, hs.keycodes.map.delete, function() clipboard:clearAll() end)