AppleScript menu bar app with icon?

Thanks to the enormously generous help of Ben Surtees at mac bartender.com, I was able to create a menu bar app in AppleScript, but I can’t figure out one detail. In the code below, the title of the app is “Menu,” but I haven’t figured out how to use an icon instead of a title. There are a few posts on StackOverflow and here, including this one:

but I haven’t been able to modify this code so that it uses an image file in the Resources folder of the applet. (Also, it’s not clear - to me at least - whether the image file needs to be a specific size like 32x32 or whether the OS will scale down a larger image.)

I’ll be very grateful for any help.


-- rewritten by Ben Surtees at macbartender.com

-- to prevent the app icon from appearing in the dock and to prevent the Esc key from exiting, 
-- add "Application is background only" - "YES"  and "Application is Agent (UIElement)" - "YES" 
-- to info.plist of the AppleScript app

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

property statusItem : missing value
property interval : 1.0
property runOption : "active"

on run
	my createStatusItem()
end run

on idle
	-- do something
	return interval
end idle

on createStatusItem()
	
	try
		current application's NSStatusBar's systemStatusBar()'s removeStatusItem:statusItem
	end try
	set my statusItem to current application's NSStatusBar's systemStatusBar's statusItemWithLength:(current application's NSVariableStatusItemLength)
	statusItem's setTitle:"Menu"
	my createMenu()
	
end createStatusItem

