src/n_clipboard.c
System clipboard: X11 selections on Linux, Win32 clipboard on Windows (see n_clipboard.h).
Functions
| Name | |
|---|---|
| int | n_clipboard_set(int which, const char * text) Own which selection and serve text to any app that requests it. |
| 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. |
| int | n_clipboard_available(void ) Whether a real clipboard backend (X11 or Win32) is active on this platform. |
Functions Documentation
function n_clipboard_set
int n_clipboard_set(
int which,
const char * text
)
Own which selection and serve text to any app that requests it.
Parameters:
- which N_CLIPBOARD_CLIPBOARD or N_CLIPBOARD_PRIMARY.
- text The UTF-8 text to publish (copied). NULL/empty relinquishes it.
Return: 0 on success, -1 on failure / no backend.
function n_clipboard_init
int n_clipboard_init(
void
)
Start the clipboard backend (idempotent; also started lazily on first use).
Return: 0 on success, -1 when no backend is available on this platform.
function n_clipboard_get
char * n_clipboard_get(
int which
)
Fetch the current text of which selection from its owner.
Parameters:
- which N_CLIPBOARD_CLIPBOARD or N_CLIPBOARD_PRIMARY.
Return: A newly allocated UTF-8 string (free with free()), or NULL when empty, unavailable, or the owner did not answer in time.
function n_clipboard_destroy
void n_clipboard_destroy(
void
)
Stop the backend thread and release the display connection.
function n_clipboard_available
int n_clipboard_available(
void
)
Whether a real clipboard backend (X11 or Win32) is active on this platform.
Return: 1 if available, 0 otherwise (caller should use its toolkit clipboard).
Source code
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "nilorea/n_clipboard.h"
#include "nilorea/n_log.h"
#include <stdlib.h>
#include <string.h>
/* Android is __linux__ too, but the NDK ships no X11: fall through to the
* unavailable stub so callers use their toolkit clipboard instead. */
#if defined(__linux__) && !defined(__ANDROID__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <pthread.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
static char* clip_dupn(const char* s, size_t n) {
char* out = malloc(n + 1);
if (!out)
return NULL;
memcpy(out, s, n);
out[n] = '\0';
return out;
}
static Display* s_dpy = NULL;
static Window s_win = 0;
static pthread_t s_thread;
static int s_running = 0;
static int s_ready = 0;
static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;
static char* s_text[2] = {NULL, NULL};
/* interned atoms */
static Atom A_CLIPBOARD, A_TARGETS, A_UTF8, A_TEXT, A_TEXTPLAIN, A_TEXTPLAIN_UTF8, A_INCR;
static Atom clip_sel_atom(int which) {
return (which == N_CLIPBOARD_PRIMARY) ? XA_PRIMARY : A_CLIPBOARD;
}
static int clip_sel_index(Atom a) {
if (a == XA_PRIMARY)
return N_CLIPBOARD_PRIMARY;
if (a == A_CLIPBOARD)
return N_CLIPBOARD_CLIPBOARD;
return -1;
}
static int clip_is_text_target(Atom t) {
return t == A_UTF8 || t == XA_STRING || t == A_TEXT || t == A_TEXTPLAIN || t == A_TEXTPLAIN_UTF8;
}
static void clip_answer_request(const XSelectionRequestEvent* req) {
XSelectionEvent resp;
int idx = clip_sel_index(req->selection);
resp.type = SelectionNotify;
resp.display = req->display;
resp.requestor = req->requestor;
resp.selection = req->selection;
resp.target = req->target;
resp.time = req->time;
resp.property = None; /* refused unless we fill it below */
if (idx >= 0) {
if (req->target == A_TARGETS) {
Atom targets[6];
targets[0] = A_TARGETS;
targets[1] = A_UTF8;
targets[2] = XA_STRING;
targets[3] = A_TEXT;
targets[4] = A_TEXTPLAIN;
targets[5] = A_TEXTPLAIN_UTF8;
XChangeProperty(s_dpy, req->requestor, req->property, XA_ATOM, 32, PropModeReplace,
(const unsigned char*)targets, (int)(sizeof(targets) / sizeof(targets[0])));
resp.property = req->property;
} else if (clip_is_text_target(req->target)) {
const char* txt;
pthread_mutex_lock(&s_lock);
txt = s_text[idx];
if (txt) {
XChangeProperty(s_dpy, req->requestor, req->property, req->target, 8, PropModeReplace,
(const unsigned char*)txt, (int)strlen(txt));
resp.property = req->property;
}
pthread_mutex_unlock(&s_lock);
}
}
XSendEvent(s_dpy, req->requestor, False, 0, (XEvent*)&resp);
XFlush(s_dpy);
}
static void* clip_thread_main(void* arg) {
(void)arg;
int fd = ConnectionNumber(s_dpy);
while (__atomic_load_n(&s_running, __ATOMIC_ACQUIRE)) {
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = 0;
tv.tv_usec = 200000; /* wake up to poll the stop flag */
select(fd + 1, &fds, NULL, NULL, &tv);
while (XPending(s_dpy)) {
XEvent ev;
XNextEvent(s_dpy, &ev);
if (ev.type == SelectionRequest) {
clip_answer_request(&ev.xselectionrequest);
} else if (ev.type == SelectionClear) {
int idx = clip_sel_index(ev.xselectionclear.selection);
if (idx >= 0) {
pthread_mutex_lock(&s_lock);
free(s_text[idx]);
s_text[idx] = NULL;
pthread_mutex_unlock(&s_lock);
}
}
}
}
return NULL;
}
int n_clipboard_init(void) {
if (s_ready)
return 0;
s_dpy = XOpenDisplay(NULL);
if (!s_dpy) {
n_log(LOG_INFO, "n_clipboard: no X11 display, clipboard backend disabled");
return -1;
}
s_win = XCreateSimpleWindow(s_dpy, RootWindow(s_dpy, DefaultScreen(s_dpy)), 0, 0, 1, 1, 0, 0, 0);
A_CLIPBOARD = XInternAtom(s_dpy, "CLIPBOARD", False);
A_TARGETS = XInternAtom(s_dpy, "TARGETS", False);
A_UTF8 = XInternAtom(s_dpy, "UTF8_STRING", False);
A_TEXT = XInternAtom(s_dpy, "TEXT", False);
A_TEXTPLAIN = XInternAtom(s_dpy, "text/plain", False);
A_TEXTPLAIN_UTF8 = XInternAtom(s_dpy, "text/plain;charset=utf-8", False);
A_INCR = XInternAtom(s_dpy, "INCR", False);
(void)A_INCR;
__atomic_store_n(&s_running, 1, __ATOMIC_RELEASE);
if (pthread_create(&s_thread, NULL, clip_thread_main, NULL) != 0) {
__atomic_store_n(&s_running, 0, __ATOMIC_RELEASE);
XDestroyWindow(s_dpy, s_win);
XCloseDisplay(s_dpy);
s_dpy = NULL;
n_log(LOG_ERR, "n_clipboard: could not start the serving thread");
return -1;
}
s_ready = 1;
n_log(LOG_INFO, "n_clipboard: X11 clipboard backend ready");
return 0;
}
int n_clipboard_available(void) {
if (!s_ready)
n_clipboard_init();
return s_ready;
}
int n_clipboard_set(int which, const char* text) {
if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
return -1;
if (!n_clipboard_available())
return -1;
pthread_mutex_lock(&s_lock);
free(s_text[which]);
s_text[which] = (text && text[0]) ? clip_dupn(text, strlen(text)) : NULL;
pthread_mutex_unlock(&s_lock);
if (s_text[which])
XSetSelectionOwner(s_dpy, clip_sel_atom(which), s_win, CurrentTime);
else
XSetSelectionOwner(s_dpy, clip_sel_atom(which), None, CurrentTime);
XFlush(s_dpy);
return 0;
}
char* n_clipboard_get(int which) {
Display* d;
Window w;
Atom sel, prop;
int fd, tries;
char* result = NULL;
if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
return NULL;
if (!n_clipboard_available())
return NULL;
/* fast path: we own it, so hand back our own copy without a round trip */
pthread_mutex_lock(&s_lock);
if (s_text[which]) {
result = clip_dupn(s_text[which], strlen(s_text[which]));
pthread_mutex_unlock(&s_lock);
return result;
}
pthread_mutex_unlock(&s_lock);
/* another client owns it: request the text on a private connection so the owner
thread's connection is untouched */
d = XOpenDisplay(NULL);
if (!d)
return NULL;
w = XCreateSimpleWindow(d, RootWindow(d, DefaultScreen(d)), 0, 0, 1, 1, 0, 0, 0);
sel = (which == N_CLIPBOARD_PRIMARY) ? XA_PRIMARY : XInternAtom(d, "CLIPBOARD", False);
prop = XInternAtom(d, "NIL_CLIPBOARD_TARGET", False);
if (XGetSelectionOwner(d, sel) == None) {
XDestroyWindow(d, w);
XCloseDisplay(d);
return NULL;
}
XConvertSelection(d, sel, XInternAtom(d, "UTF8_STRING", False), prop, w, CurrentTime);
XFlush(d);
fd = ConnectionNumber(d);
for (tries = 0; tries < 20 && !result; tries++) { /* up to ~2s */
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = 0;
tv.tv_usec = 100000;
select(fd + 1, &fds, NULL, NULL, &tv);
while (XPending(d)) {
XEvent ev;
XNextEvent(d, &ev);
if (ev.type == SelectionNotify) {
if (ev.xselection.property != None) {
Atom rtype = None;
int rfmt = 0;
unsigned long nitems = 0, after = 0;
unsigned char* data = NULL;
if (XGetWindowProperty(d, w, prop, 0, (~0L), False, AnyPropertyType, &rtype, &rfmt,
&nitems, &after, &data) == Success &&
data) {
result = clip_dupn((const char*)data, nitems);
}
if (data)
XFree(data);
XDeleteProperty(d, w, prop);
}
tries = 20; /* got the reply (even if empty): stop waiting */
break;
}
}
}
XDestroyWindow(d, w);
XCloseDisplay(d);
return result;
}
void n_clipboard_destroy(void) {
int i;
if (!s_ready)
return;
__atomic_store_n(&s_running, 0, __ATOMIC_RELEASE);
pthread_join(s_thread, NULL);
if (s_dpy) {
XDestroyWindow(s_dpy, s_win);
XCloseDisplay(s_dpy);
s_dpy = NULL;
}
for (i = 0; i < 2; i++) {
free(s_text[i]);
s_text[i] = NULL;
}
s_ready = 0;
}
#elif defined(__windows__) /* Win32 system clipboard (CLIPBOARD only, no PRIMARY selection) */
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <pthread.h>
/* Windows has a single system clipboard and no X11-style PRIMARY selection, so
* N_CLIPBOARD_PRIMARY is a graceful no-op here (set does nothing, get returns
* NULL); N_CLIPBOARD_CLIPBOARD maps to the Win32 clipboard via CF_UNICODETEXT.
* The OS owns the data once SetClipboardData succeeds, so no serving thread is
* needed. Text crosses this API as UTF-8 and is converted to/from UTF-16 here.
* A process-local mutex serialises access so two threads in this process do not
* fight over OpenClipboard (which is single-open per process). */
static int s_ready = 0;
static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;
static int clip_open(void) {
int tries;
for (tries = 0; tries < 10; tries++) {
if (OpenClipboard(NULL))
return 1;
Sleep(5);
}
return 0;
}
int n_clipboard_init(void) {
s_ready = 1; /* the Win32 clipboard is always available */
n_log(LOG_INFO, "n_clipboard: Win32 clipboard backend ready");
return 0;
}
int n_clipboard_available(void) {
if (!s_ready)
n_clipboard_init();
return s_ready;
}
int n_clipboard_set(int which, const char* text) {
int wlen, rc = -1;
HGLOBAL hmem;
if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
return -1;
if (!n_clipboard_available())
return -1;
/* no PRIMARY selection on Windows: accept the call but do nothing */
if (which == N_CLIPBOARD_PRIMARY)
return 0;
pthread_mutex_lock(&s_lock);
if (!clip_open()) {
pthread_mutex_unlock(&s_lock);
return -1;
}
if (!EmptyClipboard()) {
CloseClipboard();
pthread_mutex_unlock(&s_lock);
return -1;
}
/* empty text relinquishes the clipboard: it is now cleared, nothing to set */
if (!text || !text[0]) {
CloseClipboard();
pthread_mutex_unlock(&s_lock);
return 0;
}
wlen = MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); /* includes the NUL */
if (wlen <= 0) {
CloseClipboard();
pthread_mutex_unlock(&s_lock);
return -1;
}
hmem = GlobalAlloc(GMEM_MOVEABLE, (size_t)wlen * sizeof(wchar_t));
if (hmem) {
wchar_t* wbuf = (wchar_t*)GlobalLock(hmem);
if (wbuf) {
MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf, wlen);
GlobalUnlock(hmem);
if (SetClipboardData(CF_UNICODETEXT, hmem)) {
rc = 0; /* the system owns hmem now, do not free it */
} else {
GlobalFree(hmem); /* still ours on failure */
}
} else {
GlobalFree(hmem);
}
}
CloseClipboard();
pthread_mutex_unlock(&s_lock);
if (rc != 0)
n_log(LOG_ERR, "n_clipboard: failed to set the Win32 clipboard");
return rc;
}
char* n_clipboard_get(int which) {
HANDLE h;
char* out = NULL;
if (which != N_CLIPBOARD_CLIPBOARD && which != N_CLIPBOARD_PRIMARY)
return NULL;
if (!n_clipboard_available())
return NULL;
if (which == N_CLIPBOARD_PRIMARY)
return NULL; /* no PRIMARY selection on Windows */
if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
return NULL;
pthread_mutex_lock(&s_lock);
if (!clip_open()) {
pthread_mutex_unlock(&s_lock);
return NULL;
}
h = GetClipboardData(CF_UNICODETEXT);
if (h) {
const wchar_t* wptr = (const wchar_t*)GlobalLock(h);
if (wptr) {
int len = WideCharToMultiByte(CP_UTF8, 0, wptr, -1, NULL, 0, NULL, NULL); /* includes the NUL */
if (len > 0) {
out = malloc((size_t)len);
if (out)
WideCharToMultiByte(CP_UTF8, 0, wptr, -1, out, len, NULL, NULL);
}
GlobalUnlock(h);
}
}
CloseClipboard();
pthread_mutex_unlock(&s_lock);
return out;
}
void n_clipboard_destroy(void) {
s_ready = 0;
}
#else /* no X11 or Win32 backend: unavailable, caller falls back to its toolkit clipboard */
int n_clipboard_init(void) {
return -1;
}
int n_clipboard_available(void) {
return 0;
}
int n_clipboard_set(int which, const char* text) {
(void)which;
(void)text;
return -1;
}
char* n_clipboard_get(int which) {
(void)which;
return NULL;
}
void n_clipboard_destroy(void) {
}
#endif
Modules
- AABB3D
- ACCEPT_POOL
- ALLEGRO4
- ALLEGRO5
- ANIM_LIB
- ASTAR
- ATLAS_LIB
- AVRO
- CLOCK_SYNC
- COMMONS
- CONFIGFILE
- CYPHER_BASE64
- CYPHER_VIGENERE
- DEAD_RECKONING
- ENUMS
- EXCEPTIONS
- GAME_ENV
- GIT
- GUI
- HASH_TABLE
- ISOMETRIC_ENGINE
- LIST
- LOG
- LOGNODUP
- LZ4
- NETWORKING
- NETWORK_MSG
- N_COOKIES
- N_DIFF
- N_DIGEST
- N_DISK_CACHE
- N_DNS
- N_ECC
- N_ENTROPY
- N_FILES
- N_FLUIDS
- N_GUI_COLOR
- N_GUI_COLORWHEEL
- N_GUI_CURVE
- N_GUI_GRID
- N_GUI_POPUP
- N_HEX
- N_HTTP3
- N_HTTP_COOKIE
- N_HTTP_FORM
- N_HTTP_MIME
- N_HTTP_REQ
- N_HTTP_RESP
- N_HTTP_ROUTER
- N_HTTP_SERVER
- N_ICC
- N_JSON
- N_KAFKA
- N_LINALG
- N_MODULE
- N_OPENSSL_COMPAT
- N_PRETTY
- N_PTH
- N_QUERY
- N_RANDOM
- N_SHA256
- N_STR
- N_TIFF
- N_TIME
- N_USER
- N_X509
- N_ZIP
- PARTICLES
- PCRE
- PHYSICS
- SIGNALS
- STACK
- THREADS
- TRAJECTORY
- TREE
- ZLIB