├── .eggignore ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── deno.json ├── deno.lock ├── deps.ts ├── example ├── icon.ico ├── icon.png └── index.ts ├── mod.ts └── sample.png /.eggignore: -------------------------------------------------------------------------------- 1 | .git*/** 2 | .idea/** 3 | .vscode/** 4 | benchmarks/** 5 | eggs-debug.log 6 | bin 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | create: 5 | ref_type: 'tag' 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup repo 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Deno 15 | uses: maximousblk/setup-deno@v1 16 | with: 17 | deno-version: v1.20.x 18 | 19 | - name: Deploy to nest.land 20 | run: | 21 | deno install -Af --unstable https://x.nest.land/eggs@0.3.10/eggs.ts 22 | eggs link ${{ secrets.NESTAPIKEY }} 23 | eggs publish systray -Y --version $(git describe --tags $(git rev-list --tags --max-count=1)) --no-check 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | bin 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Robert Soriano 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno-systray 2 | 3 | [![nest.land](https://nest.land/badge.svg)](https://nest.land/package/systray) 4 | [![CI Status](https://img.shields.io/github/workflow/status/wobsoriano/deno-systray/Publish)](https://github.com/wobsoriano/deno-systray/actions) 5 | [![Tags](https://img.shields.io/github/release/wobsoriano/deno-systray)](https://github.com/wobsoriano/deno-systray/releases) 6 | [![License](https://img.shields.io/github/license/wobsoriano/deno-systray)](https://github.com/wobsoriano/deno-systray/blob/master/LICENSE) 7 | 8 | A cross-platform systray library for Deno using the [go systray library](https://github.com/getlantern/systray). 9 | 10 | 11 | 12 | ## Usage 13 | 14 | ```ts 15 | import SysTray from "https://deno.land/x/systray/mod.ts"; 16 | 17 | const Item1 = { 18 | title: 'Item 1', 19 | tooltip: 'The first item', 20 | // checked is implemented by plain text in linux 21 | checked: true, 22 | enabled: true, 23 | // click is not a standard property but a custom value 24 | click: () => { 25 | Item1.checked = !Item1.checked 26 | systray.sendAction({ 27 | type: 'update-item', 28 | item: Item1, 29 | }) 30 | } 31 | } 32 | 33 | const Item2 = { 34 | title: 'Item 2', 35 | tooltip: 'The second item', 36 | checked: false, 37 | enabled: true, 38 | // add a submenu item 39 | items: [{ 40 | title: 'Submenu', 41 | tooltip: 'this is a submenu item', 42 | checked: false, 43 | enabled: true, 44 | click: () => { 45 | // open the url 46 | console.log('open the url') 47 | } 48 | }] 49 | } 50 | 51 | const ItemExit = { 52 | title: 'Exit', 53 | tooltip: 'Exit the menu', 54 | checked: false, 55 | enabled: true, 56 | click: () => { 57 | systray.kill() 58 | } 59 | } 60 | 61 | const systray = new SysTray({ 62 | menu: { 63 | // Use .png icon in macOS/Linux and .ico format in windows 64 | icon: Deno.build.os === 'windows' ? './icon.ico' : './icon.png', 65 | // A template icon is a transparency mask that will appear to be dark in light mode and light in dark mode 66 | isTemplateIcon: Deno.build.os === 'darwin', 67 | title: "Title", 68 | tooltip: "Tooltip", 69 | items: [ 70 | Item1, 71 | Item2, 72 | SysTray.separator, // SysTray.separator is equivalent to a MenuItem with "title" equals "" 73 | ItemExit 74 | ], 75 | }, 76 | debug: true, // log actions 77 | directory: 'bin' // cache directory of binary package 78 | }); 79 | 80 | systray.on('click', (action) => { 81 | if (action.item.click) { 82 | action.item.click(); 83 | } 84 | }); 85 | 86 | systray.on('ready', () => { 87 | console.log('tray started!'); 88 | }); 89 | 90 | systray.on('exit', () => { 91 | console.log('exited'); 92 | }); 93 | 94 | systray.on('error', (error) => { 95 | console.log(error); 96 | }); 97 | ``` 98 | 99 | ## Try the example app! 100 | 101 | ```bash 102 | $ deno run -A https://deno.land/x/systray/example/index.ts 103 | ``` 104 | 105 | View [platform notes](https://github.com/getlantern/systray#platform-notes). 106 | 107 | ## Credits 108 | 109 | - https://github.com/getlantern/systray 110 | - https://github.com/zaaack/systray-portable 111 | - https://github.com/felixhao28/node-systray 112 | 113 | ## License 114 | 115 | MIT 116 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "example": "deno run --allow-run --allow-env --allow-read --allow-write --allow-net example/index.ts" 4 | }, 5 | "lint": { 6 | "files": { 7 | "include": ["mod.ts", "deps.ts", "example/"] 8 | } 9 | }, 10 | "fmt": { 11 | "files": { 12 | "include": ["mod.ts", "deps.ts", "example/"] 13 | }, 14 | "options": { 15 | "singleQuote": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/std@0.106.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", 5 | "https://deno.land/std@0.106.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", 6 | "https://deno.land/std@0.106.0/path/posix.ts": "b81974c768d298f8dcd2c720229639b3803ca4a241fa9a355c762fa2bc5ef0c1", 7 | "https://deno.land/std@0.201.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", 8 | "https://deno.land/std@0.201.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", 9 | "https://deno.land/std@0.201.0/bytes/bytes_list.ts": "ecf5098c230b793970f43c06e8f30d70b937c031658365aeb3de9a8ae4d406a3", 10 | "https://deno.land/std@0.201.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", 11 | "https://deno.land/std@0.201.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", 12 | "https://deno.land/std@0.201.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d", 13 | "https://deno.land/std@0.201.0/io/buf_reader.ts": "0bd8ad26255945b5f418940db23db03bee0c160dbb5ae4627e2c0be3b361df6a", 14 | "https://deno.land/std@0.201.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd", 15 | "https://deno.land/std@0.201.0/io/buffer.ts": "4d6883daeb2e698579c4064170515683d69f40f3de019bfe46c5cf31e74ae793", 16 | "https://deno.land/std@0.201.0/io/copy_n.ts": "c055296297b9d4897d90d1ac056b072dc02614e60c67f438e23fbce052ea4c69", 17 | "https://deno.land/std@0.201.0/io/limited_reader.ts": "1976bb087e6aab06ebd6bd511bc801da9afeda31dbee2353dfd6ec44f1402324", 18 | "https://deno.land/std@0.201.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b", 19 | "https://deno.land/std@0.201.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271", 20 | "https://deno.land/std@0.201.0/io/read_delim.ts": "f3aac4e09b14cbe397612e7d490f1a043edfad18865ad0b277eb144157f676fc", 21 | "https://deno.land/std@0.201.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f", 22 | "https://deno.land/std@0.201.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc", 23 | "https://deno.land/std@0.201.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e", 24 | "https://deno.land/std@0.201.0/io/read_range.ts": "46a2263d0f8369b6d9abb0b25d99ceb65ff08d621fc57bcc53832e6979295043", 25 | "https://deno.land/std@0.201.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20", 26 | "https://deno.land/std@0.201.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce", 27 | "https://deno.land/std@0.201.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7", 28 | "https://deno.land/std@0.201.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f", 29 | "https://deno.land/std@0.201.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e", 30 | "https://deno.land/std@0.69.0/encoding/base64.ts": "79c67b8a9d911eff89cafd84749587a2851a7bb3bed4e952f6cf4c2ff97ae120", 31 | "https://deno.land/std@0.69.0/encoding/hex.ts": "8dc1489e66b22fec26950c7a55391b12be0d9f9a80cbc7b422700cdff85cd2f7", 32 | "https://deno.land/std@0.69.0/encoding/utf8.ts": "8654fa820aa69a37ec5eb11908e20b39d056c9bf1c23ab294303ff467f3d50a1", 33 | "https://deno.land/std@0.69.0/fmt/colors.ts": "ce9375edade12ca09c257743f223fc469d72578d0c74a55cbf1a34f6198ce3df", 34 | "https://deno.land/std@0.69.0/hash/_wasm/hash.ts": "005f64c4d9343ecbc91e0da9ae5e800f146c20930ad829bbb872c5c06bd89c5f", 35 | "https://deno.land/std@0.69.0/hash/_wasm/wasm.js": "10c91f7551443bd49b8ced10df7d5c2ce9a447d224eccc6d51f77730e074cdfe", 36 | "https://deno.land/std@0.69.0/hash/mod.ts": "e764a6a9ab2f5519a97f928e17cc13d984e3dd5c7f742ff9c1c8fb3114790f0c", 37 | "https://deno.land/std@0.97.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", 38 | "https://deno.land/std@0.97.0/_util/os.ts": "e282950a0eaa96760c0cf11e7463e66babd15ec9157d4c9ed49cc0925686f6a7", 39 | "https://deno.land/std@0.97.0/encoding/base64.ts": "eecae390f1f1d1cae6f6c6d732ede5276bf4b9cd29b1d281678c054dc5cc009e", 40 | "https://deno.land/std@0.97.0/encoding/hex.ts": "f952e0727bddb3b2fd2e6889d104eacbd62e92091f540ebd6459317a61932d9b", 41 | "https://deno.land/std@0.97.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b", 42 | "https://deno.land/std@0.97.0/fs/ensure_dir.ts": "b7c103dc41a3d1dbbb522bf183c519c37065fdc234831a4a0f7d671b1ed5fea7", 43 | "https://deno.land/std@0.97.0/fs/exists.ts": "b0d2e31654819cc2a8d37df45d6b14686c0cc1d802e9ff09e902a63e98b85a00", 44 | "https://deno.land/std@0.97.0/hash/_wasm/hash.ts": "cb6ad1ab429f8ac9d6eae48f3286e08236d662e1a2e5cfd681ba1c0f17375895", 45 | "https://deno.land/std@0.97.0/hash/_wasm/wasm.js": "94b1b997ae6fb4e6d2156bcea8f79cfcd1e512a91252b08800a92071e5e84e1a", 46 | "https://deno.land/std@0.97.0/hash/mod.ts": "5d032bd34186cda2f8d17fc122d621430953a6030d4b3f11172004715e3e2441", 47 | "https://deno.land/std@0.97.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", 48 | "https://deno.land/std@0.97.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", 49 | "https://deno.land/std@0.97.0/path/_util.ts": "2e06a3b9e79beaf62687196bd4b60a4c391d862cfa007a20fc3a39f778ba073b", 50 | "https://deno.land/std@0.97.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a", 51 | "https://deno.land/std@0.97.0/path/glob.ts": "314ad9ff263b895795208cdd4d5e35a44618ca3c6dd155e226fb15d065008652", 52 | "https://deno.land/std@0.97.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", 53 | "https://deno.land/std@0.97.0/path/posix.ts": "f56c3c99feb47f30a40ce9d252ef6f00296fa7c0fcb6dd81211bdb3b8b99ca3b", 54 | "https://deno.land/std@0.97.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", 55 | "https://deno.land/std@0.97.0/path/win32.ts": "77f7b3604e0de40f3a7c698e8a79e7f601dc187035a1c21cb1e596666ce112f8", 56 | "https://deno.land/x/cache@0.2.13/cache.ts": "4005aad54fb9aac9ff02526ffa798032e57f2d7966905fdeb7949263b1c95f2f", 57 | "https://deno.land/x/cache@0.2.13/deps.ts": "6f14e76a1a09f329e3f3830c6e72bd10b53a89a75769d5ea886e5d8603e503e6", 58 | "https://deno.land/x/cache@0.2.13/directories.ts": "ef48531cab3f827252e248596d15cede0de179a2fb15392ae24cf8034519994f", 59 | "https://deno.land/x/cache@0.2.13/file.ts": "5abe7d80c6ac594c98e66eb4262962139f48cd9c49dbe2a77e9608760508a09a", 60 | "https://deno.land/x/cache@0.2.13/file_fetcher.ts": "5c793cc83a5b9377679ec313b2a2321e51bf7ed15380fa82d387f1cdef3b924f", 61 | "https://deno.land/x/cache@0.2.13/helpers.ts": "d1545d6432277b7a0b5ea254d1c51d572b6452a8eadd9faa7ad9c5586a1725c4", 62 | "https://deno.land/x/cache@0.2.13/mod.ts": "3188250d3a013ef6c9eb060e5284cf729083af7944a29e60bb3d8597dd20ebcd", 63 | "https://deno.land/x/debug@0.2.0/colors.ts": "a05af5a6bc4fabb79db34ca215f177f010eafe09a3cd2278e142a122bfab5585", 64 | "https://deno.land/x/debug@0.2.0/debug.ts": "6378213af1826e6ad870d8ffb075336ce64ea33b472ea952a38fcd6b68ee54ec", 65 | "https://deno.land/x/debug@0.2.0/deps.ts": "c00793c24e4d0a15c796c7ef3b9a9a3ac4ea06cad9ef7c68bcb538310e262ea0", 66 | "https://deno.land/x/debug@0.2.0/format.ts": "e5c81071cdb2e529b2dc3aa379428217a49af45bf59b6511812e2bba9023f1bc", 67 | "https://deno.land/x/debug@0.2.0/mod.ts": "32e961550c7358a88a9bb2606ac70eeb168305298f5ae1e9536018d8f0c54118", 68 | "https://deno.land/x/event@2.0.0/mod.ts": "8eb8bc72d29b332cc9db426f704c7606436077b4bb12e968590045d3460fbe2e", 69 | "https://deno.land/x/open@v0.0.5/index.ts": "387293f5063d620137d9ba87fa4a9aece5ac435ca9f5bf5e3f0999634f68e294" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | cache as downloadAndCache, 3 | configure as configureCache, 4 | } from 'https://deno.land/x/cache@0.2.13/mod.ts'; 5 | export { readLines } from 'https://deno.land/std@0.201.0/io/mod.ts'; 6 | export { EventEmitter } from 'https://deno.land/x/event@2.0.0/mod.ts'; 7 | export { encode as base64Encode } from 'https://deno.land/std@0.201.0/encoding/base64.ts'; 8 | export { debug, withoutEnv } from 'https://deno.land/x/debug@0.2.0/mod.ts'; 9 | -------------------------------------------------------------------------------- /example/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/deno-systray/7a13a756b57c5f8daf0812228b7b17cde6542ca1/example/icon.ico -------------------------------------------------------------------------------- /example/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/deno-systray/7a13a756b57c5f8daf0812228b7b17cde6542ca1/example/icon.png -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { open } from 'https://deno.land/x/open@v0.0.5/index.ts'; 2 | import { downloadAndCache } from '../deps.ts'; 3 | import SysTray, { Menu, MenuItem } from '../mod.ts'; 4 | 5 | async function getIconUrl() { 6 | const { os } = Deno.build; 7 | const iconUrl = 8 | 'https://raw.githubusercontent.com/wobsoriano/deno-systray/master/example'; 9 | 10 | let iconName; 11 | 12 | switch (os) { 13 | case 'windows': 14 | iconName = `${iconUrl}/icon.ico`; 15 | break; 16 | case 'darwin': 17 | case 'linux': 18 | iconName = `${iconUrl}/icon.png`; 19 | break; 20 | default: 21 | throw new Error(`Unsupported operating system: ${os}`); 22 | } 23 | 24 | const icon = (await downloadAndCache(iconName)).path; 25 | 26 | return icon; 27 | } 28 | 29 | const icon = await getIconUrl(); 30 | 31 | interface MenuItemClickable extends MenuItem { 32 | click?: () => void; 33 | items?: MenuItemClickable[]; 34 | } 35 | 36 | interface CustomMenu extends Menu { 37 | items: MenuItemClickable[]; 38 | } 39 | 40 | const menu: CustomMenu = { 41 | icon, 42 | isTemplateIcon: Deno.build.os === 'darwin', 43 | title: 'Title', 44 | tooltip: 'Tooltip', 45 | items: [ 46 | { 47 | title: 'Item 1', 48 | tooltip: 'the first item', 49 | checked: true, 50 | enabled: true, 51 | click() { 52 | const item1Idx = 0; 53 | const item2Idx = 1; 54 | menu.items[item1Idx].checked = !menu.items[item1Idx].checked; 55 | menu.items[item2Idx].checked = !menu.items[item1Idx].checked; 56 | systray.sendAction({ 57 | type: 'update-item', 58 | item: menu.items[item1Idx], 59 | seq_id: item1Idx, 60 | }); 61 | systray.sendAction({ 62 | type: 'update-item', 63 | item: menu.items[item2Idx], 64 | seq_id: item2Idx, 65 | }); 66 | }, 67 | }, 68 | { 69 | 'title': 'Item 2', 70 | 'tooltip': 'the second item', 71 | 'checked': false, 72 | 'enabled': true, 73 | click() { 74 | const item1Idx = 0; 75 | const item2Idx = 1; 76 | menu.items[item2Idx].checked = !menu.items[item2Idx].checked; 77 | menu.items[item1Idx].checked = !menu.items[item2Idx].checked; 78 | systray.sendAction({ 79 | type: 'update-item', 80 | item: menu.items[item1Idx], 81 | seq_id: item1Idx, 82 | }); 83 | systray.sendAction({ 84 | type: 'update-item', 85 | item: menu.items[item2Idx], 86 | seq_id: item2Idx, 87 | }); 88 | }, 89 | }, 90 | SysTray.separator, 91 | { 92 | title: 'GitHub', 93 | tooltip: 'Go to repository', 94 | checked: false, 95 | click() { 96 | open('https://github.com/wobsoriano/deno-systray'); 97 | }, 98 | }, 99 | { 100 | 'title': 'Item with submenu', 101 | 'tooltip': 'submenu', 102 | 'checked': false, 103 | 'enabled': true, 104 | 'items': [{ 105 | 'title': 'submenu 1', 106 | 'tooltip': 'this is submenu 1', 107 | 'checked': true, 108 | 'enabled': true, 109 | }, { 110 | 'title': 'submenu 2', 111 | 'tooltip': 'this is submenu 2', 112 | 'checked': true, 113 | 'enabled': true, 114 | }], 115 | }, 116 | { 117 | title: 'Exit', 118 | tooltip: 'Exit the tray menu', 119 | checked: false, 120 | enabled: true, 121 | click() { 122 | systray.kill(); 123 | }, 124 | }, 125 | ], 126 | }; 127 | 128 | const systray = new SysTray({ 129 | menu, 130 | debug: true, 131 | directory: 'bin', 132 | }); 133 | 134 | systray.on('click', (action) => { 135 | if ((action.item as MenuItemClickable).click) { 136 | (action.item as MenuItemClickable).click!(); 137 | } 138 | }); 139 | 140 | systray.on('exit', (d) => { 141 | console.log(d); 142 | }); 143 | 144 | systray.on('error', () => { 145 | console.log(); 146 | }); 147 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | base64Encode, 3 | configureCache, 4 | debug, 5 | downloadAndCache, 6 | EventEmitter, 7 | readLines, 8 | withoutEnv, 9 | } from './deps.ts'; 10 | 11 | const version = Deno.env.get('TRAY_VERSION') ?? 'v0.2.0'; 12 | const url = Deno.env.get('TRAY_URL') ?? 13 | `https://github.com/wobsoriano/systray-portable/releases/download/${version}`; 14 | 15 | const debugName = 'systray'; 16 | const log = debug(debugName); 17 | 18 | export interface MenuItem { 19 | title: string; 20 | tooltip: string; 21 | checked?: boolean; 22 | enabled?: boolean; 23 | hidden?: boolean; 24 | items?: MenuItem[]; 25 | icon?: string; 26 | isTemplateIcon?: boolean; 27 | } 28 | 29 | interface MenuItemEx extends MenuItem { 30 | __id: number; 31 | items?: MenuItemEx[]; 32 | } 33 | 34 | export interface Menu { 35 | icon: string; 36 | title: string; 37 | tooltip: string; 38 | items: MenuItem[]; 39 | isTemplateIcon?: boolean; 40 | } 41 | 42 | export interface ClickEvent { 43 | type: 'clicked'; 44 | item: MenuItem; 45 | seq_id: number; 46 | __id: number; 47 | } 48 | 49 | export interface ReadyEvent { 50 | type: 'ready'; 51 | } 52 | 53 | export type Event = ClickEvent | ReadyEvent; 54 | 55 | export interface UpdateItemAction { 56 | type: 'update-item'; 57 | item: MenuItem; 58 | seq_id?: number; 59 | } 60 | 61 | export interface UpdateMenuAction { 62 | type: 'update-menu'; 63 | menu: Menu; 64 | } 65 | 66 | export interface UpdateMenuAndItemAction { 67 | type: 'update-menu-and-item'; 68 | menu: Menu; 69 | item: MenuItem; 70 | seq_id?: number; 71 | } 72 | 73 | export interface ExitAction { 74 | type: 'exit'; 75 | } 76 | 77 | export type Action = 78 | | UpdateItemAction 79 | | UpdateMenuAction 80 | | UpdateMenuAndItemAction 81 | | ExitAction; 82 | 83 | export interface Conf { 84 | menu: Menu; 85 | debug?: boolean; 86 | directory?: string | undefined; 87 | } 88 | 89 | const CHECK_STR = ' (√)'; 90 | function updateCheckedInLinux(item: MenuItem) { 91 | if (Deno.build.os !== 'linux') { 92 | return; 93 | } 94 | if (item.checked) { 95 | item.title += CHECK_STR; 96 | } else { 97 | item.title = (item.title || '').replace(RegExp(CHECK_STR + '$'), ''); 98 | } 99 | if (item.items != null) { 100 | item.items.forEach(updateCheckedInLinux); 101 | } 102 | } 103 | 104 | async function loadIcon(fileName: string) { 105 | const bytes = await Deno.readFile(fileName); 106 | return base64Encode(bytes); 107 | } 108 | 109 | async function resolveIcon(item: MenuItem | Menu) { 110 | const icon = item.icon; 111 | if (icon) { 112 | try { 113 | item.icon = await loadIcon(icon); 114 | } catch (_e) { 115 | // Image not found 116 | } 117 | } 118 | if (item.items) { 119 | await Promise.all(item.items.map((_) => resolveIcon(_))); 120 | } 121 | return item; 122 | } 123 | 124 | function addInternalId( 125 | internalIdMap: Map, 126 | item: MenuItemEx, 127 | counter = { id: 1 }, 128 | ) { 129 | const id = counter.id++; 130 | internalIdMap.set(id, item); 131 | if (item.items != null) { 132 | item.items.forEach((_) => addInternalId(internalIdMap, _, counter)); 133 | } 134 | item.__id = id; 135 | } 136 | 137 | function itemTrimmer(item: MenuItem) { 138 | return { 139 | title: item.title, 140 | tooltip: item.tooltip, 141 | checked: item.checked, 142 | enabled: item.enabled === undefined ? true : item.enabled, 143 | hidden: item.hidden, 144 | items: item.items, 145 | icon: item.icon, 146 | isTemplateIcon: item.isTemplateIcon, 147 | __id: (item as MenuItemEx).__id, 148 | }; 149 | } 150 | 151 | function menuTrimmer(menu: Menu) { 152 | return { 153 | icon: menu.icon, 154 | title: menu.title, 155 | tooltip: menu.tooltip, 156 | items: menu.items.map(itemTrimmer), 157 | isTemplateIcon: menu.isTemplateIcon, 158 | }; 159 | } 160 | 161 | function actionTrimer(action: Action) { 162 | if (action.type === 'update-item') { 163 | return { 164 | type: action.type, 165 | item: itemTrimmer(action.item), 166 | seq_id: action.seq_id, 167 | }; 168 | } else if (action.type === 'update-menu') { 169 | return { 170 | type: action.type, 171 | menu: menuTrimmer(action.menu), 172 | }; 173 | } else if (action.type === 'update-menu-and-item') { 174 | return { 175 | type: action.type, 176 | item: itemTrimmer(action.item), 177 | menu: menuTrimmer(action.menu), 178 | seq_id: action.seq_id, 179 | }; 180 | } else { 181 | return { 182 | type: action.type, 183 | }; 184 | } 185 | } 186 | 187 | const getTrayPath = async () => { 188 | let binName: string; 189 | const { arch, os } = Deno.build; 190 | 191 | switch (os) { 192 | case 'windows': 193 | binName = arch === 'x86_64' 194 | ? `${url}/tray_windows_amd64.exe` 195 | : `${url}/tray_windows_386.exe`; 196 | break; 197 | case 'darwin': 198 | binName = arch === 'x86_64' 199 | ? `${url}/tray_darwin_amd64` 200 | : `${url}/tray_darwin_arm64`; 201 | break; 202 | case 'linux': 203 | binName = arch === 'x86_64' 204 | ? `${url}/tray_linux_amd64` 205 | : `${url}/tray_linux_arm64`; 206 | break; 207 | default: 208 | throw new Error('Unsupported OS for tray application'); 209 | } 210 | 211 | const file = await downloadAndCache(binName); 212 | return file.path; 213 | }; 214 | 215 | type Events = { 216 | data: [string]; 217 | error: [string]; 218 | exit: [Deno.ProcessStatus]; 219 | click: [ClickEvent]; 220 | ready: []; 221 | }; 222 | 223 | export default class SysTray extends EventEmitter { 224 | static separator: MenuItem = { 225 | title: '', 226 | tooltip: '', 227 | enabled: true, 228 | }; 229 | protected _conf: Conf; 230 | private _process: Deno.Process; 231 | public get process(): Deno.Process { 232 | return this._process; 233 | } 234 | protected _binPath: string; 235 | private _ready: Promise; 236 | private internalIdMap = new Map(); 237 | 238 | constructor(conf: Conf) { 239 | super(); 240 | this._conf = conf; 241 | this._process = null!; 242 | this._binPath = null!; 243 | 244 | if (this._conf.debug) { 245 | withoutEnv(debugName); 246 | } 247 | 248 | if (this._conf.directory) { 249 | configureCache({ 250 | directory: this._conf.directory, 251 | }); 252 | } 253 | 254 | this._ready = this.init(); 255 | } 256 | 257 | private async run(...cmd: string[]) { 258 | this._process = Deno.run({ 259 | cmd, 260 | stdin: 'piped', 261 | stderr: 'piped', 262 | stdout: 'piped', 263 | }); 264 | for await (const line of readLines(this._process.stdout!)) { 265 | if (line.trim()) this.emit('data', line); 266 | } 267 | for await (const line of readLines(this._process.stderr!)) { 268 | if (line.trim()) { 269 | if (this._conf.debug) { 270 | log('onError', line, 'binPath', this.binPath); 271 | } 272 | this.emit('error', line); 273 | } 274 | } 275 | const status = await this._process.status(); 276 | this.emit('exit', status); 277 | this._process.close(); 278 | } 279 | 280 | private async init() { 281 | const conf = this._conf; 282 | try { 283 | this._binPath = await getTrayPath(); 284 | await Deno.chmod(this._binPath, 0o755); 285 | } catch (_error) { 286 | // This API currently throws on Windows 287 | } 288 | 289 | try { 290 | this.run(this._binPath); 291 | 292 | conf.menu.items.forEach(updateCheckedInLinux); 293 | const counter = { id: 1 }; 294 | conf.menu.items.forEach((_) => 295 | addInternalId(this.internalIdMap, _ as MenuItemEx, counter) 296 | ); 297 | await resolveIcon(conf.menu); 298 | 299 | this.once('ready', () => { 300 | this.writeLine(JSON.stringify(menuTrimmer(conf.menu))); 301 | }); 302 | 303 | this.on('data', (line: string) => { 304 | const action: Event = JSON.parse(line); 305 | if (action.type === 'clicked') { 306 | const item = this.internalIdMap.get(action.__id)!; 307 | action.item = Object.assign(item, action.item); 308 | if (this._conf.debug) { 309 | log('%s, %o', 'onClick', action); 310 | } 311 | this.emit('click', action); 312 | } else if (action.type === 'ready') { 313 | if (this._conf.debug) { 314 | log('%s %o', 'onReady', action); 315 | } 316 | this.emit('ready'); 317 | } 318 | }); 319 | } catch (error) { 320 | throw error; 321 | } 322 | } 323 | 324 | ready() { 325 | return this._ready; 326 | } 327 | 328 | private writeLine(line: string) { 329 | if (line) { 330 | if (this._conf.debug) { 331 | log('%s %o', 'writeLine', line + '\n', '====='); 332 | } 333 | const encoded = new TextEncoder().encode(`${line.trim()}\n`); 334 | this._process.stdin!.write(encoded); 335 | } 336 | } 337 | 338 | async sendAction(action: Action) { 339 | switch (action.type) { 340 | case 'update-item': 341 | updateCheckedInLinux(action.item); 342 | if (action.seq_id == null) { 343 | action.seq_id = -1; 344 | } 345 | break; 346 | case 'update-menu': 347 | action.menu = await resolveIcon(action.menu) as Menu; 348 | action.menu.items.forEach(updateCheckedInLinux); 349 | break; 350 | case 'update-menu-and-item': 351 | action.menu = await resolveIcon(action.menu) as Menu; 352 | action.menu.items.forEach(updateCheckedInLinux); 353 | updateCheckedInLinux(action.item); 354 | if (action.seq_id == null) { 355 | action.seq_id = -1; 356 | } 357 | break; 358 | } 359 | if (this._conf.debug) { 360 | log('%s %o', 'sendAction', action); 361 | } 362 | this.writeLine(JSON.stringify(actionTrimer(action))); 363 | return this; 364 | } 365 | 366 | /** 367 | * Kill the systray process 368 | * @param exitNode Exit current node process after systray process is killed, default is true 369 | */ 370 | kill(exitNode = true) { 371 | this.once('exit', () => { 372 | if (exitNode) { 373 | Deno.exit(); 374 | } 375 | }); 376 | 377 | this.sendAction({ 378 | type: 'exit', 379 | }); 380 | } 381 | 382 | get binPath() { 383 | return this._binPath; 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/deno-systray/7a13a756b57c5f8daf0812228b7b17cde6542ca1/sample.png --------------------------------------------------------------------------------