├── .gitignore ├── README.md ├── generate-images.sh ├── gruntfile.js ├── images ├── DockerMenu.icns ├── DockerMenu.png ├── boxGreen.png ├── boxGreen@2x.png ├── boxRed.png ├── boxRed@2x.png ├── circleGreen.png ├── circleGreen@2x.png ├── circleRed.png ├── circleRed@2x.png ├── menuActive.png ├── menuActive@2x.png ├── menuPressed.png ├── menuPressed@2x.png ├── menuTemplate.png └── menuTemplate@2x.png ├── lib ├── config.js ├── dialog.js ├── docker.js ├── menu.js └── updater.js ├── main.js ├── package.json ├── screenshot1.png ├── screenshot2.png └── svg ├── boxGreen.svg ├── boxRed.svg ├── circleGreen.svg ├── circleRed.svg ├── menuActive.svg ├── menuPressed.svg └── menuTemplate.svg /.gitignore: -------------------------------------------------------------------------------- 1 | ### 2 | ### 3 | ### 4 | ### 5 | ### !!!DO NOT EDIT!!! 6 | ### AUTO-GENERATED BY https://www.gitignore.io 7 | ### FOR CUSTOM RULES, SCROLL TO THE BOTTOM 8 | ### 9 | ### 10 | ### 11 | ### 12 | 13 | ### Linux ### 14 | *~ 15 | 16 | # KDE directory preferences 17 | .directory 18 | 19 | # Linux trash folder which might appear on any partition or disk 20 | .Trash-* 21 | 22 | 23 | ### Windows ### 24 | # Windows image file caches 25 | Thumbs.db 26 | ehthumbs.db 27 | 28 | # Folder config file 29 | Desktop.ini 30 | 31 | # Recycle Bin used on file shares 32 | $RECYCLE.BIN/ 33 | 34 | # Windows Installer files 35 | *.cab 36 | *.msi 37 | *.msm 38 | *.msp 39 | 40 | # Windows shortcuts 41 | *.lnk 42 | 43 | 44 | ### OSX ### 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | 49 | # Icon must end with two \r 50 | Icon 51 | 52 | 53 | # Thumbnails 54 | ._* 55 | 56 | # Files that might appear in the root of a volume 57 | .DocumentRevisions-V100 58 | .fseventsd 59 | .Spotlight-V100 60 | .TemporaryItems 61 | .Trashes 62 | .VolumeIcon.icns 63 | 64 | # Directories potentially created on remote AFP share 65 | .AppleDB 66 | .AppleDesktop 67 | Network Trash Folder 68 | Temporary Items 69 | .apdisk 70 | 71 | 72 | ### Vim ### 73 | [._]*.s[a-w][a-z] 74 | [._]s[a-w][a-z] 75 | *.un~ 76 | Session.vim 77 | .netrwhist 78 | *~ 79 | 80 | 81 | ### SublimeText ### 82 | # cache files for sublime text 83 | *.tmlanguage.cache 84 | *.tmPreferences.cache 85 | *.stTheme.cache 86 | 87 | # workspace files are user-specific 88 | *.sublime-workspace 89 | 90 | # project files should be checked into the repository, unless a significant 91 | # proportion of contributors will probably not be using SublimeText 92 | # *.sublime-project 93 | 94 | # sftp configuration file 95 | sftp-config.json 96 | 97 | 98 | ### Node ### 99 | # Logs 100 | logs 101 | *.log 102 | npm-debug.log* 103 | 104 | # Runtime data 105 | pids 106 | *.pid 107 | *.seed 108 | 109 | # Directory for instrumented libs generated by jscoverage/JSCover 110 | lib-cov 111 | 112 | # Coverage directory used by tools like istanbul 113 | coverage 114 | 115 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 116 | .grunt 117 | 118 | # node-waf configuration 119 | .lock-wscript 120 | 121 | # Compiled binary addons (http://nodejs.org/api/addons.html) 122 | build/Release 123 | 124 | # Dependency directory 125 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 126 | node_modules 127 | 128 | 129 | ### grunt ### 130 | # Grunt usually compiles files inside this directory 131 | dist/ 132 | 133 | # Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory 134 | .tmp/ 135 | 136 | ### 137 | ### 138 | ### 139 | ### 140 | ### CUSTOM RULES GO HERE 141 | ### 142 | ### 143 | ### 144 | ### 145 | 146 | /build 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-menu 2 | 3 | > Menubar app for Docker built on Electron 4 | 5 | 6 | 7 | 8 | 9 | ## Installing 10 | 11 | Download the appropriate file from [Releases](https://github.com/rdsubhas/docker-menu/releases). We're working on the auto-updater. 12 | 13 | ## Building 14 | 15 | Run the following commands in the cloned source directory: 16 | 17 | * ```npm install``` 18 | * ```npm run grunt``` 19 | 20 | The resulting .app file will be placed in a ```build/``` subdirectory. 21 | -------------------------------------------------------------------------------- /generate-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm images/*.png 3 | for file in $(cd svg && ls *.svg); do 4 | filename=${file%%.svg} 5 | echo "Rasterizing $filename..." 6 | 7 | convert \ 8 | -density 1200 \ 9 | -resize 18x18 \ 10 | -background none \ 11 | svg/$file images/$filename.png 12 | 13 | convert \ 14 | -density 1200 \ 15 | -resize 36x36 \ 16 | -background none \ 17 | svg/$file images/$filename@2x.png 18 | done 19 | 20 | echo 'Creating ICNS...' 21 | convert \ 22 | -density 1200 \ 23 | -resize 1024x1024 \ 24 | -background none \ 25 | svg/menuActive.svg images/DockerMenu.png 26 | mkdir images/DockerMenu.iconset 27 | sips -z 16 16 images/DockerMenu.png --out images/DockerMenu.iconset/icon_16x16.png 28 | sips -z 32 32 images/DockerMenu.png --out images/DockerMenu.iconset/icon_16x16@2x.png 29 | sips -z 32 32 images/DockerMenu.png --out images/DockerMenu.iconset/icon_32x32.png 30 | sips -z 64 64 images/DockerMenu.png --out images/DockerMenu.iconset/icon_32x32@2x.png 31 | sips -z 128 128 images/DockerMenu.png --out images/DockerMenu.iconset/icon_128x128.png 32 | sips -z 256 256 images/DockerMenu.png --out images/DockerMenu.iconset/icon_128x128@2x.png 33 | sips -z 256 256 images/DockerMenu.png --out images/DockerMenu.iconset/icon_256x256.png 34 | sips -z 512 512 images/DockerMenu.png --out images/DockerMenu.iconset/icon_256x256@2x.png 35 | sips -z 512 512 images/DockerMenu.png --out images/DockerMenu.iconset/icon_512x512.png 36 | cp images/DockerMenu.png images/DockerMenu.iconset/icon_512x512@2x.png 37 | iconutil -c icns images/DockerMenu.iconset 38 | rm -R images/DockerMenu.iconset 39 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (grunt) { 4 | require('load-grunt-tasks')(grunt) 5 | let pkg = require('./package.json') 6 | 7 | let dep_files = Object.keys(pkg.dependencies).map(function (dep) { 8 | return 'node_modules/' + dep + '/**/*' 9 | }) 10 | 11 | grunt.initConfig({ 12 | 13 | clean: { 14 | dist: { 15 | src: ['build/'], 16 | options: { 17 | force: true 18 | } 19 | } 20 | }, 21 | 22 | copy: { 23 | sources: { 24 | cwd: '.', 25 | dest: 'build/app/', 26 | src: ['images/**/*', 'lib/**/*', 'main.js', 'package.json'] 27 | }, 28 | dependencies: { 29 | cwd: '.', 30 | dest: 'build/app/', 31 | src: dep_files 32 | } 33 | }, 34 | 35 | electron: { 36 | osx: { 37 | options: { 38 | name: 'DockerMenu-' + pkg.version + '-Mac', 39 | dir: 'build/app', 40 | icon: 'images/DockerMenu.icns', 41 | 'app-bundle-id': 'dockermenu', 42 | 'app-version': pkg.version, 43 | asar: false, 44 | overwrite: true, 45 | out: 'build', 46 | platform: 'darwin', 47 | version: '0.34.1', 48 | arch: 'x64' 49 | } 50 | } 51 | } 52 | 53 | }) 54 | 55 | grunt.registerTask('build', ['clean', 'copy', 'electron']) 56 | grunt.registerTask('default', ['build']) 57 | } 58 | -------------------------------------------------------------------------------- /images/DockerMenu.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/DockerMenu.icns -------------------------------------------------------------------------------- /images/DockerMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/DockerMenu.png -------------------------------------------------------------------------------- /images/boxGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/boxGreen.png -------------------------------------------------------------------------------- /images/boxGreen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/boxGreen@2x.png -------------------------------------------------------------------------------- /images/boxRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/boxRed.png -------------------------------------------------------------------------------- /images/boxRed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/boxRed@2x.png -------------------------------------------------------------------------------- /images/circleGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/circleGreen.png -------------------------------------------------------------------------------- /images/circleGreen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/circleGreen@2x.png -------------------------------------------------------------------------------- /images/circleRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/circleRed.png -------------------------------------------------------------------------------- /images/circleRed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/circleRed@2x.png -------------------------------------------------------------------------------- /images/menuActive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/menuActive.png -------------------------------------------------------------------------------- /images/menuActive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/menuActive@2x.png -------------------------------------------------------------------------------- /images/menuPressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/menuPressed.png -------------------------------------------------------------------------------- /images/menuPressed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/menuPressed@2x.png -------------------------------------------------------------------------------- /images/menuTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/menuTemplate.png -------------------------------------------------------------------------------- /images/menuTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/images/menuTemplate@2x.png -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const app = require('app') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const prefsPath = path.join(app.getPath('userData'), 'prefs.json') 5 | 6 | var prefs = {} 7 | 8 | module.exports = { 9 | 10 | get: function (key) { 11 | return prefs[key] 12 | }, 13 | 14 | set: function (key, value) { 15 | prefs[key] = value 16 | setTimeout(this.save, 0) 17 | }, 18 | 19 | load: function () { 20 | try { 21 | prefs = JSON.parse(fs.readFileSync(prefsPath).toString()) 22 | } catch (e) { 23 | prefs = {} 24 | } 25 | }, 26 | 27 | save: function () { 28 | fs.writeFile(prefsPath, JSON.stringify(prefs)) 29 | } 30 | 31 | } 32 | 33 | module.exports.load() 34 | -------------------------------------------------------------------------------- /lib/dialog.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Lo = require('lodash') 4 | const dialog = require('dialog') 5 | const Docker = require('./docker') 6 | const Menu = require('./menu') 7 | const Config = require('./config') 8 | 9 | const MACHINE_TEMPLATE = Lo.template('Driver: <%= DRIVER %>\nURL: <%= URL %>\nState: <%= STATE %>') 10 | const CONTAINER_TEMPLATE = Lo.template('Image: <%= Image %>\nCommand: <%= Command %>\nStatus: <%= Status %>\nPorts: <%= Portmaps %>') 11 | const ALL_CONTAINERS_TEMPLATE = 'Stop all: Stops all running containers\nCleanup: Removes all stopped containers and untagged images' 12 | 13 | exports.showMachineActions = function (machine) { 14 | let buttons = machine.isRunning ? ['Stop', 'Kill', 'Cancel'] : ['Start', 'Remove', 'Cancel'] 15 | dialog.showMessageBox({ 16 | title: machine.NAME, 17 | message: machine.NAME, 18 | detail: MACHINE_TEMPLATE(machine), 19 | type: 'info', 20 | buttons: buttons, 21 | noLink: true 22 | }, function (actionIndex) { 23 | switch (buttons[actionIndex]) { 24 | case 'Stop': 25 | Docker.machineAction([ 'stop', machine.NAME ]).then(Menu.rebuildNow) 26 | break 27 | case 'Start': 28 | Docker.machineAction([ 'start', machine.NAME ]).then(Menu.rebuildNow) 29 | break 30 | case 'Kill': 31 | Docker.machineAction([ 'kill', machine.NAME ]).then(Menu.rebuildNow) 32 | break 33 | case 'Remove': 34 | Docker.machineAction([ 'rm', machine.NAME ]).then(Menu.rebuildNow) 35 | break 36 | } 37 | }) 38 | } 39 | 40 | exports.showContainerActions = function (machine, container) { 41 | let buttons = container.isRunning ? ['Stop', 'Kill', 'Cancel'] : ['Start', 'Remove', 'Cancel'] 42 | let ports = container.Ports.map(function (port) { 43 | return `${port.Type}/${port.PrivatePort}:${port.PublicPort || '-'}` 44 | }).join(', ') 45 | container.Portmaps = ports 46 | dialog.showMessageBox({ 47 | title: container.bestName, 48 | message: container.bestName, 49 | detail: CONTAINER_TEMPLATE(container), 50 | type: 'info', 51 | buttons: buttons, 52 | noLink: true 53 | }, function (actionIndex) { 54 | Docker.getContainer(machine, container).then(function (containerInstance) { 55 | switch (buttons[actionIndex]) { 56 | case 'Stop': 57 | containerInstance.stop(Menu.rebuildNow) 58 | break 59 | case 'Start': 60 | containerInstance.start(Menu.rebuildNow) 61 | break 62 | case 'Remove': 63 | containerInstance.remove(Menu.rebuildNow) 64 | break 65 | case 'Kill': 66 | containerInstance.kill(Menu.rebuildNow) 67 | break 68 | } 69 | }) 70 | }) 71 | } 72 | 73 | exports.showAllContainerActions = function (machine, containers) { 74 | let buttons = ['Stop all', 'Cleanup', 'Cancel'] 75 | dialog.showMessageBox({ 76 | title: machine.NAME, 77 | message: machine.NAME, 78 | detail: ALL_CONTAINERS_TEMPLATE, 79 | type: 'info', 80 | buttons: buttons, 81 | noLink: true 82 | }) 83 | } 84 | 85 | exports.locateDockerMachine = function () { 86 | dialog.showOpenDialog({ 87 | title: 'Locate docker-machine', 88 | properties: ['openFile'] 89 | }, function (filenames) { 90 | if (filenames && filenames.length == 1) { 91 | Config.set('machine_cmd', filenames[0]) 92 | Menu.rebuildNow() 93 | } 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /lib/docker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('app') 4 | const Config = require('./config') 5 | const Promise = require('bluebird') 6 | const child_process = Promise.promisifyAll(require('child_process')) 7 | const parseColumns = require('parse-columns') 8 | const fs = require('fs') 9 | const Docker = require('dockerode') 10 | const nullFn = function () {} 11 | Promise.promisifyAll(Docker.prototype) 12 | 13 | const dockerMachineCmd = function () { 14 | return Config.get('machine_cmd') || '/usr/local/bin/docker-machine' 15 | } 16 | 17 | const dockerMachinePaths = function () { 18 | return ['/usr/local/bin', '/usr/local/sbin', '/opt/bin', '/opt/sbin', 19 | '/opt/local/bin', '/opt/local/sbin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'] 20 | } 21 | 22 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' 23 | process.env.PATH = dockerMachinePaths().join(':') 24 | 25 | exports.machineAction = function (args) { 26 | args.unshift(dockerMachineCmd()) 27 | return child_process.execAsync(args.join(' '), { cwd: app.getAppPath() }) 28 | } 29 | 30 | exports.machines = function () { 31 | return exports.machineAction([ 'ls' ]).spread(function (stdout, stderr) { 32 | return parseColumns(stdout).map(function (machine) { 33 | machine.isRunning = machine.STATE && machine.STATE.toLowerCase().indexOf('running') >= 0 34 | return machine 35 | }) 36 | }) 37 | } 38 | 39 | exports.connect = function (machine) { 40 | return exports.machineAction(['inspect', machine.NAME]).spread(function (stdout, stderr) { 41 | let json = JSON.parse(stdout) 42 | let driver = json.Driver 43 | let auth = json.HostOptions.AuthOptions 44 | return new Docker({ 45 | host: driver.IPAddress, 46 | port: driver.Port || 2376, 47 | ca: (auth.CaCertPath ? fs.readFileSync(auth.CaCertPath) : null), 48 | cert: (auth.ClientCertPath ? fs.readFileSync(auth.ClientCertPath) : null), 49 | key: (auth.ClientKeyPath ? fs.readFileSync(auth.ClientKeyPath) : null) 50 | }) 51 | }) 52 | } 53 | 54 | exports.getContainer = function (machine, container) { 55 | return exports.connect(machine).then(function (docker) { 56 | return docker.getContainer(container.Id) 57 | }) 58 | } 59 | 60 | exports.dockerMachineInstalled = function () { 61 | return fs.existsSync(dockerMachineCmd()) 62 | } 63 | -------------------------------------------------------------------------------- /lib/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Lo = require('lodash') 4 | const app = require('app') 5 | const path = require('path') 6 | const Menu = require('menu') 7 | const MenuItem = require('menu-item') 8 | const Dialog = require('./dialog') 9 | const Promise = require('bluebird') 10 | const Docker = require('./docker') 11 | const Config = require('./config') 12 | 13 | const UPDATE_INTERVAL = 1000 * 60 * 15 // every 15 minutes 14 | const IMAGES_FOLDER = path.resolve(app.getAppPath(), 'images') 15 | var updateTimer, trayIcon 16 | 17 | const statusIcon = function (bool, successIcon, failureIcon) { 18 | return IMAGES_FOLDER + '/' + (bool ? successIcon : failureIcon) + '.png' 19 | } 20 | 21 | const buildMainMenu = Promise.coroutine(function *() { 22 | let menu = new Menu() 23 | menu.append(new MenuItem({ 24 | label: 'Refresh', 25 | click: rebuildNow 26 | })) 27 | menu.append(new MenuItem({ type: 'separator' })) 28 | 29 | if (Docker.dockerMachineInstalled()) { 30 | try { 31 | yield buildSelectedMenu(menu) 32 | } catch (e) { 33 | console.error(e) 34 | menu.append(new MenuItem({ 35 | label: 'Error fetching data!', 36 | enabled: false 37 | })) 38 | menu.append(new MenuItem({ type: 'separator' })) 39 | trayIcon.setImage(statusIcon(true, 'menuTemplate')) 40 | } 41 | } else { 42 | Dialog.locateDockerMachine() 43 | } 44 | 45 | menu.append(yield buildOptionsMenu()) 46 | menu.append(new MenuItem({ 47 | label: 'Quit', 48 | click: app.quit 49 | })) 50 | 51 | trayIcon.setContextMenu(menu) 52 | }) 53 | 54 | const buildSelectedMenu = Promise.coroutine(function *(menu) { 55 | let machines = yield Docker.machines() 56 | let selectedMachine = Lo.find(machines, 'NAME', Config.get('selected_machine')) 57 | selectedMachine.isSelected = true 58 | 59 | if (selectedMachine) { 60 | yield buildContainersMenu(menu, selectedMachine) 61 | menu.append(new MenuItem({ type: 'separator' })) 62 | } 63 | 64 | yield buildMachinesMenu(menu, machines) 65 | menu.append(new MenuItem({ type: 'separator' })) 66 | 67 | menu.append(yield buildSwitchMenu(machines, selectedMachine)) 68 | trayIcon.setImage(statusIcon(selectedMachine.isRunning, 'menuActive', 'menuTemplate')) 69 | }) 70 | 71 | const buildSwitchMenu = Promise.coroutine(function *(machines, selectedMachine) { 72 | let menu = new Menu() 73 | for (let machine of machines) { 74 | let selected = machine.NAME === selectedMachine.NAME 75 | menu.append(new MenuItem({ 76 | label: machine.NAME, 77 | checked: selected, 78 | enabled: !selected, 79 | click: function () { 80 | Config.set('selected_machine', machine.NAME) 81 | rebuildNow() 82 | } 83 | })) 84 | } 85 | 86 | return new MenuItem({ 87 | label: 'Select Machine', 88 | submenu: menu 89 | }) 90 | }) 91 | 92 | const buildMachinesMenu = Promise.coroutine(function *(menu, machines) { 93 | menu.append(new MenuItem({ 94 | label: 'Machines', 95 | enabled: false 96 | })) 97 | for (let machine of machines) { 98 | let machineMenu = yield buildMachineMenu(machine) 99 | menu.append(machineMenu) 100 | } 101 | }) 102 | 103 | const buildMachineMenu = Promise.coroutine(function *(machine) { 104 | return new MenuItem({ 105 | label: machine.NAME, 106 | icon: statusIcon(machine.isRunning, 'circleGreen', 'circleRed'), 107 | click: function () { 108 | Dialog.showMachineActions(machine) 109 | } 110 | }) 111 | }) 112 | 113 | const buildContainersMenu = Promise.coroutine(function *(menu, machine) { 114 | if (machine.isRunning) { 115 | let docker = yield Docker.connect(machine) 116 | let containers = yield docker.listContainersAsync({ all: true }) 117 | menu.append(new MenuItem({ 118 | label: 'Containers (' + machine.NAME + ')', 119 | enabled: false 120 | })) 121 | for (let container of containers) { 122 | let containerMenu = yield buildContainerMenu(machine, container) 123 | menu.append(containerMenu) 124 | } 125 | if (containers.length > 0) { 126 | menu.append(new MenuItem({ 127 | label: 'All...', 128 | click: function () { 129 | Dialog.showAllContainerActions(machine, containers) 130 | } 131 | })) 132 | } 133 | } 134 | }) 135 | 136 | const buildContainerMenu = Promise.coroutine(function *(machine, container) { 137 | container.bestName = Lo.min(container.Names, 'length').replace(/^\//, '') 138 | container.isRunning = !!container.Status.match(/^(running|up)/i) 139 | return new MenuItem({ 140 | label: container.bestName, 141 | icon: statusIcon(container.isRunning, 'boxGreen', 'boxRed'), 142 | click: function () { 143 | Dialog.showContainerActions(machine, container) 144 | } 145 | }) 146 | }) 147 | 148 | const buildOptionsMenu = Promise.coroutine(function *() { 149 | let menu = new Menu() 150 | menu.append(new MenuItem({ 151 | label: 'Locate docker-machine', 152 | click: function () { 153 | Dialog.locateDockerMachine() 154 | } 155 | })) 156 | 157 | return new MenuItem({ 158 | label: 'Options', 159 | submenu: menu 160 | }) 161 | }) 162 | 163 | const rebuildNow = function () { 164 | setTimeout(buildMainMenu, 0) 165 | } 166 | 167 | const watch = function (_trayIcon) { 168 | trayIcon = _trayIcon 169 | clearInterval(updateTimer) 170 | updateTimer = setInterval(buildMainMenu, UPDATE_INTERVAL) 171 | rebuildNow() 172 | } 173 | 174 | exports.rebuildNow = rebuildNow 175 | exports.watch = watch 176 | -------------------------------------------------------------------------------- /lib/updater.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CHECK_FOR_UPDATES = false 4 | 5 | const autoUpdater = require('auto-updater') 6 | const pkg = require('../package.json') 7 | const autoUpdateUrl = `http://docker-menu-updater.herokuapp.com/v1/rdsubhas/docker-menu/${pkg.version}` 8 | 9 | autoUpdater.on('error', function () { 10 | console.log('error', arguments) 11 | }) 12 | 13 | autoUpdater.on('checking-for-update', function () { 14 | console.log('checking-for-update', arguments) 15 | }) 16 | 17 | autoUpdater.on('update-available', function () { 18 | console.log('update-available', arguments) 19 | }) 20 | 21 | autoUpdater.on('update-not-available', function () { 22 | console.log('update-not-available', arguments) 23 | }) 24 | 25 | autoUpdater.on('update-downloaded', function () { 26 | console.log('update-downloaded', arguments) 27 | }) 28 | 29 | let start = function () { 30 | if (CHECK_FOR_UPDATES) { 31 | autoUpdater.setFeedUrl(autoUpdateUrl) 32 | autoUpdater.checkForUpdates() 33 | } 34 | } 35 | 36 | exports.start = start 37 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const app = require('app') 4 | const fixPath = require('fix-path') 5 | const Tray = require('tray') 6 | const Menu = require('./lib/menu') 7 | const Updater = require('./lib/updater') 8 | 9 | // report crashes to the Electron project 10 | // require('crash-reporter').start() 11 | 12 | // Make single instance 13 | let shouldQuit = app.makeSingleInstance(function () { return true }) 14 | if (shouldQuit) { 15 | app.quit() 16 | } 17 | 18 | app.on('ready', function () { 19 | // Fix $PATH 20 | fixPath() 21 | 22 | // Hide in dock 23 | app.dock.hide() 24 | 25 | let trayIcon = new Tray(app.getAppPath() + '/images/menuTemplate.png') 26 | trayIcon.setPressedImage(app.getAppPath() + '/images/menuPressed.png') 27 | Menu.watch(trayIcon) 28 | 29 | // Check for updates 30 | Updater.start() 31 | }) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-menu", 3 | "version": "0.3.0", 4 | "private": true, 5 | "main": "main.js", 6 | "engines": { 7 | "node": "4.x" 8 | }, 9 | "scripts": { 10 | "start": "electron .", 11 | "test": "standard", 12 | "grunt": "grunt" 13 | }, 14 | "dependencies": { 15 | "bluebird": "^2.10.2", 16 | "fix-path": "^1.1.0", 17 | "dockerode": "^2.2.3", 18 | "lodash": "^3.10.0", 19 | "parse-columns": "^1.2.0" 20 | }, 21 | "devDependencies": { 22 | "electron-prebuilt": "^0.34.1", 23 | "grunt": "^0.4.5", 24 | "grunt-cli": "^0.1.13", 25 | "grunt-contrib-clean": "^0.6.0", 26 | "grunt-contrib-copy": "^0.8.2", 27 | "grunt-electron": "^2.0.1", 28 | "load-grunt-tasks": "^3.3.0", 29 | "standard": "^5.3.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdsubhas/menubar-docker/c8851e069886f4c29354824207f4d9bab6937a36/screenshot2.png -------------------------------------------------------------------------------- /svg/boxGreen.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /svg/boxRed.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /svg/circleGreen.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /svg/circleRed.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /svg/menuActive.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 28 | 48 | 52 | 56 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 104 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /svg/menuPressed.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 28 | 48 | 52 | 57 | 64 | 71 | 78 | 85 | 92 | 99 | 106 | 113 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /svg/menuTemplate.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 28 | 48 | 52 | 57 | 64 | 71 | 78 | 85 | 92 | 99 | 106 | 113 | 120 | 121 | 122 | --------------------------------------------------------------------------------