Script to memorize and restore windows to specific (MC) desktops

One reason I don’t reboot very often is because I use the desktops in mission control as project spaces. I typically have 3 to 9 projects in active use each week. Each project typically have a few windows from: Safari, Text Edit, Stickies, and Terminal, plus possibly another app or 2. I use Stickies in large font tucked under the dock to label each desktop (so I can identify the project easily in MC - because it doesn’t show the dock). If I have to reboot, that’s a lot of windows all getting merged onto desktop 1 and it takes awhile to sort them out.

If I could just figure out a way to automatically re-distribute Stickies, Text Edit, and Terminal windows after reboot, that would solve 90% of my problem. (I think somehow Safari remembers which desktop each window was on.)

Does anyone have any pointers for a script that could:

  1. Trigger upon shutdown to memorize which desktop each window of a list of specific apps is on
  2. Trigger after reboot (or even manually trigger) to restore the memorized windows to their pre-reboot desktop

Just thinking off the top of my head, I can imagine a script that, to memorize desktop assignments of each window, cycles through the desktops and switches to each app and cycles through the windows to record what desktop they’re on in a lookup (text) file.

The problem is, I don’t know the Applescript elements or any key commands to send a window to a specific desktop. Does anyone know how to do this?

Browser: Safari 605.1.15
Operating System: macOS 10.14

I managed to create a script that can memorize what windows are open on which desktops. As a proof of concept, it’s OK, albeit hacky. It needs however to save the windows and save them in a way I can manipulate them later using a companion script instead of just as a set of window names displayed in a dialog. It has a couple issues:

  1. Stickies window names are truncated to not include hard returns (possibly making the window name unusable)
  2. All 3 of my Fluid apps are identified inaccurately as FluidApp.app
  3. My method of determining the number of desktops present is not likely to work on all versions of macOS
  4. I don’t have a way of omitting apps assigned to all desktops

Any help in figuring out how to move windows to their original desktops (as recorded by some improved version of this script) would be appreciated. I had had an idea to simply quit an app and then launch it using a shell command like open -a app -F and then just switching desktops using keystrokes (like the below script does) and opening the associated files when I’m on that desktop. That could work in a limited fashion, however the -F argument doesn't work for untitled/unsaved documents. Those always open regardless of -F`. That would mean however that I would have to get the file paths associated with the open windows in the script below. I would settle for what is possible this way and just let the untitled docs end up wherever. That would be better than nothing. Though I could launch the app from the desktop where the first untitled doc was open…


--This applescript reports a list of application windows present on each desktop
--This is only a sample script intended to eventually be used to restore all application windows to their pre-reboot desktops

tell application "System Events"
	
	set windows_string to ""
	set numDesktops to (first paragraph of (do shell script "strings ~/Library/Preferences/com.apple.spaces.plist | grep -c ^\\\\$")) + 1
	--the following tcsh command can determine the number of desktops:
	-- @ x = ( `strings ~/Library/Preferences/com.apple.spaces.plist | grep -c '^\$'` + 1 ); echo $x
	
	-- switch to the first desktop:
	repeat with aDesktop from 1 to numDesktops
		key code 123 using {control down}
	end repeat
	
	repeat with aDesktop from 1 to numDesktops
		
		set windows_string to windows_string & return & return & "Desktop " & (aDesktop as string) & return
		delay 1
		get (the name of every application process whose class of windows contains window)
		
		repeat with P in the result
			
			set windows_string to windows_string & return & return & P & return
			
			get (every window of process (contents of P) whose value of attribute "AXMinimized" is false)
			
			repeat with W in the result
				
				set window_name to ((name of W) as string)
				
				--Stickies window names can be multi-line, so this trims from the first hard return onward
				set better_window_name to (my replacePattern:"[\\n].*" inString:window_name usingThis:"")
				if window_name is not equal to "" then
					try
						set windows_string to windows_string & better_window_name & return
					on error
						set windows_string to windows_string & "couldn't get window name" & return
					end try
				end if
				
			end repeat
			
		end repeat
		
		--switch to the next desktop
		key code 124 using {control down}
		
	end repeat
	
	display dialog "List of windows on this desktop: " & return & return & windows_string
	
end tell

--Call like this: set res to my replacePattern:"\\s+" inString:"1 subtratcing-these: -2 3 4" usingThis:"-"
use framework "Foundation"
use scripting additions

on replacePattern:thePattern inString:theString usingThis:theTemplate
	set theRegEx to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set theResult to theRegEx's stringByReplacingMatchesInString:theString options:0 range:{location:0, |length|:length of theString} withTemplate:theTemplate
	return theResult as text
end replacePattern:inString:usingThis:

I’m another step closer…

This is another proof of concept. And it works on untitled documents too. Despite this example, I wouldn’t use it for Safari windows because:

  1. It loses the window history
  2. Safari already restores windows to their original desktops

But regardless, I should be able to use this code to update the first script to get & save document file paths and then to process the saved paths and close & reopen windows on the target desktop by switching to them using control-arrow keystrokes…


