From 80df9fdcc3cff7722e345db771fad184e219c320 Mon Sep 17 00:00:00 2001 From: Mikael Brevik Date: Fri, 9 Oct 2026 21:45:57 +0200 Subject: [PATCH] fix: converts wsl icon paths for windows toaster --- CHANGELOG.md | 1 + README.md | 2 ++ lib/utils.js | 30 ++++++++++++++++++++----- test/utils.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3486cff..1d17639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Features: Fixes: +- WSL: converts `icon` paths on Windows drives (`/mnt/c/...`) and skips other icons instead of showing an empty "SnoreToast" notification ([#354](https://github.com/mikaelbr/node-notifier/issues/354)). - Windows: `WindowsToaster` now accepts `remove` without a `message`, the same as `close` ([#431](https://github.com/mikaelbr/node-notifier/issues/431)). - Windows: updates the bundled 64-bit SnoreToast to 0.9.1, so clicks and action buttons are reported back when using a custom `appID` ([#326](https://github.com/mikaelbr/node-notifier/issues/326), [#332](https://github.com/mikaelbr/node-notifier/issues/332), [#375](https://github.com/mikaelbr/node-notifier/pull/375)). The binary is KDE's own build of the official release. The 32-bit binary stays on 0.7.0, as KDE publishes no 32-bit build. Note that the new binary is not code-signed. - Windows: fixes `notify` throwing in `cluster` worker processes ([#410](https://github.com/mikaelbr/node-notifier/issues/410)). diff --git a/README.md b/README.md index 007bc4c..074181b 100644 --- a/README.md +++ b/README.md @@ -462,6 +462,8 @@ _**Short answer:** update your `appID`._ If you don't see notifications within WSL2, you might have to change permission of exe vendor files (snoreToast). [See issue for more info](https://github.com/mikaelbr/node-notifier/issues/353) +SnoreToast can only show icons from a Windows drive. Under WSL, an `icon` on a mounted drive (e.g. `/mnt/c/Users/me/icon.png`) is converted to its Windows path, and other icons (e.g. in your Linux home folder) are skipped. + ### WSL: Use Linux notifications (`notify-send`) Under WSL, the default notifier is `WindowsToaster`, which shows the notification in Windows. If you run an X server and a notification daemon and want Linux notifications instead, set `NODE_NOTIFIER_WSL_NOTIFIER=linux`: diff --git a/lib/utils.js b/lib/utils.js index f47f083..270480c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -360,18 +360,36 @@ const allowedToasterFlags = [ const toasterSoundPrefix = 'Notification.'; const toasterDefaultSound = 'Notification.Default'; +// SnoreToast only reads local Windows files. Under WSL, maps /mnt//... +// to :\... and returns undefined for paths it can't map, as passing +// a Linux path makes SnoreToast show an empty notification (#354). +function toWslIconPath(icon) { + // Already a Windows path, e.g. C:\icon.png, or /C:/icon.png from a file: URL + if (/^\/?[a-zA-Z]:[\\/]/.test(icon)) { + return icon.replace(/^\//, '').replace(/\//g, '\\'); + } + const match = /^\/mnt\/([a-z])\/(.+)$/.exec(icon); + if (!match || match[2].split('/').includes('..')) { + return undefined; + } + return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, '\\')}`; +} + function mapToWin8(options) { options = mapAppIcon(options); options = mapText(options); if (options.icon) { - if (/^file:\/+/.test(options.icon)) { - // should parse file protocol URL to path - options.p = new URL(options.icon).pathname - .replace(/^\/(\w:\/)/, '$1') - .replace(/\//g, '\\'); + const isFileUrl = /^file:\/+/.test(options.icon); + // should parse file protocol URL to path + const icon = isFileUrl ? new URL(options.icon).pathname : options.icon; + if (utils.isWSL()) { + const wslIcon = toWslIconPath(icon); + if (wslIcon) options.p = wslIcon; + } else if (isFileUrl) { + options.p = icon.replace(/^\/(\w:\/)/, '$1').replace(/\//g, '\\'); } else { - options.p = options.icon; + options.p = icon; } delete options.icon; } diff --git a/test/utils.js b/test/utils.js index 7cfffa0..9e9a1f7 100644 --- a/test/utils.js +++ b/test/utils.js @@ -117,4 +117,66 @@ describe('utils', function () { }); }); }); + + describe('mapToWin8 icon', function () { + const isWSL = _.isWSL; + afterEach(function () { + _.isWSL = isWSL; + }); + + function iconFor(icon, wsl) { + _.isWSL = () => wsl; + const mapped = _.mapToWin8({ title: 'Foo', message: 'Bar', icon }); + expect(mapped.icon).toBeUndefined(); + return mapped.p; + } + + it('should convert /mnt// paths under WSL', function () { + expect(iconFor('/mnt/c/Users/me/icon.png', true)).toBe( + 'C:\\Users\\me\\icon.png' + ); + }); + + it('should convert file:///mnt// URLs under WSL', function () { + expect(iconFor('file:///mnt/d/icons/icon.png', true)).toBe( + 'D:\\icons\\icon.png' + ); + }); + + it('should keep Windows paths under WSL', function () { + expect(iconFor('C:\\icon.png', true)).toBe('C:\\icon.png'); + expect(iconFor('file:///C:/icons/icon.png', true)).toBe( + 'C:\\icons\\icon.png' + ); + }); + + it('should drop icons not on a Windows drive under WSL', function () { + for (const icon of [ + '/home/me/icon.png', + 'icon.png', + './icon.png', + 'http://example.com/icon.png', + 'file:///home/me/icon.png', + '/mnt/c', + '/mnt/c/', + '/mnt/cdrom/icon.png', + '/mnt/C/icon.png', + '/mnt/c/../../etc/icon.png', + 'file:///mnt/c/../../home/me/icon.png', + '\\\\wsl$\\Ubuntu\\home\\me\\icon.png' + ]) { + expect(iconFor(icon, true), icon).toBeUndefined(); + } + }); + + it('should not change icons outside WSL', function () { + expect(iconFor('/mnt/c/Users/me/icon.png', false)).toBe( + '/mnt/c/Users/me/icon.png' + ); + expect(iconFor('/home/me/icon.png', false)).toBe('/home/me/icon.png'); + expect(iconFor('file:///C:/icons/icon.png', false)).toBe( + 'C:\\icons\\icon.png' + ); + }); + }); });