I'm trying to write a simple Xcode cocoa-applescript program that executes a bash script on each subfolder of a folder and shows a progressbar while doing it.
I have a NSProgressIndicator set up to be linked to barStatus and well a NSTextField to be linked to lblStatus to show some informative text:
property barStatus : missing value
property lblStatus : missing value
this is the loop where the main action takes place:
tell application "Finder" to set subfolders to every folder of folder macPath
barStatus's setIndeterminate_(false)
barStatus's setDoubleValue_(0)
barStatus's setMaxValue_(count of subfolders)
repeat with eachFolder in subfolders
set posixPath to (POSIX path of (eachFolder as text)) as text
-- set text of progress indicator text
lblStatus's setStringValue_("Resizing '" & ( do shell script "basename \"" & (posixPath) & "\" | sed \"s/^.*_//\"" ) & "'...")
delay 0.5
-- do the shell script
do shell script "bash ~/.uploader/image.sh \"" & posixPath & "\""
-- step the indicator
barStatus's incrementBy_(1)
end repeat
lblStatus's setStringValue_("Done!")
All seems to work properly, yet the UI is somewhat glitchy. Instead of just increasing smoothly, the progressbar disappears on each step and gets shown for a short while, then dissappears again. The text in lblStatus does get changed smoothly.
Things get totally lost when I remove the delay from the loop: no UI changes are made (even though the scripts get run properly) until the loop is finished. So the progressbar just dissappears and reappears filled out when the loop is done.
Here's a youtube video of the flickery UI.
What am I doing wrong? How can I have xcode draw the progressbar smoothly?
Note that this is the first time I write an Xcode app, and that my knowledge of Applescript is somewhat sketchy.
EDIT:
I found that calling the function that processes the same function does not have a flickery UI when called form a menu item (or with a key combo bound to that menu item).
I'm not sure why the progress bar hides itself. Did you bind the visible property of it to something that may make it hide?
Things get totally lost when I remove the delay from the loop: no UI
changes are made
In general though when "no UI changes are made" it's because the application is too busy on the main thread to update the interface items in real time. All of your code is running on the main thread. You need to make some of it happen on a background thread. As such I have 2 suggestions for you to try.
First try using the progress bar's method "setUsesThreadedAnimation:". Add this just above your repeat loop...
barStatus's setUsesThreadedAnimation_(true)
Second, if that doesn't help then try moving your repeat loop work onto a background thread (using NSThread's detachNewThreadSelector:)... but make the interface updates happen on the main thread. I don't know ApplescriptObjC language so the following code is probably completely wrong. You'll have to write it properly but it will show you the idea...
[NSThread detachNewThreadSelector:#selector(processFolderListOnBacgroundThread_) toTarget:self withObject:subfolders];
-- this will happen on the main thread
on updateProgressBarByOne_()
barStatus's' incrementBy_(1)
end updateProgressBarByOne_
-- this will happen on the main thread
on updateStatusTextWithText_(statusText)
lblStatus's' setStringValue_(statusText)
end updateInterfaceItemsOnMainThreadWithObject_
-- this will happen on a background thread
on processFolderListOnBacgroundThread_(subFolders)
repeat with eachFolder in subfolders
set posixPath to (POSIX path of (eachFolder as text)) as text
-- set text of progress indicator text
my updateStatusTextWithText_("Resizing '" & ( do shell script "basename \"" & (posixPath) & "\" | sed \"s/^.*_//\"" ) & "'...")
-- do the shell script
do shell script "bash ~/.uploader/image.sh \"" & posixPath & "\""
-- step the indicator
my updateProgressBarByOne_()
end repeat
my updateStatusTextWithText_("Done!")
end processFolderListOnBacgroundThread_
If you use the background thread approach you'll probably have to make the buttons in your interface inactive while the background thread is working (so the user can't press them and start a new task). So just set their enabled property to false prior to calling "detachNewThreadSelector:" and enable them again after the work is finished. You can do this by having another handler which enables them and call that from the end of the background thread code.
Related
I have a massive spreadsheet with a titanic number of rows/columns (e.g. ~250 columns, many thousands of rows) that I'm trying to convert into PDFs by looping through each row with AppleScript, copying that row's ~250 variables to TextEdit set to Rich Text (for bold formatting etc), and then using System Events to save the txt as a PDF. Here's a summary of the code:
on run
set initialRow to 1
tell application "System Events"
tell application process "TextEdit"
set frontmost to true
end tell
end tell
repeat
tell application "Microsoft Excel"
-- CLEAR MY ~250 VARIABLES FROM PREVIOUS ROW'S VALUES TO MAKE SURE NOTHING IS CARRIED OVER BY MISTAKE
-- THEN SET MY ~250 VARIABLES TO THE NEXT ROW'S VALUES
if exampleValue is "" then exit repeat
end tell
tell application "TextEdit"
set the text of the front document to ""
-- THEN SET FIRST PARAGRAPH TO MY FIRST VARIABLE PLUS A LINE BREAK SO THEN THERE'S A NEW PARAGRAPH FOR THE NEXT VARIABLE, ETC
-- THEN GO THROUGH ALL OF MY VARIABLES TO IMPORT THE IMPORTANT ONES INTO TEXTEDIT, SET SOME FORMATTING, ETC.
end tell
delay 1
tell application "System Events"
click menu item "Export as PDF…" of menu 1 of menu bar item "File" of menu bar 1 of application process "TextEdit"
delay 1
keystroke exampleValue -- SYSTEM EVENTS TYPES THE NAME OF THE PDF
delay 1
key code 36
delay 1
end tell
set myRow to (myRow + 1)
end repeat
end run
This all runs great, no bugs (seemingly!), no issues at all in small doses. The problem, however, is that something happens as the script runs where it seems to be tying up more and more memory somewhere; everything is fine for the first hundred or so rows, but at some point my Mac stops running anything at all, i.e. whether I let the script run until it starts producing super random errors (I could collect them if helpful, but it's like a random different error each time so not much help there) or even if I let the script run for a while and then stop it before it errors out - it will let me stop the script but then I can't actually quit out of Script Editor or TextEdit or Excel, my keyboard stops working, I can't Force Quit anything, can't Reset the computer, etc. It's just a complete meltdown of the machine unlike anything I've encountered, and the only way to get back to work is to force a hard boot with the power button.
I've never had this problem with my other scripts, but I also don't usually use System Events, so my hunch is that it's something to do with that. Do I need to be 'resetting' System Events somehow, or clearing out the memory for some reason, or...? Thanks for the help!!
Figured it out! After trying the script one more time with Activity Monitor running, I discovered that each time it iterates through, 3 new processes were popping up - Core Sync, Dropbox Finder Extension, and SnailSVNLite - and then never going away! So if I ran through the script 500 times, I'd end up with 1500 new processes running, which was almost certainly what was wrecking me though I have no idea why telling System Events anything was doing that. I looked around online, and it turns out those are all Finder Extensions that had been turned on at some point long ago, so just needed to go to System Preferences > Extensions > Added Extensions and then uncheck those 3 extensions - and then problem solved!!
I know this question is kinda messy. I don't know how to make it more specific. I want to make an applescript that asks me every hour what I've been doing for the past hour.
The issue I'm having is that I want it to pop up, make a sound, and wait for my response. If I do a normal dialog, and I'm busy, it will go behind the other windows on my mac. I thought maybe having a persistent banner notification would be great, but applescript doesn't allow for much control over banners.
I want something to float over all windows so that I can see that the applescript has been waiting for a response from me until I fill out the dialog.
What you're looking for is a global floating window, similar to what LittleSnitch does. You can do that, but not with pure AppleScript. You'd have to write a Cocoa app.
Adding an activate command just before a display dialog command in your AppleScripts, will ensure the pop-up dialog window will become frontmost and visible no matter what other apps and documents are currently opened (until another app or document gets activated, thus bringing that item to the front).
This following AppleScript may be of some use to you.
Save this following AppleScript code as a "stay open application" in Script Editor.app.
When running your new AppleScript applet, it will remain running (because it has an idle handler) until you press "Cancel" in any of the dialog pop-ups or choosing to quit the app in the Dock.
This code also logs everything it receives in the dialog pop-ups, to file for you.
property myComputerActivitiesLog : (path to desktop as text) & "My_Computer_Activities.log"
property theDialog : missing value
property theDate : missing value
property insertTime : missing value
property logContent : missing value
on idle
set theDate to (current date)
set insertTime to " ------ " & theDate & " ------ "
beep 5
say "It's time to log your activities"
activate
try
set theDialog to display dialog ¬
"Itemize my activities." default answer ¬
"Itemize my activities for yhe past hour." & linefeed & linefeed ¬
buttons {"Cancel", "OK"} default button 2 cancel button 1 ¬
with title "Account For My Activities" with icon 1
end try
if theDialog = missing value then
quit me
else
set logContent to insertTime & linefeed & (text returned of theDialog) & linefeed
do shell script "echo " & quoted form of logContent & ¬
" >> " & quoted form of POSIX path of myComputerActivitiesLog
set theDialog to missing value
return 3600 -- in seconds
end if
end idle
on quit -- Executed when the script quits
-- Additional code to perform (if any) goes here
continue quit -- allows the script to quit
end quit
How do I copy the result of the Calculator.app including decimals.
By defaults is selected, so if you just do a CMD+C it copies it into your clipboard. I got this code from #jweaks
set the clipboard to {?????}
I tried entering many different options but I don't know what I should put.
How do I copy the result of the Calculator.app including decimals.
set the clipboard to {?????}
As you already know ⌘C can do it, however, if you want to use a set clipboard to method, then here is one way to go about it:
Example AppleScript code:
if not running of application "Calculator" then return
tell application "System Events" to ¬
set the clipboard to ¬
(get the value of ¬
static text 1 of ¬
group 1 of ¬
window 1 of ¬
process "Calculator")
Notes:
Does not require Calculator to be frontmost.
Does not require the use of keystroke or key code to accomplish the task.
Can set the value to a variable instead of the clipboard, if wanting to process it in a different manner.
The example AppleScript code, shown below, was tested in Script Editor under macOS Catalina and macOS Monterey with Language & Region settings in System Preferences set to English (US) — Primary and worked for me without issue1.
1 Assumes necessary and appropriate settings in System Preferences > Security & Privacy > Privacy have been set/addressed as needed.
In testing, the Replies pane in Script Editor returned, e.g.,:
tell application "System Events"
get value of static text 1 of group 1 of window 1 of process "Calculator"
--> "6200.549407114624506"
set the clipboard to "6200.549407114624506"
end tell
Then when pasting into a document it pasted as: 6200.549407114624506
Update to address comments
To address the ensuing comments by sebseb under my answer and specifically…
Is it possible to run the script every time I hit enter on Calculator? then copy the result.
Basic vanilla AppleScript is not that intelligent and does not have the ability in of and by itself to understand what one is doing in Calculator and know when one has pressed the enter key to then place the result on the clipboard.
One would have to use an intermediary, an application like Hammerspoon, where it can wait for the Calculator application being activated/deactivated or its window being focused/unfocused to then enabled/disable trapping the enter key being pressed on the keyboard to then run the script to perform an action to calculate the result by pressing the = key then copy the result to the clipboard.
This works because pressing the = key in Calculator is equivalent to pressing the enter key, thus enabling trapping the enter key to perform the necessary actions using AppleScript. It quite possibly can be done without using AppleScript and just Lua, the language used by Hammerspoon and its API. However, since I already use various AppleScript scripts in conjunction with Hammerspoon and can easily recycle some existing code I'll present an addendum to the original answer using both methods in Hammerspoon.
The following example Lua code and API of Hammerspoon is placed in the ~/.hammerspoon/init.lua file.:
-- Create a hotkey used to trap the enter key and disable it.
-- It will then be enabled/disabled as Calculator is focused/unfocused
-- When enabled and the enter key is pressed it runs the AppleScript script.
local applicationCalculatorEnterHotkey = hs.hotkey.bind({}, "return", function()
local asFile = "/.hammerspoon/Scripts/CalculatorResultToClipboard.applescript"
local ok, status = hs.osascript.applescriptFromFile(os.getenv("HOME") .. asFile)
if not ok then
msg = "An error occurred running the CalculatorResultToClipboard script."
hs.notify.new({title="Hammerspoon", informativeText=msg}):send()
end
end)
applicationCalculatorEnterHotkey:disable()
-- One of two methods of watching Calculator.
--
-- The other is below this one and commented out.
-- Initialize a Calculator window filter.
local CalculatorWindowFilter = hs.window.filter.new("Calculator")
-- Subscribe to when the Calculator window is focused/unfocused.
CalculatorWindowFilter:subscribe(hs.window.filter.windowFocused, function()
-- Enable hotkey when Calculator is focused.
applicationCalculatorEnterHotkey:enable()
end)
CalculatorWindowFilter:subscribe(hs.window.filter.windowUnfocused, function()
-- Disable hotkey when Calculator is unfocused.
applicationCalculatorEnterHotkey:disable()
end)
-- Alternate method to wait for Calculator and enable/disable the hotkey.
--
-- Uncomment below method and comment the above method to test between them. Adding the
-- multiple line opening '--[[' and closing '--]]' to above method and removed from below,
-- leaving 'local CalculatorWindowFilter = hs.window.filter.new("Calculator")' uncommented.
--[[
function applicationCalculatorWatcher(appName, eventType, appObject)
if (eventType == hs.application.watcher.activated) then
if (appName == "Calculator") then
-- Enable hotkey when Calculator is activated.
applicationCalculatorEnterHotkey:enable()
end
end
if (eventType == hs.application.watcher.deactivated) then
if (appName == "Calculator") then
-- Disable hotkey when Calculator is deactivated.
applicationCalculatorEnterHotkey:disable()
end
end
end
appCalculatorWatcher = hs.application.watcher.new(applicationCalculatorWatcher)
appCalculatorWatcher:start()
-- appCalculatorwWatcher:stop()
--]]
The following example AppleScript code is used in conjunction with Hammerspoon and is saved as CalculatorResultToClipboard.applescript in ~/.hammerspoon/Scripts/, and you'll need to create the hierarchical folder structure.
Example AppleScript code:
One can use either:
tell application "Calculator" to activate
tell application "System Events"
key code 24
delay 0.5
set the theResult to the value of static text 1 of group 1 of window 1 of process "Calculator"
end tell
set the clipboard to theResult
Or:
tell application "Calculator" to activate
tell application "System Events"
key code 24
delay 0.5
key code 8 using command down
end tell
To accomplish the task.
An alternate option, as previously mentioned, is to forgo the use of AppleScript and use the following example Lua code:
local applicationCalculatorEnterHotkey = hs.hotkey.bind({}, "return", function()
-- Press the '=' key to finish the calculation.
hs.eventtap.keyStroke({}, "=")
-- Copy the result to the clipboard.
hs.eventtap.keyStroke({"cmd"}, "C")
end)
applicationCalculatorEnterHotkey:disable()
This function would be used instead of the same function further above. It replaces the execution of the AppleScript script with keystrokes generated by Hammerspoon to accomplish the same tasks, while using the remaining example Lua code and the API of Hammerspoon already presented.
Notes:
With the example Lua code, as coded, the behavior of pressing the enter key is only trapped and modified to trigger the example AppleScript code, or if using the alternate option send Hammerspoon keystrokes, while Calculator has focus. The enter key should work normally in all other applications.
See my other Hammerspoon related answers for instructions to install it and utilize the information contained herein.
One in particle is:
A: How can I make preview stop wrapping around when paging?
If using Script Editor, the example AppleScript code is saved as Text in the File Format: pop-up menu in the Save dialog box.
The example Lua code and API of Hammerspoon and AppleScript code, shown directly above, were tested respectively with Hammerspoon and Script Editor under macOS Mojave and macOS Catalina with Language & Region settings in System Preferences set to English (US) — Primary and worked for me without issue1.
1 Assumes necessary and appropriate settings in System Preferences > Security & Privacy > Privacy have been set/addressed as needed.
Calculator.app doesn't have an AppleScript dictionary.
You have to script the UI with System Events
tell application "System Events"
tell process "Calculator"
set frontmost to true
keystroke "c" using command down
end tell
end tell
As you can see from my code below I am extremely new to this. My code just about works, but my major issue is that it hogs up Finder and sometimes it does not set the Desktop picture, but does most of the time!
The script just monitors a folder, and if an "***.jpg" is added then the Desktop picture set to it.
This is my very first script so I have a lot to learn,
set reset to ""
display notification "Alarm Front Active " & (current date) as string
tell application "Finder"
set path_to_sourceFull to ":photo:FRONT CAM 1:20190929:images" -- from nsa310 network drive
set path_to_source to ":photo:FRONT CAM 1:20190929:images" -- from nsa310 network drive
set directory1 to "/Volumes/photo/FRONT CAM 1/20190929/images" as text -- from nsa310 network drive
set path_to_destinationFull to "Macintosh HD:Users:rekordbox:Documents:temp folder 2"
set path_to_destination to ":Users:rekordbox:Documents:temp folder 2"
set directory2 to "/Users/rekordbox/Documents/temp folder 2" as text
repeat while reset = ""
set allok to ""
set filelist to name of every item in folder path_to_source --of startup disk
set listSizesaved to count of filelist
delay 1
repeat while allok = ""
set filelist to name of every item in folder path_to_source --of startup disk
set listSize to count of filelist
if listSize = listSizesaved then
else
set filelist to name of every item in folder path_to_source --of startup disk
set listSize to count of filelist
set LastAddedFile to item listSize of filelist
set allok to "ALARM"
set listSizesaved to listSize -- (save the updated) count
set activefile to (path_to_source & LastAddedFile)
set selectedpicture to (directory1 & "/" & LastAddedFile)
tell application "System Events" to tell every desktop to set picture to selectedpicture
delay 1
display notification "ALARM FRONT TRIGGERED...." & (current date) as string
delay 1
end if
end repeat
end repeat
end tell
The script you want, I think, is this:
on adding folder items to thisFolder after receiving filelist
set droppedFile to first item of filelist
tell application "System Events"
tell every desktop
set picture to droppedFile
end tell
end tell
end adding folder items to
(I've left out the 'Alarm' bit, since I wasn't sure what the point of it was.)
To use this script, copy it into Script Editor, save it in the folder ~/Library/Scripts/Folder Action Scripts/, then open the applet 'Folder Actions Setup'. Add the folder you want on the left-hand side, and choose the file you just saved on the right. It should look something like this:
...where the checkmark on the left shows that folder actions are enabled for the folder (which I called 'test folder') and the script (which I called 'FADtop.scpt') is attached.
Drop an image in the folder, and it should just work.
As a general rule, don't script the Finder unless you absolutely need to; always use System Events. The Finder is a busy app, and scripting it can gum up the system. And also try to avoid this design pattern:
(* Don't do this! *)
repeat
(* test for something *)
delay x
end
The delay command is not particularly resource-efficient. If you really want to use a polling system to test for some event, it's often better to create a stand-alone app with an on idle handler. That way you let the system wake and sleep the script, with significant performance improvements.
EDIT
Since folder actions don't seem to be working with ftp drops onto remote drives, here's a reasonably efficient folder-polling approach. Save the following script as a stay-open application (choose 'Application' as the file type, and click the 'stay open' checkbox). Then launch the application and leave it running in the background.
property dateOfLastFileChosen : missing value
property targetFolder : "/Volumes/photo/FRONT CAM 1/20190929/images"
property idleTime : 300 -- 300 seconds is five minutes
on run
end run
on idle
tell application "System Events"
if exists folder targetFolder then
if dateOfLastFileChosen is missing value then
set recentFiles to every file of folder targetFolder whose visible is true
else
set recentFiles to every file of folder targetFolder whose modification date > dateOfLastFileChosen and visible is true
end if
set newFile to my mostRecentFileOfList(recentFiles)
if newFile is not missing value then
set dateOfLastFileChosen to modification date of newFile
tell every desktop
set picture to (POSIX path of newFile)
end tell
end if
end if
end tell
return idleTime -- check every 5 minutes (300 seconds)
end idle
on mostRecentFileOfList(fileList)
set maxDateObj to missing value
repeat with thisFile in fileList
if maxDateObj is missing value then
set maxDateObj to contents of thisFile
else if modification date of thisFile is greater than modification date of maxDateObj then
set maxDateObj to thisFile
end if
end repeat
return maxDateObj
end mostRecentFileOfList
Without trying to steal the thunder from #Ted Wrigley, whose solution provided the AppleScript code for the folder action, I felt there were enough comments and items for me to add to post it as another answer to the OP's dilemma.
First I will address the tell every desktop set picture to droppedFile lines of code in the following AppleScript Folder Action. If the user has only one monitor/display attached to the computer, but has created several different "Spaces", the tell every desktop set picture to droppedFile lines of code will only change the Desktop Picture for the Desktop of the current active "Space" only. The other Desktop backgrounds will not be changed. However, if the user has several monitors/displays attached to the computer, the tell every desktop set picture to droppedFile lines of code will change the Desktop Pictures for the Desktops of the current active "Space" for each attached monitor/display. If the latter is not the desired result, then tell every desktop should be changed to tell current desktop.
After testing the AppleScript Folder Action code provided by #Ted Wrigley, I noticed the image file being downloaded from an FTP server, to the test folder where I have the folder action script attached to, looked like this before the image was actually finished transferring. Because the file was kind of there and not there, it did not trigger the Folder Action.
Next, I figured I would add a delay to be beginning of the Folder Action code to allow for the transfer of the image file from the FTP server, to complete. I added a delay of 180 seconds to allow for the transfer to complete and it worked. When the transfer was complete, the file look like this.
Depending on how many files you foresee being transferred at any given time along with factoring in for file sizes... It's possible you may need to significantly increase the Delay time.
on adding folder items to thisFolder after receiving theseFiles
delay 180
set newBackground to first item of theseFiles
tell application "System Events"
set picture of current desktop to newBackground -- Single Display Attached
--set picture of every desktop to newBackground -- Multiple Displays Attached
end tell
end adding folder items to
I would like to click on the Wi-Fi icon with the option key down to reveal extra options available on Mac. How can I automate it using AppleScript?
I tried using key down option and click menu item but no luck in revealing special options.
Is there any way I can achieve this?
It's currently not possible to click with a key held down using AppleScript. Key down actions only apply to other key press actions, since the AppleScript click action doesn't actually perform a ‘click’, but rather directly actions the element.
If you don't mind using a 3rd party utility, here's an example AppleScript script that uses cliclick:
tell application "System Events"
tell application process "SystemUIServer"
set theWiFiProperties to item 1 of (get properties of every menu bar item of menu bar 1 whose description starts with "Wi-Fi")
end tell
set theXpos to (item 1 of position in theWiFiProperties) + ((item 1 of size in theWiFiProperties) / 2) as integer
set theYpos to (item 2 of position in theWiFiProperties) + ((item 2 of size in theWiFiProperties) / 2) as integer
end tell
tell current application
do shell script "/path/to/cliclick kd:alt c:" & theXpos & "," & theYpos & " ku:alt"
end tell
Note: Change /path/to/cliclick to the actual pathname of the cliclick executable.
How it works:
The theWiFiProperties variable gets set to the properties of the Wi-Fi menu extra and then the variables theXpos and theYpos get set to a position that together is the center of the Wi-Fi menu extra on the menu bar.
This info is then used in a do shell script command using cliclick to press the option key down, click at the designated x,y coordinates and let the option key up.
You can use Automator and record the process using “Watch me do” and then save the automated workflow as an application or a dictation command.
In Automator, I saved the watch me do action as an application. I named this new application “Extended_Wifi.app”. Then I had to add this application in system preferences to be able to control my computer.
Personally, I prefer to use Scripteditor rather than Automator because a huge part of me feels like using Automator is cheating. But at the end of the day, I was able to save the Automator action as an application and it functions perfectly however in Scripteditor, I Could not get the AppleScript version of the action to function correctly.
Here is a quick .gif showing the Automator application working correctly.