chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -0,0 +1,208 @@
#ifndef PI_CLIPBOARD_H
#define PI_CLIPBOARD_H
#include "napi.h"
#ifndef _WIN32
#include <stdlib.h>
#include <string.h>
#endif
typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE, CLIPBOARD_FILES } clipboard_operation;
// CLIPBOARD_PATHS stores concatenated, NUL-terminated UTF-8 paths in clipboard_job.data.
typedef enum {
CLIPBOARD_UNAVAILABLE,
CLIPBOARD_EMPTY,
CLIPBOARD_UTF8,
CLIPBOARD_LATIN1,
CLIPBOARD_UTF16,
CLIPBOARD_BUFFER,
CLIPBOARD_PATHS
} clipboard_format;
typedef struct {
napi_async_work work;
napi_deferred deferred;
clipboard_operation operation;
clipboard_format format;
void* data;
size_t length;
const char* error;
} clipboard_job;
// Only this function runs on the worker thread. It must not call N-API.
static void clipboard_execute(clipboard_job* job);
static void* clipboard_alloc(size_t size) {
#ifdef _WIN32
return GlobalAlloc(GPTR, size);
#else
return calloc(1, size);
#endif
}
static void clipboard_free(void* data) {
#ifdef _WIN32
if (data) GlobalFree(data);
#else
free(data);
#endif
}
#ifdef PI_CLIPBOARD_WRITE
static void clipboard_copy(clipboard_job* job, const void* data, size_t length, clipboard_format format) {
job->data = clipboard_alloc(length ? length : 1);
if (!job->data) {
job->error = "Out of memory";
return;
}
#ifdef _WIN32
volatile uint8_t* target = job->data;
const volatile uint8_t* source = data;
for (size_t index = 0; index < length; index++) target[index] = source[index];
#else
memcpy(job->data, data, length);
#endif
job->length = format == CLIPBOARD_UTF16 ? length / sizeof(uint16_t) : length;
job->format = format;
}
#endif
static void PI_NAPI_CALL execute_clipboard_work(napi_env env, void* data) {
(void)env;
clipboard_execute(data);
}
static int create_clipboard_paths_value(
napi_env env,
const clipboard_job* job,
napi_create_string_utf8_fn create_string,
napi_value* result
) {
napi_create_array_fn create_array = (napi_create_array_fn)node_symbol("napi_create_array");
napi_set_element_fn set_element = (napi_set_element_fn)node_symbol("napi_set_element");
if (!create_array || !set_element || create_array(env, result) != 0) return 1;
const char* cursor = job->data;
const char* end = cursor + job->length;
for (uint32_t index = 0; cursor < end; index++) {
const char* terminator = cursor;
while (terminator < end && *terminator != '\0') terminator++;
if (terminator == end) return 1;
napi_value item = 0;
if (create_string(env, cursor, (size_t)(terminator - cursor), &item) != 0 ||
set_element(env, *result, index, item) != 0) {
return 1;
}
cursor = terminator + 1;
}
return 0;
}
static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* data) {
clipboard_job* job = data;
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8");
napi_value result = undefined_value(env);
if (status != 0) job->error = "Clipboard operation cancelled";
if (!job->error && job->operation != CLIPBOARD_WRITE) {
if (job->format == CLIPBOARD_EMPTY) {
result = null_value(env);
} else if (job->format == CLIPBOARD_PATHS) {
status = create_clipboard_paths_value(env, job, create_string, &result);
} else if (job->format == CLIPBOARD_BUFFER) {
napi_create_buffer_copy_fn create_buffer = (napi_create_buffer_copy_fn)node_symbol("napi_create_buffer_copy");
status = create_buffer(env, job->length, job->data, 0, &result);
} else if (job->format == CLIPBOARD_UTF16) {
napi_create_string_utf16_fn create_utf16 = (napi_create_string_utf16_fn)node_symbol("napi_create_string_utf16");
status = create_utf16(env, job->data, job->length, &result);
} else if (job->format == CLIPBOARD_UTF8 || job->format == CLIPBOARD_LATIN1) {
napi_create_string_utf8_fn create_text = job->format == CLIPBOARD_LATIN1
? (napi_create_string_utf8_fn)node_symbol("napi_create_string_latin1") : create_string;
status = create_text(env, job->data, job->length, &result);
}
if (status != 0) job->error = "Could not create clipboard value";
}
if (job->error) {
napi_create_error_fn create_error = (napi_create_error_fn)node_symbol("napi_create_error");
napi_value message;
create_string(env, job->error, NAPI_AUTO_LENGTH, &message);
create_error(env, 0, message, &result);
}
napi_settle_deferred_fn settle = (napi_settle_deferred_fn)node_symbol(
job->error ? "napi_reject_deferred" : "napi_resolve_deferred"
);
settle(env, job->deferred, result);
napi_async_work_fn delete_work = (napi_async_work_fn)node_symbol("napi_delete_async_work");
delete_work(env, job->work);
clipboard_free(job->data);
clipboard_free(job);
}
static napi_value queue_clipboard(napi_env env, napi_callback_info info, clipboard_operation operation) {
napi_create_async_work_fn create_work = (napi_create_async_work_fn)node_symbol("napi_create_async_work");
napi_async_work_fn queue_work = (napi_async_work_fn)node_symbol("napi_queue_async_work");
napi_async_work_fn delete_work = (napi_async_work_fn)node_symbol("napi_delete_async_work");
napi_create_promise_fn create_promise = (napi_create_promise_fn)node_symbol("napi_create_promise");
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8");
if (!create_work || !queue_work || !delete_work || !create_promise || !create_string) {
return fail(env, "Async clipboard API unavailable");
}
clipboard_job* job = clipboard_alloc(sizeof(*job));
if (!job) return fail(env, "Out of memory");
job->operation = operation;
#ifdef PI_CLIPBOARD_WRITE
if (operation == CLIPBOARD_WRITE) {
napi_get_cb_info_fn get_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
#ifdef _WIN32
napi_get_value_string_utf16_fn get_string = (napi_get_value_string_utf16_fn)node_symbol("napi_get_value_string_utf16");
size_t unit_size = sizeof(uint16_t);
#else
napi_get_value_string_utf8_fn get_string = (napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
size_t unit_size = 1;
#endif
size_t argc = 1;
napi_value argument = 0;
if (!get_info || !get_string || get_info(env, info, &argc, &argument, 0, 0) != 0 || argc == 0 ||
get_string(env, argument, 0, 0, &job->length) != 0) {
clipboard_free(job);
return fail(env, "setText requires a string");
}
job->data = job->length < SIZE_MAX / unit_size
? clipboard_alloc((job->length + 1) * unit_size) : 0;
if (!job->data || get_string(env, argument, job->data, job->length + 1, &job->length) != 0) {
clipboard_free(job->data);
clipboard_free(job);
return fail(env, "Could not read clipboard text");
}
}
#else
(void)info;
#endif
napi_value name = 0;
napi_value promise = 0;
if (create_string(env, "pi.clipboard", NAPI_AUTO_LENGTH, &name) != 0 ||
create_work(env, 0, name, execute_clipboard_work, complete_clipboard_work, job, &job->work) != 0 ||
create_promise(env, &job->deferred, &promise) != 0 || queue_work(env, job->work) != 0) {
if (job->work) delete_work(env, job->work);
clipboard_free(job->data);
clipboard_free(job);
return fail(env, "Could not queue clipboard operation");
}
return promise;
}
static napi_value PI_NAPI_CALL get_clipboard_text(napi_env env, napi_callback_info info) {
return queue_clipboard(env, info, CLIPBOARD_TEXT);
}
static napi_value PI_NAPI_CALL get_clipboard_image(napi_env env, napi_callback_info info) {
return queue_clipboard(env, info, CLIPBOARD_IMAGE);
}
#ifdef PI_CLIPBOARD_WRITE
static napi_value PI_NAPI_CALL set_clipboard_text(napi_env env, napi_callback_info info) {
return queue_clipboard(env, info, CLIPBOARD_WRITE);
}
#endif
#endif

View File

@@ -1,20 +1,22 @@
# Darwin native prebuilds
Build both macOS architectures from the repository root:
Provides modifier-key state and asynchronous text, image, and file-path clipboard access using AppKit.
## Building
On macOS, run from the repository root:
```sh
npm --prefix packages/tui run build:native:darwin
```
The build uses macOS 11.0 as the arm64 deployment target and macOS 10.15 as the x86_64 deployment target. On macOS, `build.sh` finds Apple clang and the active macOS SDK through `xcrun`. Either an Intel or Apple Silicon host can build both outputs.
The build uses Apple clang and the macOS SDK found through `xcrun`. Either Intel or Apple Silicon can build both targets: arm64 (macOS 11+) and x64 (macOS 10.15+).
A non-macOS host needs a complete Darwin cross-toolchain, including a macOS SDK and a Mach-O linker. For example, an osxcross installation can be selected with `CC` and `SDKROOT`:
Cross-building requires a Darwin toolchain with a macOS SDK and Mach-O linker, such as osxcross:
```sh
CC=/path/to/osxcross/clang SDKROOT=/path/to/MacOSX.sdk \
npm --prefix packages/tui run build:native:darwin
```
The SDK must be obtained and used in accordance with Apple's license. Plain Linux or Windows clang is not enough because the addon includes and links CoreGraphics.
Zig is not used here because it does not provide the Apple SDK or CoreGraphics framework stubs. It therefore does not make this build SDK-independent, and its clang driver does not currently handle this Mach-O bundle recipe as a drop-in replacement for Apple clang.
The SDK must be obtained and used under Apple's license. Plain clang or Zig alone is not sufficient.

View File

@@ -3,7 +3,7 @@
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source_file="$script_dir/src/darwin-modifiers.c"
source_file="$script_dir/src/darwin-platform.m"
if [[ -n "${CC:-}" ]]; then
compiler="$CC"
@@ -37,25 +37,32 @@ build() {
local arch="$1"
local target="$2"
local output_dir="$script_dir/prebuilds/darwin-$arch"
local temporary_output="$temporary_dir/darwin-$arch/darwin-modifiers.node"
local temporary_output="$temporary_dir/darwin-$arch/darwin-platform.node"
mkdir -p "$(dirname "$temporary_output")"
"$compiler" \
-fobjc-arc \
-std=c11 \
-Wall \
-Wextra \
-O2 \
-Oz \
-flto \
-fvisibility=hidden \
"--target=$target" \
"${sdk_flags[@]}" \
-bundle \
-undefined dynamic_lookup \
-Wl,-dead_strip \
-Wl,-x \
-framework AppKit \
-framework CoreGraphics \
-framework Foundation \
"$source_file" \
-o "$temporary_output"
mkdir -p "$output_dir"
install -m 755 "$temporary_output" "$output_dir/darwin-modifiers.node"
echo "Built $output_dir/darwin-modifiers.node"
install -m 755 "$temporary_output" "$output_dir/darwin-platform.node"
echo "Built $output_dir/darwin-platform.node"
}
build arm64 arm64-apple-macos11.0

View File

@@ -1,70 +0,0 @@
#include <CoreGraphics/CoreGraphics.h>
#include <dlfcn.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#define NAPI_AUTO_LENGTH ((size_t)-1)
typedef void* napi_env;
typedef void* napi_value;
typedef void* napi_callback_info;
typedef napi_value (*napi_callback)(napi_env, napi_callback_info);
typedef int (*napi_create_function_fn)(napi_env, const char*, size_t, napi_callback, void*, napi_value*);
typedef int (*napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
typedef int (*napi_get_boolean_fn)(napi_env, bool, napi_value*);
typedef int (*napi_get_cb_info_fn)(napi_env, napi_callback_info, size_t*, napi_value*, napi_value*, void**);
typedef int (*napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*);
static void* node_symbol(const char* name) {
return dlsym(RTLD_DEFAULT, name);
}
static CGEventFlags modifier_mask_for_name(const char* name) {
if (strcmp(name, "shift") == 0) return kCGEventFlagMaskShift;
if (strcmp(name, "command") == 0) return kCGEventFlagMaskCommand;
if (strcmp(name, "control") == 0) return kCGEventFlagMaskControl;
if (strcmp(name, "option") == 0) return kCGEventFlagMaskAlternate;
return 0;
}
static napi_value is_modifier_pressed(napi_env env, napi_callback_info info) {
napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
napi_get_value_string_utf8_fn napi_get_value_string_utf8 = (napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
bool pressed = false;
if (napi_get_cb_info && napi_get_value_string_utf8) {
size_t argc = 1;
napi_value args[1] = {0};
if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) {
char name[16] = {0};
size_t copied = 0;
if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) {
CGEventFlags mask = modifier_mask_for_name(name);
if (mask != 0) {
CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
pressed = (flags & mask) != 0;
}
}
}
}
napi_value result = 0;
if (napi_get_boolean) napi_get_boolean(env, pressed, &result);
return result;
}
__attribute__((visibility("default"))) napi_value napi_register_module_v1(napi_env env, napi_value exports) {
napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function");
napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property");
napi_value fn = 0;
if (napi_create_function &&
napi_set_named_property &&
napi_create_function(env, "isModifierPressed", NAPI_AUTO_LENGTH, is_modifier_pressed, 0, &fn) == 0) {
napi_set_named_property(env, exports, "isModifierPressed", fn);
}
return exports;
}

View File

@@ -0,0 +1,132 @@
#import <AppKit/AppKit.h>
#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#include <stdlib.h>
#include <string.h>
#define PI_CLIPBOARD_WRITE
#include "../../clipboard.h"
static CGEventFlags modifier_mask_for_name(const char* name) {
if (strcmp(name, "shift") == 0) return kCGEventFlagMaskShift;
if (strcmp(name, "command") == 0) return kCGEventFlagMaskCommand;
if (strcmp(name, "control") == 0) return kCGEventFlagMaskControl;
if (strcmp(name, "option") == 0) return kCGEventFlagMaskAlternate;
return 0;
}
static napi_value is_modifier_pressed(napi_env env, napi_callback_info info) {
napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
napi_get_value_string_utf8_fn napi_get_value_string_utf8 =
(napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
bool pressed = false;
if (napi_get_cb_info && napi_get_value_string_utf8) {
size_t argc = 1;
napi_value args[1] = {0};
if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) {
char name[16] = {0};
size_t copied = 0;
if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) {
CGEventFlags mask = modifier_mask_for_name(name);
if (mask != 0) {
CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
pressed = (flags & mask) != 0;
}
}
}
}
napi_value result = 0;
if (!napi_get_boolean || napi_get_boolean(env, pressed, &result) != 0) {
return fail(env, "Could not inspect modifier state");
}
return result;
}
static void clipboard_execute(clipboard_job* job) {
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
if (!pasteboard) {
job->error = "Could not open clipboard";
} else if (job->operation == CLIPBOARD_WRITE) {
NSString* text = [[NSString alloc] initWithBytes:job->data length:job->length encoding:NSUTF8StringEncoding];
if (!text) {
job->error = "Clipboard text is not valid UTF-8";
return;
}
[pasteboard clearContents];
if (![pasteboard setString:text forType:NSPasteboardTypeString]) job->error = "Could not set clipboard text";
} else if (job->operation == CLIPBOARD_TEXT) {
NSString* text = [pasteboard stringForType:NSPasteboardTypeString];
if (!text) {
job->format = CLIPBOARD_EMPTY;
} else if (!text.UTF8String) {
job->error = "Could not encode clipboard text";
} else {
clipboard_copy(job, text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding], CLIPBOARD_UTF8);
}
} else if (job->operation == CLIPBOARD_FILES) {
NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]]
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
size_t total = 0;
for (NSURL* url in urls) {
const char* path = url.fileSystemRepresentation;
if (!path) continue;
size_t length = strlen(path);
if (length == SIZE_MAX || total > SIZE_MAX - length - 1) {
job->error = "Clipboard file paths are too large";
return;
}
total += length + 1;
}
if (total == 0) {
job->format = CLIPBOARD_EMPTY;
return;
}
char* paths = clipboard_alloc(total);
if (!paths) {
job->error = "Out of memory";
return;
}
char* cursor = paths;
for (NSURL* url in urls) {
const char* path = url.fileSystemRepresentation;
if (!path) continue;
size_t length = strlen(path);
memcpy(cursor, path, length + 1);
cursor += length + 1;
}
job->data = paths;
job->length = total;
job->format = CLIPBOARD_PATHS;
} else {
if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) {
job->format = CLIPBOARD_EMPTY;
return;
}
NSData* png = [pasteboard dataForType:NSPasteboardTypePNG];
if (!png) {
NSImage* image = [[NSImage alloc] initWithPasteboard:pasteboard];
NSData* tiff = image.TIFFRepresentation;
NSBitmapImageRep* bitmap = tiff ? [NSBitmapImageRep imageRepWithData:tiff] : nil;
png = bitmap ? [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}] : nil;
}
if (png) clipboard_copy(job, png.bytes, png.length, CLIPBOARD_BUFFER);
else job->error = "Clipboard does not contain an image";
}
}
}
static napi_value PI_NAPI_CALL get_clipboard_file_paths(napi_env env, napi_callback_info info) {
return queue_clipboard(env, info, CLIPBOARD_FILES);
}
PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) {
set_function_export(env, exports, "isModifierPressed", is_modifier_pressed);
set_function_export(env, exports, "getText", get_clipboard_text);
set_function_export(env, exports, "setText", set_clipboard_text);
set_function_export(env, exports, "getImage", get_clipboard_image);
set_function_export(env, exports, "getFilePaths", get_clipboard_file_paths);
return exports;
}

