Skip to content

Instantly share code, notes, and snippets.

@cmsj
Created October 22, 2014 19:51
Show Gist options
  • Save cmsj/41cb1206168a48b11528 to your computer and use it in GitHub Desktop.
Save cmsj/41cb1206168a48b11528 to your computer and use it in GitHub Desktop.
--require "pl.strict"
local window = require "hs.window"
local screen = require "hs.screen"
local hotkey = require "hs.hotkey"
local geometry = require "hs.geometry"
local fnutils = require "hs.fnutils"
local appfinder = require "hs.appfinder"
local caffeinate = require "hs.caffeinate"
local notify = require "hs.notify"
--local screenwatch = require "hs._asm.watcher.screen"
-- Define some keyboard modifier variables
-- (Node: Capslock bound to cmd+alt+ctrl+shift via Seil and Karabiner)
local alt = {"alt"}
local hyper = {"cmd", "alt", "ctrl", "shift"}
-- Define some window rects for layout purposes
local left30 = geometry.rect(0, 0, 0.3, 1)
local left50 = geometry.rect(0, 0, 0.5, 1)
local left70 = geometry.rect(0, 0, 0.7, 1)
local right30 = geometry.rect(0.7, 0, 0.3, 1)
local right50 = geometry.rect(0.5, 0, 0.5, 1)
local right70 = geometry.rect(0.3, 0, 0.7, 1)
local maximized = geometry.rect(0, 0, 1, 1)
-- Define monitor names for layout purposes
local display_laptop = "Color LCD"
local display_monitor = "Thunderbolt Display"
-- Define window layouts
-- Format reminder:
-- {"App name", "Window name", "Display Name", "unitrect", "framerect", "fullframerect"},
local internal_display = {
{"IRC", nil, display_laptop, maximized, nil, nil},
{"Reeder", nil, display_laptop, left30, nil, nil},
{"Safari", nil, display_laptop, maximized, nil, nil},
{"OmniFocus", nil, display_laptop, maximized, nil, nil},
{"Mail", nil, display_laptop, maximized, nil, nil},
{"Microsoft Outlook", nil, display_laptop, maximized, nil, nil},
{"HipChat", nil, display_laptop, maximized, nil, nil},
{"1Password 4", nil, display_laptop, maximized, nil, nil},
{"Calendar", nil, display_laptop, maximized, nil, nil},
{"Messages", nil, display_laptop, maximized, nil, nil},
{"Evernote", nil, display_laptop, maximized, nil, nil},
{"iTunes", "iTunes", display_laptop, maximized, nil, nil},
{"iTunes", "MiniPlayer", display_laptop, nil, nil, geometry.rect(0, -48, 400, 48)},
}
local dual_display = {
{"IRC", nil, display_laptop, maximized, nil, nil},
{"Reeder", nil, display_monitor, right50, nil, nil},
{"Safari", nil, display_monitor, left50, nil, nil},
{"OmniFocus", nil, display_monitor, right50, nil, nil},
{"Mail", nil, display_laptop, maximized, nil, nil},
{"Microsoft Outlook", nil, display_monitor, maximized, nil, nil},
{"HipChat", nil, display_monitor, right50, nil, nil},
{"1Password 4", nil, display_monitor, right50, nil, nil},
{"Calendar", nil, display_monitor, maximized, nil, nil},
{"Messages", nil, display_laptop, maximized, nil, nil},
{"Evernote", nil, display_monitor, right50, nil, nil},
{"iTunes", "iTunes", display_laptop, maximized, nil, nil},
{"iTunes", "MiniPlayer", display_laptop, nil, nil, geometry.rect(0, -48, 400, 48)},
}
-- Helper functions
function toggle_fullscreen()
local win = window.focusedwindow()
local isfull = win:isfullscreen()
if isfull ~= nil then
win:setfullscreen(not isfull)
end
end
function toggle_console()
local console = appfinder.window_from_window_title("Hammerspoon Console")
if console and (console ~= window.focusedwindow()) then
console:focus()
elseif console then
console:close()
else
hs.openconsole()
end
end
function relocate_window(win, src, dst)
-- Moves a window between screens, retaining relative proportions
local f = win:frame()
local old_screen = src:frame()
local new_screen = dst:frame()
local h_perc = f.h / old_screen.h
local w_perc = f.w / old_screen.w
local x_perc = math.abs(old_screen.x - f.x) / old_screen.w
local y_perc = (f.y - old_screen.y) / old_screen.h
f.x = new_screen.x + (new_screen.w * x_perc)
f.y = new_screen.y + (new_screen.h * y_perc)
f.w = new_screen.w * w_perc
f.h = new_screen.h * h_perc
win:setframe(f)
end
function move_window_one_screen_west()
local win = window.focusedwindow()
local dst = win:screen():towest()
if dst ~= nil then
relocate_window(win, win:screen(), dst)
end
end
function move_window_one_screen_east()
local win = window.focusedwindow()
local dst = win:screen():toeast()
if dst ~= nil then
relocate_window(win, win:screen(), dst)
end
end
function toggle_display_sleep()
local is_sleep = caffeinate.toggle("DisplayIdle")
local msg = "Display is now "
if not is_sleep then
msg = msg .. "de-"
end
msg = msg .. "caffeinated."
notify.show("Hammerspoon", "", msg, "")
end
function apply_layout(layout)
-- TODO: Add optional debugging
-- Layout parameter should be a table where each row takes the form of:
-- {"App name", "Window name","Display Name", "unitrect", "framerect", "fullframerect"},
-- First three items in each row are strings
-- Second three items are rects that specify the position of the window. The first one that is
-- not nil, wins.
-- unitrect is a rect passed to window:movetounit()
-- framerect is a rect passed to window:setframe()
-- If either the x or y components of framerect are negative, they will be applied as
-- offsets from the width or height of screen:frame(), respectively
-- fullframerect is a rect passed to window:setframe()
-- If either the x or y components of fullframerect are negative, they will be applied
-- as offsets from the width or height of screen:fullframe(), respectively
for n,_row in pairs(layout) do
local app = nil
local wins = nil
local display = nil
local displaypoint = nil
local unit = _row[4]
local frame = _row[5]
local fullframe = _row[6]
local windows = nil
-- Find the application's object, if wanted
if _row[1] then
app = appfinder.app_from_name(_row[1])
if not app then
print("Unable to find app: " .. _row[1])
end
end
-- Find the destination display, if wanted
if _row[3] then
local displays = fnutils.filter(screen.allscreens(), function(screen) return screen:name() == _row[3] end)
if displays then
-- TODO: This is bogus, multiple identical monitors will be impossible to lay out
display = displays[1]
end
if not display then
print("Unable to find display: " .. _row[3])
else
displaypoint = geometry.point(display:frame().x, display:frame().y)
end
end
-- Find the matching windows, if any
if _row[2] then
if app then
wins = fnutils.filter(app:allwindows(), function(win) return win:title() == _row[2] end)
else
wins = fnutils.filter(window:allwindows(), function(win) return win:title() == _row[2] end)
end
elseif app then
wins = app:allwindows()
end
-- Apply the display/frame positions requested, if any
if not wins then
print(_row[1],_row[2])
print("No windows matched, skipping.")
else
for m,_win in pairs(wins) do
local winframe = nil
local screenrect = nil
-- Move window to destination display, if wanted
if display then
_win:settopleft(displaypoint)
end
-- Apply supplied position, if any
if unit then
_win:movetounit(unit)
elseif frame then
winframe = frame
screenrect = _win:screen():frame()
elseif fullframe then
winframe = fullframe
screenrect = _win:screen():fullframe()
end
if winframe then
if winframe.x < 0 or winframe.y < 0 then
if winframe.x < 0 then
winframe.x = screenrect.w + winframe.x
end
if winframe.y < 0 then
winframe.y = screenrect.h + winframe.y
end
end
_win:setframe(winframe)
end
end
end
end
end
-- Hotkeys to move windows between screens
hotkey.bind(hyper, 'Left', move_window_one_screen_west)
hotkey.bind(hyper, 'Right', move_window_one_screen_east)
-- Hotkeys to resize windows absolutely
hotkey.bind(hyper, 'a', function() window.focusedwindow():movetounit(left30) end)
hotkey.bind(hyper, 's', function() window.focusedwindow():movetounit(right70) end)
hotkey.bind(hyper, '[', function() window.focusedwindow():movetounit(left50) end)
hotkey.bind(hyper, ']', function() window.focusedwindow():movetounit(right50) end)
hotkey.bind(hyper, 'f', function() window.focusedwindow():maximize() end)
--0.9.5 hotkey.bind(hyper, 'r', function() window.focusedwindow():toggle_fullscreen() end)
hotkey.bind(hyper, 'r', toggle_fullscreen)
-- Hotkeys to trigger defined layouts
hotkey.bind(hyper, '1', function() apply_layout(internal_display) end)
hotkey.bind(hyper, '2', function() apply_layout(dual_display) end)
-- Misc hotkeys
hotkey.bind(hyper, 'y', toggle_console)
hotkey.bind(hyper, 'n', function() os.execute("open ~") end)
hotkey.bind(hyper, 'c', toggle_display_sleep)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment