├── .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 | [](https://nest.land/package/systray)
4 | [](https://github.com/wobsoriano/deno-systray/actions)
5 | [](https://github.com/wobsoriano/deno-systray/releases)
6 | [](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
--------------------------------------------------------------------------------