├── .gitignore ├── README.md ├── caffeine-on.pdf ├── ignored.lua ├── init.lua ├── split_modal.lua └── undo.lua /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/lua 3 | 4 | ### Lua ### 5 | # Compiled Lua sources 6 | luac.out 7 | 8 | # luarocks build files 9 | *.src.rock 10 | *.zip 11 | *.tar.gz 12 | 13 | # Object files 14 | *.o 15 | *.os 16 | *.ko 17 | *.obj 18 | *.elf 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Libraries 25 | *.lib 26 | *.a 27 | *.la 28 | *.lo 29 | *.def 30 | *.exp 31 | 32 | # Shared objects (inc. Windows DLLs) 33 | *.dll 34 | *.so 35 | *.so.* 36 | *.dylib 37 | 38 | # Executables 39 | *.exe 40 | *.out 41 | *.app 42 | *.i*86 43 | *.x86_64 44 | *.hex 45 | 46 | # 47 | .DS_Store 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hammerspoon Configs 2 | 3 | ## Hammerspoon 4 | 5 | [Download here.](http://www.hammerspoon.org) 6 | 7 | ## Installation 8 | ```shell 9 | # backup old config 10 | mv ~/.hammerspoon ~/hammerspoon_bac 11 | 12 | git clone git://github.com/imty42/oh-my-hammerspoon.git ~/.hammerspoon 13 | ``` 14 | Hammerspoon->Reload Config 15 | 16 | ## Hotkey Definition By Convention 17 | 18 | - `drible`: `ctrl` + `cmd` 19 | - `dribleShift`: `ctrl` + `cmd` + `shift` 20 | - `prefix`: `ctrl` + `alt` 21 | - `prefixShift`: `ctrl` + `alt` + `shift` 22 | - `hyper`: `ctrl` + `alt` + `cmd` 23 | - `hyperShift`: `ctrl` + `alt` + `cmd` + `shift` 24 | 25 | ## Features 26 | 27 | - Switch to certain app. 28 | - `prefix`/`prefixShift` + somekey 29 | - Quick resize current window. 30 | - leftTop: `hyper` + `u` 31 | - rightTop: `hyper` + `i` 32 | - leftBottom: `hyper` + `j` 33 | - rightBottom: `hyper` + `k` 34 | - leftHalf: `hyper` + `h` 35 | - rightHalf: `hyper` + `l` 36 | - fullScreen: `hyper` + `f` 37 | - centerScreen: `hyper` + `c` 38 | - Move current window horizontally, or vertically. 39 | - upMove: `hyper` + `up` 40 | - downMove: `hyper` + `down` 41 | - leftMove: `hyper` + `left` 42 | - rightMove: `hyper` + `right` 43 | - Adjust the size of current window. 44 | - upExtend: `hyperShift` + `up` 45 | - downExtend: `hyperShift` + `down` 46 | - leftExtend: `hyperShift` + `left` 47 | - rightExtend: `hyperShift` + `right` 48 | - Move current window to another monitor. 49 | - toWest: `dribleShift` + `left` 50 | - toEast: `dribleShift` + `right` 51 | - Adjust windows for current app mode. `hyper` + `down` enter the mode. In the mode `left`, `right` adjust window, `up`, `down` switch window. 52 | - Undo. 53 | - `hyper` + `z` 54 | - Auto adjust apps when an app is just launched or a new monitor is plugged/removed. 55 | - Caffeinate. 56 | - `dribleShift` + `c` 57 | - Auto reload configs. 58 | - iTunes control. 59 | - previous: `dribleShift` + `7` 60 | - play/pause: `dribleShift` + `8` 61 | - next: `dribleShift` + `9` 62 | - Volumn control. 63 | - mute: `dribleShift` + `0` 64 | - lower: `dribleShift` + `-` 65 | - louder: `dribleShift` + `+` 66 | 67 | ## Regards 68 | 69 | @songchenwen https://github.com/songchenwen/dotfiles/tree/master/hammerspoon 70 | 71 | Mainly based on his work, i just added some specific resizing code. 72 | -------------------------------------------------------------------------------- /caffeine-on.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imty42/oh-my-hammerspoon/b109ada5e420a9d4770e137d716bcf87ee925686/caffeine-on.pdf -------------------------------------------------------------------------------- /ignored.lua: -------------------------------------------------------------------------------- 1 | local ignored_apps = { 2 | --'QQ', 3 | 'WeChat', 4 | 'Newsflow', 5 | 'SourceTree', 6 | --'iTerm' 7 | } 8 | 9 | function ignored(win) 10 | local app = win:application() 11 | if app then 12 | local title = app:title() 13 | print('identifier'..title) 14 | return hs.fnutils.contains(ignored_apps, title) 15 | end 16 | return false 17 | end 18 | 19 | return ignored 20 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | hs.alert.closeAll() 2 | local ignored = require 'ignored' 3 | 4 | -- key define 5 | local drible = {'ctrl', 'cmd'} 6 | local dribleShift = {'ctrl', 'cmd', 'shift'} 7 | local prefix = {'ctrl', 'alt'} 8 | local prefixShift = {'ctrl', 'alt', 'shift'} 9 | local hyper = {'ctrl', 'alt', 'cmd'} 10 | local hyperShift = {'ctrl', 'alt', 'cmd', 'shift'} 11 | 12 | -- states 13 | hs.window.animationDuration = 0 14 | 15 | -- cli 16 | if not hs.ipc.cliStatus() then hs.ipc.cliInstall() end 17 | 18 | -- App shortcuts 19 | local keyApp = { 20 | I = 'iTunes', 21 | K = 'Kindle', 22 | M = 'mail', 23 | O = 'Opera Developer', 24 | P = 'Paw', 25 | R = 'Reeder', 26 | S = 'Sublime Text', 27 | W = 'WeChat', 28 | ---- cutting line ---- 29 | b = 'Bilibili', 30 | c = 'Google Chrome', 31 | f = 'Finder', 32 | i = 'IntelliJ IDEA', 33 | l = 'Leanote', 34 | m = 'Sequel Pro', 35 | --n = 'Microsoft OneNote', 36 | o = 'Omnifocus', 37 | q = 'QQ', 38 | s = 'Safari', 39 | t = 'iTerm', 40 | v = 'vscode', 41 | } 42 | for key, app in pairs(keyApp) do 43 | if key == string.upper(key) then 44 | hs.hotkey.bind(prefixShift, key, function() hs.application.launchOrFocus(app) end) 45 | else 46 | hs.hotkey.bind(prefix, key, function() hs.application.launchOrFocus(app) end) 47 | end 48 | end 49 | 50 | -- Hints 51 | hs.hotkey.bind(drible, 'h', function() 52 | hs.hints.windowHints(getAllValidWindows()) 53 | end) 54 | 55 | 56 | -- iTunes control 57 | hs.hotkey.bind(dribleShift, '7', function() hs.itunes.previous() end) 58 | hs.hotkey.bind(dribleShift, '8', function() hs.itunes.playpause() end) 59 | hs.hotkey.bind(dribleShift, '9', function() hs.itunes.next() end) 60 | 61 | 62 | -- Sound 63 | hs.hotkey.bind(dribleShift, '0', function() 64 | --- dod default_output_audio 65 | dod = hs.audiodevice.defaultOutputDevice() 66 | is_muted = dod:muted() 67 | if is_muted then 68 | dod:setMuted(false) 69 | hs.alert('Audio is unmuted!') 70 | else 71 | dod:setMuted(true) 72 | hs.alert('Audio is muted!') 73 | end 74 | end) 75 | hs.hotkey.bind(dribleShift, '-', function() 76 | --- dod default_output_audio 77 | dod = hs.audiodevice.defaultOutputDevice() 78 | auv = math.floor(dod:outputVolume() - 5) 79 | dod:setVolume(auv) 80 | hs.alert('Volume down:' .. auv) 81 | print(dod:outputVolume()) 82 | end) 83 | 84 | hs.hotkey.bind(dribleShift, '=', function() 85 | --- dod default_output_audio 86 | dod = hs.audiodevice.defaultOutputDevice() 87 | auv = math.floor(dod:outputVolume() + 5) 88 | dod:setVolume(auv) 89 | hs.alert('Volume up:' .. auv) 90 | print(dod:outputVolume()) 91 | end) 92 | 93 | -- undo 94 | local undo = require 'undo' 95 | hs.hotkey.bind(hyper, 'z', function() undo:undo() end) 96 | 97 | -- Grids 98 | hs.grid.GRIDWIDTH = 16 99 | hs.grid.GRIDHEIGHT = 8 100 | hs.grid.MARGINX = 0 101 | hs.grid.MARGINY = 0 102 | 103 | local halfMaxWidth = hs.grid.GRIDWIDTH / 2 104 | local halfMaxHeight = hs.grid.GRIDHEIGHT / 2 105 | 106 | local moveMaxWidth = halfMaxWidth + 1 107 | local moveMinWidth = halfMaxWidth - 1 108 | local moveMaxHighth = halfMaxWidth + 1 109 | local moveMinHighth = halfMaxWidth - 1 110 | 111 | 112 | -- Move Window 113 | function locationSet(pos_x, pos_y, width, height) 114 | local w = hs.window.focusedWindow() 115 | if not w or not w:isStandard() then return end 116 | local s = w:screen() 117 | if not s then return end 118 | if ignored(w) then return end 119 | local g = hs.grid.get(w) 120 | 121 | g.x = pos_x 122 | g.y = pos_y 123 | 124 | if pos_x + width > hs.grid.GRIDWIDTH then 125 | g.w = hs.grid.GRIDWIDTH - pos_x 126 | else 127 | g.w = width 128 | end 129 | 130 | if pos_y + height > hs.grid.GRIDHEIGHT then 131 | g.h = hs.grid.GRIDHEIGHT - pos_y 132 | else 133 | g.h = height 134 | end 135 | 136 | undo:addToStack() 137 | hs.grid.set(w, g, s) 138 | end 139 | 140 | 141 | function straightlyMove(step_x, step_y) 142 | local w = hs.window.focusedWindow() 143 | if not w or not w:isStandard() then return end 144 | local s = w:screen() 145 | if not s then return end 146 | if ignored(w) then return end 147 | local g = hs.grid.get(w) 148 | 149 | -- if moving to the edge 150 | if step_x < 0 and g.x <= 0 and g.w <= 1 then 151 | return 152 | elseif step_y < 0 and g.y <= 0 and g.h <= 1 then 153 | return 154 | elseif step_x > 0 and g.x + g.w >= hs.grid.GRIDWIDTH and g.w <= 1 then 155 | return 156 | elseif step_y > 0 and g.y + g.h >= hs.grid.GRIDHEIGHT and g.h <= 1 then 157 | return 158 | end 159 | 160 | if step_x < 0 and g.x <= 0 then 161 | g.w = g.w - 1 162 | elseif step_x > 0 and g.x + g.w >= hs.grid.GRIDWIDTH then 163 | g.x = g.x + 1 164 | g.w = g.w - 1 165 | else 166 | g.x = g.x + step_x 167 | end 168 | 169 | 170 | if step_y < 0 and g.y <= 0 then 171 | g.h = g.h - 1 172 | elseif step_y > 0 and g.y + g.h >= hs.grid.GRIDHEIGHT then 173 | g.y = g.y + 1 174 | g.h = g.h - 1 175 | else 176 | g.y = g.y + step_y 177 | end 178 | 179 | undo:addToStack() 180 | hs.grid.set(w, g, s) 181 | end 182 | 183 | 184 | function straightlyExtend(step_x, step_y) 185 | local w = hs.window.focusedWindow() 186 | if not w or not w:isStandard() then return end 187 | local s = w:screen() 188 | if not s then return end 189 | if ignored(w) then return end 190 | local g = hs.grid.get(w) 191 | 192 | -- if moving to the edge 193 | if step_x < 0 and g.x <= 0 then 194 | return 195 | elseif step_y < 0 and g.y <= 0 then 196 | return 197 | elseif step_x > 0 and g.x + g.w >= hs.grid.GRIDWIDTH then 198 | return 199 | elseif step_y > 0 and g.y + g.h >= hs.grid.GRIDHEIGHT then 200 | return 201 | end 202 | 203 | if step_x < 0 then 204 | g.x = g.x - 1 205 | g.w = g.w + 1 206 | elseif step_x > 0 then 207 | g.w = g.w + 1 208 | end 209 | 210 | if step_y < 0 then 211 | g.y = g.y - 1 212 | g.h = g.h + 1 213 | elseif step_y > 0 then 214 | g.h = g.h + 1 215 | end 216 | 217 | undo:addToStack() 218 | hs.grid.set(w, g, s) 219 | end 220 | 221 | 222 | local leftTop = hs.hotkey.bind(hyper, 'u', function() locationSet(0, 0, halfMaxWidth, halfMaxHeight) end) 223 | local rightTop = hs.hotkey.bind(hyper, 'i', function() locationSet(halfMaxWidth, 0, halfMaxWidth, halfMaxHeight) end) 224 | local leftBottom = hs.hotkey.bind(hyper, 'j', function() locationSet(0, halfMaxHeight, halfMaxWidth, halfMaxHeight) end) 225 | local rightBottom = hs.hotkey.bind(hyper, 'k', function() locationSet(halfMaxWidth, halfMaxHeight, halfMaxWidth, halfMaxHeight) end) 226 | local leftHalf = hs.hotkey.bind(hyper, 'h', function() locationSet(0, 0, halfMaxWidth, hs.grid.GRIDHEIGHT) end) 227 | local rightHalf = hs.hotkey.bind(hyper, 'l', function() locationSet(halfMaxWidth, 0, halfMaxWidth, hs.grid.GRIDHEIGHT) end) 228 | 229 | local fullScreen = hs.hotkey.bind(hyper, 'f', function() hs.grid.maximizeWindow() end) 230 | local centerScreen = hs.hotkey.bind(hyper, 'c', function() locationSet(halfMaxWidth / 4, halfMaxHeight / 4, halfMaxWidth * 3 / 2, halfMaxHeight * 3 / 2) end) 231 | 232 | local upMove = hs.hotkey.bind(hyper, 'up', function() straightlyMove(0, -1) end) 233 | local downMove = hs.hotkey.bind(hyper, 'down', function() straightlyMove(0, 1) end) 234 | local leftMove = hs.hotkey.bind(hyper, 'left', function() straightlyMove(-1, 0) end) 235 | local rightMove = hs.hotkey.bind(hyper, 'right', function() straightlyMove(1, 0) end) 236 | 237 | local upExtend = hs.hotkey.bind(hyperShift, 'up', function() straightlyExtend(0, -1) end) 238 | local downExtend = hs.hotkey.bind(hyperShift, 'down', function() straightlyExtend(0, 1) end) 239 | local leftExtend = hs.hotkey.bind(hyperShift, 'left', function() straightlyExtend(-1, 0) end) 240 | local rightExtend = hs.hotkey.bind(hyperShift, 'right', function() straightlyExtend(1, 0) end) 241 | 242 | 243 | -- Move Screen 244 | hs.hotkey.bind(dribleShift, 'left', function() 245 | local w = hs.window.focusedWindow() 246 | if not w then 247 | return 248 | end 249 | if ignored(w) then return end 250 | 251 | local s = w:screen():toWest() 252 | if s then 253 | undo:addToStack() 254 | w:moveToScreen(s) 255 | end 256 | end) 257 | 258 | hs.hotkey.bind(dribleShift, 'right', function() 259 | local w = hs.window.focusedWindow() 260 | if not w then 261 | return 262 | end 263 | if ignored(w) then return end 264 | 265 | local s = w:screen():toEast() 266 | if s then 267 | undo:addToStack() 268 | w:moveToScreen(s) 269 | end 270 | end) 271 | 272 | -- split view 273 | SplitModal = require 'split_modal' 274 | local splitModal = SplitModal.new(dribleShift, 'down', undo) 275 | 276 | function splitModal:hotkeysToDisable() 277 | return {hyperUp, hyperRight, hyperLeft} 278 | end 279 | 280 | -- App layout 281 | local AppLayout = {} 282 | AppLayout['Safari'] = { large = true, full = true } 283 | AppLayout['Tweetbot'] = { small = true } 284 | 285 | function layoutApp(name, app, delayed) 286 | local conf = AppLayout[name] 287 | if not conf then return end 288 | 289 | if not delayed and conf.delay then 290 | print('delay layout '..name..' for '..conf.delay..' secs') 291 | hs.timer.doAfter(conf.delay, function() 292 | layoutApp(name, app, true) 293 | end) 294 | return 295 | end 296 | 297 | if not app then app = hs.appfinder.appFromName(name) end 298 | local w = nil 299 | if app then w = app:mainWindow() end 300 | if not w then return end 301 | local s = nil 302 | if conf.small then 303 | print('move app '..name..' to smallerScreen') 304 | s = smallerScreen 305 | end 306 | if conf.large then 307 | print('move app '..name..' to largerScreen') 308 | s = largerScreen 309 | end 310 | if s and s ~= w:screen() then w:moveToScreen(s) end 311 | 312 | if conf.full then 313 | print('maximize app '..name) 314 | w:maximize() 315 | end 316 | end 317 | 318 | function applicationWatcherCallback(name, event, app) 319 | if event == hs.application.watcher.launched then 320 | layoutApp(name, app) 321 | end 322 | end 323 | 324 | local appWatcher = hs.application.watcher.new(applicationWatcherCallback) 325 | appWatcher:start() 326 | 327 | -- screen change 328 | function screenChanged() 329 | local ss = hs.screen.allScreens() 330 | local count = #ss 331 | if count ~= lastNumberOfScreens then 332 | lastNumberOfScreens = count 333 | local largest = 0 334 | for i = 1, lastNumberOfScreens do 335 | local s = ss[i] 336 | local size = s:frame().w * s:frame().h 337 | local preSmall = smallerScreen 338 | smallerScreen = s 339 | if size > largest then 340 | largest = size 341 | largerScreen = s 342 | if preSmall then smallerScreen = preSmall end 343 | end 344 | end 345 | print('NumberOfScreens '.. lastNumberOfScreens) 346 | for app, conf in pairs(AppLayout) do 347 | layoutApp(app, nil, true) 348 | end 349 | end 350 | end 351 | 352 | screenChanged() 353 | 354 | local screenWatcher = hs.screen.watcher.new(screenChanged) 355 | screenWatcher:start() 356 | 357 | -- caffeinate 358 | hs.hotkey.bind(dribleShift, 'c', function() 359 | local c = hs.caffeinate 360 | if not c then return end 361 | if c.get('displayIdle') or c.get('systemIdle') or c.get('system') then 362 | if menuCaff then 363 | menuCaffRelease() 364 | else 365 | addMenuCaff() 366 | local type 367 | if c.get('displayIdle') then type = 'displayIdle' end 368 | if c.get('systemIdle') then type = 'systemIdle' end 369 | if c.get('system') then type = 'system' end 370 | hs.alert('Caffeine already on for '..type) 371 | end 372 | else 373 | acAndBatt = hs.battery.powerSource() == 'Battery Power' 374 | c.set('system', true, acAndBatt) 375 | hs.alert('Caffeinated '..(acAndBatt and '' or 'on AC Power')) 376 | addMenuCaff() 377 | end 378 | end) 379 | 380 | function addMenuCaff() 381 | menuCaff = hs.menubar.new() 382 | menuCaff:setIcon("~/.hammerspoon/caffeine-on.pdf") 383 | menuCaff:setClickCallback(menuCaffRelease) 384 | end 385 | 386 | function menuCaffRelease() 387 | local c = hs.caffeinate 388 | if not c then return end 389 | if c.get('displayIdle') then 390 | c.set('displayIdle', false, true) 391 | end 392 | if c.get('systemIdle') then 393 | c.set('systemIdle', false, true) 394 | end 395 | if c.get('system') then 396 | c.set('system', false, true) 397 | end 398 | if menuCaff then 399 | menuCaff:delete() 400 | menuCaff = nil 401 | end 402 | hs.alert('Decaffeinated') 403 | end 404 | 405 | -- console 406 | hs.hotkey.bind(hyperShift, ';', hs.openConsole) 407 | 408 | -- reload 409 | hs.hotkey.bind(hyper, 'escape', function() hs.reload() end ) 410 | function reloadConfig(files) 411 | doReload = false 412 | for _,file in pairs(files) do 413 | if file:sub(-4) == ".lua" then 414 | doReload = true 415 | end 416 | end 417 | if doReload then 418 | hs.reload() 419 | end 420 | end 421 | hs.pathwatcher.new(os.getenv("HOME") .. "/.hammerspoon/", reloadConfig):start() 422 | -- hs.alert.show("Config loaded1") 423 | 424 | -- utils 425 | function getAllValidWindows () 426 | local allWindows = hs.window.allWindows() 427 | local windows = {} 428 | local index = 1 429 | for i = 1, #allWindows do 430 | local w = allWindows[i] 431 | if w:screen() then 432 | windows[index] = w 433 | index = index + 1 434 | end 435 | end 436 | return windows 437 | end 438 | 439 | hs.alert('Hammerspoon', 0.6) 440 | -------------------------------------------------------------------------------- /split_modal.lua: -------------------------------------------------------------------------------- 1 | SplitModal = {} 2 | SplitModal.__index = SplitModal 3 | 4 | local ignored = require 'ignored' 5 | 6 | function SplitModal.new(mods, key, undo) 7 | local m = {} 8 | setmetatable(m, SplitModal) 9 | m.key = hs.hotkey.modal.new(mods, key) 10 | m.undo = undo 11 | m.stickedWindow = nil 12 | m.windows = {} 13 | m.otherWindow = nil 14 | m.splitPoint = hs.grid.GRIDWIDTH / 2 15 | m.otherOriRect = nil 16 | m.line = nil 17 | 18 | m.key:bind({}, 'escape', function() 19 | m.key:exit() 20 | end) 21 | m.key:bind({}, 'return', function() 22 | m.key:exit() 23 | end) 24 | 25 | m.key:bind({}, 'up', function() 26 | m:_moveBetweenWindows(1) 27 | end) 28 | 29 | m.key:bind({}, 'down', function() 30 | m:_moveBetweenWindows(-1) 31 | end) 32 | 33 | m.key:bind({}, 'left', function() 34 | if m.splitPoint < hs.grid.GRIDWIDTH / 2 then return end 35 | m.splitPoint = m.splitPoint - 1 36 | m:_layout() 37 | end) 38 | 39 | m.key:bind({}, 'right', function() 40 | if m.splitPoint > hs.grid.GRIDWIDTH / 2 then return end 41 | m.splitPoint = m.splitPoint + 1 42 | m:_layout() 43 | end) 44 | 45 | function m.key:entered() 46 | m:entered() 47 | end 48 | 49 | function m.key:exited() 50 | m:exited() 51 | end 52 | 53 | return m 54 | end 55 | 56 | function SplitModal:entered() 57 | local w = hs.window.focusedWindow() 58 | if w and w:isStandard() and not ignored(w) then 59 | self:_switchStickedWindow(w) 60 | else 61 | self.key:exit() 62 | return 63 | end 64 | if #self.windows > 0 then 65 | self.splitPoint = hs.grid.GRIDWIDTH / 2 66 | self.otherWindow = self.windows[1] 67 | self.otherOriRect = nil 68 | self.line = nil 69 | self:_layout() 70 | self.undo.skip = true 71 | local ks = self:hotkeysToDisable() 72 | if ks then 73 | local count = #ks 74 | for i = 1, count do 75 | ks[i]:disable() 76 | end 77 | end 78 | else 79 | hs.alert('No Window to Split') 80 | self.key:exit() 81 | return 82 | end 83 | end 84 | 85 | function SplitModal:_layout() 86 | local s = self.stickedWindow:screen() 87 | self.otherOriRect = self.otherWindow:frame() 88 | 89 | if not self.undo.skip then 90 | self.undo:addToStack({self.stickedWindow, self.otherWindow}) 91 | else 92 | local status = self.undo.stack[#self.undo.stack] 93 | local ww = nil 94 | local ff = nil 95 | for w, f in pairs(status) do 96 | if w ~= self.stickedWindow then 97 | status[w] = nil 98 | end 99 | end 100 | status[self.otherWindow] = self.otherWindow:frame() 101 | self.undo.stack[#self.undo.stack] = status 102 | end 103 | 104 | hs.grid.set(self.stickedWindow, hs.geometry.rect(0, 0, self.splitPoint, hs.grid.GRIDHEIGHT), s) 105 | hs.grid.set(self.otherWindow, hs.geometry.rect(self.splitPoint, 0, hs.grid.GRIDWIDTH - self.splitPoint, hs.grid.GRIDHEIGHT), s) 106 | self:drawLine() 107 | self.otherWindow:focus() 108 | self.stickedWindow:focus() 109 | end 110 | 111 | function SplitModal:drawLine() 112 | if self.line then self.line:delete() end 113 | local f = self.stickedWindow:frame() 114 | self.line = hs.drawing.line({x = f.x + f.w, y = f.y}, {x = f.x + f.w, y = f.y + f.h}) 115 | self.line:setStroke(true) 116 | self.line:setStrokeColor({red = 0.8, green = 0.4, blue = 0.7, alpha = 0.9}) 117 | self.line:setStrokeWidth(6) 118 | self.line:show() 119 | end 120 | 121 | function SplitModal:_switchStickedWindow(w) 122 | self.stickedWindow = w 123 | self.windows = {} 124 | local app = w:application() 125 | self:_addWindows(app:visibleWindows()) 126 | self:_addWindows(w:otherWindowsSameScreen()) 127 | self:_addWindows(w:otherWindowsAllScreens()) 128 | end 129 | 130 | function SplitModal:_addWindows(ws) 131 | local size = #ws 132 | for i = 1, size do 133 | local w = ws[i] 134 | if w:screen() and w ~= self.stickedWindow and not hs.fnutils.contains(self.windows, w) and not ignored(w) and w:isStandard() then 135 | self.windows[#self.windows + 1] = w 136 | end 137 | end 138 | end 139 | 140 | function SplitModal:_moveBetweenWindows(direction) 141 | local prevW = self.otherWindow 142 | local prevWFrame = self.otherOriRect 143 | local index = hs.fnutils.indexOf(self.windows, prevW) 144 | index = index + direction 145 | if index > #self.windows then 146 | index = 1 147 | end 148 | if index < 1 then 149 | index = #self.windows 150 | end 151 | local nextW = self.windows[index] 152 | if nextW then 153 | if prevWFrame then prevW:setFrame(prevWFrame) end 154 | self.otherWindow = nextW 155 | self:_layout() 156 | end 157 | end 158 | 159 | function SplitModal:hotkeysToDisable() 160 | return nil 161 | end 162 | 163 | function SplitModal:exited() 164 | self.stickedWindow = nil 165 | self.otherWindow = nil 166 | self.windows = nil 167 | self.undo.skip = false 168 | if self.line then 169 | self.line:delete() 170 | self.line = nil 171 | end 172 | local ks = self:hotkeysToDisable() 173 | if ks then 174 | local count = #ks 175 | for i = 1, count do 176 | ks[i]:enable() 177 | end 178 | end 179 | end 180 | 181 | return SplitModal -------------------------------------------------------------------------------- /undo.lua: -------------------------------------------------------------------------------- 1 | local undo = { 2 | stack = {}, 3 | stackMax = 100, 4 | skip = false, 5 | } 6 | 7 | function undo:addToStack(wins) 8 | if self.skip then return end 9 | if not wins then wins = { hs.window.focusedWindow() } end 10 | local size = #self.stack 11 | self.stack[size + 1] = self:getCurrentWindowsLayout(wins) 12 | size = size + 100 13 | if size > self.stackMax then 14 | for i = 1, size - self.stackMax do 15 | self.stack[1] = nil 16 | end 17 | end 18 | end 19 | 20 | function undo:undo() 21 | local size = #self.stack 22 | if size > 0 then 23 | local status = self.stack[size] 24 | for w, f in pairs(status) do 25 | if w and f and w:isVisible() and w:isStandard() and w:id() then 26 | if not compareFrame(f, w:frame()) then 27 | w:setFrame(f) 28 | end 29 | end 30 | end 31 | self.stack[size] = nil 32 | else 33 | hs.alert('Reach Undo End', 0.5) 34 | end 35 | end 36 | 37 | function undo:getCurrentWindowsLayout(wins) 38 | if not wins then wins = { hs.window.focusedWindow() } end 39 | local current = {} 40 | for i = 1, #wins do 41 | local w = wins[i] 42 | local f = w:frame() 43 | if w:isVisible() and w:isStandard() and w:id() and f then 44 | current[w] = f 45 | end 46 | end 47 | return current 48 | end 49 | 50 | function compareFrame(t1, t2) 51 | if t1 == t2 then return true end 52 | if t1 and t2 then 53 | return t1.x == t2.x and t1.y == t2.y and t1.w == t2.w and t1.h == t2.h 54 | end 55 | return false 56 | end 57 | 58 | return undo --------------------------------------------------------------------------------