├── .gitignore ├── LICENSE ├── README.md ├── demo └── demo.js ├── lib └── tray.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Brandon Horst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tray 2 | Node library for creating simple System Tray applications in OSX 3 | 4 | Currently only working on node 0.10.x. Support for 0.12.x and io.js will come when `NodObjC` is updated. 5 | 6 | ##Installation 7 | 8 | ```sh 9 | npm install native-app tray --save 10 | ``` 11 | 12 | ##Usage 13 | 14 | `tray` relies upon `native-app`. You must call `createApp` and pass the `app` instance to `createTray`. 15 | 16 | ```js 17 | var createApp = require('native-app') 18 | var createTray = require('tray') 19 | 20 | createApp(function (err, app) { 21 | createTray(app, function (err, tray) { 22 | tray.specify({ 23 | title: 'Hello, world!', 24 | menuItems: [ 25 | {title: 'Informational'}, 26 | { 27 | title: 'Do something', 28 | shortcut: 'x', 29 | action: function () { console.log('You pressed a menuItem!') } 30 | } 31 | ] 32 | }) 33 | }) 34 | }) 35 | ``` 36 | 37 | ##API Reference 38 | 39 | ### `createTray(app, callback)` 40 | `callback` is passed a `Tray` once it is ready to use. 41 | 42 | Each program should only call `createTray` once. If you want multiple Items in your Tray, simply pass an array to `Tray.specify`. 43 | 44 | ### `Tray` 45 | 46 | #### `specify(options)` 47 | `options` is an object containing these properties: 48 | 49 | - `title: String` - Text displayed in the Tray (required) 50 | - `menuItems: [MenuItem]` - Items to display in the menu. Each MenuItem is an object containing these properties: 51 | - `title: String` - Text displayed for the menuItem (required) 52 | - `action: Function(data)` - called when the menuItem is selected. Passed `data` if it is provided. 53 | - `data` - arbitrary data passed to `action` when it is invoked 54 | - `shortcut: String` - Keyboard shortcut that can be pressed when the menu is open. Currently, it must be a single symbol, which is invoked with the `cmd` modifier. Capitals are OK. For example, `k` becomes `cmd-k` and P becomes `cmd-shift-p`. 55 | 56 | #### `close()` 57 | Closes the `Tray`, releasing all memory and removing all items. Any further calls to this `Tray` will throw. 58 | 59 | ##Roadmap 60 | 61 | - [ ] Tray icons or styled text in Tray (currently only supports plain text) 62 | - [ ] Nested menuItems 63 | - [ ] Separators 64 | - [ ] State (checkmarks on menuItems) 65 | - [ ] Keyboard Alternates (different options when holding `alt`) 66 | - [ ] Indentation Level 67 | - [ ] Fancy keyboard shortcuts (support `shift-cmd-ctrl-alt-y`) 68 | - [ ] Images in menuItems 69 | - [ ] Test for memory leaks 70 | - [ ] API for changing existing Tray Items (rather than re-creating every time, for performance) 71 | - [ ] Global activation keyboard shortcut (this is [non-trivial](http://bdunagan.com/2010/10/14/cocoa-tip-global-hot-keys/)) 72 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | var createApp = require('native-app') 2 | var createTray = require('..') 3 | 4 | createApp(function (err, app) { 5 | createTray(app, function (err, tray) { 6 | tray.specify({ 7 | title: 'Hello, world!', 8 | menuItems: [{ 9 | title: 'Informational' 10 | }, { 11 | title: 'Do something', 12 | shortcut: 'x', 13 | action: function () { 14 | console.log('You pressed a menuItem!') 15 | } 16 | }, { 17 | title: 'Quit', 18 | shortcut: 'q', 19 | action: function () { 20 | tray.close() 21 | app.close() 22 | } 23 | }] 24 | }) 25 | }) 26 | 27 | createTray(app, function (err, tray) { 28 | tray.specify({ 29 | title: 'Supplemental', 30 | menuItems: [{ 31 | title: 'Do something else', 32 | shortcut: 'x', 33 | action: function () { 34 | console.log('You pressed a menuItem in another tray!') 35 | } 36 | }, { 37 | title: 'Quit', 38 | shortcut: 'q', 39 | action: function () { 40 | tray.close() 41 | } 42 | }] 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /lib/tray.js: -------------------------------------------------------------------------------- 1 | var $ = require('NodObjC') 2 | 3 | var i = 0 4 | 5 | function nsString (string) { 6 | return $.NSString('stringWithUTF8String', string) 7 | } 8 | 9 | function createTray (app, done) { 10 | i++ 11 | var pool = $.NSAutoreleasePool('alloc')('init') 12 | var TrayDelegate = $.NSObject.extend('TrayDelegate' + i) 13 | 14 | TrayDelegate.addMethod('callback:', 'v@:@', function (self, _cmd, sender) { 15 | var tag = sender('tag') 16 | var action = tray._callbacks[tag] 17 | action.callback(action.data) 18 | }) 19 | 20 | TrayDelegate.register() 21 | 22 | var tray = new Tray({ 23 | actor: TrayDelegate('alloc')('init') 24 | }) 25 | pool('drain') 26 | 27 | process.nextTick(function () { 28 | done(null, tray) 29 | }) 30 | } 31 | 32 | // var modifierMap = { 33 | // shift: $.NSShiftKeyMask, 34 | // ctrl: $.NSControlKeyMask, 35 | // cmd: $.NSCommandKeyMask, 36 | // alt: $.NSAlternateKeyMask 37 | // } 38 | // 39 | // function atomToApple(shortcut) { 40 | // 41 | // } 42 | 43 | function createStatusItem () { 44 | var statusBar = $.NSStatusBar('systemStatusBar') 45 | var statusItem = statusBar('statusItemWithLength', -1) // NSVariableStatusItemLength 46 | statusItem('retain') 47 | 48 | return statusItem 49 | } 50 | 51 | function createMenu () { 52 | var menu = $.NSMenu('alloc')('init') 53 | 54 | return menu 55 | } 56 | 57 | function Tray (options) { 58 | this._retainList = [] 59 | this._callbacks = [] 60 | this._actor = options.actor 61 | this._pool = options.pool 62 | this._statusItem = createStatusItem() 63 | this._menu = createMenu() 64 | this._statusItem('setMenu', this._menu) 65 | } 66 | 67 | Tray.prototype._createMenuItem = function (options) { 68 | var menuItem = $.NSMenuItem('alloc')( 69 | 'initWithTitle', nsString(options.title), 70 | 'action', null, 71 | 'keyEquivalent', nsString('') 72 | ) 73 | 74 | if (options.action) { 75 | menuItem('setAction', 'callback:') 76 | menuItem('setTag', this._callbacks.length) 77 | menuItem('setKeyEquivalent', nsString(options.shortcut || '')) 78 | menuItem('setTarget', this._actor) 79 | this._callbacks.push({ 80 | callback: options.action, 81 | data: options.data || null 82 | }) 83 | } 84 | 85 | this._retainList.push(menuItem) 86 | 87 | return menuItem 88 | } 89 | 90 | Tray.prototype._clear = function () { 91 | // release all retained items 92 | this._menu('removeAllItems') 93 | 94 | this._retainList.forEach(function (item) { 95 | item('release') 96 | }) 97 | 98 | this._retainList.length = 0 99 | this._callbacks.length = 0 100 | } 101 | 102 | Tray.prototype.specify = function specify (options) { 103 | var pool = $.NSAutoreleasePool('alloc')('init') 104 | var self = this 105 | 106 | this._clear() 107 | 108 | this._statusItem('setTitle', nsString(options.title)) 109 | 110 | // create the new items 111 | if (options.menuItems) { 112 | options.menuItems.forEach(function (menuItemOption) { 113 | var menuItem = self._createMenuItem(menuItemOption) 114 | self._menu('addItem', menuItem) 115 | }) 116 | } 117 | pool('drain') 118 | } 119 | 120 | Tray.prototype.close = function close () { 121 | var pool = $.NSAutoreleasePool('alloc')('init') 122 | this._clear() 123 | this._menu('release') 124 | this._statusItem('setMenu', null) 125 | $.NSStatusBar('systemStatusBar')('removeStatusItem', this._statusItem) 126 | this._statusItem('release') 127 | this._actor('release') 128 | pool('drain') 129 | } 130 | 131 | module.exports = createTray 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tray", 3 | "version": "2.0.1", 4 | "description": "Node library for creating simple System Tray applications on OSX", 5 | "main": "lib/tray.js", 6 | "scripts": { 7 | "demo": "node demo/demo.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/brandonhorst/node-tray" 12 | }, 13 | "keywords": [ 14 | "osx", 15 | "tray", 16 | "system", 17 | "systray", 18 | "icon", 19 | "notification", 20 | "mac" 21 | ], 22 | "author": "@brandonhorst", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/brandonhorst/node-tray/issues" 26 | }, 27 | "engines": { 28 | "node": ">=0.8.0 <0.11" 29 | }, 30 | "homepage": "https://github.com/brandonhorst/node-tray", 31 | "dependencies": { 32 | "NodObjC": "^1.0.0" 33 | }, 34 | "peerDependences": { 35 | "native-app": "^1.0.1" 36 | }, 37 | "devDependencies": { 38 | "native-app": "^1.0.1" 39 | } 40 | } 41 | --------------------------------------------------------------------------------