@visual-z/xeno 0.1.3 → 0.2.0
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/CHANGELOG.md +19 -0
- package/README.md +143 -33
- package/docs/install.md +18 -6
- package/docs/providers.md +147 -0
- package/package.json +8 -2
- package/src/apps/cli.ts +42 -14
- package/src/apps/commands.ts +32 -4
- package/src/apps/completions.ts +9 -6
- package/src/apps/export-cli.ts +24 -0
- package/src/apps/extension-ui.ts +330 -0
- package/src/apps/input.ts +16 -1
- package/src/apps/model-catalog.ts +42 -0
- package/src/apps/project-trust.ts +49 -0
- package/src/apps/render.ts +1 -0
- package/src/apps/resource-browser.ts +75 -0
- package/src/apps/resources.ts +25 -0
- package/src/apps/session-browser.ts +113 -0
- package/src/apps/session-picker.ts +55 -0
- package/src/apps/shell.ts +1177 -378
- package/src/apps/startup.ts +400 -268
- package/src/core/action-defaults.ts +306 -0
- package/src/core/attachment-input.ts +17 -0
- package/src/core/catalog-merge.ts +19 -0
- package/src/core/event-validation.ts +19 -2
- package/src/core/events.ts +45 -2
- package/src/core/export.ts +6 -2
- package/src/core/extension-flags.ts +14 -0
- package/src/core/images.ts +46 -0
- package/src/core/json-event.ts +2 -0
- package/src/core/keymap.ts +43 -119
- package/src/core/model-replay.ts +22 -0
- package/src/core/model-scope.ts +53 -0
- package/src/core/packages.ts +47 -0
- package/src/core/progress.ts +9 -0
- package/src/core/project.ts +21 -3
- package/src/core/prompts.ts +33 -5
- package/src/core/queue.ts +18 -0
- package/src/core/resource-selection.ts +36 -0
- package/src/{runtime → core}/resume.ts +15 -22
- package/src/core/session-archive.ts +4 -0
- package/src/core/session-browser.ts +82 -0
- package/src/core/session-entry.ts +56 -0
- package/src/core/session-position.ts +26 -0
- package/src/core/settings.ts +169 -154
- package/src/core/theme.ts +15 -6
- package/src/core/thinking.ts +26 -0
- package/src/core/tokens.ts +2 -0
- package/src/core/transcript.ts +15 -5
- package/src/runtime/auth.ts +116 -11
- package/src/runtime/catalog.ts +54 -151
- package/src/runtime/clipboard.ts +114 -0
- package/src/runtime/extension-loader.ts +37 -0
- package/src/runtime/extension-ui.ts +55 -0
- package/src/runtime/extensions.ts +273 -0
- package/src/runtime/images.ts +76 -0
- package/src/runtime/loop.ts +213 -83
- package/src/runtime/model-config.ts +232 -0
- package/src/runtime/model-data.LICENSE +27 -0
- package/src/runtime/model-data.ts +21 -0
- package/src/runtime/model-replay.ts +10 -0
- package/src/runtime/model-scope.ts +20 -0
- package/src/runtime/model-sources.ts +151 -0
- package/src/runtime/model.ts +74 -11
- package/src/runtime/models-fetch.ts +6 -104
- package/src/runtime/oauth.ts +4 -3
- package/src/runtime/openai-responses.ts +12 -4
- package/src/runtime/openai.ts +13 -4
- package/src/runtime/package-manager.ts +125 -0
- package/src/runtime/pi-provider.ts +235 -0
- package/src/runtime/progress.ts +26 -0
- package/src/runtime/provider-access.ts +21 -0
- package/src/runtime/provider-protocols.ts +29 -0
- package/src/runtime/provider-request.ts +73 -0
- package/src/runtime/provider.ts +26 -6
- package/src/runtime/rich-text.ts +36 -0
- package/src/runtime/tools.ts +41 -5
- package/src/store/command.ts +7 -1
- package/src/store/context-files.ts +1 -1
- package/src/store/log.ts +65 -0
- package/src/store/models.ts +28 -9
- package/src/store/observations.ts +4 -2
- package/src/store/packages.ts +98 -0
- package/src/store/recovery.ts +8 -0
- package/src/store/resource-index.ts +170 -0
- package/src/store/resources.ts +75 -142
- package/src/store/session-archive.ts +162 -0
- package/src/store/session-browser.ts +68 -0
- package/src/store/session-position.ts +27 -0
- package/src/store/sessions.ts +87 -66
- package/src/store/trust.ts +13 -0
- package/src/store/workspace.ts +6 -0
- package/src/tui/chat-viewport.ts +47 -0
- package/src/tui/editor.ts +62 -15
- package/src/tui/host.ts +50 -9
- package/src/tui/input.ts +8 -2
- package/src/tui/select.ts +24 -6
- package/src/tui/settings.ts +4 -0
- package/src/tui/system-theme.ts +700 -0
- package/src/tui/transcript.ts +63 -11
- package/src/tui/tree.ts +161 -95
- package/src/vendor/pi-rich/LICENSE.upstream +21 -0
- package/src/vendor/pi-rich/UPSTREAM.json +17 -0
- package/src/vendor/pi-rich/clipboard-command.ts +44 -0
- package/src/vendor/pi-rich/exif-orientation.ts +182 -0
- package/src/vendor/pi-rich/html.ts +51 -0
- package/src/vendor/pi-rich/image-convert.ts +49 -0
- package/src/vendor/pi-rich/image-resize-core.ts +164 -0
- package/src/vendor/pi-rich/image-resize-worker.ts +42 -0
- package/src/vendor/pi-rich/image-resize.ts +123 -0
- package/src/vendor/pi-rich/mime.ts +116 -0
- package/src/vendor/pi-rich/photon.ts +139 -0
- package/src/vendor/pi-rich/syntax-highlight.ts +214 -0
- package/src/vendor/pi-tui/UPSTREAM.json +50 -0
- package/src/vendor/pi-tui/VENDOR.md +7 -5
- package/src/vendor/pi-tui/autocomplete.ts +35 -10
- package/src/vendor/pi-tui/colors.ts +367 -0
- package/src/vendor/pi-tui/components/box.ts +8 -9
- package/src/vendor/pi-tui/components/editor.ts +4 -2
- package/src/vendor/pi-tui/components/markdown.ts +8 -2
- package/src/vendor/pi-tui/index.ts +31 -1
- package/src/vendor/pi-tui/native/UPSTREAM.json +23 -0
- package/src/vendor/pi-tui/native/clipboard.h +208 -0
- package/src/vendor/pi-tui/native/darwin/README.md +22 -0
- package/src/vendor/pi-tui/native/darwin/build.sh +69 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +132 -0
- package/src/vendor/pi-tui/native/linux/README.md +23 -0
- package/src/vendor/pi-tui/native/linux/build.sh +65 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +70 -0
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +274 -0
- package/src/vendor/pi-tui/native/napi.h +91 -0
- package/src/vendor/pi-tui/native/win32/README.md +31 -0
- package/src/vendor/pi-tui/native/win32/build.mjs +232 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +241 -0
- package/src/vendor/pi-tui/native-platform.ts +2 -0
- package/src/vendor/pi-tui/oklab.ts +233 -0
- package/src/vendor/pi-tui/terminal-colors.ts +26 -8
- package/src/vendor/pi-tui/terminal-image.ts +43 -8
- package/src/vendor/pi-tui/terminal.ts +16 -9
- package/src/vendor/pi-tui/tui-alt-screen.ts +26 -18
- package/src/vendor/pi-tui/tui.ts +112 -75
- package/src/vendor/pi-tui/utils.ts +89 -49
- package/src/vendor/pi-tui/wheel-scroll.ts +82 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#include <xcb/xcb.h>
|
|
2
|
+
#include <string.h>
|
|
3
|
+
#include <xcb/xcbext.h>
|
|
4
|
+
#include <errno.h>
|
|
5
|
+
#include <poll.h>
|
|
6
|
+
#include <time.h>
|
|
7
|
+
#include "clipboard-worker.h"
|
|
8
|
+
|
|
9
|
+
#define MAX_CLIPBOARD_BYTES (50u * 1024u * 1024u)
|
|
10
|
+
#define CLIPBOARD_TIMEOUT_MS 2000
|
|
11
|
+
|
|
12
|
+
static const char* const text_types[] = {
|
|
13
|
+
"text/plain;charset=utf-8", "text/plain;charset=UTF-8", "UTF8_STRING", "text/plain", "STRING",
|
|
14
|
+
};
|
|
15
|
+
static const char* const image_types[] = {
|
|
16
|
+
"image/png", "image/jpeg", "image/webp", "image/gif", "image/bmp", "image/tiff",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
static int64_t monotonic_ms(void) {
|
|
20
|
+
struct timespec now;
|
|
21
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
22
|
+
return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
typedef struct {
|
|
26
|
+
unsigned char* data;
|
|
27
|
+
size_t length;
|
|
28
|
+
uint32_t items;
|
|
29
|
+
uint8_t format;
|
|
30
|
+
xcb_atom_t type;
|
|
31
|
+
} property_data;
|
|
32
|
+
|
|
33
|
+
typedef struct {
|
|
34
|
+
xcb_connection_t* connection;
|
|
35
|
+
xcb_window_t window;
|
|
36
|
+
xcb_atom_t clipboard;
|
|
37
|
+
xcb_atom_t property;
|
|
38
|
+
xcb_atom_t targets;
|
|
39
|
+
xcb_atom_t incr;
|
|
40
|
+
int64_t deadline;
|
|
41
|
+
} x11_clipboard;
|
|
42
|
+
|
|
43
|
+
static bool append_bytes(property_data* result, const unsigned char* bytes, size_t length) {
|
|
44
|
+
if (length > MAX_CLIPBOARD_BYTES - result->length) return false;
|
|
45
|
+
unsigned char* data = realloc(result->data, result->length + length + 1);
|
|
46
|
+
if (!data) return false;
|
|
47
|
+
memcpy(data + result->length, bytes, length);
|
|
48
|
+
result->length += length;
|
|
49
|
+
data[result->length] = 0;
|
|
50
|
+
result->data = data;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Share one deadline across discovery, replies, and incremental chunks.
|
|
55
|
+
// Connection setup and flushing are libxcb calls on the private thread.
|
|
56
|
+
static bool wait_for_input(x11_clipboard* clipboard) {
|
|
57
|
+
for (;;) {
|
|
58
|
+
int64_t remaining = clipboard->deadline - monotonic_ms();
|
|
59
|
+
if (remaining <= 0 || xcb_connection_has_error(clipboard->connection)) return false;
|
|
60
|
+
struct pollfd descriptor = {xcb_get_file_descriptor(clipboard->connection), POLLIN, 0};
|
|
61
|
+
int ready = poll(&descriptor, 1, (int)remaining);
|
|
62
|
+
if (ready >= 0) return ready && (descriptor.revents & POLLIN);
|
|
63
|
+
if (errno != EINTR) return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static void* wait_for_reply(x11_clipboard* clipboard, unsigned sequence) {
|
|
68
|
+
if (xcb_flush(clipboard->connection) <= 0) return 0;
|
|
69
|
+
while (monotonic_ms() < clipboard->deadline) {
|
|
70
|
+
void* reply = 0;
|
|
71
|
+
xcb_generic_error_t* error = 0;
|
|
72
|
+
if (xcb_poll_for_reply(clipboard->connection, sequence, &reply, &error)) {
|
|
73
|
+
free(error);
|
|
74
|
+
return reply;
|
|
75
|
+
}
|
|
76
|
+
if (!wait_for_input(clipboard)) break;
|
|
77
|
+
}
|
|
78
|
+
xcb_discard_reply(clipboard->connection, sequence);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static xcb_generic_event_t* wait_for_event(x11_clipboard* clipboard, uint8_t type) {
|
|
83
|
+
if (xcb_flush(clipboard->connection) <= 0) return 0;
|
|
84
|
+
while (monotonic_ms() < clipboard->deadline) {
|
|
85
|
+
xcb_generic_event_t* event = xcb_poll_for_event(clipboard->connection);
|
|
86
|
+
if (event) {
|
|
87
|
+
if ((event->response_type & 0x7f) == type) return event;
|
|
88
|
+
free(event);
|
|
89
|
+
} else if (!wait_for_input(clipboard)) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static bool read_property(x11_clipboard* clipboard, bool remove, property_data* result) {
|
|
97
|
+
xcb_get_property_cookie_t cookie = xcb_get_property(
|
|
98
|
+
clipboard->connection, remove, clipboard->window, clipboard->property,
|
|
99
|
+
XCB_GET_PROPERTY_TYPE_ANY, 0, MAX_CLIPBOARD_BYTES / 4
|
|
100
|
+
);
|
|
101
|
+
xcb_get_property_reply_t* reply = wait_for_reply(clipboard, cookie.sequence);
|
|
102
|
+
if (!reply) return false;
|
|
103
|
+
bool copied = false;
|
|
104
|
+
bool valid_format = reply->format == 8 || reply->format == 16 || reply->format == 32;
|
|
105
|
+
uint64_t length = (uint64_t)reply->value_len * (reply->format / 8);
|
|
106
|
+
if (reply->bytes_after == 0 && reply->type != XCB_NONE && valid_format &&
|
|
107
|
+
length <= MAX_CLIPBOARD_BYTES && length <= (uint64_t)reply->length * 4) {
|
|
108
|
+
result->type = reply->type;
|
|
109
|
+
result->format = reply->format;
|
|
110
|
+
result->items = reply->value_len;
|
|
111
|
+
copied = append_bytes(result, xcb_get_property_value(reply), (size_t)length);
|
|
112
|
+
}
|
|
113
|
+
free(reply);
|
|
114
|
+
return copied;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static bool append_property(property_data* result, const property_data* chunk) {
|
|
118
|
+
if ((chunk->format != 8 && chunk->format != 16 && chunk->format != 32) ||
|
|
119
|
+
chunk->type == XCB_NONE ||
|
|
120
|
+
(uint64_t)chunk->items * (chunk->format / 8) != chunk->length ||
|
|
121
|
+
(result->type != XCB_NONE && (result->type != chunk->type || result->format != chunk->format)) ||
|
|
122
|
+
chunk->items > UINT32_MAX - result->items) return false;
|
|
123
|
+
if (!append_bytes(result, chunk->data, chunk->length)) return false;
|
|
124
|
+
result->type = chunk->type;
|
|
125
|
+
result->format = chunk->format;
|
|
126
|
+
result->items += chunk->items;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static bool request_selection(x11_clipboard* clipboard, xcb_atom_t target, property_data* result) {
|
|
131
|
+
memset(result, 0, sizeof(*result));
|
|
132
|
+
xcb_delete_property(clipboard->connection, clipboard->window, clipboard->property);
|
|
133
|
+
xcb_convert_selection(
|
|
134
|
+
clipboard->connection, clipboard->window, clipboard->clipboard,
|
|
135
|
+
target, clipboard->property, XCB_CURRENT_TIME
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
while (true) {
|
|
139
|
+
xcb_selection_notify_event_t* event = (xcb_selection_notify_event_t*)wait_for_event(clipboard, XCB_SELECTION_NOTIFY);
|
|
140
|
+
if (!event) return false;
|
|
141
|
+
bool matches = event->selection == clipboard->clipboard && event->target == target;
|
|
142
|
+
bool received = event->property != XCB_NONE;
|
|
143
|
+
free(event);
|
|
144
|
+
if (!matches) continue;
|
|
145
|
+
if (!received) return true; // No owner, or the owner does not offer this target.
|
|
146
|
+
if (!read_property(clipboard, false, result)) return false;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (result->type != clipboard->incr) return true;
|
|
150
|
+
|
|
151
|
+
free(result->data);
|
|
152
|
+
memset(result, 0, sizeof(*result));
|
|
153
|
+
xcb_delete_property(clipboard->connection, clipboard->window, clipboard->property);
|
|
154
|
+
|
|
155
|
+
while (true) {
|
|
156
|
+
xcb_property_notify_event_t* event = (xcb_property_notify_event_t*)wait_for_event(clipboard, XCB_PROPERTY_NOTIFY);
|
|
157
|
+
if (!event) return false;
|
|
158
|
+
bool matches = event->atom == clipboard->property && event->state == XCB_PROPERTY_NEW_VALUE;
|
|
159
|
+
free(event);
|
|
160
|
+
if (!matches) continue;
|
|
161
|
+
|
|
162
|
+
property_data chunk = {0};
|
|
163
|
+
if (!read_property(clipboard, true, &chunk)) return false;
|
|
164
|
+
bool appended = append_property(result, &chunk);
|
|
165
|
+
bool finished = chunk.items == 0;
|
|
166
|
+
free(chunk.data);
|
|
167
|
+
if (!appended) return false;
|
|
168
|
+
if (finished) return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static xcb_atom_t intern_atom(x11_clipboard* clipboard, const char* name) {
|
|
173
|
+
xcb_intern_atom_cookie_t cookie = xcb_intern_atom(clipboard->connection, false, strlen(name), name);
|
|
174
|
+
xcb_intern_atom_reply_t* reply = wait_for_reply(clipboard, cookie.sequence);
|
|
175
|
+
if (!reply) return XCB_NONE;
|
|
176
|
+
xcb_atom_t atom = reply->atom;
|
|
177
|
+
free(reply);
|
|
178
|
+
return atom;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static bool open_clipboard(x11_clipboard* clipboard) {
|
|
182
|
+
memset(clipboard, 0, sizeof(*clipboard));
|
|
183
|
+
clipboard->deadline = monotonic_ms() + CLIPBOARD_TIMEOUT_MS;
|
|
184
|
+
int screen_number = 0;
|
|
185
|
+
clipboard->connection = xcb_connect(0, &screen_number);
|
|
186
|
+
if (xcb_connection_has_error(clipboard->connection)) return false;
|
|
187
|
+
xcb_screen_iterator_t screens = xcb_setup_roots_iterator(xcb_get_setup(clipboard->connection));
|
|
188
|
+
while (screen_number-- > 0 && screens.rem) xcb_screen_next(&screens);
|
|
189
|
+
if (!screens.rem) return false;
|
|
190
|
+
|
|
191
|
+
clipboard->window = xcb_generate_id(clipboard->connection);
|
|
192
|
+
uint32_t mask = XCB_EVENT_MASK_PROPERTY_CHANGE;
|
|
193
|
+
xcb_create_window(
|
|
194
|
+
clipboard->connection, XCB_COPY_FROM_PARENT, clipboard->window,
|
|
195
|
+
screens.data->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
|
196
|
+
XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, &mask
|
|
197
|
+
);
|
|
198
|
+
clipboard->clipboard = intern_atom(clipboard, "CLIPBOARD");
|
|
199
|
+
clipboard->property = intern_atom(clipboard, "PI_CLIPBOARD");
|
|
200
|
+
clipboard->targets = intern_atom(clipboard, "TARGETS");
|
|
201
|
+
clipboard->incr = intern_atom(clipboard, "INCR");
|
|
202
|
+
return clipboard->clipboard && clipboard->property && clipboard->targets && clipboard->incr;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static void close_clipboard(x11_clipboard* clipboard) {
|
|
206
|
+
if (clipboard->connection) xcb_disconnect(clipboard->connection);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static bool preferred_target(x11_clipboard* clipboard, bool image, xcb_atom_t* target) {
|
|
210
|
+
const char* const* types = image ? image_types : text_types;
|
|
211
|
+
size_t type_count = image ? 6 : 5;
|
|
212
|
+
xcb_atom_t wanted[6];
|
|
213
|
+
for (size_t index = 0; index < type_count; index++) {
|
|
214
|
+
wanted[index] = intern_atom(clipboard, types[index]);
|
|
215
|
+
if (wanted[index] == XCB_NONE) return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
property_data targets = {0};
|
|
219
|
+
bool received = request_selection(clipboard, clipboard->targets, &targets);
|
|
220
|
+
bool valid = targets.type == XCB_ATOM_ATOM && targets.format == 32 &&
|
|
221
|
+
targets.items == targets.length / sizeof(xcb_atom_t) && targets.length % sizeof(xcb_atom_t) == 0;
|
|
222
|
+
if (received && valid) {
|
|
223
|
+
xcb_atom_t* offered = (xcb_atom_t*)targets.data;
|
|
224
|
+
for (size_t wanted_index = 0; wanted_index < type_count; wanted_index++) {
|
|
225
|
+
for (uint32_t offered_index = 0; offered_index < targets.items; offered_index++) {
|
|
226
|
+
if (offered[offered_index] == wanted[wanted_index]) {
|
|
227
|
+
free(targets.data);
|
|
228
|
+
*target = wanted[wanted_index];
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
free(targets.data);
|
|
235
|
+
// Some owners refuse TARGETS but still serve text directly.
|
|
236
|
+
if (received && !image && targets.type == XCB_NONE) {
|
|
237
|
+
*target = intern_atom(clipboard, "UTF8_STRING");
|
|
238
|
+
return *target != XCB_NONE;
|
|
239
|
+
}
|
|
240
|
+
return received && (valid || targets.type == XCB_NONE);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static bool read_selection(x11_clipboard* clipboard, bool image, property_data* result) {
|
|
244
|
+
xcb_atom_t target = XCB_NONE;
|
|
245
|
+
bool received = preferred_target(clipboard, image, &target) &&
|
|
246
|
+
(target == XCB_NONE || request_selection(clipboard, target, result));
|
|
247
|
+
if (!received) {
|
|
248
|
+
free(result->data);
|
|
249
|
+
memset(result, 0, sizeof(*result));
|
|
250
|
+
}
|
|
251
|
+
return received;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static void read_clipboard(clipboard_job* job) {
|
|
255
|
+
x11_clipboard clipboard;
|
|
256
|
+
if (open_clipboard(&clipboard)) {
|
|
257
|
+
property_data contents = {0};
|
|
258
|
+
if (!read_selection(&clipboard, job->operation == CLIPBOARD_IMAGE, &contents)) {
|
|
259
|
+
job->error = "Could not read X11 clipboard";
|
|
260
|
+
} else {
|
|
261
|
+
job->data = contents.data;
|
|
262
|
+
job->length = contents.length;
|
|
263
|
+
job->format = !contents.data ? CLIPBOARD_EMPTY : job->operation == CLIPBOARD_IMAGE
|
|
264
|
+
? CLIPBOARD_BUFFER : contents.type == XCB_ATOM_STRING ? CLIPBOARD_LATIN1 : CLIPBOARD_UTF8;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
close_clipboard(&clipboard);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) {
|
|
271
|
+
set_function_export(env, exports, "getText", get_clipboard_text);
|
|
272
|
+
set_function_export(env, exports, "getImage", get_clipboard_image);
|
|
273
|
+
return exports;
|
|
274
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#ifndef PI_NAPI_H
|
|
2
|
+
#define PI_NAPI_H
|
|
3
|
+
|
|
4
|
+
// Resolve the small N-API surface we use from Node or Bun, without Node headers
|
|
5
|
+
// or a link-time dependency on a particular runtime.
|
|
6
|
+
#include <stdbool.h>
|
|
7
|
+
#include <stddef.h>
|
|
8
|
+
#include <stdint.h>
|
|
9
|
+
#ifdef _WIN32
|
|
10
|
+
#include <windows.h>
|
|
11
|
+
#define PI_NAPI_CALL __cdecl
|
|
12
|
+
#define PI_NAPI_EXPORT __declspec(dllexport)
|
|
13
|
+
#else
|
|
14
|
+
#include <dlfcn.h>
|
|
15
|
+
#define PI_NAPI_CALL
|
|
16
|
+
#define PI_NAPI_EXPORT __attribute__((visibility("default")))
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#define NAPI_AUTO_LENGTH ((size_t)-1)
|
|
20
|
+
|
|
21
|
+
typedef void* napi_env;
|
|
22
|
+
typedef void* napi_value;
|
|
23
|
+
typedef void* napi_callback_info;
|
|
24
|
+
typedef void* napi_async_work;
|
|
25
|
+
typedef void* napi_deferred;
|
|
26
|
+
typedef void (PI_NAPI_CALL *napi_async_execute_callback)(napi_env, void*);
|
|
27
|
+
typedef void (PI_NAPI_CALL *napi_async_complete_callback)(napi_env, int, void*);
|
|
28
|
+
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*);
|
|
29
|
+
typedef int (PI_NAPI_CALL *napi_create_array_fn)(napi_env, napi_value*);
|
|
30
|
+
typedef int (PI_NAPI_CALL *napi_async_work_fn)(napi_env, napi_async_work);
|
|
31
|
+
typedef int (PI_NAPI_CALL *napi_create_promise_fn)(napi_env, napi_deferred*, napi_value*);
|
|
32
|
+
typedef int (PI_NAPI_CALL *napi_settle_deferred_fn)(napi_env, napi_deferred, napi_value);
|
|
33
|
+
typedef int (PI_NAPI_CALL *napi_create_error_fn)(napi_env, napi_value, napi_value, napi_value*);
|
|
34
|
+
typedef napi_value (PI_NAPI_CALL *napi_callback)(napi_env, napi_callback_info);
|
|
35
|
+
typedef int (PI_NAPI_CALL *napi_create_buffer_copy_fn)(napi_env, size_t, const void*, void**, napi_value*);
|
|
36
|
+
typedef int (PI_NAPI_CALL *napi_create_function_fn)(napi_env, const char*, size_t, napi_callback, void*, napi_value*);
|
|
37
|
+
typedef int (PI_NAPI_CALL *napi_create_string_utf8_fn)(napi_env, const char*, size_t, napi_value*);
|
|
38
|
+
typedef int (PI_NAPI_CALL *napi_create_string_utf16_fn)(napi_env, const uint16_t*, size_t, napi_value*);
|
|
39
|
+
typedef int (PI_NAPI_CALL *napi_get_boolean_fn)(napi_env, bool, napi_value*);
|
|
40
|
+
typedef int (PI_NAPI_CALL *napi_get_cb_info_fn)(napi_env, napi_callback_info, size_t*, napi_value*, napi_value*, void**);
|
|
41
|
+
typedef int (PI_NAPI_CALL *napi_get_value_fn)(napi_env, napi_value*);
|
|
42
|
+
typedef int (PI_NAPI_CALL *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*);
|
|
43
|
+
typedef int (PI_NAPI_CALL *napi_get_value_string_utf16_fn)(napi_env, napi_value, uint16_t*, size_t, size_t*);
|
|
44
|
+
typedef int (PI_NAPI_CALL *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
|
|
45
|
+
typedef int (PI_NAPI_CALL *napi_set_element_fn)(napi_env, napi_value, uint32_t, napi_value);
|
|
46
|
+
typedef int (PI_NAPI_CALL *napi_throw_error_fn)(napi_env, const char*, const char*);
|
|
47
|
+
|
|
48
|
+
static void* node_symbol(const char* name) {
|
|
49
|
+
#ifdef _WIN32
|
|
50
|
+
HMODULE module = GetModuleHandleA(0);
|
|
51
|
+
void* proc = module ? (void*)GetProcAddress(module, name) : 0;
|
|
52
|
+
if (proc) return proc;
|
|
53
|
+
module = GetModuleHandleA("node.dll");
|
|
54
|
+
return module ? (void*)GetProcAddress(module, name) : 0;
|
|
55
|
+
#else
|
|
56
|
+
return dlsym(RTLD_DEFAULT, name);
|
|
57
|
+
#endif
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static napi_value undefined_value(napi_env env) {
|
|
61
|
+
napi_get_value_fn get_value = (napi_get_value_fn)node_symbol("napi_get_undefined");
|
|
62
|
+
napi_value result = 0;
|
|
63
|
+
if (get_value) get_value(env, &result);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static napi_value null_value(napi_env env) {
|
|
68
|
+
napi_get_value_fn get_value = (napi_get_value_fn)node_symbol("napi_get_null");
|
|
69
|
+
napi_value result = 0;
|
|
70
|
+
if (get_value) get_value(env, &result);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static napi_value fail(napi_env env, const char* message) {
|
|
75
|
+
napi_throw_error_fn napi_throw_error = (napi_throw_error_fn)node_symbol("napi_throw_error");
|
|
76
|
+
if (napi_throw_error) napi_throw_error(env, 0, message);
|
|
77
|
+
return undefined_value(env);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static void set_function_export(napi_env env, napi_value exports, const char* name, napi_callback callback) {
|
|
81
|
+
napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function");
|
|
82
|
+
napi_set_named_property_fn napi_set_named_property =
|
|
83
|
+
(napi_set_named_property_fn)node_symbol("napi_set_named_property");
|
|
84
|
+
napi_value fn = 0;
|
|
85
|
+
if (napi_create_function && napi_set_named_property &&
|
|
86
|
+
napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, 0, &fn) == 0) {
|
|
87
|
+
napi_set_named_property(env, exports, name, fn);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#endif
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Windows native prebuilds
|
|
2
|
+
|
|
3
|
+
Provides console input setup, modifier-key state, and asynchronous text/image clipboard access. Links to `kernel32` and `user32`; no Node headers are required.
|
|
4
|
+
|
|
5
|
+
## Building
|
|
6
|
+
|
|
7
|
+
On Windows, install Visual Studio's "Desktop development with C++" workload, then run from the repository root to build x64 and arm64:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm --prefix packages/tui run build:native:win32
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For cross-builds or custom toolchains, provide MinGW-compatible compilers:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
PI_TUI_WIN32_TOOLCHAIN=mingw \
|
|
17
|
+
CC_X64=/path/to/x86_64-w64-mingw32-gcc \
|
|
18
|
+
CC_ARM64=/path/to/aarch64-w64-mingw32-gcc \
|
|
19
|
+
npm --prefix packages/tui run build:native:win32
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Testing clipboard writes
|
|
23
|
+
|
|
24
|
+
On a Windows test desktop, run from `packages/tui` in PowerShell:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
$env:PI_TEST_NATIVE_CLIPBOARD = "1"
|
|
28
|
+
node --test test/native-platform.test.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This opt-in test checks native text writes and reads. **It replaces the system clipboard contents.**
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const sourceFile = path.join(scriptDir, "src", "win32-platform.c");
|
|
9
|
+
const temporaryDir = mkdtempSync(path.join(tmpdir(), "pi-tui-win32-"));
|
|
10
|
+
|
|
11
|
+
const targets = [
|
|
12
|
+
{
|
|
13
|
+
arch: "x64",
|
|
14
|
+
msvcArch: "x64",
|
|
15
|
+
mingwTarget: "x86_64-w64-windows-gnu",
|
|
16
|
+
prefixedCompilers: ["x86_64-w64-mingw32-gcc", "x86_64-w64-mingw32-clang"],
|
|
17
|
+
envCompiler: "CC_X64",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
arch: "arm64",
|
|
21
|
+
msvcArch: "arm64",
|
|
22
|
+
mingwTarget: "aarch64-w64-windows-gnu",
|
|
23
|
+
prefixedCompilers: ["aarch64-w64-mingw32-gcc", "aarch64-w64-mingw32-clang"],
|
|
24
|
+
envCompiler: "CC_ARM64",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function removeTemporaryDir() {
|
|
29
|
+
rmSync(temporaryDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
process.on("exit", removeTemporaryDir);
|
|
33
|
+
process.on("SIGINT", () => {
|
|
34
|
+
removeTemporaryDir();
|
|
35
|
+
process.exit(130);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function commandExists(command) {
|
|
39
|
+
const result = spawnSync(command, ["--version"], { stdio: "ignore" });
|
|
40
|
+
return !result.error || result.error.code !== "ENOENT";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function run(command, args, options = {}) {
|
|
44
|
+
const result = spawnSync(command, args, { stdio: "inherit", ...options });
|
|
45
|
+
if (result.error) throw result.error;
|
|
46
|
+
if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status ?? 1}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function quoteBatch(value) {
|
|
50
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function msvcHostArch() {
|
|
54
|
+
return process.arch === "arm64" ? "arm64" : "x64";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findVisualStudioDevCmd() {
|
|
58
|
+
const candidates = [];
|
|
59
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
|
60
|
+
if (programFilesX86) {
|
|
61
|
+
const vswhere = path.join(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
|
|
62
|
+
if (existsSync(vswhere)) {
|
|
63
|
+
const result = spawnSync(
|
|
64
|
+
vswhere,
|
|
65
|
+
[
|
|
66
|
+
"-latest",
|
|
67
|
+
"-products",
|
|
68
|
+
"*",
|
|
69
|
+
"-requires",
|
|
70
|
+
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
|
71
|
+
"-property",
|
|
72
|
+
"installationPath",
|
|
73
|
+
],
|
|
74
|
+
{ encoding: "utf8" },
|
|
75
|
+
);
|
|
76
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
77
|
+
const installationPath = line.trim();
|
|
78
|
+
if (installationPath) candidates.push(path.join(installationPath, "Common7", "Tools", "VsDevCmd.bat"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const base of [process.env.ProgramFiles, programFilesX86]) {
|
|
84
|
+
if (!base) continue;
|
|
85
|
+
for (const edition of ["BuildTools", "Community", "Professional", "Enterprise"]) {
|
|
86
|
+
candidates.push(path.join(base, "Microsoft Visual Studio", "2022", edition, "Common7", "Tools", "VsDevCmd.bat"));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function canUseMsvc(vsDevCmd) {
|
|
94
|
+
const probeDir = path.join(temporaryDir, "msvc-probe");
|
|
95
|
+
mkdirSync(probeDir, { recursive: true });
|
|
96
|
+
const batchFile = path.join(probeDir, "probe.cmd");
|
|
97
|
+
writeFileSync(
|
|
98
|
+
batchFile,
|
|
99
|
+
[
|
|
100
|
+
"@echo off",
|
|
101
|
+
`call ${quoteBatch(vsDevCmd)} -no_logo -arch=x64 -host_arch=${msvcHostArch()} >nul`,
|
|
102
|
+
"if errorlevel 1 exit /b %errorlevel%",
|
|
103
|
+
"where cl.exe >nul 2>nul",
|
|
104
|
+
].join("\r\n"),
|
|
105
|
+
);
|
|
106
|
+
const result = spawnSync("cmd.exe", ["/d", "/s", "/c", batchFile], { cwd: probeDir, stdio: "ignore" });
|
|
107
|
+
return !result.error && result.status === 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveMingwCompiler(target) {
|
|
111
|
+
const envCompiler = process.env[target.envCompiler];
|
|
112
|
+
if (envCompiler) return { command: envCompiler, useTargetFlag: false };
|
|
113
|
+
if (process.env.CC) return { command: process.env.CC, useTargetFlag: true };
|
|
114
|
+
for (const command of target.prefixedCompilers) {
|
|
115
|
+
if (commandExists(command)) return { command, useTargetFlag: false };
|
|
116
|
+
}
|
|
117
|
+
if (commandExists("clang")) return { command: "clang", useTargetFlag: true };
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function installOutput(temporaryOutput, target) {
|
|
122
|
+
const outputDir = path.join(scriptDir, "prebuilds", `win32-${target.arch}`);
|
|
123
|
+
mkdirSync(outputDir, { recursive: true });
|
|
124
|
+
const output = path.join(outputDir, "win32-platform.node");
|
|
125
|
+
copyFileSync(temporaryOutput, output);
|
|
126
|
+
chmodSync(output, 0o755);
|
|
127
|
+
console.log(`Built ${path.relative(process.cwd(), output)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildWithMsvc(target, vsDevCmd) {
|
|
131
|
+
const buildDir = path.join(temporaryDir, `msvc-${target.arch}`);
|
|
132
|
+
const temporaryOutput = path.join(buildDir, "win32-platform.node");
|
|
133
|
+
mkdirSync(buildDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const batchFile = path.join(buildDir, "build.cmd");
|
|
136
|
+
writeFileSync(
|
|
137
|
+
batchFile,
|
|
138
|
+
[
|
|
139
|
+
"@echo off",
|
|
140
|
+
`call ${quoteBatch(vsDevCmd)} -no_logo -arch=${target.msvcArch} -host_arch=${msvcHostArch()}`,
|
|
141
|
+
"if errorlevel 1 exit /b %errorlevel%",
|
|
142
|
+
[
|
|
143
|
+
"cl",
|
|
144
|
+
"/nologo",
|
|
145
|
+
"/TC",
|
|
146
|
+
"/std:c11",
|
|
147
|
+
"/W4",
|
|
148
|
+
"/O1",
|
|
149
|
+
"/GS-",
|
|
150
|
+
"/LD",
|
|
151
|
+
`/Fe:${quoteBatch(temporaryOutput)}`,
|
|
152
|
+
quoteBatch(sourceFile),
|
|
153
|
+
"/link",
|
|
154
|
+
"/NOENTRY",
|
|
155
|
+
"/NODEFAULTLIB",
|
|
156
|
+
"kernel32.lib",
|
|
157
|
+
"user32.lib",
|
|
158
|
+
"/OPT:REF",
|
|
159
|
+
"/OPT:ICF",
|
|
160
|
+
].join(" "),
|
|
161
|
+
"if errorlevel 1 exit /b %errorlevel%",
|
|
162
|
+
].join("\r\n"),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
run("cmd.exe", ["/d", "/s", "/c", batchFile], { cwd: buildDir });
|
|
166
|
+
installOutput(temporaryOutput, target);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildWithMingw(target) {
|
|
170
|
+
const compiler = resolveMingwCompiler(target);
|
|
171
|
+
if (!compiler) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`No Windows cross-compiler found for ${target.arch}. Install Microsoft C++ Build Tools on Windows, or set ${target.envCompiler} to a MinGW-compatible compiler.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const buildDir = path.join(temporaryDir, `mingw-${target.arch}`);
|
|
178
|
+
const temporaryOutput = path.join(buildDir, "win32-platform.node");
|
|
179
|
+
mkdirSync(buildDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
const args = [
|
|
182
|
+
...(compiler.useTargetFlag ? [`--target=${target.mingwTarget}`] : []),
|
|
183
|
+
"-std=c11",
|
|
184
|
+
"-Wall",
|
|
185
|
+
"-Wextra",
|
|
186
|
+
"-Oz",
|
|
187
|
+
"-fno-builtin",
|
|
188
|
+
"-shared",
|
|
189
|
+
"-nostdlib",
|
|
190
|
+
"-Wl,--no-entry",
|
|
191
|
+
"-Wl,--strip-all",
|
|
192
|
+
sourceFile,
|
|
193
|
+
"-lkernel32",
|
|
194
|
+
"-luser32",
|
|
195
|
+
"-o",
|
|
196
|
+
temporaryOutput,
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
run(compiler.command, args, { cwd: buildDir });
|
|
200
|
+
installOutput(temporaryOutput, target);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
204
|
+
console.log(`Usage: npm --prefix packages/tui run build:native:win32
|
|
205
|
+
|
|
206
|
+
Builds win32-x64 and win32-arm64 native prebuilds.
|
|
207
|
+
|
|
208
|
+
Environment:
|
|
209
|
+
PI_TUI_WIN32_TOOLCHAIN=msvc|mingw
|
|
210
|
+
CC_X64=/path/to/x86_64-w64-mingw32-gcc
|
|
211
|
+
CC_ARM64=/path/to/aarch64-w64-mingw32-gcc
|
|
212
|
+
CC=/path/to/clang`);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const requestedToolchain = process.env.PI_TUI_WIN32_TOOLCHAIN;
|
|
217
|
+
if (requestedToolchain && requestedToolchain !== "msvc" && requestedToolchain !== "mingw") {
|
|
218
|
+
throw new Error("PI_TUI_WIN32_TOOLCHAIN must be either 'msvc' or 'mingw'");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const vsDevCmd = findVisualStudioDevCmd();
|
|
222
|
+
const hasMsvc = vsDevCmd ? canUseMsvc(vsDevCmd) : false;
|
|
223
|
+
const toolchain = requestedToolchain ?? (process.platform === "win32" && vsDevCmd ? "msvc" : "mingw");
|
|
224
|
+
|
|
225
|
+
if (toolchain === "msvc") {
|
|
226
|
+
if (!vsDevCmd || !hasMsvc) {
|
|
227
|
+
throw new Error("Microsoft C++ Build Tools not found. Install the Visual Studio C++ workload or set PI_TUI_WIN32_TOOLCHAIN=mingw.");
|
|
228
|
+
}
|
|
229
|
+
for (const target of targets) buildWithMsvc(target, vsDevCmd);
|
|
230
|
+
} else {
|
|
231
|
+
for (const target of targets) buildWithMingw(target);
|
|
232
|
+
}
|