View File

@@ -0,0 +1,23 @@
# Linux clipboard helper
Provides asynchronous X11 text and image reads using `libxcb.so.1`. Prebuilds support x64 and arm64 on glibc and musl. Reads have bounded waits; if a native operation stalls, the helper remains unavailable until it finishes.
Coding-agent falls back to command-line tools when native reads are unavailable. Wayland reads use `wl-paste`; all Linux writes use the existing command-line or terminal clipboard paths.
## Building
Install a C compiler and XCB development headers, then run from the repository root on each supported Linux architecture:
```sh
npm --prefix packages/tui run build:native:linux
```
## Testing
Install the build dependencies plus `pkg-config`, `Xvfb`, and `xclip`, then run from `packages/tui`:
```sh
node --test test/native-clipboard-linux.test.ts
```
Tests use isolated X11 servers, not the desktop clipboard. They skip when dependencies are missing.

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
compiler="${CC:-cc}"
if [[ "$(uname -s)" != "Linux" ]]; then
echo "Linux native helpers must be built on Linux" >&2
exit 1
fi
case "$(uname -m)" in
x86_64)
arch="x64"
;;
aarch64|arm64)
arch="arm64"
;;
*)
echo "Unsupported Linux architecture: $(uname -m)" >&2
exit 1
;;
esac
if ! command -v "$compiler" >/dev/null 2>&1; then
echo "Linux C compiler not found: $compiler" >&2
exit 1
fi
build_dir="$(mktemp -d "${TMPDIR:-/tmp}/pi-tui-linux.XXXXXX")"
trap 'rm -rf "$build_dir"' EXIT
common_flags=(
-std=c11
-D_POSIX_C_SOURCE=200809L
-Wall
-Wextra
-Werror
-Os
-flto
-fPIC
-pthread
-fvisibility=hidden
-fno-stack-protector
-shared
-nostdlib
-Wl,--unresolved-symbols=ignore-all
-Wl,--no-as-needed
-Wl,-O2
-Wl,--gc-sections
# A timed-out private thread may outlive the Node environment that loaded us.
-Wl,-z,nodelete
-Wl,-s
)
x11_output="$build_dir/linux-platform-x11.node"
"$compiler" "${common_flags[@]}" \
"$script_dir/src/linux-platform-x11.c" \
-lxcb \
-o "$x11_output"
output_dir="$script_dir/prebuilds/linux-$arch"
mkdir -p "$output_dir"
install -m 755 "$x11_output" "$output_dir/linux-platform-x11.node"
printf 'Built %s\n' "$output_dir/linux-platform-x11.node"

