diff --git a/hammerspoon/Makefile b/hammerspoon/Makefile new file mode 100644 index 0000000..b0a1103 --- /dev/null +++ b/hammerspoon/Makefile @@ -0,0 +1,22 @@ +.SILENT: + +ext/grid.lua: + echo "fetching ext/grid.lua..." && \ + curl -s -L -o ext/grid.lua \ + https://raw.githubusercontent.com/Hammerspoon/hammerspoon/master/extensions/grid/init.lua && \ + make patch_ext/grid.lua + +.PHONY: patch_ext/grid.lua +patch_ext/grid.lua: + echo "patching ext/grid.lua..." && \ + patch -p0 < ext/grid.patch + +.PHONY: remove_ext/grid.lua +remove_ext/grid.lua: + ( \ + test -f "ext/grid.lua" && rm "ext/grid.lua" && \ + echo "removed ext/grid.lua" \ + ) || exit 0 + +.PHONY: update_ext/grid.lua +update_ext/grid.lua: remove_ext/grid.lua ext/grid.lua diff --git a/hammerspoon/ext/grid.lua b/hammerspoon/ext/grid.lua new file mode 100644 index 0000000..4541f01 --- /dev/null +++ b/hammerspoon/ext/grid.lua @@ -0,0 +1,1163 @@ +--- === hs.grid === +--- +--- Move/resize windows within a grid +--- +--- The grid partitions your screens for the purposes of window management. The default layout of the grid is 3 columns by 3 rows. +--- You can specify different grid layouts for different screens and/or screen resolutions. +--- +--- Windows that are aligned with the grid have their location and size described as a `cell`. Each cell is an `hs.geometry` rect with these fields: +--- * x - The column of the left edge of the window +--- * y - The row of the top edge of the window +--- * w - The number of columns the window occupies +--- * h - The number of rows the window occupies +--- +--- For a grid of 3x3: +--- * a cell `'0,0 1x1'` will be in the upper-left corner +--- * a cell `'2,0 1x1'` will be in the upper-right corner +--- * and so on... +--- +--- Additionally, a modal keyboard driven interface for interactive resizing is provided via `hs.grid.show()`; +--- The grid will be overlaid on the focused or frontmost window's screen with keyboard hints. +--- To resize/move the window, you can select the corner cells of the desired position. +--- For a move-only, you can select a cell and confirm with 'return'. The celected cell will become the new upper-left of the window. +--- You can also use the arrow keys to move the window onto adjacent screens, and the tab/shift-tab keys to cycle to the next/previous window. +--- Once you selected a cell, you can use the arrow keys to navigate through the grid. In this case, the grid will highlight the selected cells. +--- After highlighting enough cells, press enter to move/resize the window to the highlighted area. + +local window = require "hs.window" +local screen = require 'hs.screen' +local drawing = require'hs.drawing' +local geom = require'hs.geometry' +local timer = require'hs.timer' +local newmodal = require'hs.hotkey'.modal.new +local log = require'hs.logger'.new('grid') + +local ipairs,pairs,min,max,floor,fmod = ipairs,pairs,math.min,math.max,math.floor,math.fmod +local sformat,smatch,ssub,ulen,type,tonumber,tostring = string.format,string.match,string.sub,utf8.len,type,tonumber,tostring +local tinsert,tpack=table.insert,table.pack +local setmetatable,rawget,rawset=setmetatable,rawget,rawset + + +local gridSizes = {[true]=geom'3x3'} -- user-defined grid sizes for each screen or geometry, default ([true]) is 3x3 +local gridFrames= {} -- user-defined grid frames; always defaults to the screen:frame() +local margins = geom'5x5' + +local grid = {setLogLevel=log.setLogLevel,getLogLevel=log.getLogLevel} -- module + + +--- hs.grid.setGrid(grid,screen,frame) -> hs.grid +--- Function +--- Sets the grid size for a given screen or screen resolution +--- +--- Parameters: +--- * grid - an `hs.geometry` size, or argument to construct one, indicating the number of columns and rows for the grid +--- * screen - an `hs.screen` object, or a valid argument to `hs.screen.find()`, indicating the screen(s) to apply the grid to; +--- if omitted or nil, sets the default grid, which is used when no specific grid is found for any given screen/resolution +--- * frame - an `hs.geometry` rect object indicating the frame that the grid will occupy for the given screen; +--- if omitted or nil, the screen's `:frame()` will be used; use this argument if you want e.g. to leave +--- a strip of the desktop unoccluded when using GeekTool or similar. The `screen` argument *must* be non-nil when setting a +--- custom grid frame. +--- +--- Returns: +--- * the `hs.grid` module for method chaining +--- +--- Usage: +--- hs.grid.setGrid('5x3','Color LCD') -- sets the grid to 5x3 for any screen named "Color LCD" +--- hs.grid.setGrid('8x5','1920x1080') -- sets the grid to 8x5 for all screens with a 1920x1080 resolution +--- hs.grid.setGrid'4x4' -- sets the default grid to 4x4 + +local deleteUI +local function getScreenParam(scr) + if scr==nil then return true end + if getmetatable(scr)==hs.getObjectMetatable'hs.screen' then scr=scr:id() end + if type(scr)=='string' or type(scr)=='table' then + local ok,res=pcall(geom.new,scr) + if ok then scr=res.string end + end + if type(scr)~='string' and type(scr)~='number' then error('invalid screen or geometry',3) end + return scr +end +function grid.setGrid(gr,scr,frame) + gr=geom.new(gr) + if geom.type(gr)~='size' then error('invalid grid',2) end + scr=getScreenParam(scr) + gr.w=min(gr.w,100) gr.h=min(gr.h,100) -- cap grid to 100x100, just in case + gridSizes[scr]=gr + if frame~=nil then + frame=geom.new(frame) + if geom.type(frame)~='rect' then error('invalid frame',2) end + if scr==true then error('can only set the grid frame for a specific screen',2) end + gridFrames[scr]=frame + end + if scr==true then log.f('default grid set to %s',gr.string) + else log.f('grid for %s set to %s',scr,gr.string) end + deleteUI() + return grid +end + +--- hs.grid.setMargins(margins) -> hs.grid +--- Function +--- Sets the margins between windows +--- +--- Parameters: +--- * margins - an `hs.geometry` point or size, or argument to construct one, indicating the desired margins between windows in screen points +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.setMargins(mar) + mar=geom.new(mar) + if geom.type(mar)=='point' then mar=geom.size(mar.x,mar.y) end + if geom.type(mar)~='size' then error('invalid margins',2)end + margins=mar + log.f('window margins set to %s',margins.string) + return grid +end + + +--- hs.grid.getGrid(screen) -> hs.geometry size +--- Function +--- Gets the defined grid size for a given screen or screen resolution +--- +--- Parameters: +--- * screen - an `hs.screen` object, or a valid argument to `hs.screen.find()`, indicating the screen to get the grid of; +--- if omitted or nil, gets the default grid, which is used when no specific grid is found for any given screen/resolution +--- +--- Returns: +--- * an `hs.geometry` size object indicating the number of columns and rows in the grid +--- +--- Notes: +--- * if a grid was not set for the specified screen or geometry, the default grid will be returned +--- +--- Usage: +--- local mygrid = hs.grid.getGrid('1920x1080') -- gets the defined grid for all screens with a 1920x1080 resolution +--- local defgrid=hs.grid.getGrid() defgrid.w=defgrid.w+2 -- increases the number of columns in the default grid by 2 + +-- interestingly, that last example above can be used to defeat the 100x100 cap + +local function getGrid(screenObject) + if not screenObject then return gridSizes[true] end + local id=screenObject:id() + for k,gridsize in pairs(gridSizes) do + if k~=true then + local screens=tpack(screen.find(k)) + for _,s in ipairs(screens) do if s:id()==id then return gridsize end end + end + end + return gridSizes[true] +end +function grid.getGrid(scr) + scr=getScreenParam(scr) + if gridSizes[scr] then return gridSizes[scr] end + return getGrid(screen.find(scr)) +end + +--- hs.grid.getGridFrame(screen) -> hs.geometry rect +--- Function +--- Gets the defined grid frame for a given screen or screen resolution. +--- +--- Parameters: +--- * screen - an `hs.screen` object, or a valid argument to `hs.screen.find()`, indicating the screen to get the grid frame of +--- +--- Returns: +--- * an `hs.geometry` rect object indicating the frame used by the grid for the given screen; if no custom frame +--- was given via `hs.grid.setGrid()`, returns the screen's frame +local function getGridFrame(screenObject) + if not screenObject then error('cannot find screen',2) end + local id=screenObject:id() + local screenFrame=screenObject:fullFrame() + for k,gridframe in pairs(gridFrames) do + local screens=tpack(screen.find(k)) + for _,s in ipairs(screens) do if s:id()==id then + local f=screenFrame:intersect(gridframe) + if f.area==0 then + error(sformat('invalid grid frame %s defined for "%s" (screen %s has frame %s)',gridframe.string,tostring(k),screenObject:name(),screenFrame.string),2) + end + return f + end end + end + return screenObject:frame() +end +function grid.getGridFrame(scr) + scr=getScreenParam(scr) + if scr==true then error('must specify a screen',2) end + if gridFrames[scr] then return gridFrames[scr] end + return getGridFrame(screen.find(scr)) +end + +--- hs.grid.show([exitedCallback][, multipleWindows]) +--- Function +--- Shows the grid and starts the modal interactive resizing process for the focused or frontmost window. +--- In most cases this function should be invoked via `hs.hotkey.bind` with some keyboard shortcut. +--- +--- Parameters: +--- * exitedCallback - (optional) a function that will be called after the user dismisses the modal interface +--- * multipleWindows - (optional) if `true`, the resizing grid won't automatically go away after selecting the desired cells +--- for the frontmost window; instead, it'll switch to the next window +--- +--- Returns: +--- * None +--- +--- Notes: +--- * In the modal interface, press the arrow keys to jump to adjacent screens; spacebar to maximize/unmaximize; esc to quit without any effect +--- * Pressing `tab` or `shift-tab` in the modal interface will cycle to the next or previous window; if `multipleWindows` +--- is false or omitted, the first press will just enable the multiple windows behaviour +--- * The keyboard hints assume a QWERTY layout; if you use a different layout, change `hs.grid.HINTS` accordingly + +--- hs.grid.hide() +--- Function +--- Hides the grid, if visible, and exits the modal resizing mode. +--- Call this function if you need to make sure the modal is exited without waiting for the user to press `esc`. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +--- +--- Notes: +--- * If an exit callback was provided when invoking the modal interface, calling `.hide()` will call it + +--- hs.grid.toggleShow([exitedCallback][, multipleWindows]) +--- Function +--- Toggles the grid and modal resizing mode - see `hs.grid.show()` and `hs.grid.hide()` +--- +--- Parameters: see `hs.grid.show()` +--- +--- Returns: +--- * None + +local function getCellSize(screen) + local grid=getGrid(screen) + local screenframe=getGridFrame(screen) + return geom.size(screenframe.w/grid.w,screenframe.h/grid.h) +end + +local function round(num, idp) + local mult = 10^(idp or 0) + return floor(num * mult + 0.5) / mult +end + +--- hs.grid.get(win) -> cell +--- Function +--- Gets the cell describing a window +--- +--- Parameters: +--- * an `hs.window` object to get the cell of +--- +--- Returns: +--- * a cell object (i.e. an `hs.geometry` rect), or nil if an error occurred +function grid.get(win) + local winframe = win:frame() + local winscreen = win:screen() + if not winscreen then log.e('Cannot get the window\'s screen') return end + local screenframe = getGridFrame(winscreen) + local cellsize = getCellSize(winscreen) + return geom{ + x = round((winframe.x - screenframe.x) / cellsize.w), + y = round((winframe.y - screenframe.y) / cellsize.h), + w = max(1, round(winframe.w / cellsize.w)), + h = max(1, round(winframe.h / cellsize.h)), + } +end + +--- hs.grid.getCell(cell, screen) -> hs.geometry +--- Function +--- Gets the `hs.geometry` rect for a cell on a particular screen +--- +--- Parameters: +--- * cell - a cell object, i.e. an `hs.geometry` rect or argument to construct one +--- * screen - an `hs.screen` object or argument to `hs.screen.find()` where the cell is located +--- +--- Returns: +--- * the `hs.geometry` rect for a cell on a particular screen or nil if the screen isn't found +function grid.getCell(cell, scr) + scr=screen.find(scr) + if not scr then log.e('screen cannot be nil') return end + cell=geom.new(cell) + local screenrect = scr:frame() + local screengrid = getGrid(scr) + -- sanitize, because why not + cell.x=max(0,min(cell.x,screengrid.w-1)) cell.y=max(0,min(cell.y,screengrid.h-1)) + cell.w=max(1,min(cell.w,screengrid.w-cell.x)) cell.h=max(1,min(cell.h,screengrid.h-cell.y)) + local cellw, cellh = screenrect.w/screengrid.w, screenrect.h/screengrid.h + local newframe = { + x = (cell.x * cellw) + screenrect.x, + y = (cell.y * cellh) + screenrect.y, + w = cell.w * cellw, + h = cell.h * cellh, + } + return newframe +end + +--- hs.grid.set(win, cell, screen) -> hs.grid +--- Function +--- Sets the cell for a window on a particular screen +--- +--- Parameters: +--- * win - an `hs.window` object representing the window to operate on +--- * cell - a cell object, i.e. an `hs.geometry` rect or argument to construct one, to apply to the window +--- * screen - (optional) an `hs.screen` object or argument to `hs.screen.find()` representing the screen to place the window on; if omitted +--- the window's current screen will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.set(win, cell, scr) + if not win then error('win cannot be nil',2) end + scr=screen.find(scr) + if not scr then scr=win:screen() end + if not scr then log.e('Cannot get the window\'s screen') return grid end + cell=geom.new(cell) + local screenrect = getGridFrame(scr) + local screengrid = getGrid(scr) + -- sanitize, because why not + cell.x=max(0,min(cell.x,screengrid.w-1)) cell.y=max(0,min(cell.y,screengrid.h-1)) + cell.w=max(1,min(cell.w,screengrid.w-cell.x)) cell.h=max(1,min(cell.h,screengrid.h-cell.y)) + local cellw, cellh = screenrect.w/screengrid.w, screenrect.h/screengrid.h + local newframe = { + x = (cell.x * cellw) + screenrect.x + margins.w, + y = (cell.y * cellh) + screenrect.y + margins.h, + w = cell.w * cellw - (margins.w * 2), + h = cell.h * cellh - (margins.h * 2), + } + + -- calculate proper margins - this fixes doubled margins betweeen windows + -- shamelessly borrowed from: + -- https://github.com/szymonkaliski/Dotfiles/blob/8f2bbf973fa31e001e183de03b65fae408877f00/Dotfiles/hammerspoon/overrides.lua#L79-L106 + if cell.h < screengrid.h and cell.h % 1 == 0 then + if cell.y ~= 0 then + newframe.h = newframe.h + margins.h / 2 + newframe.y = newframe.y - margins.h / 2 + end + + if cell.y + cell.h ~= screengrid.h then + newframe.h = newframe.h + margins.h / 2 + end + end + + if cell.w < screengrid.w and cell.w % 1 == 0 then + if cell.x ~= 0 then + newframe.w = newframe.w + margins.w / 2 + newframe.x = newframe.x - margins.w / 2 + end + + if cell.x + cell.w ~= screengrid.w then + newframe.w = newframe.w + margins.w / 2 + end + end + + win:setFrameInScreenBounds(newframe) --TODO check this (against screen bottom stickiness) + return grid +end + +--- hs.grid.snap(win) -> hs.grid +--- Function +--- Snaps a window into alignment with the nearest grid lines +--- +--- Parameters: +--- * win - an `hs.window` object to snap +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.snap(win) + if win:isStandard() then + local cell = grid.get(win) + if cell then grid.set(win, cell) + else log.e('Cannot get the window\'s cell') end + else log.e('Cannot snap nonstandard window') end + return grid +end + + +--- hs.grid.adjustWindow(fn, window) -> hs.grid +--- Function +--- Calls a user specified function to adjust a window's cell +--- +--- Parameters: +--- * fn - a function that accepts a cell object as its only argument. The function should modify it as needed and return nothing +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.adjustWindow(fn,win) + if not win then win = window.frontmostWindow() end + if not win then log.w('Cannot get frontmost window') return grid end + local f = grid.get(win) + if not f then log.e('Cannot get window cell') return grid end + fn(f) + return grid.set(win, f) +end + +grid.adjustFocusedWindow=grid.adjustWindow + +local function checkWindow(win) + if not win then win = window.frontmostWindow() end + if not win then log.w('Cannot get frontmost window') return end + if not win:screen() then log.w('Cannot get the window\'s screen') return end + return win +end + +--- hs.grid.maximizeWindow(window) -> hs.grid +--- Function +--- Moves and resizes a window to fill the entire grid +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.maximizeWindow(win) + win=checkWindow(win) if not win then return grid end + local winscreen = win:screen() + local screengrid = getGrid(winscreen) + return grid.set(win, {0,0,screengrid.w,screengrid.h}, winscreen) +end + +-- deprecate these two, :next() and :previous() screens are useless anyway due to random order +function grid.pushWindowNextScreen(win) + win=checkWindow(win) if not win then return grid end + local winscreen=win:screen() + win:moveToScreen(winscreen:next()) + return grid.snap(win) +end +function grid.pushWindowPrevScreen(win) + win=checkWindow(win) if not win then return grid end + local winscreen=win:screen() + win:moveToScreen(winscreen:previous()) + return grid.snap(win) +end + +--- hs.grid.pushWindowLeft(window) -> hs.grid +--- Function +--- Moves a window one grid cell to the left, or onto the adjacent screen's grid when necessary +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.pushWindowLeft(win) + win=checkWindow(win) if not win then return grid end + local winscreen = win:screen() + local cell = grid.get(win) + if cell.x<=0 then + -- go to left screen + local frame=win:frame() + local newscreen=winscreen:toWest(frame) + if not newscreen then return grid end + frame.x = frame.x-frame.w + win:setFrameInScreenBounds(frame) + return grid.snap(win) + else return grid.adjustWindow(function(f)f.x=f.x-1 end, win) end +end + +--- hs.grid.pushWindowRight(window) -> hs.grid +--- Function +--- Moves a window one cell to the right, or onto the adjacent screen's grid when necessary +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.pushWindowRight(win) + win=checkWindow(win) if not win then return grid end + local winscreen = win:screen() + local screengrid = getGrid(winscreen) + local cell = grid.get(win) + if cell.x+cell.w>=screengrid.w then + -- go to right screen + local frame=win:frame() + local newscreen=winscreen:toEast(frame) + if not newscreen then return grid end + frame.x = frame.x+frame.w + win:setFrameInScreenBounds(frame) + return grid.snap(win) + else return grid.adjustWindow(function(f)f.x=f.x+1 end, win) end +end + +--- hs.grid.resizeWindowWider(window) -> hs.grid +--- Function +--- Resizes a window to be one cell wider +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +--- +--- Notes: +--- * if the window hits the right edge of the screen and is asked to become wider, its left edge will shift further left +function grid.resizeWindowWider(win) + win=checkWindow(win) if not win then return grid end + local screengrid = getGrid(win:screen()) + return grid.adjustWindow(function(f) + if f.w + f.x >= screengrid.w and f.x > 0 then + f.x = f.x - 1 + end + f.w = min(f.w + 1, screengrid.w - f.x) + end, win) +end + +--- hs.grid.resizeWindowThinner(window) -> hs.grid +--- Function +--- Resizes a window to be one cell thinner +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.resizeWindowThinner(win) + return grid.adjustWindow(function(f) f.w = max(f.w - 1, 1) end, win) +end + +--- hs.grid.pushWindowDown(window) -> hs.grid +--- Function +--- Moves a window one grid cell down the screen, or onto the adjacent screen's grid when necessary +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.pushWindowDown(win) + win=checkWindow(win) if not win then return grid end + local winscreen = win:screen() + local screengrid = getGrid(winscreen) + local cell = grid.get(win) + if cell.y+cell.h>=screengrid.h then + -- go to screen below + local frame=win:frame() + local newscreen=winscreen:toSouth(frame) + if not newscreen then return grid end + frame.y = frame.y+frame.h + win:setFrameInScreenBounds(frame) + return grid.snap(win) + else return grid.adjustWindow(function(f)f.y=f.y+1 end, win) end +end + +--- hs.grid.pushWindowUp(window) -> hs.grid +--- Function +--- Moves a window one grid cell up the screen, or onto the adjacent screen's grid when necessary +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.pushWindowUp(win) + win=checkWindow(win) if not win then return grid end + local winscreen = win:screen() + local cell = grid.get(win) + if cell.y<=0 then + -- go to screen above + local frame=win:frame() + local newscreen=winscreen:toNorth(frame) + if not newscreen then return grid end + frame.y = frame.y-frame.h + win:setFrameInScreenBounds(frame) + return grid.snap(win) + else return grid.adjustWindow(function(f)f.y=f.y-1 end, win) end +end + +--- hs.grid.resizeWindowShorter(window) -> hs.grid +--- Function +--- Resizes a window so its bottom edge moves one grid cell higher +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +function grid.resizeWindowShorter(win) + return grid.adjustWindow(function(f) f.y = f.y - 0; f.h = max(f.h - 1, 1) end, win) +end + +--- hs.grid.resizeWindowTaller(window) -> hs.grid +--- Function +--- Resizes a window so its bottom edge moves one grid cell lower +--- +--- Parameters: +--- * window - an `hs.window` object to act on; if omitted, the focused or frontmost window will be used +--- +--- Returns: +--- * the `hs.grid` module for method chaining +--- +--- Notes: +--- * if the window hits the bottom edge of the screen and is asked to become taller, its top edge will shift further up +function grid.resizeWindowTaller(win) + win=checkWindow(win) if not win then return grid end + local screengrid = getGrid(win:screen()) + return grid.adjustWindow(function(f) + if f.y + f.h >= screengrid.h and f.y > 0 then + f.y = f.y -1 + end + f.h = min(f.h + 1, screengrid.h - f.y) + end, win) +end + + +--- hs.grid.HINTS +--- Variable +--- A bidimensional array (table of tables of strings) holding the keyboard hints (as per `hs.keycodes.map`) to be used for the interactive resizing interface. +--- Change this if you don't use a QWERTY layout; you need to provide 5 valid rows of hints (even if you're not going to use all 5 rows) +--- +--- Notes: +--- * `hs.inspect(hs.grid.HINTS)` from the console will show you how the table is built + + +-- modal grid stuff below + +grid.HINTS={{'f1','f2','f3','f4','f5','f6','f7','f8','f9','f10'}, + {'1','2','3','4','5','6','7','8','9','0'}, + {'Q','W','E','R','T','Y','U','I','O','P'}, + {'A','S','D','F','G','H','J','K','L',';'}, + {'Z','X','C','V','B','N','M',',','.','/'} +} + +local _HINTROWS,_HINTS = {{4},{3,4},{3,4,5},{2,3,4,5},{1,2,3,4,5},{1,2,3,9,4,5},{1,2,8,3,9,4,5},{1,2,8,3,9,4,10,5},{1,7,2,8,3,9,4,10,5},{1,6,2,7,3,8,9,4,10,5}} +-- 10x10 grid should be enough for anybody + +local function getColor(t) + if t.red then return t + else return {red=t[1] or 0,green=t[2] or 0,blue=t[3] or 0,alpha=t[4] or 1} end +end + +--- hs.grid.ui +--- Variable +--- Allows customization of the modal resizing grid user interface +--- +--- This table contains variables that you can change to customize the look of the modal resizing grid. +--- The default values are shown in the right hand side of the assignements below. +--- +--- To represent color values, you can use: +--- * a table {red=redN, green=greenN, blue=blueN, alpha=alphaN} +--- * a table {redN,greenN,blueN[,alphaN]} - if omitted alphaN defaults to 1.0 +--- where redN, greenN etc. are the desired value for the color component between 0.0 and 1.0 +--- +--- The following variables must be color values: +--- * `hs.grid.ui.textColor = {1,1,1}` +--- * `hs.grid.ui.cellColor = {0,0,0,0.25}` +--- * `hs.grid.ui.cellStrokeColor = {0,0,0}` +--- * `hs.grid.ui.selectedColor = {0.2,0.7,0,0.4}` -- for the first selected cell during a modal resize +--- * `hs.grid.ui.highlightColor = {0.8,0.8,0,0.5}` -- to highlight the frontmost window behind the grid +--- * `hs.grid.ui.highlightStrokeColor = {0.8,0.8,0,1}` +--- * `hs.grid.ui.cyclingHighlightColor = {0,0.8,0.8,0.5}` -- to highlight the window to be resized, when cycling among windows +--- * `hs.grid.ui.cyclingHighlightStrokeColor = {0,0.8,0.8,1}` +--- +--- The following variables must be numbers (in screen points): +--- * `hs.grid.ui.textSize = 200` +--- * `hs.grid.ui.cellStrokeWidth = 5` +--- * `hs.grid.ui.highlightStrokeWidth = 30` +--- +--- The following variables must be strings: +--- * `hs.grid.ui.fontName = 'Lucida Grande'` +--- +--- The following variables must be booleans: +--- * `hs.grid.ui.showExtraKeys = true` -- show non-grid keybindings in the center of the grid +local ui = { + textColor={1,1,1}, + textSize=200, + cellStrokeColor={0,0,0}, + cellStrokeWidth=5, + cellColor={0,0,0,0.25}, + highlightColor={0.8,0.8,0,0.5}, + highlightStrokeColor={0.8,0.8,0,1}, + cyclingHighlightColor={0,0.8,0.8,0.5}, + cyclingHighlightStrokeColor={0,0.8,0.8,1}, + highlightStrokeWidth=30, + selectedColor={0.2,0.7,0,0.4}, + showExtraKeys=true, + fontName='Lucida Grande' +} + +local uielements -- drawing objects +local resizing -- modal "hotkey" + +deleteUI=function() + if not uielements then return end + for _,s in pairs(uielements) do + s.howto.rect:delete() s.howto.text:delete() + for _,e in pairs(s.hints) do + e.rect:delete() e.text:delete() + end + end + uielements = nil + _HINTS=nil +end + +grid.ui=setmetatable({},{__newindex=function(t,k,v) ui[k]=v deleteUI()end,__index=ui}) +local function makeHints() -- quick hack to double up rows (for portrait screens mostly) + if _HINTS then return end + _HINTS={} + local rows=#grid.HINTS + for i,v in ipairs(grid.HINTS) do _HINTS[i]=v _HINTS[i+rows]={} end -- double up the hints + for y=1,rows do + for x,h in ipairs(_HINTS[y]) do + _HINTS[y+rows][x] = '⇧'.._HINTS[y][x] -- add shift + end + end +end + +local function makeUI() + local ts,tsh=ui.textSize,ui.textSize*0.5 + deleteUI() + makeHints() + uielements = {} + local screens = screen.allScreens() + local function dist(i,w1,w2) return round((i-1)/w1*w2)+1 end + for i,screen in ipairs(screens) do + local sgr = getGrid(screen) + local cell = getCellSize(screen) + local frame = getGridFrame(screen) + log.f('Screen #%d %s (%s) -> grid %s (%s cells)',i,screen:name(),frame.size.string,sgr.string,cell:floor().string) + local htf = {w=550,h=150} + htf.x = frame.x+frame.w/2-htf.w/2 htf.y = frame.y+frame.h/2-htf.h/3*2 + if fmod(sgr.h,2)==1 then htf.y=htf.y-cell.h/2 end + local howtorect = drawing.rectangle(htf) + howtorect:setFill(true) howtorect:setFillColor(getColor(ui.cellColor)) howtorect:setStrokeWidth(ui.cellStrokeWidth) + local howtotext=drawing.text(htf,' ←→↑↓:select screen\n ⇥:next win ⇧⇥:prev win\n space:fullscreen esc:exit') + howtotext:setTextSize(40) howtotext:setTextColor(getColor(ui.textColor)) + howtotext:setTextFont(ui.fontName) + local sid=screen:id() + uielements[sid] = {left=(screen:toWest() or screen):id(), + up=(screen:toNorth() or screen):id(), + right=(screen:toEast() or screen):id(), + down=(screen:toSouth() or screen):id(), + screen=screen, frame=frame, + howto={rect=howtorect,text=howtotext}, + hints={}} + -- create the ui for cells + local hintsw,hintsh = #_HINTS[1],#_HINTS + for hx=min(hintsw,sgr.w),1,-1 do + local cx,cx2 = hx,hx+1 + -- allow for grid width > # available hint columns + if sgr.w>hintsw then cx=dist(cx,hintsw,sgr.w) cx2=dist(cx2,hintsw,sgr.w) end + local x,x2 = frame.x+cell.w*(cx-1),frame.x+cell.w*(cx2-1) + for hy=min(hintsh,sgr.h),1,-1 do + local cy,cy2 = hy,hy+1 + -- allow for grid heigth > # available hint rows + if sgr.h>hintsh then cy=dist(cy,hintsh,sgr.h) cy2=dist(cy2,hintsh,sgr.h) end + local y,y2 = frame.y+cell.h*(cy-1),frame.y+cell.h*(cy2-1) + local elem = geom.new{x=x,y=y,x2=x2,y2=y2} + local rect = drawing.rectangle(elem) + rect:setFill(true) rect:setFillColor(getColor(ui.cellColor)) + rect:setStroke(true) rect:setStrokeColor(getColor(ui.cellStrokeColor)) rect:setStrokeWidth(ui.cellStrokeWidth) + elem.rect = rect + elem.hint = _HINTS[_HINTROWS[min(sgr.h,hintsh)][hy]][hx] + local tw=ts*ulen(elem.hint) + local text=drawing.text({x=x+(x2-x)/2-tw/2,y=y+(y2-y)/2-tsh,w=tw,h=ts*1.1},elem.hint) + text:setTextSize(ts) text:setTextFont(ui.fontName) + text:setTextColor(getColor(ui.textColor)) + elem.text=text + log.vf('[%d] %s %.0f,%.0f>%.0f,%.0f',i,elem.hint,elem.x,elem.y,elem.x2,elem.y2) + tinsert(uielements[sid].hints,elem) + end + end + end +end + + +local function showGrid(id) + if not id or not uielements[id] then log.e('Cannot get current screen, aborting') return end + local elems = uielements[id].hints + for _,e in ipairs(elems) do e.rect:show() e.text:show() end + if ui.showExtraKeys then uielements[id].howto.rect:show() uielements[id].howto.text:show() end +end +local function hideGrid(id) + if not id or not uielements or not uielements[id] then --[[log.e('Cannot obtain current screen') --]] return end + uielements[id].howto.rect:hide() uielements[id].howto.text:hide() + local elems = uielements[id].hints + for _,e in pairs(elems) do e.rect:hide() e.text:hide() end +end + +local initialized, showing, currentScreen, exitCallback +local currentWindow, currentWindowIndex, allWindows, cycledWindows, focusedWindow, reorderIndex, cycling, highlight +local function startCycling() + allWindows=window.orderedWindows() cycledWindows={} reorderIndex=1 focusedWindow=currentWindow + local cid=currentWindow:id() + for i,w in ipairs(allWindows) do + if w:id()==cid then currentWindowIndex=i break end + end + --[[focus the desktop so the windows can :raise + local finder=application.find'Finder' + for _,w in ipairs(finder:allWindows()) do + if w:role()=='AXScrollArea' then w:focus() return end + end--]] +end + +local function _start() + if initialized then return end + screen.watcher.new(deleteUI):start() + require'hs.spaces'.watcher.new(grid.hide):start() + resizing=newmodal() + local function showHighlight() + if highlight then highlight:delete() end + highlight = drawing.rectangle(currentWindow:frame()) + highlight:setFill(true) highlight:setFillColor(getColor(cycling and ui.cyclingHighlightColor or ui.highlightColor)) highlight:setStroke(true) + highlight:setStrokeColor(getColor(cycling and ui.cyclingHighlightStrokeColor or ui.highlightStrokeColor)) highlight:setStrokeWidth(ui.highlightStrokeWidth) + highlight:show() + end + function resizing:entered() + if showing then return end + if window.layout._hasActiveInstances then window.layout.pauseAllInstances() end + -- currentWindow = window.frontmostWindow() + if not currentWindow then log.w('Cannot get current window, aborting') resizing:exit() return end + log.df('Start moving %s [%s]',currentWindow:subrole(),currentWindow:application():title()) + if currentWindow:isFullScreen() then currentWindow:setFullScreen(false) --[[resizing:exit()--]] end + -- disallow resizing fullscreen windows as it doesn't really make much sense + -- so fullscreen window gets toggled back first + currentScreen = (currentWindow:screen() or screen.mainScreen()):id() + showHighlight() + if not uielements then makeUI() end + showGrid(currentScreen) + showing = true + end + + -- selectedCorner gives us the corner the user selected first with a hint + -- By this we know if we have to add or insert or remove a column/row to the selectedMatrix + -- 0 = upper-left; 1 = upper-right; 2 = bottom-left; 3 = bottom-right + local selectedCorner = 0 + -- selectedMatrix keeps track of the cells the user navigated to + local selectedMatrix = {{}} + -- dim = {x,y}; x = #columns; y = #rows + local dim = {1,1} + + -- Clear selected cells + local function clearSelection() + if selectedMatrix[1][1] then + for _,row in ipairs(selectedMatrix) do + for _,cell in ipairs(row) do cell.rect:setFillColor(getColor(ui.cellColor)) end + end + end + -- reset all matrix values + selectedCorner = 0 + selectedMatrix = {{}} + dim = {1,1} + end + + function resizing:exited() + if not showing then return true end + if highlight then highlight:delete() highlight=nil end + clearSelection() + if cycling and #allWindows>0 then + -- will STILL somewhat mess up window order, because orderedWindows~=most recently focused windows; but oh well + for i=reorderIndex,1,-1 do if cycledWindows[i] then allWindows[i]:focus() timer.usleep(80000) end end + if focusedWindow then focusedWindow:focus() end + end + hideGrid(currentScreen) + showing = nil + if window.layout._hasActiveInstances then window.layout.resumeAllInstances() end + if type(exitCallback)=='function' then return exitCallback() end + end + local function cycle(d) + if not cycling then cycling=true startCycling() currentWindowIndex=currentWindowIndex-d end + clearSelection() hideGrid(currentScreen) + local startIndex=currentWindowIndex + repeat + currentWindowIndex=(currentWindowIndex+d) % #allWindows + if currentWindowIndex==0 then currentWindowIndex=#allWindows end + currentWindow = allWindows[currentWindowIndex] + until currentWindowIndex==startIndex or currentWindow:subrole()=='AXStandardWindow' + reorderIndex=max(reorderIndex,currentWindowIndex) + currentWindow:focus() + cycledWindows[currentWindowIndex]=true + currentScreen=(currentWindow:screen() or screen.mainScreen()):id() + showHighlight() + showGrid(currentScreen) + end + + -- gets the neighbour cell in a certain direction + local function getNeighbour(elem, dir) + -- neighbour can perfectly be found by simple geom calculation + local nx,ny -- x and y values of the neighbour cell + if (dir == 'right') then + nx = elem.x + elem.w + ny = elem.y + elseif (dir == 'left') then + nx = elem.x - elem.w + ny = elem.y + elseif (dir == 'up') then + nx = elem.x + ny = elem.y - elem.h + elseif (dir == 'down') then + nx = elem.x + ny = elem.y + elem.h + end + for _,cell in ipairs(uielements[currentScreen].hints) do + if (nx == cell.x and ny == cell.y) then return cell end + end + -- no cell found, you'r going out of your screen! + return nil + end + + -- key bindings, events at certain non-hint key presses + resizing:bind({},'tab',function()cycle(1)end) + resizing:bind({'shift'},'tab',function()cycle(-1)end) + resizing:bind({},'delete',clearSelection) + resizing:bind({},'escape',function()log.d('abort move')resizing:exit()end) + resizing:bind({},'return',function() + if not selectedMatrix[1][1] then return + -- move and resize to highlighted cells + elseif dim[1] > 1 or dim[2] > 1 then + local x1,x2,y1,y2 + local selectedElem = selectedMatrix[1][1] + local elem = selectedMatrix[dim[2]][dim[1]] + x1,x2 = min(selectedElem.x,elem.x)+margins.w,max(selectedElem.x,elem.x)-margins.h + y1,y2 = min(selectedElem.y,elem.y)+margins.w,max(selectedElem.y,elem.y)-margins.h + local frame={x=x1,y=y1,w=x2-x1+elem.w,h=y2-y1+elem.h} + currentWindow:setFrameInScreenBounds(frame) + log.f('move to %.0f,%.0f[%.0fx%.0f] by navigation',frame.x,frame.y,frame.w,frame.h) + clearSelection() + if cycling then cycle(1) else resizing:exit() end + -- one element selected, do a pure move + else + local selectedElem = selectedMatrix[1][1] + local x1,y1 = selectedElem.x+margins.w,selectedElem.y+margins.w + currentWindow:setFrame(geom({x1, y1}, currentWindow:size())) + clearSelection() + if cycling then cycle(1) else resizing:exit() end + end + end) + resizing:bind({},'space',function() + -- local wasfs=currentWindow:isFullScreen() + log.d('toggle fullscreen')currentWindow:toggleFullScreen() + if currentWindow:isFullScreen() then resizing:exit() + -- elseif not wasfs then currentWindow:setFrame(currentWindow:screen():frame(),0) resizing:exit() + end + end) + for _,dir in ipairs({'left','right','up','down'}) do + resizing:bind({},dir,function() + if not selectedMatrix[1][1] then + -- arrows are in screen selecting mode + log.d('select screen '..dir) + clearSelection() hideGrid(currentScreen) + currentScreen=uielements[currentScreen][dir] + currentWindow:moveToScreen(uielements[currentScreen].screen,0) + showHighlight() + showGrid(currentScreen) + else + -- once one cell is selected, the arrows will navigate to other cells + -- check for transition of position of the first selected cell in the matrix + if dim[2] == 1 then + -- checks for only one cell in selectedMatrix; dim == {1,1} + if dim[1] == 1 and dir == 'left' then + selectedCorner = 1 + elseif dim[1] == 1 and dir == 'right' then + selectedCorner = 0 + elseif dim[1] == 1 and dir == 'down' then + selectedCorner = 0 + elseif dim[1] == 1 and dir == 'up' then + selectedCorner = 2 + -- multiple cells in the matrix + elseif ( selectedCorner == 0 or selectedCorner == 2 ) and dir == 'up' then + selectedCorner = 2 + elseif ( selectedCorner == 1 or selectedCorner == 3 ) and dir == 'up' then + selectedCorner = 3 + elseif ( selectedCorner == 0 or selectedCorner == 2 ) and dir == 'down' then + selectedCorner = 0 + elseif ( selectedCorner == 1 or selectedCorner == 3 ) and dir == 'down' then + selectedCorner = 1 + end + elseif dim[1] == 1 then + if ( selectedCorner == 0 or selectedCorner == 1 ) and dir == 'right' then + selectedCorner = 0 + elseif ( selectedCorner == 2 or selectedCorner == 3 ) and dir == 'right' then + selectedCorner = 2 + elseif ( selectedCorner == 0 or selectedCorner == 1 ) and dir == 'left' then + selectedCorner = 1 + elseif ( selectedCorner == 2 or selectedCorner == 3 ) and dir == 'left' then + selectedCorner = 3 + end + end + + -- In case of valid next cell, add them to the matrix and fill the rectangle + if dir == 'right' then + if selectedCorner == 0 or selectedCorner == 2 then + -- add extra column + for i=1,dim[2] do + local lastInRow = selectedMatrix[i][dim[1]] + local newElem = getNeighbour(lastInRow, 'right') + -- getNeighbour() can return nil when you run out of screen + if newElem == nil then return end + -- if valid neighbour, add it to the matrix + selectedMatrix[i][dim[1] + 1] = newElem + -- and color the cell + newElem.rect:setFillColor(getColor(ui.selectedColor)) + end + dim[1] = dim[1] + 1 + else + -- if selectedCorner == 1 or selectedCorner == 2 + -- remove first column, only if more than one column left in matrix! + if dim[1] > 1 then + for i=1,dim[2] do + selectedMatrix[i][1].rect:setFillColor(getColor(ui.cellColor)) + table.remove(selectedMatrix[i], 1) + end + dim[1] = dim[1] - 1 + end + end + + elseif dir == 'left' then + if selectedCorner == 0 or selectedCorner == 2 then + -- remove last column + if dim[1] > 1 then + for i=1,dim[2] do + selectedMatrix[i][dim[1]].rect:setFillColor(getColor(ui.cellColor)) + table.remove(selectedMatrix[i], dim[1]) + end + dim[1] = dim[1] - 1 + end + else + -- insert column + for i=1,dim[2] do + local firstInRow = selectedMatrix[i][1] + local newElem = getNeighbour(firstInRow, 'left') + if newElem == nil then return end + table.insert(selectedMatrix[i], 1, newElem) + newElem.rect:setFillColor(getColor(ui.selectedColor)) + end + dim[1] = dim[1] + 1 + end + + elseif dir == 'down' then + if selectedCorner == 0 or selectedCorner == 1 then + -- add/append row + selectedMatrix[dim[2] + 1] = {} + for i=1,dim[1] do + local lastInColumn = selectedMatrix[dim[2]][i] + local newElem = getNeighbour(lastInColumn, 'down') + if newElem == nil then return end + selectedMatrix[dim[2] + 1][i] = getNeighbour(lastInColumn, 'down') + newElem.rect:setFillColor(getColor(ui.selectedColor)) + end + dim[2] = dim[2] + 1 + else + -- delete first row + if dim[2] > 1 then + for i=1,dim[1] do + selectedMatrix[1][i].rect:setFillColor(getColor(ui.cellColor)) + end + table.remove(selectedMatrix, 1) + dim[2] = dim[2] - 1 + end + end + + elseif dir == 'up' then + if selectedCorner == 0 or selectedCorner == 1 then + -- delete last row + if dim[2] > 1 then + for i=1,dim[1] do + selectedMatrix[dim[2]][i].rect:setFillColor(getColor(ui.cellColor)) + end + table.remove(selectedMatrix, dim[2]) + dim[2] = dim[2] - 1 + end + else + -- insert row + table.insert(selectedMatrix, 1, {}) + for i=1,dim[1] do + local firstInColumn = selectedMatrix[2][i] + local newElem = getNeighbour(firstInColumn, 'up') + if newElem == nil then return end + selectedMatrix[1][i] = newElem + newElem.rect:setFillColor(getColor(ui.selectedColor)) + end + dim[2] = dim[2] + 1 + end + end + end + end) + end + + local function hintPressed(c) + -- find the elem; if there was a way to unbind modals, we'd unbind on screen change, and pass here the elem directly + local elem + for _,hint in ipairs(uielements[currentScreen].hints) do + if hint.hint==c then elem=hint break end + end + -- local elem = fnutils.find(uielements[currentScreen].hints,function(e)return e.hint==c end) + if not elem then return end + if selectedMatrix[1][1] == nil then + selectedMatrix[1][1] = elem + elem.rect:setFillColor(getColor(ui.selectedColor)) + else + local x1,x2,y1,y2 + local selectedElem = selectedMatrix[1][1] + x1,x2 = min(selectedElem.x,elem.x)+margins.w,max(selectedElem.x,elem.x)-margins.h + y1,y2 = min(selectedElem.y,elem.y)+margins.w,max(selectedElem.y,elem.y)-margins.h + local frame={x=x1,y=y1,w=x2-x1+elem.w,h=y2-y1+elem.h} + currentWindow:setFrameInScreenBounds(frame) + log.f('move to %.0f,%.0f[%.0fx%.0f]',frame.x,frame.y,frame.w,frame.h) + clearSelection() + if cycling then cycle(1) else resizing:exit() end + end + end + makeHints() + for _,row in ipairs(_HINTS) do + for _,c in ipairs(row) do + local key,mod=c,'' + if ssub(c,1,3)=='⇧' then key,mod=ssub(c,4),'⇧' end -- re: "quick hack" @makeHints() + resizing:bind(mod,key,function()hintPressed(c) end) + end + end + initialized=true +end + +function grid.show(cb,stay) + if showing then return end + if type(cb)=='boolean' then stay=cb cb=nil end + exitCallback=cb + if not initialized then _start() end + cycling=stay and true or nil + -- there will be some inconsistency when cycling (focusedWindow~=frontmost), but oh well + currentWindowIndex,currentWindow=1,window.frontmostWindow() + if cycling then startCycling() end + -- else resizing:exit() end + resizing:enter() +end + +function grid.hide() + if showing then resizing:exit() end +end + +function grid.toggleShow(cb,stay) + if showing then grid.hide() else grid.show(stay,cb) end +end + + + +-- Legacy stuff below, deprecated +setmetatable(grid,{ + __index = function(t,k) + if k=='GRIDWIDTH' then return gridSizes[true].w + elseif k=='GRIDHEIGHT' then return gridSizes[true].h + elseif k=='MARGINX' then return margins.w + elseif k=='MARGINY' then return margins.h + else return rawget(t,k) end + end, + __newindex = function(t,k,v) + if k=='GRIDWIDTH' then grid.setGrid{w=v,h=gridSizes[true].h} + elseif k=='GRIDHEIGHT' then grid.setGrid{w=gridSizes[true].w,h=v} + elseif k=='MARGINX' then grid.setMargins{v,margins.h} + elseif k=='MARGINY' then grid.setMargins{margins.w,v} + else rawset(t,k,v) end + end, +}) -- metatable for legacy variables + +-- deprecate these too +function grid.adjustNumberOfRows(delta) + grid.GRIDHEIGHT = max(1, grid.GRIDHEIGHT + delta) + require'hs.fnutils'.map(window.visibleWindows(), grid.snap) +end + +function grid.adjustNumberOfColumns(delta) + grid.GRIDWIDTH = max(1, grid.GRIDWIDTH + delta) + require'hs.fnutils'.map(window.visibleWindows(), grid.snap) +end +-- these are now doubly-deprecated :) +grid.adjustHeight = grid.adjustNumberOfRows +grid.adjustWidth = grid.adjustNumberOfColumns + + +return grid diff --git a/hammerspoon/ext/grid.patch b/hammerspoon/ext/grid.patch new file mode 100644 index 0000000..e2c0ca5 --- /dev/null +++ b/hammerspoon/ext/grid.patch @@ -0,0 +1,35 @@ +--- ext/grid.lua 2017-08-05 01:44:03.000000000 +0100 ++++ ext/grid.patched.lua 2017-08-05 01:43:43.000000000 +0100 +@@ -319,6 +319,32 @@ function grid.set(win, cell, scr) + w = cell.w * cellw - (margins.w * 2), + h = cell.h * cellh - (margins.h * 2), + } ++ ++ -- calculate proper margins - this fixes doubled margins betweeen windows ++ -- shamelessly borrowed from: ++ -- https://github.com/szymonkaliski/Dotfiles/blob/8f2bbf973fa31e001e183de03b65fae408877f00/Dotfiles/hammerspoon/overrides.lua#L79-L106 ++ if cell.h < screengrid.h and cell.h % 1 == 0 then ++ if cell.y ~= 0 then ++ newframe.h = newframe.h + margins.h / 2 ++ newframe.y = newframe.y - margins.h / 2 ++ end ++ ++ if cell.y + cell.h ~= screengrid.h then ++ newframe.h = newframe.h + margins.h / 2 ++ end ++ end ++ ++ if cell.w < screengrid.w and cell.w % 1 == 0 then ++ if cell.x ~= 0 then ++ newframe.w = newframe.w + margins.w / 2 ++ newframe.x = newframe.x - margins.w / 2 ++ end ++ ++ if cell.x + cell.w ~= screengrid.w then ++ newframe.w = newframe.w + margins.w / 2 ++ end ++ end ++ + win:setFrameInScreenBounds(newframe) --TODO check this (against screen bottom stickiness) + return grid + end