Time Machine Assistant

I have very little experience with AppleScript, thus I welcome any suggested improvements!





PURPOSE

This script provides Time Machine information and control.

No configurations is required as all information is retrieved from tmutil and diskutil.

I created Time Machine Assistant for myself and others that I help with Time Machine. For those of us that periodically connect an external backup drive to a MacBook Pro or MacBook Air, it is important to check the status of Time Machine before disconnecting the external drive.

Also, in some cases, itโ€™s nice to run Time Machine immediately before disconnecting the drive.

-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
-- Title            : Time Machine Assistant, v1.1
-- Modified	     : 2024-04-29
-- Author	     : Jim Sauer, [@_jims](https://forum.keyboardmaestro.com/u/_jims/summary)
-- Purpose
-- This script provides Time Machine information and control. 
--
-- No configurations is required as all information is retrieved from tmutil 
-- and diskutil.
--
-- This macro provides Time Machine information and control.
--
-- I created Time Machine Assistant for myself and others that I help 
-- with Time Machine. For those of us that periodically connect an external 
-- backup drive to a MacBook Pro or MacBook Air, it is important to check 
-- the status of Time Machine before disconnecting the external drive.
--
-- Also, in some cases, itโ€™s nice to run Time Machine immediately before 
-- disconnecting the drive.
--
-- Tested With. : Sonoma 14.4.1 (23E224)/MacBookPro18,2
-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
-- Version History
-- 1.0 - initial version
-- 1.1 
-- a) Modified the method to determine myName so that the name
--    is successfully returned when the script runs within
--    Keyboard Maestro.
-- b) Updated the Purpose.
--
-- The latest version of this macro is available on the 
-- [Keyboard Maestro Forum](https://forum.keyboardmaestro.com/)
-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท

set myName to getMyName()

set tmDestinationsInfo to getTmDestinationsInfo()

set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)

set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)

set buttonList to {}

if tmStatus's backup_phase is not "" then
	
	if tmStatus's encryption then
		set encryptionStr to "Yes"
	else
		set encryptionStr to "No"
	end if
	
	set dialogResult to display dialog ยฌ
		"Time Machine Status : " & tmStatus's backup_phase & return & return & ยฌ
		"Backup Volume : " & tmStatus's label & return & ยฌ
		"Encryption : " & encryptionStr & return ยฌ
		with title myName buttons {"Interrupt & Eject", "Wait & Eject", "Cancel"} ยฌ
		default button {"Cancel"}
	
	if button returned of dialogResult is "Interrupt & Eject" then
		
		do shell script "tmutil stopbackup"
		do shell script "diskutil unmountDisk " & quoted form of tmStatus's mount_point
		display notification "Time Machine backup has been interrupted and " & ยฌ
			quoted form of tmStatus's label & " has been ejected." with title myName
		
	else if button returned of dialogResult is "Wait & Eject" then
		
		set timeoutLimit to 10
		set startTime to current date
		
		repeat
			set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
			if tmStatus's backup_phase is "" then
				exit repeat
			end if
			delay 1
			if ((current date) - startTime) > timeoutLimit then
				display dialog "Time Machine to " & tmStatus's label & ยฌ
					" was running. An attempt to stop it failed after " & timeoutLimit & ยฌ
					" seconds." with title myName buttons {"OK"} default button {"OK"}
				return "Timeout"
			end if
		end repeat
		
		do shell script "diskutil unmountDisk " & quoted form of tmStatus's mount_point
		display notification "Time Machine backup has been interrupted and '" & ยฌ
			tmStatus's label & "' has been ejected." with title myName
		
		
	end if
	
	return
	
end if

set connected_cnt to 0
set mounted_cnt to 0
set ejected_cnt to 0
set toUserMustReconnect_cnt to 0

set volumeListString to ""

repeat with volume in tmDestinationsDiskutilInfo
	
	set mountedStatus to ""
	
	if volume's connected then
		set connected_cnt to connected_cnt + 1
		if volume's status is "mounted" then
			set mounted_cnt to mounted_cnt + 1
		else if volume's status is "ejected" then
			set ejected_cnt to ejected_cnt + 1
		else if volume's status is "to use, must reconnect" then
			set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
		end if
	end if
	
	set volumeListString to volumeListString & "โ€ข " & volume's label & " (" & volume's status & ")" & return
	
end repeat

if (mounted_cnt + ejected_cnt) < 1 then
	
	set dialogResult to display dialog "Time Machine volumes:" & return & return & ยฌ
		volumeListString & return & ยฌ
		"Time Machine is not running and there a no volumes to eject." with title myName buttons {"OK"} ยฌ
		default button {"OK"}
	
	return
	
else
	
	set end of buttonList to "Cancel"
	
	if mounted_cnt > 0 then
		set beginning of buttonList to "Eject Volume"
	end if
	
	
	set mountNote to ""
	
	if (mounted_cnt + ejected_cnt + toUserMustReconnect_cnt) > 0 then
		set beginning of buttonList to "Start Time Machine"
		if ejected_cnt > 0 then
			set mountNote to return & "Note: When starting Time Machine, ejected volumes will be automatically mounted." & return
		end if
		
	end if
	
	set defaultButton to "Cancel"
	
	set dialogResult to display dialog "Time Machine volumes:" & return & return & ยฌ
		volumeListString & mountNote ยฌ
		with title myName buttons buttonList default button defaultButton
	
	if button returned of dialogResult is "Eject Volume" then
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toUserMustReconnect_cnt to 0
		
		set volumeListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set mountedStatus to ""
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if volume's status is "mounted" then
					set mounted_cnt to mounted_cnt + 1
					set theLabel to volume's label
					set mountedStatus to volume's status
					set mountedMountPoint to volume's mount_point
					set mountedId to volume's id
					set mountedDeviceIndentifier to volume's device_identifier
				else if volume's status is "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if volume's status is "to use, must reconnect" then
					set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
				end if
			end if
			
			if volume's status is not "unavailable" then
				set volumeListString to volumeListString & volume's label & " (" & volume's status & ")" & return
			end if
			
		end repeat
		
		if mounted_cnt > 1 then
			
			set volumeList to paragraphs of volumeListString
			set volumesString to listToString(volumeList)
			set thePrompt to "Select a volume to eject:"
			set selectedVolume to do shell script "osascript -e 'return choose from list {" & volumesString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			if selectedVolume is "false" then
				
				return "User Cancelled"
				
			else
				
				set theLabel to do shell script "echo " & quoted form of selectedVolume & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if theLabel is equal to volume's label then
						set mountedMountPoint to volume's mount_point
					end if
				end repeat
				
			end if
			
		end if
		
		try
			do shell script "diskutil unmountDisk " & quoted form of mountedMountPoint
			display notification quoted form of theLabel & ยฌ
				" has been ejected." with title myName
		on error
			display dialog quoted form of theLabel & " could not be ejected." with title myName
		end try
		
	else if button returned of dialogResult is "Start Time Machine" then
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toUserMustReconnect_cnt to 0
		
		set volumeListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set mountedStatus to ""
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if volume's status is "mounted" then
					set mounted_cnt to mounted_cnt + 1
				else if volume's status is "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if volume's status is "to use, must reconnect" then
					set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
				end if
				
				if volume's status is "mounted" or volume's status is "ejected" then
					
					set theLabel to volume's label
					set mountedOrMoutableStatus to volume's status
					set mountedOrMoutableMountPoint to volume's mount_point
					set mountedOrMoutableId to volume's id
					set mountedOrMoutableDeviceIndentifier to volume's device_identifier
					
				end if
				
			end if
			
			if volume's status is not "unavailable" then
				set volumeListString to volumeListString & volume's label & " (" & volume's status & ")" & return
			end if
			
		end repeat
		
		if (mounted_cnt + ejected_cnt) > 1 then
			
			set volumeList to paragraphs of volumeListString
			set volumesString to listToString(volumeList)
			if ejected_cnt > 0 then
				set thePrompt to "Select a Time Machine volume (ejected volumes will be automatically mounted):"
			else
				set thePrompt to "Select a Time Machine volume:"
			end if
			set selectedVolume to do shell script "osascript -e 'return choose from list {" & volumesString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			
			if selectedVolume is "false" then
				
				return "User Cancelled"
				
			else
				
				set theLabel to do shell script "echo " & quoted form of selectedVolume & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if theLabel is equal to volume's label then
						set mountedOrMoutableStatus to volume's status
						set mountedOrMoutableId to volume's id
						set mountedOrMoutableDeviceIndentifier to volume's device_identifier
					end if
				end repeat
				
			end if
			
		end if
		
		if mountedOrMoutableStatus is not "mounted" then
			
			do shell script "diskutil mountDisk " & quoted form of mountedOrMoutableDeviceIndentifier
			
		end if
		
		-- It's possible that Time Machine automatically started during the period
		-- that the above dialogs were open. If it automatically started for the volume
		-- that was selected, let it continue. If it was another volume, stop it before
		-- starting Time Machine for the selected volume.
		
		set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
		
		if tmStatus's destination_id is mountedOrMoutableId then
			
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" is already running." with title myName
			
		else if tmStatus's backup_phase is not "" then
			
			set prevLabel to tmStatus's label
			
			do shell script "tmutil stopbackup"
			
			set timeoutLimit to 10
			set startTime to current date
			
			repeat
				set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
				if tmStatus's backup_phase is "" then
					exit repeat
				end if
				delay 1
				if ((current date) - startTime) > timeoutLimit then
					display dialog "Time Machine to " & quoted form of prevLabel & ยฌ
						" was already running. An attempt to stop it failed after " & timeoutLimit & ยฌ
						" seconds." with title myName buttons {"OK"} default button {"OK"}
					return "Timeout"
				end if
			end repeat
			
			display notification "Time Machine to " & quoted form of prevLabel & ยฌ
				" was running and was stopped." with title myName
			
			delay 2.0
			
			do shell script "tmutil startbackup --destination " & quoted form of mountedOrMoutableId
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" started." with title myName
			
		else
			
			do shell script "tmutil startbackup --destination " & quoted form of mountedOrMoutableId
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" started." with title myName
			
		end if
		
	end if
	