View File

@@ -0,0 +1,70 @@
#include <pthread.h>
#include <time.h>
#include "../../clipboard.h"
static void read_clipboard(clipboard_job* job);
static pthread_mutex_t worker_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_once_t worker_once = PTHREAD_ONCE_INIT;
static pthread_cond_t worker_changed;
static bool worker_ready, worker_busy, worker_waiting, worker_finished;
static clipboard_job worker_result;
static int (*signal_worker)(pthread_cond_t*);
static int (*wait_for_worker)(pthread_cond_t*, pthread_mutex_t*, const struct timespec*);
static void initialize_worker_condition(void) {
// Unversioned ELF imports otherwise bind obsolete condition variables on glibc x64.
int (*initialize)(pthread_cond_t*, const pthread_condattr_t*) = node_symbol("pthread_cond_init");
signal_worker = node_symbol("pthread_cond_signal");
wait_for_worker = node_symbol("pthread_cond_timedwait");
if (!initialize || !signal_worker || !wait_for_worker) return;
pthread_condattr_t attributes;
if (pthread_condattr_init(&attributes) != 0) return;
worker_ready = pthread_condattr_setclock(&attributes, CLOCK_MONOTONIC) == 0 &&
initialize(&worker_changed, &attributes) == 0;
pthread_condattr_destroy(&attributes);
}
static void* read_on_private_thread(void* unused) {
(void)unused;
read_clipboard(&worker_result);
pthread_mutex_lock(&worker_mutex);
worker_finished = true;
if (!worker_waiting) {
free(worker_result.data); // The caller timed out; discard the late result.
worker_busy = false;
}
signal_worker(&worker_changed);
pthread_mutex_unlock(&worker_mutex);
return 0;
}
static void clipboard_execute(clipboard_job* job) {
pthread_once(&worker_once, initialize_worker_condition);
if (!worker_ready) return;
pthread_mutex_lock(&worker_mutex);
if (!worker_busy) {
worker_busy = worker_waiting = true;
worker_finished = false;
worker_result = (clipboard_job){.operation = job->operation};
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += 3; // Allow the two-second transfer deadline to report its own error first.
pthread_t thread;
if (pthread_create(&thread, 0, read_on_private_thread, 0) == 0) {
pthread_detach(thread);
// Only this bounded wait occupies libuv. Other callers return unavailable.
while (!worker_finished && wait_for_worker(&worker_changed, &worker_mutex, &deadline) == 0) {}
if (worker_finished) {
job->data = worker_result.data;
job->length = worker_result.length;
job->format = worker_result.format;
job->error = worker_result.error;
worker_busy = false;
}
} else {
worker_busy = false;
}
worker_waiting = false;
}
pthread_mutex_unlock(&worker_mutex);
}

