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