-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f79d2dd
Showing
6 changed files
with
485 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# GridLayout.spoon | ||
|
||
Save preset layouts for your most commonly used apps using hs.grid for positioning! | ||
|
||
> _NOTE: Experimental! Things may change without notice._ | ||
## Rationale | ||
|
||
Window managers like Divvy, Spectacle, Moom, etc. are rad, but are often too simplistic. | ||
|
||
Automatic tiling managers like Yabai are also rad, but are often too intrusive. | ||
|
||
This spoon is an opinionated 'best-of-both-worlds' approach, allowing you to control your own floating windows, but with an easy way to automatically position your most common used apps into nice grid-based preset layouts. | ||
|
||
## Credit | ||
|
||
This has largely been a back-and-forth collab between my lovely co-worker [Jason Varga](https://github.com/jasonvarga) and I, but Jason deserves much of the credit for this layout system ❤️🔥 | ||
|
||
Also special thank you to my brother-in-arms [Evan Travers](https://github.com/evantravers) for all the insanely helpful [Hammerspoon blog posts](https://evantravers.com/articles/tags/hammerspoon/) and example [Spoons](https://github.com/evantravers?tab=repositories&q=spoon), which have proven immensely useful in this Hammerspoon journey 💘 | ||
|
||
## Install | ||
|
||
1. MacOS | ||
2. [Hammerspoon](https://www.hammerspoon.org/go/) | ||
3. Download a [release](https://github.com/jesseleite/GridLayout.spoon/releases) to `~/.hammerspoon/Spoons/Headspace.spoon` | ||
4. Load the Spoon by adding the following code snippet to `~/.hammerspoon/init.lua`: | ||
|
||
```lua | ||
local layout = hs.loadSpoon('GridLayout'):start() | ||
``` | ||
|
||
## Grid Configuration | ||
|
||
```lua | ||
layout | ||
:setGrid('60x20') | ||
:setMargins('15x15') | ||
``` | ||
|
||
> _NOTE: These methods will set the above mentioned `hs.grid` methods for you, but will also inform GridLayout.spoon of your grid configuration as well._ | ||
## Apps Configuration | ||
|
||
```lua | ||
layout:apps({ | ||
WezTerm = { id = 'com.github.wez.wezterm' }, | ||
Brave = { id = 'com.brave.Browser' }, | ||
Slack = { id = 'com.tinyspeck.slackmacgap' }, | ||
Tower = { id = 'com.fournova.Tower3' }, | ||
Ray = { id = 'be.spatie.ray' }, | ||
Obsidian = { id = 'md.obsidian' }, | ||
}) | ||
|
||
``` | ||
|
||
> _NOTE: Extracting an [apps.lua](https://github.com/jesseleite/dotfiles/blob/master/hammerspoon/apps.lua) object/module in your hammerspoon config is recommended; It's a nice pattern for assigning app-specific hotkeys, etc. for use throughout your hammerspoon config, but inline is fine too!_ | ||
## Layouts Configuration | ||
|
||
```lua | ||
layout:setLayouts({ | ||
{ | ||
name = 'Standard Dev', -- Define a 'Standard Dev' layout | ||
cells = { | ||
{ '0,0 7x20' }, -- Cell 1 | ||
{ '7,0 21x20' }, -- Cell 2 | ||
{ '28,0 32x20' }, -- Cell 3 | ||
{ '42,2 16x16' }, -- Cell 4 | ||
}, | ||
apps = { | ||
Ray = { cell = 1, open = true }, -- Assign to cell 1, and ensure app opens | ||
Brave = { cell = 2, open = true }, -- Assign to cell 2, and ensure app opens | ||
WezTerm = { cell = 3, open = true }, -- Assign to cell 3, and ensure app opens | ||
Tower = { cell = 3 }, -- Assign to cell 3, app being open is optional | ||
Slack = { cell = 4 }, -- Assign to cell 4, app being open is optional | ||
}, | ||
}, | ||
{ | ||
name = 'Code Focused', -- Define a 'Code Focused' layout | ||
cells = { | ||
-- etc. | ||
}, | ||
} | ||
}) | ||
``` | ||
|
||
> _NOTE: You may define as many layouts as you wish! Extracting [layouts.lua](https://github.com/jesseleite/dotfiles/blob/master/hammerspoon/layouts.lua) object/module in your hammerspoon config is recommended, but inline is fine too!_ | ||
### Defining Layout Variants | ||
|
||
WIP | ||
|
||
## Available Action Methods | ||
|
||
WIP | ||
|
||
| Method | Params | Description | | ||
| :--- | :--- | :--- | | ||
| `layout:selectLayout()` | | Open fuzzy layout selector. | | ||
| `layout:applyLayout()` | | Programatically apply a specific layout. | | ||
| `layout:selectNextVariant()` | | Select next [layout variant](#defining-layout-variants). | | ||
| `layout:bindToCell()` | | Bind currently focused app to a specific layout cell. | | ||
| `layout:resetLayout()` | | Reset currently selected layout state. | | ||
| `layout:resetAll()` | | Reset all in-memory GridLayout.spoon state. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
local M = {} | ||
|
||
-- Print focused window info to console for debugging purposes. | ||
M.window_info = hs.window.filter.new():subscribe(hs.window.filter.windowFocused, function(window) | ||
local f = window:frame() | ||
print(hs.inspect({ | ||
Focused = { | ||
['App Name'] = window:application():name(), | ||
['Bundle ID'] = window:application():bundleID(), | ||
['Window Title'] = window:title(), | ||
['Window ID'] = window:id(), | ||
['Window Frame'] = string.format("{x=%s,y=%s,w=%s,h=%s}", f._x, f._y, f._w, f._h), | ||
} | ||
})) | ||
end) | ||
|
||
-- Unsubscribe to all registered events. | ||
function M:unsubscribeAll() | ||
M.window_info:unsubscribeAll() | ||
end | ||
|
||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
-- This might be a nice FR / PR to hammerspoon, so we'll keep it out | ||
-- of sight as a separate module that could be deleted later on. | ||
-- | ||
-- Normally hs.grid.getCell() does not respect margins, so this helper | ||
-- accepts a cell string and returns a frame with margins respected. | ||
-- Most of this code was ripped from hs.grid.set(), and is just | ||
-- modified to return a frame that respects margin settings. | ||
-- | ||
-- Unfortunately, for this to work we also need to know the user's | ||
-- desired margins, but there doesn't seem to be an easy way to | ||
-- hs.grid.getMargins() or similar, so we need to set here. | ||
|
||
local M = {} | ||
|
||
local grid = require('hs.grid') | ||
local geom = require('hs.geometry') | ||
local screen = require('hs.screen') | ||
|
||
local margins = geom'5x5' | ||
|
||
function M.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 | ||
end | ||
|
||
local min,max = math.min,math.max | ||
|
||
function M.getCellWithMargins(cell, scr) | ||
scr=screen.find(scr) | ||
if not scr then scr=hs.screen.mainScreen() end | ||
cell=geom.new(cell) | ||
local screenrect = grid.getGridFrame(scr) | ||
local screengrid = grid.getGrid(scr) | ||
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), | ||
} | ||
|
||
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 | ||
|
||
-- Return newframe instead of setting window frame like hs.grid.set() does | ||
return newframe | ||
end | ||
|
||
return M |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
local M = {} | ||
|
||
-- Extra grid helper, which maybe we can PR to hammerspoon and/or remove later. | ||
M.grid = dofile(hs.spoons.resourcePath('grid.lua')) | ||
|
||
-- Normalize layout table from spoon convention for use in hs.layout.apply(). | ||
function M.normalizeLayoutForApply(layout, state) | ||
if not layout.apps then | ||
return layout | ||
end | ||
|
||
local normalized = {} | ||
|
||
for app,config in pairs(layout.apps) do | ||
table.insert(normalized, M.normalizeElementForApply( | ||
state.apps[app].id, | ||
state.apps[app].window, | ||
config.cell, | ||
layout, | ||
state | ||
)) | ||
end | ||
|
||
return normalized | ||
end | ||
|
||
-- Apply single layout element for use in hs.layout.apply. | ||
function M.normalizeElementForApply(app_id, window, cell, layout, state) | ||
return { | ||
app_id, | ||
window, | ||
nil, | ||
nil, | ||
M.grid.getCellWithMargins(layout.cells[cell][state.current_layout_variant]), | ||
} | ||
end | ||
|
||
-- Ensure app is open if `open = true` in layout.apps configuration. | ||
-- Also ensure app is unhidden. | ||
function M.ensureOpenWhenConfigured(layoutApps, allApps) | ||
for name,config in pairs(layoutApps) do | ||
local app | ||
if config.open then | ||
app = hs.application.open(allApps[name].id or name, 10, true) | ||
else | ||
app = hs.application.get(allApps[name].id or name) | ||
end | ||
if app == nil then return end | ||
app:unhide() | ||
end | ||
end | ||
|
||
-- Hide all windows except those relevant to the layout.apps configuration. | ||
function M.hideAllWindowsExcept(layoutApps, allApps) | ||
local allowedIds = {} | ||
for name,_ in pairs(layoutApps) do | ||
table.insert(allowedIds, allApps[name].id) | ||
end | ||
for _,window in pairs(hs.window.visibleWindows()) do | ||
local app = window:application() | ||
local found = hs.fnutils.find(allowedIds, function(allowedId) | ||
return allowedId == app:bundleID() | ||
end) | ||
if not found then | ||
app:hide() | ||
end | ||
end | ||
end | ||
|
||
-- List apps in cells | ||
function M.listAppsInCells(layout, state) | ||
local cells = {} | ||
|
||
for key,_ in pairs(layout.cells) do | ||
cells[key] = { | ||
['key'] = key, | ||
['apps'] = {}, | ||
} | ||
end | ||
|
||
for app,config in pairs(layout.apps) do | ||
table.insert(cells[config.cell].apps, app) | ||
end | ||
|
||
for _,config in pairs(state.layout_customizations[state.current_layout_key] or {}) do | ||
table.insert(cells[config.cell].apps, config.window:application():name()..' ('..config.window:title()..', '..config.window:id()..')') | ||
end | ||
|
||
return cells | ||
end | ||
|
||
return M |
Oops, something went wrong.