View File

@@ -0,0 +1,274 @@
#include <xcb/xcb.h>
#include <string.h>
#include <xcb/xcbext.h>
#include <errno.h>
#include <poll.h>
#include <time.h>
#include "clipboard-worker.h"
#define MAX_CLIPBOARD_BYTES (50u * 1024u * 1024u)
#define CLIPBOARD_TIMEOUT_MS 2000
static const char* const text_types[] = {
"text/plain;charset=utf-8", "text/plain;charset=UTF-8", "UTF8_STRING", "text/plain", "STRING",
};
static const char* const image_types[] = {
"image/png", "image/jpeg", "image/webp", "image/gif", "image/bmp", "image/tiff",
};
static int64_t monotonic_ms(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
}
typedef struct {
unsigned char* data;
size_t length;
uint32_t items;
uint8_t format;
xcb_atom_t type;
} property_data;
typedef struct {
xcb_connection_t* connection;
xcb_window_t window;
xcb_atom_t clipboard;
xcb_atom_t property;
xcb_atom_t targets;
xcb_atom_t incr;
int64_t deadline;
} x11_clipboard;
static bool append_bytes(property_data* result, const unsigned char* bytes, size_t length) {
if (length > MAX_CLIPBOARD_BYTES - result->length) return false;
unsigned char* data = realloc(result->data, result->length + length + 1);
if (!data) return false;
memcpy(data + result->length, bytes, length);
result->length += length;
data[result->length] = 0;
result->data = data;
return true;
}
// Share one deadline across discovery, replies, and incremental chunks.
// Connection setup and flushing are libxcb calls on the private thread.
static bool wait_for_input(x11_clipboard* clipboard) {
for (;;) {
int64_t remaining = clipboard->deadline - monotonic_ms();
if (remaining <= 0 || xcb_connection_has_error(clipboard->connection)) return false;
struct pollfd descriptor = {xcb_get_file_descriptor(clipboard->connection), POLLIN, 0};
int ready = poll(&descriptor, 1, (int)remaining);
if (ready >= 0) return ready && (descriptor.revents & POLLIN);
if (errno != EINTR) return false;
}
}
static void* wait_for_reply(x11_clipboard* clipboard, unsigned sequence) {
if (xcb_flush(clipboard->connection) <= 0) return 0;
while (monotonic_ms() < clipboard->deadline) {
void* reply = 0;
xcb_generic_error_t* error = 0;
if (xcb_poll_for_reply(clipboard->connection, sequence, &reply, &error)) {
free(error);
return reply;
}
if (!wait_for_input(clipboard)) break;
}
xcb_discard_reply(clipboard->connection, sequence);
return 0;
}
static xcb_generic_event_t* wait_for_event(x11_clipboard* clipboard, uint8_t type) {
if (xcb_flush(clipboard->connection) <= 0) return 0;
while (monotonic_ms() < clipboard->deadline) {
xcb_generic_event_t* event = xcb_poll_for_event(clipboard->connection);
if (event) {
if ((event->response_type & 0x7f) == type) return event;
free(event);
} else if (!wait_for_input(clipboard)) {
break;
}
}
return 0;
}
static bool read_property(x11_clipboard* clipboard, bool remove, property_data* result) {
xcb_get_property_cookie_t cookie = xcb_get_property(
clipboard->connection, remove, clipboard->window, clipboard->property,
XCB_GET_PROPERTY_TYPE_ANY, 0, MAX_CLIPBOARD_BYTES / 4
);
xcb_get_property_reply_t* reply = wait_for_reply(clipboard, cookie.sequence);
if (!reply) return false;
bool copied = false;
bool valid_format = reply->format == 8 || reply->format == 16 || reply->format == 32;
uint64_t length = (uint64_t)reply->value_len * (reply->format / 8);
if (reply->bytes_after == 0 && reply->type != XCB_NONE && valid_format &&
length <= MAX_CLIPBOARD_BYTES && length <= (uint64_t)reply->length * 4) {
result->type = reply->type;
result->format = reply->format;
result->items = reply->value_len;
copied = append_bytes(result, xcb_get_property_value(reply), (size_t)length);
}
free(reply);
return copied;
}
static bool append_property(property_data* result, const property_data* chunk) {
if ((chunk->format != 8 && chunk->format != 16 && chunk->format != 32) ||
chunk->type == XCB_NONE ||
(uint64_t)chunk->items * (chunk->format / 8) != chunk->length ||
(result->type != XCB_NONE && (result->type != chunk->type || result->format != chunk->format)) ||
chunk->items > UINT32_MAX - result->items) return false;
if (!append_bytes(result, chunk->data, chunk->length)) return false;
result->type = chunk->type;
result->format = chunk->format;
result->items += chunk->items;
return true;
}
static bool request_selection(x11_clipboard* clipboard, xcb_atom_t target, property_data* result) {
memset(result, 0, sizeof(*result));
xcb_delete_property(clipboard->connection, clipboard->window, clipboard->property);
xcb_convert_selection(
clipboard->connection, clipboard->window, clipboard->clipboard,
target, clipboard->property, XCB_CURRENT_TIME
);
while (true) {
xcb_selection_notify_event_t* event = (xcb_selection_notify_event_t*)wait_for_event(clipboard, XCB_SELECTION_NOTIFY);
if (!event) return false;
bool matches = event->selection == clipboard->clipboard && event->target == target;
bool received = event->property != XCB_NONE;
free(event);
if (!matches) continue;
if (!received) return true; // No owner, or the owner does not offer this target.
if (!read_property(clipboard, false, result)) return false;
break;
}
if (result->type != clipboard->incr) return true;
free(result->data);
memset(result, 0, sizeof(*result));
xcb_delete_property(clipboard->connection, clipboard->window, clipboard->property);
while (true) {
xcb_property_notify_event_t* event = (xcb_property_notify_event_t*)wait_for_event(clipboard, XCB_PROPERTY_NOTIFY);
if (!event) return false;
bool matches = event->atom == clipboard->property && event->state == XCB_PROPERTY_NEW_VALUE;
free(event);
if (!matches) continue;
property_data chunk = {0};
if (!read_property(clipboard, true, &chunk)) return false;
bool appended = append_property(result, &chunk);
bool finished = chunk.items == 0;
free(chunk.data);
if (!appended) return false;
if (finished) return true;
}
}
static xcb_atom_t intern_atom(x11_clipboard* clipboard, const char* name) {
xcb_intern_atom_cookie_t cookie = xcb_intern_atom(clipboard->connection, false, strlen(name), name);
xcb_intern_atom_reply_t* reply = wait_for_reply(clipboard, cookie.sequence);
if (!reply) return XCB_NONE;
xcb_atom_t atom = reply->atom;
free(reply);
return atom;
}
static bool open_clipboard(x11_clipboard* clipboard) {
memset(clipboard, 0, sizeof(*clipboard));
clipboard->deadline = monotonic_ms() + CLIPBOARD_TIMEOUT_MS;
int screen_number = 0;
clipboard->connection = xcb_connect(0, &screen_number);
if (xcb_connection_has_error(clipboard->connection)) return false;
xcb_screen_iterator_t screens = xcb_setup_roots_iterator(xcb_get_setup(clipboard->connection));
while (screen_number-- > 0 && screens.rem) xcb_screen_next(&screens);
if (!screens.rem) return false;
clipboard->window = xcb_generate_id(clipboard->connection);
uint32_t mask = XCB_EVENT_MASK_PROPERTY_CHANGE;
xcb_create_window(
clipboard->connection, XCB_COPY_FROM_PARENT, clipboard->window,
screens.data->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, &mask
);
clipboard->clipboard = intern_atom(clipboard, "CLIPBOARD");
clipboard->property = intern_atom(clipboard, "PI_CLIPBOARD");
clipboard->targets = intern_atom(clipboard, "TARGETS");
clipboard->incr = intern_atom(clipboard, "INCR");
return clipboard->clipboard && clipboard->property && clipboard->targets && clipboard->incr;
}
static void close_clipboard(x11_clipboard* clipboard) {
if (clipboard->connection) xcb_disconnect(clipboard->connection);
}
static bool preferred_target(x11_clipboard* clipboard, bool image, xcb_atom_t* target) {
const char* const* types = image ? image_types : text_types;
size_t type_count = image ? 6 : 5;
xcb_atom_t wanted[6];
for (size_t index = 0; index < type_count; index++) {
wanted[index] = intern_atom(clipboard, types[index]);
if (wanted[index] == XCB_NONE) return false;
}
property_data targets = {0};
bool received = request_selection(clipboard, clipboard->targets, &targets);
bool valid = targets.type == XCB_ATOM_ATOM && targets.format == 32 &&
targets.items == targets.length / sizeof(xcb_atom_t) && targets.length % sizeof(xcb_atom_t) == 0;
if (received && valid) {
xcb_atom_t* offered = (xcb_atom_t*)targets.data;
for (size_t wanted_index = 0; wanted_index < type_count; wanted_index++) {
for (uint32_t offered_index = 0; offered_index < targets.items; offered_index++) {
if (offered[offered_index] == wanted[wanted_index]) {
free(targets.data);
*target = wanted[wanted_index];
return true;
}
}
}
}
free(targets.data);
// Some owners refuse TARGETS but still serve text directly.
if (received && !image && targets.type == XCB_NONE) {
*target = intern_atom(clipboard, "UTF8_STRING");
return *target != XCB_NONE;
}
return received && (valid || targets.type == XCB_NONE);
}
static bool read_selection(x11_clipboard* clipboard, bool image, property_data* result) {
xcb_atom_t target = XCB_NONE;
bool received = preferred_target(clipboard, image, &target) &&
(target == XCB_NONE || request_selection(clipboard, target, result));
if (!received) {
free(result->data);
memset(result, 0, sizeof(*result));
}
return received;
}
static void read_clipboard(clipboard_job* job) {
x11_clipboard clipboard;
if (open_clipboard(&clipboard)) {
property_data contents = {0};
if (!read_selection(&clipboard, job->operation == CLIPBOARD_IMAGE, &contents)) {
job->error = "Could not read X11 clipboard";
} else {
job->data = contents.data;
job->length = contents.length;
job->format = !contents.data ? CLIPBOARD_EMPTY : job->operation == CLIPBOARD_IMAGE
? CLIPBOARD_BUFFER : contents.type == XCB_ATOM_STRING ? CLIPBOARD_LATIN1 : CLIPBOARD_UTF8;
}
}
close_clipboard(&clipboard);
}
PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) {
set_function_export(env, exports, "getText", get_clipboard_text);
set_function_export(env, exports, "getImage", get_clipboard_image);
return exports;
}