end if


-- == Handlers =================================================

on getMyName()
	tell application "Finder"
		set myPath to (path to me) as alias
		set myFileName to name of myPath
		set myName to (text 1 thru ((offset of "." in myFileName) - 1) of myFileName)
	end tell
	
	if myName begins with "Keyboard-Maestro-Script" then
		set kmInst to system attribute "KMINSTANCE"
		tell application "Keyboard Maestro Engine"
			set kmMyName to getvariable "local_myName" instance kmInst
		end tell
		set myName to kmMyName
	end if
	
	return myName
end getMyName

on listToString(theList)
	-- Convert the AppleScript list to a string
	set str to ""
	repeat with i from 1 to count of theList
		set str to str & "\"" & item i of theList & "\"" & ", "
	end repeat
	-- Remove the trailing comma
	set str to text 1 thru -3 of str
	return str
end listToString

-- Information gathered here, will not change during the execution of this script
on getTmDestinationsInfo()
	
	set tmudi_raw to do shell script "tmutil destinationinfo"
	set tmutilDestinationinfo to do shell script "echo " & quoted form of tmudi_raw & " | sed 's/> ===/=====/g'"
	
	set AppleScript's text item delimiters to "===================================================="
	set theItms to text items of tmutilDestinationinfo
	
	set tmDestinationsInfo to {}
	
	repeat with cItm in theItms
		set AppleScript's text item delimiters to return
		set cItmLines to text items of cItm
		set cItmRec to {label:"", id:""}
		
		repeat with cLine in cItmLines
			if cLine starts with "Name" then
				set label of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			else if cLine starts with "ID" then
				set id of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			end if
		end repeat
		
		if label of cItmRec is not "" then
			set end of tmDestinationsInfo to cItmRec
		end if
	end repeat
	
	return tmDestinationsInfo
	
end getTmDestinationsInfo

-- Information here will update as Time Machine changes
on getTmDestinationsDiskutilInfo(tmDestinationsInfo)
	
	set tmDestinationsDiskutilInfo to {}
	
	repeat with i in tmDestinationsInfo
		
		set iRecord to {label:"", id:"", device_identifier:"", mount_point:"", encryption:"", connected:"", status:""}
		
		set device_identifier to ""
		set mount_point to ""
		set encryption to ""
		set status to ""
		set connected to true
		
		try
			set du to do shell script "diskutil info -plist " & quoted form of i's label
			
			set device_identifier to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>DeviceIdentifier<\\/key>/{getline; print $3}'"
			set mount_point to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>MountPoint<\\/key>/{getline; print $3}'"
			set encryption to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>Encryption<\\/key>/{getline; print}'"
		on error
			set connected to false
		end try
		
		set label of iRecord to i's label
		set id of iRecord to i's id
		set device_identifier of iRecord to device_identifier
		set mount_point of iRecord to mount_point
		
		set encryption to (encryption contains "true")
		set encryption of iRecord to encryption
		
		if connected then
			set connected of iRecord to true
			if mount_point is not "" then
				set status of iRecord to "mounted"
			else
				if encryption then
					set status of iRecord to "to use, must reconnect"
				else
					set status of iRecord to "ejected"
				end if
			end if
		else
			set connected of iRecord to false
			set status of iRecord to "unavailable"
		end if
		
		if label of iRecord is not "" then
			set end of tmDestinationsDiskutilInfo to iRecord
		end if
		
	end repeat
	
	return tmDestinationsDiskutilInfo
	
end getTmDestinationsDiskutilInfo

-- Information here will update as Time Machine changes
on getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
	
	set tmStatus to {backup_phase:"", destination_id:"", label:"", mount_point:"", destination_identifier:"", encryption:""}
	
	set tms to do shell script "tmutil status"
	
	set backup_phase of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep BackupPhase | awk -F' = ' '{print $2}' | tr -d ';'"
	
	set destination_id of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep DestinationID | awk -F' = ' '{print $2}' | tr -d ';' | tr -d '\"'"
	
	repeat with i in tmDestinationsInfo
		if i's id is equal to destination_id of tmStatus then
			set label of tmStatus to i's label
			exit repeat
		end if
	end repeat
	
	repeat with i in tmDestinationsDiskutilInfo
		if i's label is equal to label of tmStatus then
			set mount_point of tmStatus to i's mount_point
			set destination_identifier of tmStatus to i's device_identifier
			set encryption of tmStatus to i's encryption
			exit repeat
		end if
	end repeat
	
	return tmStatus
	
end getTmStatus
2 Likes

Looks nice. Got an error in the getMyName() handler when trying to compile โ€“ it requires Keyboard Maestro, which I donโ€™t have. Compiles after commenting out the โ€œif myName begins with "Keyboard-Maestro-Script"โ€ If/end block.

Cheers.

Hi, @Garry. Sorry about that. Yes, that block of code is only necessary when running the AppleScript within and Keyboard Maestro Execute an AppleScript action. Hereโ€™s the version that should be run elsewhere:

-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
-- Title            : Time Machine Assistant, v1.1
-- Modified	     : 2024-04-29
-- Author	     : Jim Sauer, [@_jims](https://forum.keyboardmaestro.com/u/_jims/summary)
-- Purpose
-- This script provides Time Machine information and control. 
--
-- No configurations is required as all information is retrieved from tmutil 
-- and diskutil.
--
-- This macro provides Time Machine information and control.
--
-- I created Time Machine Assistant for myself and others that I help 
-- with Time Machine. For those of us that periodically connect an external 
-- backup drive to a MacBook Pro or MacBook Air, it is important to check 
-- the status of Time Machine before disconnecting the external drive.
--
-- Also, in some cases, itโ€™s nice to run Time Machine immediately before 
-- disconnecting the drive.
--
-- Tested With. : Sonoma 14.4.1 (23E224)/MacBookPro18,2
-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
-- Version History
-- 1.0 - initial version
-- 1.1 - Updated the Purpose.
-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท

set myName to getMyName()

set tmDestinationsInfo to getTmDestinationsInfo()

set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)

set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)

set buttonList to {}

