├── .gitignore ├── LICENSE ├── README.md ├── border.lua ├── init.lua ├── keyboard.lua ├── tools.lua ├── tree.lua └── window.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 NTT123 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hswm 2 | A tiling window manager on macOS, powered by Lua and Hammerspoon. 3 | 4 | 5 | # Install 6 | 7 | - You need to install Hammerspoon first. You can get it here http://www.hammerspoon.org/ 8 | 9 | - To support multi spaces, you also need to install https://github.com/asmagill/hs._asm.undocumented.spaces.git . You will need Xcode to compile and install this library. If you get a *modulemap file error* when compiling, try this command instead: 10 | > `CFLAGS=-fmodule-map-file=/usr/include/xpc/module.modulemap make install` 11 | 12 | - Create config directory with 13 | 14 | > `git clone https://github.com/NTT123/hswm.git ~/.hammerspoon` 15 | 16 | It will clone this repository to hammerspoon config directory. 17 | 18 | Now, start hammerspoon app. Good luck. 19 | 20 | # Usage 21 | 22 | ### Swap two windows 23 | Hold `Ctrl` and `left mouse` clicked to the window you want to swap, then drag the mouse to the other window which you want to swap with. 24 | 25 | Release the mouse to take action. 26 | 27 | ### Resize windows 28 | 29 | Hold `Ctrl` and `right mouse` clicked, then drag the mouse to resize windows. 30 | 31 | Release the mouse to take action. 32 | 33 | ## Moving focuses between windows 34 | 35 | Hold `Shift` + `Up` | `Down` | `Left` | `Right` keys to move to a new focused window. 36 | 37 | ### Mirror windows 38 | 39 | Hold `Alt` + `y` to mirror the current focused window in the y-axis. 40 | 41 | Hold `Alt` + `x` to mirror the current focused window in the x-axis. 42 | 43 | ### Swap horizontal and vertical splittings 44 | 45 | Hold `Alt` + `e` to swap between splitting the window horizontally and vertically. 46 | 47 | # Screenshot 48 | 49 | ![Imgur](https://i.imgur.com/DDvKkGt.png) 50 | 51 | # Tips 52 | 53 | ### Turn on Dock hiding 54 | 55 | ![](https://i.imgur.com/G6bibkm.png) 56 | 57 | ### Turn on menubar hiding 58 | 59 | ![](https://i.imgur.com/BknMXV0.png) 60 | 61 | ### Hide iTerm titlebar 62 | 63 | ![Imgur](https://i.imgur.com/JhoUVFP.png) 64 | 65 | Looking for `Style` -> `No titlebar` 66 | 67 | # How thing works 68 | 69 | Windows are managed in a binary tree. 70 | 71 | # Hacking 72 | 73 | Enjoy your hacks by editing `~/.hammerspoon/init.lua` and `tree.lua`. 74 | 75 | 76 | # Credit 77 | 78 | This is a Lua version mini-copy of chunkwm at https://github.com/koekeishiya/chunkwm 79 | 80 | -------------------------------------------------------------------------------- /border.lua: -------------------------------------------------------------------------------- 1 | local pkg = {} 2 | 3 | local tools = dofile( os.getenv("HOME") .. "/.hammerspoon/tools.lua") 4 | 5 | local function cloneBorder(frame) 6 | local f = {x = frame.x, y= frame.y, w= frame.w, h = frame.h} 7 | return f 8 | end 9 | 10 | pkg.cloneBorder = cloneBorder 11 | 12 | local function create_canvas_border(frame) 13 | local border = {} 14 | border.action = "stroke" 15 | border.strokeColor = {alpha = 1, red = 1.0, green = 1.0} 16 | border.fillColor = { alpha = 1 } 17 | border.antialias = false 18 | border.arcRadii = false 19 | border.canvasAlpha = 1.0 20 | border.imageAlpha = 1.0 21 | border.strokeWidth = 4 22 | border.frame = cloneBorder(frame) 23 | border.type = "rectangle" 24 | 25 | return border 26 | end 27 | pkg.create_canvas_border = create_canvas_border 28 | 29 | local function init_border() 30 | local Border = hs.drawing.rectangle(hs.geometry.rect(0,0,0,0)) 31 | Border:setFill(false) 32 | Border:setStrokeWidth(2) 33 | Border:setStrokeColor({["red"]=1,["blue"]=0,["green"]=0,["alpha"]=0.8}) 34 | return Border 35 | end 36 | 37 | pkg.init_border = init_border 38 | 39 | local function convertFromBorderToFrame(root) 40 | if root == nil then 41 | return 42 | end 43 | 44 | if root.canvas ~= nil then 45 | root.canvas:delete() 46 | root.canvas = nil 47 | end 48 | 49 | if root.border_ ~= nil then 50 | root.frame = tools.cloneFrame(root.border_) 51 | root.border_ = nil 52 | end 53 | 54 | convertFromBorderToFrame(root.left) 55 | convertFromBorderToFrame(root.right) 56 | end 57 | 58 | pkg.convertFromBorderToFrame = convertFromBorderToFrame 59 | 60 | local function travelAndAppendToCanvas(root, canvas) 61 | if root == nil then 62 | return 63 | end 64 | 65 | if root.windowID ~= nil and root.border_ ~= nil then 66 | canvas:appendElements( create_canvas_border(root.border_) ) 67 | 68 | return 69 | end 70 | 71 | if root.left ~= nil then 72 | travelAndAppendToCanvas(root.left, canvas) 73 | end 74 | 75 | if root.right ~= nil then 76 | travelAndAppendToCanvas(root.right, canvas) 77 | end 78 | end 79 | 80 | pkg.travelAndAppendToCanvas = travelAndAppendToCanvas 81 | 82 | local function create_window_border(frame) 83 | local border = hs.drawing.rectangle(hs.geometry.rect(frame)) 84 | 85 | border:setFill(false) 86 | border:setStrokeWidth(5) 87 | border:setStrokeColor({["red"]=1,["blue"]=0,["green"]=1,["alpha"]=0.9}) 88 | border = border:wantsLayer(true) 89 | return border 90 | end 91 | 92 | local function rescaleBorder(node, canvas) 93 | if node == nil then 94 | return 95 | end 96 | 97 | if node.windowID then 98 | return 99 | end 100 | 101 | node.left.border_ = tools.cloneFrame(node.left.frame) 102 | node.right.border_ = tools.cloneFrame(node.right.frame) 103 | 104 | local f1 = node.left.border_ 105 | local f2 = node.right.border_ 106 | 107 | if node.isDividedHorizontal then 108 | 109 | local H = f1.h + f2.h 110 | local r = f1.h / f2.h 111 | local delta = node.border_.h - H 112 | local h2 = delta / (r + 1.0) 113 | local h1 = (delta - h2) 114 | f1.h = f1.h + h1 115 | f1.w = node.border_.w 116 | f1.x = node.border_.x 117 | f1.y = node.border_.y 118 | 119 | f2.x = f1.x 120 | f2.y = f1.y + f1.h 121 | f2.h = node.border_.h - f1.h 122 | f2.w = f1.w 123 | else 124 | local W = f1.w + f2.w 125 | local r = f1.w / f2.w 126 | local delta = node.border_.w - W 127 | local w2 = delta / (r + 1) 128 | local w1 = delta - w2 129 | 130 | f1.w = f1.w + w1 131 | f1.h = node.border_.h 132 | f1.x = node.border_.x 133 | f1.y = node.border_.y 134 | 135 | f2.x = f1.x + f1.w 136 | f2.y = f1.y 137 | f2.w = node.border_.w - f1.w 138 | f2.h = f1.h 139 | end 140 | 141 | node.left.border_ = tools.cloneFrame(f1) 142 | node.right.border_ = tools.cloneFrame(f2) 143 | rescaleBorder(node.left, canvas) 144 | rescaleBorder(node.right, canvas) 145 | end 146 | 147 | pkg.rescaleBorder = rescaleBorder 148 | 149 | 150 | return pkg 151 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | local hswm = {} 2 | 3 | local GLOBAL = {} 4 | 5 | GLOBAL.root = nil 6 | GLOBAL.global_padding = 40 7 | GLOBAL.window_padding = 5 8 | 9 | hs.window.animationDuration = 0.0 10 | 11 | -- local tree = dofile( os.getenv("HOME") .. "/.hammerspoon/tree.lua") 12 | -- local border = dofile( os.getenv("HOME") .. "/.hammerspoon/border.lua") 13 | -- local tools = dofile( os.getenv("HOME") .. "/.hammerspoon/tools.lua") 14 | 15 | hswm.window_handlers = dofile( os.getenv("HOME") .. "/.hammerspoon/window.lua") 16 | hswm.window_handlers.init(GLOBAL) 17 | 18 | hswm.keyboard_handlers = dofile( os.getenv("HOME") .. "/.hammerspoon/keyboard.lua") 19 | hswm.keyboard_handlers.init(GLOBAL, hswm.window_handlers.window_manager) 20 | 21 | hswm.GLOBAL = GLOBAL 22 | 23 | return hswm 24 | -------------------------------------------------------------------------------- /keyboard.lua: -------------------------------------------------------------------------------- 1 | local pkg = {} 2 | 3 | local tree = dofile( os.getenv("HOME") .. "/.hammerspoon/tree.lua") 4 | 5 | local function moveFocuses(root, direction) 6 | local fw = hs.window.focusedWindow() 7 | if fw ~= nil then 8 | focusedWindowID = fw:id() 9 | else 10 | return 11 | end 12 | 13 | if fw:isStandard() then 14 | local node = tree.getNodeFromWindowID(root, focusedWindowID) 15 | if node == nil then 16 | return 17 | end 18 | 19 | local id = tree.getNextToNode(root, node, direction) 20 | if id ~= nil then 21 | local w = hs.window.get(id) 22 | if w ~= nil then 23 | hs.window.focus(w) 24 | end 25 | end 26 | 27 | end 28 | end 29 | 30 | 31 | local function init(GLOBAL, window_manager_callback) 32 | 33 | pkg.GLOBAL = GLOBAL 34 | 35 | pkg.right = hs.hotkey.bind({"shift"}, "Right", function() 36 | hs.timer.doAfter(0, function () 37 | moveFocuses(pkg.GLOBAL.root, "right") 38 | end) 39 | end ) 40 | 41 | pkg.left = hs.hotkey.bind({"shift"}, "Left", function() 42 | hs.timer.doAfter(0, function() 43 | moveFocuses(pkg.GLOBAL.root, "left") 44 | end) 45 | end ) 46 | 47 | pkg.down = hs.hotkey.bind({"shift"}, "Down", function() 48 | hs.timer.doAfter(0, function() 49 | moveFocuses(pkg.GLOBAL.root, "down") 50 | end) 51 | end ) 52 | 53 | pkg.up = hs.hotkey.bind({"shift"}, "Up", function() 54 | hs.timer.doAfter(0, function() 55 | moveFocuses(pkg.GLOBAL.root, "up") 56 | end) 57 | end ) 58 | 59 | pkg.swapy = hs.hotkey.bind({"alt"}, "y", function() 60 | hs.timer.doAfter(0, function() 61 | tree.swap_y(pkg.GLOBAL.root) 62 | window_manager_callback() 63 | end ) 64 | end ) 65 | 66 | pkg.swapx = hs.hotkey.bind({"alt"}, "x", function() 67 | hs.timer.doAfter(0, function() 68 | tree.swap_x(pkg.GLOBAL.root) 69 | window_manager_callback() 70 | end ) 71 | end ) 72 | 73 | pkg.swaphv = hs.hotkey.bind({"alt"}, "e", function() 74 | hs.timer.doAfter(0, function() 75 | tree.swap_hv(pkg.GLOBAL.root) 76 | window_manager_callback() 77 | end ) 78 | end ) 79 | 80 | end 81 | 82 | pkg.init = init 83 | 84 | return pkg 85 | 86 | -------------------------------------------------------------------------------- /tools.lua: -------------------------------------------------------------------------------- 1 | local pkg = {} 2 | 3 | local function cloneFrame(frame) 4 | local f = hs.geometry({x = frame.x, y= frame.y, w= frame.w, h = frame.h}) 5 | return f 6 | end 7 | 8 | pkg.cloneFrame = cloneFrame 9 | 10 | -- rounding up the coordinates in a grid of 10 pixels 11 | local function grid(frame) 12 | local new = cloneFrame(frame) 13 | new.x = math.ceil(frame.x / 10) * 10 14 | new.y = math.ceil(frame.y / 10) * 10 15 | new.w = math.floor(frame.w / 10) * 10 16 | new.h = math.floor(frame.h / 10) * 10 17 | return new 18 | end 19 | 20 | pkg.grid = grid 21 | 22 | -- for debug 23 | local function dump(o) 24 | if type(o) == 'table' then 25 | local s = '{ ' 26 | for k,v in pairs(o) do 27 | if type(k) ~= 'number' then k = '"'..k..'"' end 28 | s = s .. '['..k..'] = ' .. dump(v) .. ',' 29 | end 30 | return s .. '} ' 31 | else 32 | return tostring(o) 33 | end 34 | end 35 | 36 | pkg.dump = dump 37 | 38 | return pkg 39 | 40 | -------------------------------------------------------------------------------- /tree.lua: -------------------------------------------------------------------------------- 1 | local pkg = {} 2 | 3 | local tools = dofile( os.getenv("HOME") .. "/.hammerspoon/tools.lua") 4 | local border = dofile( os.getenv("HOME") .. "/.hammerspoon/border.lua") 5 | 6 | 7 | local function copyNode(fromA, toB) 8 | toB.frame = fromA.frame 9 | toB.left = fromA.left 10 | toB.right = fromA.right 11 | toB.isDividedHorizontal = fromA.isDividedHorizontal 12 | toB.isAvailable = fromA.isAvailable 13 | toB.windowID = fromA.windowID 14 | end 15 | 16 | local function createNode(father) 17 | local node = {} 18 | node.left = nil 19 | node.right = nil 20 | node.isDividedHorizontal = false 21 | node.isAvailable = true 22 | node.windowID = nil 23 | node.frame = hs.geometry.rect(0, 0, 0, 0) 24 | node.floating = false 25 | node.father = father 26 | 27 | return node 28 | end 29 | 30 | local function initTreeforWorkSpace(padding) 31 | local tree = createNode() 32 | local f = hs.screen.mainScreen():frame() 33 | 34 | tree.frame = hs.geometry.rect(f.x + padding, f.y + padding, f.w - 2 * padding, f.h - 2*padding) 35 | 36 | 37 | return tree 38 | end 39 | 40 | pkg.initTreeforWorkSpace = initTreeforWorkSpace 41 | 42 | local function splitVertical(node, window) 43 | local leftNode = createNode(node) 44 | local rightNode = createNode(node) 45 | 46 | leftNode.frame = hs.geometry(node.frame.x, node.frame.y, 0.7*node.frame.w, node.frame.h) 47 | 48 | leftNode.windowID = node.windowID 49 | leftNode.isAvailable = false 50 | leftNode.isDividedHorizontal = true 51 | 52 | rightNode.frame = hs.geometry(node.frame.x + 0.7*node.frame.w , node.frame.y, 0.3*node.frame.w, node.frame.h) 53 | rightNode.isAvailable = false 54 | rightNode.windowID = window:id() 55 | rightNode.isDividedHorizontal = true 56 | 57 | node.left = leftNode 58 | node.right = rightNode 59 | node.windowID = nil 60 | node.splitHorizontal = false 61 | end 62 | 63 | local function splitHorizontal(node, window) 64 | local leftNode = createNode(node) 65 | local rightNode = createNode(node) 66 | leftNode.frame = hs.geometry(node.frame.x, node.frame.y, node.frame.w, 0.7*node.frame.h) 67 | leftNode.windowID = node.windowID 68 | leftNode.isAvailable = false 69 | leftNode.isDividedHorizontal = false 70 | 71 | 72 | rightNode.frame = hs.geometry(node.frame.x, node.frame.y + 0.7*node.frame.h, node.frame.w, 0.3*node.frame.h ) 73 | rightNode.isAvailable = false 74 | rightNode.windowID = window:id() 75 | rightNode.isDividedHorizontal = false 76 | 77 | node.left = leftNode 78 | node.right = rightNode 79 | node.windowID = nil 80 | node.splitHorizontal = true 81 | end 82 | 83 | local function heightOfNode(node) 84 | if node == nil then 85 | return 0 86 | else 87 | local h1 = heightOfNode(node.left) 88 | local h2 = heightOfNode(node.right) 89 | if h1 > h2 then 90 | return h1 91 | else 92 | return h2 93 | end 94 | end 95 | end 96 | 97 | local function insertToNode(node, win) 98 | if node == nil then 99 | return 100 | end 101 | 102 | if node.frame.w > node.frame.h then 103 | node.isDividedHorizontal = false 104 | else 105 | node.isDividedHorizontal = true 106 | end 107 | 108 | if node.isDividedHorizontal then 109 | splitHorizontal(node, win) 110 | else 111 | splitVertical(node, win) 112 | end 113 | end 114 | 115 | pkg.insertToNode = insertToNode 116 | 117 | 118 | local function insertToTree(root, window) 119 | if root.isAvailable then 120 | root.windowID = window:id() 121 | root.isAvailable = false 122 | else 123 | if root.windowID then 124 | if root.isDividedHorizontal then 125 | splitHorizontal(root, window) 126 | else 127 | splitVertical(root, window) 128 | end 129 | else 130 | local h1 = heightOfNode(root.left) 131 | local h2 = heightOfNode(root.right) 132 | 133 | if h1 > h2 then 134 | insertToTree(root.right, window) 135 | else 136 | insertToTree(root.left, window) 137 | end 138 | end 139 | end 140 | end 141 | 142 | pkg.insertToTree = insertToTree 143 | 144 | local function travelTree(root, dic) 145 | if root == nil or dic == nil then 146 | return 147 | else 148 | if root.windowID then 149 | dic[root.windowID] = 1 150 | else 151 | travelTree(root.left, dic) 152 | travelTree(root.right, dic) 153 | end 154 | end 155 | end 156 | 157 | pkg.travelTree = travelTree 158 | 159 | local function travelTreeAndTiling(root, dic, pad) 160 | if root == nil then 161 | return 162 | else 163 | root.frame = tools.grid(root.frame) 164 | if root.windowID then 165 | local f = root.frame 166 | local ff = tools.cloneFrame(root.frame) 167 | ff.x = ff.x + pad 168 | ff.y = ff.y + pad 169 | ff.w = ff.w - 2*pad 170 | ff.h = ff.h - 2*pad 171 | dic[root.windowID] = ff 172 | else 173 | travelTreeAndTiling(root.left, dic, pad) 174 | travelTreeAndTiling(root.right, dic, pad) 175 | end 176 | end 177 | end 178 | 179 | pkg.travelTreeAndTiling = travelTreeAndTiling 180 | 181 | 182 | local function findFatherOfNode(root, ID) 183 | if root == nil then 184 | return nil 185 | end 186 | 187 | if root.left ~= nil then 188 | if root.left.windowID == ID then 189 | return root 190 | end 191 | end 192 | 193 | if root.right ~= nil then 194 | if root.right.windowID == ID then 195 | return root 196 | end 197 | end 198 | 199 | local rl = findFatherOfNode(root.left, ID) 200 | if rl ~= nil then 201 | return rl 202 | end 203 | 204 | rl = findFatherOfNode(root.right, ID) 205 | if rl ~= nil then 206 | return rl 207 | end 208 | 209 | return nil 210 | end 211 | 212 | pkg.findFatherOfNode = findFatherOfNode 213 | 214 | local function divideFrameHorizontal(frame) 215 | local rl = {} 216 | 217 | rl[1] = hs.geometry(frame.x, frame.y, frame.w, frame.h *0.7) 218 | rl[2] = hs.geometry(frame.x, frame.y + frame.h* 0.7, frame.w, frame.h * 0.3) 219 | 220 | return rl 221 | end 222 | 223 | local function divideFrameVerical(frame) 224 | local rl = {} 225 | 226 | rl[1] = hs.geometry(frame.x, frame.y, frame.w *0.7, frame.h) 227 | rl[2] = hs.geometry(frame.x + frame.w *0.7, frame.y, frame.w * 0.3, frame.h) 228 | 229 | return rl 230 | end 231 | 232 | local function retilingNodeWithFrame(node, frame) 233 | if node == nil then 234 | return 235 | end 236 | 237 | node.frame = tools.cloneFrame(frame) 238 | if not node.windowID then 239 | local rl = 1 240 | if node.isDividedHorizontal then 241 | rl = divideFrameHorizontal(frame) 242 | else 243 | rl = divideFrameVerical(frame) 244 | end 245 | 246 | retilingNodeWithFrame(node.left, rl[1]) 247 | retilingNodeWithFrame(node.right, rl[2]) 248 | end 249 | end 250 | 251 | local function fatherOfNode(root, node) 252 | if root == nil then 253 | return nil 254 | end 255 | 256 | if root.left == node then 257 | return root 258 | end 259 | 260 | if root.right == node then 261 | return root 262 | end 263 | 264 | local rl = fatherOfNode(root.left, node) 265 | if rl ~= nil then 266 | return rl 267 | end 268 | 269 | rl = fatherOfNode(root.right, node) 270 | if rl ~= nil then 271 | return rl 272 | end 273 | 274 | return nil 275 | end 276 | 277 | 278 | pkg.fatherOfNode = fatherOfNode 279 | 280 | pkg.retilingNodeWithFrame = retilingNodeWithFrame 281 | 282 | pkg.deleteWindowFromTree = function(root, windowID, global_padding) 283 | if root == nil then 284 | return 285 | end 286 | if root.windowID == windowID then 287 | root = initTreeforWorkSpace(global_padding) 288 | else 289 | local father = findFatherOfNode(root, windowID) 290 | if father == nil then 291 | return 292 | end 293 | 294 | local frame = tools.cloneFrame(father.frame) 295 | 296 | if father.left.windowID == windowID then 297 | father.right.isDividedHorizontal = father.isDividedHorizontal 298 | retilingNodeWithFrame(father.right, frame) 299 | copyNode(father.right, father) 300 | else 301 | father.left.isDividedHorizontal = father.isDividedHorizontal 302 | retilingNodeWithFrame(father.left, frame) 303 | copyNode(father.left, father) 304 | end 305 | end 306 | end 307 | 308 | local function isInSideFrame(frame, point) 309 | if frame == nil or point == nil then 310 | return false 311 | end 312 | 313 | local dx = point.x - frame.x 314 | local dy = point.y - frame.y 315 | 316 | if dx > 0 and dx < frame.w and dy > 0 and dy < frame.h then 317 | return true 318 | else 319 | return false 320 | end 321 | 322 | return false 323 | end 324 | 325 | local function findNodewithPointer(root, point) 326 | if root == nil then 327 | return nil 328 | end 329 | 330 | if not isInSideFrame(root.frame, point) then 331 | return nil 332 | end 333 | 334 | if root.windowID then 335 | return root 336 | else 337 | local rl = findNodewithPointer(root.left, point) 338 | if rl ~= nil then 339 | return rl 340 | end 341 | 342 | rl = findNodewithPointer(root.right, point) 343 | if rl ~= nil then 344 | return rl 345 | end 346 | end 347 | 348 | return nil 349 | end 350 | 351 | pkg.findNodewithPointer = findNodewithPointer 352 | 353 | local function resizeNode(root, node, dx, dy) 354 | 355 | 356 | if root == nil or node == nil then 357 | return 358 | end 359 | 360 | local father = node.father 361 | 362 | if father == nil then 363 | return 364 | end 365 | 366 | local grandpa = father.father 367 | 368 | if grandpa ~= nil and grandpa.isDividedHorizontal ~= father.isDividedHorizontal then 369 | 370 | local f1 = tools.cloneFrame(grandpa.left.frame) 371 | local f2 = tools.cloneFrame(grandpa.right.frame) 372 | 373 | 374 | if not grandpa.isDividedHorizontal then 375 | f1.w = f1.w + dx 376 | f2.w = grandpa.frame.w - f1.w 377 | 378 | if f2.w < 50 then 379 | f2.w = 50 380 | f1.w = grandpa.frame.w - f2.w 381 | end 382 | 383 | f2.x = f1.x + f1.w 384 | else 385 | f1.h = f1.h + dy 386 | f2.h = grandpa.frame.h - f1.h 387 | 388 | if f2.h < 50 then 389 | f2.h = 50 390 | f1.h = grandpa.frame.h - f2.h 391 | end 392 | 393 | f2.y = f1.y + f1.h 394 | end 395 | 396 | grandpa.left.border_ = tools.cloneFrame(f1) 397 | grandpa.right.border_ = tools.cloneFrame(f2) 398 | 399 | border.rescaleBorder(grandpa.left, root.canvas) 400 | border.rescaleBorder(grandpa.right, root.canvas) 401 | end 402 | 403 | 404 | local f1 = tools.cloneFrame(father.left.frame) 405 | local f2 = tools.cloneFrame(father.right.frame) 406 | 407 | if grandpa ~= nil and grandpa.isDividedHorizontal ~= father.isDividedHorizontal then 408 | f1 = tools.cloneFrame(father.left.border_) 409 | f2 = tools.cloneFrame(father.right.border_) 410 | end 411 | 412 | if not father.isDividedHorizontal then 413 | f1.w = f1.w + dx 414 | f2.w = father.frame.w - f1.w 415 | if f2.w < 100 then 416 | f2.w = 100 417 | f1.w = father.frame.w - f2.w 418 | end 419 | f2.x = f1.x + f1.w 420 | else 421 | f1.h = f1.h + dy 422 | f2.h = father.frame.h - f1.h 423 | if f2.h < 100 then 424 | f2.h = 100 425 | f1.h = father.frame.h - f2.h 426 | end 427 | f2.y = f1.y + f1.h 428 | end 429 | 430 | 431 | father.left.border_ = tools.cloneFrame(f1) 432 | father.right.border_ = tools.cloneFrame(f2) 433 | 434 | border.rescaleBorder(father.left, root.canvas) 435 | border.rescaleBorder(father.right, root.canvas) 436 | 437 | end 438 | 439 | pkg.resizeNode = resizeNode 440 | 441 | 442 | local function deleteZombies(root, winmap) 443 | 444 | if root == nil then 445 | return 446 | end 447 | 448 | if root.left == nil and root.right == nil and not root.windowID then 449 | root.windowID = -1 450 | end 451 | 452 | if root.left == nil and root.right == nil then 453 | return 454 | end 455 | 456 | if root.left ~= nil then 457 | if root.left.windowID and (not winmap[root.left.windowID]) then 458 | pkg.deleteWindowFromTree(root, root.left.windowID) 459 | else 460 | deleteZombies(root.left, winmap) 461 | end 462 | else 463 | retilingNodeWithFrame(root.right, root.frame) 464 | if root.right ~= nil then 465 | root.left = root.right.left 466 | root.isDividedHorizontal = root.right.isDividedHorizontal 467 | root.isAvailable = root.right.isAvailable 468 | root.right = root.right.right 469 | end 470 | end 471 | 472 | if root.right ~= nil then 473 | if root.right.windowID and (not winmap[root.right.windowID]) then 474 | pkg.deleteWindowFromTree(root, root.right.windowID) 475 | else 476 | deleteZombies(root.right, winmap) 477 | end 478 | else 479 | retilingNodeWithFrame(root.left, root.frame) 480 | if root.left ~= nil then 481 | root.right = root.left.right 482 | root.isDividedHorizontal = root.left.isDividedHorizontal 483 | root.isAvailable = root.left.isAvailable 484 | root.left = root.left.left 485 | end 486 | end 487 | end 488 | 489 | 490 | 491 | 492 | pkg.deleteZombies = deleteZombies 493 | 494 | local function findWindow(root) 495 | if root == nil then 496 | return nil 497 | end 498 | 499 | if root.windowID ~= nil and root.windowID ~= -1 then 500 | return root.windowID 501 | end 502 | 503 | if root.windowID == -1 then 504 | return nil 505 | end 506 | 507 | 508 | if root.left.windowID ~= nil then 509 | return root.left.windowID 510 | end 511 | 512 | if root.right.windowID ~= nil then 513 | return root.right.windowID 514 | end 515 | 516 | local rl = findWindow(root.left) 517 | if rl ~= nil then 518 | return rl 519 | end 520 | 521 | rl = findWindow(root.right) 522 | return rl 523 | end 524 | 525 | local function getNextToNode(root, node, direction) 526 | if root == node then 527 | return nil 528 | end 529 | 530 | local father = node.father 531 | 532 | if father == nil then 533 | return 534 | end 535 | if father.isDividedHorizontal then 536 | if direction == "up" and father.right == node then 537 | return findWindow(father.left) 538 | end 539 | 540 | if direction == "down" and father.left == node then 541 | return findWindow(father.right) 542 | end 543 | 544 | end 545 | 546 | if not father.isDividedHorizontal then 547 | if direction == "left" and father.right == node then 548 | return findWindow(father.left) 549 | end 550 | 551 | if direction == "right" and father.left == node then 552 | return findWindow(father.right) 553 | end 554 | 555 | end 556 | 557 | return getNextToNode(root, father, direction) 558 | end 559 | pkg.getNextToNode = getNextToNode 560 | 561 | local function getNodeFromWindowID(root, id) 562 | if root == nil then 563 | return nil 564 | end 565 | 566 | if root.windowID == id then 567 | return root 568 | end 569 | 570 | local rl = getNodeFromWindowID(root.left, id) 571 | if rl ~= nil then 572 | return rl 573 | end 574 | 575 | return getNodeFromWindowID(root.right, id) 576 | end 577 | 578 | pkg.getNodeFromWindowID = getNodeFromWindowID 579 | 580 | local function swap(father) 581 | local left_frame = nil 582 | local right_frame = nil 583 | 584 | if father == nil then 585 | return 586 | end 587 | 588 | if father.left == nil or father.right == nil then 589 | return 590 | end 591 | 592 | left_frame = tools.cloneFrame(father.left.frame) 593 | right_frame = tools.cloneFrame(father.right.frame) 594 | 595 | retilingNodeWithFrame(father.right, left_frame) 596 | retilingNodeWithFrame(father.left, right_frame) 597 | 598 | local tmp = father.left 599 | father.left = father.right 600 | father.right = tmp 601 | 602 | local t = father.left.border 603 | father.left.border = father.right.border 604 | father.right.border = t 605 | 606 | local tt = father.left.border_ 607 | father.left.border_ = father.right.border_ 608 | father.right.border_ = tt 609 | end 610 | 611 | local function swap_hv(root) 612 | 613 | local fw = hs.window.focusedWindow() 614 | 615 | local focusedWindowID = nil 616 | 617 | if fw ~= nil then 618 | focusedWindowID = fw:id() 619 | else 620 | return 621 | end 622 | 623 | if focusedWindowID == nil then 624 | return 625 | end 626 | 627 | local father = findFatherOfNode(root, focusedWindowID) 628 | 629 | if father == nil then 630 | return 631 | end 632 | 633 | father.isDividedHorizontal = not father.isDividedHorizontal 634 | retilingNodeWithFrame(father, tools.cloneFrame(father.frame)) 635 | end 636 | pkg.swap_hv = swap_hv 637 | 638 | local function swap_x(root) 639 | local fw = hs.window.focusedWindow() 640 | 641 | local focusedWindowID = nil 642 | 643 | if fw ~= nil then 644 | focusedWindowID = fw:id() 645 | else 646 | return 647 | end 648 | 649 | if focusedWindowID == nil then 650 | return 651 | end 652 | 653 | local father = findFatherOfNode(root, focusedWindowID) 654 | 655 | if father == nil then 656 | return 657 | end 658 | 659 | if father.isDividedHorizontal == true then 660 | swap(father) 661 | else 662 | local grandpa = fatherOfNode(root, father) 663 | if grandpa == nil then 664 | return 665 | end 666 | if grandpa.isDividedHorizontal == true then 667 | swap(grandpa) 668 | end 669 | end 670 | end 671 | pkg.swap_x = swap_x 672 | 673 | local function swap_y(root) 674 | 675 | local fw = hs.window.focusedWindow() 676 | 677 | local focusedWindowID = nil 678 | 679 | if fw ~= nil then 680 | focusedWindowID = fw:id() 681 | else 682 | return 683 | end 684 | 685 | if focusedWindowID == nil then 686 | return 687 | end 688 | 689 | local father = findFatherOfNode(root, focusedWindowID) 690 | if father == nil then 691 | return 692 | end 693 | 694 | if father.isDividedHorizontal == false then 695 | swap(father) 696 | else 697 | local grandpa = fatherOfNode(root, father) 698 | if grandpa == nil then 699 | return 700 | end 701 | if grandpa.isDividedHorizontal == false then 702 | swap(grandpa) 703 | end 704 | end 705 | end 706 | 707 | pkg.swap_y = swap_y 708 | 709 | pkg.init = function(GLOBAL) 710 | pkg.GLOBAL = GLOBAL 711 | end 712 | 713 | 714 | return pkg 715 | -------------------------------------------------------------------------------- /window.lua: -------------------------------------------------------------------------------- 1 | local pkg = {} 2 | 3 | local border = dofile( os.getenv("HOME") .. "/.hammerspoon/border.lua") 4 | local tools = dofile( os.getenv("HOME") .. "/.hammerspoon/tools.lua") 5 | local tree = dofile( os.getenv("HOME") .. "/.hammerspoon/tree.lua") 6 | 7 | pkg.GLOBAL = {} 8 | 9 | local events = hs.uielement.watcher 10 | 11 | local watchers = {} 12 | local bdw = {} 13 | local workspace = {} 14 | 15 | local current_space = {} 16 | 17 | local mouse_loc = nil 18 | local cur_node = nil 19 | local new_node = nil 20 | 21 | 22 | local disableClick = false 23 | local isSwap = false 24 | local isResize = false 25 | 26 | local spaces = require("hs._asm.undocumented.spaces") 27 | 28 | local floatingWindows = {} 29 | 30 | local function space_manager(spaceid) 31 | spaceid = spaces.activeSpace() 32 | local global_padding = pkg.GLOBAL.global_padding 33 | 34 | if workspace[spaceid] == nil then 35 | workspace[spaceid] = {} 36 | end 37 | 38 | current_space = workspace[spaceid] 39 | 40 | if current_space.root == nil then 41 | current_space.root = tree.initTreeforWorkSpace(global_padding) 42 | end 43 | 44 | pkg.GLOBAL.root = nil 45 | pkg.GLOBAL.root = current_space.root 46 | if previous_border ~= nil then 47 | previous_border:hide() 48 | end 49 | 50 | hs.timer.doAfter(0, pkg.window_manager) 51 | 52 | return false 53 | end 54 | 55 | pkg.space_manager = space_manager 56 | 57 | local function window_manager() 58 | 59 | local global_padding = pkg.GLOBAL.global_padding 60 | local window_padding = pkg.GLOBAL.window_padding 61 | local root = pkg.GLOBAL.root 62 | 63 | if root == nil then 64 | pkg.space_manager() 65 | return 66 | end 67 | 68 | 69 | local function compare(a,b) 70 | if a == nil or b == nil then 71 | return false 72 | end 73 | 74 | if a:id() == nil or b:id() == nil then 75 | return false 76 | end 77 | 78 | return a:id() < b:id() 79 | end 80 | 81 | local ws = hs.window.visibleWindows() 82 | 83 | table.sort(ws, compare) 84 | 85 | local mmm = {} 86 | tree.travelTree(root, mmm) 87 | 88 | local father = tree.findFatherOfNode(root, focusedWindowID) 89 | 90 | local mm = {} 91 | 92 | for i = 1, #ws do 93 | local w = ws[i] 94 | if w:application():name() ~= "Hammerspoon" and w:isStandard() and floatingWindows[w:id()] == nil then 95 | if not mmm[w:id()] then 96 | if father == nil then 97 | tree.insertToTree(root, w) 98 | else 99 | if father.left.windowID == focusedWindowID then 100 | tree.insertToNode(father.left, w) 101 | else 102 | tree.insertToNode(father.right, w) 103 | end 104 | 105 | if father.left == nil then 106 | end 107 | end 108 | end 109 | mm[w:id()] = ws[i] 110 | end 111 | end 112 | 113 | 114 | tree.deleteZombies(root, mm) 115 | tree.deleteWindowFromTree(root, -1, global_padding) 116 | 117 | 118 | local dic = {} 119 | tree.travelTreeAndTiling(root, dic, window_padding) 120 | 121 | local screen = hs.screen.mainScreen():name() 122 | 123 | for i = 1, #ws do 124 | local w = ws[i] 125 | if dic[w:id()] then 126 | local f = function() 127 | w:setFrame(dic[w:id()]) 128 | end 129 | f() 130 | end 131 | end 132 | 133 | 134 | local fw = hs.window.focusedWindow() 135 | if fw ~= nil then 136 | focusedWindowID = fw:id() 137 | else 138 | return 139 | end 140 | 141 | local frame = fw:frame() 142 | if bdw[fw:id()] == nil then 143 | bdw[fw:id()] = border.init_border() 144 | end 145 | 146 | bdw[fw:id()]:setFrame(frame) 147 | 148 | if previous_border ~= nil and previous_border ~= bdw[fw:id()] then 149 | previous_border:hide() 150 | end 151 | 152 | if previous_border ~= bdw[fw:id()] then 153 | if fw:isStandard() and fw:application():name() ~= "Hammerspoon" and not fw:isFullScreen() then 154 | bdw[fw:id()]:show() 155 | end 156 | end 157 | 158 | previous_border = bdw[fw:id()] 159 | 160 | end 161 | 162 | pkg.window_manager = window_manager 163 | 164 | local function watchWindow(win, initializing) 165 | if win == nil then 166 | return 167 | end 168 | 169 | pcall(function() 170 | local appWindows = watchers[win:application():pid()].windows 171 | if win:isStandard() and not appWindows[win:id()] and win:application():name() ~= "Hammerspoon" then 172 | local watcher = win:newWatcher(pkg.handleWindowEvent, {pid=win:pid(), id=win:id()}) 173 | appWindows[win:id()] = watcher 174 | 175 | watcher:start({events.elementDestroyed}) 176 | 177 | if not initializing then 178 | -- 179 | end 180 | end 181 | end) 182 | end 183 | 184 | pkg.watchWindow = watchWindow 185 | 186 | local function watchApp(app, initializing) 187 | if watchers[app:pid()] then return end 188 | 189 | local watcher = app:newWatcher(pkg.handleAppEvent) 190 | watchers[app:pid()] = {watcher = watcher, windows = {}} 191 | 192 | watcher:start({events.windowCreated, events.focusedWindowChanged, events.mainWindowChanged, events.titleChanged}) 193 | 194 | -- Watch any window that already exist 195 | for i, window in pairs(app:allWindows()) do 196 | if window:id() ~= null then 197 | bdw[window:id()] = border.init_border() 198 | bdw[window:id()]:hide() 199 | 200 | watchWindow(window, initializing) 201 | end 202 | end 203 | end 204 | 205 | pkg.watchApp = watchApp 206 | 207 | 208 | local function handleGlobalAppEvent(name, event, app) 209 | if event == hs.application.watcher.launched then 210 | watchApp(app) 211 | elseif event == hs.application.watcher.terminated then 212 | -- Clean up 213 | local appWatcher = watchers[app:pid()] 214 | if appWatcher then 215 | appWatcher.watcher:stop() 216 | 217 | for id, watcher in pairs(appWatcher.windows) do 218 | watcher:stop() 219 | end 220 | watchers[app:pid()] = nil 221 | end 222 | end 223 | hs.timer.doAfter(0, window_manager) 224 | return false 225 | end 226 | 227 | pkg.handleGlobalAppEvent = handleGlobalAppEvent 228 | 229 | local function handleWindowEvent(win, event, watcher, info) 230 | if event == events.elementDestroyed then 231 | watcher:stop() 232 | watchers[info.pid].windows[info.id] = nil 233 | if bdw[win:id()] ~= nil then 234 | bdw[win:id()]:delete() 235 | bdw[win:id()] = nil 236 | end 237 | 238 | if floatingWindows[win:id()] ~= nil then 239 | floatingWindows[win:id()] = nil 240 | end 241 | 242 | tree.deleteWindowFromTree(pkg.GLOBAL.root, win:id(), pkg.GLOBAL.global_padding) 243 | if pkg.GLOBAL.root == nil then 244 | space_manager() 245 | end 246 | end 247 | 248 | hs.timer.doAfter(0, window_manager) 249 | 250 | return false 251 | end 252 | 253 | pkg.handleWindowEvent = handleWindowEvent 254 | 255 | 256 | local function handleAppEvent(element, event) 257 | if event == events.windowCreated then 258 | watchWindow(element) 259 | elseif event == events.focusedWindowChanged then 260 | -- Handle window change 261 | end 262 | 263 | hs.timer.doAfter(0, window_manager) 264 | return false 265 | end 266 | 267 | pkg.handleAppEvent = handleAppEvent 268 | 269 | 270 | 271 | local windowResizeAndSwap = function(ev) 272 | local root = pkg.GLOBAL.root 273 | local result = ev:getFlags().ctrl 274 | 275 | if root == nil then 276 | return 277 | end 278 | 279 | disableClick = false 280 | 281 | if ev:getType() == hs.eventtap.event.types.leftMouseUp or 282 | ev:getType() == hs.eventtap.event.types.rightMouseUp then 283 | if root.canvas ~= nil then 284 | root.canvas:delete() 285 | root.canvas = nil 286 | end 287 | 288 | if isSwap then 289 | isSwap = false 290 | 291 | if cur_node ~= nil and new_node ~= nil and cur_node ~= new_node then 292 | local t = cur_node.windowID 293 | cur_node.windowID = new_node.windowID 294 | new_node.windowID = t 295 | hs.timer.doAfter(0, window_manager) 296 | end 297 | 298 | cur_node = nil 299 | new_node = nil 300 | end 301 | 302 | if isResize then 303 | isResize = false 304 | border.convertFromBorderToFrame(root) 305 | hs.timer.doAfter(0, window_manager) 306 | cur_node = nil 307 | new_node = nil 308 | end 309 | 310 | return 311 | end 312 | 313 | if result then 314 | disableClick = true 315 | end 316 | 317 | if disableClick then 318 | if ev:getType() == hs.eventtap.event.types.rightMouseDown then 319 | 320 | if root.canvas == nil then 321 | root.canvas = hs.canvas.new(border.cloneBorder(hs.screen.mainScreen():frame())) 322 | root.canvas:wantsLayer(true) 323 | root.canvas:show() 324 | end 325 | 326 | disableClick = false 327 | isResize = true 328 | mouse_loc = hs.mouse.getAbsolutePosition() 329 | cur_node = tree.findNodewithPointer(root, mouse_loc) 330 | 331 | for i, appWatcher in pairs(watchers) do 332 | if appWatcher then 333 | for id, watcher in pairs(appWatcher.windows) do 334 | watcher:stop() 335 | end 336 | end 337 | end 338 | 339 | return true 340 | end 341 | end 342 | 343 | if disableClick then 344 | if ev:getType() == hs.eventtap.event.types.leftMouseDown then 345 | 346 | disableClick = false 347 | 348 | isSwap = true 349 | mouse_loc = hs.mouse.getAbsolutePosition() 350 | cur_node = tree.findNodewithPointer(root, mouse_loc) 351 | 352 | if root.canvas == nil then 353 | root.canvas = hs.canvas.new(border.cloneBorder(hs.screen.mainScreen():frame())) 354 | root.canvas:wantsLayer(true) 355 | root.canvas:show() 356 | end 357 | 358 | if cur_node ~= nil then 359 | root.canvas[1] = border.create_canvas_border(cur_node.frame) 360 | else 361 | root.canvas[1] = nil 362 | end 363 | 364 | return true 365 | end 366 | end 367 | 368 | if isResize then 369 | if ev:getType() == hs.eventtap.event.types.rightMouseDragged then 370 | 371 | local ml = hs.mouse.getAbsolutePosition() 372 | local dx = ml.x - mouse_loc.x 373 | local dy = ml.y - mouse_loc.y 374 | tree.resizeNode(root, cur_node, dx, dy) 375 | 376 | hs.canvas.disableScreenUpdates() 377 | local t = #(root.canvas) 378 | 379 | for i = 1, t do 380 | root.canvas[t - i + 1] = nil 381 | end 382 | 383 | border.travelAndAppendToCanvas(root, root.canvas) 384 | hs.canvas.enableScreenUpdates() 385 | 386 | return false 387 | end 388 | end 389 | 390 | if isSwap then 391 | if ev:getType() == hs.eventtap.event.types.leftMouseDragged then 392 | local ml = hs.mouse.getAbsolutePosition() 393 | local temp_new_node = new_node 394 | 395 | new_node = tree.findNodewithPointer(root, ml) 396 | 397 | if root.canvas == nil then 398 | root.canvas = hs.canvas.new(border.cloneBorder(hs.screen.mainScreen():frame())) 399 | root.canvas:wantsLayer(true) 400 | root.canvas:show() 401 | end 402 | 403 | if cur_node ~= nil then 404 | root.canvas[1] = border.create_canvas_border(cur_node.frame) 405 | end 406 | 407 | if new_node ~= nil then 408 | root.canvas[2] = border.create_canvas_border(new_node.frame) 409 | else 410 | root.canvas[#(root.canvas)] = nil 411 | end 412 | 413 | return false 414 | end 415 | end 416 | 417 | 418 | 419 | return false 420 | end 421 | 422 | -- 423 | -- verify that *only* the ctrl key flag is being pressed 424 | local function onlyShiftCmd(ev) 425 | 426 | hs.timer.doAfter(0, function() 427 | windowResizeAndSwap(ev) 428 | end) 429 | 430 | if disableClick then 431 | if ev:getType() == hs.eventtap.event.types.leftMouseDown or ev:getType() == hs.eventtap.event.types.rightMouseDown then 432 | return true 433 | end 434 | end 435 | 436 | return false 437 | end 438 | 439 | local function onlyShiftCmd3(ev) 440 | 441 | local dC = disableClick 442 | 443 | windowResizeAndSwap(ev) 444 | 445 | if dC then 446 | if ev:getType() == hs.eventtap.event.types.leftMouseDown or ev:getType() == hs.eventtap.event.types.rightMouseDown then 447 | return true 448 | end 449 | end 450 | return false 451 | end 452 | 453 | 454 | local ttt = hs.eventtap.event.types 455 | 456 | pkg.resizeWatcher1 = hs.eventtap.new({ttt.rightMouseUp}, onlyShiftCmd3) 457 | pkg.resizeWatcher1:start() 458 | 459 | pkg.resizeWatcher4 = hs.eventtap.new({ttt.leftMouseUp, ttt.leftMouseDown}, onlyShiftCmd3) 460 | pkg.resizeWatcher4:start() 461 | 462 | pkg.resizeWatcher3 = hs.eventtap.new({ttt.rightMouseDragged}, onlyShiftCmd) 463 | pkg.resizeWatcher3:start() 464 | 465 | pkg.resizeWatcher2 = hs.eventtap.new({ttt.flagsChanged, ttt.keyUp, ttt.keyDown, ttt.rightMouseDown, ttt.leftMouseDragged}, onlyShiftCmd) 466 | pkg.resizeWatcher2:start() 467 | 468 | 469 | pkg.spaceWatcher = hs.spaces.watcher.new(pkg.space_manager) 470 | pkg.spaceWatcher:start() 471 | 472 | 473 | local function init(GLOBAL) 474 | appsWatcher = hs.application.watcher.new(pkg.handleGlobalAppEvent) 475 | appsWatcher:start() 476 | 477 | pkg.GLOBAL = GLOBAL 478 | 479 | -- Watch any apps that already exist 480 | local apps = hs.application.runningApplications() 481 | for i = 1, #apps do 482 | if apps[i]:name() ~= "Hammerspoon" then 483 | print(apps[i]:name()) 484 | watchApp(apps[i], true) 485 | end 486 | end 487 | 488 | pkg.timer = hs.timer.doEvery(10, pkg.window_manager) 489 | end 490 | 491 | pkg.init = init 492 | 493 | local function toggleFloat(windowID) 494 | if floatingWindows[windowID] == nil then 495 | -- something 496 | else 497 | -- 498 | end 499 | end 500 | 501 | pkg.toggleFloat = toggleFloat 502 | 503 | return pkg 504 | --------------------------------------------------------------------------------