Tile visible apps on active display

This script tiles apps on the active display. To test this script, open and run it in a script editor with at least one other visible app. Note should be made that:

  • The user can change the tiling grid pattern by editing the windowSetting list and can change the buffer between apps by editing the wB variable.
  • The user can exclude apps from being tiled by adding them to the doNotTile list.
  • Other than those in the doNotTile list, all apps including those without a visible window are tiled.
  • Some apps have minimum/maximum window sizes and may not tile as expected.
-- Revised 2022.05.22

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set {appCount, openApps} to getAppData()
	set windowBounds to getWindowData(appCount)
	tileApps(openApps, windowBounds)
end main

on getAppData()
	set doNotTile to {} -- intended for use with apps that do not have a visible window
	set theWorkspace to current application's NSWorkspace's sharedWorkspace()
	set activeApp to theWorkspace's frontmostApplication()
	set openApps to theWorkspace's runningApplications()
	set thePredicates to current application's NSPredicate's predicateWithFormat:"activationPolicy == 0 AND NOT localizedName IN %@" argumentArray:{doNotTile}
	set openApps to (openApps's filteredArrayUsingPredicate:thePredicates)'s mutableCopy()
	
	tell application "Finder" to set finderWindow to (exists Finder window 1)
	if finderWindow = false then openApps's removeObjectAtIndex:0
	set appCount to openApps's |count|()
	
	if appCount < 2 or appCount > 9 then
		display alert "An error has occurred" message "Less than 2 or more than 9 apps are open" as critical
		error number -128
	end if
	
	if (openApps's containsObject:activeApp) as boolean = true then -- tile active app first
		openApps's removeObject:activeApp
		openApps's insertObject:activeApp atIndex:0
	end if
	set openApps to (openApps's valueForKey:"processIdentifier") as list
	
	return {appCount, openApps}
end getAppData

on getWindowData(windowCount)
	set wB to 5 -- window buffer
	set windowSetting to {{1, 1}, {1, 2}, {2, 2}, {2, 3}, {3, 3}, {2, 2, 3}, {2, 3, 3}, {3, 3, 3}}
	set windowSetting to item (windowCount - 1) of windowSetting
	set columnCount to (count windowSetting)
	set {x1, y1, x2, y2} to visibleDisplayBounds()
	
	set wFH to ((y2 - y1) - (wB * 2)) div 1 -- window heights
	set wHH to ((y2 - y1) - (wB * 3)) div 2
	set wTH to ((y2 - y1) - (wB * 4)) div 3
	
	if columnCount = 2 then -- column widths
		set wW to ((x2 - x1) - (wB * 3)) div 2
	else
		set wW to ((x2 - x1) - (wB * 4)) div 3
	end if
	
	set windowBounds to {}
	set {x, y} to {x1, y1}
	repeat with i from 1 to columnCount -- loop through columns
		repeat with j from 1 to item i of windowSetting -- loop through windows in a column
			if item i of windowSetting = 1 then
				set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wW))), (y + (j * wB) + ((j - 1) * wFH))}, {wW, wFH}}
			else if item i of windowSetting = 2 then
				set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wW))), (y + (j * wB) + ((j - 1) * wHH))}, {wW, wHH}}
			else
				set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wW))), (y + (j * wB) + ((j - 1) * wTH))}, {wW, wTH}}
			end if
		end repeat
	end repeat
	
	return windowBounds
end getWindowData

on tileApps(openApps, windowBounds)
	tell application "System Events"
		repeat with i from (count openApps) to 1 by -1
			tell (first process whose unix id is (item i of openApps)) to tell every window
				set position to item 1 of item i of windowBounds
				set size to item 2 of item i of windowBounds
			end tell
		end repeat
	end tell
end tileApps

on visibleDisplayBounds()
	set theScreen to current application's NSScreen's mainScreen()
	set {{aF, bF}, {cF, dF}} to theScreen's frame()
	set {{aV, bV}, {cV, dV}} to theScreen's visibleFrame()
	return {aV as integer, (dF - bV - dV) as integer, (aV + cV) as integer, (dF - bV) as integer}
end visibleDisplayBounds

main()