if tmStatus's backup_phase is not "" then
	
	if tmStatus's encryption then
		set encryptionStr to "Yes"
	else
		set encryptionStr to "No"
	end if
	
	set dialogResult to display dialog ยฌ
		"Time Machine Status : " & tmStatus's backup_phase & return & return & ยฌ
		"Backup Volume : " & tmStatus's label & return & ยฌ
		"Encryption : " & encryptionStr & return ยฌ
		with title myName buttons {"Interrupt & Eject", "Wait & Eject", "Cancel"} ยฌ
		default button {"Cancel"}
	
	if button returned of dialogResult is "Interrupt & Eject" then
		
		do shell script "tmutil stopbackup"
		do shell script "diskutil unmountDisk " & quoted form of tmStatus's mount_point
		display notification "Time Machine backup has been interrupted and " & ยฌ
			quoted form of tmStatus's label & " has been ejected." with title myName
		
	else if button returned of dialogResult is "Wait & Eject" then
		
		set timeoutLimit to 10
		set startTime to current date
		
		repeat
			set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
			if tmStatus's backup_phase is "" then
				exit repeat
			end if
			delay 1
			if ((current date) - startTime) > timeoutLimit then
				display dialog "Time Machine to " & tmStatus's label & ยฌ
					" was running. An attempt to stop it failed after " & timeoutLimit & ยฌ
					" seconds." with title myName buttons {"OK"} default button {"OK"}
				return "Timeout"
			end if
		end repeat
		
		do shell script "diskutil unmountDisk " & quoted form of tmStatus's mount_point
		display notification "Time Machine backup has been interrupted and '" & ยฌ
			tmStatus's label & "' has been ejected." with title myName
		
		
	end if
	
	return
	
end if

set connected_cnt to 0
set mounted_cnt to 0
set ejected_cnt to 0
set toUserMustReconnect_cnt to 0

set volumeListString to ""

repeat with volume in tmDestinationsDiskutilInfo
	
	set mountedStatus to ""
	
	if volume's connected then
		set connected_cnt to connected_cnt + 1
		if volume's status is "mounted" then
			set mounted_cnt to mounted_cnt + 1
		else if volume's status is "ejected" then
			set ejected_cnt to ejected_cnt + 1
		else if volume's status is "to use, must reconnect" then
			set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
		end if
	end if
	
	set volumeListString to volumeListString & "โ€ข " & volume's label & " (" & volume's status & ")" & return
	
end repeat

if (mounted_cnt + ejected_cnt) < 1 then
	
	set dialogResult to display dialog "Time Machine volumes:" & return & return & ยฌ
		volumeListString & return & ยฌ
		"Time Machine is not running and there a no volumes to eject." with title myName buttons {"OK"} ยฌ
		default button {"OK"}
	
	return
	
else
	
	set end of buttonList to "Cancel"
	
	if mounted_cnt > 0 then
		set beginning of buttonList to "Eject Volume"
	end if
	
	
	set mountNote to ""
	
	if (mounted_cnt + ejected_cnt + toUserMustReconnect_cnt) > 0 then
		set beginning of buttonList to "Start Time Machine"
		if ejected_cnt > 0 then
			set mountNote to return & "Note: When starting Time Machine, ejected volumes will be automatically mounted." & return
		end if
		
	end if
	
	set defaultButton to "Cancel"
	
	set dialogResult to display dialog "Time Machine volumes:" & return & return & ยฌ
		volumeListString & mountNote ยฌ
		with title myName buttons buttonList default button defaultButton
	
	if button returned of dialogResult is "Eject Volume" then
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toUserMustReconnect_cnt to 0
		
		set volumeListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set mountedStatus to ""
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if volume's status is "mounted" then
					set mounted_cnt to mounted_cnt + 1
					set theLabel to volume's label
					set mountedStatus to volume's status
					set mountedMountPoint to volume's mount_point
					set mountedId to volume's id
					set mountedDeviceIndentifier to volume's device_identifier
				else if volume's status is "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if volume's status is "to use, must reconnect" then
					set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
				end if
			end if
			
			if volume's status is not "unavailable" then
				set volumeListString to volumeListString & volume's label & " (" & volume's status & ")" & return
			end if
			
		end repeat
		
		if mounted_cnt > 1 then
			
			set volumeList to paragraphs of volumeListString
			set volumesString to listToString(volumeList)
			set thePrompt to "Select a volume to eject:"
			set selectedVolume to do shell script "osascript -e 'return choose from list {" & volumesString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			if selectedVolume is "false" then
				
				return "User Cancelled"
				
			else
				
				set theLabel to do shell script "echo " & quoted form of selectedVolume & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if theLabel is equal to volume's label then
						set mountedMountPoint to volume's mount_point
					end if
				end repeat
				
			end if
			
		end if
		
		try
			do shell script "diskutil unmountDisk " & quoted form of mountedMountPoint
			display notification quoted form of theLabel & ยฌ
				" has been ejected." with title myName
		on error
			display dialog quoted form of theLabel & " could not be ejected." with title myName
		end try
		
	else if button returned of dialogResult is "Start Time Machine" then
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toUserMustReconnect_cnt to 0
		
		set volumeListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set mountedStatus to ""
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if volume's status is "mounted" then
					set mounted_cnt to mounted_cnt + 1
				else if volume's status is "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if volume's status is "to use, must reconnect" then
					set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
				end if
				
				if volume's status is "mounted" or volume's status is "ejected" then
					
					set theLabel to volume's label
					set mountedOrMoutableStatus to volume's status
					set mountedOrMoutableMountPoint to volume's mount_point
					set mountedOrMoutableId to volume's id
					set mountedOrMoutableDeviceIndentifier to volume's device_identifier
					
				end if
				
			end if
			
			if volume's status is not "unavailable" then
				set volumeListString to volumeListString & volume's label & " (" & volume's status & ")" & return
			end if
			
		end repeat
		
		if (mounted_cnt + ejected_cnt) > 1 then
			
			set volumeList to paragraphs of volumeListString
			set volumesString to listToString(volumeList)
			if ejected_cnt > 0 then
				set thePrompt to "Select a Time Machine volume (ejected volumes will be automatically mounted):"
			else
				set thePrompt to "Select a Time Machine volume:"
			end if
			set selectedVolume to do shell script "osascript -e 'return choose from list {" & volumesString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			
			if selectedVolume is "false" then
				
				return "User Cancelled"
				
			else
				
				set theLabel to do shell script "echo " & quoted form of selectedVolume & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if theLabel is equal to volume's label then
						set mountedOrMoutableStatus to volume's status
						set mountedOrMoutableId to volume's id
						set mountedOrMoutableDeviceIndentifier to volume's device_identifier
					end if
				end repeat
				
			end if
			
		end if
		
		if mountedOrMoutableStatus is not "mounted" then
			
			do shell script "diskutil mountDisk " & quoted form of mountedOrMoutableDeviceIndentifier
			
		end if
		
		-- It's possible that Time Machine automatically started during the period
		-- that the above dialogs were open. If it automatically started for the volume
		-- that was selected, let it continue. If it was another volume, stop it before
		-- starting Time Machine for the selected volume.
		
		set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
		
		if tmStatus's destination_id is mountedOrMoutableId then
			
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" is already running." with title myName
			
		else if tmStatus's backup_phase is not "" then
			
			set prevLabel to tmStatus's label
			
			do shell script "tmutil stopbackup"
			
			set timeoutLimit to 10
			set startTime to current date
			
			repeat
				set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
				if tmStatus's backup_phase is "" then
					exit repeat
				end if
				delay 1
				if ((current date) - startTime) > timeoutLimit then
					display dialog "Time Machine to " & quoted form of prevLabel & ยฌ
						" was already running. An attempt to stop it failed after " & timeoutLimit & ยฌ
						" seconds." with title myName buttons {"OK"} default button {"OK"}
					return "Timeout"
				end if
			end repeat
			
			display notification "Time Machine to " & quoted form of prevLabel & ยฌ
				" was running and was stopped." with title myName
			
			delay 2.0
			
			do shell script "tmutil startbackup --destination " & quoted form of mountedOrMoutableId
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" started." with title myName
			
		else
			
			do shell script "tmutil startbackup --destination " & quoted form of mountedOrMoutableId
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" started." with title myName
			
		end if
		
	end if
	
end if


-- == Handlers =================================================

on getMyName()
	tell application "Finder"
		set myPath to (path to me) as alias
		set myFileName to name of myPath
		set myName to (text 1 thru ((offset of "." in myFileName) - 1) of myFileName)
	end tell
	
	return myName
