write text on menu bar

Hello Mark,

Thank you for provide the example. I tried you example in script editor and it works perfectly. as I understand it should work if I export it to as app as you use code below to see it’s mainThread or not. however it looks like the app just briefly flash in the menu bar, then disappeared (ended, I guess). Could you help more?

Thanks

if my NSThread's isMainThread() as boolean then
   my displayStatusItem:statusItemTitle
else
   my performSelectorOnMainThread:"displayStatusItem:" withObject:statusItemTitle waitUntilDone:true
end if

I also tried to remove the if and export it to as app and it’s the same.

use framework "Foundation"
use framework "AppKit"
use scripting additions

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value

set statusItemTitle to "Status Bar Title"

set my statusBar to myApp's NSStatusBar's systemStatusBar()

my displayStatusItem:statusItemTitle

on displayStatusItem:title
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	
	set statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setTarget:me
	
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end displayStatusItem:

on quitStatusItem()
	my removeStatusItem()
end quitStatusItem

on removeStatusItem()
	set removeStatusItem to button returned of (display alert "Do you want to remove the status bar item ?" buttons {"NO", "YES"})
	if removeStatusItem = "YES" then
		statusBar's removeStatusItem:statusItem
	end if
end removeStatusItem

@aerolite

Firstly you are correct with your last script posting, in that you can remove the main thread check and method calling, once your script is saved as an application.
But do remember that you will need that main thread code when testing in the Script Editor.

Secondly the reason that the status item only flashes up quickly, is that the code runs once from top to bottom, and then exits the script, so you need to save it as a stay open application.

So copy and paste your last script into a new script file, then change the bottom of the script to this example below, you will see that I have added the “tell me to quit” to the “on quitStatusItem()” function.
And also that I have added the “on Quit{}” function as shown below.


on quitStatusItem()
	set quitStatusItem to button returned of (display alert "Do you want to quit the status bar item ?" buttons {"NO", "YES"})
	if quitStatusItem = "YES" then
		my removeStatusItem()
		tell me to quit
	end if
end quitStatusItem


on removeStatusItem()
	statusBar's removeStatusItem:statusItem
end removeStatusItem

-- And Add This Function

on quit {}
	continue quit
end quit

Then once you have made these changes resave the file from the “File” → “Save” menu.
And in the save dialog box, select the “Application” type from the drop down box.
And then select the “Stay open after run handler” check box.

with the above changes you should have a stay open status item application.

Regards Mark

Thank you so much @Mark FX for your detailed explanation and now I have an apple scripts app running to show CPU temperature in the menubar using the script below.

now I want to improve it. you see that I have to use “statusBar’s removeStatusItem:statusItem” to remove it in the loop before I update the text or it will be multiple text in the menu bar. Is there a way to update the text without remove it? remove and re-write cause the text jump around when I have multiple similar apple scripts app running.

thx


use framework "Foundation"
use framework "AppKit"
use scripting additions

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value

set statusItemTitle to "Hello World!"

set theREPEAT to 1
repeat while true
	
	--repeat 5 times
	set batt_level to do shell script "/Volumes/data/bin/batt_level.cmd"
	set batt_temp to do shell script "/Volumes/data/bin/batt_temp.cmd"
	set batt_volt to do shell script "/Volumes/data/bin/batt_volt.cmd"
	set cpu_temp to do shell script "/Volumes/data/bin/cpu_temp.cmd"
	set cpu_fans to do shell script "/Volumes/data/bin/cpu_fans.cmd"
	
	-- display alert "CPU" & (cpu_temp as string) & "°C"
	-- display alert cpu_fans
	if cpu_fans = "0" then
		set statusItemTitle to ("CPU " & (cpu_temp as string) & "°C")
	else
		set statusItemTitle to (("CPU " & (cpu_temp as string) & "°C FAN" & cpu_fans as string) & "R/M")
	end if
	
	
	set my statusBar to myApp's NSStatusBar's systemStatusBar()
	
	if my NSThread's isMainThread() as boolean then
		my displayStatusItem:statusItemTitle
	else
		my performSelectorOnMainThread:"displayStatusItem:" withObject:statusItemTitle waitUntilDone:true
	end if
	
	delay 3
	statusBar's removeStatusItem:statusItem
