Skip to content

Commit

Permalink
Finalization stuff to publish a package.
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbuchan committed Mar 21, 2019
1 parent 087df18 commit 66875ac
Show file tree
Hide file tree
Showing 15 changed files with 437 additions and 238 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode/
build/
node_modules/
*.log
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2019 Simon Buchan

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Windows Notification Icons

Because calling it the system tray [would be wrong](https://devblogs.microsoft.com/oldnewthing/20030910-00/?p=42583).

## Description

Provides fairly direct access to the Win32
[`Shell_NotifyIcon`](https://docs.microsoft.com/en-nz/windows/desktop/api/shellapi/nf-shellapi-shell_notifyiconw)
API, which allows adding an icon to the notification area, and
support for the related windows APIs to load icons and use context
(e.g. right-click) menus.

Prebuilt as N-API (64-bit only for now, sorry), meaning you
don't need Visual Studio or build tools installed on your
development machine or CI server.

Note that this is using the Win32 desktop APIs, so you don't
need to be running under the UWP (Windows Store) model,
and thus has odd limitations like requiring an icon in the
notification area to show a notification message, and not
supporting notification actions (e.g. buttons).

## Usage

The [type definitions](index.d.ts) have fuller jsdoc descriptions,
but as a basic overview:

```js
import { NotifyIcon, Icon, Menu } from "not-the-systray";

// Creates an empty (blank) icon in the notification area
// that does nothing.
// Unfortunately the Windows terminology I'm exposing here is
// pretty confusing, they have "Notification icons" that have
// icons and can show notifications that also have icons.
const emptyIcon = new NotifyIcon();
// Remove it!
emptyIcon.remove();

// Creates and adds an initialized icon, with a select callback.
const appIcon = new NotifyIcon({
icon: Icon.load("my-icon.ico", Icon.small), // Notify icons should use
tooltip: "My icon from nodejs",
onSelect({ target, rightButton, mouseX, mouseY }) {
// `this` is `target` is `appIcon`.
// `rightButton` is which mouse button it was selected with,
// `mouseX and `mouseX` are the screen co-ordinates of the click.
// If selected with the keyboard, these values will be simulated.

if (rightButton) {
handleMenu(mouseX, mouseY);
} else {
// some default action, or just show the same menu.
}
},
});

// Notifications should use the size `Icon.large`.
// Icons can be loaded ahead of time to catch errors earlier,
// and save a bit of time and memory.
const notificationIcon = Icon.load("notification-icon.ico", Icon.large);
// You can also use some built-in icons, for example:
const errorIcon = Icon.load(Icon.ids.error, Icon.large);

const toggleId = 1;
const notificationId = 2;
const submenuId = 3;

const menu = new Menu([
{ id: toggleId, text: "Toggle item", checked: true },
{ id: notificationId, text: "Show notification" },
{ separator: true },
{ text: "Sub-menu", items: [
{ id: submenuId, text: "Sub-menu item" },
] }
]);

