on run argv
    set command to (item 1 of argv)

    tell application iTerm2
      set _window to (current window)

      if _window is equal to missing value then
        create window with default profile
      end if

      tell current window
        tell current session
          write text command
        end tell
      end tell
    end tell
end run

on run argv
    set command to (item 1 of argv)

    tell application "iTerm2"
      set _window to (current window)

      if _window is equal to missing value then
        create window with default profile
      end if

      tell current window
        tell current session
          write text command
        end tell
      end tell
    end tell
end run

Update script again

Update script again


set close_vagrant to "z puppet; vagrant halt -f;"

-- close vagrant
tell application "iTerm"
	activate
	set myterm to (make new terminal)
	tell myterm
		launch session "Default"
		set mysession to current session
	end tell

	tell mysession
		write text close_vagrant
	end tell

	delay 5
end tell

-- quit iTerm
ignoring application responses
	tell application "iTerm"
		quit
	end tell
end ignoring

delay 2
tell application "System Events" to keystroke return

set close_vagrant to "z puppet; vagrant halt -f;"

-- close vagrant
tell application "iTerm"
	activate
	set myterm to (make new terminal)
	tell myterm
		launch session "Default"
		set mysession to current session
	end tell

	tell mysession
		write text close_vagrant
	end tell
end tell

Remove closing iTerm2

Remove closing iTerm2


# AppleScript folder action for the pdfmatch command
#
# Open with script editor, compile and save to
# ~/Library/Workflows/Applications/Folder\ Actions/
#
# Then open the "Folder Actions Setup" application and add the work folder
# where your pdfmatch.json config lives and attach this script.
#
on adding folder items to this_folder after receiving these_files
	try
		repeat with each_file in these_files
			tell application "System Events"
				if name extension of each_file is "pdf" or "tif" or "tiff" or "jpeg" or "jpg" then
					# You might need to add your node binary to your $PATH. For example:
					# export PATH=/usr/local/bin:/usr/local/opt/node@6/bin:$PATH
					set output to (do shell script "cd '" & POSIX path of this_folder & "' && pdfmatch '" & POSIX path of each_file & "' --delete 2>&1")
					#display alert output
				end if
			end tell
		end repeat
	on error err_string number error_number
		display alert "Error " & error_number & ": " & err_string
	end try
end adding folder items to

# AppleScript folder action for the pdfmatch command
#
# Open with script editor, compile and save to
# ~/Library/Workflows/Applications/Folder\ Actions/
#
# Then open the "Folder Actions Setup" application and add the work folder
# where your pdfmatch.json config lives and attach this script.
#
on adding folder items to this_folder after receiving these_files
	try
    set ext to {"pdf", "tif", "tiff", "jpeg", "jpg"}
		repeat with each_file in these_files
			tell application "System Events"
				if ext contains name extension of each_file then
					# You might need to add your node binary to your $PATH. For example:
					# export PATH=/usr/local/bin:/usr/local/opt/node@6/bin:$PATH
					set output to (do shell script "cd '" & POSIX path of this_folder & "' && pdfmatch '" & POSIX path of each_file & "' --delete 2>&1")
					#display alert output
				end if
			end tell
		end repeat
	on error err_string number error_number
		display alert "Error " & error_number & ": " & err_string
	end try
end adding folder items to

Fix AppleScript extension matching

Fix AppleScript extension matching


tell application "iTerm2"
  activate

  set cercaWindow to (current window)

  tell cercaWindow

    tell current session
      split vertically with default profile
      write text "tcd"
    end tell

    tell last session of current tab
      write text "tcs"
    end tell

    set secondTab to (create tab with default profile)
    secondTab
    tell secondTab
      tell current session
        split vertically with default profile
        write text "tcf"
      end tell

      tell last session
        write text "tc"
      end tell
    end tell

    set thirdTab to (create tab with default profile)
    thirdTab
    tell thirdTab
      tell first session
        write text "tcv"
      end tell
    end tell
  end tell
end tell

tell application "iTerm2"
  activate

  set cercaWindow to (current window)

  tell cercaWindow

    tell current session
      split vertically with default profile
      write text "tcd"
    end tell

    tell last session of current tab
      write text "tcs"
    end tell

    set secondTab to (create tab with default profile)
    secondTab
    tell secondTab
      tell current session
        split vertically with default profile
        write text "tcf"
      end tell

      tell last session
        write text "tc; ty; stt aux"
      end tell
    end tell

    set thirdTab to (create tab with default profile)
    thirdTab
    tell thirdTab
      tell first session
        write text "tcv"
      end tell
    end tell

    set fourthTab to (create tab with default profile)
    fourthTab
    tell fourthTab
      tell first session
        write text "tc"
      end tell
    end tell
  end tell
end tell

update cerca script

update cerca script


activate application "SystemUIServer"
tell application "System Events"
    tell process "SystemUIServer"
        set btMenu to (menu bar item 1 of menu bar 1 whose description contains "bluetooth")
        tell btMenu
            click
            try
                tell (menu item "Rosco's Headphones" of menu 1)
                    click
                    if exists menu item "Connect" of menu 1 then
                        click menu item "Connect" of menu 1
                        return "Connecting..."
                    else if exists menu item "Disconnect" of menu 1 then
                        click menu item "Disconnect" of menu 1
                        return "Disconnecting..."
                    end if
                end tell
            end try
        end tell
    end tell
    key code 53
    return "Device not found or Bluetooth turned off"
end tell

-- Source: https://www.reddit.com/r/MacOS/comments/i4czgu/big_sur_airpods_script/gck3gz3
-- Works on macOS 11 Big Sur

use framework "IOBluetooth"
use scripting additions

set DeviceName to "Rosco's Headphones"