end repeat

on displayStatusItem:title
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	
	set statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setTarget:me
	
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end displayStatusItem:

on quitStatusItem()
	set quitStatusItem to button returned of (display alert "Do you want to quit the status bar item ?" buttons {"NO", "YES"})
	if quitStatusItem = "YES" then
		my removeStatusItem()
		tell me to quit
	end if
end quitStatusItem


on removeStatusItem()
	statusBar's removeStatusItem:statusItem
end removeStatusItem

-- And Add This Function

on quit {}
	continue quit
end quit

@aerolite

Your CPU sent me an email, which said HELP!

So on behalf of your CPU, could you please stop with that crazy repeat loop.
That loop is running 5 shell scripts, and redisplaying the status bar item, and also rebuilding the status item menu, over and over forever without a break.
And it was all going so well up to this point.

Firstly you do not need to remove and redisplay the status bar item, and also rebuild the status item menu repeatedly, you only needed to do it once.
your CPU usage must be off the charts.

The first half of the script was done well in your previous posting for the moment at least.


use framework "Foundation"
use framework "AppKit"
use scripting additions

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value

set statusItemTitle to "Status Bar Title"

set my statusBar to myApp's NSStatusBar's systemStatusBar()

if my NSThread's isMainThread() as boolean then
	my displayStatusItem:statusItemTitle
else
	my performSelectorOnMainThread:"displayStatusItem:" withObject:statusItemTitle waitUntilDone:true
end if

Done!

The bottom half of the script was also fine, and does not need any changes for the moment.


on displayStatusItem:title
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	
	set statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setTarget:me
	
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end displayStatusItem:

on quitStatusItem()
	set quitStatusItem to button returned of (display alert "Do you want to quit the status bar item ?" buttons {"NO", "YES"})
	if quitStatusItem = "YES" then
		my performSelectorOnMainThread:"removeStatusItem" withObject:(missing value) waitUntilDone:true
		tell me to quit
	end if
end quitStatusItem

on removeStatusItem()
	statusBar's removeStatusItem:statusItem
end removeStatusItem

on quit {}
	continue quit
end quit

Done!

The only part that you now need to focus on is the middle of the script, changing the status item’s button’s title, and you don’t have to continually remove or put back the status item to do that.
So we only have to post the code for the middle part of the script at this point.

To change the status item’s button’s title we only have to use this code.


statusItem's button's setTitle:newTitle

So a new function or method, could be called from within your repeat loop, a function something like this.


repeat
	set statusItemTitle to (time string of (current date))
	my changeStatusItemTitle:statusItemTitle
	delay (1.0)
end repeat

on changeStatusItemTitle:title
	statusItem's button's setTitle:title
end changeStatusItemTitle:

I’m only guessing at this point, but are the five shell script calls to command line tools on an external drive ?
or something else ?

Please explain to me and others a bit more about these do shell script calls.
Can they be done one at a time, instead of all at once ?
Because you need to make your little status bar application, less of a system resources hog.

Regards Mark

Thank you so much @Mark FX. You are so nice and helpful. I modified the script and it works perfectly. for this app it only needs two of the shell scripts, the others are for separate apps. My shell scripts are very simple one line scripts using the build-in macOS command on other partition in my Mac.

I wonder if it’s possible to show a little icon on the menu bar. now my apps shows “CPU 52.13°C”. it will be better if I can change the text “CPU” to a little icon. I have the icns file ready.

Thanks again.

cat cpu_fans.cmd
#!/bin/zsh
sudo powermetrics --samplers smc -i1 -n1 | grep Fan | awk ‘{print $2}’

cat cpu_temp.cmd
#!/bin/zsh
sudo powermetrics --samplers smc -i1 -n1 | grep temp | awk ‘{print $4}’


use framework "Foundation"
use framework "AppKit"
use scripting additions

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value