View File

@@ -0,0 +1,91 @@
#ifndef PI_NAPI_H
#define PI_NAPI_H
// Resolve the small N-API surface we use from Node or Bun, without Node headers
// or a link-time dependency on a particular runtime.
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef _WIN32
#include <windows.h>
#define PI_NAPI_CALL __cdecl
#define PI_NAPI_EXPORT __declspec(dllexport)
#else
#include <dlfcn.h>
#define PI_NAPI_CALL
#define PI_NAPI_EXPORT __attribute__((visibility("default")))
#endif
#define NAPI_AUTO_LENGTH ((size_t)-1)
typedef void* napi_env;
typedef void* napi_value;
typedef void* napi_callback_info;
typedef void* napi_async_work;
typedef void* napi_deferred;
typedef void (PI_NAPI_CALL *napi_async_execute_callback)(napi_env, void*);
typedef void (PI_NAPI_CALL *napi_async_complete_callback)(napi_env, int, void*);
typedef int (PI_NAPI_CALL *napi_create_async_work_fn)(napi_env, napi_value, napi_value, napi_async_execute_callback, napi_async_complete_callback, void*, napi_async_work*);
typedef int (PI_NAPI_CALL *napi_create_array_fn)(napi_env, napi_value*);
typedef int (PI_NAPI_CALL *napi_async_work_fn)(napi_env, napi_async_work);
typedef int (PI_NAPI_CALL *napi_create_promise_fn)(napi_env, napi_deferred*, napi_value*);
typedef int (PI_NAPI_CALL *napi_settle_deferred_fn)(napi_env, napi_deferred, napi_value);
typedef int (PI_NAPI_CALL *napi_create_error_fn)(napi_env, napi_value, napi_value, napi_value*);
typedef napi_value (PI_NAPI_CALL *napi_callback)(napi_env, napi_callback_info);
typedef int (PI_NAPI_CALL *napi_create_buffer_copy_fn)(napi_env, size_t, const void*, void**, napi_value*);
typedef int (PI_NAPI_CALL *napi_create_function_fn)(napi_env, const char*, size_t, napi_callback, void*, napi_value*);
typedef int (PI_NAPI_CALL *napi_create_string_utf8_fn)(napi_env, const char*, size_t, napi_value*);
typedef int (PI_NAPI_CALL *napi_create_string_utf16_fn)(napi_env, const uint16_t*, size_t, napi_value*);
typedef int (PI_NAPI_CALL *napi_get_boolean_fn)(napi_env, bool, napi_value*);
typedef int (PI_NAPI_CALL *napi_get_cb_info_fn)(napi_env, napi_callback_info, size_t*, napi_value*, napi_value*, void**);
typedef int (PI_NAPI_CALL *napi_get_value_fn)(napi_env, napi_value*);
typedef int (PI_NAPI_CALL *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*);
typedef int (PI_NAPI_CALL *napi_get_value_string_utf16_fn)(napi_env, napi_value, uint16_t*, size_t, size_t*);
typedef int (PI_NAPI_CALL *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
typedef int (PI_NAPI_CALL *napi_set_element_fn)(napi_env, napi_value, uint32_t, napi_value);
typedef int (PI_NAPI_CALL *napi_throw_error_fn)(napi_env, const char*, const char*);
static void* node_symbol(const char* name) {
#ifdef _WIN32
HMODULE module = GetModuleHandleA(0);
void* proc = module ? (void*)GetProcAddress(module, name) : 0;
if (proc) return proc;
module = GetModuleHandleA("node.dll");
return module ? (void*)GetProcAddress(module, name) : 0;
#else
return dlsym(RTLD_DEFAULT, name);
#endif
}
static napi_value undefined_value(napi_env env) {
napi_get_value_fn get_value = (napi_get_value_fn)node_symbol("napi_get_undefined");
napi_value result = 0;
if (get_value) get_value(env, &result);
return result;
}
static napi_value null_value(napi_env env) {
napi_get_value_fn get_value = (napi_get_value_fn)node_symbol("napi_get_null");
napi_value result = 0;
if (get_value) get_value(env, &result);
return result;
}
static napi_value fail(napi_env env, const char* message) {
napi_throw_error_fn napi_throw_error = (napi_throw_error_fn)node_symbol("napi_throw_error");
if (napi_throw_error) napi_throw_error(env, 0, message);
return undefined_value(env);
}
static void set_function_export(napi_env env, napi_value exports, const char* name, napi_callback callback) {
napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function");
napi_set_named_property_fn napi_set_named_property =
(napi_set_named_property_fn)node_symbol("napi_set_named_property");
napi_value fn = 0;
if (napi_create_function && napi_set_named_property &&
napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, 0, &fn) == 0) {
napi_set_named_property(env, exports, name, fn);
}
}
#endif