on createMenu()
	
	set newMenu to current application's NSMenu's alloc()'s initWithTitle:""
	(newMenu's addItemWithTitle:"About Menu" action:"action1:" keyEquivalent:"h")'s setTarget:me
	newMenu's addItem:(current application's NSMenuItem's separatorItem)
	set returnValue to (newMenu's addItemWithTitle:"Timer: 0.25 sec" action:"action2:" keyEquivalent:"2")
	returnValue's setTarget:me
	if interval is 0.25 then
		returnValue's setState:1
	end if
	set returnValue to (newMenu's addItemWithTitle:"Timer: 0.5 sec" action:"action3:" keyEquivalent:"5")
	returnValue's setTarget:me
	if interval is 0.5 then
		returnValue's setState:1
	end if
	set returnValue to (newMenu's addItemWithTitle:"Timer: 1.0 sec" action:"action4:" keyEquivalent:"1")
	returnValue's setTarget:me
	if interval is 1.0 then
		returnValue's setState:1
	end if
	newMenu's addItem:(current application's NSMenuItem's separatorItem)
	if runOption is "active" then
		set returnValue to (newMenu's addItemWithTitle:"Enabled" action:"action5:" keyEquivalent:"e")
		returnValue's setTarget:me
		returnValue's setState:1
	else
		set returnValue to (newMenu's addItemWithTitle:"Click to enable" action:"action5:" keyEquivalent:"e")
		returnValue's setTarget:me
	end if
	newMenu's addItem:(current application's NSMenuItem's separatorItem)
	(newMenu's addItemWithTitle:"Quit" action:"terminate" keyEquivalent:"q")'s setTarget:me
	statusItem's setMenu:newMenu
	
end createMenu

on action1:sender
	display dialog "This app doesn't do anything at all." buttons ("OK") default button 1
end action1:

on action2:sender
	set interval to 0.25
	my createMenu()
end action2:

on action3:sender
	set interval to 0.5
	my createMenu()
end action3:

on action4:sender
	set interval to 1.0
	my createMenu()
end action4:

on action5:sender
	if runOption is "disabled" then
		set runOption to "active"
		my createMenu()
	else
		set runOption to "disabled"
		my createMenu()
	end if
end action5:

to terminate() -- quit handler is not called from normal NSApplication terminate:
	if name of current application does not start with "Script" then tell me to quit
end terminate

@emendelson

This subject of ‘StatusBarItems’ was covered in some detail on another MacScripter thread.

https://www.macscripter.net/t/write-text-on-menu-bar/73366

I think using an image in ‘StatusBarItems’ was covered near the end of the above linked thread.

Regards Mark

@Mark_FX - Thank you for that pointer. I’ve been struggling to make it work. The following code runs, but nothing get displayed in the status bar. The resource png file does exist. I’m clearly missing something obvious, but I can’t imagine what it is. If you can help at all, I’ll be very grateful.

-- rewritten by Ben Surtees at macbartender.com

-- to prevent the app icon from appearing in the dock and to prevent the Esc key from exiting, 
-- add "Application is background only" - "YES"  and "Application is Agent (UIElement)" - "YES" 
-- to info.plist of the AppleScript app

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

property statusItem : missing value
property statusBar : missing value
property statusItemImage : missing value
property interval : 1.0
property runOption : "active"

on run
	my createStatusItem()
end run

on idle
	-- do something
	return interval
end idle

on createStatusItem()
	
	try
		current application's NSStatusBar's systemStatusBar()'s removeStatusItem:statusItem
	end try
	set my statusItem to current application's NSStatusBar's systemStatusBar's statusItemWithLength:(current application's NSVariableStatusItemLength)
	-- statusItem's setTitle:"Menu"
	
	-- new code here
	set imageFilePath to POSIX path of (path to resource "Menu.png") -- Your file name
	set my statusItemImage to current application's NSImage's alloc()'s initWithContentsOfFile:imageFilePath
	set my statusBar to current application's NSStatusBar's systemStatusBar()
	set statusImageSize to (statusBar's thickness()) - 4
	statusItemImage's setSize:{statusImageSize, statusImageSize}
	
	my createMenu()
	
end createStatusItem

on createMenu()
	
	set newMenu to current application's NSMenu's alloc()'s initWithTitle:""
	(newMenu's addItemWithTitle:"About Menu" action:"action1:" keyEquivalent:"h")'s setTarget:me
	newMenu's addItem:(current application's NSMenuItem's separatorItem)
	set returnValue to (newMenu's addItemWithTitle:"Timer: 0.25 sec" action:"action2:" keyEquivalent:"2")
	returnValue's setTarget:me
	if interval is 0.25 then
		returnValue's setState:1
	end if
	set returnValue to (newMenu's addItemWithTitle:"Timer: 0.5 sec" action:"action3:" keyEquivalent:"5")
	returnValue's setTarget:me
	if interval is 0.5 then
		returnValue's setState:1
	end if
	set returnValue to (newMenu's addItemWithTitle:"Timer: 1.0 sec" action:"action4:" keyEquivalent:"1")
	returnValue's setTarget:me
	if interval is 1.0 then
		returnValue's setState:1
	end if
	newMenu's addItem:(current application's NSMenuItem's separatorItem)
	if runOption is "active" then
		set returnValue to (newMenu's addItemWithTitle:"Enabled" action:"action5:" keyEquivalent:"e")
		returnValue's setTarget:me
		returnValue's setState:1
	else
		set returnValue to (newMenu's addItemWithTitle:"Click to enable" action:"action5:" keyEquivalent:"e")
		returnValue's setTarget:me
	end if
	newMenu's addItem:(current application's NSMenuItem's separatorItem)
	(newMenu's addItemWithTitle:"Quit" action:"terminate" keyEquivalent:"q")'s setTarget:me
	statusItem's setMenu:newMenu
	
end createMenu

on action1:sender
	display dialog "This app doesn't do anything at all." buttons ("OK") default button 1
end action1:

on action2:sender
	set interval to 0.25
	my createMenu()
end action2:

on action3:sender
	set interval to 0.5
	my createMenu()
end action3:

on action4:sender
	set interval to 1.0
	my createMenu()
end action4:

on action5:sender
	if runOption is "disabled" then
		set runOption to "active"
		my createMenu()
	else
		set runOption to "disabled"
		my createMenu()
	end if
end action5:

to terminate() -- quit handler is not called from normal NSApplication terminate:
	if name of current application does not start with "Script" then tell me to quit
end terminate

Firstly its worth saying that AppleScriptObjC code examples should be in the AppleScriptObjC category of the forum, maybe one of the moderators will move it across there.

Secondly your ‘createStatusItem()’ method is not laid out correctly, you need to get a reference to the system’s ‘NSStatusBar’, and then add the newly created ‘NSStatusItem’.
Like this basic example below.


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

property myApp : a reference to current application

property statusBar : missing value
property statusItem : missing value
property statusItemImage : missing value
property statusItemTitle : "Status Item Title"

on run {}
	-- Remove this Thread check code, once your stand alone App bundle is built and running. 
	if my NSThread's isMainThread() as boolean then
		my createStatusItem:statusItemTitle
	else
		my performSelectorOnMainThread:"createStatusItem:" withObject:statusItemTitle waitUntilDone:true
	end if
end run

on createStatusItem:title
	-- Get the systems Status Bar object
	set my statusBar to myApp's NSStatusBar's systemStatusBar()
	
	-- load a standard MacOS image, but you can load an image from the appBundle
	set my statusItemImage to myApp's NSImage's imageNamed:(myApp's NSImageNameAdvanced)
	
	set statusBarThickness to statusBar's thickness() -- Get thickness of the Status Bar
	
	-- Set the Image size to be 4 pixels less that the thickness of the Status Bar, and square.
	statusItemImage's setSize:(myApp's NSMakeSize((statusBarThickness - 4), (statusBarThickness - 4)))
	
	-- Create the Status Item with a title and image, for just an image, then set empty title string.
	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)
	
	my createMenuItems()
end createStatusItem:

on createMenuItems()
	set statusItemMenu to myApp's NSMenu's alloc()'s initWithTitle:""
	
	set doStuffMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"DoStuff" action:"doStuff" keyEquivalent:"d"
	doStuffMenuItem's setTarget:me
	statusItemMenu's addItem:doStuffMenuItem
	
	set sepMenuItem to myApp's NSMenuItem's separatorItem()
	statusItemMenu's addItem:sepMenuItem
	
	set quitMenuItem to myApp's NSMenuItem's alloc()'s initWithTitle:"Quit" action:"quitStatusItem" keyEquivalent:"q"
	quitMenuItem's setIndentationLevel:2
	quitMenuItem's setTarget:me
	statusItemMenu's addItem:quitMenuItem
	
	statusItem's setMenu:statusItemMenu
end createMenuItems

on doStuff()
	-- Do stuff here when the doStuffMenuItem is clicked
end doStuff

on quitStatusItem()
	-- Remove this Thread check code, once your stand alone App bundle is built and running. 
	if my NSThread's isMainThread() as boolean then
		my removeStatusItem()
	else
		my performSelectorOnMainThread:"removeStatusItem" withObject:(missing value) waitUntilDone:true
	end if
	if name of myApp does not start with "Script" then
		tell me to quit
	end if
end quitStatusItem

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

on idle {}
	return 1.0
end idle

on quit {}
	continue quit
end quit

Ive used a standard macOS image in my example, but you can load the image as you have been doing in your own code.
But copy and paste the above code into a new Script Editor file, and click the run button to see it in action.

Do remember that when loading an image from Resources, that it will only work properly when your app package is built, and the requested image file is in your app’s Resources folder.
Doing so when running in the Script Editor app, means that it will be looking in the Script Editor’s Resources folder instead, so won’t work as expected.

Regards Mark

1 Like

@Mark_FX - That was exactly what I needed to know, and I can’t thank you enough. I spent hours trying to get that right, and now you’ve made it entirely clear. Thank you again!