set statusItemTitle to "Hello World!"

set theREPEAT to 1

--repeat 5 times
set batt_level to do shell script "/Volumes/data/bin/batt_level.cmd"
set batt_temp to do shell script "/Volumes/data/bin/batt_temp.cmd"
set batt_volt to do shell script "/Volumes/data/bin/batt_volt.cmd"
set cpu_temp to do shell script "/Volumes/data/bin/cpu_temp.cmd"
set cpu_fans to do shell script "/Volumes/data/bin/cpu_fans.cmd"

-- display alert "CPU" & (cpu_temp as string) & "°C"
-- display alert cpu_fans
if cpu_fans = "0" then
	set statusItemTitle to ("CPU " & (cpu_temp as string) & "°C")
else
	set statusItemTitle to (("CPU " & (cpu_temp as string) & "°C FAN" & cpu_fans as string) & "RPM")
end if


set my statusBar to myApp's NSStatusBar's systemStatusBar()

if my NSThread's isMainThread() as boolean then
	my displayStatusItem:statusItemTitle
else
	my performSelectorOnMainThread:"displayStatusItem:" withObject:statusItemTitle waitUntilDone:true
end if

delay 3

repeat
	set cpu_temp to do shell script "/Volumes/data/bin/cpu_temp.cmd"
	set cpu_fans to do shell script "/Volumes/data/bin/cpu_fans.cmd"
	if cpu_fans = "0" then
		set statusItemTitle to ("CPU " & (cpu_temp as string) & "°C")
	else
		set statusItemTitle to (("CPU " & (cpu_temp as string) & "°C FAN" & cpu_fans as string) & "RPM")
	end if
	-- set statusItemTitle to (time string of (current date))
	my changeStatusItemTitle:statusItemTitle
	delay (1.0)
end repeat

on changeStatusItemTitle:title
	statusItem's button's setTitle:title
end changeStatusItemTitle:

on displayStatusItem:title
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	
	set statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setTarget:me
	
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end displayStatusItem:

on quitStatusItem()
	set quitStatusItem to button returned of (display alert "Do you want to quit the status bar item ?" buttons {"NO", "YES"})
	if quitStatusItem = "YES" then
		my removeStatusItem()
		tell me to quit
	end if
end quitStatusItem


on removeStatusItem()
	statusBar's removeStatusItem:statusItem
end removeStatusItem

-- And Add This Function

on quit {}
	continue quit
end quit

That’s looking a bit more efficient, but I think you should increase the delay in the repeat loop to say 5.0 or 10.0, because you don’t really need to check the CPU temperature every second, and your app is still a bit greedy with the CPU usage, but that’s your choice.


delay(5.0)

Your .icns file is no good for the status item’s image, but you could use it for your saved AppleScript application’s icon.
Find your saved application file in a Finder window, then right click or cmd click the application file.
Select “Show Package Contents” from the pull down menu, then double click the “Contents” folder.
Then double click the “Resources” folder, then inside the that folder you should see the “applet.icns” file, so you can simply rename your custom .icns file with that same name, and drag it in the the folder to replace the original icon file.

As for displaying an image next to the status item’s title, you have three options, from easy to more difficult, and the more difficult options may require help from someone with “Monetary” OS, because the code I could post for the more difficult options, will work fine on my “Mojave”, but will probably not work on “Monterey”.

The first and easiest option is to use an emoji or symbol.
Go to the “Edit” menu in the Script Editor app, down at the bottom you will see the emoji menu item.
have a browse through the installed emoji’s and pictographs, and see if there is a picture you like.
Then you simply do this in your “changeStatusItemTitle:” function, like this.


on changeStatusItemTitle:title
	statusItem's button's setTitle:("⚙️" & title)
end changeStatusItemTitle:

The second option is to use one of the installed system images, which may be different on your OS, to the ones on my system, but you can find them here.
https://developer.apple.com/documentation/appkit/nsimagename?language=objc

you would change the top of your script to something like this.


use framework "Foundation"
use framework "AppKit"
use scripting additions

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value
property statusItemImage : missing value