View File

@@ -1,16 +1,16 @@
# Windows native prebuilds
Build both Windows architectures from the repository root:
Provides console input setup, modifier-key state, and asynchronous text/image clipboard access. Links to `kernel32` and `user32`; no Node headers are required.
## Building
On Windows, install Visual Studio's "Desktop development with C++" workload, then run from the repository root to build x64 and arm64:
```sh
npm --prefix packages/tui run build:native:win32
```
On Windows, the build uses the Microsoft C++ Build Tools from Visual Studio. `build.mjs` locates `VsDevCmd.bat`, initializes a developer environment for `amd64` and `arm64`, and builds the addon with `cl.exe`/`link.exe`.
Install the "Desktop development with C++" workload, or at minimum the MSVC toolset and Windows SDK components. No Node headers are required; the addon resolves N-API symbols from the host process.
For non-Windows cross-builds, or for custom Windows toolchains, set `PI_TUI_WIN32_TOOLCHAIN=mingw` and provide MinGW-compatible compilers:
For cross-builds or custom toolchains, provide MinGW-compatible compilers:
```sh
PI_TUI_WIN32_TOOLCHAIN=mingw \
@@ -19,4 +19,13 @@ CC_ARM64=/path/to/aarch64-w64-mingw32-gcc \
npm --prefix packages/tui run build:native:win32
```
The addon intentionally avoids the C runtime and links only against `kernel32`.
## Testing clipboard writes
On a Windows test desktop, run from `packages/tui` in PowerShell:
```powershell
$env:PI_TEST_NATIVE_CLIPBOARD = "1"
node --test test/native-platform.test.ts
```
This opt-in test checks native text writes and reads. **It replaces the system clipboard contents.**

View File

