How Do I Manipulate Windows in AppleScript?

In addition to my above macro, here are some other examples:

Did you take a look at:

Thanks, @JMichaelTX, my initial question was concerning the fact that my script was only running in Script editor or Script debugger but not in KM or from the menu bar. It has since morphed into how to manipulate windows be it with AppleScript OR with keyboard maestro.

Hi @JeffLambert670,

I had a quick look at your script and noticed it could be modified in a couple of ways that you might find useful.

Firstly, rather than sending your commands to the application itself via tell application my Application (which limits you to controlling windows only belonging to those applications that are AppleScript-able), sending the commands through System Events allows you to control the windows of (almost) any application. You would then use size and position properties of the application process's windows rather than the bounds property of the application's windows.

Secondly, since we're already using System Events, it means we can also easily obtain the screen's width and height programmatically, instead of having to supply the dimensions ourselves.

Lastly, the positioning and sizing of windows follows a distinct, consistent mathematical pattern, which allows us to streamline the process that sets these properties and apply it to an arbitrary number of windows without having to examine if...then...else clauses:

use application "System Events"

set frontApp to the name of the first process whose frontmost is true

tell process "Finder"
	set [screenWidth, screenHeight] to the size of scroll area 1
	set [null, menubarHeight] to the size of menu bar 1
end tell

tell the process named frontApp
	set myWindowsCount to the number of windows
	set N to 0.5 * myWindowsCount as integer
	set myWidth to screenWidth / N as integer
	set myHeight to (screenHeight - menubarHeight) / 2 as integer
	repeat with i from 1 to myWindowsCount
		script win
			property a : (i - 1) mod N
			property b : (i - 1) div N
			property rect : [¬
				myWidth * a, ¬
				myHeight * b + menubarHeight, ¬
				myWidth * (a + 1), ¬
				myHeight * b + myHeight + menubarHeight] as bounding rectangle
			
			on width()
				(rect's item 3) - (rect's item 1)
			end width
			
			on height()
				(rect's item 4) - (rect's item 2)
			end height
		end script
		set window i's position to win's rect as point
		set window i's size to [win's width(), win's height()]
	end repeat
end tell

OH man, I was so sure there was a better way to do it!

But unfortunately, your script doesn't do anything:-( I've tried to save it and run it via execute script file, and execute text script in KM and nothing happens.

Works for me. Run it in Script Editor and see if it organises Script Editor's windows.

I also tested it in KM, having created a quick macro like so:

Again, seems to do what it's supposed to.

AppleScript code
use application "System Events"

set frontApp to the name of the first process whose frontmost is true

tell process "Finder"
	set [screenWidth, screenHeight] to the size of scroll area 1
	set [null, menubarHeight] to the size of menu bar 1
end tell

tell the process named frontApp
	set myWindowsCount to the number of windows
	set N to 0.5 * myWindowsCount as integer
	set myWidth to screenWidth / N as integer
	set myHeight to (screenHeight - menubarHeight) / 2 as integer
	repeat with i from 1 to myWindowsCount
		script win
			property a : (i - 1) mod N
			property b : (i - 1) div N
			property rect : [¬
				myWidth * a, ¬
				myHeight * b + menubarHeight, ¬
				myWidth * (a + 1), ¬
				myHeight * b + myHeight + menubarHeight] as rectangle
			
			on width()
				(rect's item 3) - (rect's item 1)
			end width
			
			on height()
				(rect's item 4) - (rect's item 2)
			end height
		end script
		set window i's position to win's rect as point
		set window i's size to [win's width(), win's height()]
	end repeat
end tell

Thanks CJK,

I seem to have not copied it correctly the first time, the closing bracket was after as rectangle.

It works, but it puts the tiling in the wrong orientation if you have 2 windows, meaning, they are split horizontally, one on top, one on the bottom instead of side to side. Also, if you have a number like 5 it does the layout as 4 windows, not six -1, which is weird because 3 and 7 work fine.

Thanks for the feedback. It was a quick draft that I, admittedly, hadn't thoroughly tested. Let me attend to those bits and update you. The principle is there, though. The issue with 5 is AppleScript being annoying, and rounding 2.5 down to 2 instead of up to 3. That fix, at least, is an easy one.

Here you go, @JeffLambert670. This should address the issues you encountered. Essentially, the two-window scenario is a special case.

Revised AppleScript
use application "System Events"

set frontApp to the name of the first process whose frontmost is true

tell process "Finder"
	set [screenWidth, screenHeight] to the size of scroll area 1
	set [null, menubarHeight] to the size of menu bar 1
end tell

