Last active
May 23, 2022 01:57
-
-
Save Adriem/d8f98308c91a9688f7efaa0f93729f12 to your computer and use it in GitHub Desktop.
Script for managing windows in hammerspoon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local US = 'U.S.' | |
| local US_INTER = 'U.S. International - PC' | |
| local config = { | |
| ['iTerm2'] = US, | |
| ['IntelliJ IDEA'] = US | |
| } | |
| local default = US_INTER | |
| function handleAppWatcher (appName, eventType, app) | |
| if (eventType == hs.application.watcher.launched | |
| or eventType == hs.application.watcher.activated) then | |
| hs.keycodes.setLayout(config[appName] or default); | |
| end | |
| end | |
| hs.application.watcher.new(handleAppWatcher):start() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ============================================================================= | |
| -- Shortcuts for managing windows and window layouts. | |
| -- | |
| -- In order to enable this module, just require it | |
| -- from init.lua file inside ~/.hammerspoon/ folder | |
| -- | |
| -- author: Adrian Moreno | |
| -- ============================================================================= | |
| -- ---===[ CONFIG ]===---------------------------------------------------------- | |
| -- BINDINGS are lists of objects in which the first element represents the | |
| -- prefix of the key strokes and the second one represents the key that will | |
| -- trigger the action | |
| -- ACTIONS are objects that contain a test() method that checks if the | |
| -- action has been performed and an exec() method which performs the action | |
| -- Bindings are automatically bound to actions based on the name of each entry. | |
| -- In order to add a new action, it is necessary to add a new entry with the | |
| -- same key to both objects. When a keystroke is pressed, it will cycle over | |
| -- all the actions. If it finds an action that has already been executed, it | |
| -- will call the next action; otherwise it will call the first one. | |
| function config() | |
| hs.window.animationDuration = 0 | |
| bindings = { | |
| -- Snap a window to a grid on the active screen | |
| snapWindowLeft = {{"cmd", "ctrl"}, "h"}, | |
| snapWindowRight = {{"cmd", "ctrl"}, "l"}, | |
| snapWindowCenter = {{"cmd", "ctrl"}, "k"}, | |
| snapWindowDown = {{"cmd", "ctrl"}, "j"}, | |
| -- Move a window to an adjacent screen | |
| moveToNorthScreen = {{"cmd", "ctrl", "shift"}, "k"}, | |
| moveToSouthScreen = {{"cmd", "ctrl", "shift"}, "j"}, | |
| moveToEastScreen = {{"cmd", "ctrl", "shift"}, "l"}, | |
| moveToWestScreen = {{"cmd", "ctrl", "shift"}, "h"} | |
| } | |
| actions = { | |
| -- Snap a window to a grid on the active screen | |
| snapWindowLeft = { | |
| snapLeft(0.5), | |
| snapLeft(0.65), | |
| snapLeft(0.35), | |
| }, | |
| snapWindowRight = { | |
| snapRight(0.5), | |
| snapRight(0.65), | |
| snapRight(0.35), | |
| }, | |
| snapWindowCenter = { | |
| snapToUnits(hs.layout.maximized), | |
| snapCenter(0.65, 0.75), | |
| snapCenter(0.75, 0.65), | |
| }, | |
| snapWindowDown = { | |
| snapVertical(0, 1), | |
| snapVertical(0, 0.5), | |
| snapVertical(0.5, 0.5), | |
| }, | |
| -- Move a window to an adjacent screen | |
| moveToNorthScreen = { moveToScreen('north') }, | |
| moveToSouthScreen = { moveToScreen('south') }, | |
| moveToEastScreen = { moveToScreen('east') }, | |
| moveToWestScreen = { moveToScreen('west') } | |
| } | |
| -- For each action binding, cycle through available actions | |
| for actionName,binding in pairs(bindings) do | |
| local actionList = actions[actionName] or {} | |
| local cycleActionsHandler = function() | |
| local actionIdx = 1 -- Indexes in Lua are 1-based | |
| while actionIdx < #actionList and not actionList[actionIdx].test() do | |
| actionIdx = actionIdx + 1 | |
| end | |
| actionIdx = actionIdx % #actionList + 1 | |
| actionList[actionIdx].exec() | |
| end | |
| hs.hotkey.bind(binding[1], binding[2], cycleActionsHandler) | |
| end | |
| end | |
| -- ---===[ SNAPPING HELPERS ]===------------------------------------------------ | |
| function snapLeft(width) | |
| return snapToUnits({ | |
| x = 0.005, | |
| y = 0.01, | |
| w = width - 0.01, | |
| h = 0.985 | |
| }) | |
| end | |
| function snapRight(width) | |
| return snapToUnits({ | |
| x = 1 - width, | |
| y = 0.01, | |
| w = width - 0.005, | |
| h = 0.985 | |
| }) | |
| end | |
| function snapCenter(width, height) | |
| return snapToUnits({ | |
| x = (1 - width) / 2 + 0.005, | |
| y = (1 - height) / 2 + 0.005, | |
| w = width, | |
| h = height | |
| }) | |
| end | |
| function snapVertical(y, height) | |
| return snapToUnits({ | |
| y = y + 0.01, | |
| h = height - 0.015 | |
| }) | |
| end | |
| function snapToUnits(positionUnits) | |
| return { | |
| -- Return true if the window is already on the target position | |
| test = (function() | |
| local screen = hs.screen.mainScreen() | |
| local window = hs.window.focusedWindow() | |
| local targetGeom = calculateTargetGeom(positionUnits) | |
| return matchGrid(window, targetGeom, screen) | |
| end), | |
| -- Move the window to the target relative position | |
| exec = (function() | |
| local screen = hs.screen.mainScreen() | |
| local window = hs.window.focusedWindow() | |
| local targetGeom = calculateTargetGeom(positionUnits) | |
| window:moveToUnit(targetGeom) | |
| correctWindowPosition(window, screen) | |
| if not matchGrid(window, targetGeom, screen) then | |
| snapExceptions.set(window:id(), targetGeom, window:frame()) | |
| end | |
| end) | |
| } | |
| end | |
| function calculateTargetGeom(positionUnits) | |
| local screenGeom = hs.screen.mainScreen():frame() | |
| local windowGeom = hs.window.focusedWindow():frame() | |
| local relativeX = math.ceil(windowGeom.x / screenGeom.w * 1000) / 1000 | |
| local relativeW = math.ceil(windowGeom.w / screenGeom.w * 1000) / 1000 | |
| local relativeY = math.ceil(windowGeom.y / screenGeom.h * 1000) / 1000 | |
| local relativeH = math.ceil(windowGeom.h / screenGeom.h * 1000) / 1000 | |
| return { | |
| x = positionUnits.x or math.max(relativeX, 0.005), | |
| y = positionUnits.y or math.max(relativeY, 0.01), | |
| w = positionUnits.w or math.min(relativeW, 0.99), | |
| h = positionUnits.h or math.min(relativeH, 0.985) | |
| } | |
| end | |
| function matchGrid(window, grid, screen) | |
| local screenGeom = screen:frame() | |
| local windowGeom = window:frame() | |
| local expectedGeom = (snapExceptions.get(window:id(), grid) | |
| or hs.geometry.new(grid):fromUnitRect(screenGeom):floor()) | |
| return windowGeom:equals(expectedGeom) | |
| end | |
| snapExceptions = (function() | |
| snapExceptions = {} | |
| function positionToId(positionUnits) | |
| local re= string.format("%1.3f-%1.3f-%1.3f-%1.3f-%d", | |
| positionUnits.x, positionUnits.y, positionUnits.w, positionUnits.h, | |
| hs.screen.mainScreen():id()) | |
| print(re) | |
| return re | |
| end | |
| return { | |
| set = (function(windowId, expectedPos, actualPos) | |
| snapExceptions[windowId] = snapExceptions[windowId] or {} | |
| snapExceptions[windowId][positionToId(expectedPos)] = actualPos | |
| end), | |
| get = (function(windowId, expectedPos) | |
| return (snapExceptions[windowId] | |
| and snapExceptions[windowId][positionToId(expectedPos)]) | |
| end) | |
| } | |
| end)() | |
| function correctWindowPosition(window, screen) | |
| local windowGeometry = window:frame() | |
| local screenGeometry = screen:frame() | |
| local newGeom = windowGeometry:copy() | |
| local applyCorrections = false | |
| local maxWidth = screenGeometry. x + screenGeometry.w - windowGeometry.x | |
| if windowGeometry.w > maxWidth then | |
| applyCorrections = true | |
| newGeom.x = (screenGeometry.x + screenGeometry.w | |
| - windowGeometry.w - screenGeometry.h * 0.005) | |
| end | |
| -- TODO: Correct vertical position | |
| if applyCorrections then window:move(newGeom) end | |
| end | |
| -- ---===[ MOVE HELPERS ]===---------------------------------------------------- | |
| function moveToScreen(direction) | |
| -- direction = 'north' | 'south' | 'east' | 'west' (case insensitive) | |
| return { | |
| test = (function() return false end), | |
| exec = (function() | |
| -- Check if window was `snapped` | |
| local snapAction = nil | |
| for actionName, actionList in pairs(actions) do | |
| if actionName:sub(1,4) == 'snap' then | |
| snapAction = hs.fnutils.find(actionList, function(action) | |
| return action.test() | |
| end) | |
| end | |
| end | |
| -- Move window | |
| local functionName = ('moveOneScreen' | |
| ..direction:sub(1,1):upper() | |
| ..direction:sub(2):lower()) | |
| hs.window[functionName](hs.window.focusedWindow(), false, true) | |
| if snapAction then snapAction.exec() end | |
| end) | |
| } | |
| end | |
| -- ---===[ SETUP ]===----------------------------------------------------------- | |
| config() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment