@code-yeongyu/senpi-tui 2026.9.12 → 2026.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -4
- package/dist/alt-screen-search.d.ts +21 -1
- package/dist/alt-screen-search.d.ts.map +1 -1
- package/dist/alt-screen-search.js +199 -42
- package/dist/alt-screen-search.js.map +1 -1
- package/dist/components/box.d.ts +3 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +28 -0
- package/dist/components/box.js.map +1 -1
- package/dist/components/editor.d.ts +6 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +98 -18
- package/dist/components/editor.js.map +1 -1
- package/dist/components/input.d.ts +12 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +40 -6
- package/dist/components/input.js.map +1 -1
- package/dist/components/latex.d.ts.map +1 -1
- package/dist/components/latex.js +7 -0
- package/dist/components/latex.js.map +1 -1
- package/dist/components/loader.d.ts +3 -0
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +20 -8
- package/dist/components/loader.js.map +1 -1
- package/dist/components/mouse-region.d.ts +12 -0
- package/dist/components/mouse-region.d.ts.map +1 -0
- package/dist/components/mouse-region.js +19 -0
- package/dist/components/mouse-region.js.map +1 -0
- package/dist/components/scroll-view.d.ts +6 -3
- package/dist/components/scroll-view.d.ts.map +1 -1
- package/dist/components/scroll-view.js +6 -1
- package/dist/components/scroll-view.js.map +1 -1
- package/dist/components/select-list.d.ts +4 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +48 -2
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +5 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +54 -5
- package/dist/components/settings-list.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +3 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +32 -9
- package/dist/layout.js.map +1 -1
- package/dist/native-modifiers.d.ts +2 -1
- package/dist/native-modifiers.d.ts.map +1 -1
- package/dist/native-modifiers.js +3 -44
- package/dist/native-modifiers.js.map +1 -1
- package/dist/native-platform.d.ts +18 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +42 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +2 -20
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +31 -1
- package/dist/tui-alt-screen.d.ts.map +1 -1
- package/dist/tui-alt-screen.js +335 -41
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui.d.ts +83 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +118 -4
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -1
- package/native/clipboard.h +170 -0
- package/native/darwin/README.md +8 -6
- package/native/darwin/build.sh +12 -5
- package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/prebuilds/darwin-x64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/src/darwin-platform.m +93 -0
- package/native/linux/README.md +23 -0
- package/native/linux/build.sh +65 -0
- package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/native/linux/src/clipboard-worker.h +70 -0
- package/native/linux/src/linux-platform-x11.c +274 -0
- package/native/napi.h +89 -0
- package/native/win32/README.md +18 -7
- package/native/win32/build.mjs +7 -4
- package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/native/win32/src/win32-platform.c +241 -0
- package/package.json +10 -3
- package/native/darwin/src/darwin-modifiers.c +0 -70
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/native/win32/src/win32-console-mode.c +0 -119
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#ifndef PI_CLIPBOARD_H
|
|
2
|
+
#define PI_CLIPBOARD_H
|
|
3
|
+
|
|
4
|
+
#include "napi.h"
|
|
5
|
+
#ifndef _WIN32
|
|
6
|
+
#include <stdlib.h>
|
|
7
|
+
#include <string.h>
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE } clipboard_operation;
|
|
11
|
+
typedef enum { CLIPBOARD_UNAVAILABLE, CLIPBOARD_EMPTY, CLIPBOARD_UTF8, CLIPBOARD_LATIN1, CLIPBOARD_UTF16, CLIPBOARD_BUFFER } clipboard_format;
|
|
12
|
+
|
|
13
|
+
typedef struct {
|
|
14
|
+
napi_async_work work;
|
|
15
|
+
napi_deferred deferred;
|
|
16
|
+
clipboard_operation operation;
|
|
17
|
+
clipboard_format format;
|
|
18
|
+
void* data;
|
|
19
|
+
size_t length;
|
|
20
|
+
const char* error;
|
|
21
|
+
} clipboard_job;
|
|
22
|
+
|
|
23
|
+
// Only this function runs on the worker thread. It must not call N-API.
|
|
24
|
+
static void clipboard_execute(clipboard_job* job);
|
|
25
|
+
|
|
26
|
+
static void* clipboard_alloc(size_t size) {
|
|
27
|
+
#ifdef _WIN32
|
|
28
|
+
return GlobalAlloc(GPTR, size);
|
|
29
|
+
#else
|
|
30
|
+
return calloc(1, size);
|
|
31
|
+
#endif
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static void clipboard_free(void* data) {
|
|
35
|
+
#ifdef _WIN32
|
|
36
|
+
if (data) GlobalFree(data);
|
|
37
|
+
#else
|
|
38
|
+
free(data);
|
|
39
|
+
#endif
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#ifdef PI_CLIPBOARD_WRITE
|
|
43
|
+
static void clipboard_copy(clipboard_job* job, const void* data, size_t length, clipboard_format format) {
|
|
44
|
+
job->data = clipboard_alloc(length ? length : 1);
|
|
45
|
+
if (!job->data) {
|
|
46
|
+
job->error = "Out of memory";
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
#ifdef _WIN32
|
|
50
|
+
volatile uint8_t* target = job->data;
|
|
51
|
+
const volatile uint8_t* source = data;
|
|
52
|
+
for (size_t index = 0; index < length; index++) target[index] = source[index];
|
|
53
|
+
#else
|
|
54
|
+
memcpy(job->data, data, length);
|
|
55
|
+
#endif
|
|
56
|
+
job->length = format == CLIPBOARD_UTF16 ? length / sizeof(uint16_t) : length;
|
|
57
|
+
job->format = format;
|
|
58
|
+
}
|
|
59
|
+
#endif
|
|
60
|
+
|
|
61
|
+
static void PI_NAPI_CALL execute_clipboard_work(napi_env env, void* data) {
|
|
62
|
+
(void)env;
|
|
63
|
+
clipboard_execute(data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* data) {
|
|
67
|
+
clipboard_job* job = data;
|
|
68
|
+
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8");
|
|
69
|
+
napi_value result = undefined_value(env);
|
|
70
|
+
if (status != 0) job->error = "Clipboard operation cancelled";
|
|
71
|
+
if (!job->error && job->operation != CLIPBOARD_WRITE) {
|
|
72
|
+
if (job->format == CLIPBOARD_EMPTY) {
|
|
73
|
+
result = null_value(env);
|
|
74
|
+
} else if (job->format == CLIPBOARD_BUFFER) {
|
|
75
|
+
napi_create_buffer_copy_fn create_buffer = (napi_create_buffer_copy_fn)node_symbol("napi_create_buffer_copy");
|
|
76
|
+
status = create_buffer(env, job->length, job->data, 0, &result);
|
|
77
|
+
} else if (job->format == CLIPBOARD_UTF16) {
|
|
78
|
+
napi_create_string_utf16_fn create_utf16 = (napi_create_string_utf16_fn)node_symbol("napi_create_string_utf16");
|
|
79
|
+
status = create_utf16(env, job->data, job->length, &result);
|
|
80
|
+
} else if (job->format == CLIPBOARD_UTF8 || job->format == CLIPBOARD_LATIN1) {
|
|
81
|
+
napi_create_string_utf8_fn create_text = job->format == CLIPBOARD_LATIN1
|
|
82
|
+
? (napi_create_string_utf8_fn)node_symbol("napi_create_string_latin1") : create_string;
|
|
83
|
+
status = create_text(env, job->data, job->length, &result);
|
|
84
|
+
}
|
|
85
|
+
if (status != 0) job->error = "Could not create clipboard value";
|
|
86
|
+
}
|
|
87
|
+
if (job->error) {
|
|
88
|
+
napi_create_error_fn create_error = (napi_create_error_fn)node_symbol("napi_create_error");
|
|
89
|
+
napi_value message;
|
|
90
|
+
create_string(env, job->error, NAPI_AUTO_LENGTH, &message);
|
|
91
|
+
create_error(env, 0, message, &result);
|
|
92
|
+
}
|
|
93
|
+
napi_settle_deferred_fn settle = (napi_settle_deferred_fn)node_symbol(
|
|
94
|
+
job->error ? "napi_reject_deferred" : "napi_resolve_deferred"
|
|
95
|
+
);
|
|
96
|
+
settle(env, job->deferred, result);
|
|
97
|
+
napi_async_work_fn delete_work = (napi_async_work_fn)node_symbol("napi_delete_async_work");
|
|
98
|
+
delete_work(env, job->work);
|
|
99
|
+
clipboard_free(job->data);
|
|
100
|
+
clipboard_free(job);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static napi_value queue_clipboard(napi_env env, napi_callback_info info, clipboard_operation operation) {
|
|
104
|
+
napi_create_async_work_fn create_work = (napi_create_async_work_fn)node_symbol("napi_create_async_work");
|
|
105
|
+
napi_async_work_fn queue_work = (napi_async_work_fn)node_symbol("napi_queue_async_work");
|
|
106
|
+
napi_async_work_fn delete_work = (napi_async_work_fn)node_symbol("napi_delete_async_work");
|
|
107
|
+
napi_create_promise_fn create_promise = (napi_create_promise_fn)node_symbol("napi_create_promise");
|
|
108
|
+
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8");
|
|
109
|
+
if (!create_work || !queue_work || !delete_work || !create_promise || !create_string) {
|
|
110
|
+
return fail(env, "Async clipboard API unavailable");
|
|
111
|
+
}
|
|
112
|
+
clipboard_job* job = clipboard_alloc(sizeof(*job));
|
|
113
|
+
if (!job) return fail(env, "Out of memory");
|
|
114
|
+
job->operation = operation;
|
|
115
|
+
#ifdef PI_CLIPBOARD_WRITE
|
|
116
|
+
if (operation == CLIPBOARD_WRITE) {
|
|
117
|
+
napi_get_cb_info_fn get_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
|
|
118
|
+
#ifdef _WIN32
|
|
119
|
+
napi_get_value_string_utf16_fn get_string = (napi_get_value_string_utf16_fn)node_symbol("napi_get_value_string_utf16");
|
|
120
|
+
size_t unit_size = sizeof(uint16_t);
|
|
121
|
+
#else
|
|
122
|
+
napi_get_value_string_utf8_fn get_string = (napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
|
|
123
|
+
size_t unit_size = 1;
|
|
124
|
+
#endif
|
|
125
|
+
size_t argc = 1;
|
|
126
|
+
napi_value argument = 0;
|
|
127
|
+
if (!get_info || !get_string || get_info(env, info, &argc, &argument, 0, 0) != 0 || argc == 0 ||
|
|
128
|
+
get_string(env, argument, 0, 0, &job->length) != 0) {
|
|
129
|
+
clipboard_free(job);
|
|
130
|
+
return fail(env, "setText requires a string");
|
|
131
|
+
}
|
|
132
|
+
job->data = job->length < SIZE_MAX / unit_size
|
|
133
|
+
? clipboard_alloc((job->length + 1) * unit_size) : 0;
|
|
134
|
+
if (!job->data || get_string(env, argument, job->data, job->length + 1, &job->length) != 0) {
|
|
135
|
+
clipboard_free(job->data);
|
|
136
|
+
clipboard_free(job);
|
|
137
|
+
return fail(env, "Could not read clipboard text");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
#else
|
|
141
|
+
(void)info;
|
|
142
|
+
#endif
|
|
143
|
+
napi_value name = 0;
|
|
144
|
+
napi_value promise = 0;
|
|
145
|
+
if (create_string(env, "pi.clipboard", NAPI_AUTO_LENGTH, &name) != 0 ||
|
|
146
|
+
create_work(env, 0, name, execute_clipboard_work, complete_clipboard_work, job, &job->work) != 0 ||
|
|
147
|
+
create_promise(env, &job->deferred, &promise) != 0 || queue_work(env, job->work) != 0) {
|
|
148
|
+
if (job->work) delete_work(env, job->work);
|
|
149
|
+
clipboard_free(job->data);
|
|
150
|
+
clipboard_free(job);
|
|
151
|
+
return fail(env, "Could not queue clipboard operation");
|
|
152
|
+
}
|
|
153
|
+
return promise;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static napi_value PI_NAPI_CALL get_clipboard_text(napi_env env, napi_callback_info info) {
|
|
157
|
+
return queue_clipboard(env, info, CLIPBOARD_TEXT);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static napi_value PI_NAPI_CALL get_clipboard_image(napi_env env, napi_callback_info info) {
|
|
161
|
+
return queue_clipboard(env, info, CLIPBOARD_IMAGE);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#ifdef PI_CLIPBOARD_WRITE
|
|
165
|
+
static napi_value PI_NAPI_CALL set_clipboard_text(napi_env env, napi_callback_info info) {
|
|
166
|
+
return queue_clipboard(env, info, CLIPBOARD_WRITE);
|
|
167
|
+
}
|
|
168
|
+
#endif
|
|
169
|
+
|
|
170
|
+
#endif
|
package/native/darwin/README.md
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# Darwin native prebuilds
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Provides modifier-key state and asynchronous text/image clipboard access using AppKit.
|
|
4
|
+
|
|
5
|
+
## Building
|
|
6
|
+
|
|
7
|
+
On macOS, run from the repository root:
|
|
4
8
|
|
|
5
9
|
```sh
|
|
6
10
|
bun run --cwd packages/tui build:native:darwin
|
|
7
11
|
```
|
|
8
12
|
|
|
9
|
-
The build uses
|
|
13
|
+
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+).
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Cross-building requires a Darwin toolchain with a macOS SDK and Mach-O linker, such as osxcross:
|
|
12
16
|
|
|
13
17
|
```sh
|
|
14
18
|
CC=/path/to/osxcross/clang SDKROOT=/path/to/MacOSX.sdk \
|
|
15
19
|
bun run --cwd packages/tui build:native:darwin
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
The SDK must be obtained and used
|
|
19
|
-
|
|
20
|
-
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.
|
|
22
|
+
The SDK must be obtained and used under Apple's license. Plain clang or Zig alone is not sufficient.
|
package/native/darwin/build.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
-
source_file="$script_dir/src/darwin-
|
|
6
|
+
source_file="$script_dir/src/darwin-platform.m"
|
|
7
7
|
|
|
8
8
|
if [[ -n "${CC:-}" ]]; then
|
|
9
9
|
compiler="$CC"
|
|
@@ -37,25 +37,32 @@ build() {
|
|
|
37
37
|
local arch="$1"
|
|
38
38
|
local target="$2"
|
|
39
39
|
local output_dir="$script_dir/prebuilds/darwin-$arch"
|
|
40
|
-
local temporary_output="$temporary_dir/darwin-$arch/darwin-
|
|
40
|
+
local temporary_output="$temporary_dir/darwin-$arch/darwin-platform.node"
|
|
41
41
|
|
|
42
42
|
mkdir -p "$(dirname "$temporary_output")"
|
|
43
43
|
"$compiler" \
|
|
44
|
+
-fobjc-arc \
|
|
44
45
|
-std=c11 \
|
|
45
46
|
-Wall \
|
|
46
47
|
-Wextra \
|
|
47
|
-
-
|
|
48
|
+
-Oz \
|
|
49
|
+
-flto \
|
|
50
|
+
-fvisibility=hidden \
|
|
48
51
|
"--target=$target" \
|
|
49
52
|
"${sdk_flags[@]}" \
|
|
50
53
|
-bundle \
|
|
51
54
|
-undefined dynamic_lookup \
|
|
55
|
+
-Wl,-dead_strip \
|
|
56
|
+
-Wl,-x \
|
|
57
|
+
-framework AppKit \
|
|
52
58
|
-framework CoreGraphics \
|
|
59
|
+
-framework Foundation \
|
|
53
60
|
"$source_file" \
|
|
54
61
|
-o "$temporary_output"
|
|
55
62
|
|
|
56
63
|
mkdir -p "$output_dir"
|
|
57
|
-
install -m 755 "$temporary_output" "$output_dir/darwin-
|
|
58
|
-
echo "Built $output_dir/darwin-
|
|
64
|
+
install -m 755 "$temporary_output" "$output_dir/darwin-platform.node"
|
|
65
|
+
echo "Built $output_dir/darwin-platform.node"
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
build arm64 arm64-apple-macos11.0
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#import <AppKit/AppKit.h>
|
|
2
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
3
|
+
#import <Foundation/Foundation.h>
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
#include <string.h>
|
|
6
|
+
#define PI_CLIPBOARD_WRITE
|
|
7
|
+
#include "../../clipboard.h"
|
|
8
|
+
|
|
9
|
+
static CGEventFlags modifier_mask_for_name(const char* name) {
|
|
10
|
+
if (strcmp(name, "shift") == 0) return kCGEventFlagMaskShift;
|
|
11
|
+
if (strcmp(name, "command") == 0) return kCGEventFlagMaskCommand;
|
|
12
|
+
if (strcmp(name, "control") == 0) return kCGEventFlagMaskControl;
|
|
13
|
+
if (strcmp(name, "option") == 0) return kCGEventFlagMaskAlternate;
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static napi_value is_modifier_pressed(napi_env env, napi_callback_info info) {
|
|
18
|
+
napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
|
|
19
|
+
napi_get_value_string_utf8_fn napi_get_value_string_utf8 =
|
|
20
|
+
(napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
|
|
21
|
+
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
|
|
22
|
+
|
|
23
|
+
bool pressed = false;
|
|
24
|
+
if (napi_get_cb_info && napi_get_value_string_utf8) {
|
|
25
|
+
size_t argc = 1;
|
|
26
|
+
napi_value args[1] = {0};
|
|
27
|
+
if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) {
|
|
28
|
+
char name[16] = {0};
|
|
29
|
+
size_t copied = 0;
|
|
30
|
+
if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) {
|
|
31
|
+
CGEventFlags mask = modifier_mask_for_name(name);
|
|
32
|
+
if (mask != 0) {
|
|
33
|
+
CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
|
|
34
|
+
pressed = (flags & mask) != 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
napi_value result = 0;
|
|
41
|
+
if (!napi_get_boolean || napi_get_boolean(env, pressed, &result) != 0) {
|
|
42
|
+
return fail(env, "Could not inspect modifier state");
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static void clipboard_execute(clipboard_job* job) {
|
|
48
|
+
@autoreleasepool {
|
|
49
|
+
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
|
50
|
+
if (!pasteboard) {
|
|
51
|
+
job->error = "Could not open clipboard";
|
|
52
|
+
} else if (job->operation == CLIPBOARD_WRITE) {
|
|
53
|
+
NSString* text = [[NSString alloc] initWithBytes:job->data length:job->length encoding:NSUTF8StringEncoding];
|
|
54
|
+
if (!text) {
|
|
55
|
+
job->error = "Clipboard text is not valid UTF-8";
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
[pasteboard clearContents];
|
|
59
|
+
if (![pasteboard setString:text forType:NSPasteboardTypeString]) job->error = "Could not set clipboard text";
|
|
60
|
+
} else if (job->operation == CLIPBOARD_TEXT) {
|
|
61
|
+
NSString* text = [pasteboard stringForType:NSPasteboardTypeString];
|
|
62
|
+
if (!text) {
|
|
63
|
+
job->format = CLIPBOARD_EMPTY;
|
|
64
|
+
} else if (!text.UTF8String) {
|
|
65
|
+
job->error = "Could not encode clipboard text";
|
|
66
|
+
} else {
|
|
67
|
+
clipboard_copy(job, text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding], CLIPBOARD_UTF8);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) {
|
|
71
|
+
job->format = CLIPBOARD_EMPTY;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
NSData* png = [pasteboard dataForType:NSPasteboardTypePNG];
|
|
75
|
+
if (!png) {
|
|
76
|
+
NSImage* image = [[NSImage alloc] initWithPasteboard:pasteboard];
|
|
77
|
+
NSData* tiff = image.TIFFRepresentation;
|
|
78
|
+
NSBitmapImageRep* bitmap = tiff ? [NSBitmapImageRep imageRepWithData:tiff] : nil;
|
|
79
|
+
png = bitmap ? [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}] : nil;
|
|
80
|
+
}
|
|
81
|
+
if (png) clipboard_copy(job, png.bytes, png.length, CLIPBOARD_BUFFER);
|
|
82
|
+
else job->error = "Clipboard does not contain an image";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) {
|
|
88
|
+
set_function_export(env, exports, "isModifierPressed", is_modifier_pressed);
|
|
89
|
+
set_function_export(env, exports, "getText", get_clipboard_text);
|
|
90
|
+
set_function_export(env, exports, "setText", set_clipboard_text);
|
|
91
|
+
set_function_export(env, exports, "getImage", get_clipboard_image);
|
|
92
|
+
return exports;
|
|
93
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Linux clipboard helper
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Building
|
|
8
|
+
|
|
9
|
+
Install a C compiler and XCB development headers, then run from the repository root on each supported Linux architecture:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun run --cwd packages/tui build:native:linux
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Testing
|
|
16
|
+
|
|
17
|
+
Install the build dependencies plus `pkg-config`, `Xvfb`, and `xclip`, then run from `packages/tui`:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
node --test test/native-clipboard-linux.test.ts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Tests use isolated X11 servers, not the desktop clipboard. They skip when dependencies are missing.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
compiler="${CC:-cc}"
|
|
7
|
+
|
|
8
|
+
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
9
|
+
echo "Linux native helpers must be built on Linux" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
case "$(uname -m)" in
|
|
14
|
+
x86_64)
|
|
15
|
+
arch="x64"
|
|
16
|
+
;;
|
|
17
|
+
aarch64|arm64)
|
|
18
|
+
arch="arm64"
|
|
19
|
+
;;
|
|
20
|
+
*)
|
|
21
|
+
echo "Unsupported Linux architecture: $(uname -m)" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
;;
|
|
24
|
+
esac
|
|
25
|
+
|
|
26
|
+
if ! command -v "$compiler" >/dev/null 2>&1; then
|
|
27
|
+
echo "Linux C compiler not found: $compiler" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
build_dir="$(mktemp -d "${TMPDIR:-/tmp}/pi-tui-linux.XXXXXX")"
|
|
31
|
+
trap 'rm -rf "$build_dir"' EXIT
|
|
32
|
+
|
|
33
|
+
common_flags=(
|
|
34
|
+
-std=c11
|
|
35
|
+
-D_POSIX_C_SOURCE=200809L
|
|
36
|
+
-Wall
|
|
37
|
+
-Wextra
|
|
38
|
+
-Werror
|
|
39
|
+
-Os
|
|
40
|
+
-flto
|
|
41
|
+
-fPIC
|
|
42
|
+
-pthread
|
|
43
|
+
-fvisibility=hidden
|
|
44
|
+
-fno-stack-protector
|
|
45
|
+
-shared
|
|
46
|
+
-nostdlib
|
|
47
|
+
-Wl,--unresolved-symbols=ignore-all
|
|
48
|
+
-Wl,--no-as-needed
|
|
49
|
+
-Wl,-O2
|
|
50
|
+
-Wl,--gc-sections
|
|
51
|
+
# A timed-out private thread may outlive the Node environment that loaded us.
|
|
52
|
+
-Wl,-z,nodelete
|
|
53
|
+
-Wl,-s
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
x11_output="$build_dir/linux-platform-x11.node"
|
|
57
|
+
"$compiler" "${common_flags[@]}" \
|
|
58
|
+
"$script_dir/src/linux-platform-x11.c" \
|
|
59
|
+
-lxcb \
|
|
60
|
+
-o "$x11_output"
|
|
61
|
+
|
|
62
|
+
output_dir="$script_dir/prebuilds/linux-$arch"
|
|
63
|
+
mkdir -p "$output_dir"
|
|
64
|
+
install -m 755 "$x11_output" "$output_dir/linux-platform-x11.node"
|
|
65
|
+
printf 'Built %s\n' "$output_dir/linux-platform-x11.node"
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#include <pthread.h>
|
|
2
|
+
#include <time.h>
|
|
3
|
+
#include "../../clipboard.h"
|
|
4
|
+
|
|
5
|
+
static void read_clipboard(clipboard_job* job);
|
|
6
|
+
static pthread_mutex_t worker_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
7
|
+
static pthread_once_t worker_once = PTHREAD_ONCE_INIT;
|
|
8
|
+
static pthread_cond_t worker_changed;
|
|
9
|
+
static bool worker_ready, worker_busy, worker_waiting, worker_finished;
|
|
10
|
+
static clipboard_job worker_result;
|
|
11
|
+
static int (*signal_worker)(pthread_cond_t*);
|
|
12
|
+
static int (*wait_for_worker)(pthread_cond_t*, pthread_mutex_t*, const struct timespec*);
|
|
13
|
+
|
|
14
|
+
static void initialize_worker_condition(void) {
|
|
15
|
+
// Unversioned ELF imports otherwise bind obsolete condition variables on glibc x64.
|
|
16
|
+
int (*initialize)(pthread_cond_t*, const pthread_condattr_t*) = node_symbol("pthread_cond_init");
|
|
17
|
+
signal_worker = node_symbol("pthread_cond_signal");
|
|
18
|
+
wait_for_worker = node_symbol("pthread_cond_timedwait");
|
|
19
|
+
if (!initialize || !signal_worker || !wait_for_worker) return;
|
|
20
|
+
pthread_condattr_t attributes;
|
|
21
|
+
if (pthread_condattr_init(&attributes) != 0) return;
|
|
22
|
+
worker_ready = pthread_condattr_setclock(&attributes, CLOCK_MONOTONIC) == 0 &&
|
|
23
|
+
initialize(&worker_changed, &attributes) == 0;
|
|
24
|
+
pthread_condattr_destroy(&attributes);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static void* read_on_private_thread(void* unused) {
|
|
28
|
+
(void)unused;
|
|
29
|
+
read_clipboard(&worker_result);
|
|
30
|
+
pthread_mutex_lock(&worker_mutex);
|
|
31
|
+
worker_finished = true;
|
|
32
|
+
if (!worker_waiting) {
|
|
33
|
+
free(worker_result.data); // The caller timed out; discard the late result.
|
|
34
|
+
worker_busy = false;
|
|
35
|
+
}
|
|
36
|
+
signal_worker(&worker_changed);
|
|
37
|
+
pthread_mutex_unlock(&worker_mutex);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static void clipboard_execute(clipboard_job* job) {
|
|
42
|
+
pthread_once(&worker_once, initialize_worker_condition);
|
|
43
|
+
if (!worker_ready) return;
|
|
44
|
+
pthread_mutex_lock(&worker_mutex);
|
|
45
|
+
if (!worker_busy) {
|
|
46
|
+
worker_busy = worker_waiting = true;
|
|
47
|
+
worker_finished = false;
|
|
48
|
+
worker_result = (clipboard_job){.operation = job->operation};
|
|
49
|
+
struct timespec deadline;
|
|
50
|
+
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
|
51
|
+
deadline.tv_sec += 3; // Allow the two-second transfer deadline to report its own error first.
|
|
52
|
+
pthread_t thread;
|
|
53
|
+
if (pthread_create(&thread, 0, read_on_private_thread, 0) == 0) {
|
|
54
|
+
pthread_detach(thread);
|
|
55
|
+
// Only this bounded wait occupies libuv. Other callers return unavailable.
|
|
56
|
+
while (!worker_finished && wait_for_worker(&worker_changed, &worker_mutex, &deadline) == 0) {}
|
|
57
|
+
if (worker_finished) {
|
|
58
|
+
job->data = worker_result.data;
|
|
59
|
+
job->length = worker_result.length;
|
|
60
|
+
job->format = worker_result.format;
|
|
61
|
+
job->error = worker_result.error;
|
|
62
|
+
worker_busy = false;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
worker_busy = false;
|
|
66
|
+
}
|
|
67
|
+
worker_waiting = false;
|
|
68
|
+
}
|
|
69
|
+
pthread_mutex_unlock(&worker_mutex);
|
|
70
|
+
}
|