function handleMenu(x, y) {
const id = menu.showSync(x, y);
switch (id) {
case null:
// user cancelled selection.
break;
case toggleId: {
// You can read the immediate properties (ie. not counting `items`)
// of any item by id.
const { checked } = menu.get(toggleId);
// You can update any properties of an item whenever you want, including
// `items`.
menu.update(toggleId, { checked: !checked });
break;
}
case notificationId:
// The win32 (non-UWP) model of notifications are as
// a property of a notification area icon, and can't
// be displayed without one (remember the old balloons?)
// Updating with a new notification will remove the
// previous one.
appIcon.update({
notification: {
icon: notificationIcon,
title: "Hello, world",
text: "A notification from nodejs",
},
});
break;
case submenuId:
// Ids work in submenus too.
console.log("Selected the submenu item");
break;
}
}
```
3 changes: 2 additions & 1 deletion binding.gyp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"targets": [
{
"target_name": "tray",
"target_name": "notify_icon",
"include_dirs": [
"src"
],
Expand All @@ -14,6 +14,7 @@
"src/menu-object.cc",
"src/notify-icon.cc",
"src/notify-icon-object.cc",
"src/reg-icon-stream.cc",
"src/parse_guid.cc",
"src/module.cc"
],
Expand Down
102 changes: 102 additions & 0 deletions icon-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
let native;
try {
native = require('./build/Release/notify_icon.node');
} catch (e) {
native = require('./build/Debug/notify_icon.node');
}

module.exports = { printIconStream };

// For debugging icon registration, see
// https://github.com/electron/electron/issues/2468#issuecomment-142684129
function printIconStream() {
/** @type Buffer */
const buffer = native.icon_stream;
// const headerSize = buffer.readUInt32LE(0);
// buffer.readUInt32LE(4); = 7
// buffer.readUInt16LE(8); = 1
// buffer.readUInt16LE(10); = 1
const itemCount = buffer.readUInt32LE(12);
const itemListOffset = buffer.readUInt32LE(16);

const itemSize = 1640;

const pathStart = 0;
const pathEnd = pathStart + 2 * 260;
const tipStart = pathEnd + 16;
const tipEnd = tipStart + 2 * 260; // 260 seems to be closer than the NOTIFYITEMDATA size of 128
const infoStart = tipEnd + 60;
const infoEnd = infoStart + 2 * 260;

console.log("header data = %s", buffer.slice(0, itemListOffset).toString("hex"));

for (let itemIndex = 0; itemIndex < itemCount; itemIndex++) {
const itemOffset = itemListOffset + itemIndex * itemSize;
const itemData = buffer.slice(itemOffset, itemOffset + itemSize);
console.group('Item %d/%d at 0x%s', itemIndex, itemCount, itemOffset.toString(16));

const [path, pathTrailing] = readUTF16(itemData, pathStart, pathEnd);
console.log("path = %O", stringRot(path, 13));
if (pathTrailing.some(b => b != 0)) {
console.log(" trailing = %s", pathTrailing.toString("hex"));
}

console.group("? = %s", itemData.slice(pathEnd, pathEnd + 12).toString("hex"));
console.log("? = %O", itemData.readInt32LE(pathEnd));
console.log("? = %O", itemData.readInt32LE(pathEnd + 4));
console.log("? = %O", itemData.readInt32LE(pathEnd + 8));
console.groupEnd();

console.log("last seen = %d-%d", itemData.readUInt16LE(pathEnd + 12), itemData.readUInt16LE(pathEnd + 14));

const [tip, tipTrailing] = readUTF16(itemData, tipStart, tipEnd);
console.log("tooltip = %O", stringRot(tip, 13));
if (tipTrailing.some(b => b != 0)) {
console.log(" trailing = %s", tipTrailing.toString("hex"));
}
console.log("? = %O", itemData.readInt32LE(tipEnd));
console.log("? = %O", itemData.readInt32LE(tipEnd + 4));
console.log("order? = %O", itemData.readInt32LE(tipEnd + 8));
console.log("? = %s", itemData.slice(tipEnd + 12, tipEnd + 40).toString("hex"));
console.log("? = %s", itemData.slice(tipEnd + 40, infoStart).toString("hex"));

const [info, infoTrailing] = readUTF16(itemData, infoStart, infoEnd);
console.log("info? = %O", stringRot(info, 13));
console.log("? = %s", infoTrailing.slice(0, 4).toString("hex"));
const [info2, info2Trailing] = readUTF16(infoTrailing, 4);
console.log("? = %O", stringRot(info2, 13));
if (info2Trailing.some(b => b != 0)) {
console.log(" trailing = %s", info2Trailing.toString("hex"));
}

console.log("? = %O", itemData.readUInt32LE(infoEnd));
console.groupEnd();
}

console.log("trailer data = %s", buffer.slice(itemListOffset + itemCount * itemSize).toString("hex"));
}

function stringRot(value, rot) {
const codes = Array(value.length);
for (let i = 0; i != value.length; i++) {
const code = value.charCodeAt(i);
if (65 <= code && code <= 90) {
codes[i] = (code - 65 + rot) % 26 + 65;
} else if (97 <= code && code <= 122) {
codes[i] = (code - 97 + rot) % 26 + 97;
} else {
codes[i] = code;
}
}
return String.fromCharCode(...codes);
}

function readUTF16(buffer, offset = 0, bufferEnd = buffer.length) {
let end = offset;
while (end < bufferEnd && buffer.readUInt16LE(end)) {
end += 2;
}
const value = buffer.slice(offset, end).toString('utf16le');
const trailing = buffer.slice(end + 2, bufferEnd);
return [value, trailing];
}
98 changes: 3 additions & 95 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,105 +1,13 @@
let native;
try {
native = require('./build/Release/tray.node');
native = require('./build/Release/notify_icon.node');
} catch (e) {
native = require('./build/Debug/tray.node');
native = require('./build/Debug/notify_icon.node');
}

const { NotifyIcon, Icon, Menu } = native;

module.exports = { NotifyIcon, Icon, Menu, printIconStream };

function printIconStream() {
/** @type Buffer */
const buffer = native.icon_stream;
// const headerSize = buffer.readUInt32LE(0);
// buffer.readUInt32LE(4); = 7
// buffer.readUInt16LE(8); = 1
// buffer.readUInt16LE(10); = 1
const itemCount = buffer.readUInt32LE(12);
const itemListOffset = buffer.readUInt32LE(16);

const itemSize = 1640;

const pathStart = 0;
const pathEnd = pathStart + 2 * 260;
const tipStart = pathEnd + 16;
const tipEnd = tipStart + 2 * 260; // 260 seems to be closer than the NOTIFYITEMDATA size of 128
const infoStart = tipEnd + 60;
const infoEnd = infoStart + 2 * 260;

console.log("header data = %s", buffer.slice(0, itemListOffset).toString("hex"));

for (let itemIndex = 0; itemIndex < itemCount; itemIndex++) {
const itemOffset = itemListOffset + itemIndex * itemSize;
const itemData = buffer.slice(itemOffset, itemOffset + itemSize);
console.group('Item %d/%d at 0x%s', itemIndex, itemCount, itemOffset.toString(16));

const [path, pathTrailing] = readUTF16(itemData, pathStart, pathEnd);
console.log("path = %O", stringRot(path, 13));
if (pathTrailing.some(b => b != 0)) {
console.log(" trailing = %s", pathTrailing.toString("hex"));
}

console.group("? = %s", itemData.slice(pathEnd, pathEnd + 12).toString("hex"));
console.log("? = %O", itemData.readInt32LE(pathEnd));
console.log("? = %O", itemData.readInt32LE(pathEnd + 4));
console.log("? = %O", itemData.readInt32LE(pathEnd + 8));
console.groupEnd();

console.log("last seen = %d-%d", itemData.readUInt16LE(pathEnd + 12), itemData.readUInt16LE(pathEnd + 14));

const [tip, tipTrailing] = readUTF16(itemData, tipStart, tipEnd);
console.log("tooltip = %O", stringRot(tip, 13));
if (tipTrailing.some(b => b != 0)) {
console.log(" trailing = %s", tipTrailing.toString("hex"));
}
console.log("? = %O", itemData.readInt32LE(tipEnd));
console.log("? = %O", itemData.readInt32LE(tipEnd + 4));
console.log("order? = %O", itemData.readInt32LE(tipEnd + 8));
console.log("? = %s", itemData.slice(tipEnd + 12, tipEnd + 40).toString("hex"));
console.log("? = %s", itemData.slice(tipEnd + 40, infoStart).toString("hex"));

const [info, infoTrailing] = readUTF16(itemData, infoStart, infoEnd);
console.log("info? = %O", stringRot(info, 13));
console.log("? = %s", infoTrailing.slice(0, 4).toString("hex"));
const [info2, info2Trailing] = readUTF16(infoTrailing, 4);
console.log("? = %O", stringRot(info2, 13));
if (info2Trailing.some(b => b != 0)) {
console.log(" trailing = %s", info2Trailing.toString("hex"));
}

console.log("? = %O", itemData.readUInt32LE(infoEnd));
console.groupEnd();
}

console.log("trailer data = %s", buffer.slice(itemListOffset + itemCount * itemSize).toString("hex"));
}

function stringRot(value, rot) {
const codes = Array(value.length);
for (let i = 0; i != value.length; i++) {
const code = value.charCodeAt(i);
if (65 <= code && code <= 90) {
codes[i] = (code - 65 + rot) % 26 + 65;
} else if (97 <= code && code <= 122) {
codes[i] = (code - 97 + rot) % 26 + 97;
} else {
codes[i] = code;
}
}
return String.fromCharCode(...codes);
}

function readUTF16(buffer, offset = 0, bufferEnd = buffer.length) {
let end = offset;
while (end < bufferEnd && buffer.readUInt16LE(end)) {
end += 2;
}
const value = buffer.slice(offset, end).toString('utf16le');
const trailing = buffer.slice(end + 2, bufferEnd);
return [value, trailing];
}
module.exports = { NotifyIcon, Icon, Menu };

Object.defineProperties(Menu, {
createTemplate: { value: createMenuTemplate, enumerable: true },
Expand Down
Loading

0 comments on commit 66875ac

Please sign in to comment.