end getMyName

on listToString(theList)
	-- Convert the AppleScript list to a string
	set str to ""
	repeat with i from 1 to count of theList
		set str to str & "\"" & item i of theList & "\"" & ", "
	end repeat
	-- Remove the trailing comma
	set str to text 1 thru -3 of str
	return str
end listToString

-- Information gathered here, will not change during the execution of this script
on getTmDestinationsInfo()
	
	set tmudi_raw to do shell script "tmutil destinationinfo"
	set tmutilDestinationinfo to do shell script "echo " & quoted form of tmudi_raw & " | sed 's/> ===/=====/g'"
	
	set AppleScript's text item delimiters to "===================================================="
	set theItms to text items of tmutilDestinationinfo
	
	set tmDestinationsInfo to {}
	
	repeat with cItm in theItms
		set AppleScript's text item delimiters to return
		set cItmLines to text items of cItm
		set cItmRec to {label:"", id:""}
		
		repeat with cLine in cItmLines
			if cLine starts with "Name" then
				set label of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			else if cLine starts with "ID" then
				set id of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			end if
		end repeat
		
		if label of cItmRec is not "" then
			set end of tmDestinationsInfo to cItmRec
		end if
	end repeat
	
	return tmDestinationsInfo
	
end getTmDestinationsInfo

-- Information here will update as Time Machine changes
on getTmDestinationsDiskutilInfo(tmDestinationsInfo)
	
	set tmDestinationsDiskutilInfo to {}
	
	repeat with i in tmDestinationsInfo
		
		set iRecord to {label:"", id:"", device_identifier:"", mount_point:"", encryption:"", connected:"", status:""}
		
		set device_identifier to ""
		set mount_point to ""
		set encryption to ""
		set status to ""
		set connected to true
		
		try
			set du to do shell script "diskutil info -plist " & quoted form of i's label
			
			set device_identifier to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>DeviceIdentifier<\\/key>/{getline; print $3}'"
			set mount_point to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>MountPoint<\\/key>/{getline; print $3}'"
			set encryption to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>Encryption<\\/key>/{getline; print}'"
		on error
			set connected to false
		end try
		
		set label of iRecord to i's label
		set id of iRecord to i's id
		set device_identifier of iRecord to device_identifier
		set mount_point of iRecord to mount_point
		
		set encryption to (encryption contains "true")
		set encryption of iRecord to encryption
		
		if connected then
			set connected of iRecord to true
			if mount_point is not "" then
				set status of iRecord to "mounted"
			else
				if encryption then
					set status of iRecord to "to use, must reconnect"
				else
					set status of iRecord to "ejected"
				end if
			end if
		else
			set connected of iRecord to false
			set status of iRecord to "unavailable"
		end if
		
		if label of iRecord is not "" then
			set end of tmDestinationsDiskutilInfo to iRecord
		end if
		
	end repeat
	
	return tmDestinationsDiskutilInfo
	
end getTmDestinationsDiskutilInfo

-- Information here will update as Time Machine changes
on getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
	
	set tmStatus to {backup_phase:"", destination_id:"", label:"", mount_point:"", destination_identifier:"", encryption:""}
	
	set tms to do shell script "tmutil status"
	
	set backup_phase of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep BackupPhase | awk -F' = ' '{print $2}' | tr -d ';'"
	
	set destination_id of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep DestinationID | awk -F' = ' '{print $2}' | tr -d ';' | tr -d '\"'"
	
	repeat with i in tmDestinationsInfo
		if i's id is equal to destination_id of tmStatus then
			set label of tmStatus to i's label
			exit repeat
		end if
	end repeat
	
	repeat with i in tmDestinationsDiskutilInfo
		if i's label is equal to label of tmStatus then
			set mount_point of tmStatus to i's mount_point
			set destination_identifier of tmStatus to i's device_identifier
			set encryption of tmStatus to i's encryption
			exit repeat
		end if
	end repeat
	
	return tmStatus
	
end getTmStatus
1 Like




Iโ€™ve updated the Time Machine script to Version 2.0:

  • Added the variable ejectTimeoutMin (set to 10) and added it to the โ€˜Wait & Ejectโ€™ dialog button.

  • Revised the โ€˜Wait & Ejectโ€™ logic to incorporate ejectTimeoutMin.


-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
-- Title            : Time Machine Assistant, v2.0
-- Modified	     : 2024-12-02
-- Author	     : Jim Sauer, [@_jims](https://forum.keyboardmaestro.com/u/_jims/summary)
-- Purpose
-- This script provides Time Machine information and control. 
--
-- No configurations is required as all information is retrieved from tmutil 
-- and diskutil.
--
-- This macro provides Time Machine information and control.
--
-- I created 'Time Machine Assistant' for myself and others that I help 
-- with Time Machine. For those of us that periodically connect an external 
-- backup drive to a MacBook Pro or MacBook Air, it is important to check 
-- the status of Time Machine before disconnecting the external drive.
--
-- Also, in some cases, itโ€™s nice to run Time Machine immediately before 
-- disconnecting the drive.
--
-- Tested With. : Sonoma 14.4.1 (23E224)/MacBookPro18,2
-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
-- Version History
-- 1.0 - initial version
--
-- 1.1 - Updated the Purpose.
--
-- 2.0
-- a) Added the variable ejectTimeoutMin (set to 10) and added it to the 
--     'Wait & Eject' dialog button.
-- b) Revised the 'Wait & Eject' logic to incorporate ejectTimeoutMin.
-- ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท

set ejectTimeoutMin to 10

set myName to getMyName()

set tmDestinationsInfo to getTmDestinationsInfo()

set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)

set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)

set buttonList to {}

if tmStatus's backup_phase is not "" then
	
	set btnWaitAndEdject to "Wait (up to " & ejectTimeoutMin & " min) & Eject"
	
	if tmStatus's encryption then
		set encryptionStr to "Yes"
	else
		set encryptionStr to "No"
	end if
	
	set dialogResult to display dialog ยฌ
		"Time Machine Status : " & tmStatus's backup_phase & return & return & ยฌ
		"Backup Volume : " & tmStatus's label & return & ยฌ
		"Encryption : " & encryptionStr & return ยฌ
		with title myName buttons {"Interrupt & Eject", btnWaitAndEdject, "Cancel"} ยฌ
		default button {"Cancel"}
	
	set tmInfo to tmStatus
	
	if button returned of dialogResult is "Interrupt & Eject" then
		
		do shell script "tmutil stopbackup"
		do shell script "diskutil unmountDisk " & quoted form of tmInfo's mount_point
		display notification "Time Machine backup has been interrupted and '" & ยฌ
			tmInfo's label & "' has been ejected." with title myName
		
	else if button returned of dialogResult is btnWaitAndEdject then
		
		set startTime to current date
		
		repeat
			set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
			if tmStatus's backup_phase is "" then
				exit repeat
			end if
			delay 1
			if ((current date) - startTime) > ejectTimeoutMin * 60 then
				display dialog "Time Machine to '" & tmInfo's label & "' is still running. " & return & return & ยฌ
					"The timeout of " & ejectTimeoutMin & " minutes was exceeded, " & ยฌ
					"thus the volume will not be automatically ejected when it " & ยฌ
					"completes." & return & return & ยฌ
					"You can start '" & myName & "' again, " & ยฌ
					"to resume waiting." with title myName buttons {"OK"} default button {"OK"}
				return "Timeout"
			end if
		end repeat
		
		do shell script "diskutil unmountDisk " & quoted form of tmInfo's mount_point
		display notification "Time Machine backup has completed and " & ยฌ
			quoted form of tmInfo's label & " has been ejected." with title myName
		
	end if
	
	return
	
end if

set connected_cnt to 0
set mounted_cnt to 0
set ejected_cnt to 0
set toUserMustReconnect_cnt to 0

set volumeListString to ""

repeat with volume in tmDestinationsDiskutilInfo
	
	set mountedStatus to ""
	
	if volume's connected then
		set connected_cnt to connected_cnt + 1
		if volume's status is "mounted" then
			set mounted_cnt to mounted_cnt + 1
		else if volume's status is "ejected" then
			set ejected_cnt to ejected_cnt + 1
		else if volume's status is "to use, must reconnect" then
			set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
		end if
	end if
	
	set volumeListString to volumeListString & "โ€ข " & volume's label & " (" & volume's status & ")" & return
	