For windows that are “active” but do not show (e.g. Boinc, AAMPS, etc) on the desktop, what modifications would be necessary to include them or exclude them in the script?

I’ve modified the script in post 1 to include a doNotTile list.

This script is similar to that above, differing in that it displays a dialog in which the user can select the apps to tile. It should be noted that:

  • If one of the selected apps has the focus, that app will be tiled in the first position at the upper-left of the screen.

  • The order of the apps shown in the dialog is the order in which they will be tiled.

  • The script has a do-not-tile list but this is only intended for use with apps that do not have visible windows.

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set {appCount, openApps, tileAllApps} to getAppData()
	set windowBounds to getWindowData(appCount)
	tileApps(openApps, windowBounds, tileAllApps)
end main

on getAppData()
	set doNotTile to {} -- intended for use with apps that do not have a visible window
	set theWorkspace to current application's NSWorkspace's sharedWorkspace()
	set activeApp to theWorkspace's frontmostApplication()
	set openApps to theWorkspace's runningApplications()
	set thePredicates to current application's NSPredicate's predicateWithFormat:"activationPolicy == 0 AND NOT localizedName IN %@" argumentArray:{doNotTile}
	set openApps to (openApps's filteredArrayUsingPredicate:thePredicates)'s mutableCopy()
	
	tell application "Finder" to set finderWindow to (exists Finder window 1)
	if finderWindow = false then openApps's removeObjectAtIndex:0
	set appCountOne to openApps's |count|()
	if appCountOne < 2 or appCountOne > 9 then errorAlert("Less than 2 or more than 9 apps were found")
	
	if (openApps's containsObject:activeApp) as boolean = true then
		openApps's removeObject:activeApp
		openApps's insertObject:activeApp atIndex:0
	end if
	
	set dialogList to (openApps's valueForKey:"localizedName") as list
	set selectedApps to (choose from list dialogList with prompt "Select two or more apps to tile:" default items (item 1 of dialogList) with multiple selections allowed) -- delete item 1 of to preselect all apps
	if selectedApps = false then error number -128
	set thePredicate to current application's NSPredicate's predicateWithFormat:"localizedName IN %@" argumentArray:{selectedApps}
	set openApps to ((openApps's filteredArrayUsingPredicate:thePredicate)'s valueForKey:"processIdentifier")
	
	set appCountTwo to openApps's |count|()
	if appCountTwo < 2 or appCountTwo > 9 then errorAlert("One or more than 9 apps were selected")
	set tileAllApps to appCountOne = appCountTwo
	set openApps to openApps as list
	
	return {appCountTwo, openApps, tileAllApps}
end getAppData

on getWindowData(windowCount)
	set wB to 5 -- window buffer
	set windowSetting to {{1, 1}, {1, 2}, {2, 2}, {2, 3}, {3, 3}, {2, 2, 3}, {2, 3, 3}, {3, 3, 3}}
	set windowSetting to item (windowCount - 1) of windowSetting
	set columnCount to (count windowSetting)
	set {x1, y1, x2, y2} to getDisplayBounds() -- usable display bounds
	
	set wFH to ((y2 - y1) - (wB * 2)) div 1 -- window heights
	set wHH to ((y2 - y1) - (wB * 3)) div 2
	set wTH to ((y2 - y1) - (wB * 4)) div 3
	
	if columnCount = 2 then -- column widths
		set wW to ((x2 - x1) - (wB * 3)) div 2
	else
		set wW to ((x2 - x1) - (wB * 4)) div 3
	end if
	
	set windowBounds to {}
	set {x, y} to {x1, y1}
	repeat with i from 1 to columnCount -- loop through columns
		repeat with j from 1 to item i of windowSetting -- loop through windows in a column
			if item i of windowSetting = 1 then
				set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wW))), (y + (j * wB) + ((j - 1) * wFH))}, {wW, wFH}}
			else if item i of windowSetting = 2 then
				set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wW))), (y + (j * wB) + ((j - 1) * wHH))}, {wW, wHH}}
			else
				set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wW))), (y + (j * wB) + ((j - 1) * wTH))}, {wW, wTH}}
			end if
		end repeat
	end repeat
	
	return windowBounds
