Filter windows by application, title, location on screen and more, and easily subscribe to events on these windows
Warning: this module is still somewhat experimental. Should you encounter any issues, please feel free to report them on https://github.com/Hammerspoon/hammerspoon/issues or #hammerspoon on irc.libera.chat.
Windowfilters monitor all windows as they're created, closed, moved etc., and select some (or none) among these windows according to specific filtering rules. These filtering rules are app-specific, i.e. they start off by selecting all windows belonging to a certain application (but you can also define default and override filters - see :setAppFilter(), :setDefaultFilter(), :setOverrideFilter()) and they can allow or reject windows based on:
visibility, focused and/or fullscreen status
title length or patterns in the title
position on screen (inside or outside a certain region or screen)
accessibility role (standard window, dialog, etc.)
whether they're in the current Mission Control Space or not
The filtering happens automatically in the background; windowfilters then:
generate a dynamic list of the windows that currently satisfy the filtering rules (see :getWindows())
sanitize and expose all pertinent events on these windows (see :subscribe() and the module constants with all the events)
A default windowfilter (not to be confused with the default filter within a windowfilter) is provided as convenience; it excludes some known apps and windows that are transient in nature, therefore unlikely to be "interesting" for e.g. window management. hs.window.filter.new() (with no arguments) returns a copy of the default windowfilter that you can further tailor to your needs - see hs.window.filter.default and hs.window.filter.new() for more information.
Usage examples:
local wf=hs.window.filter
-- alter the default windowfilter
wf.default:setAppFilter('My IDE',{allowTitles=1}) -- ignore no-title windows (e.g. transient autocomplete suggestions) in My IDE
-- set the exact scope of what you're interested in - see hs.window.filter:setAppFilter()
wf_terminal = wf.new{'Terminal','iTerm2'} -- all visible terminal windows
wf_timewaster = wf.new(false):setAppFilter('Safari',{allowTitles='reddit'}) -- any Safari windows with "reddit" anywhere in the title
wf_leftscreen = wf.new{override={visible=true,fullscreen=false,allowScreens='-1,0',currentSpace=true}}
-- all visible and non-fullscreen windows that are on the screen to the left of the primary screen in the current Space
wf_editors_righthalf = wf.new{'TextEdit','Sublime Text','BBEdit'}:setRegions(hs.screen.primaryScreen():fromUnitRect'0.5,0/1,1')
-- text editor windows that are on the right half of the primary screen
wf_bigwindows = wf.new(function(w)return w:frame().area>3000000 end) -- only very large windows
wf_notif = wf.new{['Notification Center']={allowRoles='AXNotificationCenterAlert'}} -- notification center alerts
-- subscribe to events
wf_terminal:subscribe(wf.windowFocused,some_fn) -- run a function whenever a terminal window is focused
wf_timewaster:subscribe(wf.hasWindow,startAnnoyingMe):subscribe(wf.hasNoWindows,stopAnnoyingMe) -- fight procrastination :)
The default windowfilter; it filters apps whose windows are transient in nature so that you're unlikely (and often
Notes
While you can customize the default windowfilter, it's usually advisable to make your customizations on a local copy via mywf=hs.window.filter.new(); the default windowfilter can potentially be used in several Hammerspoon modules and changing it might have unintended consequences. Common customizations:
to exclude fullscreen windows: nofs_wf=hs.window.filter.new():setOverrideFilter{fullscreen=false}
to include invisible windows: inv_wf=windowfilter.new():setDefaultFilter{}
If you still want to alter the default windowfilter:
you should probably apply your customizations at the top of your init.lua, or at any rate before instantiating any other windowfilter; this way copies created via hs.window.filter.new(nil,...) will inherit your modifications
to list the known exclusions: hs.inspect(hs.window.filter.default:getFilters()) from the console
to add an exclusion: hs.window.filter.default:rejectApp'Cool New Launcher'
to add an app-specific rule: hs.window.filter.default:setAppFilter('My IDE',1); ignore tooltips/code completion (empty title) in My IDE
to remove an exclusion (e.g. if you want to have access to Spotlight windows): hs.window.filter.default:allowApp'Spotlight'; for specialized uses you can make a specific windowfilter with myfilter=hs.window.filter.new'Spotlight'
Sort order for hs.window.filter:getWindows(): windows are sorted in order of focus received, least recently first (see also hs.window.filter:setSortOrder())
Sort order for hs.window.filter:getWindows(): windows are sorted in order of focus received, most recently first (see also hs.window.filter:setSortOrder())
Notes
This is the default sort order for all windowfilters
Pseudo-event for hs.window.filter:subscribe(): the list of allowed windows (as per windowfilter:getWindows()) has changed
Notes
callbacks for this event will receive (as the first argument) either a random window among the currently allowed ones, or nil if the windowfilter is rejecting all windows
similarly, the second argument passed to callbacks (window's app name) will be nil if the windowfilter is rejecting all windows
this pseudo-event will be emitted after the actual event(s) that caused the list of allowed windows to change
Tells all windowfilters whether to refresh all windows when the user switches to a different Mission Control Space.
Notes
If you defined one or more Spaces-aware windowfilters (i.e. when the currentSpace field of a filter is present), windows need refreshing at every space change anyway, so this variable is ignored
A table of application names (as per hs.application:name()) that are always ignored by this module.
Notes
As the name implies, even the empty, "allow all" windowfilter will ignore these apps.
You don't need to keep this table up to date, since non GUI apps will simply never show up anywhere; this table is just used as a "root" filter to gain a (very small) performance improvement.
Callback to inform all windowfilters that the user initiated a switch to a (numbered) Mission Control Space.
Parameters
space - the Space number the user is switching to
Returns
Notes
Only use this function if "Displays have separate Spaces" and "Automatically rearrange Spaces" are OFF in System Preferences>Mission Control
Calling this function will set hs.window.filter.forceRefreshOnSpaceChange to false
If you defined one or more Spaces-aware windowfilters (i.e. when the currentSpace field of a filter is present), windows need refreshing at every space change anyway, so using this callback will not result in improved performance
See hs.window.filter.forceRefreshOnSpaceChange for an overview of Spaces limitations in Hammerspoon. If you often (or always) change Space via the "numbered" Mission Control keyboard shortcuts (by default, ctrl-1 etc.), you can call this function from your init.lua when intercepting these shortcuts; for example:
hs.hotkey.bind('ctrl','1',nil,function()hs.window.filter.switchedToSpace(1)end)
hs.hotkey.bind('ctrl','2',nil,function()hs.window.filter.switchedToSpace(2)end)
-- etc.
Using this callback results in slightly better performance than setting forceRefreshOnSpaceChange to true, since already visited Spaces are remembered and no refreshing is necessary when switching back to those.
if nil, returns a copy of the default windowfilter, including any customizations you might have applied to it so far; you can then further restrict or expand it
if true, returns an empty windowfilter that allows every window
if false, returns a windowfilter with a default rule to reject every window
if a string or table of strings, returns a windowfilter that only allows visible windows of the specified apps as per hs.application:name()
if a table, you can fully define a windowfilter without having to call any methods after construction; the table must be structured as per hs.window.filter:setFilters(); if not specified in the table, the default filter in the new windowfilter will reject all windows
otherwise it must be a function that accepts an hs.window object and returns true if the window is allowed or false otherwise; this way you can define a fully custom windowfilter
logname - (optional) name of the hs.logger instance for the new windowfilter; if omitted, the class logger will be used
loglevel - (optional) log level for the hs.logger instance for the new windowfilter
hs.window.filter:getWindows([sortOrder]) -> list of hs.window objects
Type
Method
Description
Gets the current windows allowed by this windowfilter
Parameters
sortOrder - (optional) one of the hs.window.filter.sortBy... constants to determine the sort order of the returned list; if omitted, uses the windowfilter's sort order as per hs.window.filter:setSortOrder() (defaults to sortByFocusedLast)
Stops the windowfilter event subscriptions; no more event callbacks will be triggered, but the subscriptions remain intact for a subsequent call to hs.window.filter:resume()
Sets the detailed filtering rules for the windows of a specific app
Parameters
appname - app name as per hs.application:name()
filter - if false, reject the app; if true, nil, or omitted, allow all visible windows (in any Space) for the app; otherwise it must be a table describing the filtering rules for the app, via the following fields:
visible - if true, only allow visible windows (in any Space); if false, reject visible windows; if omitted, this rule is ignored
currentSpace - if true, only allow windows in the current Mission Control Space (minimized and hidden windows are included, as they're considered to belong to all Spaces); if false, reject windows in the current Space (including all minimized and hidden windows); if omitted, this rule is ignored
fullscreen - if true, only allow fullscreen windows; if false, reject fullscreen windows; if omitted, this rule is ignored
hasTitlebar - if true, only allow windows with titlebar; if false, reject window with titlebar; if omitted, this rule is ignored
focused - if true, only allow a window while focused; if false, reject the focused window; if omitted, this rule is ignored
activeApplication - only allow any of this app's windows while it is (if true) or it's not (if false) the active application; if omitted, this rule is ignored
allowTitles * if a number, only allow windows whose title is at least as many characters long; e.g. pass 1 to filter windows with an empty title * if a string or table of strings, only allow windows whose title matches (one of) the pattern(s) as per string.match * if omitted, this rule is ignored
rejectTitles - if a string or table of strings, reject windows whose titles matches (one of) the pattern(s) as per string.match; if omitted, this rule is ignored
allowRegions - an hs.geometry rect or constructor argument, or a list of them, designating (a) screen "region(s)" in absolute coordinates: only allow windows that "cover" at least 50% of (one of) the region(s), and/or windows that have at least 50% of their surface inside (one of) the region(s); if omitted, this rule is ignored
rejectRegions - an hs.geometry rect or constructor argument, or a list of them, designating (a) screen "region(s)" in absolute coordinates: reject windows that "cover" at least 50% of (one of) the region(s), and/or windows that have at least 50% of their surface inside (one of) the region(s); if omitted, this rule is ignored
allowScreens - a valid argument for hs.screen.find(), or a list of them, indicating one (or more) screen(s): only allow windows that (mostly) lie on (one of) the screen(s); if omitted, this rule is ignored
rejectScreens - a valid argument for hs.screen.find(), or a list of them, indicating one (or more) screen(s): reject windows that (mostly) lie on (one of) the screen(s); if omitted, this rule is ignored
allowRoles * if a string or table of strings, only allow these window roles as per hs.window:subrole() * if the special string '*', this rule is ignored (i.e. all window roles, including empty ones, are allowed) * if omitted, use the default allowed roles (defined in hs.window.filter.allowedWindowRoles)
Returns
the hs.window.filter object for method chaining
Notes
Passing focused=true in filter will (naturally) result in the windowfilter ever allowing 1 window at most
If you want to allow all windows for an app, including invisible ones, pass an empty table for filter
Spaces-aware windowfilters might experience a (sometimes significant) delay after every Space switch, since (due to OS X limitations) they must re-query for the list of all windows in the current Space every time.
If System Preferences>Mission Control>Displays have separate Spaces is on, the current Space is defined as the union of all the Spaces that are currently visible
This table explains the effects of different combinations of visible and currentSpace, showing which windows will be allowed:
|visible= nil | true | false |
|currentSpace|------------------------------------------|------------------------------|--------------|
| nil |all |visible in ANY space |min and hidden|
| true |visible in CURRENT space+min and hidden |visible in CURRENT space |min and hidden|
| false |visible in OTHER space only+min and hidden|visible in OTHER space only |none |
Sets whether the windowfilter should only allow (or reject) windows in the current Mission Control Space
Parameters
val - boolean; if true, only allow windows in the current Mission Control Space, plus minimized and hidden windows; if false, reject them; if nil, ignore Mission Control Spaces
Returns
the hs.window.filter object for method chaining
Notes
This is just a convenience wrapper for setting the currentSpace field in the override filter (other fields will be left untouched); per-app filters will maintain their currentSpace field, if present, as is
Spaces-aware windowfilters might experience a (sometimes significant) delay after every Space switch, since (due to OS X limitations) they must re-query for the list of all windows in the current Space every time.
filters - table, every element will set an application filter; these elements must: - have a key of type string, denoting an application name as per hs.application:name() - if the value is a boolean, the app will be allowed or rejected accordingly - see hs.window.filter:allowApp() and hs.window.filter:rejectApp() - if the value is a table, it must contain the accept/reject rules for the app as key/value pairs; valid keys and values are described in hs.window.filter:setAppFilter() - the key can be one of the special strings "default" and "override", which will set the default and override filter respectively - the key can be the special string "sortOrder"; the value must be one of the sortBy... constants as per hs.window.filter:setSortOrder()
Returns
the hs.window.filter object for method chaining
Notes
every filter definition in filters will overwrite the preexisting one for the relevant application, if present; this also applies to the special default and override filters, if included
Sets the allowed screen regions for this windowfilter
Parameters
regions - an hs.geometry rect or constructor argument, or a list of them, indicating the allowed region(s) for this windowfilter
Returns
the hs.window.filter object for method chaining
Notes
This is just a convenience wrapper for setting the allowRegions field in the override filter (other fields will be left untouched); per-app filters will maintain their allowRegions and rejectRegions fields, if present
regions - a valid argument for hs.screen.find(), or a list of them, indicating the allowed screen(s) for this windowfilter
Returns
the hs.window.filter object for method chaining
Notes
This is just a convenience wrapper for setting the allowScreens field in the override filter (other fields will be left untouched); per-app filters will maintain their allowScreens and rejectScreens fields, if present
Subscribe to one or more events on the allowed windows
Parameters
event - string or list of strings, the event(s) to subscribe to (see the hs.window.filter constants); alternatively, this can be a map {event1=fn1,event2=fn2,...}: fnN will be subscribed to eventN, and the parameter fn will be ignored
fn - function or list of functions, the callback(s) to add for the event(s); each will be passed 3 parameters
a hs.window object referring to the event's window
a string containing the application name (window:application():name()) for convenience
a string containing the event that caused the callback, i.e. (one of) the event(s) you subscribed to
immediate - (optional) if true, also call all the callbacks immediately for windows that satisfy the event(s) criteria
Returns
the hs.window.filter object for method chaining
Notes
Passing lists means that all the fns will be called when any of the events fires, so it's not a shortcut for subscribing distinct callbacks to distinct events; use a map or chained :subscribe calls for that.
Use caution with immediate: if for example you're subscribing to hs.window.filter.windowUnfocused, fn(s) will be called for all the windows except the currently focused one.
If the windowfilter was paused with hs.window.filter:pause(), calling this will resume it.
event - string or list of strings, the event(s) to unsubscribe; if omitted, fn(s) will be unsubscribed from all events; alternatively, this can be a map {event1=fn1,event2=fn2,...}: fnN will be unsubscribed from eventN, and the parameter fn will be ignored
fn - function or list of functions, the callback(s) to remove; if omitted, all callbacks will be unsubscribed from event(s)
Returns
the hs.window.filter object for method chaining
Notes
You must pass at least one of event or fn
If calling this on the default (or any other shared use) windowfilter, do not pass events, as that would remove all the callbacks for the events including ones subscribed elsewhere that you might not be aware of. You should instead keep references to your functions and pass in those.