end repeat

if (mounted_cnt + ejected_cnt) < 1 then
	
	set dialogResult to display dialog "Time Machine volumes:" & return & return & ยฌ
		volumeListString & return & ยฌ
		"Time Machine is not running and there are no volumes to eject." with title myName buttons {"OK"} ยฌ
		default button {"OK"}
	
	return
	
else
	
	set end of buttonList to "Cancel"
	
	if mounted_cnt > 0 then
		set beginning of buttonList to "Eject Volume"
	end if
	
	set mountNote to ""
	
	if (mounted_cnt + ejected_cnt + toUserMustReconnect_cnt) > 0 then
		set beginning of buttonList to "Start Time Machine"
		if ejected_cnt > 0 then
			set mountNote to return & "Note: When starting Time Machine, ejected volumes will be automatically mounted." & return
		end if
	end if
	
	set defaultButton to "Cancel"
	
	set dialogResult to display dialog "Time Machine volumes:" & return & return & ยฌ
		volumeListString & mountNote ยฌ
		with title myName buttons buttonList default button defaultButton
	
	if button returned of dialogResult is "Eject Volume" then
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toUserMustReconnect_cnt to 0
		
		set volumeListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set mountedStatus to ""
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if volume's status is "mounted" then
					set mounted_cnt to mounted_cnt + 1
					set theLabel to volume's label
					set mountedStatus to volume's status
					set mountedMountPoint to volume's mount_point
					set mountedId to volume's id
					set mountedDeviceIndentifier to volume's device_identifier
				else if volume's status is "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if volume's status is "to use, must reconnect" then
					set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
				end if
			end if
			
			if volume's status is not "unavailable" then
				set volumeListString to volumeListString & volume's label & " (" & volume's status & ")" & return
			end if
			
		end repeat
		
		if mounted_cnt > 1 then
			
			set volumeList to paragraphs of volumeListString
			set volumesString to listToString(volumeList)
			set thePrompt to "Select a volume to eject:"
			set selectedVolume to do shell script "osascript -e 'return choose from list {" & volumesString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			if selectedVolume is "false" then
				
				return "User Cancelled"
				
			else
				
				set theLabel to do shell script "echo " & quoted form of selectedVolume & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if theLabel is equal to volume's label then
						set mountedMountPoint to volume's mount_point
					end if
				end repeat
				
			end if
			
		end if
		
		try
			do shell script "diskutil unmountDisk " & quoted form of mountedMountPoint
			display notification quoted form of theLabel & ยฌ
				" has been ejected." with title myName
		on error
			display dialog quoted form of theLabel & " could not be ejected." with title myName
		end try
		
	else if button returned of dialogResult is "Start Time Machine" then
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toUserMustReconnect_cnt to 0
		
		set volumeListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set mountedStatus to ""
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if volume's status is "mounted" then
					set mounted_cnt to mounted_cnt + 1
				else if volume's status is "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if volume's status is "to use, must reconnect" then
					set toUserMustReconnect_cnt to toUserMustReconnect_cnt + 1
				end if
				
				if volume's status is "mounted" or volume's status is "ejected" then
					
					set theLabel to volume's label
					set mountedOrMoutableStatus to volume's status
					set mountedOrMoutableMountPoint to volume's mount_point
					set mountedOrMoutableId to volume's id
					set mountedOrMoutableDeviceIndentifier to volume's device_identifier
					
				end if
				
			end if
			
			if volume's status is not "unavailable" then
				set volumeListString to volumeListString & volume's label & " (" & volume's status & ")" & return
			end if
			
		end repeat
		
		if (mounted_cnt + ejected_cnt) > 1 then
			
			set volumeList to paragraphs of volumeListString
			set volumesString to listToString(volumeList)
			if ejected_cnt > 0 then
				set thePrompt to "Select a Time Machine volume (ejected volumes will be automatically mounted):"
			else
				set thePrompt to "Select a Time Machine volume:"
			end if
			set selectedVolume to do shell script "osascript -e 'return choose from list {" & volumesString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			
			if selectedVolume is "false" then
				
				return "User Cancelled"
				
			else
				
				set theLabel to do shell script "echo " & quoted form of selectedVolume & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if theLabel is equal to volume's label then
						set mountedOrMoutableStatus to volume's status
						set mountedOrMoutableId to volume's id
						set mountedOrMoutableDeviceIndentifier to volume's device_identifier
					end if
				end repeat
				
			end if
			
		end if
		
		if mountedOrMoutableStatus is not "mounted" then
			
			do shell script "diskutil mountDisk " & quoted form of mountedOrMoutableDeviceIndentifier
			
		end if
		
		-- It's possible that Time Machine automatically started during the period
		-- that the above dialogs were open. If it automatically started for the volume
		-- that was selected, let it continue. If it was another volume, stop it before
		-- starting Time Machine for the selected volume.
		
		set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
		
		if tmStatus's destination_id is mountedOrMoutableId then
			
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" is already running." with title myName
			
		else if tmStatus's backup_phase is not "" then
			
			set prevLabel to tmStatus's label
			
			do shell script "tmutil stopbackup"
			
			set timeoutLimit to 20
			set startTime to current date
			
			repeat
				set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
				if tmStatus's backup_phase is "" then
					exit repeat
				end if
				delay 1
				if ((current date) - startTime) > timeoutLimit then
					display dialog "Time Machine to '" & prevLabel & ยฌ
						"' was already running. An attempt to stop it failed after " & timeoutLimit & ยฌ
						" seconds." with title myName buttons {"OK"} default button {"OK"}
					return "Timeout"
				end if
			end repeat
			
			display notification "Time Machine to " & quoted form of prevLabel & ยฌ
				" was running and was stopped." with title myName
			
			delay 2.0
			
			do shell script "tmutil startbackup --destination " & quoted form of mountedOrMoutableId
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" started." with title myName
			
		else
			
			do shell script "tmutil startbackup --destination " & quoted form of mountedOrMoutableId
			display notification "Time Machine to " & quoted form of theLabel & ยฌ
				" started." with title myName
			
		end if
		
	end if
	
end if


-- == Handlers =================================================

on getMyName()
	tell application "Finder"
		set myPath to (path to me) as alias
		set myFileName to name of myPath
		set myName to (text 1 thru ((offset of "." in myFileName) - 1) of myFileName)
	end tell
	
	return myName
end getMyName

on listToString(theList)
	-- Convert the AppleScript list to a string
	set str to ""
	repeat with i from 1 to count of theList
		set str to str & "\"" & item i of theList & "\"" & ", "
	end repeat
	-- Remove the trailing comma
	set str to text 1 thru -3 of str
	return str
end listToString

-- Information gathered here, will not change during the execution of this script
on getTmDestinationsInfo()
	
	set tmudi_raw to do shell script "tmutil destinationinfo"
	set tmutilDestinationinfo to do shell script "echo " & quoted form of tmudi_raw & " | sed 's/> ===/=====/g'"
	
	set AppleScript's text item delimiters to "===================================================="
	set theItms to text items of tmutilDestinationinfo
	
	set tmDestinationsInfo to {}
	
	repeat with cItm in theItms
		set AppleScript's text item delimiters to return
		set cItmLines to text items of cItm
		set cItmRec to {label:"", id:""}
		
		repeat with cLine in cItmLines
			if cLine starts with "Name" then
				set label of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			else if cLine starts with "ID" then
				set id of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			end if
		end repeat
		
		if label of cItmRec is not "" then
			set end of tmDestinationsInfo to cItmRec
		end if
	end repeat
	
	return tmDestinationsInfo
	
end getTmDestinationsInfo