on getFirstMatchingDevice(deviceName)
    repeat with device in (current application's IOBluetoothDevice's pairedDevices() as list)
        if (device's nameOrAddress as string) contains deviceName then return device
    end repeat
end getFirstMatchingDevice

on toggleDevice(device)
    if not (device's isConnected as boolean) then
        device's openConnection()
        return "Connecting " & (device's nameOrAddress as string)
    else
        device's closeConnection()
        return "Disconnecting " & (device's nameOrAddress as string)
    end if
end toggleDevice

return toggleDevice(getFirstMatchingDevice(DeviceName))

Update headphone connect script for Big Sur


property stdout : ""
on run
	set newline to ASCII character 10

	set stdout to stdout & "  shutdown TotalTerminalCrashWatcher ..." & newline
	try
		do shell script "killall -SIGINT TotalTerminalCrashWatcher" with administrator privileges
	on error
		set stdout to stdout & "    TotalTerminalCrashWatcher.com not running" & newline
	end try

	set stdout to stdout & "  shutdown Terminal ..." & newline
	try
		tell application "Terminal" to quit
	on error
		set stdout to stdout & "    Terminal was not running prior uninstallation" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.app ..." & newline
	try
		do shell script "sudo rm -rf \"/Applications/TotalTerminal.app\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Applications/TotalTerminal.app" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.osax ..." & newline
	try
		do shell script "sudo rm -rf \"/Library/ScriptingAdditions/TotalTerminal.osax\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Library/ScriptingAdditions/TotalTerminal.osax" & newline
	end try

	set stdout to stdout & "  relaunch Terminal ..." & newline
	try
		tell application "Terminal" to launch
	on error
		set stdout to stdout & "    failed to relaunch Terminal" & newline
	end try

	-- at this point Terminal should start cleanly and with no signs of TotalTerminal
	-- you may check Events/Replies tab to see if there were no issues with uninstallation

	stdout -- this is needed for platypus to display output in details window
end run

property stdout : ""
on run
	set newline to ASCII character 10

	set stdout to stdout & "  shutdown TotalTerminalCrashWatcher ..." & newline
	try
		do shell script "killall -SIGINT TotalTerminalCrashWatcher" with administrator privileges
	on error
		set stdout to stdout & "    TotalTerminalCrashWatcher was not running" & newline
	end try

	set stdout to stdout & "  shutdown Terminal ..." & newline
	try
		tell application "Terminal" to quit
	on error
		set stdout to stdout & "    Terminal was not running prior uninstallation" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.app ..." & newline
	try
		do shell script "sudo rm -rf \"/Applications/TotalTerminal.app\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Applications/TotalTerminal.app" & newline
	end try

	set stdout to stdout & "  remove TotalTerminal.osax ..." & newline
	try
		do shell script "sudo rm -rf \"/Library/ScriptingAdditions/TotalTerminal.osax\"" with administrator privileges
	on error
		set stdout to stdout & "    unable to remove /Library/ScriptingAdditions/TotalTerminal.osax" & newline
	end try

	set stdout to stdout & "  relaunch Terminal ..." & newline
	try
		tell application "Terminal" to launch
	on error
		set stdout to stdout & "    failed to relaunch Terminal" & newline
	end try

	set stdout to stdout & "TotalTerminal uninstallation finished" & newline

	-- at this point Terminal should start cleanly and with no signs of TotalTerminal
	-- you may check Events/Replies tab to see if there were no issues with uninstallation

	stdout -- this is needed for platypus to display output in details window
end run

improve uninstaller script

improve uninstaller script


FasdUAS 1.101.10
lL
OL
YK
ZF

=

l
n
1

pKnd
4

cSrc
o0n

m
eSrckPod
QB
I"'
.hookUpdtnullcSrc
o"#0n
R
.ascrerr ********
o0errorstrerrorStr

errn
o0errornumbererrorNumber
I/B
.ascrcmnt********
b/>
b/<
b/:
b/8 !
 b/6"#
"b/4$%
$m/0&&''The device
%n03()
(113

pnam
)o010n
#m45**++* was unable to sync (
!o670errorstrerrorStr
m89,,--:
o:;0errornumbererrorNumber
m<=..//)0n
m
	l0
0I1
.corecnte********
12

cSrc
m22hookalisLMacintosh HDSH+q
iTunes.app7l	 Applicationsm$wq$Macintosh HD:Applications:iTunes.app
iTunes.appMacintosh HDApplications/iTunes.app/343
.aevtoappnull****4567
.aevtoappnull****
5kL8860n0errorstrerrorStr0errornumbererrorNumber729&*,.

cSrc
.corecnte********

pKnd
eSrckPod
.hookUpdtnullcSrc0errorstrerrorStr9

errn0errornumbererrorNumber

pnam
.ascrcmnt********MIFk*-jkh*/,(
jWX,%%%%%%j
Yh[OYUascrޭ

FasdUAS 1.101.10
k
lK
OK
XJ

ZE

l
=
n
1

pKnd
o0itunessourceiTunesSource
m
eSrckPod

QA
I!&
.hookUpdtnullcSrc
o!"0itunessourceiTunesSource
R
.ascrerr ********
o0errorstrerrorStr

errn
o0errornumbererrorNumber
I.A
.ascrcmnt********
b.=
b.;
b.9
b.7!"
!b.5#$
#b.3%&
%m./''((The device
&n/2)*
)102

pnam
*o/00itunessourceiTunesSource
$m34++,,* was unable to sync (
"o560errorstrerrorStr
 m78--..:
o9:0errornumbererrorNumber
m;<//00)0itunessourceiTunesSource
2

cSrc
	m11hookalisNMacintosh HDQH+\
iTunes.app	 Applications1r\%Macintosh HD:Applications:iTunes.app
iTunes.appMacintosh HDApplications/iTunes.app/2
2l343
.aevtoappnull****4567
.aevtoappnull****
5kK8860itunessourceiTunesSource0errorstrerrorStr0errornumbererrorNumber719'+-/

cSrc

kocl

cobj
.corecnte********

pKnd
eSrckPod
.hookUpdtnullcSrc0errorstrerrorStr9

errn0errornumbererrorNumber

pnam
.ascrcmnt********LHE*-[lkh,(
jWX	,%%%%%%jYh[OYUascr
ޭ

TIMOB-12740: Refactor script to use proper AppleScript types and iTunes idioms.


on run argv
	set song_title to item 1 in argv

	try
		tell application "iTunes" to play track song_title of current playlist
	on error error_message
		tell application "iTunes" to play playlist song_title
	end try
end run

on run argv
	set song_title to item 1 in argv

	tell application "iTunes" to set playlist_list to the name of every user playlist

	if playlist_list does not contain song_title then
		tell application "iTunes" to play track song_title of current playlist
	else
		tell application "iTunes" to play playlist song_title
	end if

end run

improve readability of play_song.scpt

improve readability of play_song.scpt


set kalaicon to 2
tell application "System Events" to set itExists to ¬
  exists of alias ((path to me as text) & ":app.icns")
if itExists
  set kalaicon to alias ((path to me as text) & ":app.icns")
end if
tell application "SystemUIServer"
  activate
  set my_password to display dialog ¬
    "Please enter your administrator password:" with title ¬
    "Password" with icon kalaicon ¬
    default answer ¬
    "" buttons {"Cancel", "OK"} default button 2 ¬
    giving up after 295 ¬
    with hidden answer
end tell

set kalaicon to 2
tell application "System Events" to set itExists to ¬
  exists of alias ((path to me as text) & ":app.icns")
if itExists
  set kalaicon to alias ((path to me as text) & ":app.icns")
end if
tell application "SystemUIServer"
  activate
  set my_password to display dialog ¬
    "Please enter your Mac user account password. Installation will fail if your account does not have administrative privileges." with title ¬
    "Password" with icon kalaicon ¬
    default answer ¬
    "" buttons {"Cancel", "OK"} default button 2 ¬
    giving up after 295 ¬
    with hidden answer
end tell

Fix #73

Fix #73


set close_ubervu_vagrant to "cd ~/uberVU/puppet; vagrant halt -f;"
set close_hs_vagrant to "cd ~/hootsuite/dash-board; vagrant halt -f;"

-- close vagrant
tell application "iTerm"
	activate
	set myterm to (make new terminal)
	tell myterm
		launch session "Default"
		set mysession to current session
	end tell

	tell mysession
		write text close_ubervu_vagrant
		write text close_hs_vagrant
	end tell
end tell

set close_ubervu_vagrant to "cd ~/uberVU/puppet/hs_analytics_vagrant; vagrant halt -f;"

-- close vagrant
tell application "iTerm"
	activate
	set myterm to (make new terminal)
	tell myterm
		launch session "Default"
		set mysession to current session
	end tell

	tell mysession
		write text close_ubervu_vagrant
	end tell
end tell

Update halt_vagrant.applescript

Update halt_vagrant.applescript


############################
# How to generate the PDFs #
############################
# 1. Update cover.html with the new version date
# 2. Open cover.html in safari and generate Cover.pdf
# 3. Place Cover.pdf in ~/Downloads folder
# 4. Ensure "Downloads" folder is an option when "Export to PDF…" in Safari
# 5. In terminal, run "osascript pdf.scpt"

set theChaptersStr to "index
who-is-this-guide-for
what-does-this-guide-cover
how-to-get-help
what-is-serverless
why-create-serverless-apps
create-an-aws-account
create-an-iam-user
what-is-iam
what-is-an-arn
configure-the-aws-cli
create-a-dynamodb-table
create-an-s3-bucket-for-file-uploads
create-a-cognito-user-pool
create-a-cognito-test-user
setup-the-serverless-framework
add-support-for-es6-es7-javascript
add-a-create-note-api
add-a-get-note-api
add-a-list-all-the-notes-api
add-an-update-note-api
add-a-delete-note-api
deploy-the-apis
create-a-cognito-identity-pool
cognito-user-pool-vs-identity-pool
test-the-apis
create-a-new-reactjs-app
add-app-favicons
setup-custom-fonts
setup-bootstrap
handle-routes-with-react-router
create-containers
adding-links-in-the-navbar
handle-404s
create-a-login-page
login-with-aws-cognito
add-the-session-to-the-state
load-the-state-from-the-session
clear-the-session-on-logout
redirect-on-login-and-logout
give-feedback-while-logging-in
create-a-signup-page
create-the-signup-form
signup-with-aws-cognito
add-the-create-note-page
connect-to-api-gateway-with-iam-auth
call-the-create-api
upload-a-file-to-s3
clear-aws-credentials-cache
list-all-the-notes
call-the-list-api
display-a-note
render-the-note-form
save-changes-to-a-note
delete-a-note
setup-secure-pages
create-a-route-that-redirects
use-the-redirect-routes
redirect-on-login
deploy-the-frontend
create-an-s3-bucket
deploy-to-s3
create-a-cloudfront-distribution
setup-your-domain-with-cloudfront
setup-www-domain-redirect
setup-ssl
deploy-updates
update-the-app
deploy-again
code-splitting-in-create-react-app
wrapping-up
giving-back
older-versions
staying-up-to-date
serverless-es7-service"

set text item delimiters to "
"

set theChapters to text items of theChaptersStr

#################
# Download PDFs #
#################
#repeat with theCurrentChapter in theChapters
#  downloadPdf(theCurrentChapter)
#end repeat

##############
# Merge PDFs #
##############
set outputFile to "~/Downloads/ServerlessStack.pdf"

set pdfFiles to "~/Downloads/Cover.pdf"
repeat with theCurrentChapter in theChapters
  set pdfFiles to pdfFiles & " ~/Downloads/" & theCurrentChapter & ".pdf"
end repeat

do shell script "/System/Library/Automator/Combine\\ PDF\\ Pages.action/Contents/Resources/join.py " & "-o " & outputFile & " " & pdfFiles

on downloadPdf(theChapterName)
  tell application "System Events"

    tell process "Safari"
      set frontmost to true

      if ((theChapterName as string) is equal to "index") then
        set urlPath to theChapterName
      else
        set urlPath to "chapters/" & theChapterName
      end if
      open location "https://serverless-stack.com/" & urlPath & ".html"

      delay 1

      repeat until menu item "Export as PDF…" of menu "File" of menu bar 1 exists
        delay 0.2
      end repeat
      repeat until menu item "Export as PDF…" of menu "File" of menu bar 1 is enabled
        delay 0.2
      end repeat
      click menu item "Export as PDF…" of menu "File" of menu bar 1

      tell window 1
        repeat until sheet 1 exists
        end repeat

        tell sheet 1
          keystroke theChapterName
          click pop up button "Where:"

          repeat until menu 1 of pop up button "Where:" exists
          end repeat
          click menu item "Downloads" of menu 1 of pop up button "Where:"
          click button "Save"
        end tell

      end tell

      click menu item "Close Tab" of menu "File" of menu bar 1

    end tell

  end tell
end downloadPdf

############################
# How to generate the PDFs #
############################
# 1. Update cover.html with the new version date
# 2. Open cover.html in safari and generate Cover.pdf
# 3. Place Cover.pdf in ~/Downloads folder
# 4. Ensure "Downloads" folder is an option when "Export to PDF…" in Safari
# 5. In terminal, run "osascript pdf.scpt"

set theChaptersStr to "index
who-is-this-guide-for
what-does-this-guide-cover
how-to-get-help
what-is-serverless
why-create-serverless-apps
create-an-aws-account
create-an-iam-user
what-is-iam
what-is-an-arn
configure-the-aws-cli
create-a-dynamodb-table
create-an-s3-bucket-for-file-uploads
create-a-cognito-user-pool
create-a-cognito-test-user
setup-the-serverless-framework
add-support-for-es6-es7-javascript
add-a-create-note-api
add-a-get-note-api
add-a-list-all-the-notes-api
add-an-update-note-api
add-a-delete-note-api
deploy-the-apis
create-a-cognito-identity-pool
cognito-user-pool-vs-identity-pool
test-the-apis
create-a-new-reactjs-app
add-app-favicons
setup-custom-fonts
setup-bootstrap
handle-routes-with-react-router
create-containers
adding-links-in-the-navbar
handle-404s
configure-aws-amplify
create-a-login-page
login-with-aws-cognito
add-the-session-to-the-state
load-the-state-from-the-session
clear-the-session-on-logout
redirect-on-login-and-logout
give-feedback-while-logging-in
create-a-signup-page
create-the-signup-form
signup-with-aws-cognito
add-the-create-note-page
call-the-create-api
upload-a-file-to-s3
list-all-the-notes
call-the-list-api
display-a-note
render-the-note-form
save-changes-to-a-note
delete-a-note
setup-secure-pages
create-a-route-that-redirects
use-the-redirect-routes
redirect-on-login
deploy-the-frontend
create-an-s3-bucket
deploy-to-s3
create-a-cloudfront-distribution
setup-your-domain-with-cloudfront
setup-www-domain-redirect
setup-ssl
deploy-updates
update-the-app
deploy-again
wrapping-up
giving-back
changelog
staying-up-to-date
api-gateway-and-lambda-logs
debugging-serverless-api-issues
serverless-environment-variables
stages-in-serverless-framework
configure-multiple-aws-profiles
customize-the-serverless-iam-policy
code-splitting-in-create-react-app
environments-in-create-react-app
connect-to-api-gateway-with-iam-auth
serverless-nodejs-starter"

set text item delimiters to "
"

set theChapters to text items of theChaptersStr

#################
# Download PDFs #
#################
#repeat with theCurrentChapter in theChapters
#  downloadPdf(theCurrentChapter)
#end repeat

##############
# Merge PDFs #
##############
set outputFile to "~/Downloads/ServerlessStack.pdf"

set pdfFiles to "~/Downloads/Cover.pdf"
repeat with theCurrentChapter in theChapters
  set pdfFiles to pdfFiles & " ~/Downloads/" & theCurrentChapter & ".pdf"
end repeat

do shell script "/System/Library/Automator/Combine\\ PDF\\ Pages.action/Contents/Resources/join.py " & "-o " & outputFile & " " & pdfFiles

on downloadPdf(theChapterName)
  tell application "System Events"

    tell process "Safari"
      set frontmost to true

      if ((theChapterName as string) is equal to "index") then
        set urlPath to theChapterName
      else
        set urlPath to "chapters/" & theChapterName
      end if
      open location "https://serverless-stack.com/" & urlPath & ".html"

      delay 1

      repeat until menu item "Export as PDF…" of menu "File" of menu bar 1 exists
        delay 0.2
      end repeat
      repeat until menu item "Export as PDF…" of menu "File" of menu bar 1 is enabled
        delay 0.2
      end repeat
      click menu item "Export as PDF…" of menu "File" of menu bar 1

      tell window 1
        repeat until sheet 1 exists
        end repeat

        tell sheet 1
          keystroke theChapterName
          click pop up button "Where:"

          repeat until menu 1 of pop up button "Where:" exists
          end repeat
          click menu item "Downloads" of menu 1 of pop up button "Where:"
          click button "Save"
        end tell

      end tell

      click menu item "Close Tab" of menu "File" of menu bar 1

    end tell

  end tell
end downloadPdf

Update chapter list for PDF

Update chapter list for PDF


-- core configuration --

-- configurable options --

-- limit number of songs to improve efficiency
property resultLimit : 15
-- whether or not to retrieve/display album artwork for each result
property albumArtEnabled : true

-- workflow parameters --

-- workflow folders
property libraryFolder : (path to library folder from user domain as text)
property cacheFolder : (libraryFolder & "Caches:")
property alfredWorkflowDataFolder : (cacheFolder & "com.runningwithcrayons.Alfred:Workflow Data:")
property bundleId : "com.calebevans.playsong"
property workflowCacheFolder : (alfredWorkflowDataFolder & bundleId & ":") as text

-- the default icon used for search results without album artwork
property defaultIconName : "resources/icon-noartwork.png"
-- the name of the playlist used by the workflow for playing songs
property workflowPlaylistName : "Alfred Play Song"
-- list of Alfred results
property resultList : {}

-- replaces substring in string with another substring
on replace(replaceThis, replaceWith, theString)

	set oldDelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to replaceThis
	set strItems to text items of theString
	set AppleScript's text item delimiters to replaceWith
	set newString to strItems as text
	set AppleScript's text item delimiters to oldDelims
	return newString

end replace

-- retrieve the plural/singular form of a quantity based on the given number
on quantifyNumber(theNumber, quantityName, pluralQuantityName)

	if theNumber is 1 then

		set theString to (theNumber as text) & space & quantityName

	else

		set theString to (theNumber as text) & space & pluralQuantityName

	end if

	return theString

end quantifyNumber

-- encodes JSON reserved characters in the given string
on encodeFeedbackChars(theString)

	set theString to replace("\\", "\\\\", theString)
	set theString to replace("\"", "\\\"", theString)
	return theString

end encodeFeedbackChars

-- decodes JSON reserved characters in the given string
on decodeFeedbackChars(theString)

	set theString to replace("\\\\", "\\", theString)
	set theString to replace("\\\"", "\"", theString)
	return theString

end decodeFeedbackChars

-- adds Alfred result to result list
on addResult(theResult)

	copy theResult to the end of resultList

end addResult

-- adds item for "No Results" message
on addNoResultsItem(query, queryType)

	addResult({uid:"no-results", valid:"no", title:"No Results Found", subtitle:("No " & queryType & "s matching '" & query & "'"), icon:defaultIconName})

end addNoResultsItem

on resultListIsFull()

	return (length of resultList is resultLimit)

end resultListIsFull

on resultListIsEmpty()

	return (length of resultList is 0)

end resultListIsFull

-- builds Alfred result item as JSON
on getResultFeedback(theResult)

	-- encode reserved JSON characters
	set resultUid to encodeFeedbackChars(uid of theResult)
	set resultValid to (valid of theResult) as text
	set resultTitle to encodeFeedbackChars(title of theResult)
	set resultSubtitle to encodeFeedbackChars(subtitle of theResult)

	if (icon of theResult) contains ":" then

		set resultIcon to encodeFeedbackChars(POSIX path of icon of theResult)

	else

		set resultIcon to icon of theResult

	end if

	set json to "{"
	set json to json & "\"uid\":\"" & resultUid & "\","
	set json to json & "\"arg\":\"" & resultUid & "\","
	set json to json & "\"valid\":\"" & resultValid & "\","
	set json to json & "\"title\":\"" & resultTitle & "\","
	set json to json & "\"subtitle\":\"" & resultSubtitle & "\","
	set json to json & "\"icon\":{\"path\":\"" & resultIcon & "\"}"
	set json to json & "}"
	return json

end getResultFeedback

-- retrieves JSON document for Alfred results
on getResultListFeedback()

	set json to "{\"items\": ["

	repeat with theResult in resultList

		set json to json & getResultFeedback(theResult)
		set json to json & ","

	end repeat

	-- remove trailing comma after last item
	set json to text 1 thru (length of json - 1) of json
	set json to json & "]}"
	return json

end getResultListFeedback

-- query path to artwork image file cached natively by Music.app in Catalina
on getSongArtworkPath(theSong)

	try

		if albumArtEnabled is false then return defaultIconName

		tell application "Music"

			-- get persistent ID of song and use it to fetch album artwork
			set songId to persistent ID of theSong
			-- the base path to the artwork file (without extension)
			set artworkPath to (do shell script "./resources/get-song-artwork-path.sh" & space & songId & space & defaultIconName)

		end tell

		return artworkPath

	on error errorMessage

		log errorMessage

	end try

end getSongArtworkPath

-- creates album artwork cache
on createWorkflowPlaylist()

	tell application "Music"

		if not (user playlist workflowPlaylistName exists) then

			make new user playlist with properties {name:workflowPlaylistName, shuffle:false}

		end if

	end tell

end createWorkflowPlaylist

on clearQueue()

	tell application "Music"

		if user playlist workflowPlaylistName exists then

			delete tracks of user playlist workflowPlaylistName

		end if

	end tell

end clearQueue

on queueSongs(theSongs)

	tell application "Music"

		repeat with theSong in theSongs

			duplicate theSong to user playlist workflowPlaylistName

		end repeat

	end tell

end queueSongs

on playQueue()

	tell application "Music"

		if number of tracks in user playlist workflowPlaylistName is not 0 then

			play user playlist workflowPlaylistName

		end if

	end tell

end playQueue

on getPlaylist(playlistId)

	tell application "Music"

		return (first playlist whose id is playlistId)

	end tell

end getPlaylist

on getPlaylistSongs(playlistId)

	tell application "Music"

		set thePlaylist to getPlaylist(playlistId) of me
		set playlistSongs to every track of thePlaylist

	end tell

	return playlistSongs

end getPlaylistSongs

-- retrieves list of songs within the given genre, sorted by artist
on getGenreSongs(genreName)

	tell application "Music"

		return every track of playlist 2 whose genre is genreName

	end tell

end getGenreSongs

-- retrieves list of album names for the given artist
on getArtistAlbums(artistName)

	tell application "Music"

		set artistSongs to every track of playlist 2 whose artist is artistName
		set albumNames to {}

		repeat with theSong in artistSongs

			if (album of theSong) is not in albumNames then

				set albumNames to albumNames & (album of theSong)

			end if

		end repeat

	end tell

	return albumNames

end getArtistAlbums

-- retrieves list of songs by the given artist, sorted by album
on getArtistSongs(artistName)

	tell application "Music"

		set albumNames to getArtistAlbums(artistName) of me
		set artistSongs to {}

		repeat with albumName in albumNames

			set albumSongs to (every track of playlist 2 whose artist is artistName and album is albumName)
			set artistSongs to artistSongs & albumSongs

		end repeat

	end tell

	return artistSongs

end getArtistSongs

-- retrieves list of songs in the given album
on getAlbumSongs(albumName)

	tell application "Music"

		set albumSongs to every track of playlist 2 whose album is albumName

	end tell

	return albumSongs

end getAlbumSongs

-- retrieves the song with the given ID
on getSong(songId)

	tell application "Music"

		set theSong to first track of playlist 2 whose database ID is songId

	end tell

	return theSong

end getSong

-- retrieves a list of objects or names matching the given query and type
on getResultsFromQuery(query, queryType)

	set evalScript to run script "
	script

		on findResults(query, queryType, resultLimit)

			tell application \"Music\"

				set theSongs to (get every track in playlist 2 whose " & queryType & " starts with query)

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains (space & query) and " & queryType & " does not start with query)

				end if

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains query and " & queryType & " does not start with query and " & queryType & " does not contain (space & query))

				end if

				if length of theSongs is 0 then

					if queryType is \"name\" then

						set theSongs to theSongs & (search playlist 2 for query only songs)

					else if queryType is not \"genre\" then

						set theSongs to theSongs & (search playlist 2 for query only " & queryType & "s)

					end if

				end if

				if queryType is \"name\" then

					if length of theSongs > resultLimit then

						set theSongs to items 1 thru resultLimit of theSongs

					end if

					set theResults to theSongs

				else

					set theResults to {}

					repeat with theSong in theSongs

						if length of theResults is resultLimit then exit repeat

						set theResult to " & queryType & " of theSong

						if theResult is not in theResults then

							set theResults to theResults & theResult

						end if

					end repeat

				end if

			end tell

			return theResults

		end findResults

	end script
	"

	evalScript's findResults(query, queryType, resultLimit)

end getResultsFromQuery

-- returns the given string with leading and trailing whitespace removed
on trimWhitespace(theString)

	-- trim leading whitespace
	repeat while theString begins with space

		if length of theString is 1 then return ""
		set theString to text 2 thru end of theString

	end repeat

	-- trim trailing whitespace
	repeat while theString ends with space

		set theString to text 1 thru ((length of theString) - 1) of theString

	end repeat

	return theString

end trimWhitespace

-- queues the song with the given ID
on queueSong(songId)

	set theSong to getSong(songId)
	queueSongs({theSong})

end queueSong

-- queues all songs belonging to the given album
on queueAlbum(albumName)

	set albumName to decodeFeedbackChars(albumName)
	set albumSongs to getAlbumSongs(albumName)
	queueSongs(albumSongs)

end queueAlbum

-- queues all songs by the given artist
on queueArtist(artistName)

	set artistName to decodeFeedbackChars(artistName)
	set artistSongs to getArtistSongs(artistName)
	queueSongs(artistSongs)

end queueArtist

-- queues all songs within the given genre
on queueGenre(genreName)

	set genreName to decodeFeedbackChars(genreName)
	set genreSongs to getGenreSongs(genreName)
	queueSongs(genreSongs)

end queueGenre

-- queues all songs in the given playlist
on queuePlaylist(playlistId)

	set playlistSongs to getPlaylistSongs(playlistId)
	queueSongs(playlistSongs)

end queuePlaylist

-- parses the given result query to retrieve type and id of item to queue
on parseResultQuery(query)

	set pos to offset of "-" in query
	set theType to text 1 thru (pos - 1) of query
	set theId to text (pos + 1) thru end of query
	return {type:theType, id:theId}

end parseResultQuery

on queue(query)

	set typeAndId to parseResultQuery(query)
	set theType to type of typeAndId
	set theId to id of typeAndId

	createWorkflowPlaylist()

	if theType is "song" then
		queueSong(theId)
	else if theType is "album" then
		queueAlbum(theId)
	else if theType is "artist" then
		queueArtist(theId)
	else if theType is "genre" then
		queueGenre(theId)
	else if theType is "playlist" then
		queuePlaylist(theId)
	else
		log "Unsupported type: " & theType
	end if

end queue

on play(query)

	clearQueue()
	queue(query)
	playQueue()

end play

-- core configuration --

-- configurable options --

-- limit number of songs to improve efficiency
property resultLimit : 15
-- whether or not to retrieve/display album artwork for each result
property albumArtEnabled : true

-- workflow parameters --

-- workflow folders
property libraryFolder : (path to library folder from user domain as text)
property cacheFolder : (libraryFolder & "Caches:")
property alfredWorkflowDataFolder : (cacheFolder & "com.runningwithcrayons.Alfred:Workflow Data:")
property bundleId : "com.calebevans.playsong"
property workflowCacheFolder : (alfredWorkflowDataFolder & bundleId & ":") as text

-- the default icon used for search results without album artwork
property defaultIconName : "resources/icon-noartwork.png"
-- the name of the playlist used by the workflow for playing songs
property workflowPlaylistName : "Alfred Play Song"
-- list of Alfred results
property resultList : {}

-- replaces substring in string with another substring
on replace(replaceThis, replaceWith, theString)

	set oldDelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to replaceThis
	set strItems to text items of theString
	set AppleScript's text item delimiters to replaceWith
	set newString to strItems as text
	set AppleScript's text item delimiters to oldDelims
	return newString

end replace

-- retrieve the plural/singular form of a quantity based on the given number
on quantifyNumber(theNumber, quantityName, pluralQuantityName)

	if theNumber is 1 then

		set theString to (theNumber as text) & space & quantityName

	else

		set theString to (theNumber as text) & space & pluralQuantityName

	end if

	return theString

end quantifyNumber

-- encodes JSON reserved characters in the given string
on encodeFeedbackChars(theString)

	set theString to replace("\\", "\\\\", theString)
	set theString to replace("\"", "\\\"", theString)
	return theString

end encodeFeedbackChars

-- decodes JSON reserved characters in the given string
on decodeFeedbackChars(theString)

	set theString to replace("\\\\", "\\", theString)
	set theString to replace("\\\"", "\"", theString)
	return theString

end decodeFeedbackChars

-- adds Alfred result to result list
on addResult(theResult)

	copy theResult to the end of resultList

end addResult

-- adds item for "No Results" message
on addNoResultsItem(query, queryType)

	addResult({uid:"no-results", valid:"no", title:"No Results Found", subtitle:("No " & queryType & "s matching '" & query & "'"), icon:defaultIconName})

end addNoResultsItem

on resultListIsFull()

	return (length of resultList is resultLimit)

end resultListIsFull

on resultListIsEmpty()

	return (length of resultList is 0)

end resultListIsFull

-- builds Alfred result item as JSON
on getResultFeedback(theResult)

	-- encode reserved JSON characters
	set resultUid to encodeFeedbackChars(uid of theResult)
	set resultValid to (valid of theResult) as text
	set resultTitle to encodeFeedbackChars(title of theResult)
	set resultSubtitle to encodeFeedbackChars(subtitle of theResult)

	if (icon of theResult) contains ":" then

		set resultIcon to encodeFeedbackChars(POSIX path of icon of theResult)

	else

		set resultIcon to icon of theResult

	end if

	set json to "{"
	set json to json & "\"uid\":\"" & resultUid & "\","
	set json to json & "\"arg\":\"" & resultUid & "\","
	set json to json & "\"valid\":\"" & resultValid & "\","
	set json to json & "\"title\":\"" & resultTitle & "\","
	set json to json & "\"subtitle\":\"" & resultSubtitle & "\","
	set json to json & "\"icon\":{\"path\":\"" & resultIcon & "\"}"
	set json to json & "}"
	return json

end getResultFeedback

-- retrieves JSON document for Alfred results
on getResultListFeedback()

	set json to "{\"items\": ["

	repeat with theResult in resultList

		set json to json & getResultFeedback(theResult)
		set json to json & ","

	end repeat

	-- remove trailing comma after last item
	set json to text 1 thru (length of json - 1) of json
	set json to json & "]}"
	return json

end getResultListFeedback

-- query path to artwork image file cached natively by Music.app in Catalina
on getSongArtworkPath(theSong)

	try

		if albumArtEnabled is false then return defaultIconName

		tell application "Music"

			-- get persistent ID of song and use it to fetch album artwork
			set songId to persistent ID of theSong
			-- the base path to the artwork file (without extension)
			set artworkPath to (do shell script "./resources/get-song-artwork-path.sh" & space & songId & space & defaultIconName)

		end tell

		return artworkPath

	on error errorMessage

		log errorMessage

	end try

end getSongArtworkPath

-- creates album artwork cache
on createWorkflowPlaylist()

	tell application "Music"

		if not (user playlist workflowPlaylistName exists) then

			make new user playlist with properties {name:workflowPlaylistName, shuffle:false}

		end if

	end tell

end createWorkflowPlaylist

on clearQueue()

	tell application "Music"

		if user playlist workflowPlaylistName exists then

			delete tracks of user playlist workflowPlaylistName

		end if

	end tell

end clearQueue

on queueSongs(theSongs)

	tell application "Music"

		repeat with theSong in theSongs

			duplicate theSong to user playlist workflowPlaylistName

		end repeat

	end tell

end queueSongs

on playQueue()

	tell application "Music"

		if number of tracks in user playlist workflowPlaylistName is not 0 then

			play user playlist workflowPlaylistName

		end if

	end tell

end playQueue

on getPlaylist(playlistId)

	tell application "Music"

		return (first playlist whose id is playlistId)

	end tell

end getPlaylist

on getPlaylistSongs(playlistId)

	tell application "Music"

		set thePlaylist to getPlaylist(playlistId) of me
		set playlistSongs to every track of thePlaylist

	end tell

	return playlistSongs

end getPlaylistSongs

-- retrieves list of songs within the given genre, sorted by artist
on getGenreSongs(genreName)

	tell application "Music"

		return every track of playlist 2 whose genre is genreName

	end tell

end getGenreSongs

-- retrieves list of songs by the given artist, sorted by album
on getArtistSongs(artistName)

	tell application "Music"

		set artistSongs to every track of playlist 2 whose artist is artistName

	end tell

	return artistSongs

end getArtistSongs

-- retrieves list of songs in the given album
on getAlbumSongs(albumName)

	tell application "Music"

		set albumSongs to every track of playlist 2 whose album is albumName

	end tell

	return albumSongs

end getAlbumSongs

-- retrieves the song with the given ID
on getSong(songId)

	tell application "Music"

		set theSong to first track of playlist 2 whose database ID is songId

	end tell

	return theSong

end getSong

-- retrieves a list of objects or names matching the given query and type
on getResultsFromQuery(query, queryType)

	set evalScript to run script "
	script

		on findResults(query, queryType, resultLimit)

			tell application \"Music\"

				set theSongs to (get every track in playlist 2 whose " & queryType & " starts with query)

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains (space & query) and " & queryType & " does not start with query)

				end if

				if length of theSongs < resultLimit then

					set theSongs to theSongs & (get every track in playlist 2 whose " & queryType & " contains query and " & queryType & " does not start with query and " & queryType & " does not contain (space & query))

				end if

				if length of theSongs is 0 then

					if queryType is \"name\" then

						set theSongs to theSongs & (search playlist 2 for query only songs)

					else if queryType is not \"genre\" then

						set theSongs to theSongs & (search playlist 2 for query only " & queryType & "s)

					end if

				end if

				if queryType is \"name\" then

					if length of theSongs > resultLimit then

						set theSongs to items 1 thru resultLimit of theSongs

					end if

					set theResults to theSongs

				else

					set theResults to {}

					repeat with theSong in theSongs

						if length of theResults is resultLimit then exit repeat

						set theResult to " & queryType & " of theSong

						if theResult is not in theResults then

							set theResults to theResults & theResult

						end if

					end repeat

				end if

			end tell

			return theResults

		end findResults

	end script
	"

	evalScript's findResults(query, queryType, resultLimit)

end getResultsFromQuery

-- returns the given string with leading and trailing whitespace removed
on trimWhitespace(theString)

	-- trim leading whitespace
	repeat while theString begins with space

		if length of theString is 1 then return ""
		set theString to text 2 thru end of theString

	end repeat

	-- trim trailing whitespace
	repeat while theString ends with space

		set theString to text 1 thru ((length of theString) - 1) of theString

	end repeat

	return theString

end trimWhitespace

-- queues the song with the given ID
on queueSong(songId)

	set theSong to getSong(songId)
	queueSongs({theSong})

end queueSong

-- queues all songs belonging to the given album
on queueAlbum(albumName)

	set albumName to decodeFeedbackChars(albumName)
	set albumSongs to getAlbumSongs(albumName)
	queueSongs(albumSongs)

end queueAlbum

-- queues all songs by the given artist
on queueArtist(artistName)

	set artistName to decodeFeedbackChars(artistName)
	set artistSongs to getArtistSongs(artistName)
	queueSongs(artistSongs)

end queueArtist

-- queues all songs within the given genre
on queueGenre(genreName)

	set genreName to decodeFeedbackChars(genreName)
	set genreSongs to getGenreSongs(genreName)
	queueSongs(genreSongs)

end queueGenre

-- queues all songs in the given playlist
on queuePlaylist(playlistId)

	set playlistSongs to getPlaylistSongs(playlistId)
	queueSongs(playlistSongs)

end queuePlaylist

-- parses the given result query to retrieve type and id of item to queue
on parseResultQuery(query)

	set pos to offset of "-" in query
	set theType to text 1 thru (pos - 1) of query
	set theId to text (pos + 1) thru end of query
	return {type:theType, id:theId}

end parseResultQuery

on queue(query)

	set typeAndId to parseResultQuery(query)
	set theType to type of typeAndId
	set theId to id of typeAndId

	createWorkflowPlaylist()

	if theType is "song" then
		queueSong(theId)
	else if theType is "album" then
		queueAlbum(theId)
	else if theType is "artist" then
		queueArtist(theId)
	else if theType is "genre" then
		queueGenre(theId)
	else if theType is "playlist" then
		queuePlaylist(theId)
	else
		log "Unsupported type: " & theType
	end if

end queue

on play(query)

	clearQueue()
	queue(query)
	playQueue()

end play

Make playartist filter faster and honor sort order


on escape_quotes(string_to_escape)
	set AppleScript's text item delimiters to the "\""
	set the item_list to every text item of string_to_escape
	set AppleScript's text item delimiters to the "\\\""
	set string_to_escape to the item_list as string
	set AppleScript's text item delimiters to ""
	return string_to_escape
end escape_quotes

tell application "Spotify"
	set ctrack to "{"
	set ctrack to ctrack & "\"artist\": \"" & my escape_quotes(current track's artist) & "\""
	set ctrack to ctrack & ",\"album\": \"" & my escape_quotes(current track's album) & "\""
	set ctrack to ctrack & ",\"disc_number\": " & current track's disc number
	set ctrack to ctrack & ",\"duration\": " & current track's duration
	set ctrack to ctrack & ",\"played_count\": " & current track's played count
	set ctrack to ctrack & ",\"track_number\": " & current track's track number
	set ctrack to ctrack & ",\"popularity\": " & current track's popularity
	set ctrack to ctrack & ",\"id\": \"" & current track's id & "\""
	set ctrack to ctrack & ",\"name\": \"" & my escape_quotes(current track's name) & "\""
	set ctrack to ctrack & ",\"album_artist\": \"" & my escape_quotes(current track's album artist) & "\""
	set ctrack to ctrack & ",\"artwork_url\": \"" & current track's artwork url & "\""
	set ctrack to ctrack & ",\"spotify_url\": \"" & current track's spotify url & "\""
	set ctrack to ctrack & "}"
end tell

remove tab

remove tab


on replace_chars(this_text, search_string, replacement_string)
  set AppleScript's text item delimiters to the search_string
  set the item_list to every text item of this_text
  set AppleScript's text item delimiters to the replacement_string
  set this_text to the item_list as string
  set AppleScript's text item delimiters to ""
  return this_text
end replace_chars

on file_exists(filePath)
  tell application "Finder" to return exists filePath as POSIX file
end file_exists

tell application "Spotify"
  set currentArtwork to current track's artwork
  set savePath to POSIX path of (path to temporary items from user domain as string) & my replace_chars(current track's id,":","_")
  set tiffPath to (savePath & ".tiff")
  set pngPath to (savePath & ".png")
end tell

-- don't do the image events dance when the file already exists
if my file_exists(pngPath) then return pngPath

tell application "System Events"
  if not (my file_exists(tiffPath)) then
    set fileRef to (open for access tiffPath with write permission)
    write currentArtwork to fileRef
    close access fileRef
  end

  tell application "Image Events"
    launch
    set theImage to open tiffPath
    save theImage as PNG in pngPath
    quit
  end tell

  delete file tiffPath
  quit
end tell

return pngPath

on replace_chars(this_text, search_string, replacement_string)
  set AppleScript's text item delimiters to the search_string
  set the item_list to every text item of this_text
  set AppleScript's text item delimiters to the replacement_string
  set this_text to the item_list as string
  set AppleScript's text item delimiters to ""
  return this_text
end replace_chars

on file_exists(filePath)
  tell application "Finder" to return exists filePath as POSIX file
end file_exists

tell application "Spotify"
  set currentArtwork to current track's artwork
  set savePath to POSIX path of (path to temporary items from user domain as string) & my replace_chars(current track's id,":","_")
  set tiffPath to (savePath & ".tiff")
  set pngPath to (savePath & ".png")
end tell

-- don't do the image events dance when the file already exists
if my file_exists(pngPath) then return pngPath

if not (my file_exists(tiffPath)) then
  set fileRef to (open for access tiffPath with write permission)
  write currentArtwork to fileRef
  close access fileRef
end

tell application "Image Events"
  launch
  set theImage to open tiffPath
  save theImage as PNG in pngPath
  delete file tiffPath
  quit
end tell

return pngPath

remove unnecessary usage of System Events


-- fmGUI_ManageScripts_FmScript_Select({})
-- Daniel A. Shockley, NYHTC
-- Select an existing script in the Script Workspace.

(*
HISTORY:
	2020-03-03 ( dshockley, hdu ): Updated as standalone function for fm-scripts git repository.
	1.3.1 - 2017-06-05 ( eshagdar ): the name of the script is the name of UI elemnt, not the value of a text field.
	1.3 - 2017-01-12 ( eshagdar ): updated for FM15 - script names are now in a splitter group.
	2017-06-05 ( eshagdar ): we should just be using the search bar instead of scanning through the list

REQUIRES:
	fmGUI_AppFrontMost
	fmGUI_DataViewer_Close
	fmGUI_Inspector_Close
	fmGUI_ManageScripts_Open
	fmGUI_ManageScripts_ScriptListFocus
	fmGUI_ManageScripts_SearchBoxClear
	fmGUI_ManageScripts_SearchBoxFind
	unParseChars

*)

property debugMode : true
property ScriptName : "fmGUI_ManageScripts_FmScript_Select"

on run
	fmGUI_ManageScripts_FmScript_Select({})
end run

--------------------
-- START OF CODE
--------------------

on fmGUI_ManageScripts_FmScript_Select(prefs)
	-- version 1.3.1

	-- Attempt to select a FileMaker Script based on name or, alternatively, other partial matches.

	set defaultPrefs to {fmScriptName:null, altPatterns:null}
	set prefs to prefs & defaultPrefs
	(* EXAMPLE:
	altPatterns:{
		{pattern:{{testType:"is", testMatch:"z_Auto1"}}},
		{pattern:{{testType:"startsWith", testMatch:"z"},{testType:"endsWith", testMatch:"_Auto1"}}}
		}
	*)

	-- CONSTANTS:
	set testCodeHeader to "tell application \"System Events\"" & return & "tell application process \"FileMaker Pro Advanced\"" & return
	set selectCode to "select ("
	set rowRefCode to "first row of (outline 1 of scroll area 1 of splitter group 1 of window 1)"
	set whoseCode to " whose "
	set scriptNameObjectCode to "name of UI Element 1"
	-- (then some test will be inserted)
	set betweenTestsCode to " and "
	-- (then possibly some other test will be inserted)
	set afterAllTestsCode to ")"
	set testCodeFooter to return & "end tell" & return & "end tell"

	try
		if fmScriptName of prefs is equal to "-" then error "This is a SEPARATOR SCRIPT - don't try to edit it!" number -1024

		tell application "htcLib"
			fmGUI_AppFrontMost()
			fmGUI_Inspector_Close()
			fmGUI_DataViewer_Close()
			fmGUI_ManageScripts_Open({})
			fmGUI_ManageScripts_ScriptListFocus({})
		end tell

		fmGUI_ManageScripts_SearchBoxFind({searchCriteria:fmScriptName of prefs})
		tell application "System Events"
			tell application process "FileMaker Pro Advanced"
				(*
				set searchField to text field 1 of splitter group 1 of window 1
				set focused of searchField to true
				delay 0.25
				keystroke "a" using command down
				key code 51 -- clear the search box
				delay 0.25
				set value of searchField to fmScriptName of prefs
				keystroke tab
				*)

				-- SELECT the easy way if the object is directly available:
				if (exists (first row of (outline 1 of scroll area 1 of splitter group 1 of window 1) whose name of UI element 1 is fmScriptName of prefs)) then
					select (first row of (outline 1 of scroll area 1 of splitter group 1 of window 1) whose name of UI element 1 is fmScriptName of prefs)
					return true

				else -- if NOT found, then seek by alternate patterns, if specified:

					-- NOT FOUND, so try each altPattern:
					set scriptNowSelected to false

					my fmGUI_ManageScripts_SearchBoxClear({})
					repeat with oneAltPattern in altPatterns of prefs
						set testList to contents of pattern of oneAltPattern
						if testList is equal to {{}} then
							--list of empty list, so we can't do anything with it
						else
							-- there are some valid items, so loop over them

							try
								-- BEGIN: try oneAltPattern:
								set testCode to {}
								tell application "htcLib"
									repeat with oneTestRec in testList
										copy ("" & testType of oneTestRec & " " & quoteString(testMatch of oneTestRec)) to end of testCode
									end repeat
								end tell

								tell application "htcLib" to set testCode to unParseChars(testCode, betweenTestsCode & scriptNameObjectCode & " ")
								set testCode to selectCode & rowRefCode & whoseCode & scriptNameObjectCode & " " & testCode & afterAllTestsCode
								set testCode to testCodeHeader & testCode & testCodeFooter
								tell me to run script testCode

								set scriptNowSelected to true -- no error, so we selected it.
								exit repeat -- no need to keep checking altPatterns

								-- ERROR: try oneAltPattern.
							on error errMsg number errNum
								if errNum is not -1719 then
									-- some error OTHER than not found in list:
									error errMsg number errNum
								else
									-- just try the next altPattern, so continue repeat loop.
								end if
								-- END OF: try oneAltPattern.
							end try
							-- END OF: List had things.
						end if
					end repeat
				end if
			end tell
		end tell

		return scriptNowSelected
	on error errMsg number errNum
		error "unable to fmGUI_ManageScripts_FmScript_Select '" & fmScriptName of prefs & "' - " & errMsg number errNum
	end try
end fmGUI_ManageScripts_FmScript_Select

--------------------
-- END OF CODE
--------------------

on fmGUI_AppFrontMost()
	tell application "htcLib" to fmGUI_AppFrontMost()
end fmGUI_AppFrontMost

on fmGUI_DataViewer_Close()
	tell application "htcLib" to fmGUI_DataViewer_Close()
end fmGUI_DataViewer_Close

on fmGUI_Inspector_Close()
	tell application "htcLib" to fmGUI_Inspector_Close()
end fmGUI_Inspector_Close

on fmGUI_ManageScripts_Open(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_Open(prefs)
end fmGUI_ManageScripts_Open

on fmGUI_ManageScripts_ScriptListFocus(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_ScriptListFocus(prefs)
end fmGUI_ManageScripts_ScriptListFocus

on fmGUI_ManageScripts_SearchBoxClear(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_SearchBoxClear(prefs)
end fmGUI_ManageScripts_SearchBoxClear

on fmGUI_ManageScripts_SearchBoxFind(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_SearchBoxFind(prefs)
end fmGUI_ManageScripts_SearchBoxFind

on unParseChars(prefs)
	tell application "htcLib" to unParseChars(prefs)
end unParseChars

-- fmGUI_ManageScripts_FmScript_Select({})
-- Daniel A. Shockley, NYHTC
-- Select an existing script in the Script Workspace.

(*
HISTORY:
	2020-03-03 ( dshockley, hdu ): Updated as standalone function for fm-scripts git repository.
	1.3.1 - 2017-06-05 ( eshagdar ): the name of the script is the name of UI elemnt, not the value of a text field.
	1.3 - 2017-01-12 ( eshagdar ): updated for FM15 - script names are now in a splitter group.
	2017-06-05 ( eshagdar ): we should just be using the search bar instead of scanning through the list

REQUIRES:
	fmGUI_AppFrontMost
	fmGUI_DataViewer_Close
	fmGUI_Inspector_Close
	fmGUI_ManageScripts_Open
	fmGUI_ManageScripts_ScriptListFocus
	fmGUI_ManageScripts_SearchBoxClear
	fmGUI_ManageScripts_SearchBoxFind
	quoteString
	unParseChars

*)

property debugMode : true
property ScriptName : "fmGUI_ManageScripts_FmScript_Select"

on run
	fmGUI_ManageScripts_FmScript_Select({})
end run

--------------------
-- START OF CODE
--------------------

on fmGUI_ManageScripts_FmScript_Select(prefs)
	-- version 1.3.1

	-- Attempt to select a FileMaker Script based on name or, alternatively, other partial matches.

	set defaultPrefs to {fmScriptName:null, altPatterns:null}
	set prefs to prefs & defaultPrefs
	(* EXAMPLE:
	altPatterns:{
		{pattern:{{testType:"is", testMatch:"z_Auto1"}}},
		{pattern:{{testType:"startsWith", testMatch:"z"},{testType:"endsWith", testMatch:"_Auto1"}}}
		}
	*)

	-- CONSTANTS:
	set testCodeHeader to "tell application \"System Events\"" & return & "tell application process \"FileMaker Pro Advanced\"" & return
	set selectCode to "select ("
	set rowRefCode to "first row of (outline 1 of scroll area 1 of splitter group 1 of window 1)"
	set whoseCode to " whose "
	set scriptNameObjectCode to "name of UI Element 1"
	-- (then some test will be inserted)
	set betweenTestsCode to " and "
	-- (then possibly some other test will be inserted)
	set afterAllTestsCode to ")"
	set testCodeFooter to return & "end tell" & return & "end tell"

	try
		if fmScriptName of prefs is equal to "-" then error "This is a SEPARATOR SCRIPT - don't try to edit it!" number -1024

		fmGUI_AppFrontMost()
		fmGUI_Inspector_Close()
		fmGUI_DataViewer_Close()
		fmGUI_ManageScripts_Open({})
		fmGUI_ManageScripts_ScriptListFocus({})

		fmGUI_ManageScripts_SearchBoxFind({searchCriteria:fmScriptName of prefs})
		tell application "System Events"
			tell application process "FileMaker Pro Advanced"
				(*
				set searchField to text field 1 of splitter group 1 of window 1
				set focused of searchField to true
				delay 0.25
				keystroke "a" using command down
				key code 51 -- clear the search box
				delay 0.25
				set value of searchField to fmScriptName of prefs
				keystroke tab
				*)

				-- SELECT the easy way if the object is directly available:
				if (exists (first row of (outline 1 of scroll area 1 of splitter group 1 of window 1) whose name of UI element 1 is fmScriptName of prefs)) then
					select (first row of (outline 1 of scroll area 1 of splitter group 1 of window 1) whose name of UI element 1 is fmScriptName of prefs)
					return true

				else -- if NOT found, then seek by alternate patterns, if specified:

					-- NOT FOUND, so try each altPattern:
					set scriptNowSelected to false

					my fmGUI_ManageScripts_SearchBoxClear({})
					repeat with oneAltPattern in altPatterns of prefs
						set testList to contents of pattern of oneAltPattern
						if testList is equal to {{}} then
							--list of empty list, so we can't do anything with it
						else
							-- there are some valid items, so loop over them

							try
								-- BEGIN: try oneAltPattern:
								set testCode to {}
								repeat with oneTestRec in testList
									copy ("" & testType of oneTestRec & " " & quoteString(testMatch of oneTestRec)) to end of testCode
								end repeat

								set testCode to unParseChars(testCode, betweenTestsCode & scriptNameObjectCode & " ")
								set testCode to selectCode & rowRefCode & whoseCode & scriptNameObjectCode & " " & testCode & afterAllTestsCode
								set testCode to testCodeHeader & testCode & testCodeFooter
								tell me to run script testCode

								set scriptNowSelected to true -- no error, so we selected it.
								exit repeat -- no need to keep checking altPatterns

								-- ERROR: try oneAltPattern.
							on error errMsg number errNum
								if errNum is not -1719 then
									-- some error OTHER than not found in list:
									error errMsg number errNum
								else
									-- just try the next altPattern, so continue repeat loop.
								end if
								-- END OF: try oneAltPattern.
							end try
							-- END OF: List had things.
						end if
					end repeat
				end if
			end tell
		end tell

		return scriptNowSelected
	on error errMsg number errNum
		error "unable to fmGUI_ManageScripts_FmScript_Select '" & fmScriptName of prefs & "' - " & errMsg number errNum
	end try
end fmGUI_ManageScripts_FmScript_Select

--------------------
-- END OF CODE
--------------------

on fmGUI_AppFrontMost()
	tell application "htcLib" to fmGUI_AppFrontMost()
end fmGUI_AppFrontMost

on fmGUI_DataViewer_Close()
	tell application "htcLib" to fmGUI_DataViewer_Close()
end fmGUI_DataViewer_Close

on fmGUI_Inspector_Close()
	tell application "htcLib" to fmGUI_Inspector_Close()
end fmGUI_Inspector_Close

on fmGUI_ManageScripts_Open(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_Open(prefs)
end fmGUI_ManageScripts_Open

on fmGUI_ManageScripts_ScriptListFocus(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_ScriptListFocus(prefs)
end fmGUI_ManageScripts_ScriptListFocus

on fmGUI_ManageScripts_SearchBoxClear(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_SearchBoxClear(prefs)
end fmGUI_ManageScripts_SearchBoxClear

on fmGUI_ManageScripts_SearchBoxFind(prefs)
	tell application "htcLib" to fmGUI_ManageScripts_SearchBoxFind(prefs)
end fmGUI_ManageScripts_SearchBoxFind

on quoteString(prefs)
	tell application "htcLib" to quoteString(prefs)
end quoteString

on unParseChars(prefs)
	tell application "htcLib" to unParseChars(prefs)
end unParseChars

update fmGUI_ManageScripts_FmScript_Select


on run argv
  set window_from to date (item 1 of argv)
  set window_to   to date (item 2 of argv)

  tell application "Mail"
    set results to {}

    repeat with acct in every account
      set username to user name of acct
      try
        set mbox to mailbox "[Gmail]/All Mail" of acct
      on error
        set mbox to mailbox "Sent" of acct
      end try

      set msgs to (messages whose sender contains username and date sent >= window_from and date sent <= window_to) in mbox
      repeat with msg in msgs
        set the end of results to {subject:subject of msg, datetime:date sent of msg as string}
      end repeat
    end repeat

    results
  end tell
end run

on run argv
  set window_from to date (item 1 of argv)
  set window_to   to date (item 2 of argv)

  tell application "Mail"
    set results to {}

    set msgs to (messages of sent mailbox whose date sent >= window_from and date sent <= window_to)
    repeat with msg in msgs
      set the end of results to {subject:subject of msg, datetime:date sent of msg as string}
    end repeat

    results
  end tell
end run

Simplify sent mail script

Simplify sent mail script


(* GLOBAL PROPERTIES

&& $? for bash status codes

*)
property BUNDLER_VERSION : "devel"
--# Bundler paths
property _home : POSIX path of (path to "cusr" as text)
property BUNDLER_DIR : (_home) & "Library/Application Support/Alfred 2/Workflow Data/alfred.bundler-" & BUNDLER_VERSION
property DATA_DIR : BUNDLER_DIR & "/data"
property CACHE_DIR : (_home) & "Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/alfred.bundler-" & BUNDLER_VERSION
--# Main Applescript Bundler
property AS_BUNDLER : BUNDLER_DIR & "/bundler/AlfredBundler.scpt"
--# Root directory under which workflow-specific applescript libraries are installed
property APPLESCRIPT_DIR : DATA_DIR & "/assets/applescript"
--# Root directory under which Bundler utilities are installed
property UTILITIES_DIR : DATA_DIR & "/assets/utility"
--# Directory for Bundler icons
property ICONS_DIR : DATA_DIR & "/assets/icons"
--# Where color alternatives are cached
property COLOR_CACHE : DATA_DIR & "/color-cache"

(* GLOBAL VARIABLES *)

--# Where installer.sh can be downloaded from
global BASH_BUNDLET_URL
set BASH_BUNDLET_URL to my formatter("https://raw.githubusercontent.com/shawnrice/alfred-bundler/{}/bundler/bundlets/alfred.bundler.sh", BUNDLER_VERSION)

--# Bundler log file
global BUNDLER_LOGFILE
set BUNDLER_LOGFILE to my formatter((DATA_DIR & "/logs/bundler-{}.log"), BUNDLER_VERSION)

(* ///
USER API
/// *)

on utility(_name, _version, _json)
	--# partial support for "optional" args in Applescript
	if my is_empty(_version) then
		set _version to "latest"
	end if
	if my is_empty(_json) then
		set _json to ""
	end if
	--# path to specific utility
	set utility to my joiner({UTILITIES_DIR, _name, _version}, "/")
	if my folder_exists(utility) = false then
		--# if utility isn't already installed
		my logger("utility", "INFO", my formatter("Utility at `{}` not found...", utility))
		set bash_bundlet to (my dirname(AS_BUNDLER)) & "bundlets/alfred.bundler.sh"
		if my file_exists(bash_bundlet) = true then
			set bash_bundlet_cmd to quoted form of bash_bundlet
			set cmd to my joiner({bash_bundlet_cmd, "utility", _name, _version, _json}, space)
			set cmd to my prepare_cmd(cmd)
			my logger("utility", "INFO", my formatter("Running shell command: `{}`...", cmd))
			set full_path to (do shell script cmd)
			return full_path
		else
			set error_msg to my logger("utility", "DEBUG", my formatter("Internal bash bundlet `{}` does not exist.", bash_bundlet))
			error error_msg number 1
			--##TODO: need a stable error number schema
		end if
	else
		--# if utility is already installed
		my logger("utility", "INFO", my formatter("Utility at `{}` found...", utility))
		--# read utilities invoke file
		set invoke_file to utility & "/invoke"
		if my file_exists(invoke_file) = true then
			set invoke_path to my read_file(invoke_file)
			--# combine utility path with invoke path
			set full_path to utility & "/" & invoke_path
			return full_path
		else
			set error_msg to my logger("utility", "DEBUG", my formatter("Internal invoke file `{}` does not exist.", invoke_file))
			error error_msg number 1
			--##TODO: need a stable error number schema
		end if
	end if
end utility

on icon(_font, _name, _color, _alter)
	--# partial support for "optional" args in Applescript
	if my is_empty(_color) then
		set _color to "000000"
		if my is_empty(_alter) then
			set _alter to true
		end if
	end if
	if my is_empty(_alter) then
		set _alter to false
	end if
	--# path to specific icon
	set icon to my joiner({ICONS_DIR, _font, _color, _name}, "/")
	if my folder_exists(icon) = false then
		--# if icon isn't already installed
		my logger("icon", "INFO", my formatter("Icon at `{}` not found...", icon))
		set bash_bundlet to (my dirname(AS_BUNDLER)) & "bundlets/alfred.bundler.sh"
		if my file_exists(bash_bundlet) = true then
			set bash_bundlet_cmd to quoted form of bash_bundlet
			set cmd to my joiner({bash_bundlet_cmd, "icon", _font, _name, _color, _alter}, space)
			set cmd to my prepare_cmd(cmd)
			my logger("icon", "INFO", my formatter("Running shell command: `{}`...", cmd))
			set full_path to (do shell script cmd)
		else
			set error_msg to my logger("icon", "DEBUG", my formatter("Internal bash bundlet `{}` does not exist.", bash_bundlet))
			error error_msg number 1
			--##TODO: need a stable error number schema
		end if
	else
		--# if icon is already installed
		my logger("icon", "INFO", my formatter("Icon at `{}` found...", icon))
		return icon
	end if
end icon

(* ///
LOGGING HANDLER
/// *)

on logger(_handler, _level, _message)
	--# Ensure log file exists, with
	--# checking for scope of global var
	try
		my check_file(BUNDLER_LOGFILE)
	on error
		set BUNDLER_LOGFILE to my formatter((DATA_DIR & "/logs/bundler-{}.log"), BUNDLER_VERSION)
		my check_file(BUNDLER_LOGFILE)
	end try
	delay 0.1 -- delay to ensure IO action is completed
	--# Prepare time string format %Y-%m-%d %H:%M:%S
	set log_time to "[" & (my date_formatter()) & "]"
	set formatterted_time to text items 1 thru -4 of (time string of (current date)) as string
	set error_time to "[" & formatterted_time & "]"
	--# Prepare location message
	set _location to "[bundler.scpt:" & _handler & "]"
	--# Prepare level message
	set _level to "[" & _level & "]"
	--# Generate full error message for logging
	set log_msg to (my joiner({log_time, _location, _level, _message}, space)) & (ASCII character 10)
	my write_to_file(log_msg, BUNDLER_LOGFILE, true)
	--# Generate regular error message for returning to user
	set error_msg to error_time & space & _location & space & _level & space & _message
	return error_msg
end logger

(* ///
SUB-ACTION HANDLERS
/// *)

on date_formatter()
	set {year:y, month:m, day:d} to current date
	set date_num to (y * 10000 + m * 100 + d) as string
	set formatterted_date to ((text 1 thru 4 of date_num) & "-" & (text 5 thru 6 of date_num) & "-" & (text 7 thru 8 of date_num))
	set formatterted_time to text items 1 thru -4 of (time string of (current date)) as string
	return formatterted_date & space & formatterted_time
end date_formatter

on read_file(target_file)
	open for access POSIX file target_file
	set _contents to (read target_file)
	close access target_file
	return _contents
end read_file

on prepare_cmd(cmd)
	set pwd to quoted form of (my _pwd())
	return "cd " & pwd & "; bash " & cmd
end prepare_cmd

(* ///
IO HELPER FUNCTIONS
/// *)

on write_to_file(this_data, target_file, append_data)
	try
		set the target_file to the target_file as string
		set the open_target_file to open for access POSIX file target_file with write permission
		if append_data is false then set eof of the open_target_file to 0
		write this_data to the open_target_file starting at eof
		close access the open_target_file
		return true
	on error
		try
			close access file target_file
		end try
		return false
	end try
end write_to_file

--#
on pwd()
	set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "/"}
	set _path to (text items 1 thru -2 of (POSIX path of (path to me)) as string) & "/"
	set AppleScript's text item delimiters to ASTID
	return _path
end pwd

on dirname(_file)
	set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "/"}
	set _path to (text items 1 thru -2 of _file as string) & "/"
	set AppleScript's text item delimiters to ASTID
	return _path
end dirname

on check_dir(_folder)
	if not my folder_exists(_folder) then
		do shell script "mkdir -p " & (quoted form of _folder)
	end if
	return _folder
end check_dir

on check_file(_path)
	if not my file_exists(_path) then
		set _dir to dirname(_path)
		my check_dir(_dir)
		delay 0.1
		do shell script "touch " & (quoted form of _path)
	end if
end check_file

--#  handler to check if a folder exists
on folder_exists(_folder)
	if my path_exists(_folder) then
		tell application "System Events"
			return (class of (disk item _folder) is folder)
		end tell
	end if
	return false
end folder_exists

--# handler to check if a file exists
on file_exists(_file)
	if my path_exists(_file) then
		tell application "System Events"
			return (class of (disk item _file) is file)
		end tell
	end if
	return false
end file_exists

--#  handler to check if a path exists
on path_exists(_path)
	if _path is missing value or my is_empty(_path) then return false

	try
		if class of _path is alias then return true
		if _path contains ":" then
			alias _path
			return true
		else if _path contains "/" then
			POSIX file _path as alias
			return true
		else
			return false
		end if
	on error msg
		return false
	end try
end path_exists

(* ///
TEXT HELPER FUNCTIONS
/// *)

on split(str, delimiter)
	local delimiter, str, ASTID
	set ASTID to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to delimiter
		set str to text items of str
		set AppleScript's text item delimiters to ASTID
		return str --> list
	on error msg number num
		set AppleScript's text item delimiters to ASTID
		error "Can't explode: " & msg number num
	end try
end split

--# format string á la Python
on formatter(str, arg)
	local ASTID, str, arg, lst
	set ASTID to AppleScript's text item delimiters
	try
		considering case
			set AppleScript's text item delimiters to "{}"
			set lst to every text item of str
			set AppleScript's text item delimiters to arg
			set str to lst as string
		end considering
		set AppleScript's text item delimiters to ASTID
		return str
	on error msg number num
		set AppleScript's text item delimiters to ASTID
		error "Can't replaceString: " & msg number num
	end try
end formatter

--# removes white space surrounding a string
on trim(str)
	if class of str is not text or class of str is not string or str is missing value then return str
	if str is "" then return str
	repeat while str begins with " "
		try
			set str to items 2 thru -1 of str as text
		on error msg
			return ""
		end try
	end repeat
	repeat while str ends with " "
		try
			set str to items 1 thru -2 of str as text
		on error
			return ""
		end try
	end repeat
	return str
end trim

on joiner(pieces, delim)
	local delim, pieces, ASTID
	set ASTID to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to delim
		set pieces to "" & pieces
		set AppleScript's text item delimiters to ASTID
		return pieces --> text
	on error emsg number eNum
		set AppleScript's text item delimiters to ASTID
		error "Can't implode: " & emsg number eNum
	end try
end joiner

(* ///
LOWER LEVEL HELPER FUNCTIONS
/// *)

--# checks if a value is empty
on is_empty(str)
	if {true, false} contains str then return false
	if str is missing value then return true
	return length of (my trim(str)) is 0
end is_empty

--TESTING!!
on test()
	set as_pwd to POSIX path of (path to me) as string
	set pwd to quoted form of my dirname(as_pwd)
	set cmd to quoted form of "/Users/smargheim/Documents/DEVELOPMENT/GitHub/alfred-bundler/bundler/bundlets/TEST.sh"
	set sh_pwd to do shell script "cd " & pwd & "; bash " & cmd
	return "Applescript PWD: " & as_pwd & return & "Shell PWD: " & sh_pwd
end test

(* GLOBAL PROPERTIES

&& $? for bash status codes

*)
property BUNDLER_VERSION : "devel"
--# Bundler paths
property _home : POSIX path of (path to "cusr" as text)
property BUNDLER_DIR : (_home) & "Library/Application Support/Alfred 2/Workflow Data/alfred.bundler-" & BUNDLER_VERSION
property DATA_DIR : BUNDLER_DIR & "/data"
property CACHE_DIR : (_home) & "Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/alfred.bundler-" & BUNDLER_VERSION
--# Main Applescript Bundler
property AS_BUNDLER : BUNDLER_DIR & "/bundler/AlfredBundler.scpt"
--# Root directory under which workflow-specific applescript libraries are installed
property APPLESCRIPT_DIR : DATA_DIR & "/assets/applescript"
--# Root directory under which Bundler utilities are installed
property UTILITIES_DIR : DATA_DIR & "/assets/utility"
--# Directory for Bundler icons
property ICONS_DIR : DATA_DIR & "/assets/icons"
--# Where color alternatives are cached
property COLOR_CACHE : DATA_DIR & "/color-cache"

(* GLOBAL VARIABLES *)

--# Where installer.sh can be downloaded from
global BASH_BUNDLET_URL
set BASH_BUNDLET_URL to my formatter("https://raw.githubusercontent.com/shawnrice/alfred-bundler/{}/bundler/bundlets/alfred.bundler.sh", BUNDLER_VERSION)

--# Bundler log file
global BUNDLER_LOGFILE
set BUNDLER_LOGFILE to my formatter((DATA_DIR & "/logs/bundler-{}.log"), BUNDLER_VERSION)

(* ///
USER API
/// *)

on utility(_name, _version, _json)
	--# partial support for "optional" args in Applescript
	if my is_empty(_version) then
		set _version to "latest"
	end if
	if my is_empty(_json) then
		set _json to ""
	end if
	--# path to specific utility
	set utility to my joiner({UTILITIES_DIR, _name, _version}, "/")
	if my folder_exists(utility) = false then
		--# if utility isn't already installed
		my logger("utility", "INFO", my formatter("Utility at `{}` not found...", utility))
		set bash_bundlet to (my dirname(AS_BUNDLER)) & "bundlets/alfred.bundler.sh"
		if my file_exists(bash_bundlet) = true then
			set bash_bundlet_cmd to quoted form of bash_bundlet
			set cmd to my joiner({bash_bundlet_cmd, "utility", _name, _version, _json}, space)
			set cmd to my prepare_cmd(cmd)
			my logger("utility", "INFO", my formatter("Running shell command: `{}`...", cmd))
			set full_path to (do shell script cmd)
			return full_path
		else
			set error_msg to my logger("utility", "DEBUG", my formatter("Internal bash bundlet `{}` does not exist.", bash_bundlet))
			error error_msg number 1
			--##TODO: need a stable error number schema
		end if
	else
		--# if utility is already installed
		my logger("utility", "INFO", my formatter("Utility at `{}` found...", utility))
		--# read utilities invoke file
		set invoke_file to utility & "/invoke"
		if my file_exists(invoke_file) = true then
			set invoke_path to my _read_file(invoke_file)
			--# combine utility path with invoke path
			set full_path to utility & "/" & invoke_path
			return full_path
		else
			set error_msg to my logger("utility", "DEBUG", my formatter("Internal invoke file `{}` does not exist.", invoke_file))
			error error_msg number 1
			--##TODO: need a stable error number schema
		end if
	end if
end utility

on icon(_font, _name, _color, _alter)
	--# partial support for "optional" args in Applescript
	if my is_empty(_color) then
		set _color to "000000"
		if my is_empty(_alter) then
			set _alter to true
		end if
	end if
	if my is_empty(_alter) then
		set _alter to false
	end if
	--# path to specific icon
	set icon to my joiner({ICONS_DIR, _font, _color, _name}, "/")
	if my folder_exists(icon) = false then
		--# if icon isn't already installed
		my logger("icon", "INFO", my formatter("Icon at `{}` not found...", icon))
		set bash_bundlet to (my dirname(AS_BUNDLER)) & "bundlets/alfred.bundler.sh"
		if my file_exists(bash_bundlet) = true then
			set bash_bundlet_cmd to quoted form of bash_bundlet
			set cmd to my joiner({bash_bundlet_cmd, "icon", _font, _name, _color, _alter}, space)
			set cmd to my prepare_cmd(cmd)
			my logger("icon", "INFO", my formatter("Running shell command: `{}`...", cmd))
			set full_path to (do shell script cmd)
		else
			set error_msg to my logger("icon", "DEBUG", my formatter("Internal bash bundlet `{}` does not exist.", bash_bundlet))
			error error_msg number 1
			--##TODO: need a stable error number schema
		end if
	else
		--# if icon is already installed
		my logger("icon", "INFO", my formatter("Icon at `{}` found...", icon))
		return icon
	end if
end icon

(* ///
LOGGING HANDLER
/// *)

on logger(_handler, _level, _message)
	--# Ensure log file exists, with
	--# checking for scope of global var
	try
		my check_file(BUNDLER_LOGFILE)
	on error
		set BUNDLER_LOGFILE to my formatter((DATA_DIR & "/logs/bundler-{}.log"), BUNDLER_VERSION)
		my check_file(BUNDLER_LOGFILE)
	end try
	delay 0.1 -- delay to ensure IO action is completed
	--# Prepare time string format %Y-%m-%d %H:%M:%S
	set log_time to "[" & (my date_formatter()) & "]"
	set formatterted_time to text items 1 thru -4 of (time string of (current date)) as string
	set error_time to "[" & formatterted_time & "]"
	--# Prepare location message
	set _location to "[bundler.scpt:" & _handler & "]"
	--# Prepare level message
	set _level to "[" & _level & "]"
	--# Generate full error message for logging
	set log_msg to (my joiner({log_time, _location, _level, _message}, space)) & (ASCII character 10)
	my write_to_file(log_msg, BUNDLER_LOGFILE, true)
	--# Generate regular error message for returning to user
	set error_msg to error_time & space & _location & space & _level & space & _message
	return error_msg
end logger

(* ///
SUB-ACTION HANDLERS
/// *)

on date_formatter()
	set {year:y, month:m, day:d} to current date
	set date_num to (y * 10000 + m * 100 + d) as string
	set formatterted_date to ((text 1 thru 4 of date_num) & "-" & (text 5 thru 6 of date_num) & "-" & (text 7 thru 8 of date_num))
	set formatterted_time to text items 1 thru -4 of (time string of (current date)) as string
	return formatterted_date & space & formatterted_time
end date_formatter

on _read_file(target_file)
	open for access POSIX file target_file
	set _contents to (read target_file)
	close access target_file
	return _contents
end _read_file

on prepare_cmd(cmd)
	set pwd to quoted form of (my pwd())
	return "cd " & pwd & "; bash " & cmd
end prepare_cmd

(* ///
IO HELPER FUNCTIONS
/// *)

on write_to_file(this_data, target_file, append_data)
	try
		set the target_file to the target_file as string
		set the open_target_file to open for access POSIX file target_file with write permission
		if append_data is false then set eof of the open_target_file to 0
		write this_data to the open_target_file starting at eof
		close access the open_target_file
		return true
	on error
		try
			close access file target_file
		end try
		return false
	end try
end write_to_file

--#
on pwd()
	set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "/"}
	set _path to (text items 1 thru -2 of (POSIX path of (path to me)) as string) & "/"
	set AppleScript's text item delimiters to ASTID
	return _path
end pwd

on dirname(_file)
	set {ASTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "/"}
	set _path to (text items 1 thru -2 of _file as string) & "/"
	set AppleScript's text item delimiters to ASTID
	return _path
end dirname

on check_dir(_folder)
	if not my folder_exists(_folder) then
		do shell script "mkdir -p " & (quoted form of _folder)
	end if
	return _folder
end check_dir

on check_file(_path)
	if not my file_exists(_path) then
		set _dir to dirname(_path)
		my check_dir(_dir)
		delay 0.1
		do shell script "touch " & (quoted form of _path)
	end if
end check_file

--#  handler to check if a folder exists
on folder_exists(_folder)
	if my path_exists(_folder) then
		tell application "System Events"
			return (class of (disk item _folder) is folder)
		end tell
	end if
	return false
end folder_exists

--# handler to check if a file exists
on file_exists(_file)
	if my path_exists(_file) then
		tell application "System Events"
			return (class of (disk item _file) is file)
		end tell
	end if
	return false
end file_exists

--#  handler to check if a path exists
on path_exists(_path)
	if _path is missing value or my is_empty(_path) then return false

	try
		if class of _path is alias then return true
		if _path contains ":" then
			alias _path
			return true
		else if _path contains "/" then
			POSIX file _path as alias
			return true
		else
			return false
		end if
	on error msg
		return false
	end try
end path_exists

(* ///
TEXT HELPER FUNCTIONS
/// *)

on split(str, delimiter)
	local delimiter, str, ASTID
	set ASTID to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to delimiter
		set str to text items of str
		set AppleScript's text item delimiters to ASTID
		return str --> list
	on error msg number num
		set AppleScript's text item delimiters to ASTID
		error "Can't explode: " & msg number num
	end try
end split

--# format string á la Python
on formatter(str, arg)
	local ASTID, str, arg, lst
	set ASTID to AppleScript's text item delimiters
	try
		considering case
			set AppleScript's text item delimiters to "{}"
			set lst to every text item of str
			set AppleScript's text item delimiters to arg
			set str to lst as string
		end considering
		set AppleScript's text item delimiters to ASTID
		return str
	on error msg number num
		set AppleScript's text item delimiters to ASTID
		error "Can't replaceString: " & msg number num
	end try
end formatter

--# removes white space surrounding a string
on trim(str)
	if class of str is not text or class of str is not string or str is missing value then return str
	if str is "" then return str
	repeat while str begins with " "
		try
			set str to items 2 thru -1 of str as text
		on error msg
			return ""
		end try
	end repeat
	repeat while str ends with " "
		try
			set str to items 1 thru -2 of str as text
		on error
			return ""
		end try
	end repeat
	return str
end trim

on joiner(pieces, delim)
	local delim, pieces, ASTID
	set ASTID to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to delim
		set pieces to "" & pieces
		set AppleScript's text item delimiters to ASTID
		return pieces --> text
	on error emsg number eNum
		set AppleScript's text item delimiters to ASTID
		error "Can't implode: " & emsg number eNum
	end try
end joiner

(* ///
LOWER LEVEL HELPER FUNCTIONS
/// *)

--# checks if a value is empty
on is_empty(str)
	if {true, false} contains str then return false
	if str is missing value then return true
	return length of (my trim(str)) is 0
end is_empty

--TESTING!!
on test()
	set as_pwd to POSIX path of (path to me) as string
	set pwd to quoted form of my dirname(as_pwd)
	set cmd to quoted form of "/Users/smargheim/Documents/DEVELOPMENT/GitHub/alfred-bundler/bundler/bundlets/TEST.sh"
	set sh_pwd to do shell script "cd " & pwd & "; bash " & cmd
	return "Applescript PWD: " & as_pwd & return & "Shell PWD: " & sh_pwd
end test

change `_read_file` to `read_file`

change `_read_file` to `read_file`