summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-11-13 15:12:08 +0100
committerOskar <oskar@mullvad.net>2024-11-14 16:43:38 +0100
commitd36acf1176bd3ee5a3a2fc5d8241fb0eca7dfb96 (patch)
tree767d6e2305d7b641c058dfb466cfa4c30f1e6c82
parentcc831fd604a10a409480a33d526a2cb708de5584 (diff)
downloadmullvadvpn-d36acf1176bd3ee5a3a2fc5d8241fb0eca7dfb96.tar.xz
mullvadvpn-d36acf1176bd3ee5a3a2fc5d8241fb0eca7dfb96.zip
Add `nseventforwarder`
-rw-r--r--desktop/package-lock.json28
-rw-r--r--desktop/packages/nseventforwarder/.gitignore8
-rw-r--r--desktop/packages/nseventforwarder/Cargo.lock325
-rw-r--r--desktop/packages/nseventforwarder/Cargo.toml7
-rw-r--r--desktop/packages/nseventforwarder/README.md19
-rw-r--r--desktop/packages/nseventforwarder/build.sh47
-rw-r--r--desktop/packages/nseventforwarder/crates/nseventforwarder/Cargo.toml14
-rw-r--r--desktop/packages/nseventforwarder/crates/nseventforwarder/src/lib.rs113
-rw-r--r--desktop/packages/nseventforwarder/eslint.config.mjs3
-rw-r--r--desktop/packages/nseventforwarder/package.json41
-rw-r--r--desktop/packages/nseventforwarder/src/index.cts14
-rw-r--r--desktop/packages/nseventforwarder/src/index.mts3
-rw-r--r--desktop/packages/nseventforwarder/src/load.cts11
-rw-r--r--desktop/packages/nseventforwarder/tsconfig.json9
14 files changed, 642 insertions, 0 deletions
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index be2f2c483d..f454d28091 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -1590,6 +1590,12 @@
"node": ">=8"
}
},
+ "node_modules/@neon-rs/load": {
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.1.81.tgz",
+ "integrity": "sha512-A6w26BOWkFNXw04nfmcC75mLiPd3HPYC/PAuNDlXswvNb/rf55qXpTqGSz2txJmc7V8UDfIE1CAq5FUjL9wOmQ==",
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -10173,6 +10179,10 @@
"node": ">= 0.10"
}
},
+ "node_modules/nseventforwarder": {
+ "resolved": "packages/nseventforwarder",
+ "link": true
+ },
"node_modules/nseventmonitor": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nseventmonitor/-/nseventmonitor-1.0.5.tgz",
@@ -14670,6 +14680,13 @@
"grpc-tools": "^1.12.4",
"nseventmonitor": "^1.0.5"
}
+ },
+ "packages/nseventforwarder": {
+ "version": "0.0.0",
+ "license": "GPL-3.0",
+ "dependencies": {
+ "@neon-rs/load": "^0.1.73"
+ }
}
},
"dependencies": {
@@ -15847,6 +15864,11 @@
}
}
},
+ "@neon-rs/load": {
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.1.81.tgz",
+ "integrity": "sha512-A6w26BOWkFNXw04nfmcC75mLiPd3HPYC/PAuNDlXswvNb/rf55qXpTqGSz2txJmc7V8UDfIE1CAq5FUjL9wOmQ=="
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -22679,6 +22701,12 @@
"once": "^1.3.2"
}
},
+ "nseventforwarder": {
+ "version": "file:packages/nseventforwarder",
+ "requires": {
+ "@neon-rs/load": "^0.1.73"
+ }
+ },
"nseventmonitor": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nseventmonitor/-/nseventmonitor-1.0.5.tgz",
diff --git a/desktop/packages/nseventforwarder/.gitignore b/desktop/packages/nseventforwarder/.gitignore
new file mode 100644
index 0000000000..1444c8f490
--- /dev/null
+++ b/desktop/packages/nseventforwarder/.gitignore
@@ -0,0 +1,8 @@
+target
+index.node
+**/node_modules
+**/.DS_Store
+npm-debug.log*
+lib
+*.log
+dist/
diff --git a/desktop/packages/nseventforwarder/Cargo.lock b/desktop/packages/nseventforwarder/Cargo.lock
new file mode 100644
index 0000000000..c334d14c6d
--- /dev/null
+++ b/desktop/packages/nseventforwarder/Cargo.lock
@@ -0,0 +1,325 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.159"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+
+[[package]]
+name = "libloading"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
+dependencies = [
+ "cfg-if",
+ "windows-targets",
+]
+
+[[package]]
+name = "neon"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d75440242411c87dc39847b0e33e961ec1f10326a9d8ecf9c1ea64a3b3c13dc"
+dependencies = [
+ "getrandom",
+ "libloading",
+ "neon-macros",
+ "once_cell",
+ "semver",
+ "send_wrapper",
+ "smallvec",
+]
+
+[[package]]
+name = "neon-macros"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b"
+dependencies = [
+ "quote",
+ "syn",
+ "syn-mid",
+]
+
+[[package]]
+name = "nseventforwarder"
+version = "0.1.0"
+dependencies = [
+ "block2",
+ "neon",
+ "objc2-app-kit",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+
+[[package]]
+name = "send_wrapper"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "syn"
+version = "2.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn-mid"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/desktop/packages/nseventforwarder/Cargo.toml b/desktop/packages/nseventforwarder/Cargo.toml
new file mode 100644
index 0000000000..af92e12b9f
--- /dev/null
+++ b/desktop/packages/nseventforwarder/Cargo.toml
@@ -0,0 +1,7 @@
+[workspace]
+members = ["crates/nseventforwarder"]
+resolver = "2"
+
+[profile.release]
+strip = true # Automatically strip symbols from the binary.
+opt-level = "z" # Optimize for size.
diff --git a/desktop/packages/nseventforwarder/README.md b/desktop/packages/nseventforwarder/README.md
new file mode 100644
index 0000000000..4d758b62db
--- /dev/null
+++ b/desktop/packages/nseventforwarder/README.md
@@ -0,0 +1,19 @@
+# nseventforwarder
+
+## Building nseventforwarder
+
+Building nseventforwarder requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
+
+To run the build, run:
+
+```sh
+$ npm run build-debug
+```
+
+## Learn More
+
+Learn more about:
+
+- [Neon](https://neon-bindings.com).
+- [Rust](https://www.rust-lang.org).
+- [Node](https://nodejs.org).
diff --git a/desktop/packages/nseventforwarder/build.sh b/desktop/packages/nseventforwarder/build.sh
new file mode 100644
index 0000000000..53d39c762f
--- /dev/null
+++ b/desktop/packages/nseventforwarder/build.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+set -eu
+
+# Parse arguments
+while [[ "$#" -gt 0 ]]; do
+ case $1 in
+ --target)
+ TARGET_TRIPLE="$2"
+ shift
+ shift
+ ;;
+ *)
+ echo "Unknown parameter: $1"
+ exit 1
+ ;;
+ esac
+done
+
+# Fail if TARGET_TRIPLE is not set
+if [[ -z ${TARGET_TRIPLE-} ]]; then
+ echo "The variable TARGET_TRIPLE is not set."
+ echo "Set it using the --target flag"
+ echo "Available targets: aarch64-apple-darwin, x86_64-apple-darwin"
+ exit 1
+fi
+
+# Map the target to an output folder (where the dylib / node binary will end up).
+# This is neon-convention.
+case "$TARGET_TRIPLE" in
+ aarch64-apple-darwin) PLATFORM_DIR_NAME=darwin-arm64;;
+ x86_64-apple-darwin) PLATFORM_DIR_NAME=darwin-x64;;
+ *)
+ echo "Unknown target: $TARGET_TRIPLE"
+ echo "Available targets: aarch64-apple-darwin, x86_64-apple-darwin"
+ exit 1
+ ;;
+esac
+
+if [[ "$(uname -s)" == "Darwin" ]]; then
+ npm run cargo-build -- --release --target "$TARGET_TRIPLE"
+ # Copy the neon library to the correct dists folder, which is what node will
+ # pick up when loading the library at runtime.
+ PLATFORM_DIR="dist/$PLATFORM_DIR_NAME"
+ mkdir -p $PLATFORM_DIR
+ cp "target/$TARGET_TRIPLE/release/libnseventforwarder.dylib" "$PLATFORM_DIR/index.node"
+fi
diff --git a/desktop/packages/nseventforwarder/crates/nseventforwarder/Cargo.toml b/desktop/packages/nseventforwarder/crates/nseventforwarder/Cargo.toml
new file mode 100644
index 0000000000..2243cbeae6
--- /dev/null
+++ b/desktop/packages/nseventforwarder/crates/nseventforwarder/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "nseventforwarder"
+version = "0.1.0"
+license = "GPL-3.0"
+edition = "2021"
+exclude = ["index.node"]
+
+[lib]
+crate-type = ["cdylib"]
+
+[target.'cfg(target_os = "macos")'.dependencies]
+neon = "1"
+block2 = "0.5.1"
+objc2-app-kit = { version = "0.2.2", features = ["NSEvent", "block2"] }
diff --git a/desktop/packages/nseventforwarder/crates/nseventforwarder/src/lib.rs b/desktop/packages/nseventforwarder/crates/nseventforwarder/src/lib.rs
new file mode 100644
index 0000000000..c07fac279c
--- /dev/null
+++ b/desktop/packages/nseventforwarder/crates/nseventforwarder/src/lib.rs
@@ -0,0 +1,113 @@
+//! Forward [NSEvent]s from macOS to node.
+#![cfg(target_os = "macos")]
+#![warn(clippy::undocumented_unsafe_blocks)]
+
+use std::sync::{mpsc, Arc, Mutex};
+use std::thread::JoinHandle;
+
+use block2::RcBlock;
+use neon::prelude::{
+ Context, FunctionContext, Handle, JsFunction, JsNull, JsResult, JsUndefined, ModuleContext,
+ NeonResult, Object, Root,
+};
+use neon::result::Throw;
+use objc2_app_kit::{NSEvent, NSEventMask};
+
+#[neon::main]
+fn main(mut cx: ModuleContext) -> NeonResult<()> {
+ cx.export_function("start", start)?;
+ Ok(())
+}
+
+/// NSEventForwarder instance. It must be initialized by `start` and cleaned up by the callback
+/// function returned from `start`.
+static NSEVENTFORWARDER: Mutex<Option<NSEventForwarder>> = Mutex::new(None);
+
+struct NSEventForwarder {
+ /// The thread listening for incoming [NSEvent]s.
+ thread: JoinHandle<()>,
+ /// Signal for the current execution context to stop.
+ stop: mpsc::Sender<()>,
+}
+
+impl NSEventForwarder {
+ fn stop(self) {
+ // Tell the thread to stop running
+ let _ = self.stop.send(());
+ // Wait for the thread to shutdown
+ self.thread
+ .join()
+ .expect("Couldn't join the NSEventForwarder thread");
+ }
+}
+
+/// Register a callback to fire every time a [NSEventMask::LeftMouseDown] or [NSEventMask::RightMouseDown] event occur.
+///
+/// Returns a stop function to call when the original callback shouldn't be called anymore.
+fn start(mut cx: FunctionContext) -> JsResult<JsFunction> {
+ // Set up neon stuff
+ let callback = cx.argument::<JsFunction>(0)?.root(&mut cx);
+ let callback: Arc<Root<JsFunction>> = Arc::new(callback);
+ let channel = cx.channel();
+
+ // Start a long-running thread which handles incoming NS events
+ // When a new event is received, call the callback passed to us from the JavaScript caller
+ let (stop_tx, stop_rx) = mpsc::channel();
+ let join_handle = std::thread::spawn(move || {
+ // Scaffolding for calling the JavaScript callback function
+ let call_callback = move || {
+ let cb = Arc::clone(&callback);
+ channel.send(move |mut cx| {
+ let this = JsNull::new(&mut cx);
+ let _ = cb.to_inner(&mut cx).call(&mut cx, this, []);
+ Ok(())
+ })
+ };
+ // Start monitoring incoming NS events
+ let block = RcBlock::new(move |_event| {
+ call_callback();
+ });
+ // SAFETY: This function is trivially safe to call.
+ // Note: Make sure to cancel this handler with [NSEvent::removeMonitor] to unregister the
+ // listener.
+ let mut handler = unsafe {
+ NSEvent::addGlobalMonitorForEventsMatchingMask_handler(
+ NSEventMask::LeftMouseDown | NSEventMask::RightMouseDown,
+ &block,
+ )
+ };
+
+ // Listen for stop signal
+ let _ = stop_rx.recv();
+ if let Some(handler) = handler.take() {
+ // SAFETY: handler is removed only once.
+ // See https://developer.apple.com/documentation/appkit/nsevent/1533709-removemonitor#discussion
+ unsafe { NSEvent::removeMonitor(&handler) }
+ }
+ // The thread's execution will stop when this function returns
+ });
+
+ let new_context = NSEventForwarder {
+ thread: join_handle,
+ stop: stop_tx,
+ };
+
+ // Update the global NSEventForwarder state
+ let mut nseventmonitor_context = NSEVENTFORWARDER.lock().unwrap();
+ // Stop any old NSEventForwarder
+ if let Some(context) = nseventmonitor_context.take() {
+ context.stop();
+ }
+ let _ = nseventmonitor_context.insert(new_context);
+ drop(nseventmonitor_context);
+
+ JsFunction::new(&mut cx, stop)
+}
+
+fn stop(mut cx: FunctionContext<'_>) -> Result<Handle<'_, JsUndefined>, Throw> {
+ if let Some(context) = NSEVENTFORWARDER.lock().unwrap().take() {
+ context.stop();
+ }
+
+ Ok(JsUndefined::new(&mut cx))
+}
diff --git a/desktop/packages/nseventforwarder/eslint.config.mjs b/desktop/packages/nseventforwarder/eslint.config.mjs
new file mode 100644
index 0000000000..9e16ad57ea
--- /dev/null
+++ b/desktop/packages/nseventforwarder/eslint.config.mjs
@@ -0,0 +1,3 @@
+import workspaceConfig from '../../eslint.config.mjs';
+
+export default [...workspaceConfig, { ignores: ['lib/'] }];
diff --git a/desktop/packages/nseventforwarder/package.json b/desktop/packages/nseventforwarder/package.json
new file mode 100644
index 0000000000..1cd9985747
--- /dev/null
+++ b/desktop/packages/nseventforwarder/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "nseventforwarder",
+ "version": "0.0.0",
+ "author": "Mullvad VPN",
+ "license": "GPL-3.0",
+ "description": "",
+ "main": "./lib/index.cjs",
+ "scripts": {
+ "cargo-build": "tsc && cargo build",
+ "build-debug": "npm run cargo-build && cp target/debug/libnseventforwarder.dylib target/debug/index.node",
+ "build-arm": "bash ./build.sh --target aarch64-apple-darwin",
+ "build-x86": "bash ./build.sh --target x86_64-apple-darwin",
+ "lint": "eslint .",
+ "lint-fix": "eslint --fix ."
+ },
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./lib/index.d.mts",
+ "default": "./lib/index.mjs"
+ },
+ "require": {
+ "types": "./lib/index.d.cts",
+ "default": "./lib/index.cjs"
+ }
+ }
+ },
+ "types": "./lib/index.d.cts",
+ "files": [
+ "lib/**/*.?({c,m}){t,j}s"
+ ],
+ "neon": {
+ "type": "library",
+ "org": "mullvad-vpn",
+ "platforms": "common",
+ "load": "./src/load.cts"
+ },
+ "dependencies": {
+ "@neon-rs/load": "^0.1.73"
+ }
+}
diff --git a/desktop/packages/nseventforwarder/src/index.cts b/desktop/packages/nseventforwarder/src/index.cts
new file mode 100644
index 0000000000..bee20624ae
--- /dev/null
+++ b/desktop/packages/nseventforwarder/src/index.cts
@@ -0,0 +1,14 @@
+// This module is the CJS entry point for the library.
+
+// The Rust addon.
+import * as addon from './load.cjs';
+
+// Use this declaration to assign types to the addon's exports,
+// which otherwise by default are `any`.
+declare module './load.cjs' {
+ function start(cb: () => void): () => void;
+}
+
+export function start(cb: () => void): () => void {
+ return addon.start(cb);
+}
diff --git a/desktop/packages/nseventforwarder/src/index.mts b/desktop/packages/nseventforwarder/src/index.mts
new file mode 100644
index 0000000000..5e1ab260f6
--- /dev/null
+++ b/desktop/packages/nseventforwarder/src/index.mts
@@ -0,0 +1,3 @@
+// This module is the ESM entry point for the library.
+
+export * from './index.cjs';
diff --git a/desktop/packages/nseventforwarder/src/load.cts b/desktop/packages/nseventforwarder/src/load.cts
new file mode 100644
index 0000000000..c82a2a9e48
--- /dev/null
+++ b/desktop/packages/nseventforwarder/src/load.cts
@@ -0,0 +1,11 @@
+// This module loads the platform-specific build of the addon on
+// the current system.
+
+/* eslint-disable @typescript-eslint/no-require-imports */
+module.exports = require('@neon-rs/load').proxy({
+ platforms: {
+ 'darwin-x64': () => require('../dist/darwin-x64'),
+ 'darwin-arm64': () => require('../dist/darwin-arm64'),
+ },
+ debug: () => require('../target/debug/index.node'),
+});
diff --git a/desktop/packages/nseventforwarder/tsconfig.json b/desktop/packages/nseventforwarder/tsconfig.json
new file mode 100644
index 0000000000..31e6603771
--- /dev/null
+++ b/desktop/packages/nseventforwarder/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "node16",
+ "declaration": true,
+ "outDir": "lib"
+ },
+ "exclude": ["lib"]
+}