end getWindowData

on tileApps(openApps, windowBounds, tileAllApps)
	tell application "System Events"
		repeat with i from (count openApps) to 1 by -1
			tell (first process whose unix id is (item i of openApps))
				tell every window
					set position to item 1 of item i of windowBounds
					set size to item 2 of item i of windowBounds
				end tell
				if tileAllApps = false then
					set frontmost to true
					delay 0.1
				end if
			end tell
		end repeat
	end tell
end tileApps

on getDisplayBounds()
	set theScreen to current application's NSScreen's mainScreen()
	set {{aF, bF}, {cF, dF}} to theScreen's frame()
	set {{aV, bV}, {cV, dV}} to theScreen's visibleFrame()
	return {aV as integer, (dF - bV - dV) as integer, (aV + cV) as integer, (dF - bV) as integer}
end getDisplayBounds

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()

Just as a side note, for those looking for a simpler solution, recent version of macOS have the ability to tile windows to the left and right side of the screen. And, for those running Monterey, the Shortcuts app includes “Get Organized” shortcuts that will tile apps. After trying these, I decided to use the second script above, but in the end this is just a matter of personal preference.

This script differs from that in post 4 above in that the user can set the width of a window column as a percentage of the usable display width. To simplify somewhat, the script only supports a tiling grid two columns wide and three rows high. All of the user settings are contained in the first few lines of the getWindowBounds and getAppData handlers.

-- Revised 2022.02.06

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set {appCount, openApps, tileAllApps} to getAppData()
	set windowBounds to getWindowBounds(appCount)
	tileApps(openApps, tileAllApps, windowBounds)
end main

on getAppData()
	set doNotTile to {} -- intended for use with apps that do not have a visible window
	set theWorkspace to current application's NSWorkspace's sharedWorkspace()
	set activeApp to theWorkspace's frontmostApplication()
	set openApps to theWorkspace's runningApplications()
	set thePredicates to current application's NSPredicate's predicateWithFormat:"activationPolicy == 0 AND NOT localizedName IN %@" argumentArray:{doNotTile}
	set openApps to (openApps's filteredArrayUsingPredicate:thePredicates)'s mutableCopy()
	
	tell application "Finder" to set finderWindow to (exists Finder window 1)
	if finderWindow = false then openApps's removeObjectAtIndex:0
	set appCountOne to openApps's |count|()
	if appCountOne < 2 then errorAlert("One or no active apps were found")
	
	if (openApps's containsObject:activeApp) as boolean = true then
		openApps's removeObject:activeApp
		openApps's insertObject:activeApp atIndex:0
	end if
	
	set dialogList to (openApps's valueForKey:"localizedName") as list
	set selectedApps to (choose from list dialogList with prompt "Select two or more apps to tile:" default items dialogList with multiple selections allowed)
	if selectedApps = false then error number -128
	set thePredicate to current application's NSPredicate's predicateWithFormat:"localizedName IN %@" argumentArray:{selectedApps}
	set openApps to ((openApps's filteredArrayUsingPredicate:thePredicate)'s valueForKey:"processIdentifier")
	
	set appCountTwo to openApps's |count|()
	if appCountTwo < 2 or appCountTwo > 6 then errorAlert("One or more than 6 apps were selected")
	set tileAllApps to appCountOne = appCountTwo
	set openApps to openApps as list
	
	return {appCountTwo, openApps, tileAllApps}
end getAppData

on getWindowBounds(windowCount)
	set wB to 5 -- window buffer
	set windowSetting to {{1, 1}, {1, 2}, {2, 2}, {2, 3}, {3, 3}} -- tiling grid for 2 thru 6 apps
	set windowRatio to {55, 55, 50, 55, 50} -- column width percent for 2 thru 6 apps
	set windowSetting to item (windowCount - 1) of windowSetting
	set windowRatio to item (windowCount - 1) of windowRatio
	set {x1, y1, x2, y2} to getDisplayBounds() -- usable display bounds
	
	set wFH to ((y2 - y1) - (wB * 2)) div 1 -- window heights
	set wHH to ((y2 - y1) - (wB * 3)) div 2
	set wTH to ((y2 - y1) - (wB * 4)) div 3
	
	set wLW to ((x2 - x1) - (wB * 3)) * windowRatio div 100 -- window widths
	set wRW to ((x2 - x1) - (wB * 3)) * (100 - windowRatio) div 100
	
	set windowBounds to {}
	set {x, y} to {x1, y1}
	repeat with i from 1 to 2 -- loop through columns
		repeat with j from 1 to item i of windowSetting -- loop through windows in a column
			set columnWindowCount to item i of windowSetting
			if i = 1 then
				if columnWindowCount = 1 then
					set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wLW))), (y + (j * wB) + ((j - 1) * wFH))}, {wLW, wFH}}
				else if columnWindowCount = 2 then
					set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wLW))), (y + (j * wB) + ((j - 1) * wHH))}, {wLW, wHH}}
				else
					set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wLW))), (y + (j * wB) + ((j - 1) * wTH))}, {wLW, wTH}}
				end if
			else
				if columnWindowCount = 1 then
					set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wLW))), (y + (j * wB) + ((j - 1) * wFH))}, {wRW, wFH}}
				else if columnWindowCount = 2 then
					set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wLW))), (y + (j * wB) + ((j - 1) * wHH))}, {wRW, wHH}}
				else
					set end of windowBounds to {{(x + (i * wB) + ((i - 1) * (wLW))), (y + (j * wB) + ((j - 1) * wTH))}, {wRW, wTH}}
				end if
			end if
		end repeat
	end repeat
	
	return windowBounds