-- Information here will update as Time Machine changes
on getTmDestinationsDiskutilInfo(tmDestinationsInfo)
	
	set tmDestinationsDiskutilInfo to {}
	
	repeat with i in tmDestinationsInfo
		
		set iRecord to {label:"", id:"", device_identifier:"", mount_point:"", encryption:"", connected:"", status:""}
		
		set device_identifier to ""
		set mount_point to ""
		set encryption to ""
		set status to ""
		set connected to true
		
		try
			set du to do shell script "diskutil info -plist " & quoted form of i's label
			
			set device_identifier to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>DeviceIdentifier<\\/key>/{getline; print $3}'"
			set mount_point to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>MountPoint<\\/key>/{getline; print $3}'"
			set encryption to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>Encryption<\\/key>/{getline; print}'"
		on error
			set connected to false
		end try
		
		set label of iRecord to i's label
		set id of iRecord to i's id
		set device_identifier of iRecord to device_identifier
		set mount_point of iRecord to mount_point
		
		set encryption to (encryption contains "true")
		set encryption of iRecord to encryption
		
		if connected then
			set connected of iRecord to true
			if mount_point is not "" then
				set status of iRecord to "mounted"
			else
				if encryption then
					set status of iRecord to "to use, must reconnect"
				else
					set status of iRecord to "ejected"
				end if
			end if
		else
			set connected of iRecord to false
			set status of iRecord to "unavailable"
		end if
		
		if label of iRecord is not "" then
			set end of tmDestinationsDiskutilInfo to iRecord
		end if
		
	end repeat
	
	return tmDestinationsDiskutilInfo
	
end getTmDestinationsDiskutilInfo

-- Information here will update as Time Machine changes
on getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
	
	set tmStatus to {backup_phase:"", destination_id:"", label:"", mount_point:"", destination_identifier:"", encryption:""}
	
	set tms to do shell script "tmutil status"
	
	set backup_phase of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep BackupPhase | awk -F' = ' '{print $2}' | tr -d ';'"
	
	set destination_id of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep DestinationID | awk -F' = ' '{print $2}' | tr -d ';' | tr -d '\"'"
	
	repeat with i in tmDestinationsInfo
		if i's id is equal to destination_id of tmStatus then
			set label of tmStatus to i's label
			exit repeat
		end if
	end repeat
	
	repeat with i in tmDestinationsDiskutilInfo
		if i's label is equal to label of tmStatus then
			set mount_point of tmStatus to i's mount_point
			set destination_identifier of tmStatus to i's device_identifier
			set encryption of tmStatus to i's encryption
			exit repeat
		end if
	end repeat
	
	return tmStatus
	
end getTmStatus

For those that use Keyboard Maestro, Iโ€™ve shared a version of this utility in a macro.

1 Like

Iโ€™ve updated the Time Machine script to Version 4.0:

  • Added yellow indicator before each ejected volume.

  • Changed ejectTimeoutMin to IfTimeMachineRunningTimeoutBeforeEject.

  • Improved error checking.

  • Expanded comments.

  • Bug fix: Incorrect Time Machine volume was reported when Time Machine was being started.


(*
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
Title			: Time Machine Assistant, v4.0
Modified		: 2024-12-05
Author			: Jim Sauer, [@_jims](https://forum.keyboardmaestro.com/u/_jims/summary)

Purpose
This script provides Time Machine information and control. 

No configurations is required as all information is retrieved from tmutil 
and diskutil.

This macro provides Time Machine information and control.

I created 'Time Machine Assistant' for myself and others that I help 
with Time Machine. For those of us that periodically connect an external 
backup drive to a MacBook Pro or MacBook Air, it is important to check 
the status of Time Machine before disconnecting the external drive.

Also, in some cases, itโ€™s nice to run Time Machine immediately before 
disconnecting the drive.

Tested With. : Sonoma 14.4.1 (23E224)/MacBookPro18,2
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
Version History
1.0 - initial version

1.1 
a) Modified the method to determine myName so that the name is
   isuccessfully returned when the script runs within Keyboard Maestro.
b) Updated the Purpose.

2.0
a) Added the variable ejectTimeoutMin (set to 10) and added it to the 
    'Wait & Eject' dialog button.
b) Revised the 'Wait & Eject' logic to incorporate ejectTimeoutMin.

3.0
a) After each `diskutil unmountDisk` added a `diskutil eject`.
b) Added green and red indicators before each volume.

4.0
a) Added yellow indicator before each ejected volume.
b) Changed ejectTimeoutMin to IfTimeMachineRunningTimeoutBeforeEject.
c) Improved error checking.
d) Expanded comments.
e) Bug fix: Incorrect Time Machine volume was reported when
    Time Machine was being started.
ยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยทยท
*)

set IfTimeMachineRunningTimeoutBeforeEject to 20

(* For standalone AppleScript of this script
set IfTimeMachineRunningTimeoutBeforeEject to 20
*)

(* For Keyboard Maestro application of this script
set kmInst to system attribute "KMINSTANCE"
tell application "Keyboard Maestro Engine"
	set IfTimeMachineRunningTimeoutBeforeEject to getvariable ยฌ
	"local_IfTimeMachineRunningTimeoutBeforeEject" instance kmInst
end tell
*)

set myName to getMyName()

set tmDestinationsInfo to getTmDestinationsInfo()

set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)

set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)

set buttonList to {}

if tmStatus's backup_phase is not "" then
	
	-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
	-- = = = = Time Machine Running = = = = = = = = = = = = = = = = = = = = =
	-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
	
	set tmLabel to tmStatus's label
	set tmBackupPhase to tmStatus's backup_phase
	set tmMountPoint to tmStatus's mount_point
	set tmDeviceIndentifier to tmStatus's device_identifier
	
	set btnInteruptAndEject to "Interrupt & Eject"
	set btnWaitAndEdject to "Wait (up to " & IfTimeMachineRunningTimeoutBeforeEject & " min) & Eject"
	
	if tmStatus's encryption then
		set encryptionStr to "Yes"
	else
		set encryptionStr to "No"
	end if
	
	set thePrompt to "Time Machine Status : " & tmBackupPhase & return & return & ยฌ
		"Backup Volume : " & tmLabel & return & ยฌ
		"Encryption : " & encryptionStr
	
	set dialogResult to display dialog thePrompt ยฌ
		with title myName buttons {btnInteruptAndEject, btnWaitAndEdject, "Cancel"} ยฌ
		default button {"Cancel"}
	
	if button returned of dialogResult is btnInteruptAndEject then
		
		-- = Interrupt & Eject = = = = = = = = = = = = = = = = = = = = = = = = =
		-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
		
		try
			do shell script "tmutil stopbackup"
		on error errMsg number errNum
			display dialog "Time Machine to '" & tmLabel & "' could not be stopped." & return & return & ยฌ
				errNum & ": " & errMsg with title myName
			return "Time Machine Not Interrupted"
		end try
		
		display notification "Time Machine to '" & tmLabel & "' interrupted." with title myName
		
	else if button returned of dialogResult is btnWaitAndEdject then
		
		-- = Wait & Eject = = = = = = = = = = = = = = = = = = = = = = = = = = = 
		-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
		
		set startTime to current date
		
		repeat
			set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
			if tmStatus's backup_phase is "" then
				exit repeat
			end if
			delay 1
			if ((current date) - startTime) > IfTimeMachineRunningTimeoutBeforeEject * 60 then
				display dialog "Time Machine to '" & tmLabel & "' is still running. " & return & return & ยฌ
					"The timeout of " & IfTimeMachineRunningTimeoutBeforeEject & " minutes was exceeded, " & ยฌ
					"thus the volume will not be automatically ejected when it " & ยฌ
					"completes." & return & return & ยฌ
					"You can start '" & myName & "' again, " & ยฌ
					"to resume waiting." with title myName buttons {"OK"} default button {"OK"}
				return "Timeout"
			end if
		end repeat
		
		display notification "Time Machine to '" & tmLabel & "' completed." with title myName
		
	end if
	
	try
		do shell script "diskutil unmountDisk " & quoted form of tmMountPoint
	on error errMsg number errNum
		display dialog tmMountPoint & "' could not be unmounted." & return & return & ยฌ
			errNum & ": " & errMsg with title myName
		return "Volume Not Unmounted"
	end try
	
	try
		do shell script "diskutil eject " & quoted form of tmDeviceIndentifier
	on error errMsg number errNum
		display dialog "'" & tmMountPoint & "' (" & ยฌ
			tmDeviceIndentifier & ") could not be ejected." & return & return & ยฌ
			errNum & ": " & errMsg with title myName
		return "Volume Not Ejected"
	end try
	
	display notification "'" & tmMountPoint & "' (" & ยฌ
		tmDeviceIndentifier & ") has been ejected." with title myName
	
	return
	
