--- === 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 selected 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,ssub,ulen,type,tostring = string.format,string.sub,utf8.len,type,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
---
--- Examples:
---  * 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.
---
--- 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 most cases this function should be invoked via `hs.hotkey.bind` with some keyboard shortcut.
---  * 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
---  * If grid dimensions are greater than 10x10 then you may have to change `hs.grid.HINTS` depending on your
---    requirements. See note in `HINTS`.

--- hs.grid.hide()
--- Function
--- Hides the grid, if visible, and exits the modal resizing mode.
---
--- Parameters:
---  * None
---
--- Returns:
---  * None
---
--- Notes:
---  * Call this function if you need to make sure the modal is exited without waiting for the user to press `esc`.
---  * 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:
---  * 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

local function getCellSize(theScreen)
  local g = getGrid(theScreen)
  local screenframe=getGridFrame(theScreen)
  return geom.size(screenframe.w / g.w, screenframe.h / g.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),
  }

  -- ensure windows are not spaced by a double margin
  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)
---
--- Default `HINTS` is an array to 5 rows and 10 columns.
---
--- Notes:
---  * `hs.inspect(hs.grid.HINTS)` from the console will show you how the table is built
---  * `hs.grid.show()`
---     When displaying interactive grid, if gird dimensions (`hs.grid.setGrid()`) are greater than `HINTS` dimensions,
---     then Hammerspoon merges few cells such that interactive grid dimensions do not exceed `HINTS` dimensions.
---     This is done to make sure interactive grid cells do not run out of hints. The interactive grid ends up with
---     cells of varying height and width.
---     The actual grid is not affected. If you use API methods like `hs.grid.pushWindowDown()`, you will not face this
---     issue at all.
---     If you have a grid of higher dimensions and require an interactive gird that accurately models underlying grid
---     then set `HINTS` variable to a table that has same dimensions as your grid.
---     Following is an example of grid that has 16 columns
---
--- ```
--- hs.grid.setGrid('16x4')
--- hs.grid.HINTS={
---     {'f1', 'f2' , 'f3' , 'f4' , 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'f13', 'f14', 'f15', 'f16'},
---     {'1' , 'f11', 'f15', 'f19', 'f3', '=' , ']' , '2' , '3' , '4'  , '5'  , '6'  , '7'  , '8'  , '9'  , '0'  },
---     {'Q' , 'f12', 'f16', 'f20', 'f4', '-' , '[' , 'W' , 'E' , 'R'  , 'T'  , 'Y'  , 'U'  , 'I'  , 'O'  , 'P'  },
---     {'A' , 'f13', 'f17', 'f1' , 'f5', 'f7', '\\', 'S' , 'D' , 'F'  , 'G'  , 'H'  , 'J'  , 'K'  , 'L'  , ','  },
---     {'X' , 'f14', 'f18', 'f2' , 'f6', 'f8', ';' , '/' , '.' , 'Z'  , 'X'  , 'C'  , 'V'  , 'B'  , 'N'  , 'M'  }
--- }
--- ```
---

-- 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 assignments 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(_,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,_ 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,theScreen in ipairs(screens) do
    local sgr = getGrid(theScreen)
    local cell = getCellSize(theScreen)
    local frame = getGridFrame(theScreen)
    log.f('Screen #%d %s (%s) -> grid %s (%s cells)',i,theScreen: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=theScreen:id()
    uielements[sid] = {left=(theScreen:toWest() or theScreen):id(),
      up=(theScreen:toNorth() or theScreen):id(),
      right=(theScreen:toEast() or theScreen):id(),
      down=(theScreen:toSouth() or theScreen):id(),
      screen=theScreen, 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 height > # 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() -- luacheck: ignore
    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() -- luacheck: ignore
    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're 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