end getWindowBounds

on tileApps(openApps, tileAllApps, windowBounds)
	tell application "System Events"
		repeat with i from (count openApps) to 1 by -1
			tell (first process whose unix id is (item i of openApps))
				tell every window
					set position to item 1 of item i of windowBounds
					set size to item 2 of item i of windowBounds
				end tell
				if tileAllApps = false then
					set frontmost to true
					delay 0.1 -- test different values
				end if
			end tell
		end repeat
	end tell
end tileApps

on getDisplayBounds()
	set theScreen to current application's NSScreen's mainScreen()
	set {{aF, bF}, {cF, dF}} to theScreen's frame()
	set {{aV, bV}, {cV, dV}} to theScreen's visibleFrame()
	return {aV as integer, (dF - bV - dV) as integer, (aV + cV) as integer, (dF - bV) as integer}
end getDisplayBounds

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()

The following script is similar to that above, the primary difference being that apps not selected by the user in the dialog are hidden rather than placed behind the tiled apps.

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set {appCount, openApps, skipApps} to getAppData()
	set windowBounds to getWindowBounds(appCount)
	tileApps(openApps, windowBounds, skipApps)
end main

on getAppData()
	set doNotTile to {} -- intended for use with apps that do not have a visible window
	set theWorkspace to current application's NSWorkspace's sharedWorkspace()
	set activeApp to theWorkspace's frontmostApplication()
	set openApps to theWorkspace's runningApplications()
	set thePredicates to current application's NSPredicate's predicateWithFormat:"activationPolicy == 0 AND NOT localizedName IN %@" argumentArray:{doNotTile}
	set openApps to (openApps's filteredArrayUsingPredicate:thePredicates)'s mutableCopy()
	
	tell application "Finder"
		if (exists Finder window 1) then
			set finderWindow to true
		else if visible = false then
			set finderWindow to true
		else
			set finderWindow to false
		end if
	end tell
	
	if finderWindow = false then openApps's removeObjectAtIndex:0
	if (openApps's |count|()) < 2 then errorAlert("One or no active apps were found")
	
	if (openApps's containsObject:activeApp) as boolean = true then
		openApps's removeObject:activeApp
		openApps's insertObject:activeApp atIndex:0
	end if
	
	set dialogList to (openApps's valueForKey:"localizedName") as list
	set selectedApps to (choose from list dialogList with prompt "Select two or more apps to tile:" default items dialogList with multiple selections allowed)
	if selectedApps = false then error number -128
	set appCount to (count selectedApps)
	if appCount < 2 or appCount > 6 then errorAlert("One or more than 6 apps were selected")
	set thePredicate to current application's NSPredicate's predicateWithFormat:"NOT localizedName IN %@" argumentArray:{selectedApps}
	set skipApps to ((openApps's filteredArrayUsingPredicate:thePredicate)'s valueForKey:"processIdentifier")
	set thePredicate to current application's NSPredicate's predicateWithFormat:"localizedName IN %@" argumentArray:{selectedApps}
	set openApps to ((openApps's filteredArrayUsingPredicate:thePredicate)'s valueForKey:"processIdentifier")
	
	return {appCount, openApps as list, skipApps as list}
end getAppData

on getWindowBounds(windowCount)
	set wB to 5 -- window buffer
	set windowSetting to {{1, 1}, {1, 2}, {2, 2}, {2, 3}, {3, 3}} -- tiling grid for 2 thru 6 apps
	set windowRatio to {55, 55, 50, 55, 50} -- percent column width for 2 thru 6 apps
	set windowSetting to item (windowCount - 1) of windowSetting
	set windowRatio to item (windowCount - 1) of windowRatio
	set {x1, y1, x2, y2} to getDisplayBounds() -- usable display bounds
	
	set wFH to ((y2 - y1) - (wB * 2)) div 1 -- window heights
	set wHH to ((y2 - y1) - (wB * 3)) div 2
	set wTH to ((y2 - y1) - (wB * 4)) div 3
	
	set wLW to ((x2 - x1) - (wB * 3)) * windowRatio div 100 -- window widths
	set wRW to ((x2 - x1) - (wB * 3)) * (100 - windowRatio) div 100
	
	set windowBounds to {}
	repeat with i from 1 to 2 -- loop through columns
		repeat with j from 1 to item i of windowSetting -- loop through windows in a column
			set columnWindowCount to item i of windowSetting
			if i = 1 then
				if columnWindowCount = 1 then
					set end of windowBounds to {{(x1 + wB), (y1 + wB)}, {wLW, wFH}}
				else if columnWindowCount = 2 then
					set end of windowBounds to {{(x1 + wB), (y1 + (j * wB) + ((j - 1) * wHH))}, {wLW, wHH}}
				else
					set end of windowBounds to {{(x1 + wB), (y1 + (j * wB) + ((j - 1) * wTH))}, {wLW, wTH}}
				end if
			else
				if columnWindowCount = 1 then
					set end of windowBounds to {{(x1 + (i * wB) + ((i - 1) * (wLW))), (y1 + wB)}, {wRW, wFH}}
				else if columnWindowCount = 2 then
					set end of windowBounds to {{(x1 + (i * wB) + ((i - 1) * (wLW))), (y1 + (j * wB) + ((j - 1) * wHH))}, {wRW, wHH}}
				else
					set end of windowBounds to {{(x1 + (i * wB) + ((i - 1) * (wLW))), (y1 + (j * wB) + ((j - 1) * wTH))}, {wRW, wTH}}
				end if
			end if
		end repeat
	end repeat
	
	return windowBounds
end getWindowBounds

on tileApps(openApps, windowBounds, skipApps)
	tell application "System Events"
		repeat with i from 1 to (count skipApps)
			tell (first process whose unix id is (item i of skipApps))
				set visible to false
				delay 0.1
			end tell
		end repeat
		
		repeat with i from (count openApps) to 1 by -1
			tell (first process whose unix id is (item i of openApps))
				tell every window
					set position to item 1 of item i of windowBounds
					set size to item 2 of item i of windowBounds
				end tell
				if (visible = false) then set visible to true
			end tell
		end repeat
	end tell
end tileApps

on getDisplayBounds()
	set theScreen to current application's NSScreen's mainScreen()
	set {{aF, bF}, {cF, dF}} to theScreen's frame()
	set {{aV, bV}, {cV, dV}} to theScreen's visibleFrame()
	return {aV as integer, (dF - bV - dV) as integer, (aV + cV) as integer, (dF - bV) as integer}
end getDisplayBounds

on errorAlert(dialogMessage)
	display alert "An error has occurred" message dialogMessage as critical
	error number -128
end errorAlert

main()