@@ -5,7 +5,7 @@ import * as path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const sourceFile = path.join(scriptDir, "src", "win32-console-mode.c");
const sourceFile = path.join(scriptDir, "src", "win32-platform.c");
const temporaryDir = mkdtempSync(path.join(tmpdir(), "pi-tui-win32-"));
const targets = [
@@ -121,7 +121,7 @@ function resolveMingwCompiler(target) {
function installOutput(temporaryOutput, target) {
const outputDir = path.join(scriptDir, "prebuilds", `win32-${target.arch}`);
mkdirSync(outputDir, { recursive: true });
const output = path.join(outputDir, "win32-console-mode.node");
const output = path.join(outputDir, "win32-platform.node");
copyFileSync(temporaryOutput, output);
chmodSync(output, 0o755);
console.log(`Built ${path.relative(process.cwd(), output)}`);
@@ -129,7 +129,7 @@ function installOutput(temporaryOutput, target) {
function buildWithMsvc(target, vsDevCmd) {
const buildDir = path.join(temporaryDir, `msvc-${target.arch}`);
const temporaryOutput = path.join(buildDir, "win32-console-mode.node");
const temporaryOutput = path.join(buildDir, "win32-platform.node");
mkdirSync(buildDir, { recursive: true });
const batchFile = path.join(buildDir, "build.cmd");
@@ -154,6 +154,7 @@ function buildWithMsvc(target, vsDevCmd) {
"/NOENTRY",
"/NODEFAULTLIB",
"kernel32.lib",
"user32.lib",
"/OPT:REF",
"/OPT:ICF",
].join(" "),
@@ -174,7 +175,7 @@ function buildWithMingw(target) {
}
const buildDir = path.join(temporaryDir, `mingw-${target.arch}`);
const temporaryOutput = path.join(buildDir, "win32-console-mode.node");
const temporaryOutput = path.join(buildDir, "win32-platform.node");
mkdirSync(buildDir, { recursive: true });
const args = [
@@ -183,12 +184,14 @@ function buildWithMingw(target) {
"-Wall",
"-Wextra",
"-Oz",
"-fno-builtin",
"-shared",
"-nostdlib",
"-Wl,--no-entry",
"-Wl,--strip-all",
sourceFile,
"-lkernel32",
"-luser32",
"-o",
temporaryOutput,
];

View File

@@ -1,119 +0,0 @@
#include <windows.h>
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
#endif
#define NAPI_AUTO_LENGTH ((unsigned long long)-1)
#define KEY_PRESSED_MASK 0x8000
typedef void* napi_env;
typedef void* napi_value;
typedef void* napi_callback_info;
typedef napi_value (__cdecl *napi_callback)(napi_env, napi_callback_info);
typedef int (__cdecl *napi_create_function_fn)(napi_env, const char*, unsigned long long, napi_callback, void*, napi_value*);
typedef int (__cdecl *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
typedef int (__cdecl *napi_get_boolean_fn)(napi_env, int, napi_value*);
typedef int (__cdecl *napi_get_cb_info_fn)(napi_env, napi_callback_info, unsigned long long*, napi_value*, napi_value*, void**);
typedef int (__cdecl *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, unsigned long long, unsigned long long*);
typedef SHORT (WINAPI *get_async_key_state_fn)(int);
static void* node_symbol(const char* name) {
HMODULE module = GetModuleHandleA(0);
void* proc = module ? (void*)GetProcAddress(module, name) : 0;
if (proc) return proc;
module = GetModuleHandleA("node.dll");
return module ? (void*)GetProcAddress(module, name) : 0;
}
static int string_equals(const char* left, const char* right) {
while (*left && *right && *left == *right) {
left++;
right++;
}
return *left == 0 && *right == 0;
}
static get_async_key_state_fn get_async_key_state_symbol(void) {
static int loaded = 0;
static get_async_key_state_fn get_async_key_state = 0;
if (!loaded) {
HMODULE module = GetModuleHandleA("user32.dll");
if (!module) module = LoadLibraryA("user32.dll");
get_async_key_state = module ? (get_async_key_state_fn)GetProcAddress(module, "GetAsyncKeyState") : 0;
loaded = 1;
}
return get_async_key_state;
}
static int is_key_pressed(int virtual_key) {
get_async_key_state_fn get_async_key_state = get_async_key_state_symbol();
return get_async_key_state && (((unsigned short)get_async_key_state(virtual_key)) & KEY_PRESSED_MASK) != 0;
}
static int is_modifier_name_pressed(const char* name) {
if (string_equals(name, "shift")) return is_key_pressed(VK_SHIFT) || is_key_pressed(VK_LSHIFT) || is_key_pressed(VK_RSHIFT);
if (string_equals(name, "control")) return is_key_pressed(VK_CONTROL) || is_key_pressed(VK_LCONTROL) || is_key_pressed(VK_RCONTROL);
if (string_equals(name, "option") || string_equals(name, "alt")) return is_key_pressed(VK_MENU) || is_key_pressed(VK_LMENU) || is_key_pressed(VK_RMENU);
if (string_equals(name, "command") || string_equals(name, "super") || string_equals(name, "win")) return is_key_pressed(VK_LWIN) || is_key_pressed(VK_RWIN);
return 0;
}
static napi_value __cdecl enable_virtual_terminal_input(napi_env env, napi_callback_info info) {
(void)info;
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
int enabled = handle != INVALID_HANDLE_VALUE &&
GetConsoleMode(handle, &mode) &&
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_INPUT);
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
napi_value result = 0;
if (napi_get_boolean) napi_get_boolean(env, enabled, &result);
return result;
}
static napi_value __cdecl is_modifier_pressed(napi_env env, napi_callback_info info) {
napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
napi_get_value_string_utf8_fn napi_get_value_string_utf8 = (napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
int pressed = 0;
if (napi_get_cb_info && napi_get_value_string_utf8) {
unsigned long long argc = 1;
napi_value args[1] = {0};
if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) {
char name[16] = {0};
unsigned long long copied = 0;
if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) {
pressed = is_modifier_name_pressed(name);
}
}
}
napi_value result = 0;
if (napi_get_boolean) napi_get_boolean(env, pressed, &result);
return result;
}
static void set_function_export(napi_env env, napi_value exports, const char* name, napi_callback callback) {
napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function");
napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property");
napi_value fn = 0;
if (napi_create_function &&
napi_set_named_property &&
napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, 0, &fn) == 0) {
napi_set_named_property(env, exports, name, fn);
}
}
__declspec(dllexport) napi_value __cdecl napi_register_module_v1(napi_env env, napi_value exports) {
set_function_export(env, exports, "enableVirtualTerminalInput", enable_virtual_terminal_input);
set_function_export(env, exports, "isModifierPressed", is_modifier_pressed);
return exports;
}

View File

@@ -0,0 +1,241 @@
#define PI_CLIPBOARD_WRITE
#include "../../clipboard.h"
#ifndef BI_ALPHABITFIELDS
#define BI_ALPHABITFIELDS 6
#endif
#define KEY_PRESSED_MASK 0x8000
#define OPEN_CLIPBOARD_ATTEMPTS 10
#define OPEN_CLIPBOARD_RETRY_MS 5
#define BITMAP_FILE_HEADER_SIZE 14
static int string_equals(const char* left, const char* right) {
while (*left && *right && *left == *right) {
left++;
right++;
}
return *left == 0 && *right == 0;
}
static int is_key_pressed(int virtual_key) {
return ((unsigned short)GetAsyncKeyState(virtual_key) & KEY_PRESSED_MASK) != 0;
}
static int is_modifier_name_pressed(const char* name) {
if (string_equals(name, "shift")) return is_key_pressed(VK_SHIFT) || is_key_pressed(VK_LSHIFT) || is_key_pressed(VK_RSHIFT);
if (string_equals(name, "control")) return is_key_pressed(VK_CONTROL) || is_key_pressed(VK_LCONTROL) || is_key_pressed(VK_RCONTROL);
if (string_equals(name, "option") || string_equals(name, "alt")) return is_key_pressed(VK_MENU) || is_key_pressed(VK_LMENU) || is_key_pressed(VK_RMENU);
if (string_equals(name, "command") || string_equals(name, "super") || string_equals(name, "win")) return is_key_pressed(VK_LWIN) || is_key_pressed(VK_RWIN);
return 0;
}
static napi_value __cdecl enable_virtual_terminal_input(napi_env env, napi_callback_info info) {
(void)info;
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
bool enabled = handle != INVALID_HANDLE_VALUE &&
GetConsoleMode(handle, &mode) &&
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_INPUT);
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
napi_value result = 0;
if (!napi_get_boolean || napi_get_boolean(env, enabled, &result) != 0) {
return fail(env, "Could not configure console input");
}
return result;
}
static napi_value __cdecl is_modifier_pressed(napi_env env, napi_callback_info info) {
napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
napi_get_value_string_utf8_fn napi_get_value_string_utf8 =
(napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
bool pressed = false;
if (napi_get_cb_info && napi_get_value_string_utf8) {
size_t argc = 1;
napi_value args[1] = {0};
if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) {
char name[16] = {0};
size_t copied = 0;
if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) {
pressed = is_modifier_name_pressed(name);
}
}
}
napi_value result = 0;
if (!napi_get_boolean || napi_get_boolean(env, pressed, &result) != 0) {
return fail(env, "Could not inspect modifier state");
}
return result;
}
static bool open_clipboard(HWND owner) {
for (int attempt = 0; attempt < OPEN_CLIPBOARD_ATTEMPTS; attempt++) {
if (OpenClipboard(owner)) return true;
Sleep(OPEN_CLIPBOARD_RETRY_MS);
}
return false;
}
static uint16_t read_u16(const uint8_t* data) {
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
static uint32_t read_u32(const uint8_t* data) {
return (uint32_t)data[0] |
((uint32_t)data[1] << 8) |
((uint32_t)data[2] << 16) |
((uint32_t)data[3] << 24);
}
static void write_u32(uint8_t* data, uint32_t value) {
data[0] = (uint8_t)value;
data[1] = (uint8_t)(value >> 8);
data[2] = (uint8_t)(value >> 16);
data[3] = (uint8_t)(value >> 24);
}
static void copy_bytes(uint8_t* destination, const uint8_t* source, size_t length) {
volatile uint8_t* target = destination;
const volatile uint8_t* input = source;
for (size_t index = 0; index < length; index++) target[index] = input[index];
}
static size_t dib_pixel_offset(const uint8_t* dib, size_t size) {
if (size < 12) return 0;
uint32_t header_size = read_u32(dib);
if (header_size == 12) {
uint16_t bits_per_pixel = read_u16(dib + 10);
uint32_t color_count = bits_per_pixel <= 8 ? 1u << bits_per_pixel : 0;
size_t offset = header_size + (size_t)color_count * 3;
return offset <= size ? offset : 0;
}
if (header_size < 40 || header_size > size) return 0;
uint16_t bits_per_pixel = read_u16(dib + 14);
uint32_t compression = read_u32(dib + 16);
uint32_t color_count = read_u32(dib + 32);
if (!color_count && bits_per_pixel <= 8) color_count = 1u << bits_per_pixel;
size_t offset = header_size + (size_t)color_count * 4;
if (header_size == 40 && compression == BI_BITFIELDS) offset += 3 * sizeof(uint32_t);
if (header_size == 40 && compression == BI_ALPHABITFIELDS) offset += 4 * sizeof(uint32_t);
return offset <= size ? offset : 0;
}
static void read_clipboard_image(clipboard_job* job) {
UINT png_format = RegisterClipboardFormatW(L"PNG");
if (!(png_format && IsClipboardFormatAvailable(png_format)) &&
!IsClipboardFormatAvailable(CF_DIBV5) && !IsClipboardFormatAvailable(CF_DIB)) {
job->format = CLIPBOARD_EMPTY;
return;
}
HGLOBAL handle = png_format && IsClipboardFormatAvailable(png_format)
? (HGLOBAL)GetClipboardData(png_format) : 0;
bool is_png = handle != 0;
if (!handle && IsClipboardFormatAvailable(CF_DIBV5)) handle = (HGLOBAL)GetClipboardData(CF_DIBV5);
if (!handle && IsClipboardFormatAvailable(CF_DIB)) handle = (HGLOBAL)GetClipboardData(CF_DIB);
const uint8_t* source = handle ? (const uint8_t*)GlobalLock(handle) : 0;
size_t size = handle ? GlobalSize(handle) : 0;
if (!source || !size) {
if (source) GlobalUnlock(handle);
job->error = "Clipboard does not contain an image";
return;
}
if (is_png) {
clipboard_copy(job, source, size, CLIPBOARD_BUFFER);
} else {
size_t offset = dib_pixel_offset(source, size);
uint8_t* bitmap = offset && size <= UINT32_MAX - BITMAP_FILE_HEADER_SIZE
? clipboard_alloc(size + BITMAP_FILE_HEADER_SIZE) : 0;
if (bitmap) {
bitmap[0] = 'B';
bitmap[1] = 'M';
write_u32(bitmap + 2, (uint32_t)(size + BITMAP_FILE_HEADER_SIZE));
write_u32(bitmap + 10, (uint32_t)(BITMAP_FILE_HEADER_SIZE + offset));
copy_bytes(bitmap + BITMAP_FILE_HEADER_SIZE, source, size);
job->data = bitmap;
job->length = size + BITMAP_FILE_HEADER_SIZE;
job->format = CLIPBOARD_BUFFER;
} else {
job->error = "Could not create clipboard image buffer";
}
}
GlobalUnlock(handle);
}
static void clipboard_execute(clipboard_job* job) {
// A real clipboard owner is required for text writes, including under Node
// without a console. Create and destroy it on the same worker thread.
HWND owner = 0;
if (job->operation == CLIPBOARD_WRITE) {
owner = CreateWindowExW(0, L"STATIC", 0, 0, 0, 0, 0, 0,
HWND_MESSAGE, 0, GetModuleHandleA(0), 0);
if (!owner) {
job->error = "Could not create clipboard owner window";
return;
}
}
if (!open_clipboard(owner)) {
if (owner) DestroyWindow(owner);
job->error = "Could not open clipboard";
return;
}
if (job->operation == CLIPBOARD_WRITE) {
size_t size = (job->length + 1) * sizeof(uint16_t);
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, size);
void* text = handle ? GlobalLock(handle) : 0;
bool success = false;
if (text) {
copy_bytes(text, job->data, size);
GlobalUnlock(handle);
success = EmptyClipboard() && SetClipboardData(CF_UNICODETEXT, handle) != 0;
}
if (!success) {
if (handle) GlobalFree(handle);
job->error = "Could not set clipboard text";
}
} else if (job->operation == CLIPBOARD_TEXT) {
if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) {
job->format = CLIPBOARD_EMPTY;
} else {
HGLOBAL handle = (HGLOBAL)GetClipboardData(CF_UNICODETEXT);
const uint16_t* text = handle ? GlobalLock(handle) : 0;
if (text) {
size_t capacity = GlobalSize(handle) / sizeof(uint16_t);
size_t length = 0;
while (length < capacity && text[length]) length++;
clipboard_copy(job, text, length * sizeof(uint16_t), CLIPBOARD_UTF16);
GlobalUnlock(handle);
} else {
job->error = "Clipboard does not contain text";
}
}
} else {
read_clipboard_image(job);
}
CloseClipboard();
if (owner) DestroyWindow(owner);
}
BOOL WINAPI _DllMainCRTStartup(HINSTANCE instance, DWORD reason, LPVOID reserved) {
(void)instance;
(void)reason;
(void)reserved;
return TRUE;
}
PI_NAPI_EXPORT napi_value PI_NAPI_CALL napi_register_module_v1(napi_env env, napi_value exports) {
set_function_export(env, exports, "enableVirtualTerminalInput", enable_virtual_terminal_input);
set_function_export(env, exports, "isModifierPressed", is_modifier_pressed);
set_function_export(env, exports, "getText", get_clipboard_text);
set_function_export(env, exports, "setText", set_clipboard_text);
set_function_export(env, exports, "getImage", get_clipboard_image);
return exports;
}