Hammerspoon: Feedback request: what are your window management needs?

Created on 11 Sep 2015  路  16Comments  路  Source: Hammerspoon/hammerspoon

I have the basics for hs.window.layout working, now it's a matter of implementing some missing features and fixing bugs (and some day rewriting the tiling engine, it's so ugly I can't believe it works at all).

Before I finalise it, I'd like to gather as much feedback as possible on user needs for all things layouts, because

  • I want to make sure it's flexible enough to cover as many use cases as possible
  • such flexibility means layouts are not super-simple to set up, so I plan on having "presets" for the more common scenarios, with simplified parameters

So, my question to anyone interested in (semi- or fully) automatic window management is:

can you describe how your ideal setup would work?

If you're currently using (or have used in the past) an app/script/whatever for any window-management-related purposes, you can simply mention (or link to) it along with the relevant feature(s) for your use case (for example, I'd been using the "snapshots" feature in Moom to restore window positions when switching between "roaming" (single-screen) and "desk" (4 screens) modes on my laptop).


For those interested in the details, this is the module docstring showing the current feature set (amenable to change, and some things - namely, the restore command and :guessLayout() - aren't implemented yet):

Windowlayouts work by selecting certain windows via windowfilters and arranging them onscreen according to specific rules.
A layout is composed of a list of rules and, optionally, a screen arrangement definition.
Rules within a layout are evaluated in order; once a window is acted upon by a rule, subsequent rules will not affect it further.
A rule needs a windowfilter, producing a dynamic list of windows (the "window pool") to which the rule is applied, and a list of commands, evaluated in order.
A command acts on one or more of the windows, and is composed of:

  • an action, it can be

    • move: moves the window(s) to a specified onscreen rect (if the action is omitted, move is assumed)

    • minimize, maximize, fullscreen

    • tile, fit: tiles the windows onto a specified rect, using hs.window.tiling.tileWindows(); for fit, the

      preserveRelativeArea parameter will be set to true

    • hide, unhide: hides or unhides the window's application (like when using cmd-h)

  • a maxn number, indicating how many windows from this rule's window pool will be affected (at most) by this command; if omitted (or if explicitly the string all) all the remaining windows will be processed by this command; processed windows are "consumed" and are excluded from the window pool for subsequent commands in this rule, and from subsequent rules
  • a selector, describing the sort order used to pick the first _maxn_ windows from the window pool for this command; it can be one of focused (pick _maxn_ most recently focused windows), newest (most recently created), oldest (least recently created), or closest (pick the _maxn_ windows that are closest to the destination rect); if omitted, defaults to closest for move, tile and fit, and newest for everything else
  • an hs.geometry _size_ (only valid for tile and fit) indicating the desired optimal aspect ratio for the tiled windows; if omitted, defaults to 1x1 (i.e. square windows)
  • for move, tile and fit, an hs.geometry _rect_, or a _unit rect_ plus a _screen hint_ (for hs.screen.find()),
    indicating the destination rect for the command
  • for fullscreen and maximize, a _screen hint_ indicating the desired screen; if omitted, uses the window's current screen

You should place higher-priority rules (with highly specialized windowfilters) first, and "fallback" rules
(with more generic windowfilters) last; similarly, _within_ a rule, you should have commands for the more "important" (i.e. relevant to your current workflow) windows first (move, maximize...) and after that deal with less prominent windows, if any remain, e.g. by placing them out of the way (minimize).

unhide and hide, if used, should usually go into their own rules (with a windowfilter that allows invisible windows for unhide) that come _before_ other rules that deal with actual window placement - unlike the other actions, they don't "consume" windows making them unavailable for subsequent rules, as they act on applications.

In order to avoid dealing with deeply nested maps, you can define a layout in your scripts via a list, where each element (or row) denotes a rule; in turn every rule can be a simplified list of two elements:

  • a windowfilter or a constructor argument table for one (see hs.window.filter.new() and hs.window.filter:setFilters())
  • a single string containing all the commands (action and parameters) in order; actions and selectors can be shortened to 3 characters; all tokens must be separated by spaces (do not use spaces inside hs.geometry constructor strings); for greater clarity you can separate commands with | (pipe character)

Some command string examples:

  • "move 1 [0,0,50,50] -1,0" moves the closest window to the topleft quadrant of the left screen
  • "max 0,0" maximizes all the windows onto the primary screen, one on top of another
  • "move 1 foc [0,0,30,100] 0,0 | tile all foc [30,0,100,100] 0,0" moves the most recently focused window to the left third, and tiles the remaining windows onto the right side, keeping the most recently focused on top and to the left
  • "1 new [0,0,50,100] 0,0 | 1 new [50,0,100,100] 0,0 | min" divides the primary screen between the two newest windows and minimizes any other windows

Each layout can work in "passive" or "active" modes; passive layouts must be triggered manually (via hs.hotkey.bind(), hs.menubar, etc.) while active layouts continuously keep their rules enforced (see hs.window.layout:start() for more information); in general you should avoid having multiple active layouts targeting the same windows, as the results will be unpredictable (if such a situation is detected, you'll see an error in the Hammerspoon console); you _can_ have multiple active layouts, but be careful to maintain a clear "separation of concerns" between their respective windowfilters.

Each layout can have an associated screen configuration; if so, the layout will only be valid while the current screen arrangement satisfies it; see hs.window.layout:setScreenConfiguration() for more information.


For a real-world example of (some of) the possibilities, and to get an idea on how to setup layouts (spoiler: not as easy as I'd like), this is a mostly HS-related subset of my current config - for a rMBP that I use both standalone and with a Philips 4K and two Dells in portrait on either side when in "desk" mode.

sharedwl=hs.window.layout.new({ -- first, a "shared" layout that's always active
  -- Safari windows, excluding preferences and fullscreen windows (for video)
  -- allowScreens='0,0' so that it only applies to windows on the main screen, 
  -- so in desk mode i can temporarily "tear off" Safari windows to the side 
  -- screens for manual management
  {{['Safari']={allowScreens='0,0',fullscreen=false,rejectTitles={'^General$','^Tabs$','^AutoFill$','^Passwords$','^Search$','^Security$','^Privacy$','^Notifications$','^Extensions$','^Advanced$'}}},
  -- main window in the middle, 2 windows tiled on the left side, remaining 
  -- windows on the right; the selector "closest" lets me change the "main" 
  -- window by sloppily moving it to the center (I use hs.grid)
    'move 1 closest [30,0,70,100] 0,0 | tile 2 vert [0,0,30,100] 0,0',
    'tile all 2x1 [70,0,100,100] 0,0'},
  -- Finder windows also tiled on the right
  {{['Finder']={allowScreens='0,0'}},'tile all 2x1 [70,0,100,100] 0,0'},
  -- Screen Sharing always at the top left whatever the resolution
  {'Screen Sharing','tile [0,0,100,100] 0,0'},
},'SHARED'):start()

laptopwl=hs.window.layout.new({ -- standalone, use in-constructor screen config
  screens={['Color LCD']='0,0',phl=false,dell=false}, -- when no external screens
  -- the main LDT window uses most of the screen
  {['Lua Development Tools Product']={allowTitles='- Lua Development Tools -'},
    'move 1 [0,0,80,100] 0,0'},
  -- with HS console and Dash sharing the remaining slice; if I need the full 
  -- height for the console I just hit the hotkey for Dash twice to hide it; the 
  -- console expands and shrinks automatically
  {{['Hammerspoon']={allowRoles='AXStandardWindow'},['Dash']={visible=true}},
    'tile all newest vert [80,0,100,100] 0,0'},
  -- "main" Sublime Text window on the right, remaining tiled on the left as
  -- columns; the "focused" selector means that the main window is 
  -- whichever one is currently focused; the windows shuffle around as I cmd-`
  {{'Sublime Text'},'move 1 focused [60,0,100,100] 0,0 | tile all horiz [0,0,60,100] 0,0'},
  -- main (oldest, as it's always created first upon launching the app) iTerm panel
  -- on the left; temporary additional windows on the right
  {{'iTerm2'},'move 1 oldest [0,0,30,100] 0,0 | tile all oldest [70,0,100,100] 0,0'},
},'LAPTOP'):start()

deskwl=hs.window.layout.new({ -- desk mode
  -- use the post-constructor method at the bottom for screen config
  -- LDT, HS, Dash, Sublime similar to laptop, accounting for much larger 
  -- horizontal resolution on the main screen
  {['Lua Development Tools Product']={allowTitles='- Lua Development Tools -'},
    'move 1 [0,0,70,100] 0,0'},
  {{['Hammerspoon']={allowRoles='AXStandardWindow'},['Dash']={visible=true}},
    'tile all newest vert [70,0,100,100] 0,0'},
  {{'Sublime Text'},'move 1 focused [70,0,100,100] 0,0 | tile all [0,0,70,100] 0,0'},
  -- shove all of these onto the left screen
  {{'Messages','Slack'},'2 [0,70,100,100] -1,0'},
  {{'Reminders'},'[0,70,100,100] -1,0'},
  -- main iTerm maximised on the left screen, additional windows on main
  {{['iTerm2']={allowScreens={'-1,0','0,0'}}},
    'max 1 oldest -1,0 | tile all oldest [0,0,30,100] 0,0'},
  -- the main Evernote window (notes list) is always on the left screen
  {{['Evernote']={allowTitles='^Evernote Plus$'}},'[0,0,100,70] -1,0' }, 
  -- the right screen is for "research": the next windowfilter takes Chrome 
  -- windows (but if I move one on another screen it's managed manually) 
  -- and any remaining Evernote windows, i.e. single notes (rule priority 
  -- means the main window is already consumed by the rule above);
  -- like for Safari, I can quickly switch which of these is the "main" window 
  -- occupying the whole top half via the implicit "closest" selector by 
  -- issuing an approximate hs.grid command; if I open too many windows 
  -- (which I tend to do for quick "unrelated" tasks), oldest ones get
  -- automatically minimised, and conversely unminimised when I'm done
  {{['Google Chrome']={allowScreens='1,0',currentSpace=true},'Evernote'},
    '[0,0,100,50] 1,0','tile 2 [0,50,100,100] 1,0','minimize'},
},'DESK'):setScreenConfiguration{dell='-1,0',phl='0,0',['dell u']='1,0'}:start() 

It's worth adding that you can get the same "snap to defined layout" vs "tear off and manage manually" behaviour _within_ a given screen by using allowRegions instead of allowScreens in the windowfilters.


If you read all that (!) and have comments, suggestions or questions, they're more than welcome.

Most helpful comment

any chance this could do auto save/restore of windows depending on the various connected screens?

i guess one way this could work is save last positions/sizes for all windows, as I would manually setup my screens just as i want them, then when the screen configuration changes, apply the last saved config for that screen configuration.

that way i could setup my screens just once, then always have windows pop and reconfigure like i last had them and no need to hardcode layouts

thanks for a great tool

/edit: i guess it would need to save configuration for all windows as they have been last seen, and apply when a new app starts as well, so you don t loose positions of an app is closed and the setup changes.

All 16 comments

I have not used the windowlayout stuff myself yet, so I look forward to looking at the presets.

That said, I do have a few arrangements I sometimes manually set up for working styles when doing certain tasks, so I do plan to use it at some point... is it possible (or how hard would it be to add a way) to generate a "snapshot" which is a "best guess" at the rules which would create the "current layout"? Obviously it would need editing/tweaking afterwards, but might provide a starting point for someone coming in cold to the filters. I know that kind of "guessing" gets very complex very fast, so is it even feasible?

Yes, it should be possible to _some_ extent; in fact, the design I'm using was partially determined by trying to keep the door open for precisely that feature (let's call it hs.window.layout.guessRules()) which I'd like to have eventually. However I'm not entirely clear yet on what exactly is "guessable" deterministically (let alone how) - e.g. I don't think it'd be possible to have anything other than basic per-app windowfilters, which means some more sophisticated scenarios involving a multi-app windowfilter for a given rule wouldn't be covered (then again, I haven't given this a lot of thought yet).

On a somewhat related note, an adjacent feature that I had already working (and will reimplement here) on my previous hs.windowlayout incarnation is getLayout() (captures a "dumb" snapshot, i.e. just a list of windows with their titles, frames and other info) and the converse applyLayout() (tries to reapply it "smartly", comparing the current windows with the captured snapshot via heuristics). Ideally we'd find an elegant way to combine this with guessRules() somehow - if only to contain the API surface area - but prima facie it doesn't look trivial.

@lowne curious about the consuming stuff, seems like a good idea, although I'm wondering how you'd do rules that would match an arbitrary number of windows - e.g. if I wanted all my Safari windows gathered up into one spot

@cmsj for "tile" and "fit", if N is omitted, it consumes all remaining windows; unlike what I've written above, "minimize" also consumes all remaining windows; so, these two aren't currently possible:

  • "minimize" N (not all) windows and do something different to the remaining windows, if any - I thought that'd be a very unlikely scenario, since windows are in order of focus (so why would you minimize a window, then do something more "important" to a less "important" window?)
  • "move" N/all windows (one on top of the other): currently you'd have to repeat the rect N times (which would admittedly be weird, if N is say 20) - again, that didn't seem a useful scenario:

    • one move command already ensures that the currently _focused_ window is at the desired rect; as you cmd-, eventually all the windows will be gathered there; but it's true that it's not instant on first apply;</li> <li>one move command + one tile command for the remaining windows: would "hide" all the other windows behind the focused one on first apply; would restore the desired window to the full rect as you cmd-; but while near-instantaneous this dynamic resizing isn't completely unobtrusive as having all the windows gathered up to the correct size to begin with

So on second thought that should probably be an option; I could

  • add a "repeat" command that would just consume any remaining windows with the previous command, which would give multi-window capability to "move", "maximise" and "fullscreen", that currently lack it; but be redundant for the others (therefore, less elegant API)
  • add an optional N parameter to "move" (and maybe the other commands); downside is again less elegant API :/

    • for "move", to say "all" you'd type "move all" (or "move 999"), and omitted means 1

    • for "tile", omitted means "all" (I wouldn't default to 1 since "tile 1" is just a "move")

@lowne, what you have described sounds incredible!

I suppose my window management needs have been constrained by the tools available. I only recently found Hammerspoon and began learning Lua.

My scripts are here: https://github.com/wsmith323/dev_station/tree/master/etc/hammerspoon

I have recently been working on adding a rudimentary enhancement to hs.layout.apply to enable the management of windows whose titles are dynamic (see https://github.com/Hammerspoon/hammerspoon/pull/551) , but what you are working on is light years ahead of any of that. Excellent work!

Here's what I would like to have. Perhaps it's already written by someone without using hs.layout, I'm a noob so I don't know.

I have two profiles, depending on monitors, which I can detect as there are changes. I would like to drag my usual windows into place and then hit a hotkey for "save", then associate that layout with a monitor profile. Then each profile would resize and move any known windows as it became active.

@mnp, yes, that's my primary use case as well, which I had implemented already in my original "windowlayout" module; it will be (re)implemented here - according to the draft plan, it's the "restore" action I mentioned in OP without further explanation, but that's not set in stone yet (see 3rd comment for some related complications)

I've updated the first post with

  • a better explanation of the currently implemented API (importantly, selectors have been added)
  • probably more usefully, a commented excerpt from my own windowlayouts that should give a better idea of how it all works, in a moderately advanced use case

As always, questions and comments are welcome, as they help me figure out ways to simplify the api or if I should change some defaults.

My use case is automated window positioning in a multi-screen setup. There are two modes that I use:

  • Work on the laptop screen without external screens or with screen mirroring
  • Work on two external screens and the laptop is closed

The switch between those 2 modes is usually while the system sleeps (I put a sleeping Mac into the docking station and wake it up or I just take the Mac out of the docking station).

I came here and installed Hammerspoon to find a solution that would actually work in this scenario, the (commercial) tools I tried all fail to recognize the screen change in combination with a sleep cycle.

From a user experience perspective I would expect the following:

  • Hammerspoon keeps track of all my windows and their positions, e.g. by storing the window positions when the change or every minute.
  • This tracking happens per screen layout: One tracking database for each screen layout in use.
  • When the screen layout changes (e.g. suddenly there are two big screens instead of the laptop display) then the windows are rearranged according to the last saved positions for that screen layout.

The expected effect is that the windows all the time go there where I put them last and that this is persistent with the screen layouts I use.

Makes sense? I would really appreciate it if Hammerspoon could provide this feature.

I looked at the examples above and they seem to be made for people who want to "arrange" their windows with a text editor. I actually want to use the mouse for that and have the automation handle the task to remember the details.

Thanks a lot!

These two webm's show off bspwm and some of the features I'd like to see in a tiling WM run by Hammerspoon:

webm1
webm2

  • Toggle gaps between windows
  • New windows spawn into place, tiling any others. See: automatic mode
  • Toggle between tiling and floating modes
  • Focus follows mouse
  • Windows can be moved, resized (in proportion) and rotated using hotkeys
  • "Monocle" mode - Windows are all fullscreened (with a gap around them, so not true 'fullscreeen' mac mode) and stacked ontop of one another. Focus/BringToFront is controlled by hotkey and/or titles in the bar
  • Having a border around the focused window would be nice, but it's not absolutely necessary
  • Support for multiple monitors (and virtual desktops) is a must
  • A simple configuration file for specifying hotkeys

I tried to create a status bar using hammerspoon (https://github.com/internaught/hammered/blob/master/bar.lua), but had issues with text bleeding. I would love to see a bar similar to LemonBoy's bar, where it just reads STDIN and you can pipe text to it, with clickable areas and such. This would be used to manage windows, like the webm's above show and manage virtual workspaces using something like asmagill's spaces. I think a bar would contribute to a tiling window manager setup.

TL;DR: I would love to see all the functionality of BSPWM (and LemonBar) in Hammerspoon or even a third party piece of software.

At home/work: laptop screen + 2 external displays
At meeting: laptop screen
Elsewhere: laptop screen + iPad with Duet

Center external: Chrome left, Safari right
Right external: iTerm left and right
Laptop screen: IntelliJ

I need Slate's repeat feature to be able to show two iTerm windows side by side.

I basically want XMonad, but for OSX. Also, is there a way to _snap_ windows to the left/right halves of the screen rather than slowly sliding them over with these functions?

Re snapping rather then sliding, check out the help for hs.window.animationDuration (type help.hs.window.animationDuration in the Hammerspoon Console). I set this to 0 in my init.lua file because I don't want _any_ animation for most things; if you do, but just don't want it for some bindings, just add 0 as a second argument to setFrame in your functions.

Thanks @asmagill. Personally, I hate animations. Give me instantaneous moving and resizing any day.

I'm going to close this because I'm gardening the bugs and this has been open for ages with no further discussion, feel free to re-open it if there are specific things you want to do :)

any chance this could do auto save/restore of windows depending on the various connected screens?

i guess one way this could work is save last positions/sizes for all windows, as I would manually setup my screens just as i want them, then when the screen configuration changes, apply the last saved config for that screen configuration.

that way i could setup my screens just once, then always have windows pop and reconfigure like i last had them and no need to hardcode layouts

thanks for a great tool

/edit: i guess it would need to save configuration for all windows as they have been last seen, and apply when a new app starts as well, so you don t loose positions of an app is closed and the setup changes.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tomrbowden picture tomrbowden  路  3Comments

specter78 picture specter78  路  3Comments

zhenwenc picture zhenwenc  路  4Comments

luckman212 picture luckman212  路  4Comments

lazandrei19 picture lazandrei19  路  4Comments