├── .tsignore ├── benchmark.txt ├── logo_s.ico ├── logo_s.png ├── typings.json ├── traybin ├── tray_darwin_release ├── tray_linux_release └── tray_windows_release.exe ├── .travis.yml ├── tslint.json ├── .npmignore ├── .vscode └── launch.json ├── menu.js ├── .gitignore ├── LICENSE ├── package.json ├── example.mjs ├── index.test.ts ├── tsconfig.json ├── tsconfig-lint.json ├── .eslintrc.js ├── README.md └── index.ts /.tsignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | src -------------------------------------------------------------------------------- /benchmark.txt: -------------------------------------------------------------------------------- 1 | Start Suit: Get one key 2 | -------------------------------------------------------------------------------- /logo_s.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixhao28/node-systray/HEAD/logo_s.ico -------------------------------------------------------------------------------- /logo_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixhao28/node-systray/HEAD/logo_s.png -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "systray", 3 | "main": "lib/index.d.ts", 4 | "version": "1.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /traybin/tray_darwin_release: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixhao28/node-systray/HEAD/traybin/tray_darwin_release -------------------------------------------------------------------------------- /traybin/tray_linux_release: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixhao28/node-systray/HEAD/traybin/tray_linux_release -------------------------------------------------------------------------------- /traybin/tray_windows_release.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixhao28/node-systray/HEAD/traybin/tray_windows_release.exe -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - '6' 5 | script: 6 | - node --version 7 | - yarn --version 8 | - yarn run flow && yarn run lint && yarn run test 9 | sudo: false 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "defaultSeverity": "warning", 4 | "extends": [ 5 | "tslint-config-standard" 6 | ], 7 | "jsRules": {}, 8 | "rules": { 9 | "trailing-comma": true, 10 | "space-before-function-paren": false 11 | }, 12 | "rulesDirectory": [] 13 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | yarn-error.log 3 | .nyc_output 4 | coverage 5 | .DS_Store 6 | node_modules 7 | .eslintrc.json 8 | .eslintignore 9 | .travis.yml 10 | .editorconfig 11 | .gitignore 12 | test-dist 13 | .vscode 14 | index.ts 15 | index.test.ts 16 | menu.js 17 | .tsignore 18 | benchmark.txt 19 | test.ts 20 | tsconfig.json 21 | tslint.json 22 | example.js 23 | .eslintrc.js 24 | example.mjs 25 | index.js.map 26 | logo_s.ico 27 | logo_s.png 28 | tsconfig-lint.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/example.mjs", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /menu.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'icon': '', 3 | 'title': '标题', 4 | 'tooltip': 'Tips', 5 | 'items': [{ 6 | 'title': 'aa', 7 | 'tooltip': 'bb', 8 | 'checked': true, 9 | 'enabled': true 10 | }, { 11 | 'title': '', 12 | 'tooltip': '', 13 | 'enabled': true 14 | }, { 15 | 'title': 'aa2', 16 | 'tooltip': 'bb', 17 | 'checked': false, 18 | 'enabled': true, 19 | 'items': [{ 20 | 'title': 'submenu 1', 21 | 'tooltip': 'this is submenu 1', 22 | 'checked': true, 23 | 'enabled': true 24 | }, { 25 | 'title': 'submenu 2', 26 | 'tooltip': 'this is submenu 2', 27 | 'checked': true, 28 | 'enabled': true 29 | }] 30 | }, { 31 | 'title': 'Exit', 32 | 'tooltip': 'bb', 33 | 'checked': false, 34 | 'enabled': true 35 | }] 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_STORE 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | index.d.ts 42 | index.js 43 | index.js.map 44 | es 45 | test-dist 46 | 47 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zack Young 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "systray2", 3 | "version": "2.1.4", 4 | "description": "An systray libray for nodejs, more updated than the original systray", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc --watch", 10 | "lint": "tslint ./src/** --type-check -p ./tsconfig.json", 11 | "test": "cross-env DEBUG=systray* mocha -r ts-node/register ./*.test.ts", 12 | "cover": "cross-env NODE_ENV=development nyc ava", 13 | "preversion": "npm run build" 14 | }, 15 | "engines": { 16 | "node": ">=4.0.0" 17 | }, 18 | "pre-commit": [], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/felixhao28/node-systray.git" 22 | }, 23 | "keywords": [ 24 | "systray", 25 | "tray", 26 | "gui" 27 | ], 28 | "author": "felixhao28 & zaaack", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/felixhao28/node-systray/issues" 32 | }, 33 | "homepage": "https://github.com/felixhao28/node-systray#readme", 34 | "devDependencies": { 35 | "@types/debug": "^4.1.7", 36 | "@types/fs-extra": "^9.0.13", 37 | "@types/mocha": "^9.0.0", 38 | "@types/node": "^17.0.1", 39 | "@typescript-eslint/eslint-plugin": "^5.7.0", 40 | "@typescript-eslint/eslint-plugin-tslint": "^5.7.0", 41 | "@typescript-eslint/parser": "^5.7.0", 42 | "cross-env": "^7.0.3", 43 | "del-cli": "^4.0.1", 44 | "eslint": "^8.5.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-plugin-import": "^2.25.3", 47 | "eslint-plugin-jsdoc": "^37.3.0", 48 | "mocha": "^9.1.3", 49 | "npm-run-all": "^4.1.5", 50 | "nyc": "^15.1.0", 51 | "pre-commit": "^1.2.2", 52 | "ts-node": "^10.4.0", 53 | "typescript": "^4.5.4" 54 | }, 55 | "dependencies": { 56 | "debug": "^4.3.3", 57 | "fs-extra": "^10.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import os from 'os' 3 | import SysTray from './index.js' 4 | 5 | const item1 = { 6 | title: 'aa', 7 | tooltip: 'bb', 8 | // checked is implemented by plain text in linux 9 | checked: false, 10 | enabled: true, 11 | // click is not a standard property but a custom value 12 | click: () => { 13 | item1.checked = !item1.checked 14 | systray.sendAction({ 15 | type: 'update-item', 16 | item: item1, 17 | }) 18 | // toggle item 2 19 | itemExit.hidden = !itemExit.hidden 20 | systray.sendAction({ 21 | type: 'update-item', 22 | item: itemExit, 23 | }) 24 | } 25 | } 26 | const item2 = { 27 | title: 'aa2', 28 | tooltip: 'bb', 29 | checked: false, 30 | enabled: true, 31 | // hidden 32 | hidden: false, 33 | // add a submenu item 34 | items: [{ 35 | title: 'Submenu', 36 | tooltip: 'this is a submenu item', 37 | checked: false, 38 | enabled: true, 39 | click: () => { 40 | // open the url 41 | console.log('open the url') 42 | } 43 | }] 44 | } 45 | const itemExit = { 46 | title: 'Exit', 47 | tooltip: 'bb', 48 | checked: false, 49 | enabled: true, 50 | click: () => { 51 | systray.kill(false) 52 | } 53 | } 54 | const systray = new SysTray.default({ 55 | menu: { 56 | // you should use .png icon on macOS/Linux, and .ico format on Windows 57 | icon: os.platform() === 'win32' ? './logo_s.ico' : './logo_s.png', 58 | isTemplateIcon: os.platform() === 'darwin', 59 | title: '标题', 60 | tooltip: 'Tips', 61 | items: [ 62 | item1, 63 | SysTray.default.separator, // SysTray.separator is equivalent to a MenuItem with "title" equals "" 64 | item2, 65 | itemExit 66 | ] 67 | }, 68 | debug: false, 69 | copyDir: false // copy go tray binary to outside directory, useful for packing tool like pkg. 70 | }) 71 | 72 | systray.onClick(action => { 73 | if (action.item.click != null) { 74 | action.item.click() 75 | } 76 | }) 77 | 78 | // Systray.ready is a promise which resolves when the tray is ready. 79 | systray.ready().then(() => { 80 | systray.process.stderr?.on('data', (chunk) => { 81 | console.log(chunk.toString()) 82 | }) 83 | console.log('systray started!') 84 | }) 85 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-floating-promises 2 | import * as os from 'os' 3 | import * as path from 'path' 4 | import * as assert from 'assert' 5 | import SysTray from './index' 6 | const menu = require('./menu.js') 7 | const pkg = require('./package.json') 8 | 9 | menu.icon = os.platform() === 'win32' ? './logo_s.ico' : './logo_s.png' 10 | 11 | describe('test', function () { 12 | this.timeout(1000 * 24 * 3600) 13 | 14 | it('systray release is ok', async () => { 15 | const systray = new SysTray({ menu, debug: false }) 16 | await systray.onClick(async action => { 17 | if (action.seq_id === 0) { 18 | await systray.sendAction({ 19 | type: 'update-item', 20 | item: { 21 | ...(action.item), 22 | checked: !action.item.checked 23 | } 24 | }) 25 | } else if (action.seq_id === 2) { 26 | await systray.kill() 27 | } 28 | console.log('action', action) 29 | }) 30 | await systray.ready() 31 | systray.process.stderr?.on('data', (chunk) => { 32 | console.log(chunk.toString()) 33 | }) 34 | console.log('Exit the tray in 1000ms...') 35 | const exitInfo = new Promise<{ code: number | null; signal: string | null }>(resolve => systray.onExit((code, signal) => resolve({ code, signal }))) 36 | await new Promise((resolve, reject) => { 37 | setTimeout(async () => { 38 | await systray.kill(false) 39 | resolve() 40 | }, 1000) 41 | }) 42 | const { code, signal } = await exitInfo 43 | console.log('code', code, 'signal', signal) 44 | assert.strictEqual(code, 0) 45 | assert.strictEqual(signal, null) 46 | }) 47 | 48 | it('systray copyDir is ok', async () => { 49 | const debug = false 50 | const systray = new SysTray({ menu, debug, copyDir: true }) 51 | const binName = ({ 52 | win32: `tray_windows${debug ? '' : '_release'}.exe`, 53 | darwin: `tray_darwin${debug ? '' : '_release'}`, 54 | linux: `tray_linux${debug ? '' : '_release'}` 55 | })[process.platform] 56 | await systray.ready() 57 | assert.strictEqual(systray.binPath, path.resolve(`${os.homedir()}/.cache/node-systray/`, pkg.version, binName)) 58 | await systray.onClick(async action => { 59 | if (action.seq_id === 0) { 60 | await systray.sendAction({ 61 | type: 'update-item', 62 | item: { 63 | ...(action.item), 64 | checked: !action.item.checked 65 | } 66 | }) 67 | } else if (action.seq_id === 2) { 68 | await systray.kill() 69 | } 70 | console.log('action', action) 71 | }) 72 | console.log('Exit the tray in 1000ms...') 73 | const exitInfo = new Promise<{ code: number | null; signal: string | null }>(resolve => systray.onExit((code, signal) => resolve({ code, signal }))) 74 | await new Promise((resolve, reject) => { 75 | setTimeout(async () => { 76 | await systray.kill(false) 77 | resolve() 78 | }, 1000) 79 | }) 80 | const { code, signal } = await exitInfo 81 | console.log('code', code, 'signal', signal) 82 | assert.strictEqual(code, 0) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ 6 | "esModuleInterop": true, 7 | "lib": ["es2015"], /* Specify library files to be included in the compilation: */ 8 | "allowJs": false, /* Allow javascript files to be compiled. */ 9 | "checkJs": false, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "removeComments": true, /* Do not emit comments to output. */ 17 | // "noEmit": true, /* Do not emit outputs. */ 18 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 20 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 21 | 22 | /* Strict Type-Checking Options */ 23 | "strict": true, /* Enable all strict type-checking options. */ 24 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 27 | // "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */ 28 | 29 | /* Additional Checks */ 30 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 31 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 32 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 33 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 34 | 35 | /* Module Resolution Options */ 36 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 37 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 38 | "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 39 | // "rootDirs": ["./", "../test"], /* List of root folders whose combined content represents the structure of the project at runtime. */ 40 | // "typeRoots": [], /* List of folders to include type definitions from. */ 41 | // "types": [], /* Type declaration files to be included in compilation. */ 42 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 43 | 44 | /* Source Map Options */ 45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 49 | 50 | /* Experimental Options */ 51 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 52 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 53 | }, 54 | "include": [ 55 | "index.ts" 56 | ], 57 | "exclude": [ 58 | "./.history", 59 | "./node_modules", 60 | "**/*.test.*" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd' or 'es2015'. */ 6 | "esModuleInterop": true, 7 | "lib": ["es2015"], /* Specify library files to be included in the compilation: */ 8 | "allowJs": false, /* Allow javascript files to be compiled. */ 9 | "checkJs": false, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "removeComments": true, /* Do not emit comments to output. */ 17 | // "noEmit": true, /* Do not emit outputs. */ 18 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 20 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 21 | 22 | /* Strict Type-Checking Options */ 23 | "strict": true, /* Enable all strict type-checking options. */ 24 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 27 | // "alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */ 28 | 29 | /* Additional Checks */ 30 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 31 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 32 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 33 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 34 | 35 | /* Module Resolution Options */ 36 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 37 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 38 | "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 39 | // "rootDirs": ["./", "../test"], /* List of root folders whose combined content represents the structure of the project at runtime. */ 40 | // "typeRoots": [], /* List of folders to include type definitions from. */ 41 | // "types": [], /* Type declaration files to be included in compilation. */ 42 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 43 | 44 | /* Source Map Options */ 45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 49 | 50 | /* Experimental Options */ 51 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 52 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 53 | }, 54 | "include": [ 55 | "index.ts", 56 | "index.test.ts", 57 | ".eslintrc.js", 58 | "menu.js", 59 | "example.mjs" 60 | ], 61 | "exclude": [ 62 | "./.history", 63 | "./node_modules", 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/quotes */ 2 | /* 3 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 4 | https://github.com/typescript-eslint/tslint-to-eslint-config 5 | 6 | It represents the closest reasonable ESLint configuration to this 7 | project's original TSLint configuration. 8 | 9 | We recommend eventually switching this configuration to extend from 10 | the recommended rulesets in typescript-eslint. 11 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 12 | 13 | Happy linting! 💖 14 | */ 15 | module.exports = { 16 | 'env': { 17 | 'node': true 18 | }, 19 | 'extends': [ 20 | 'prettier', 21 | 'prettier/@typescript-eslint' 22 | ], 23 | 'parser': '@typescript-eslint/parser', 24 | 'parserOptions': { 25 | 'project': 'tsconfig-lint.json', 26 | 'sourceType': 'module', 27 | 'extraFileExtensions': ['.mjs'] 28 | }, 29 | 'plugins': [ 30 | 'eslint-plugin-import', 31 | 'eslint-plugin-jsdoc', 32 | '@typescript-eslint', 33 | '@typescript-eslint/tslint' 34 | ], 35 | 'rules': { 36 | '@typescript-eslint/await-thenable': 'warn', 37 | '@typescript-eslint/consistent-type-assertions': 'warn', 38 | '@typescript-eslint/member-delimiter-style': [ 39 | 'warn', 40 | { 41 | 'multiline': { 42 | 'delimiter': 'none', 43 | 'requireLast': true 44 | }, 45 | 'singleline': { 46 | 'delimiter': 'semi', 47 | 'requireLast': false 48 | } 49 | } 50 | ], 51 | '@typescript-eslint/naming-convention': 'warn', 52 | '@typescript-eslint/no-empty-function': 'warn', 53 | '@typescript-eslint/no-floating-promises': 'warn', 54 | '@typescript-eslint/no-misused-new': 'warn', 55 | '@typescript-eslint/no-unnecessary-qualifier': 'warn', 56 | '@typescript-eslint/no-unnecessary-type-assertion': 'warn', 57 | '@typescript-eslint/no-unused-expressions': [ 58 | 'warn', 59 | { 60 | 'allowTaggedTemplates': true, 61 | 'allowShortCircuit': true 62 | } 63 | ], 64 | '@typescript-eslint/prefer-namespace-keyword': 'warn', 65 | '@typescript-eslint/quotes': [ 66 | 'warn', 67 | 'single', 68 | { 69 | 'avoidEscape': true 70 | } 71 | ], 72 | '@typescript-eslint/semi': [ 73 | 'warn', 74 | 'never' 75 | ], 76 | '@typescript-eslint/triple-slash-reference': [ 77 | 'warn', 78 | { 79 | 'path': 'always', 80 | 'types': 'prefer-import', 81 | 'lib': 'always' 82 | } 83 | ], 84 | '@typescript-eslint/tslint/config': [ 85 | 'error', 86 | { 87 | 'rules': { 88 | 'block-spacing': true, 89 | 'brace-style': true, 90 | 'handle-callback-err': true, 91 | 'no-duplicate-case': true, 92 | 'no-empty-character-class': true, 93 | 'no-ex-assign': true, 94 | 'no-extra-boolean-cast': true, 95 | 'no-inner-declarations': true, 96 | 'no-multi-spaces': true, 97 | 'no-unexpected-multiline': true, 98 | 'object-curly-spacing': true, 99 | 'strict-type-predicates': true, 100 | 'ter-arrow-spacing': true, 101 | 'ter-func-call-spacing': true, 102 | 'ter-indent': true, 103 | 'ter-no-irregular-whitespace': true, 104 | 'ter-no-sparse-arrays': true, 105 | 'valid-typeof': true, 106 | 'whitespace': true 107 | } 108 | } 109 | ], 110 | '@typescript-eslint/type-annotation-spacing': 'warn', 111 | '@typescript-eslint/unified-signatures': 'warn', 112 | 'brace-style': [ 113 | 'warn', 114 | '1tbs' 115 | ], 116 | 'comma-dangle': 'warn', 117 | 'curly': [ 118 | 'warn', 119 | 'multi-line' 120 | ], 121 | 'eol-last': 'warn', 122 | 'eqeqeq': [ 123 | 'warn', 124 | 'smart' 125 | ], 126 | 'id-blacklist': [ 127 | 'warn', 128 | 'any', 129 | 'Number', 130 | 'number', 131 | 'String', 132 | 'string', 133 | 'Boolean', 134 | 'boolean', 135 | 'Undefined', 136 | 'undefined' 137 | ], 138 | 'id-match': 'warn', 139 | 'import/no-deprecated': 'warn', 140 | 'jsdoc/check-alignment': 'warn', 141 | 'jsdoc/check-indentation': 'warn', 142 | 'jsdoc/newline-after-description': 'warn', 143 | 'new-parens': 'warn', 144 | 'no-caller': 'warn', 145 | 'no-cond-assign': 'warn', 146 | 'no-constant-condition': 'warn', 147 | 'no-control-regex': 'warn', 148 | 'no-duplicate-imports': 'warn', 149 | 'no-empty': 'warn', 150 | 'no-eval': 'warn', 151 | 'no-fallthrough': 'warn', 152 | 'no-invalid-regexp': 'warn', 153 | 'no-multiple-empty-lines': 'warn', 154 | // note you must disable the base rule as it can report incorrect errors 155 | "no-redeclare": "off", 156 | "@typescript-eslint/no-redeclare": ["error"], 157 | 'no-regex-spaces': 'warn', 158 | 'no-return-await': 'warn', 159 | 'no-throw-literal': 'warn', 160 | 'no-trailing-spaces': 'warn', 161 | 'no-underscore-dangle': 'warn', 162 | 'no-unused-labels': 'warn', 163 | 'no-var': 'warn', 164 | 'one-var': [ 165 | 'warn', 166 | 'never' 167 | ], 168 | 'radix': 'warn', 169 | 'space-before-function-paren': 'off', 170 | 'space-in-parens': [ 171 | 'warn', 172 | 'never' 173 | ], 174 | 'spaced-comment': [ 175 | 'warn', 176 | 'always', 177 | { 178 | 'markers': [ 179 | '/' 180 | ] 181 | } 182 | ], 183 | 'use-isnan': 'warn' 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-systray 2 | 3 | > SysTray2 library for nodejs using [systray-portable](https://github.com/felixhao28/systray-portable) (a portable version of [the go systray library](https://github.com/getlantern/systray)). 4 | 5 | 6 | ## Install 7 | ```sh 8 | npm i systray2 9 | # or 10 | yarn add systray2 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Simple Example 16 | 17 | This example shows how to display a menu with two menu items, and how to handle mouse click events and exit the tray process. 18 | 19 | ```ts 20 | import SysTray from 'systray2'; 21 | import os from 'os' 22 | 23 | const item = { 24 | title: 'Show Exit Button', 25 | tooltip: 'This menu item will toggle the display of the exit button.', 26 | // The "checked" property will create a check mark on the side of this menu item. 27 | // To dynamically update the display of the check mark, use the "sendAction" method, as shown below. 28 | // Note that "checked" is implemented by plain text in linux 29 | checked: false, 30 | enabled: true, 31 | } 32 | 33 | const itemExit = { 34 | title: 'Exit', 35 | tooltip: 'bb', 36 | checked: false, 37 | enabled: true, 38 | } 39 | 40 | // Simple menu example 41 | const systray = new SysTray({ 42 | menu: { 43 | // you should use .png icon on macOS/Linux, and .ico format on Windows 44 | icon: os.platform() === 'win32' ? './logo_s.ico' : './logo_s.png', 45 | // a template icon is a transparency mask that will appear to be dark in light mode and light in dark mode 46 | isTemplateIcon: os.platform() === 'darwin', 47 | title: '标题', 48 | tooltip: 'Tips', 49 | items: [ 50 | item, 51 | itemExit 52 | ] 53 | }, 54 | debug: false, 55 | copyDir: false // copy go tray binary to an outside directory, useful for packing tool like pkg. 56 | }) 57 | 58 | // The actual handling of the click events. 59 | // This is obviously important if you want your MenuItems to react to mouse clicks. 60 | systray.onClick(action => { 61 | if (action.item.title === "Exit") { 62 | systray.kill(false) 63 | } else { 64 | console.log("menu item clicked!") 65 | } 66 | }) 67 | 68 | // Systray.ready is a promise which resolves when the tray is ready. 69 | systray.ready().then(() => { 70 | console.log('systray started!') 71 | }).catch(err => { 72 | console.log('systray failed to start: ' + err.message) 73 | }) 74 | ``` 75 | 76 | ### Reactive Menu Example 77 | 78 | This example shows how to use checkbox-style menu items and menus with sub menu items. 79 | 80 | ```ts 81 | import SysTray from 'systray2'; 82 | import os from 'os' 83 | 84 | /** 85 | * Represents a user-defined clickable MenuItem. 86 | * `click` here is a custom property. You can name it whatever you want, but it should 87 | * be in consistent with the name used in the systray.onClick callback. 88 | * 89 | * You do not necessarily need this type. But in most cases, you will need some ways to 90 | * react to the mouse clicks. 91 | * 92 | * The actual code for handling click events can be found at the end of the code sample. 93 | * ``` 94 | * systray.onClick(action => { 95 | * if (action.item.click != null) { 96 | * action.item.click() 97 | * } 98 | * }) 99 | * ``` 100 | */ 101 | interface MenuItemClickable extends MenuItem { 102 | click?: () => void; 103 | items?: MenuItemClickable[]; 104 | } 105 | 106 | const item1: MenuItemClickable = { 107 | title: 'Show Exit Button', 108 | tooltip: 'This menu item will toggle the display of the exit button.', 109 | // The "checked" property will create a check mark on the side of this menu item. 110 | // To dynamically update the display of the check mark, use the "sendAction" method, as shown below. 111 | // Note that "checked" is implemented by plain text in linux 112 | checked: false, 113 | enabled: true, 114 | // click is not a standard property but a custom value 115 | click: () => { 116 | // change the state 117 | item1.checked = !item1.checked 118 | // and then send it to the background tray service. 119 | systray.sendAction({ 120 | type: 'update-item', 121 | item: item1, 122 | }) 123 | // toggle Exit 124 | itemExit.hidden = !itemExit.hidden 125 | systray.sendAction({ 126 | type: 'update-item', 127 | item: itemExit, 128 | }) 129 | } 130 | } 131 | const item2: MenuItemClickable = { 132 | title: 'Submenu Parent', 133 | tooltip: 'this is the parent menu', 134 | checked: false, 135 | enabled: true, 136 | hidden: false, 137 | // add a submenu item 138 | items: [{ 139 | title: 'Submenu', 140 | tooltip: 'this is a submenu item', 141 | checked: false, 142 | enabled: true, 143 | click: () => { 144 | // open the url 145 | console.log('open the url') 146 | } 147 | }] 148 | } 149 | const itemExit: MenuItemClickable = { 150 | title: 'Exit', 151 | tooltip: 'bb', 152 | checked: false, 153 | enabled: true, 154 | click: () => { 155 | systray.kill(false) 156 | } 157 | } 158 | const systray = new SysTray({ 159 | menu: { 160 | // you should use .png icon on macOS/Linux, and .ico format on Windows 161 | icon: os.platform() === 'win32' ? './logo_s.ico' : './logo_s.png', 162 | // a template icon is a transparency mask that will appear to be dark in light mode and light in dark mode 163 | isTemplateIcon: os.platform() === 'darwin', 164 | title: '标题', 165 | tooltip: 'Tips', 166 | items: [ 167 | item1, 168 | SysTray.separator, // SysTray.separator is equivalent to a MenuItem with "title" equals "" 169 | item2, 170 | itemExit 171 | ] 172 | }, 173 | debug: false, 174 | copyDir: false // copy go tray binary to an outside directory, useful for packing tool like pkg. 175 | }) 176 | 177 | // The actual handling of the click events. 178 | // This is obviously important if you want your MenuItems to react to mouse clicks. 179 | systray.onClick(action => { 180 | if (action.item.click != null) { 181 | action.item.click() 182 | } 183 | }) 184 | 185 | // Systray.ready is a promise which resolves when the tray is ready. 186 | systray.ready().then(() => { 187 | console.log('systray started!') 188 | }).catch(err => { 189 | console.log('systray failed to start: ' + err.message) 190 | }) 191 | 192 | ``` 193 | 194 | To integrate with packing tools like `webpack`, use something like `copy-webpack-plugin` to copy the desired `tray_*_release[.exe]` to the `traybin/` folder of the working directory. 195 | 196 | ## Known Issues 197 | 198 | Toggling `hiding` on a menu item with a sub-menu causes the sub-menu to disappear. 199 | 200 | ## License 201 | MIT 202 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable @typescript-eslint/naming-convention */ 3 | /* eslint-disable @typescript-eslint/tslint/config */ 4 | import * as child from 'child_process' 5 | import * as path from 'path' 6 | import * as os from 'os' 7 | import * as fs from 'fs-extra' 8 | import * as readline from 'readline' 9 | const pkg = require('./package.json') 10 | 11 | function _debug(msgType: string, ...msg: any[]) { 12 | console.log(msgType + ':' + msg.map(m => { 13 | let t = typeof (m) === 'string' ? m : JSON.stringify(m) 14 | const p = t.indexOf('"icon":') 15 | if (p >= 0) { 16 | const e = t.indexOf('"', p + 8) 17 | t = t.substring(0, p + 8) + '' + t.substring(e) 18 | } 19 | const limit = 500 20 | if (t.length > limit) { 21 | t = t.substring(0, limit / 2) + '...' + t.substring(t.length - limit / 2) 22 | } 23 | return t 24 | }).join(' ')) 25 | return 26 | } 27 | 28 | export interface MenuItem { 29 | title: string 30 | tooltip: string 31 | checked?: boolean 32 | enabled?: boolean 33 | hidden?: boolean 34 | items?: MenuItem[] 35 | icon?: string 36 | isTemplateIcon?: boolean 37 | } 38 | 39 | interface MenuItemEx extends MenuItem { 40 | __id: number 41 | items?: MenuItemEx[] 42 | } 43 | 44 | export interface Menu { 45 | icon: string 46 | title: string 47 | tooltip: string 48 | items: MenuItem[] 49 | isTemplateIcon?: boolean 50 | } 51 | 52 | export interface ClickEvent { 53 | type: 'clicked' 54 | item: MenuItem 55 | seq_id: number 56 | __id: number 57 | } 58 | 59 | export interface ReadyEvent { 60 | type: 'ready' 61 | } 62 | 63 | export type Event = ClickEvent | ReadyEvent 64 | 65 | export interface UpdateItemAction { 66 | type: 'update-item' 67 | item: MenuItem 68 | seq_id?: number 69 | } 70 | 71 | export interface UpdateMenuAction { 72 | type: 'update-menu' 73 | menu: Menu 74 | } 75 | 76 | export interface UpdateMenuAndItemAction { 77 | type: 'update-menu-and-item' 78 | menu: Menu 79 | item: MenuItem 80 | seq_id?: number 81 | } 82 | 83 | export interface ExitAction { 84 | type: 'exit' 85 | } 86 | 87 | export type Action = UpdateItemAction | UpdateMenuAction | UpdateMenuAndItemAction | ExitAction 88 | 89 | export interface Conf { 90 | menu: Menu 91 | debug?: boolean 92 | copyDir?: boolean | string 93 | } 94 | 95 | const getTrayBinPath = async (debug: boolean = false, copyDir: boolean | string = false) => { 96 | const binName = ({ 97 | win32: `tray_windows${debug ? '' : '_release'}.exe`, 98 | darwin: `tray_darwin${debug ? '' : '_release'}`, 99 | linux: `tray_linux${debug ? '' : '_release'}` 100 | })[process.platform] 101 | let binPath = path.join('.', 'traybin', binName) 102 | if (!await fs.pathExists(binPath)) { 103 | binPath = path.join(__dirname, 'traybin', binName) 104 | } 105 | if (copyDir) { 106 | copyDir = path.join(( 107 | typeof copyDir === 'string' 108 | ? copyDir 109 | : `${os.homedir()}/.cache/node-systray/`), pkg.version) 110 | 111 | const copyDistPath = path.join(copyDir, binName) 112 | try { 113 | await fs.stat(copyDistPath) 114 | } catch (error) { 115 | await fs.ensureDir(copyDir) 116 | await fs.copy(binPath, copyDistPath) 117 | } 118 | 119 | return copyDistPath 120 | } 121 | return binPath 122 | } 123 | const CHECK_STR = ' (√)' 124 | function updateCheckedInLinux(item: MenuItem) { 125 | if (process.platform !== 'linux') { 126 | return 127 | } 128 | if (item.checked) { 129 | item.title += CHECK_STR 130 | } else { 131 | item.title = (item.title || '').replace(RegExp(CHECK_STR + '$'), '') 132 | } 133 | if (item.items != null) { 134 | item.items.forEach(updateCheckedInLinux) 135 | } 136 | } 137 | 138 | async function resolveIcon(item: MenuItem | Menu) { 139 | let icon = item.icon 140 | if (icon != null) { 141 | if (await fs.pathExists(icon)) { 142 | item.icon = await loadIcon(icon) 143 | } 144 | } 145 | if (item.items != null) { 146 | await Promise.all(item.items.map(_ => resolveIcon(_))) 147 | } 148 | return item 149 | } 150 | 151 | function addInternalId(internalIdMap: Map, item: MenuItemEx, counter = {id: 1}) { 152 | const id = counter.id++ 153 | internalIdMap.set(id, item) 154 | if (item.items != null) { 155 | item.items.forEach(_ => addInternalId(internalIdMap, _, counter)) 156 | } 157 | item.__id = id 158 | } 159 | 160 | function itemTrimmer(item: MenuItem) { 161 | return { 162 | title: item.title, 163 | tooltip: item.tooltip, 164 | checked: item.checked, 165 | enabled: item.enabled === undefined ? true : item.enabled, 166 | hidden: item.hidden, 167 | items: item.items, 168 | icon: item.icon, 169 | isTemplateIcon: item.isTemplateIcon, 170 | __id: (item as MenuItemEx).__id 171 | } 172 | } 173 | 174 | function menuTrimmer(menu: Menu) { 175 | return { 176 | icon: menu.icon, 177 | title: menu.title, 178 | tooltip: menu.tooltip, 179 | items: menu.items.map(itemTrimmer), 180 | isTemplateIcon: menu.isTemplateIcon 181 | } 182 | } 183 | 184 | function actionTrimer(action: Action) { 185 | if (action.type === 'update-item') { 186 | return { 187 | type: action.type, 188 | item: itemTrimmer(action.item), 189 | seq_id: action.seq_id 190 | } 191 | } else if (action.type === 'update-menu') { 192 | return { 193 | type: action.type, 194 | menu: menuTrimmer(action.menu) 195 | } 196 | } else if (action.type === 'update-menu-and-item') { 197 | return { 198 | type: action.type, 199 | item: itemTrimmer(action.item), 200 | menu: menuTrimmer(action.menu), 201 | seq_id: action.seq_id 202 | } 203 | } else { 204 | return { 205 | type: action.type 206 | } 207 | } 208 | } 209 | 210 | async function loadIcon(fileName: string) { 211 | const buffer = await fs.readFile(fileName) 212 | return buffer.toString('base64') 213 | } 214 | 215 | export default class SysTray { 216 | static separator: MenuItem = { 217 | title: '', 218 | tooltip: '', 219 | enabled: true 220 | } 221 | protected _conf: Conf 222 | private _process: child.ChildProcess 223 | public get process(): child.ChildProcess { 224 | return this._process 225 | } 226 | protected _rl: readline.ReadLine 227 | protected _binPath: string 228 | private _ready: Promise 229 | private internalIdMap = new Map() 230 | 231 | constructor(conf: Conf) { 232 | this._conf = conf 233 | this._process = null! 234 | this._rl = null! 235 | this._binPath = null! 236 | this._ready = this.init() 237 | } 238 | 239 | private async init() { 240 | const conf = this._conf 241 | this._binPath = await getTrayBinPath(conf.debug, conf.copyDir) 242 | try { 243 | await fs.chmod(this._binPath, '+x') 244 | } catch (error) { 245 | // ignore 246 | } 247 | return new Promise(async (resolve, reject) => { 248 | try { 249 | this._process = child.spawn(this._binPath, [], { 250 | windowsHide: true 251 | }) 252 | this._process.on('error', reject) 253 | this._rl = readline.createInterface({ 254 | input: this._process.stdout! 255 | }) 256 | conf.menu.items.forEach(updateCheckedInLinux) 257 | let counter = {id: 1} 258 | conf.menu.items.forEach(_ => addInternalId(this.internalIdMap, _ as MenuItemEx, counter)) 259 | await resolveIcon(conf.menu) 260 | if (conf.debug) { 261 | this._rl.on('line', data => _debug('onLine', data)) 262 | } 263 | this.onReady(() => { 264 | this.writeLine(JSON.stringify(menuTrimmer(conf.menu))) 265 | resolve() 266 | }) 267 | } catch (error) { 268 | reject(error) 269 | } 270 | }) 271 | } 272 | 273 | ready() { 274 | return this._ready 275 | } 276 | 277 | onReady(listener: () => void) { 278 | this._rl.on('line', (line: string) => { 279 | const action: Event = JSON.parse(line) 280 | if (action.type === 'ready') { 281 | listener() 282 | if (this._conf.debug) { 283 | _debug('onReady', action) 284 | } 285 | } 286 | }) 287 | return this 288 | } 289 | 290 | async onClick(listener: (action: ClickEvent) => void) { 291 | await this.ready() 292 | this._rl.on('line', (line: string) => { 293 | const action: ClickEvent = JSON.parse(line) 294 | if (action.type === 'clicked') { 295 | const item = this.internalIdMap.get(action.__id)! 296 | action.item = Object.assign(item, action.item) 297 | if (this._conf.debug) { 298 | _debug('onClick', action) 299 | } 300 | listener(action) 301 | } 302 | }) 303 | return this 304 | } 305 | 306 | private writeLine(line: string) { 307 | if (line) { 308 | if (this._conf.debug) { 309 | _debug('writeLine', line + '\n', '=====') 310 | } 311 | this._process.stdin!.write(line.trim() + '\n') 312 | } 313 | return this 314 | } 315 | 316 | async sendAction(action: Action) { 317 | switch (action.type) { 318 | case 'update-item': 319 | updateCheckedInLinux(action.item) 320 | if (action.seq_id == null) { 321 | action.seq_id = -1 322 | } 323 | break 324 | case 'update-menu': 325 | action.menu = await resolveIcon(action.menu) as Menu 326 | action.menu.items.forEach(updateCheckedInLinux) 327 | break 328 | case 'update-menu-and-item': 329 | action.menu = await resolveIcon(action.menu) as Menu 330 | action.menu.items.forEach(updateCheckedInLinux) 331 | updateCheckedInLinux(action.item) 332 | if (action.seq_id == null) { 333 | action.seq_id = -1 334 | } 335 | break 336 | } 337 | if (this._conf.debug) { 338 | _debug('sendAction', action) 339 | } 340 | this.writeLine(JSON.stringify(actionTrimer(action))) 341 | return this 342 | } 343 | /** 344 | * Kill the systray process 345 | * 346 | * @param exitNode Exit current node process after systray process is killed, default is true 347 | */ 348 | async kill(exitNode = true) { 349 | return new Promise(async (resolve, reject) => { 350 | try { 351 | this.onExit(() => { 352 | resolve() 353 | if (exitNode) { 354 | process.exit(0) 355 | } 356 | }) 357 | await this.sendAction({ 358 | type: 'exit' 359 | }) 360 | // this._rl.close(); 361 | // this._process.kill(); 362 | } catch (error) { 363 | reject(error) 364 | } 365 | }) 366 | } 367 | 368 | onExit(listener: (code: number | null, signal: string | null) => void) { 369 | this._process.on('exit', listener) 370 | } 371 | 372 | onError(listener: (err: Error) => void) { 373 | this._process.on('error', err => { 374 | if (this._conf.debug) { 375 | _debug('onError', err, 'binPath', this.binPath) 376 | } 377 | listener(err) 378 | }) 379 | } 380 | 381 | get killed() { 382 | return this._process.killed 383 | } 384 | 385 | get binPath() { 386 | return this._binPath 387 | } 388 | } 389 | --------------------------------------------------------------------------------