tell the process named frontApp
	set myWindowsCount to the number of windows
	set N to 0.5 * myWindowsCount + 0.01 as integer
	set myWidth to screenWidth / N as integer
	set myHeight to (screenHeight - menubarHeight) / 2 as integer
	
	if myWindowsCount ≤ 2 then
		set N to 2
		set myHeight to myHeight * 2
		set myWidth to myWidth / myWindowsCount
	end if
	
	repeat with i from 1 to myWindowsCount
		script win
			property a : (i - 1) mod N
			property b : (i - 1) div N
			property rect : [¬
				myWidth * a, ¬
				myHeight * b + menubarHeight, ¬
				myWidth * (a + 1), ¬
				myHeight * b + myHeight + menubarHeight] as bounding rectangle
			
			on width()
				(rect's item 3) - (rect's item 1)
			end width
			
			on height()
				(rect's item 4) - (rect's item 2)
			end height
		end script
		set window i's position to win's rect as point
		set window i's size to [win's width(), win's height()]
	end repeat
end tell

Thanks for your help CJK, if I may add a few comments.

If you run the script on 7 windows or more, the width of some windows is a bit larger than what they should. I've tested this with Safari. If I take my script, it can resize those windows fine, so it's not that those have a minimum width. If you have more than 9, none of them have the right width. Maybe the math work up to 6?

Also, how would you isolate windows that are only on the main monitor, but not count and resize windows on a second monitor (which is on the left of the first monitor)? Again, thanks for your help with this.

There's nothing special about the number 6 that would cause the maths to falter. And, in testing this further—specifically the cases you mentioned, with 7 or more and 9 or more windows—I can't reproduce the error you're seeing.

Here's a screenshot with 10 windows of TextEdit having just run the script:

I'm afraid I don't have experience working with multiple monitors, so can't assist you in this regard. My apologies.

that's weird, I was doing my testing with Safari. I get the wrong size in it. If I do it with BBEDIT or TextEdit, it works. If I do it with Word, it does until 8, at 9 it doesn't because word will have a minimum width window size. What is weird is that it works for my script in Safari when it's 7 and 8 windows, but not yours, at least not consistently. At 9 and 10 windows, I hit the minimum width of Safari's window. Oh but wait, I'm trying this on my Macbook pro which is not connected to my second monitor at the moment and has a 1920X1200 resolution. If you're running this on a 5k iMac, you won't have the same problem.

@JeffLambert670, it sounds like the limits imposed on window size are the issue rather than the script. It's odd that the limits seems to be imposed differently for your script versus mine, but we have used two different means of setting the window size.

If you want to have your cake and eat it too, then this script might solve the problem:

Revised AppleScript with handlers
tell application "System Events"
	set frontApp to the name of the first process whose frontmost is true
	
	tell process "Finder"
		set [screenWidth, screenHeight] to the size of scroll area 1
		set [null, menubarHeight] to the size of menu bar 1
	end tell
	
	if the process named frontApp's has scripting terminology then
		my targetScriptableApp:frontApp
	else
		my targetSystemEvents:frontApp
	end if
end tell

to targetScriptableApp:frontApp
	global screenWidth, screenHeight, menubarHeight
	
	tell the application named frontApp
		set myWindowsCount to the number of windows
		set N to 0.5 * myWindowsCount + 0.01 as integer
		set myWidth to screenWidth / N as integer
		set myHeight to (screenHeight - menubarHeight) / 2 as integer
		
		if myWindowsCount ≤ 2 then
			set N to 2
			set myHeight to myHeight * 2
			set myWidth to myWidth / myWindowsCount
		end if
		
		repeat with i from 1 to myWindowsCount
			set a to (i - 1) mod N
			set b to (i - 1) div N
			set window i's bounds to [¬
				myWidth * a, ¬
				myHeight * b + menubarHeight, ¬
				myWidth * (a + 1), ¬
				myHeight * b + myHeight + menubarHeight]
		end repeat
	end tell
end targetScriptableApp:

to targetSystemEvents:frontApp
	global screenWidth, screenHeight, menubarHeight
	
	tell application "System Events" to tell the process named frontApp
		set myWindowsCount to the number of windows
		set N to 0.5 * myWindowsCount + 0.01 as integer
		set myWidth to screenWidth / N as integer
		set myHeight to (screenHeight - menubarHeight) / 2 as integer
		
		if myWindowsCount ≤ 2 then
			set N to 2
			set myHeight to myHeight * 2
			set myWidth to myWidth / myWindowsCount
		end if
		
		repeat with i from 1 to myWindowsCount
			script win
				property a : (i - 1) mod N
				property b : (i - 1) div N
				property rect : [¬
					myWidth * a, ¬
					myHeight * b + menubarHeight, ¬
					myWidth * (a + 1), ¬
					myHeight * b + myHeight + menubarHeight] as rectangle
				
				on width()
					(rect's item 3) - (rect's item 1)
				end width
				
				on height()
					(rect's item 4) - (rect's item 2)
				end height
			end script
			set window i's position to win's rect as point
			set window i's size to [win's width(), win's height()]
		end repeat
	end tell
