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:
208
third_party/pi/packages/tui/native/clipboard.h
vendored
Normal file
208
third_party/pi/packages/tui/native/clipboard.h
vendored
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
third_party/pi/packages/tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node
vendored
Executable file
BIN
third_party/pi/packages/tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node
vendored
Executable file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
132
third_party/pi/packages/tui/native/darwin/src/darwin-platform.m
vendored
Normal file
132
third_party/pi/packages/tui/native/darwin/src/darwin-platform.m
vendored
Normal 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;
|
||||
}
|
||||
23
third_party/pi/packages/tui/native/linux/README.md
vendored
Normal file
23
third_party/pi/packages/tui/native/linux/README.md
vendored
Normal 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.
|
||||
65
third_party/pi/packages/tui/native/linux/build.sh
vendored
Executable file
65
third_party/pi/packages/tui/native/linux/build.sh
vendored
Executable 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"
|
||||
BIN
third_party/pi/packages/tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node
vendored
Executable file
BIN
third_party/pi/packages/tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node
vendored
Executable file
Binary file not shown.
BIN
third_party/pi/packages/tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node
vendored
Executable file
BIN
third_party/pi/packages/tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node
vendored
Executable file
Binary file not shown.
70
third_party/pi/packages/tui/native/linux/src/clipboard-worker.h
vendored
Normal file
70
third_party/pi/packages/tui/native/linux/src/clipboard-worker.h
vendored
Normal 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);
|
||||
}
|
||||
274
third_party/pi/packages/tui/native/linux/src/linux-platform-x11.c
vendored
Normal file
274
third_party/pi/packages/tui/native/linux/src/linux-platform-x11.c
vendored
Normal 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;
|
||||
}
|
||||
91
third_party/pi/packages/tui/native/napi.h
vendored
Normal file
91
third_party/pi/packages/tui/native/napi.h
vendored
Normal 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
|
||||
@@ -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.**
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
Binary file not shown.
BIN
third_party/pi/packages/tui/native/win32/prebuilds/win32-arm64/win32-platform.node
vendored
Executable file
BIN
third_party/pi/packages/tui/native/win32/prebuilds/win32-arm64/win32-platform.node
vendored
Executable file
Binary file not shown.
Binary file not shown.
BIN
third_party/pi/packages/tui/native/win32/prebuilds/win32-x64/win32-platform.node
vendored
Executable file
BIN
third_party/pi/packages/tui/native/win32/prebuilds/win32-x64/win32-platform.node
vendored
Executable file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
241
third_party/pi/packages/tui/native/win32/src/win32-platform.c
vendored
Normal file
241
third_party/pi/packages/tui/native/win32/src/win32-platform.c
vendored
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user