Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_clipboard.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
26#include "nilorea/n_clipboard.h"
27#include "nilorea/n_log.h"
28
29#include <stdlib.h>
30#include <string.h>
31
32/* Android is __linux__ too, but the NDK ships no X11: fall through to the
33 * unavailable stub so callers use their toolkit clipboard instead. */
34#if defined(__linux__) && !defined(__ANDROID__)
35
36#include <X11/Xatom.h>
37#include <X11/Xlib.h>
38#include <pthread.h>
39#include <sys/select.h>
40#include <time.h>
41#include <unistd.h>
42
44static char* clip_dupn(const char* s, size_t n) {
45 char* out = malloc(n + 1);
46 if (!out)
47 return NULL;
48 memcpy(out, s, n);
49 out[n] = '\0';
50 return out;
51}
52
54static Display* s_dpy = NULL;
56static Window s_win = 0;
57static pthread_t s_thread;
58static int s_running = 0;
59static int s_ready = 0;
60static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;
62static char* s_text[2] = {NULL, NULL};
63
64/* interned atoms */
65static Atom A_CLIPBOARD, A_TARGETS, A_UTF8, A_TEXT, A_TEXTPLAIN, A_TEXTPLAIN_UTF8, A_INCR;
66
68static Atom clip_sel_atom(int which) {
69 return (which == N_CLIPBOARD_PRIMARY) ? XA_PRIMARY : A_CLIPBOARD;
70}
71
73static int clip_sel_index(Atom a) {
74 if (a == XA_PRIMARY)
76 if (a == A_CLIPBOARD)
78 return -1;
79}
80
82static int clip_is_text_target(Atom t) {
83 return t == A_UTF8 || t == XA_STRING || t == A_TEXT || t == A_TEXTPLAIN || t == A_TEXTPLAIN_UTF8;
84}
85
87static void clip_answer_request(const XSelectionRequestEvent* req) {
88 XSelectionEvent resp;
89 int idx = clip_sel_index(req->selection);
90 resp.type = SelectionNotify;
91 resp.display = req->display;
92 resp.requestor = req->requestor;
93 resp.selection = req->selection;
94 resp.target = req->target;
95 resp.time = req->time;
96 resp.property = None; /* refused unless we fill it below */
97
98 if (idx >= 0) {
99 if (req->target == A_TARGETS) {
100 Atom targets[6];
101 targets[0] = A_TARGETS;
102 targets[1] = A_UTF8;
103 targets[2] = XA_STRING;
104 targets[3] = A_TEXT;
105 targets[4] = A_TEXTPLAIN;
106 targets[5] = A_TEXTPLAIN_UTF8;
107 XChangeProperty(s_dpy, req->requestor, req->property, XA_ATOM, 32, PropModeReplace,
108 (const unsigned char*)targets, (int)(sizeof(targets) / sizeof(targets[0])));
109 resp.property = req->property;
110 } else if (clip_is_text_target(req->target)) {
111 const char* txt;
112 pthread_mutex_lock(&s_lock);
113 txt = s_text[idx];
114 if (txt) {
115 XChangeProperty(s_dpy, req->requestor, req->property, req->target, 8, PropModeReplace,
116 (const unsigned char*)txt, (int)strlen(txt));
117 resp.property = req->property;
118 }
119 pthread_mutex_unlock(&s_lock);
120 }
121 }
122 XSendEvent(s_dpy, req->requestor, False, 0, (XEvent*)&resp);
123 XFlush(s_dpy);
124}
125
127static void* clip_thread_main(void* arg) {
128 (void)arg;
129 int fd = ConnectionNumber(s_dpy);
130 while (__atomic_load_n(&s_running, __ATOMIC_ACQUIRE)) {
131 fd_set fds;
132 struct timeval tv;
133 FD_ZERO(&fds);
134 FD_SET(fd, &fds);
135 tv.tv_sec = 0;
136 tv.tv_usec = 200000; /* wake up to poll the stop flag */
137 select(fd + 1, &fds, NULL, NULL, &tv);
138 while (XPending(s_dpy)) {
139 XEvent ev;
140 XNextEvent(s_dpy, &ev);
141 if (ev.type == SelectionRequest) {
142 clip_answer_request(&ev.xselectionrequest);
143 } else if (ev.type == SelectionClear) {
144 int idx = clip_sel_index(ev.xselectionclear.selection);
145 if (idx >= 0) {
146 pthread_mutex_lock(&s_lock);
147 free(s_text[idx]);
148 s_text[idx] = NULL;
149 pthread_mutex_unlock(&s_lock);
150 }
151 }
152 }
153 }
154 return NULL;
155}
156
157int n_clipboard_init(void) {
158 if (s_ready)
159 return 0;
160 s_dpy = XOpenDisplay(NULL);
161 if (!s_dpy) {
162 n_log(LOG_INFO, "n_clipboard: no X11 display, clipboard backend disabled");
163 return -1;
164 }
165 s_win = XCreateSimpleWindow(s_dpy, RootWindow(s_dpy, DefaultScreen(s_dpy)), 0, 0, 1, 1, 0, 0, 0);
166 A_CLIPBOARD = XInternAtom(s_dpy, "CLIPBOARD", False);
167 A_TARGETS = XInternAtom(s_dpy, "TARGETS", False);
168 A_UTF8 = XInternAtom(s_dpy, "UTF8_STRING", False);
169 A_TEXT = XInternAtom(s_dpy, "TEXT", False);
170 A_TEXTPLAIN = XInternAtom(s_dpy, "text/plain", False);
171 A_TEXTPLAIN_UTF8 = XInternAtom(s_dpy, "text/plain;charset=utf-8", False);
172 A_INCR = XInternAtom(s_dpy, "INCR", False);
173 (void)A_INCR;
174 __atomic_store_n(&s_running, 1, __ATOMIC_RELEASE);
175 if (pthread_create(&s_thread, NULL, clip_thread_main, NULL) != 0) {
176 __atomic_store_n(&s_running, 0, __ATOMIC_RELEASE);
177 XDestroyWindow(s_dpy, s_win);
178 XCloseDisplay(s_dpy);
179 s_dpy = NULL;
180 n_log(LOG_ERR, "n_clipboard: could not start the serving thread");
181 return -1;
182 }
183 s_ready = 1;
184 n_log(LOG_INFO, "n_clipboard: X11 clipboard backend ready");
185 return 0;
186}
187
188int n_clipboard_available(void) {
189 if (!s_ready)
191 return s_ready;
192}
193
194int n_clipboard_set(int which, const char* text) {
195 if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
196 return -1;
198 return -1;
199 pthread_mutex_lock(&s_lock);
200 free(s_text[which]);
201 s_text[which] = (text && text[0]) ? clip_dupn(text, strlen(text)) : NULL;
202 pthread_mutex_unlock(&s_lock);
203 if (s_text[which])
204 XSetSelectionOwner(s_dpy, clip_sel_atom(which), s_win, CurrentTime);
205 else
206 XSetSelectionOwner(s_dpy, clip_sel_atom(which), None, CurrentTime);
207 XFlush(s_dpy);
208 return 0;
209}
210
211char* n_clipboard_get(int which) {
212 Display* d;
213 Window w;
214 Atom sel, prop;
215 int fd, tries;
216 char* result = NULL;
217 if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
218 return NULL;
220 return NULL;
221 /* fast path: we own it, so hand back our own copy without a round trip */
222 pthread_mutex_lock(&s_lock);
223 if (s_text[which]) {
224 result = clip_dupn(s_text[which], strlen(s_text[which]));
225 pthread_mutex_unlock(&s_lock);
226 return result;
227 }
228 pthread_mutex_unlock(&s_lock);
229 /* another client owns it: request the text on a private connection so the owner
230 thread's connection is untouched */
231 d = XOpenDisplay(NULL);
232 if (!d)
233 return NULL;
234 w = XCreateSimpleWindow(d, RootWindow(d, DefaultScreen(d)), 0, 0, 1, 1, 0, 0, 0);
235 sel = (which == N_CLIPBOARD_PRIMARY) ? XA_PRIMARY : XInternAtom(d, "CLIPBOARD", False);
236 prop = XInternAtom(d, "NIL_CLIPBOARD_TARGET", False);
237 if (XGetSelectionOwner(d, sel) == None) {
238 XDestroyWindow(d, w);
239 XCloseDisplay(d);
240 return NULL;
241 }
242 XConvertSelection(d, sel, XInternAtom(d, "UTF8_STRING", False), prop, w, CurrentTime);
243 XFlush(d);
244 fd = ConnectionNumber(d);
245 for (tries = 0; tries < 20 && !result; tries++) { /* up to ~2s */
246 fd_set fds;
247 struct timeval tv;
248 FD_ZERO(&fds);
249 FD_SET(fd, &fds);
250 tv.tv_sec = 0;
251 tv.tv_usec = 100000;
252 select(fd + 1, &fds, NULL, NULL, &tv);
253 while (XPending(d)) {
254 XEvent ev;
255 XNextEvent(d, &ev);
256 if (ev.type == SelectionNotify) {
257 if (ev.xselection.property != None) {
258 Atom rtype = None;
259 int rfmt = 0;
260 unsigned long nitems = 0, after = 0;
261 unsigned char* data = NULL;
262 if (XGetWindowProperty(d, w, prop, 0, (~0L), False, AnyPropertyType, &rtype, &rfmt,
263 &nitems, &after, &data) == Success &&
264 data) {
265 result = clip_dupn((const char*)data, nitems);
266 }
267 if (data)
268 XFree(data);
269 XDeleteProperty(d, w, prop);
270 }
271 tries = 20; /* got the reply (even if empty): stop waiting */
272 break;
273 }
274 }
275 }
276 XDestroyWindow(d, w);
277 XCloseDisplay(d);
278 return result;
279}
280
281void n_clipboard_destroy(void) {
282 int i;
283 if (!s_ready)
284 return;
285 __atomic_store_n(&s_running, 0, __ATOMIC_RELEASE);
286 pthread_join(s_thread, NULL);
287 if (s_dpy) {
288 XDestroyWindow(s_dpy, s_win);
289 XCloseDisplay(s_dpy);
290 s_dpy = NULL;
291 }
292 for (i = 0; i < 2; i++) {
293 free(s_text[i]);
294 s_text[i] = NULL;
295 }
296 s_ready = 0;
297}
298
299#elif defined(__windows__) /* Win32 system clipboard (CLIPBOARD only, no PRIMARY selection) */
300
301#define WIN32_LEAN_AND_MEAN
302#include <windows.h>
303#include <pthread.h>
304
305/* Windows has a single system clipboard and no X11-style PRIMARY selection, so
306 * N_CLIPBOARD_PRIMARY is a graceful no-op here (set does nothing, get returns
307 * NULL); N_CLIPBOARD_CLIPBOARD maps to the Win32 clipboard via CF_UNICODETEXT.
308 * The OS owns the data once SetClipboardData succeeds, so no serving thread is
309 * needed. Text crosses this API as UTF-8 and is converted to/from UTF-16 here.
310 * A process-local mutex serialises access so two threads in this process do not
311 * fight over OpenClipboard (which is single-open per process). */
312
313static int s_ready = 0;
314static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;
315
317static int clip_open(void) {
318 int tries;
319 for (tries = 0; tries < 10; tries++) {
320 if (OpenClipboard(NULL))
321 return 1;
322 Sleep(5);
323 }
324 return 0;
325}
326
327int n_clipboard_init(void) {
328 s_ready = 1; /* the Win32 clipboard is always available */
329 n_log(LOG_INFO, "n_clipboard: Win32 clipboard backend ready");
330 return 0;
331}
332
333int n_clipboard_available(void) {
334 if (!s_ready)
336 return s_ready;
337}
338
339int n_clipboard_set(int which, const char* text) {
340 int wlen, rc = -1;
341 HGLOBAL hmem;
342 if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
343 return -1;
345 return -1;
346 /* no PRIMARY selection on Windows: accept the call but do nothing */
347 if (which == N_CLIPBOARD_PRIMARY)
348 return 0;
349 pthread_mutex_lock(&s_lock);
350 if (!clip_open()) {
351 pthread_mutex_unlock(&s_lock);
352 return -1;
353 }
354 if (!EmptyClipboard()) {
355 CloseClipboard();
356 pthread_mutex_unlock(&s_lock);
357 return -1;
358 }
359 /* empty text relinquishes the clipboard: it is now cleared, nothing to set */
360 if (!text || !text[0]) {
361 CloseClipboard();
362 pthread_mutex_unlock(&s_lock);
363 return 0;
364 }
365 wlen = MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); /* includes the NUL */
366 if (wlen <= 0) {
367 CloseClipboard();
368 pthread_mutex_unlock(&s_lock);
369 return -1;
370 }
371 hmem = GlobalAlloc(GMEM_MOVEABLE, (size_t)wlen * sizeof(wchar_t));
372 if (hmem) {
373 wchar_t* wbuf = (wchar_t*)GlobalLock(hmem);
374 if (wbuf) {
375 MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf, wlen);
376 GlobalUnlock(hmem);
377 if (SetClipboardData(CF_UNICODETEXT, hmem)) {
378 rc = 0; /* the system owns hmem now, do not free it */
379 } else {
380 GlobalFree(hmem); /* still ours on failure */
381 }
382 } else {
383 GlobalFree(hmem);
384 }
385 }
386 CloseClipboard();
387 pthread_mutex_unlock(&s_lock);
388 if (rc != 0)
389 n_log(LOG_ERR, "n_clipboard: failed to set the Win32 clipboard");
390 return rc;
391}
392
393char* n_clipboard_get(int which) {
394 HANDLE h;
395 char* out = NULL;
396 if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
397 return NULL;
399 return NULL;
400 if (which == N_CLIPBOARD_PRIMARY)
401 return NULL; /* no PRIMARY selection on Windows */
402 if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
403 return NULL;
404 pthread_mutex_lock(&s_lock);
405 if (!clip_open()) {
406 pthread_mutex_unlock(&s_lock);
407 return NULL;
408 }
409 h = GetClipboardData(CF_UNICODETEXT);
410 if (h) {
411 const wchar_t* wptr = (const wchar_t*)GlobalLock(h);
412 if (wptr) {
413 int len = WideCharToMultiByte(CP_UTF8, 0, wptr, -1, NULL, 0, NULL, NULL); /* includes the NUL */
414 if (len > 0) {
415 out = malloc((size_t)len);
416 if (out)
417 WideCharToMultiByte(CP_UTF8, 0, wptr, -1, out, len, NULL, NULL);
418 }
419 GlobalUnlock(h);
420 }
421 }
422 CloseClipboard();
423 pthread_mutex_unlock(&s_lock);
424 return out;
425}
426
427void n_clipboard_destroy(void) {
428 s_ready = 0;
429}
430
431#else /* no X11 or Win32 backend: unavailable, caller falls back to its toolkit clipboard */
432
434 return -1;
435}
437 return 0;
438}
439int n_clipboard_set(int which, const char* text) {
440 (void)which;
441 (void)text;
442 return -1;
443}
444char* n_clipboard_get(int which) {
445 (void)which;
446 return NULL;
447}
449}
450
451#endif
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_ERR
error conditions
Definition n_log.h:76
#define LOG_INFO
informational
Definition n_log.h:82
int n_clipboard_set(int which, const char *text)
Own which selection and serve text to any app that requests it.
int n_clipboard_available(void)
Whether a real clipboard backend (X11 or Win32) is active on this platform.
int n_clipboard_init(void)
Start the clipboard backend (idempotent; also started lazily on first use).
char * n_clipboard_get(int which)
Fetch the current text of which selection from its owner.
void n_clipboard_destroy(void)
Stop the backend thread and release the display connection.
System clipboard: full X11 selection support on Linux, Win32 clipboard on Windows/MinGW.
#define N_CLIPBOARD_PRIMARY
the X11 PRIMARY selection (mouse select-to-copy / middle-click paste); a no-op on Windows,...
Definition n_clipboard.h:59
#define N_CLIPBOARD_CLIPBOARD
the Ctrl+C / Ctrl+V clipboard selection
Definition n_clipboard.h:56
Generic log system.