end if

-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
-- = = = = Time Machine NOT Running = = = = = = = = = = = = = = = = =
-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

set connected_cnt to 0
set mounted_cnt to 0
set ejected_cnt to 0
set toMountMustReconnect_cnt to 0

set vListString to ""

repeat with volume in tmDestinationsDiskutilInfo
	
	set vStatus to volume's status
	
	if volume's connected then
		set connected_cnt to connected_cnt + 1
		if vStatus begins with "mounted" then
			set mounted_cnt to mounted_cnt + 1
		else if vStatus begins with "ejected" then
			set ejected_cnt to ejected_cnt + 1
		else if vStatus begins with "to mount, must reconnect" then
			set toMountMustReconnect_cnt to toMountMustReconnect_cnt + 1
		end if
	end if
	
	if vStatus starts with "mounted" then
		set vListString to vListString & "๐ŸŸข"
	else if vStatus starts with "ejected" then
		set vListString to vListString & "๐ŸŸก"
	else
		set vListString to vListString & "๐Ÿ”ด"
	end if
	
	set vListString to vListString & " " & volume's label & " (" & vStatus & ")" & return & return
	
end repeat

set vListString to text 1 thru -3 of vListString

if (mounted_cnt + ejected_cnt) < 1 then
	
	set thePrompt to "Time Machine volumes:" & return & return & ยฌ
		vListString & return & return & ยฌ
		"๐Ÿ‘‰๐Ÿฟ Time Machine is not running, no volumes are available, and there are no volumes to eject."
	
	set dialogResult to display dialog thePrompt with title myName buttons {"OK"} ยฌ
		default button {"OK"}
	
	return
	
else
	
	set end of buttonList to "Cancel"
	
	if mounted_cnt > 0 then
		set beginning of buttonList to "Eject Volume"
	end if
	
	set mountNote to ""
	
	if (mounted_cnt + ejected_cnt) > 0 then
		set beginning of buttonList to "Start Time Machine"
		if ejected_cnt > 0 then
			set mountNote to return & return & "๐Ÿ‘‰๐Ÿฟ When starting Time Machine, ejected volumes will be automatically mounted."
		end if
	end if
	
	set defaultButton to "Cancel"
	
	set thePrompt to "Time Machine volumes:" & return & return & ยฌ
		vListString & mountNote
	
	set dialogResult to display dialog thePrompt with title myName buttons buttonList default button defaultButton
	
	if button returned of dialogResult is "Eject Volume" then
		
		-- = Eject Volume = = = = = = = = = = = = = = = = = = = = = = = = = = =
		-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toMountMustReconnect_cnt to 0
		
		set mvListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set vStatus to volume's status
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if vStatus starts with "mounted" then
					
					set mounted_cnt to mounted_cnt + 1
					set mvLabel to volume's label
					set mvId to volume's id
					set mvMountPoint to volume's mount_point
					set mvDeviceIndentifier to volume's device_identifier
					
					set mvListString to mvListString & mvLabel & " (" & vStatus & ")" & return
					
				else if vStatus starts with "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if vStatus starts with "to mount, must reconnect" then
					set toMountMustReconnect_cnt to toMountMustReconnect_cnt + 1
				end if
			end if
			
		end repeat
		
		if mounted_cnt > 1 then
			
			set mvList to paragraphs of mvListString
			set mvString to listToString(mvList)
			set thePrompt to "Select a volume to eject:"
			set mvSelected to do shell script "osascript -e 'return choose from list {" & mvString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			
			if mvSelected is "false" then
				
				return "User Cancelled"
				
			else
				
				set vLabel to do shell script "echo " & quoted form of mvSelected & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if vLabel is equal to volume's label then
						set mvMountPoint to volume's mount_point
						set mvDeviceIndentifier to volume's device_identifier
					end if
				end repeat
				
			end if
			
		end if
		
		try
			do shell script "diskutil unmountDisk " & quoted form of mvMountPoint
		on error errMsg number errNum
			display dialog mvMountPoint & "' could not be unmounted." & return & return & ยฌ
				errNum & ": " & errMsg with title myName
			return "Volume Not Unmounted"
		end try
		
		try
			do shell script "diskutil eject " & quoted form of mvDeviceIndentifier
		on error errMsg number errNum
			display dialog "'" & mvMountPoint & "' (" & ยฌ
				mvDeviceIndentifier & ") could not be ejected." & return & return & ยฌ
				errNum & ": " & errMsg with title myName
			return "Volume Not Ejected"
		end try
		
		display notification "'" & mvMountPoint & "' (" & ยฌ
			mvDeviceIndentifier & ") has been ejected." with title myName
		
	else if button returned of dialogResult is "Start Time Machine" then
		
		-- = Start Time Machine = = = = = = = = = = = = = = = = = = = = = = = =
		-- = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
		
		set connected_cnt to 0
		set mounted_cnt to 0
		set ejected_cnt to 0
		set toMountMustReconnect_cnt to 0
		
		set tmvListString to ""
		
		-- The state of one or more of the volumes could have potentially have changed,
		-- thus regenerate tmDestinationsDiskutilInfo (e.g., by the user)
		set tmDestinationsDiskutilInfo to getTmDestinationsDiskutilInfo(tmDestinationsInfo)
		
		repeat with volume in tmDestinationsDiskutilInfo
			
			set vStatus to volume's status
			
			if volume's connected then
				set connected_cnt to connected_cnt + 1
				if vStatus starts with "mounted" then
					set mounted_cnt to mounted_cnt + 1
				else if vStatus starts with "ejected" then
					set ejected_cnt to ejected_cnt + 1
				else if vStatus starts with "to mount, must reconnect" then
					set toMountMustReconnect_cnt to toMountMustReconnect_cnt + 1
				end if
				
				if vStatus starts with "mounted" or vStatus starts with "ejected" then
					
					set tmvLabel to volume's label
					set tmvStatus to volume's status
					set tmvId to volume's id
					set tmvMountPoint to volume's mount_point
					set tmvDeviceIndentifier to volume's device_identifier
					
					set tmvListString to tmvListString & tmvLabel & " (" & tmvStatus & ")" & return
					
				end if
				
			end if
			
		end repeat
		
		if (mounted_cnt + ejected_cnt) > 1 then
			
			set tmvList to paragraphs of tmvListString
			set tmvString to listToString(tmvList)
			if ejected_cnt > 0 then
				set thePrompt to "Select a Time Machine volume (ejected volumes will be automatically mounted):"
			else
				set thePrompt to "Select a Time Machine volume:"
			end if
			set tmvSelected to do shell script "osascript -e 'return choose from list {" & tmvString & "} with prompt \"" & thePrompt & "\" default items {\"Cancel\"} with title \"" & myName & "\"'"
			
			if tmvSelected is "false" then
				
				return "User Cancelled"
				
			else
				
				set vLabel to do shell script "echo " & quoted form of tmvSelected & " | awk 'BEGIN{FS=\" \\\\(\"}{print $1}'"
				
				repeat with volume in tmDestinationsDiskutilInfo
					if vLabel is equal to volume's label then
						set tmvLabel to volume's label
						set tmvStatus to volume's status
						set tmvId to volume's id
						set tmvMountPoint to volume's mount_point
						set tmvDeviceIndentifier to volume's device_identifier
					end if
				end repeat
				
			end if
			
		end if
		
		if tmvStatus does not start with "mounted" then
			
			try
				do shell script "diskutil mountDisk " & quoted form of tmvDeviceIndentifier
			on error
				display dialog "'" & tmvMountPoint & "' (" & ยฌ
					tmvDeviceIndentifier & ") could not be ejected." with title myName
				return "Error Mounting"
			end try
			
		end if
		
		-- It's possible that Time Machine automatically started during the period
		-- that the above dialogs were open. If it automatically started for the volume
		-- that was selected, let it continue. If it was another volume, stop it before
		-- starting Time Machine for the selected volume.
		
		set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
		
		if tmStatus's destination_id is tmvId then
			
			display notification "Time Machine to " & quoted form of tmvLabel & ยฌ
				" is already running." with title myName
			
		else if tmStatus's backup_phase is not "" then
			
			set tmrvLabel to tmStatus's label
			
			try
				do shell script "tmutil stopbackup"
			on error errMsg number errNum
				display dialog "Time Machine to '" & tmrvLabel & "' was already running and could not be stopped." & return & return & ยฌ
					errNum & ": " & errMsg with title myName
				return "Time Machine Not Stopped"
			end try
			
			set timeoutLimit to 20
			set startTime to current date
			
			repeat
				set tmStatus to getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
				if tmStatus's backup_phase is "" then
					exit repeat
				end if
				delay 1
				if ((current date) - startTime) > timeoutLimit then
					display dialog "Time Machine to '" & vLabel & ยฌ
						"' was already running. An attempt to stop it failed after " & timeoutLimit & ยฌ
						" seconds." with title myName buttons {"OK"} default button {"OK"}
					return "Timeout Stopping Time Machine"
				end if
			end repeat
			
			display notification "Time Machine to '" & tmrvLabel & ยฌ
				"' was running and was stopped." with title myName
			
			delay 2.0
			
			try
				do shell script "tmutil startbackup --destination " & quoted form of tmvId
			on error errMsg number errNum
				display dialog "Time Machine to '" & tmvLabel & "' could not be started." & return & return & ยฌ
					errNum & ": " & errMsg with title myName
				return "Time Machine Not Started"
			end try
			
			display notification "Time Machine to '" & tmvLabel & ยฌ
				"' started." with title myName
			
		else
			
			try
				do shell script "tmutil startbackup --destination " & quoted form of tmvId
			on error errMsg number errNum
				display dialog "Time Machine to '" & tmvLabel & "' could not be started." & return & return & ยฌ
					errNum & ": " & errMsg with title myName
				return "Time Machine Not Started"
			end try
			
			display notification "Time Machine to '" & tmvLabel & ยฌ
				"' started." with title myName
			
		end if
		
	end if
	