set statusItemTitle to "Status Bar Title"

set my statusBar to myApp's NSStatusBar's systemStatusBar()

set my statusItemImage to myApp's NSImage's imageNamed:(myApp's NSImageNameAdvanced)
set statusImageSize to (statusBar's thickness()) - 4
statusItemImage's setSize:(myApp's NSMakeSize(statusImageSize, statusImageSize))

And you would change the top of the “displayStatusItem:” function to something like this.


on displayStatusItem:title
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	statusItem's button's setImage:statusItemImage
	statusItem's button's setImagePosition:(myApp's NSImageLeft)


So have a testing session with these first two options, and see if any of the ready made system pictures are what your looking for.
Otherwise create a png image file that you want to use, and I will show you how to load it into your application, although that might be the point where the OS differences might cause problems.
also it will be difficult to test in the Script Editor app.

if you do create your own image file, make it very small, as the height of the Status bar on my system is only 22 pixels in height, and yours may be different, but it will still be small in height.

You can get the height of your status bar with this code.


log (statusBar's thickness())

So your created image file should be 2 to 4 pixels less than you status bar height.

Or if you intend giving your app to others, who may have different screen sizes and resolutions.
Then make your image about 24 pixels in height, and it will be resized with the code I posted above.

Regards Mark

Thank you again @Mark FX. I tried both options.

the first option works perfect, but the second option doesn’t. I think it works once as I saw the gear like image appears on one of the run. I tried to run it both in script Editor and app. My status bar is also 22.

I saw the changeStatusItemTitle is called in the repeat. who called displayStatusItem, which add the image to the title.

I also tried to move the two lines below to changeStatusItemTitle and it works. should I?
statusItem’s button’s setImage:statusItemImage
statusItem’s button’s setImagePosition:(myApp’s NSImageLeft)

By the way I did change the app icon by icns file successfully and have png file ready.

thx

Here is my code.


use framework "Foundation"
use framework "AppKit"
use scripting additions

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value
property statusItemImage : missing value

set statusItemTitle to "Hello World! ⛵️"


set my statusBar to myApp's NSStatusBar's systemStatusBar()

if my NSThread's isMainThread() as boolean then
	my displayStatusItem:statusItemTitle
else
	my performSelectorOnMainThread:"displayStatusItem:" withObject:statusItemTitle waitUntilDone:true
end if


set my statusItemImage to myApp's NSImage's imageNamed:(myApp's NSImageNameAdvanced)
set statusImageSize to (statusBar's thickness()) - 4
statusItemImage's setSize:(myApp's NSMakeSize(statusImageSize, statusImageSize))

my changeStatusItemTitle:statusItemTitle
delay 3

-- display alert (statusBar's thickness())


repeat
	set statusItemTitle to (time string of (current date))
	my changeStatusItemTitle:statusItemTitle
	delay (1.0)
end repeat

on changeStatusItemTitle:title
	statusItem's button's setTitle:(title)
end changeStatusItemTitle:

on displayStatusItem:title
	set my statusItem to statusBar's statusItemWithLength:(myApp's NSVariableStatusItemLength)
	statusItem's button's setTitle:title
	statusItem's button's setImage:statusItemImage
	statusItem's button's setImagePosition:(myApp's NSImageLeft)
	
	set statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setTarget:me
	
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end displayStatusItem:

on quitStatusItem()
	set quitStatusItem to button returned of (display alert "Do you want to quit the status bar item ?" buttons {"NO", "YES"})
	if quitStatusItem = "YES" then
		my removeStatusItem()
		tell me to quit
	end if
end quitStatusItem


on removeStatusItem()
	statusBar's removeStatusItem:statusItem
end removeStatusItem

-- And Add This Function

on quit {}
	continue quit
end quit

Hello Mark,

I was looking at the link you provided “https://developer.apple.com/documentation/appkit/nsimagename?language=objc” and found the symbol image SF Symbols (https://developer.apple.com/design/resources/). Is it easy than load a image?

thx

I’m not sure what your saying now, are you saying the second example is now working ?

Because if it’s working here on my “Mojave”, but not on your “Monterey”, then there would be no point in me showing the third option, as it works the same way as the previous example.
Except for the way it loads the image file.

Mark

Hello Mark,

It works once I changed to this.

thx for the help.

on changeStatusItemTitle:title
	statusItem's button's setTitle:(title)
	statusItem's button's setImage:statusItemImage
	statusItem's button's setImagePosition:(myApp's NSImageLeft)
end changeStatusItemTitle:

That sucks that you have to continually update the image.
I only had to load the image once at the top of the script, and the image stayed put.
Which is a better way to do things, so clearly “Monterey” changed something there.

So are you all good now, or did you want to load your custom image file ?

Mark

Hi Mark, like I said it did load the image once without modification, but that’s ok if we have to keep update it.

I do want to load the image if you have time to help me.

Much appreciate.

OK the last piece of the jigsaw puzzle is to load your image file into the “statusItemImage” variable.

The first thing you have to do is open up your application bundle’s folder structure again, and go to the “Resources” folder, where you updated your application icon file.
You need to put your image file in this “Resources” folder, along side the .icns file.

You may also see the “Scripts” folder in there as well, if you go into that folder you will see a AppleScript file named “main.scpt”, that’s the AppleScript file you have been working on in the Script Editor app, but it’s been renamed “main.scpt”, you can double click that script file to open it.

Then change this code.


set my statusItemImage to myApp's NSImage's imageNamed:(myApp's NSImageNameAdvanced)
set statusImageSize to (statusBar's thickness()) - 4
statusItemImage's setSize:(myApp's NSMakeSize(statusImageSize, statusImageSize))

Into this code instead.


set appBundle to myApp's NSBundle's mainBundle()
set imageFilePath to appBundle's pathForResource:"imageFileName" ofType:"png" -- Your file name
set my statusItemImage to myApp's NSImage's alloc()'s initWithContentsOfFile:imageFilePath
set statusImageSize to (statusBar's thickness()) - 4
statusItemImage's setSize:(myApp's NSMakeSize(statusImageSize, statusImageSize))

You will have to put the correct image file name in the second line down from the top.

And it’s very important not to run this code in the Script Editor app, as it will only fail and give you error messages, because the top line will load the application bundle for the Script Editor app, and will look for your image file in that bundle, instead of your own AppleScript app bundle.

So you’re going to have to save the changes, and trust that the code change works at it is intended.
If it doesn’t work, just double check you have put your image file into the correct “Resources” folder.
Or the other reason it might fail is that the code is not compatible with the “Monterey” OS.
Because I can only test on my “Mojave” system, and it works fine for me on my system.

Good Luck

Regards Mark

Mark, you can avoid a bit of that by using path to me to build the path to the image. Then it will work fine in both an editor and the applet.

Hello @Shane Stanley,

I am new to applescripts. Could you give the exact script for “path to me”?

I tried Mark script and it didn’t work without any error as I run it as app. I double check and it shouldn’t be file name problem. I installed Mojave on a VM so it shouldn’t OS difference.

Thanks

Using path to resource is even simpler:

set imageFilePath to POSIX path of (path to resource "imageFileName.png") -- Your file name
set my statusItemImage to current application's NSImage's alloc()'s initWithContentsOfFile:imageFilePath
set statusImageSize to (statusBar's thickness()) - 4
statusItemImage's setSize:{statusImageSize, statusImageSize}

Hello @Shane Stanley,

where do I put my image file if I use apple script editor? we only have Resources folder after I export the Script to applet. Is it easier to use SF Symbols (https://developer.apple.com/sf-symbols/)?

much appreciated

Thank you @Shane Stanley and @Mark FX. my script finally works.
I use code from Shane provided and manually put the image file to Resources folder of the exported applet.

Much appreciated

Shane Stanley Said:

@Shane that makes more sense from the testing point of view, I was getting stuck in ASObjc land for a moment there.

At least the OP has a working solution now, and that’s the main thing.

Regards Mark