├── .gitignore ├── .hammerspoon ├── stdout ├── images │ ├── swap.png │ ├── warp.png │ ├── split.png │ ├── stack.png │ └── stripe.png ├── images.lua ├── yabai.lua ├── windowAction.lua ├── init 2.lua ├── init.lua ├── initbépo.lua └── initqwerty.lua ├── .yabairc └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.hammerspoon/stdout: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.hammerspoon/images/swap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtauziac/Hammerspoon-Yabai/HEAD/.hammerspoon/images/swap.png -------------------------------------------------------------------------------- /.hammerspoon/images/warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtauziac/Hammerspoon-Yabai/HEAD/.hammerspoon/images/warp.png -------------------------------------------------------------------------------- /.hammerspoon/images/split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtauziac/Hammerspoon-Yabai/HEAD/.hammerspoon/images/split.png -------------------------------------------------------------------------------- /.hammerspoon/images/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtauziac/Hammerspoon-Yabai/HEAD/.hammerspoon/images/stack.png -------------------------------------------------------------------------------- /.hammerspoon/images/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rtauziac/Hammerspoon-Yabai/HEAD/.hammerspoon/images/stripe.png -------------------------------------------------------------------------------- /.hammerspoon/images.lua: -------------------------------------------------------------------------------- 1 | return { 2 | swap = hs.image.imageFromPath("images/swap.png"), 3 | stack = hs.image.imageFromPath("images/stack.png"), 4 | split = hs.image.imageFromPath("images/split.png"), 5 | warp = hs.image.imageFromPath("images/warp.png"), 6 | } -------------------------------------------------------------------------------- /.hammerspoon/yabai.lua: -------------------------------------------------------------------------------- 1 | return function(args, completion) 2 | local yabai_output = "" 3 | local yabai_error = "" 4 | -- Runs in background very fast 5 | local yabai_task = hs.task.new("/usr/local/bin/yabai", function(err, stdout, stderr) 6 | print() 7 | end, function(task, stdout, stderr) 8 | -- print("stdout:"..stdout, "stderr:"..stderr) 9 | if stdout ~= nil then 10 | yabai_output = yabai_output .. stdout 11 | end 12 | if stderr ~= nil then 13 | yabai_error = yabai_error .. stderr 14 | end 15 | return true 16 | end, args) 17 | if type(completion) == "function" then 18 | yabai_task:setCallback(function() 19 | completion(yabai_output, yabai_error) 20 | end) 21 | end 22 | yabai_task:start() 23 | end 24 | -------------------------------------------------------------------------------- /.yabairc: -------------------------------------------------------------------------------- 1 | 2 | yabai -m config debug_output on 3 | 4 | # config 5 | yabai -m config window_placement first_child 6 | yabai -m config mouse_follows_focus on 7 | yabai -m config layout bsp 8 | yabai -m config window_gap 20 9 | yabai -m config top_padding 20 10 | yabai -m config bottom_padding 20 11 | yabai -m config left_padding 20 12 | yabai -m config right_padding 20 13 | 14 | # rules 15 | yabai -m rule --add app="(^Préférences Système$)" manage=off 16 | yabai -m rule --add app="(^Android Studio$)" title="Tip of the Day" manage=off 17 | yabai -m rule --add app="(^Android Studio$)" title="Generate Signed Bundle or APK" manage=off 18 | yabai -m rule --add app="(^Finder$)" title="(^Copier$)" manage=off 19 | yabai -m rule --add app="(^Mozilla VPN$)" title="(^Mozilla VPN$)" manage=off 20 | yabai -m rule --add app="(^JetBrains Toolbox$)" title="(^JetBrains Toolbox$)" manage=off 21 | yabai -m rule --add app="(^Firefox$)" title="(^Ouverture de)" manage=off 22 | 23 | # signals 24 | yabai -m signal --add event=window_focused action="hs -c \"yabaidirectcall.window_focused(\`printenv YABAI_WINDOW_ID\`)\"" 25 | yabai -m signal --add event=application_activated action="hs -c \"yabaidirectcall.application_activated(\`printenv YABAI_PROCESS_ID\`)\"" 26 | yabai -m signal --add event=window_resized action="hs -c \"yabaidirectcall.window_resized(\`printenv YABAI_WINDOW_ID\`)\"" 27 | yabai -m signal --add event=window_moved action="hs -c \"yabaidirectcall.window_moved(\`printenv YABAI_WINDOW_ID\`)\"" 28 | yabai -m signal --add event=window_destroyed action="hs -c \"yabaidirectcall.window_destroyed(\`printenv YABAI_WINDOW_ID\`)\"" 29 | -------------------------------------------------------------------------------- /.hammerspoon/windowAction.lua: -------------------------------------------------------------------------------- 1 | local c = require("hs.canvas") 2 | local json = require("hs.json") 3 | local yabai = require("yabai") 4 | 5 | local windowAction = { 6 | new = function(modKeys, key, action, image) 7 | local a = { 8 | action = action, 9 | selected = nil, 10 | icon = image 11 | } 12 | a.modal = hs.hotkey.modal.new(modKeys, key) 13 | function a.modal:entered() 14 | yabai({"-m", "query", "--windows", "--window"}, function(out) 15 | local window = json.decode(out) 16 | if window ~= nil then 17 | a.selected = window 18 | a:showOverlay() 19 | else 20 | a.modal:exit() 21 | end 22 | end) 23 | end 24 | function a.modal:exited() 25 | a.selected = nil 26 | a:hideOverlay() 27 | end 28 | a.modal:bind(modKeys, key, function() 29 | yabai({"-m", "window", "--" .. a.action, tostring(a.selected.id)}, function() 30 | yabai({"-m", "window", "--focus", tostring(a.selected.id)}, function() 31 | a.modal:exit() 32 | end) 33 | end) 34 | end) 35 | a.modal:bind(nil, hs.keycodes.map["escape"], function() 36 | a.modal:exit() 37 | end) 38 | a.canvas = c.new({ 39 | x = 0, 40 | y = 0, 41 | w = 100, 42 | h = 100 43 | }):replaceElements({ 44 | type = "image", 45 | image = a.icon, 46 | imageScaling = "none" 47 | }, { 48 | type = "rectangle", 49 | action = "fill", 50 | fillColor = { 51 | white = 1, 52 | alpha = 0.66 53 | }, 54 | roundedRectRadii = { 55 | xRadius = windowCornerRadius, 56 | yRadius = windowCornerRadius 57 | }, 58 | compositeRule = "plusDarker" 59 | }) 60 | function a:showOverlay() 61 | self.canvas:topLeft({ 62 | x = self.selected.frame.x, 63 | y = self.selected.frame.y 64 | }):size({ 65 | w = self.selected.frame.w, 66 | h = self.selected.frame.h 67 | }):show() 68 | end 69 | function a:hideOverlay() 70 | self.canvas:hide() 71 | end 72 | return a 73 | end 74 | } 75 | return windowAction -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Hammerspoon × Yabai config 2 | 3 | ## Index 4 | 5 | - Why 6 | - Contents 7 | - `.yabairc` 8 | - `.hammerspoon/init.lua` 9 | - `.hammerspoon/windowAction.lua` 10 | - How to use it 11 | - Directions and actions 12 | - Space layout 13 | - Insert rule 14 | - Move mouse to display 15 | - Move window to display 16 | - Resize mode 17 | - Debug 18 | - Windows actions 19 | 20 | My personal workspace configuration I use at work. 21 | 22 | ## Why? 23 | 24 | I wanted to use tiled window manager on MacOS, I found Yabai as a great candidate for this: 25 | 26 | - CLI commands only _(no cluttered GUI)_ 27 | - Lightweigth 28 | - Installation with `brew` _(no Appstore)_ 29 | 30 | I installed Yabai _without disabling SIP_. Much features were not so useful and I didn’t want to risk exposing my work computer to some flaws _(including me!)_. 31 | 32 | I already have Hammerspoon installed and was pretty happy with it so I decided to use it for hotkey configurations: 33 | 34 | - Fully customisable with lua 👍 35 | - You can draw stuff on screen 36 | - You can do a lot more stuff with, event window management! 37 | 38 | Note this is a very early setting and I’m still configuring this day to day. I’m seeing this much like a evolving tool I shape for my current needs. 39 | 40 | Also, **big notice**, this is optimised for full keyboard use with **BÉPO**, a french first keyboard layout. 41 | 42 | I added the corresponding **QWERTY** keys commented in front of the line so you can figure the actual layout. I also made the `.initqwerty.lua` file with the replaced hotkeys for ease. 43 | 44 | ## Contents 45 | 46 | ### `.yabairc` 47 | 48 | Startup file for Yabai, it configures global settings, adds some rules for certain windows and adds signals to communicate with hammerspoon. 49 | 50 | ### `.hammerspoon/init.lua` 51 | 52 | My hammerspoon main config file. Originaly it was a huge file but I started fragmenting it in several pieces to make it more digest to read. I tried to segment its content in different categories: 53 | 54 | - `require`, constrants, images 55 | - actions from my `windowAction` class 56 | - helpers 57 | - yabai command 58 | - toast 59 | - global chooser 60 | - bindings 61 | - general purpose 62 | - define space layout 63 | - window actions _(rotate, fullscreen-focus, toggle float)_ 64 | - focus change 65 | - window ratio 66 | - modals 67 | - focus display 68 | - window insert rule 69 | - change window screen 70 | - resize window 71 | - debug 72 | - callbacks 73 | - respond to window events _(focus, resize, move)_ 74 | - draw window borders 75 | - yabai queries 76 | - yabai ipc 77 | 78 | All of my shortcuts are triggered with my super key, wich is binded to `⌃⌥` _(control + alt)_. You can change it at the top level of my file in the global variable `super`. 79 | 80 | ### `.hammerspoon/windowAction.lua` 81 | 82 | This is some nice reusable code I made for windows actions. There’s a dedicated section below that explains the use of them. 83 | 84 | ## How to use it? 85 | 86 | I’m glad you ask. Here is how I use it. For simplicity I’ll refer to the `QWERTY` layout keys. 87 | 88 | Most of the actions applies to the currently focused window/the space the focused window is. 89 | 90 | Some alerts will briefly show with the actions you made, and will persistently show for the modes you enter in. They are just emojis that shows centered as a visual feedback. 91 | 92 | Modes are triggered with `hs.modals`, which means you enter them as long as you enter a modal and leaves them when you exit a modal. You can use `escape` to exit modes. 93 | 94 | ### Directions and actions 95 | 96 | `j` `k` `l` `;` keys corresponds to the four directions ⬅️ ⬇️ ⬆️ ➡️. 97 | You can use them in combination with many other modes. 98 | 99 | For example if you use them on their own, `super` + `j` changes the focus to the left _(focus change works only with Yabai’s BSP mode)_. 100 | 101 | `t` `g` are for browsing stacks up and down. 102 | 103 | `y` `h` `n` stands for **swap**, **stack** and **warp**. They’ll be used later as both modes and actions. 104 | 105 | ### Space layout 106 | 107 | You can switch to **BSP**, **Stacked** and **Float** layout for the current space with `super` + `1`, `2` or `3` 108 | 109 | ### Insert rule 110 | 111 | You can decide which window portion to split for a new window to appear or if you want to stack it. 112 | Use `super` + `tab` to enter **Insert** mode, then use `super` + a direction or `h` to set the rule. Yabai will color the window portion in red. 113 | If you press `super` + `tab` again, you enter resize mode. 114 | 115 | ### Move mouse to display 116 | 117 | Use `super` + `v` to enter the mode and right or left direction to put your mouse to the next or previous display. 118 | 119 | ### Move window to display 120 | 121 | Use `super` + `b` to enter the mode and right or left direction to send your window to the next or previous display. 122 | 123 | ### Resize mode 124 | 125 | When you are in Insert rule mode, press `super` + `tab` to enter this mode. You can move edges of windows with the directions. This works only in Yabai’s BSP mode for the sides that aren’t on the edge of a screen. 126 | 127 | You first use a direction to select horizontal/vertical edge, then all other directions will move it by 20px. 128 | 129 | ### Debug 130 | 131 | This is just for testing purpose. The keystoke `super` + `§` will print the current window details in the console. 132 | 133 | ### Windows actions 134 | 135 | Those are actions that applies to two windows, like swaping, stacking and warping. You first launch the action to select the first window to apply the action, then change focus to the second window, and finally you call that action again to execute the windows action. To quickly explain with an example: 136 | 137 | > To swap two windows, you press `super` + `y` to engage swapping, then move your focus to another window with the directions, then you hit `super` + `y` again to effectively swap those two windows. 138 | -------------------------------------------------------------------------------- /.hammerspoon/init 2.lua: -------------------------------------------------------------------------------- 1 | -- Test 2 | --[[ hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() 3 | hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send() 4 | end) ]] 5 | 6 | local previousAppNameAlert = nil 7 | local focusHistory = {} 8 | local historySize = 8 9 | local justTiled2W = false -- used to track if we toggle 10 | local canvas = require("hs.canvas") 11 | 12 | local screenWidth = hs.screen.mainScreen():fullFrame().w 13 | local screenHeight = hs.screen.mainScreen():fullFrame().h 14 | 15 | local mainScreenCanvas = canvas.new({x=0, y=0, w=screenWidth, h=screenHeight}) 16 | 17 | local lineElement = { 18 | action = "fill", type = "rectangle", fillColor = { red=1.0, green=1.0, blue=1.0, alpha=0.5 }, frame = {x=0, y=23, w="100%", h=10}, padding = 0 19 | } 20 | mainScreenCanvas:appendElements(lineElement) 21 | 22 | -- mainScreenCanvas:show() 23 | 24 | hs.window.animationDuration = 0 25 | 26 | --//////////////// 27 | --//// TILING //// 28 | --//////////////// 29 | -- //// LEFT 30 | -- hs.hotkey.bind({"ctrl", "alt"}, "a", function() 31 | -- local currentWindow = hs.window.focusedWindow() 32 | -- local currentApp = currentWindow:application() 33 | -- tileLeft(currentWindow:title()) 34 | -- end) 35 | -- //// MAXIMIZED 36 | -- hs.hotkey.bind({"ctrl", "alt"}, "é", function() 37 | -- local currentWindow = hs.window.focusedWindow() 38 | -- local currentApp = currentWindow:application() 39 | -- maximize(currentWindow:title()) 40 | -- end) 41 | -- //// RIGHT 42 | -- hs.hotkey.bind({"ctrl", "alt"}, "i", function() 43 | -- local currentWindow = hs.window.focusedWindow() 44 | -- local currentApp = currentWindow:application() 45 | -- tileRight(currentWindow:title()) 46 | -- end) 47 | -- //// TILE THE LAST 2 WINDOWS 48 | -- hs.hotkey.bind({"ctrl", "alt"}, "u", function() 49 | -- local windowR = focusHistory[#focusHistory] 50 | -- local windowL = focusHistory[#focusHistory-1] 51 | -- if justTiled2W then 52 | -- windowR = focusHistory[#focusHistory-1] 53 | -- windowL = focusHistory[#focusHistory] 54 | -- justTiled2W = false 55 | -- else 56 | -- justTiled2W = true 57 | -- end 58 | 59 | -- if windowL and windowR and windowL ~= windowR then 60 | -- tileLeft(windowL:title()) 61 | -- tileRight(windowR:title()) 62 | -- end 63 | -- end) 64 | 65 | -- //// GLOBAL MENU 66 | -- hs.hotkey.bind({"ctrl", "alt", "shift"}, "return", function() 67 | -- local ch = hs.chooser.new(function(option) 68 | -- if option then 69 | -- if option.alert then 70 | -- hs.alert(option.alert) 71 | -- end 72 | 73 | -- if option.cmd == "reset" then 74 | -- hs.reload() 75 | -- end 76 | -- end 77 | -- end) 78 | -- ch:bgDark(false) 79 | -- ch:choices({ 80 | -- --[[ {text="Okay", subText="Choose this if you agree", alert="Great!"}, ]] 81 | -- --[[ {text="Nope", subText="Choose this if you don't agree", alert="Whatever…"}, ]] 82 | -- {text="Restart Hammerspoon", subText="Reload the new settings", cmd="reset"} 83 | -- }) 84 | -- ch:show() 85 | -- end) 86 | 87 | -- //// FOCUS WATCHER 88 | local windowFilter = hs.window.filter.new() 89 | windowFilter:subscribe(hs.window.filter.windowFocused, function(window, appName, event) 90 | -- alert 91 | if previousAppNameAlert then 92 | hs.alert.closeSpecific(previousAppNameAlert) 93 | end 94 | previousAppNameAlert = hs.alert.show(appName, hs.alert.defaultStyle, hs.screen.mainScreen(), 0.9) 95 | 96 | -- store window focus history 97 | -- focusHistory[#focusHistory+1] = window 98 | -- if #focusHistory >= 8 then 99 | -- table.remove(focusHistory, 1) 100 | -- end 101 | 102 | --[[ for i, v in ipairs(focusHistory) do 103 | hs.printf(v:title()) 104 | hs.printf(v:application():name()) 105 | end 106 | hs.printf("========") ]] 107 | 108 | -- justTiled2W = false 109 | end) 110 | 111 | -- -- //// APP SWITCHER 112 | -- hs.hotkey.bind({"ctrl", "shift"}, "return", function() 113 | -- local ch = hs.chooser.new(function(option) 114 | -- if option then 115 | -- if option.alert then 116 | -- hs.alert.show(option.alert) 117 | -- end 118 | 119 | -- if option.cmd == "reset" then 120 | -- hs.reload() 121 | -- elseif option.cmd == "switch" then 122 | -- option.focus() 123 | -- end 124 | -- end 125 | -- end) 126 | -- ch:bgDark(true) 127 | -- local allWindows = hs.window.filter:getWindows() 128 | -- print(tostring(allWindows)) 129 | -- setmetatable(allWindows, {__index=function(the_table, the_key) 130 | -- if the_key == "title" then 131 | -- return the_table.title 132 | -- elseif the_key == "cmd" then 133 | -- return "switch" 134 | -- end 135 | -- end}) 136 | -- --ch:choices({ 137 | -- --[[ {text="Okay", subText="Choose this if you agree", alert="Great!"}, ]] 138 | -- --[[ {text="Nope", subText="Choose this if you don't agree", alert="Whatever…"}, ]] 139 | -- --{text="Restart Hammerspoon", subText="Reload the new settings", cmd="reset"} 140 | -- --}) 141 | -- ch:choices(allWindows) 142 | -- ch:show() 143 | -- end) 144 | 145 | -- //// APP PICKER 146 | -- Indicator 147 | local lineIndicator = hs.drawing.line(hs.geometry.point(0, 20), hs.geometry.point(screenWidth, 20)) 148 | lineIndicator:setStroke(true) 149 | lineIndicator:setStrokeWidth(10) 150 | 151 | 152 | --a = canvas.new{ x = 100, y = 100, h = 100, w = 100 }:show() 153 | --a:insertElement({ type = "rectangle", id = "part1", fillColor = { blue = 1 } }) 154 | --a:insertElement({ type = "circle", id = "part2", fillColor = { green = 1 } }) 155 | 156 | local globalPickerModal = hs.hotkey.modal.new({"ctrl", "alt"}, "\"") 157 | local codePickerModal = hs.hotkey.modal.new({"ctrl", "alt"}, "»") 158 | local webPickerModal = hs.hotkey.modal.new({"ctrl", "alt"}, "(") 159 | 160 | -- Indicator 161 | function globalPickerModal:entered() 162 | -- old drawing 163 | lineIndicator:setStrokeColor({ 164 | red=1, 165 | green=0.75, 166 | blue=0.6, 167 | alpha=1 168 | }) 169 | codePickerModal:exit() 170 | webPickerModal:exit() 171 | lineIndicator:show() 172 | 173 | -- new canvas 174 | lineElement.fillColor = {red=1.0, green=0.89, blue=0.3, alpha=0.5} 175 | mainScreenCanvas:assignElement(lineElement, 1) 176 | mainScreenCanvas:show() 177 | end 178 | function globalPickerModal:exited() 179 | lineIndicator:hide() -- old api 180 | mainScreenCanvas:hide() -- new api 181 | end 182 | 183 | globalPickerModal:bind(nil, "escape", function() 184 | globalPickerModal:exit() 185 | end) 186 | globalPickerModal:bind(nil, "\"", function() 187 | focusApplication("Finder") 188 | globalPickerModal:exit() 189 | end) 190 | globalPickerModal:bind(nil, "«", function() 191 | focusApplication("Skype") 192 | globalPickerModal:exit() 193 | end) 194 | globalPickerModal:bind(nil, "»", function() 195 | focusApplication("Aperçu") 196 | globalPickerModal:exit() 197 | end) 198 | globalPickerModal:bind(nil, ")", function() 199 | focusApplication("KeePassXC") 200 | globalPickerModal:exit() 201 | end) 202 | 203 | -- Indicator 204 | function codePickerModal:entered() 205 | -- old drawing 206 | lineIndicator:setStrokeColor({ 207 | red=0.75, 208 | green=0.89, 209 | blue=1, 210 | alpha=1 211 | }) 212 | globalPickerModal:exit() 213 | webPickerModal:exit() 214 | lineIndicator:show() 215 | 216 | -- new canvas 217 | lineElement.fillColor = {red=0.75, green=0.89, blue=1.0, alpha=0.5} 218 | mainScreenCanvas:assignElement(lineElement, 1) 219 | mainScreenCanvas:show() 220 | end 221 | function codePickerModal:exited() 222 | lineIndicator:hide() -- old api 223 | mainScreenCanvas:hide() -- new api 224 | end 225 | 226 | codePickerModal:bind(nil, "escape", function() 227 | codePickerModal:exit() 228 | end) 229 | codePickerModal:bind(nil, "»", function() 230 | focusApplication("Xcode") 231 | codePickerModal:exit() 232 | end) 233 | codePickerModal:bind(nil, "«", function() 234 | focusApplication("Simulator") 235 | codePickerModal:exit() 236 | end) 237 | codePickerModal:bind(nil, "(", function() 238 | focusApplication("Fork") 239 | codePickerModal:exit() 240 | end) 241 | codePickerModal:bind(nil, ")", function() 242 | focusApplication("Code") 243 | codePickerModal:exit() 244 | end) 245 | codePickerModal:bind(nil, "\"", function() 246 | focusApplication("Unity") 247 | codePickerModal:exit() 248 | end) 249 | 250 | function webPickerModal:entered() 251 | -- old drawing 252 | lineIndicator:setStrokeColor({ 253 | red=1, 254 | green=0.5, 255 | blue=0.45, 256 | alpha=1 257 | }) 258 | globalPickerModal:exit() 259 | codePickerModal:exit() 260 | lineIndicator:show() 261 | 262 | -- new canvas 263 | lineElement.fillColor = {red=1.0, green=0.5, blue=0.45, alpha=0.5} 264 | mainScreenCanvas:assignElement(lineElement, 1) 265 | mainScreenCanvas:show() 266 | end 267 | function webPickerModal:exited() 268 | lineIndicator:hide() -- old api 269 | mainScreenCanvas:hide() -- new api 270 | end 271 | 272 | webPickerModal:bind(nil, "escape", function() 273 | webPickerModal:exit() 274 | end) 275 | webPickerModal:bind(nil, "(", function() 276 | focusApplication("Firefox") 277 | webPickerModal:exit() 278 | end) 279 | webPickerModal:bind(nil, "»", function() 280 | focusApplication("Google Chrome") 281 | webPickerModal:exit() 282 | end) 283 | 284 | --[[ k = hs.hotkey.modal.new('cmd-shift', 'd') 285 | function k:entered() 286 | hs.alert'Entered mode' 287 | end 288 | function k:exited() 289 | hs.alert'Exited mode' 290 | end 291 | k:bind('', 'escape', function() 292 | k:exit() 293 | end) 294 | k:bind('', 'J', 'Pressed J', function() 295 | print'let the record show that J was pressed' 296 | end) ]] 297 | 298 | -- //// QUICK SWITCHER 299 | 300 | -- hs.hotkey.bind({"ctrl", "alt"}, "space", function() 301 | -- if #focusHistory >= 2 then 302 | -- local back = 1 303 | -- while focusHistory[#focusHistory-back]:focus() == nil and back < historySize - 1 do 304 | -- back = back + 1 305 | -- end 306 | -- end 307 | -- end) 308 | 309 | -- //// HELPERS 310 | 311 | local windowTileWidthViewport = 0.4 312 | 313 | function tileRight(windowTitle) 314 | local screenFrame = hs.screen.mainScreen():frame() 315 | screenFrame.x = screenFrame.x + screenFrame.w * windowTileWidthViewport 316 | screenFrame.w = (screenFrame.w * (1 - windowTileWidthViewport)) 317 | hs.layout.apply({ 318 | { 319 | nil, 320 | windowTitle, 321 | hs.screen.mainScreen(), 322 | nil, 323 | screenFrame, 324 | nil 325 | } 326 | }) 327 | end 328 | 329 | function tileLeft(windowTitle) 330 | local screenFrame = hs.screen.mainScreen():frame() 331 | screenFrame.w = screenFrame.w * windowTileWidthViewport - 1 332 | hs.layout.apply({ 333 | { 334 | nil, 335 | windowTitle, 336 | hs.screen.mainScreen(), 337 | nil, 338 | screenFrame, 339 | nil 340 | } 341 | }) 342 | end 343 | 344 | function maximize(windowTitle) 345 | hs.layout.apply({ 346 | { 347 | nil, 348 | windowTitle, 349 | hs.screen.mainScreen(), 350 | hs.layout.maximized, 351 | nil, 352 | nil 353 | } 354 | }) 355 | end 356 | 357 | function focusApplication(name) 358 | local finder = hs.application.get(name) 359 | if finder then 360 | finder:activate() 361 | local fw = hs.window.focusedWindow() 362 | --[[ if fw == nil then 363 | finder:allWindows()[1]:focus() 364 | end ]] 365 | if fw and fw:isVisible() ~= true then 366 | fw:raise() 367 | end 368 | --[[ hs.printf(tostring(fw:isMinimized())) ]] 369 | --[[ if fw:isMinimized() then 370 | fw:unminimize() 371 | end ]] 372 | else 373 | local launchAlert = hs.alert("Launching "..name.."…") 374 | -- hs.alert.closeSpecific(launchAlert, 2) 375 | hs.application.open(name) 376 | end 377 | end 378 | 379 | --hs.notify.new({title="Hammerspoon", informativeText="Config file loaded"}):send() 380 | -------------------------------------------------------------------------------- /.hammerspoon/init.lua: -------------------------------------------------------------------------------- 1 | require("hs.ipc") 2 | 3 | 4 | --# constants 5 | super = "⌃⌥" 6 | empty_table = {} 7 | windowCornerRadius = 10 8 | 9 | 10 | --# images 11 | local images = require("images") 12 | 13 | 14 | local windowAction = require("windowAction") 15 | windowAction.new(super, hs.keycodes.map["^"], "swap", images.swap) --["y"] 16 | windowAction.new(super, hs.keycodes.map["’"], "warp", images.warp) --["n"] 17 | windowAction.new(super, hs.keycodes.map["c"], "stack", images.stack) --["h"] 18 | 19 | 20 | --# canvas elements 21 | local canvases = { 22 | winFocusRect = hs.canvas.new({ x = 0, y = 0, w = 100, h = 100 }), 23 | } 24 | 25 | 26 | local focus_ = { 27 | --hideTimer = nil 28 | } 29 | 30 | 31 | --# helpers 32 | function yabai(args, completion) 33 | local yabai_output = "" 34 | local yabai_error = "" 35 | -- Runs in background very fast 36 | local yabai_task = hs.task.new("/usr/local/bin/yabai",nil, function(task, stdout, stderr) 37 | --print("stdout:"..stdout, "stderr:"..stderr) 38 | if stdout ~= nil then yabai_output = yabai_output..stdout end 39 | if stderr ~= nil then yabai_error = yabai_error..stderr end 40 | return true 41 | end, args) 42 | if type(completion) == "function" then 43 | yabai_task:setCallback(function() completion(yabai_output, yabai_error) end) 44 | end 45 | yabai_task:start() 46 | end 47 | 48 | 49 | function delayed(fn, delay) 50 | return hs.timer.delayed.new(delay, fn):start() 51 | end 52 | 53 | 54 | toasts = { 55 | main = nil 56 | } 57 | function killToast(params) 58 | params = params or empty_table 59 | local name = params.name or "main" 60 | if toasts[name] ~= nil then 61 | hs.alert.closeSpecific(toasts[name], params.fadeOutDuration or 0.1) 62 | toasts[name] = nil 63 | end 64 | end 65 | function toast(str, time, params) 66 | killToast(params) 67 | params = params or empty_table 68 | local name = params.name or "main" 69 | --local toast = toasts[name] 70 | toasts[name] = hs.alert(str, { 71 | fillColor = { white = 0, alpha = 0.4 }, 72 | strokeColor = { white = 0, alpha = 0 }, 73 | strokeWidth = 0, 74 | textColor = { white = 1, alpha = 1 }, 75 | radius = 0, 76 | padding = 6, 77 | atScreenEdge = 0, 78 | fadeInDuration = 0.1, 79 | fadeOutDuration = params.fadeOutDuration or 0.1 80 | }, time or 0.6) 81 | end 82 | 83 | 84 | --# Main chooser 85 | local mainChooser = hs.chooser.new(function(option) 86 | if option ~= nil then 87 | if option.action == "reload" then 88 | hs.reload() 89 | elseif option.action == "toggle_gap" then 90 | yabai({"-m", "space", "--toggle", "padding"}, function() yabai({"-m", "space", "--toggle", "gap"}) end) 91 | end 92 | end 93 | end):choices({ 94 | { 95 | text = "Toggle Gap", 96 | subText = "Toggles padding and gaps around the current space", 97 | action = "toggle_gap" 98 | }, 99 | { 100 | text = "Reload", 101 | subText = "Reload Hammerspoon configuration", 102 | action = "reload" 103 | }, 104 | }) 105 | 106 | 107 | --# bindings 108 | 109 | --# reload config 110 | hs.hotkey.bind(super, hs.keycodes.map["return"], nil, function() hs.reload() end) 111 | --# open main chooser 112 | hs.hotkey.bind(super, hs.keycodes.map["space"], nil, function() mainChooser:show() end) 113 | 114 | 115 | --# set layout under mouse 116 | hs.hotkey.bind(super, hs.keycodes.map["\""], function() yabai({"-m", "space", "mouse", "--layout", "bsp"}, function() toast("🖖") end) end) --["1"] 117 | hs.hotkey.bind(super, hs.keycodes.map["«"], function() yabai({"-m", "space", "mouse", "--layout", "stack"}, function() toast("📚") end) end) --["2"] 118 | hs.hotkey.bind(super, hs.keycodes.map["»"], function() yabai({"-m", "space", "mouse", "--layout", "float"}, function() toast("☁️") end) end) --["3"] 119 | 120 | 121 | --# rotate space 122 | hs.hotkey.bind(super, hs.keycodes.map["h"], function() yabai({"-m", "space", "--rotate", "270"}, function() toast("🔲🔁") end) end) --["."] 123 | 124 | 125 | --# focus fullscreen 126 | hs.hotkey.bind(super, hs.keycodes.map["q"], function() yabai({"-m", "window", "--toggle", "zoom-fullscreen"}) end) --["m"] 127 | --hs.hotkey.bind(super, hs.keycodes.map["g"], function() yabai({"-m", "window", "--toggle", "zoom-parent"}) end) -- not so useful 128 | 129 | 130 | --# toggle float layout for window 131 | hs.hotkey.bind(super, hs.keycodes.map["f"], function() yabai({"-m", "window", "--toggle", "float"}) toast("🎚☁️") end) --["/"] 132 | 133 | 134 | --# change window stack focus 135 | hs.hotkey.bind(super, hs.keycodes.map["è"], function() yabai({"-m", "window", "--focus", "stack.next"}, function() toast("📚↥") end) end) --["t"] 136 | hs.hotkey.bind(super, hs.keycodes.map[","], function() yabai({"-m", "window", "--focus", "stack.prev"}, function() toast("📚↧") end) end) --["g"] 137 | 138 | 139 | --# change window focus to direction 140 | hs.hotkey.bind(super, hs.keycodes.map["n"], function() yabai({"-m", "window", "--focus", "east"}) end) --[";"] 141 | hs.hotkey.bind(super, hs.keycodes.map["t"], function() yabai({"-m", "window", "--focus", "west"}) end) --["j"] 142 | hs.hotkey.bind(super, hs.keycodes.map["r"], function() yabai({"-m", "window", "--focus", "north"}) end) --["l"] 143 | hs.hotkey.bind(super, hs.keycodes.map["s"], function() yabai({"-m", "window", "--focus", "south"}) end) --["k"] 144 | 145 | 146 | --# bsp ratio 147 | hs.hotkey.bind(super, hs.keycodes.map["+"], function() yabai({"-m", "window", "--ratio", "abs:0.38"}) toast("🔲⅓") end) --["7"] 148 | hs.hotkey.bind(super, hs.keycodes.map["-"], function() yabai({"-m", "window", "--ratio", "abs:0.5"}) toast("🔲½") end) --["8"] 149 | hs.hotkey.bind(super, hs.keycodes.map["/"], function() yabai({"-m", "window", "--ratio", "abs:0.62"}) toast("🔲⅔") end) --["9"] 150 | hs.hotkey.bind(super, hs.keycodes.map["="], function() yabai({"-m", "space", "--balance"}) toast("🔲⚖️") end) --["-"] 151 | 152 | 153 | --# modals 154 | 155 | local focus_display_mod = hs.hotkey.modal.new(super, hs.keycodes.map["."]) --["v"] 156 | local insert_window_modal = hs.hotkey.modal.new(super, hs.keycodes.map["tab"]) 157 | local move_display_modal = hs.hotkey.modal.new(super, hs.keycodes.map["k"]) --["b"] 158 | local resize_window_modal = hs.hotkey.modal.new() 159 | 160 | --# focus display 161 | function focus_display_mod:entered() 162 | toast("🖥🧭", true, { name = "modal" }) 163 | end 164 | function focus_display_mod:exited() 165 | killToast({ name = "modal" }) 166 | end 167 | focus_display_mod:bind("", hs.keycodes.map["escape"], function() focus_display_mod:exit() end) 168 | focus_display_mod:bind(super, hs.keycodes.map["n"], function() yabai({"-m", "display", "--focus", "next"}, function() delayed(function() toast("🖥➡️") end, 0.1) end) focus_display_mod:exit() end) --[";"] 169 | focus_display_mod:bind(super, hs.keycodes.map["t"], function() yabai({"-m", "display", "--focus", "prev"}, function() delayed(function() toast("🖥⬅️") end, 0.1) end) focus_display_mod:exit() end) --["j"] 170 | 171 | 172 | --# insert window rule 173 | --# insert window rule functions 174 | function insert_window_modal:entered() 175 | toast("🔲🌱 ", true, { name = "modal" }) 176 | end 177 | function insert_window_modal:exited() 178 | killToast({ name = "modal" }) 179 | end 180 | insert_window_modal:bind("", hs.keycodes.map["escape"], function() insert_window_modal:exit() end) 181 | insert_window_modal:bind(super, hs.keycodes.map["n"], function() yabai({"-m", "window", "--insert", "east"}) end) --[";"] 182 | insert_window_modal:bind(super, hs.keycodes.map["t"], function() yabai({"-m", "window", "--insert", "west"}) end) --["j"] 183 | insert_window_modal:bind(super, hs.keycodes.map["r"], function() yabai({"-m", "window", "--insert", "north"}) end) --["l"] 184 | insert_window_modal:bind(super, hs.keycodes.map["s"], function() yabai({"-m", "window", "--insert", "south"}) end) --["k"] 185 | insert_window_modal:bind(super, hs.keycodes.map["c"], function() yabai({"-m", "window", "--insert", "stack"}) end) --["h"] 186 | insert_window_modal:bind(super, hs.keycodes.map["tab"], function() insert_window_modal:exit() resize_window_modal:enter() end) 187 | 188 | 189 | --# send window to display 190 | local move_display_ = { 191 | selected = nil 192 | } 193 | function move_display_modal:entered() 194 | yabai({"-m", "query", "--windows", "--window"}, 195 | function(out) 196 | local window = hs.json.decode(out) 197 | if (window ~= nil) then 198 | --print(hs.inspect(hs.json.decode(out))) 199 | move_display_.selected = window 200 | toast("🔲🖥", true, { name = "move_display" }) 201 | end 202 | end 203 | ) 204 | end 205 | function move_display_modal:exited() 206 | move_display_.selected = nil 207 | killToast({ name = "move_display" }) 208 | end 209 | move_display_modal:bind(super, hs.keycodes.map["n"], --[";"] 210 | function() 211 | if (move_display_.selected ~= nil) then 212 | yabai({"-m", "window", "--display", "next"}, 213 | function() 214 | move_display_modal:exit() 215 | end 216 | ) 217 | end 218 | end 219 | ) 220 | move_display_modal:bind(super, hs.keycodes.map["t"], --["j"] 221 | function() 222 | if (move_display_.selected ~= nil) then 223 | yabai({"-m", "window", "--display", "prev"}, 224 | function() 225 | move_display_modal:exit() 226 | end 227 | ) 228 | end 229 | end 230 | ) 231 | move_display_modal:bind("", hs.keycodes.map["escape"], function() move_display_modal:exit() end) 232 | 233 | 234 | --# resize window 235 | local resize_window = { 236 | size = 40, 237 | horizontalEdge = nil, -- 1 is for right, -1 is for left 238 | verticalEdge = nil -- 1 is for bottom, -1 is for top 239 | } 240 | function resize_window_modal:entered() 241 | toast("🔲↔️", true, { name = "resize_window" }) 242 | end 243 | function resize_window_modal:exited() 244 | resize_window.horizontalEdge = nil 245 | resize_window.verticalEdge = nil 246 | killToast({ name = "resize_window" }) 247 | end 248 | resize_window_modal:bind(super, hs.keycodes.map["n"], function() --[";"] 249 | if resize_window.horizontalEdge == nil then 250 | resize_window.horizontalEdge = 1 251 | end 252 | if resize_window.horizontalEdge == 1 then 253 | -- grow from right 254 | print("grow from right") 255 | yabai({"-m", "window", "--resize", "right:"..resize_window.size..":0"}, function(out, err) print(out, err) end) 256 | else 257 | -- shrink from left 258 | print("shrink from left") 259 | yabai({"-m", "window", "--resize", "left:"..resize_window.size..":0"}, function(out, err) print(out, err) end) 260 | end 261 | end) 262 | resize_window_modal:bind(super, hs.keycodes.map["t"], function() --["j"] 263 | if resize_window.horizontalEdge == nil then 264 | resize_window.horizontalEdge = -1 265 | end 266 | if resize_window.horizontalEdge == 1 then 267 | -- shrink from right 268 | print("shrink from right") 269 | yabai({"-m", "window", "--resize", "right:-"..resize_window.size..":0"}, function(out, err) print(out, err) end) 270 | else 271 | -- grow from left 272 | print("grow from left") 273 | yabai({"-m", "window", "--resize", "left:-"..resize_window.size..":0"}, function(out, err) print(out, err) end) 274 | end 275 | end) 276 | resize_window_modal:bind(super, hs.keycodes.map["s"], function() --["k"] 277 | if resize_window.verticalEdge == nil then 278 | resize_window.verticalEdge = 1 279 | end 280 | if resize_window.verticalEdge == 1 then 281 | -- grow from bottom 282 | print("grow from bottom") 283 | yabai({"-m", "window", "--resize", "bottom:0:"..resize_window.size}, function(out, err) print(out, err) end) 284 | else 285 | -- shrink from top 286 | print("shrink from top") 287 | yabai({"-m", "window", "--resize", "top:0:"..resize_window.size}, function(out, err) print(out, err) end) 288 | end 289 | end) 290 | resize_window_modal:bind(super, hs.keycodes.map["r"], function() --["l"] 291 | if resize_window.verticalEdge == nil then 292 | resize_window.verticalEdge = -1 293 | end 294 | if resize_window.verticalEdge == 1 then 295 | -- shrink from bottom 296 | print("shrink from bottom") 297 | yabai({"-m", "window", "--resize", "bottom:0:-"..resize_window.size}, function(out, err) print(out, err) end) 298 | else 299 | -- grow from top 300 | print("grow from top") 301 | yabai({"-m", "window", "--resize", "top:0:-"..resize_window.size}, function(out, err) print(out, err) end) 302 | end 303 | end) 304 | resize_window_modal:bind("", hs.keycodes.map["escape"], function() resize_window_modal:exit() end) 305 | 306 | 307 | --# debug 308 | hs.hotkey.bind(super, hs.keycodes.map["$"], function() yabai({"-m", "query", "--windows", "--window"}, function(out) print(out) end) toast("🐞") end) --["§"] 309 | 310 | 311 | --# window focus listener 312 | currentFocus = nil 313 | function onWindowFocusChanged(window_id) 314 | getFocusedWindow(function(win) 315 | if win ~= nil then 316 | if currentFocus == nil or currentFocus.id ~= win.id then 317 | currentFocus = win 318 | --deleteBorder() 319 | --createBorder(win) 320 | end 321 | else 322 | currentFocus = nil 323 | deleteBorder() 324 | end 325 | end) 326 | end 327 | 328 | 329 | function onWindowResized(window_id) 330 | if currentFocus ~= nil and currentFocus.id == window_id then 331 | getWindow(currentFocus.id, 332 | function(win) 333 | --deleteBorder() 334 | --createBorder(win) 335 | end 336 | ) 337 | end 338 | end 339 | 340 | 341 | function onWindowMoved(window_id) 342 | if currentFocus ~= nil and currentFocus.id == window_id then 343 | getWindow(currentFocus.id, 344 | function(win) 345 | --deleteBorder() 346 | --createBorder(win) 347 | end 348 | ) 349 | end 350 | end 351 | 352 | 353 | function onWindowDestroyed(window_id) 354 | if currentFocus ~= nil and currentFocus.id == window_id then 355 | deleteBorder() 356 | end 357 | end 358 | 359 | 360 | function createBorder(win) 361 | if win == nil or canvases.winFocusRect == nil then 362 | return 363 | end 364 | canvases.winFocusRect:topLeft({ x = win.frame.x - 2, y = win.frame.y - 2 }) 365 | canvases.winFocusRect:size({ w = win.frame.w + 4, h = win.frame.h + 4 }) 366 | local borderColor = { red = 0.8, green = 0.8, blue = 0.2 , alpha = 0.6 } 367 | local zoomed = win["zoom-fullscreen"] == 1 368 | if zoomed then 369 | borderColor = { red = 0.8, green = 0.2, blue = 0.2 , alpha = 0.6 } 370 | end 371 | canvases.winFocusRect:replaceElements({ 372 | type = "rectangle", 373 | action = "stroke", 374 | strokeColor = borderColor, 375 | strokeWidth = 4, 376 | --strokeDashPattern = { 60, 40 }, 377 | roundedRectRadii = { xRadius = windowCornerRadius, yRadius = windowCornerRadius }, 378 | padding = 2 379 | }) 380 | canvases.winFocusRect:show() 381 | end 382 | 383 | function deleteBorder(fadeTime) 384 | canvases.winFocusRect:hide() 385 | end 386 | 387 | 388 | --# query 389 | function getFocusedWindow(callback) 390 | yabai({"-m", "query", "--windows"}, 391 | function(out, err) 392 | if out == nil or type(out) ~= "string" or string.len(out) == 0 then 393 | callback(nil) 394 | else 395 | out = string.gsub(out, ":inf,", ":0.0,") 396 | local json = "{\"windows\":"..out.."}" 397 | --print(json) 398 | local json_obj = hs.json.decode(json) 399 | if json_obj ~= nil then 400 | local windows = json_obj.windows 401 | for i, win in ipairs(windows) do 402 | if win.focused == 1 then 403 | callback(win) 404 | return 405 | end 406 | end 407 | callback(nil) 408 | else 409 | getFocusedWindow(callback) 410 | end 411 | end 412 | end 413 | ) 414 | end 415 | 416 | 417 | function getWindow(window_id, callback) 418 | yabai({"-m", "query", "--windows", "--window", tostring(window_id)}, 419 | function(out, err) 420 | if out == nil or string.len(out) == 0 then 421 | callback(nil) 422 | else 423 | --print("json|"..out.."|len"..string.len(out)) 424 | local win = hs.json.decode(out) 425 | callback(win) 426 | end 427 | end 428 | ) 429 | end 430 | 431 | 432 | --# install cli 433 | hs.ipc.cliInstall() 434 | 435 | 436 | -- calls made by yabai frow cli, see .yabairc 437 | yabaidirectcall = { 438 | window_focused = function(window_id) -- called when another window from the current app is focused 439 | onWindowFocusChanged(window_id) 440 | end, 441 | application_activated = function(process_id) -- called when a window from a different app is focused. Doesn’t exclude a window_focused call. 442 | onWindowFocusChanged(window_id) 443 | end, 444 | window_resized = function(window_id) -- called when a window changes dimensions 445 | onWindowResized(window_id) 446 | end, 447 | window_moved = function(window_id) -- called when a window is moved 448 | onWindowMoved(window_id) 449 | end, 450 | window_destroyed = function(window_id) -- called when a window is destroyed 451 | onWindowDestroyed(window_id) 452 | end 453 | } 454 | 455 | 456 | --# start yabai 457 | --os.execute("/usr/local/bin/yabai") 458 | -- so far I start yabai by hand from terminal so I can see logs 459 | 460 | --toast("Hello world", 1) 461 | 462 | onWindowFocusChanged(nil) -- show borders of focused window at startup 463 | -------------------------------------------------------------------------------- /.hammerspoon/initbépo.lua: -------------------------------------------------------------------------------- 1 | require("hs.ipc") 2 | 3 | 4 | --# constants 5 | super = "⌃⌥" 6 | empty_table = {} 7 | windowCornerRadius = 10 8 | 9 | 10 | --# images 11 | local images = require("images") 12 | 13 | 14 | local windowAction = require("windowAction") 15 | windowAction.new(super, hs.keycodes.map["^"], "swap", images.swap) --["y"] 16 | windowAction.new(super, hs.keycodes.map["’"], "warp", images.warp) --["n"] 17 | windowAction.new(super, hs.keycodes.map["c"], "stack", images.stack) --["h"] 18 | 19 | 20 | --# canvas elements 21 | local canvases = { 22 | winFocusRect = hs.canvas.new({ x = 0, y = 0, w = 100, h = 100 }), 23 | } 24 | 25 | 26 | local focus_ = { 27 | --hideTimer = nil 28 | } 29 | 30 | 31 | --# helpers 32 | function yabai(args, completion) 33 | local yabai_output = "" 34 | local yabai_error = "" 35 | -- Runs in background very fast 36 | local yabai_task = hs.task.new("/usr/local/bin/yabai",nil, function(task, stdout, stderr) 37 | --print("stdout:"..stdout, "stderr:"..stderr) 38 | if stdout ~= nil then yabai_output = yabai_output..stdout end 39 | if stderr ~= nil then yabai_error = yabai_error..stderr end 40 | return true 41 | end, args) 42 | if type(completion) == "function" then 43 | yabai_task:setCallback(function() completion(yabai_output, yabai_error) end) 44 | end 45 | yabai_task:start() 46 | end 47 | 48 | 49 | function delayed(fn, delay) 50 | return hs.timer.delayed.new(delay, fn):start() 51 | end 52 | 53 | 54 | toasts = { 55 | main = nil 56 | } 57 | function killToast(params) 58 | params = params or empty_table 59 | local name = params.name or "main" 60 | if toasts[name] ~= nil then 61 | hs.alert.closeSpecific(toasts[name], params.fadeOutDuration or 0.1) 62 | toasts[name] = nil 63 | end 64 | end 65 | function toast(str, time, params) 66 | killToast(params) 67 | params = params or empty_table 68 | local name = params.name or "main" 69 | --local toast = toasts[name] 70 | toasts[name] = hs.alert(str, { 71 | fillColor = { white = 0, alpha = 0.4 }, 72 | strokeColor = { white = 0, alpha = 0 }, 73 | strokeWidth = 0, 74 | textColor = { white = 1, alpha = 1 }, 75 | radius = 0, 76 | padding = 6, 77 | atScreenEdge = 0, 78 | fadeInDuration = 0.1, 79 | fadeOutDuration = params.fadeOutDuration or 0.1 80 | }, time or 0.6) 81 | end 82 | 83 | 84 | --# Main chooser 85 | local mainChooser = hs.chooser.new(function(option) 86 | if option ~= nil then 87 | if option.action == "reload" then 88 | hs.reload() 89 | elseif option.action == "toggle_gap" then 90 | yabai({"-m", "space", "--toggle", "padding"}, function() yabai({"-m", "space", "--toggle", "gap"}) end) 91 | end 92 | end 93 | end):choices({ 94 | { 95 | text = "Toggle Gap", 96 | subText = "Toggles padding and gaps around the current space", 97 | action = "toggle_gap" 98 | }, 99 | { 100 | text = "Reload", 101 | subText = "Reload Hammerspoon configuration", 102 | action = "reload" 103 | }, 104 | }) 105 | 106 | 107 | --# bindings 108 | 109 | --# reload config 110 | hs.hotkey.bind(super, hs.keycodes.map["return"], nil, function() hs.reload() end) 111 | --# open main chooser 112 | hs.hotkey.bind(super, hs.keycodes.map["space"], nil, function() mainChooser:show() end) 113 | 114 | 115 | --# set layout under mouse 116 | hs.hotkey.bind(super, hs.keycodes.map["\""], function() yabai({"-m", "space", "mouse", "--layout", "bsp"}, function() toast("🖖") end) end) --["1"] 117 | hs.hotkey.bind(super, hs.keycodes.map["«"], function() yabai({"-m", "space", "mouse", "--layout", "stack"}, function() toast("📚") end) end) --["2"] 118 | hs.hotkey.bind(super, hs.keycodes.map["»"], function() yabai({"-m", "space", "mouse", "--layout", "float"}, function() toast("☁️") end) end) --["3"] 119 | 120 | 121 | --# rotate space 122 | hs.hotkey.bind(super, hs.keycodes.map["h"], function() yabai({"-m", "space", "--rotate", "270"}, function() toast("🔲🔁") end) end) --["."] 123 | 124 | 125 | --# focus fullscreen 126 | hs.hotkey.bind(super, hs.keycodes.map["q"], function() yabai({"-m", "window", "--toggle", "zoom-fullscreen"}) end) --["m"] 127 | --hs.hotkey.bind(super, hs.keycodes.map["g"], function() yabai({"-m", "window", "--toggle", "zoom-parent"}) end) -- not so useful 128 | 129 | 130 | --# toggle float layout for window 131 | hs.hotkey.bind(super, hs.keycodes.map["f"], function() yabai({"-m", "window", "--toggle", "float"}) toast("🎚☁️") end) --["/"] 132 | 133 | 134 | --# change window stack focus 135 | hs.hotkey.bind(super, hs.keycodes.map["è"], function() yabai({"-m", "window", "--focus", "stack.next"}, function() toast("📚↥") end) end) --["t"] 136 | hs.hotkey.bind(super, hs.keycodes.map[","], function() yabai({"-m", "window", "--focus", "stack.prev"}, function() toast("📚↧") end) end) --["g"] 137 | 138 | 139 | --# change window focus to direction 140 | hs.hotkey.bind(super, hs.keycodes.map["n"], function() yabai({"-m", "window", "--focus", "east"}) end) --[";"] 141 | hs.hotkey.bind(super, hs.keycodes.map["t"], function() yabai({"-m", "window", "--focus", "west"}) end) --["j"] 142 | hs.hotkey.bind(super, hs.keycodes.map["r"], function() yabai({"-m", "window", "--focus", "north"}) end) --["l"] 143 | hs.hotkey.bind(super, hs.keycodes.map["s"], function() yabai({"-m", "window", "--focus", "south"}) end) --["k"] 144 | 145 | 146 | --# bsp ratio 147 | hs.hotkey.bind(super, hs.keycodes.map["+"], function() yabai({"-m", "window", "--ratio", "abs:0.38"}) toast("🔲⅓") end) --["7"] 148 | hs.hotkey.bind(super, hs.keycodes.map["-"], function() yabai({"-m", "window", "--ratio", "abs:0.5"}) toast("🔲½") end) --["8"] 149 | hs.hotkey.bind(super, hs.keycodes.map["/"], function() yabai({"-m", "window", "--ratio", "abs:0.62"}) toast("🔲⅔") end) --["9"] 150 | hs.hotkey.bind(super, hs.keycodes.map["="], function() yabai({"-m", "space", "--balance"}) toast("🔲⚖️") end) --["-"] 151 | 152 | 153 | --# modals 154 | 155 | local focus_display_mod = hs.hotkey.modal.new(super, hs.keycodes.map["."]) --["v"] 156 | local insert_window_modal = hs.hotkey.modal.new(super, hs.keycodes.map["tab"]) 157 | local move_display_modal = hs.hotkey.modal.new(super, hs.keycodes.map["k"]) --["b"] 158 | local resize_window_modal = hs.hotkey.modal.new() 159 | 160 | --# focus display 161 | function focus_display_mod:entered() 162 | toast("🖥🧭", true, { name = "modal" }) 163 | end 164 | function focus_display_mod:exited() 165 | killToast({ name = "modal" }) 166 | end 167 | focus_display_mod:bind("", hs.keycodes.map["escape"], function() focus_display_mod:exit() end) 168 | focus_display_mod:bind(super, hs.keycodes.map["n"], function() yabai({"-m", "display", "--focus", "next"}, function() delayed(function() toast("🖥➡️") end, 0.1) end) focus_display_mod:exit() end) --[";"] 169 | focus_display_mod:bind(super, hs.keycodes.map["t"], function() yabai({"-m", "display", "--focus", "prev"}, function() delayed(function() toast("🖥⬅️") end, 0.1) end) focus_display_mod:exit() end) --["j"] 170 | 171 | 172 | --# insert window rule 173 | --# insert window rule functions 174 | function insert_window_modal:entered() 175 | toast("🔲🌱 ", true, { name = "modal" }) 176 | end 177 | function insert_window_modal:exited() 178 | killToast({ name = "modal" }) 179 | end 180 | insert_window_modal:bind("", hs.keycodes.map["escape"], function() insert_window_modal:exit() end) 181 | insert_window_modal:bind(super, hs.keycodes.map["n"], function() yabai({"-m", "window", "--insert", "east"}) end) --[";"] 182 | insert_window_modal:bind(super, hs.keycodes.map["t"], function() yabai({"-m", "window", "--insert", "west"}) end) --["j"] 183 | insert_window_modal:bind(super, hs.keycodes.map["r"], function() yabai({"-m", "window", "--insert", "north"}) end) --["l"] 184 | insert_window_modal:bind(super, hs.keycodes.map["s"], function() yabai({"-m", "window", "--insert", "south"}) end) --["k"] 185 | insert_window_modal:bind(super, hs.keycodes.map["c"], function() yabai({"-m", "window", "--insert", "stack"}) end) --["h"] 186 | insert_window_modal:bind(super, hs.keycodes.map["tab"], function() insert_window_modal:exit() resize_window_modal:enter() end) 187 | 188 | 189 | --# send window to display 190 | local move_display_ = { 191 | selected = nil 192 | } 193 | function move_display_modal:entered() 194 | yabai({"-m", "query", "--windows", "--window"}, 195 | function(out) 196 | local window = hs.json.decode(out) 197 | if (window ~= nil) then 198 | --print(hs.inspect(hs.json.decode(out))) 199 | move_display_.selected = window 200 | toast("🔲🖥", true, { name = "move_display" }) 201 | end 202 | end 203 | ) 204 | end 205 | function move_display_modal:exited() 206 | move_display_.selected = nil 207 | killToast({ name = "move_display" }) 208 | end 209 | move_display_modal:bind(super, hs.keycodes.map["n"], --[";"] 210 | function() 211 | if (move_display_.selected ~= nil) then 212 | yabai({"-m", "window", "--display", "next"}, 213 | function() 214 | move_display_modal:exit() 215 | end 216 | ) 217 | end 218 | end 219 | ) 220 | move_display_modal:bind(super, hs.keycodes.map["t"], --["j"] 221 | function() 222 | if (move_display_.selected ~= nil) then 223 | yabai({"-m", "window", "--display", "prev"}, 224 | function() 225 | move_display_modal:exit() 226 | end 227 | ) 228 | end 229 | end 230 | ) 231 | move_display_modal:bind("", hs.keycodes.map["escape"], function() move_display_modal:exit() end) 232 | 233 | 234 | --# resize window 235 | local resize_window = { 236 | size = 20, 237 | horizontalEdge = nil, -- 1 is for right, -1 is for left 238 | verticalEdge = nil -- 1 is for bottom, -1 is for top 239 | } 240 | function resize_window_modal:entered() 241 | toast("🔲↔️", true, { name = "resize_window" }) 242 | end 243 | function resize_window_modal:exited() 244 | resize_window.horizontalEdge = nil 245 | resize_window.verticalEdge = nil 246 | killToast({ name = "resize_window" }) 247 | end 248 | resize_window_modal:bind(super, hs.keycodes.map["n"], function() --[";"] 249 | if resize_window.horizontalEdge == nil then 250 | resize_window.horizontalEdge = 1 251 | end 252 | if resize_window.horizontalEdge == 1 then 253 | -- grow from right 254 | print("grow from right") 255 | yabai({"-m", "window", "--resize", "right:"..resize_window.size..":0"}, function(out, err) print(out, err) end) 256 | else 257 | -- shrink from left 258 | print("shrink from left") 259 | yabai({"-m", "window", "--resize", "left:"..resize_window.size..":0"}, function(out, err) print(out, err) end) 260 | end 261 | end) 262 | resize_window_modal:bind(super, hs.keycodes.map["t"], function() --["j"] 263 | if resize_window.horizontalEdge == nil then 264 | resize_window.horizontalEdge = -1 265 | end 266 | if resize_window.horizontalEdge == 1 then 267 | -- shrink from right 268 | print("shrink from right") 269 | yabai({"-m", "window", "--resize", "right:-"..resize_window.size..":0"}, function(out, err) print(out, err) end) 270 | else 271 | -- grow from left 272 | print("grow from left") 273 | yabai({"-m", "window", "--resize", "left:-"..resize_window.size..":0"}, function(out, err) print(out, err) end) 274 | end 275 | end) 276 | resize_window_modal:bind(super, hs.keycodes.map["s"], function() --["k"] 277 | if resize_window.verticalEdge == nil then 278 | resize_window.verticalEdge = 1 279 | end 280 | if resize_window.verticalEdge == 1 then 281 | -- grow from bottom 282 | print("grow from bottom") 283 | yabai({"-m", "window", "--resize", "bottom:0:"..resize_window.size}, function(out, err) print(out, err) end) 284 | else 285 | -- shrink from top 286 | print("shrink from top") 287 | yabai({"-m", "window", "--resize", "top:0:"..resize_window.size}, function(out, err) print(out, err) end) 288 | end 289 | end) 290 | resize_window_modal:bind(super, hs.keycodes.map["r"], function() --["l"] 291 | if resize_window.verticalEdge == nil then 292 | resize_window.verticalEdge = -1 293 | end 294 | if resize_window.verticalEdge == 1 then 295 | -- shrink from bottom 296 | print("shrink from bottom") 297 | yabai({"-m", "window", "--resize", "bottom:0:-"..resize_window.size}, function(out, err) print(out, err) end) 298 | else 299 | -- grow from top 300 | print("grow from top") 301 | yabai({"-m", "window", "--resize", "top:0:-"..resize_window.size}, function(out, err) print(out, err) end) 302 | end 303 | end) 304 | resize_window_modal:bind("", hs.keycodes.map["escape"], function() resize_window_modal:exit() end) 305 | 306 | 307 | --# debug 308 | hs.hotkey.bind(super, hs.keycodes.map["$"], function() yabai({"-m", "query", "--windows", "--window"}, function(out) print(out) end) toast("🐞") end) --["§"] 309 | 310 | 311 | --# window focus listener 312 | currentFocus = nil 313 | function onWindowFocusChanged(window_id) 314 | getFocusedWindow(function(win) 315 | if win ~= nil then 316 | if currentFocus == nil or currentFocus.id ~= win.id then 317 | currentFocus = win 318 | --deleteBorder() 319 | createBorder(win) 320 | end 321 | else 322 | currentFocus = nil 323 | deleteBorder() 324 | end 325 | end) 326 | end 327 | 328 | 329 | function onWindowResized(window_id) 330 | if currentFocus ~= nil and currentFocus.id == window_id then 331 | getWindow(currentFocus.id, 332 | function(win) 333 | --deleteBorder() 334 | createBorder(win) 335 | end 336 | ) 337 | end 338 | end 339 | 340 | 341 | function onWindowMoved(window_id) 342 | if currentFocus ~= nil and currentFocus.id == window_id then 343 | getWindow(currentFocus.id, 344 | function(win) 345 | --deleteBorder() 346 | createBorder(win) 347 | end 348 | ) 349 | end 350 | end 351 | 352 | 353 | function createBorder(win) 354 | if win == nil or canvases.winFocusRect == nil then 355 | return 356 | end 357 | canvases.winFocusRect:topLeft({ x = win.frame.x - 2, y = win.frame.y - 2 }) 358 | canvases.winFocusRect:size({ w = win.frame.w + 4, h = win.frame.h + 4 }) 359 | local borderColor = { red = 0.8, green = 0.8, blue = 0.2 , alpha = 0.6 } 360 | local zoomed = win["zoom-fullscreen"] == 1 361 | if zoomed then 362 | borderColor = { red = 0.8, green = 0.2, blue = 0.2 , alpha = 0.6 } 363 | end 364 | canvases.winFocusRect:replaceElements({ 365 | type = "rectangle", 366 | action = "stroke", 367 | strokeColor = borderColor, 368 | strokeWidth = 4, 369 | --strokeDashPattern = { 60, 40 }, 370 | roundedRectRadii = { xRadius = windowCornerRadius, yRadius = windowCornerRadius }, 371 | padding = 2 372 | }) 373 | canvases.winFocusRect:show() 374 | end 375 | function deleteBorder(fadeTime) 376 | canvases.winFocusRect:hide() 377 | end 378 | 379 | 380 | --# query 381 | function getFocusedWindow(callback) 382 | yabai({"-m", "query", "--windows"}, 383 | function(out, err) 384 | if out == nil or type(out) ~= "string" or string.len(out) == 0 then 385 | callback(nil) 386 | else 387 | out = string.gsub(out, ":inf,", ":0.0,") 388 | local json = "{\"windows\":"..out.."}" 389 | --print(json) 390 | local json_obj = hs.json.decode(json) 391 | if json_obj ~= nil then 392 | local windows = json_obj.windows 393 | for i, win in ipairs(windows) do 394 | if win.focused == 1 then 395 | callback(win) 396 | return 397 | end 398 | end 399 | callback(nil) 400 | else 401 | getFocusedWindow(callback) 402 | end 403 | end 404 | end 405 | ) 406 | end 407 | 408 | 409 | function getWindow(window_id, callback) 410 | yabai({"-m", "query", "--windows", "--window", tostring(window_id)}, 411 | function(out, err) 412 | if out == nil or string.len(out) == 0 then 413 | callback(nil) 414 | else 415 | --print("json|"..out.."|len"..string.len(out)) 416 | local win = hs.json.decode(out) 417 | callback(win) 418 | end 419 | end 420 | ) 421 | end 422 | 423 | 424 | --# install cli 425 | hs.ipc.cliInstall() 426 | 427 | 428 | -- calls made by yabai frow cli, see .yabairc 429 | yabaidirectcall = { 430 | window_focused = function(window_id) -- called when another window from the current app is focused 431 | onWindowFocusChanged(window_id) 432 | end, 433 | application_activated = function(process_id) -- called when a window from a different app is focused. Doesn’t exclude a window_focused call. 434 | onWindowFocusChanged(window_id) 435 | end, 436 | window_resized = function(window_id) -- called when a window changes dimensions 437 | onWindowResized(window_id) 438 | end, 439 | window_moved = function(window_id) -- called when a window is moved 440 | onWindowMoved(window_id) 441 | end 442 | } 443 | 444 | 445 | --# start yabai 446 | --os.execute("/usr/local/bin/yabai") 447 | -- so far I start yabai by hand from terminal so I can see logs 448 | 449 | --toast("Hello world", 1) 450 | 451 | onWindowFocusChanged(nil) -- show borders of focused window at startup 452 | -------------------------------------------------------------------------------- /.hammerspoon/initqwerty.lua: -------------------------------------------------------------------------------- 1 | require("hs.ipc") 2 | 3 | 4 | --# constants 5 | super = "⌃⌥" 6 | empty_table = {} 7 | windowCornerRadius = 10 8 | 9 | 10 | --# images 11 | local images = require("images") 12 | 13 | 14 | local windowAction = require("windowAction") 15 | windowAction.new(super, hs.keycodes.map["y"], "swap", images.swap) --["y"] 16 | windowAction.new(super, hs.keycodes.map["n"], "warp", images.warp) --["n"] 17 | windowAction.new(super, hs.keycodes.map["h"], "stack", images.stack) --["h"] 18 | 19 | 20 | --# canvas elements 21 | local canvases = { 22 | winFocusRect = hs.canvas.new({ x = 0, y = 0, w = 100, h = 100 }), 23 | } 24 | 25 | 26 | local focus_ = { 27 | --hideTimer = nil 28 | } 29 | 30 | 31 | --# helpers 32 | function yabai(args, completion) 33 | local yabai_output = "" 34 | local yabai_error = "" 35 | -- Runs in background very fast 36 | local yabai_task = hs.task.new("/usr/local/bin/yabai",nil, function(task, stdout, stderr) 37 | --print("stdout:"..stdout, "stderr:"..stderr) 38 | if stdout ~= nil then yabai_output = yabai_output..stdout end 39 | if stderr ~= nil then yabai_error = yabai_error..stderr end 40 | return true 41 | end, args) 42 | if type(completion) == "function" then 43 | yabai_task:setCallback(function() completion(yabai_output, yabai_error) end) 44 | end 45 | yabai_task:start() 46 | end 47 | 48 | 49 | function delayed(fn, delay) 50 | return hs.timer.delayed.new(delay, fn):start() 51 | end 52 | 53 | 54 | toasts = { 55 | main = nil 56 | } 57 | function killToast(params) 58 | params = params or empty_table 59 | local name = params.name or "main" 60 | if toasts[name] ~= nil then 61 | hs.alert.closeSpecific(toasts[name], params.fadeOutDuration or 0.1) 62 | toasts[name] = nil 63 | end 64 | end 65 | function toast(str, time, params) 66 | killToast(params) 67 | params = params or empty_table 68 | local name = params.name or "main" 69 | --local toast = toasts[name] 70 | toasts[name] = hs.alert(str, { 71 | fillColor = { white = 0, alpha = 0.4 }, 72 | strokeColor = { white = 0, alpha = 0 }, 73 | strokeWidth = 0, 74 | textColor = { white = 1, alpha = 1 }, 75 | radius = 0, 76 | padding = 6, 77 | atScreenEdge = 0, 78 | fadeInDuration = 0.1, 79 | fadeOutDuration = params.fadeOutDuration or 0.1 80 | }, time or 0.6) 81 | end 82 | 83 | 84 | --# Main chooser 85 | local mainChooser = hs.chooser.new(function(option) 86 | if option ~= nil then 87 | if option.action == "reload" then 88 | hs.reload() 89 | elseif option.action == "toggle_gap" then 90 | yabai({"-m", "space", "--toggle", "padding"}, function() yabai({"-m", "space", "--toggle", "gap"}) end) 91 | end 92 | end 93 | end):choices({ 94 | { 95 | text = "Toggle Gap", 96 | subText = "Toggles padding and gaps around the current space", 97 | action = "toggle_gap" 98 | }, 99 | { 100 | text = "Reload", 101 | subText = "Reload Hammerspoon configuration", 102 | action = "reload" 103 | }, 104 | }) 105 | 106 | 107 | --# bindings 108 | 109 | --# reload config 110 | hs.hotkey.bind(super, hs.keycodes.map["return"], nil, function() hs.reload() end) --["return"] 111 | --# open main chooser 112 | hs.hotkey.bind(super, hs.keycodes.map["space"], nil, function() mainChooser:show() end) --["space"] 113 | 114 | 115 | --# set layout under mouse 116 | hs.hotkey.bind(super, hs.keycodes.map["1"], function() yabai({"-m", "space", "mouse", "--layout", "bsp"}, function() toast("🖖") end) end) --["1"] 117 | hs.hotkey.bind(super, hs.keycodes.map["2"], function() yabai({"-m", "space", "mouse", "--layout", "stack"}, function() toast("📚") end) end) --["2"] 118 | hs.hotkey.bind(super, hs.keycodes.map["3"], function() yabai({"-m", "space", "mouse", "--layout", "float"}, function() toast("☁️") end) end) --["3"] 119 | 120 | 121 | --# rotate space 122 | hs.hotkey.bind(super, hs.keycodes.map["."], function() yabai({"-m", "space", "--rotate", "270"}, function() toast("🔲🔁") end) end) --["."] 123 | 124 | 125 | --# focus fullscreen 126 | hs.hotkey.bind(super, hs.keycodes.map["m"], function() yabai({"-m", "window", "--toggle", "zoom-fullscreen"}) end) --["m"] 127 | --hs.hotkey.bind(super, hs.keycodes.map[","], function() yabai({"-m", "window", "--toggle", "zoom-parent"}) end) -- not so useful --[","] 128 | 129 | 130 | --# toggle float layout for window 131 | hs.hotkey.bind(super, hs.keycodes.map["/"], function() yabai({"-m", "window", "--toggle", "float"}) toast("🎚☁️") end) --["/"] 132 | 133 | 134 | --# change window stack focus 135 | hs.hotkey.bind(super, hs.keycodes.map["t"], function() yabai({"-m", "window", "--focus", "stack.next"}, function() toast("📚↥") end) end) --["t"] 136 | hs.hotkey.bind(super, hs.keycodes.map["g"], function() yabai({"-m", "window", "--focus", "stack.prev"}, function() toast("📚↧") end) end) --["g"] 137 | 138 | 139 | --# change window focus to direction 140 | hs.hotkey.bind(super, hs.keycodes.map[";"], function() yabai({"-m", "window", "--focus", "east"}) end) --[";"] 141 | hs.hotkey.bind(super, hs.keycodes.map["j"], function() yabai({"-m", "window", "--focus", "west"}) end) --["j"] 142 | hs.hotkey.bind(super, hs.keycodes.map["l"], function() yabai({"-m", "window", "--focus", "north"}) end) --["l"] 143 | hs.hotkey.bind(super, hs.keycodes.map["k"], function() yabai({"-m", "window", "--focus", "south"}) end) --["k"] 144 | 145 | 146 | --# bsp ratio 147 | hs.hotkey.bind(super, hs.keycodes.map["7"], function() yabai({"-m", "window", "--ratio", "abs:0.38"}) toast("🔲⅓") end) --["7"] 148 | hs.hotkey.bind(super, hs.keycodes.map["8"], function() yabai({"-m", "window", "--ratio", "abs:0.5"}) toast("🔲½") end) --["8"] 149 | hs.hotkey.bind(super, hs.keycodes.map["9"], function() yabai({"-m", "window", "--ratio", "abs:0.62"}) toast("🔲⅔") end) --["9"] 150 | hs.hotkey.bind(super, hs.keycodes.map["-"], function() yabai({"-m", "space", "--balance"}) toast("🔲⚖️") end) --["-"] 151 | 152 | 153 | --# modals 154 | 155 | local focus_display_mod = hs.hotkey.modal.new(super, hs.keycodes.map["v"]) --["v"] 156 | local insert_window_modal = hs.hotkey.modal.new(super, hs.keycodes.map["tab"]) --["tab"] 157 | local move_display_modal = hs.hotkey.modal.new(super, hs.keycodes.map["b"]) --["b"] 158 | local resize_window_modal = hs.hotkey.modal.new() 159 | 160 | --# focus display 161 | function focus_display_mod:entered() 162 | toast("🖥🧭", true, { name = "modal" }) 163 | end 164 | function focus_display_mod:exited() 165 | killToast({ name = "modal" }) 166 | end 167 | focus_display_mod:bind("", hs.keycodes.map["escape"], function() focus_display_mod:exit() end) --["escape"] 168 | focus_display_mod:bind(super, hs.keycodes.map[";"], function() yabai({"-m", "display", "--focus", "next"}, function() delayed(function() toast("🖥➡️") end, 0.1) end) focus_display_mod:exit() end) --[";"] 169 | focus_display_mod:bind(super, hs.keycodes.map["j"], function() yabai({"-m", "display", "--focus", "prev"}, function() delayed(function() toast("🖥⬅️") end, 0.1) end) focus_display_mod:exit() end) --["j"] 170 | 171 | 172 | --# insert window rule 173 | --# insert window rule functions 174 | function insert_window_modal:entered() 175 | toast("🔲🌱 ", true, { name = "modal" }) 176 | end 177 | function insert_window_modal:exited() 178 | killToast({ name = "modal" }) 179 | end 180 | insert_window_modal:bind("", hs.keycodes.map["escape"], function() insert_window_modal:exit() end) --["escape"] 181 | insert_window_modal:bind(super, hs.keycodes.map[";"], function() yabai({"-m", "window", "--insert", "east"}) end) --[";"] 182 | insert_window_modal:bind(super, hs.keycodes.map["j"], function() yabai({"-m", "window", "--insert", "west"}) end) --["j"] 183 | insert_window_modal:bind(super, hs.keycodes.map["l"], function() yabai({"-m", "window", "--insert", "north"}) end) --["l"] 184 | insert_window_modal:bind(super, hs.keycodes.map["k"], function() yabai({"-m", "window", "--insert", "south"}) end) --["k"] 185 | insert_window_modal:bind(super, hs.keycodes.map["h"], function() yabai({"-m", "window", "--insert", "stack"}) end) --["h"] 186 | insert_window_modal:bind(super, hs.keycodes.map["tab"], function() insert_window_modal:exit() resize_window_modal:enter() end) --["tab"] 187 | 188 | 189 | --# send window to display 190 | local move_display_ = { 191 | selected = nil 192 | } 193 | function move_display_modal:entered() 194 | yabai({"-m", "query", "--windows", "--window"}, 195 | function(out) 196 | local window = hs.json.decode(out) 197 | if (window ~= nil) then 198 | --print(hs.inspect(hs.json.decode(out))) 199 | move_display_.selected = window 200 | toast("🔲🖥", true, { name = "move_display" }) 201 | end 202 | end 203 | ) 204 | end 205 | function move_display_modal:exited() 206 | move_display_.selected = nil 207 | killToast({ name = "move_display" }) 208 | end 209 | move_display_modal:bind(super, hs.keycodes.map[";"], --[";"] 210 | function() 211 | if (move_display_.selected ~= nil) then 212 | yabai({"-m", "window", "--display", "next"}, 213 | function() 214 | move_display_modal:exit() 215 | end 216 | ) 217 | end 218 | end 219 | ) 220 | move_display_modal:bind(super, hs.keycodes.map["j"], --["j"] 221 | function() 222 | if (move_display_.selected ~= nil) then 223 | yabai({"-m", "window", "--display", "prev"}, 224 | function() 225 | move_display_modal:exit() 226 | end 227 | ) 228 | end 229 | end 230 | ) 231 | move_display_modal:bind("", hs.keycodes.map["escape"], function() move_display_modal:exit() end) --["escape"] 232 | 233 | 234 | --# resize window 235 | local resize_window = { 236 | size = 20, 237 | horizontalEdge = nil, -- 1 is for right, -1 is for left 238 | verticalEdge = nil -- 1 is for bottom, -1 is for top 239 | } 240 | function resize_window_modal:entered() 241 | toast("🔲↔️", true, { name = "resize_window" }) 242 | end 243 | function resize_window_modal:exited() 244 | resize_window.horizontalEdge = nil 245 | resize_window.verticalEdge = nil 246 | killToast({ name = "resize_window" }) 247 | end 248 | resize_window_modal:bind(super, hs.keycodes.map[";"], function() --[";"] 249 | if resize_window.horizontalEdge == nil then 250 | resize_window.horizontalEdge = 1 251 | end 252 | if resize_window.horizontalEdge == 1 then 253 | -- grow from right 254 | print("grow from right") 255 | yabai({"-m", "window", "--resize", "right:"..resize_window.size..":0"}, function(out, err) print(out, err) end) 256 | else 257 | -- shrink from left 258 | print("shrink from left") 259 | yabai({"-m", "window", "--resize", "left:"..resize_window.size..":0"}, function(out, err) print(out, err) end) 260 | end 261 | end) 262 | resize_window_modal:bind(super, hs.keycodes.map["j"], function() --["j"] 263 | if resize_window.horizontalEdge == nil then 264 | resize_window.horizontalEdge = -1 265 | end 266 | if resize_window.horizontalEdge == 1 then 267 | -- shrink from right 268 | print("shrink from right") 269 | yabai({"-m", "window", "--resize", "right:-"..resize_window.size..":0"}, function(out, err) print(out, err) end) 270 | else 271 | -- grow from left 272 | print("grow from left") 273 | yabai({"-m", "window", "--resize", "left:-"..resize_window.size..":0"}, function(out, err) print(out, err) end) 274 | end 275 | end) 276 | resize_window_modal:bind(super, hs.keycodes.map["k"], function() --["k"] 277 | if resize_window.verticalEdge == nil then 278 | resize_window.verticalEdge = 1 279 | end 280 | if resize_window.verticalEdge == 1 then 281 | -- grow from bottom 282 | print("grow from bottom") 283 | yabai({"-m", "window", "--resize", "bottom:0:"..resize_window.size}, function(out, err) print(out, err) end) 284 | else 285 | -- shrink from top 286 | print("shrink from top") 287 | yabai({"-m", "window", "--resize", "top:0:"..resize_window.size}, function(out, err) print(out, err) end) 288 | end 289 | end) 290 | resize_window_modal:bind(super, hs.keycodes.map["l"], function() --["l"] 291 | if resize_window.verticalEdge == nil then 292 | resize_window.verticalEdge = -1 293 | end 294 | if resize_window.verticalEdge == 1 then 295 | -- shrink from bottom 296 | print("shrink from bottom") 297 | yabai({"-m", "window", "--resize", "bottom:0:-"..resize_window.size}, function(out, err) print(out, err) end) 298 | else 299 | -- grow from top 300 | print("grow from top") 301 | yabai({"-m", "window", "--resize", "top:0:-"..resize_window.size}, function(out, err) print(out, err) end) 302 | end 303 | end) 304 | resize_window_modal:bind("", hs.keycodes.map["escape"], function() resize_window_modal:exit() end) --["escape"] 305 | 306 | 307 | --# debug 308 | hs.hotkey.bind(super, hs.keycodes.map["§"], function() yabai({"-m", "query", "--windows", "--window"}, function(out) print(out) end) toast("🐞") end) --["§"] 309 | 310 | 311 | --# window focus listener 312 | currentFocus = nil 313 | function onWindowFocusChanged(window_id) 314 | getFocusedWindow(function(win) 315 | if win ~= nil then 316 | if currentFocus == nil or currentFocus.id ~= win.id then 317 | currentFocus = win 318 | --deleteBorder() 319 | createBorder(win) 320 | end 321 | else 322 | currentFocus = nil 323 | deleteBorder() 324 | end 325 | end) 326 | end 327 | 328 | 329 | function onWindowResized(window_id) 330 | if currentFocus ~= nil and currentFocus.id == window_id then 331 | getWindow(currentFocus.id, 332 | function(win) 333 | --deleteBorder() 334 | createBorder(win) 335 | end 336 | ) 337 | end 338 | end 339 | 340 | 341 | function onWindowMoved(window_id) 342 | if currentFocus ~= nil and currentFocus.id == window_id then 343 | getWindow(currentFocus.id, 344 | function(win) 345 | --deleteBorder() 346 | createBorder(win) 347 | end 348 | ) 349 | end 350 | end 351 | 352 | 353 | function createBorder(win) 354 | if win == nil or canvases.winFocusRect == nil then 355 | return 356 | end 357 | canvases.winFocusRect:topLeft({ x = win.frame.x - 2, y = win.frame.y - 2 }) 358 | canvases.winFocusRect:size({ w = win.frame.w + 4, h = win.frame.h + 4 }) 359 | local borderColor = { red = 0.8, green = 0.8, blue = 0.2 , alpha = 0.6 } 360 | local zoomed = win["zoom-fullscreen"] == 1 361 | if zoomed then 362 | borderColor = { red = 0.8, green = 0.2, blue = 0.2 , alpha = 0.6 } 363 | end 364 | canvases.winFocusRect:replaceElements({ 365 | type = "rectangle", 366 | action = "stroke", 367 | strokeColor = borderColor, 368 | strokeWidth = 4, 369 | --strokeDashPattern = { 60, 40 }, 370 | roundedRectRadii = { xRadius = windowCornerRadius, yRadius = windowCornerRadius }, 371 | padding = 2 372 | }) 373 | canvases.winFocusRect:show() 374 | end 375 | function deleteBorder(fadeTime) 376 | canvases.winFocusRect:hide() 377 | end 378 | 379 | 380 | --# query 381 | function getFocusedWindow(callback) 382 | yabai({"-m", "query", "--windows"}, 383 | function(out, err) 384 | if out == nil or type(out) ~= "string" or string.len(out) == 0 then 385 | callback(nil) 386 | else 387 | out = string.gsub(out, ":inf,", ":0.0,") 388 | local json = "{\"windows\":"..out.."}" 389 | --print(json) 390 | local json_obj = hs.json.decode(json) 391 | if json_obj ~= nil then 392 | local windows = json_obj.windows 393 | for i, win in ipairs(windows) do 394 | if win.focused == 1 then 395 | callback(win) 396 | return 397 | end 398 | end 399 | callback(nil) 400 | else 401 | getFocusedWindow(callback) 402 | end 403 | end 404 | end 405 | ) 406 | end 407 | 408 | 409 | function getWindow(window_id, callback) 410 | yabai({"-m", "query", "--windows", "--window", tostring(window_id)}, 411 | function(out, err) 412 | if out == nil or string.len(out) == 0 then 413 | callback(nil) 414 | else 415 | --print("json|"..out.."|len"..string.len(out)) 416 | local win = hs.json.decode(out) 417 | callback(win) 418 | end 419 | end 420 | ) 421 | end 422 | 423 | 424 | --# install cli 425 | hs.ipc.cliInstall() 426 | 427 | 428 | -- calls made by yabai frow cli, see .yabairc 429 | yabaidirectcall = { 430 | window_focused = function(window_id) -- called when another window from the current app is focused 431 | onWindowFocusChanged(window_id) 432 | end, 433 | application_activated = function(process_id) -- called when a window from a different app is focused. Doesn’t exclude a window_focused call. 434 | onWindowFocusChanged(window_id) 435 | end, 436 | window_resized = function(window_id) -- called when a window changes dimensions 437 | onWindowResized(window_id) 438 | end, 439 | window_moved = function(window_id) -- called when a window is moved 440 | onWindowMoved(window_id) 441 | end 442 | } 443 | 444 | 445 | --# start yabai 446 | --os.execute("/usr/local/bin/yabai") 447 | -- so far I start yabai by hand from terminal so I can see logs 448 | 449 | --toast("Hello world", 1) 450 | 451 | onWindowFocusChanged(nil) -- show borders of focused window at startup 452 | --------------------------------------------------------------------------------