--This is a proof of concept that docs open in an app can be closed on another desktop and be reopened on another desktop.  It works with untitled documents as well as saved documents.  When an app launches (even with open ... -F) all reopened windows open on the current desktop instead of the desktop where they were before a reboot.  This simply proves that you can close & re-open the docs on whichever desktop you are currently on.  Combined with my "ReportWindowsOnEachDesktop" script, this could allow all windows to be re-opened on their original desktop...

--switch to a different desktop and run...
tell application "Safari"
	set p to get URL of document 1 of application "Safari"
	close document 1
	--display dialog p
	do shell script "open '" & p & "'"
end tell

tell application "TextEdit"
	set doclist to every document
	set pathlist to {}
	repeat with adoc in doclist
		--display dialog path of adoc as text
		set p to path of adoc as text
		set end of pathlist to p
		--display dialog p
		close adoc saving yes
	end repeat
	repeat with apath in pathlist
		--display dialog path of adoc as text
		--display dialog p
		do shell script "open -a 'TextEdit' '" & apath & "'"
	end repeat
	return doclist
end tell

I’m still making progress. This is a modification of the first script to save the desktop, app, and path of each open document. It unfortunately doesn’t work with Stickies, but I have an idea on how to possibly circumvent that issue. This script skips any app that doesn’t have open “document” windows.


--This applescript reports a list of application windows present on each desktop
--This is only a sample script intended to eventually be used to restore all application windows to their pre-reboot desktops

tell application "System Events"
	
	set windows_string to ""
	set docs_string to ""
	set numDesktops to (first paragraph of (do shell script "strings ~/Library/Preferences/com.apple.spaces.plist | grep -c ^\\\\$")) + 1
	
	-- switch to the first desktop:
	repeat with aDesktop from 1 to numDesktops
		key code 123 using {control down}
	end repeat
	
	repeat with aDesktop from 1 to numDesktops
		
		set windows_string to windows_string & return & return & "Desktop " & (aDesktop as string) & return
		delay 1
		get (the name of every application process whose class of windows contains window)
		
		repeat with P in the result
			
			set windows_string to windows_string & return & return & P & return
			set alldocs to {}
			set doclist to my getAppDocs(P)
			set docnames to first item in doclist
			set docpaths to second item in doclist
			set adoc to null
			
			--For some reason, this stopped working for me: 'whose value of attribute "AXMinimized" is false' so I commented it out
			get (every window of process (contents of P)) -- whose value of attribute "AXMinimized" is false)
			
			repeat with W in the result
				
				set window_name to ((name of W) as string)
				try
					set attrs to attributes of W
					display dialog attrs
				end try
				--Stickies window names can be multi-line, so this trims from the first hard return onward
				--Stickies isn't applescript-able. I may however be able to use my "StickiesBackup2" script to circumvent that issue
				set better_window_name to (my replacePattern:"[\\n].*" inString:window_name usingThis:"")
				
				if window_name is not equal to "" then
					try
						set i to 0
						repeat with listdoc in docnames
							set i to i + 1
							if (listdoc as string) is window_name then
								--set adoc to item i of alldocs
								set adoc to item i of docpaths
							end if
						end repeat
						set adocpath to adoc
						if adocpath is not null then
							set docs_string to docs_string & "Desktop " & (aDesktop as string) & tab & P & tab & adocpath & return
						end if
						--on error errstr
						--	set docs_string to docs_string & "Desktop " & (aDesktop as string) & tab & P & tab & "ERROR for " & better_window_name & ": " & errstr & return
					end try
					
					try
						set windows_string to windows_string & better_window_name & return
						--on error
						--	set windows_string to windows_string & "couldn't get window name" & return
					end try
				else
					--set docs_string to docs_string & "Desktop " & (aDesktop as string) & tab & P & tab & "no docs" & return
				end if
				
			end repeat
			
		end repeat
		
		--switch to the next desktop
		key code 124 using {control down}
		
	end repeat
	
	display dialog "List of documents on each desktop: " & return & return & docs_string
	
end tell

on getAppDocs(appName)
	set doclist to {}
	
	set myscript to "tell application \"" & appName & "\"" & return
	set myscript to myscript & tab & "set docnames to {}" & return
	set myscript to myscript & tab & "set docpaths to {}" & return
	set myscript to myscript & tab & "try" & return
	set myscript to myscript & tab & tab & "set docnames to name of every document" & return
	set myscript to myscript & tab & tab & "set docpaths to path of every document" & return
	set myscript to myscript & tab & "end try" & return
	set myscript to myscript & tab & "return {docnames,docpaths}" & return
	set myscript to myscript & "end tell" & return
	
	try
		run script myscript
		set doclist to result as list
	on error errstr
		display dialog "Error running script:" & return & return & myscript & return & return & "Error: " & errstr
	end try
	
	return doclist
end getAppDocs

--Call like this: set res to my replacePattern:"\\s+" inString:"1 subtratcing-these: -2 3 4" usingThis:"-"
use framework "Foundation"
use scripting additions
on replacePattern:thePattern inString:theString usingThis:theTemplate
	set theRegEx to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set theResult to theRegEx's stringByReplacingMatchesInString:theString options:0 range:{location:0, |length|:length of theString} withTemplate:theTemplate
	return theResult as text
end replacePattern:inString:usingThis:

It also assumes that window names are the same as the file names (which is not necessarily true).