end if

-- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-- + HANDLERS + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

on getMyName()
	tell application "Finder"
		set myPath to (path to me) as alias
		set myFileName to name of myPath
		set myName to (text 1 thru ((offset of "." in myFileName) - 1) of myFileName)
	end tell
	return myName
end getMyName

(* For standalone AppleScript of this script
on getMyName()
	tell application "Finder"
		set myPath to (path to me) as alias
		set myFileName to name of myPath
		set myName to (text 1 thru ((offset of "." in myFileName) - 1) of myFileName)
	end tell
	return myName
end getMyName
*)

(* For Keyboard Maestro application of this script
on getMyName()
	set kmInst to system attribute "KMINSTANCE"
	tell application "Keyboard Maestro Engine"
		set myName to getvariable "local_myName" instance kmInst
	end tell
	return myName
end getMyName
*)

on listToString(theList)
	-- Convert the AppleScript list to a string
	set str to ""
	repeat with i from 1 to count of theList
		set str to str & "\"" & item i of theList & "\"" & ", "
	end repeat
	-- Remove the trailing comma
	set str to text 1 thru -3 of str
	return str
end listToString

-- Information gathered by this handler, will not change during the execution of this script
on getTmDestinationsInfo()
	
	set tmudi_raw to do shell script "tmutil destinationinfo"
	set tmutilDestinationinfo to do shell script "echo " & quoted form of tmudi_raw & " | sed 's/> ===/=====/g'"
	
	set AppleScript's text item delimiters to "===================================================="
	set theItms to text items of tmutilDestinationinfo
	
	set tmDestinationsInfo to {}
	
	repeat with cItm in theItms
		set AppleScript's text item delimiters to return
		set cItmLines to text items of cItm
		set cItmRec to {label:"", id:""}
		
		repeat with cLine in cItmLines
			if cLine starts with "Name" then
				set label of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			else if cLine starts with "ID" then
				set id of cItmRec to do shell script "echo " & quoted form of cLine & " | awk -F': ' '{print $2}'"
			end if
		end repeat
		
		if label of cItmRec is not "" then
			set end of tmDestinationsInfo to cItmRec
		end if
	end repeat
	
	return tmDestinationsInfo
	
end getTmDestinationsInfo

-- Information gathered by this handler, will change as Time Machine progresses
on getTmDestinationsDiskutilInfo(tmDestinationsInfo)
	
	set tmDestinationsDiskutilInfo to {}
	
	repeat with i in tmDestinationsInfo
		
		set iRecord to {label:"", id:"", device_identifier:"", mount_point:"", encryption:"", connected:"", status:""}
		
		set device_identifier to ""
		set mount_point to ""
		set encryption to ""
		set status to ""
		set connected to true
		
		try
			set du to do shell script "diskutil info -plist " & quoted form of i's label
			
			set device_identifier to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>DeviceIdentifier<\\/key>/{getline; print $3}'"
			set mount_point to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>MountPoint<\\/key>/{getline; print $3}'"
			set encryption to do shell script "echo " & quoted form of du & " | tr '\\015' '\\012' | awk -F'<|>' '/<key>Encryption<\\/key>/{getline; print}'"
		on error
			set connected to false
		end try
		
		set label of iRecord to i's label
		set id of iRecord to i's id
		set device_identifier of iRecord to device_identifier
		set mount_point of iRecord to mount_point
		
		set encryption to (encryption contains "true")
		set encryption of iRecord to encryption
		
		if connected then
			set connected of iRecord to true
			if mount_point is not "" then
				if encryption then
					set status of iRecord to "mounted; encrypted"
				else
					set status of iRecord to "mounted; unencrypted"
				end if
			else
				if encryption then
					set status of iRecord to "to mount, must reconnect; encrypted"
				else
					set status of iRecord to "ejected; unencrypted"
				end if
			end if
		else
			set connected of iRecord to false
			set status of iRecord to "unavailable"
		end if
		
		if label of iRecord is not "" then
			set end of tmDestinationsDiskutilInfo to iRecord
		end if
		
	end repeat
	
	return tmDestinationsDiskutilInfo
	
end getTmDestinationsDiskutilInfo

-- Information gathered by this handler, will change as Time Machine progresses
on getTmStatus(tmDestinationsInfo, tmDestinationsDiskutilInfo)
	
	set tmStatus to {backup_phase:"", destination_id:"", label:"", mount_point:"", device_identifier:"", encryption:""}
	
	set tms to do shell script "tmutil status"
	
	set backup_phase of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep BackupPhase | awk -F' = ' '{print $2}' | tr -d ';'"
	
	set destination_id of tmStatus ยฌ
		to do shell script "echo " & quoted form of tms & " | tr '\\015' '\\012' | grep DestinationID | awk -F' = ' '{print $2}' | tr -d ';' | tr -d '\"'"
	
	repeat with i in tmDestinationsInfo
		if i's id is equal to destination_id of tmStatus then
			set label of tmStatus to i's label
			exit repeat
		end if
	end repeat
	
	repeat with i in tmDestinationsDiskutilInfo
		if i's label is equal to label of tmStatus then
			set mount_point of tmStatus to i's mount_point
			set device_identifier of tmStatus to i's device_identifier
			set encryption of tmStatus to i's encryption
			exit repeat
		end if
	end repeat
	
	return tmStatus
	
end getTmStatus

If Time Machine is not running when the macro is triggered, a dialog like to the following will appear.

Note that if an encrypted volume is ejected, it must be disconnected and reconnected before it can be used again. When it is reconnected, the encryption key can be entered manually or retrieved from Keychain.


If the Start Time Machine button is selected, a dialog like to the following will appear.

Note the mouted and ejected volumes can be used; the latter will be automatically mounted.


If Time Machine is running when the macro is triggered, a dialog like to the following will appear.

If Interrupt & Eject or Wait (up to X min) & Eject is selected, the macro will provide a notification when Time Machine is interrupted or completes. A second notification will appear when the volume is ejected.


For those that use Keyboard Maestro, Iโ€™ve shared a version of this utility in a macro.

The macro includes some additionally features. For example:

  • If triggered, the macro now cancels the previous instance of itself.

  • If triggered by a USB Device, the macro will display a progress indicator and wait a configured number of seconds. This provides time for the HD or SSD to mount before the first dialog is rendered. The setting local_UsbDevicesInfo was added to support this new feature.

2 Likes