end targetSystemEvents:

It has one handler that sizes windows using System Events, and one handler that sizes windows using the scriptable components of the application in question. It picks the most appropriate handler to use depending on whether or not the application is AppleScript-able.

@CJK, you hit the nail on the head!

Minimum size for windows:

Hey CJ,

Vanilla AppleScript considers multiple monitors to be one big Desktop.

----------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2018/07/20 04:38
# dMod: 2018/07/20 04:38 
# Appl: Miscellaneous
# Task: Handler for returning the bounds of the Desktop.
#     : Considers multiple monitors as one big desktop from left to right.
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @Finder, @Handler, @Desktop, @Bounds
----------------------------------------------------------------

desktopBounds()

----------------------------------------------------------------
--» HANDLERS
----------------------------------------------------------------
on desktopBounds()
   tell application "Finder"
      set _bounds to bounds of window of desktop
      return _bounds
   end tell
end desktopBounds
----------------------------------------------------------------

From there you have to figure out the coordinates of each monitor.

I used to have quite a few multi-monitor scripts, but unfortunately those have been lost for some time now.

It would probably be worthwhile to search MacScripter.net for:

window multiple monitor

And the Applescript Users List for:

multiple monitor site:http://lists.apple.com/archives/applescript-users

-Chris

1 Like

@ccstone yeah, I got the error too when trying to run @CJK on a dual monitor setup (at work). So I found that if you add at the beginning :

set screenWidth to {word 3 of (do shell script "defaults read /Library/Preferences/com.apple.windowserver | grep -w Width") as number}
set screenHeight to {word 3 of (do shell script "defaults read /Library/Preferences/com.apple.windowserver | grep -w Height") as number}

and remove the line

set [screenWidth, screenHeight] to the size of scroll area 1

the script runs correctly.

I have a couple of clues as to how to go about isolating the second monitor with the links you provided but man, this is way more complicated than I had anticipated. This script is for my father who has problem saving his document at the right place, so I just wanted a little script that would help set him up with all is Word documents. The tiling fonction in word is NOT what he wants. Anyway, thank you all for your help!

Well, it doesn't work when running from my MacBook pro only, it's like it sees another resolution or the old resolution from that file. I'll probably have to make an if else statement in KM and run two scripts. Or find a way to deal with variables from KM and put them in my Applescript.

There is no reason to depend on shell scripts for this, when both AppleScript (using ASObjC) and KM native Actions/Tokens/Functions can easily tell you which screen contains the frontmost window.

I'm not sure what other suggestions to offer at this point, because I'm confused on what exactly your current objective is.

In your OP, you stated:

Has that question/goal been solved?

If so, please:

  1. Check the "Solved" checkbox (click for details) at the bottom of that post that best solves it.
  2. Post a new topic if you have other questions.

If not, please tell us specifically what the issue is, and post the script/macro that you are currently trying to make work.

Thanks.

Thanks @JMichaelTX I know I'm a bit all over the place. I try to take baby steps to resolve problems. In that case, i wanted a solution to tile windows on the main monitor. Ultimate goal was to be able to tile windows on the main monitor but ignoring those on the second monitor, which my script still doesn't do. After thinking about it, the option to tile windows on the monitor where the cursor is currently that would solve all my problem. Since you're the expert here, how would you go about doing that? I only need to tile windows from the same app, but on the current monitor (meaning the one with the mouse cursor) or the active window.

The thing I'm trying to solve is that my father has about 6 documents open in word on his second monitor (which is on his left), than, he uses is main monitor to open other documents that can vary in number but that will change over time. I wanted a way for him to hit a key and have those documents magically get tiled WITHOUT affecting the windows on the second monitor. Since this is a very particular workflow, I wanted to make a macro that could be applied to more than just his senario, hensed, the macro I'm currently working on. I'm sorry if i wasn't clear about all this in the first place.

Again thanks for all your help.

When asking for help here, it is best for you and us to give us the complete picture/objective at the very beginning -- do not spoon feed us. :wink:

It sounds like you have stated a new problem. So, again, I'd ask you:

I thought about moving your last post to a new topic, but that is something that I think you should do after answering my above question.

I'd suggest this:

  1. First build the macro that your father needs
    • It is the simplest
    • But it will provide the building blocks for a more complicated multi-monitor macro
    • It will give your father a solution now while you work on other stuff.
  2. Then build the "ultimate" macro you now seek
    • Could be after we help you with #1, you will be able to do #2.
    • Of course, we will still help if need be.

It seems to me that @CJK has solved your OP with this post:

Perhaps it is not perfect, but it does seem to answer your OP.
I think we explained why the window size will have certain minimums in some cases.
If @CJK's post does not solve your OP, please state (without expanding the requirements) in what way specifically it does not work.

Once we get that out of the way, we can move on to your other questions, one-at-a-time. :wink: