add x11 clipboard

This commit is contained in:
Matthew 2025-10-05 14:39:50 +11:00
parent 2af4a29314
commit 57e3634691
2 changed files with 479 additions and 43 deletions

View File

@ -159,6 +159,16 @@ struct InputEvent
i32 rel_y; i32 rel_y;
} }
enum ClipboardMode
{
Clipboard,
Primary,
Selection = Primary,
Secondary,
Max
}
alias CBM = ClipboardMode;
struct Inputs struct Inputs
{ {
@ -166,6 +176,24 @@ struct Inputs
Arena arena; Arena arena;
} }
pragma(inline) bool
Shift(Modifier md)
{
return cast(bool)(md & (MD.LeftShift | MD.RightShift));
}
pragma(inline) bool
Ctrl(Modifier md)
{
return cast(bool)(md & (MD.LeftCtrl | MD.RightCtrl));
}
pragma(inline) bool
Alt(Modifier md)
{
return cast(bool)(md & (MD.LeftAlt | MD.RightAlt));
}
Inputs* Inputs*
GetEvents(PlatformWindow* window) GetEvents(PlatformWindow* window)
{ {
@ -347,6 +375,7 @@ import core.sys.posix.dlfcn;
import core.sys.posix.sys.mman; import core.sys.posix.sys.mman;
import core.sys.linux.sys.inotify; import core.sys.linux.sys.inotify;
import core.sys.linux.fcntl; import core.sys.linux.fcntl;
import core.sys.posix.sys.time : timespec, timeval, gettimeofday;
import core.sys.posix.unistd; import core.sys.posix.unistd;
import core.sys.posix.pthread : PThread = pthread_t, import core.sys.posix.pthread : PThread = pthread_t,
PThreadCond = pthread_cond_t, PThreadCond = pthread_cond_t,
@ -358,10 +387,14 @@ import core.sys.posix.pthread : PThread = pthread_t,
PThreadCondWait = pthread_cond_wait, PThreadCondWait = pthread_cond_wait,
PThreadMutexUnlock = pthread_mutex_unlock, PThreadMutexUnlock = pthread_mutex_unlock,
PThreadCondSignal = pthread_cond_signal, PThreadCondSignal = pthread_cond_signal,
PThreadExit = pthread_exit; PThreadExit = pthread_exit,
PThreadCondTimedWait = pthread_cond_timedwait;
import core.stdc.string : strlen; import core.stdc.string : strlen;
const u32 X11_CB_TRANSFER_SIZE_DEFAULT = 1048576;
const u32 X11_TIMEOUT_DEFAULT = 1500;
struct SysThread struct SysThread
{ {
PThread handle; PThread handle;
@ -369,6 +402,40 @@ struct SysThread
PThreadMutex mut; PThreadMutex mut;
} }
struct Selection
{
bool owned;
u8[] data;
xcb_atom_t target;
xcb_atom_t xmode;
}
enum Atoms
{
Targets,
Multiple,
Timestamp,
Incr,
Clipboard,
Utf8String,
WMProtocols,
DeleteWindow,
StateHidden,
Max,
}
const char[][] ATOM_STRS = [
CastStr!(char)("TARGETS"),
CastStr!(char)("MULTIPLE"),
CastStr!(char)("TIMESTAMP"),
CastStr!(char)("INCR"),
CastStr!(char)("CLIPBOARD"),
CastStr!(char)("UTF8_STRING"),
CastStr!(char)("WM_PROTOCOLS"),
CastStr!(char)("WM_DELETE_WINDOW"),
CastStr!(char)("_NET_WM_STATE_HIDDEN"),
];
alias PThreadProc = extern (C) void* function(void*); alias PThreadProc = extern (C) void* function(void*);
SysThread SysThread
@ -442,12 +509,11 @@ PushMotion(Inputs* inputs, i32 rel_x, i32 rel_y, i32 x, i32 y)
struct PlatformWindow struct PlatformWindow
{ {
Display *display; xcb_atom_t[Atoms.max] atoms;
xcb_connection_t *conn; Display* display;
xcb_screen_t *screen; xcb_connection_t* conn;
xcb_screen_t* screen;
xcb_window_t window; xcb_window_t window;
xcb_atom_t close_event;
xcb_atom_t minimize_event;
u16 w; u16 w;
u16 h; u16 h;
i32 mouse_prev_x; i32 mouse_prev_x;
@ -461,6 +527,10 @@ struct PlatformWindow
u32 input_idx; u32 input_idx;
Inputs[2] inputs; Inputs[2] inputs;
TicketMut input_mutex; TicketMut input_mutex;
Mut cb_mut;
Selection[CBM.max] selections;
version(linux) u32 cb_transfer_size;
}; };
struct Library struct Library
@ -496,12 +566,18 @@ CreateWindow(string name, u16 width, u16 height)
h: height, h: height,
input_mutex: CreateTicketMut(), input_mutex: CreateTicketMut(),
msg_queue: CreateMessageQueue(), msg_queue: CreateMessageQueue(),
cb_mut: CreateMut(),
inputs: [ inputs: [
{ arena: CreateArena(MB(1)) }, { arena: CreateArena(MB(1)) },
{ arena: CreateArena(MB(1)) }, { arena: CreateArena(MB(1)) },
], ],
}; };
version(linux)
{
window.cb_transfer_size = X11_CB_TRANSFER_SIZE_DEFAULT;
}
assert(width > 0 && height > 0, "CreateWindow error: width and height must be above 0"); assert(width > 0 && height > 0, "CreateWindow error: width and height must be above 0");
window.display = XOpenDisplay(null); window.display = XOpenDisplay(null);
@ -523,7 +599,8 @@ CreateWindow(string name, u16 width, u16 height)
XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_PRESS |
XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_RELEASE |
XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_POINTER_MOTION |
XCB_EVENT_MASK_STRUCTURE_NOTIFY; XCB_EVENT_MASK_STRUCTURE_NOTIFY|
XCB_EVENT_MASK_PROPERTY_CHANGE;
i32[2] val_win = [window.screen.black_pixel, event_mask]; i32[2] val_win = [window.screen.black_pixel, event_mask];
i32 val_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; i32 val_mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
@ -568,42 +645,36 @@ CreateWindow(string name, u16 width, u16 height)
CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure");
pureFree(error); pureFree(error);
xcb_intern_atom_cookie_t c_proto = xcb_intern_atom(window.conn, 1, 12, "WM_PROTOCOLS"); for(u64 i = 0; i < Atoms.max; i += 1)
xcb_intern_atom_reply_t *r_proto = xcb_intern_atom_reply(window.conn, c_proto, &error); {
CheckErr(&window, &cookie, error, "xcb_intern_atom WM_PROTOCOLS failure"); xcb_intern_atom_cookie_t intern = xcb_intern_atom(window.conn, 1, cast(u16)ATOM_STRS[i].length, ATOM_STRS[i].ptr);
pureFree(error); xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(window.conn, intern, &error);
CheckErr(&window, &cookie, error, "xcb_intern_atom failure");
xcb_intern_atom_cookie_t c_close = xcb_intern_atom(window.conn, 0, 16, "WM_DELETE_WINDOW"); window.atoms[i] = reply.atom;
xcb_intern_atom_reply_t *r_close = xcb_intern_atom_reply(window.conn, c_close, &error);
CheckErr(&window, &cookie, error, "xcb_intern_atom WM_DELETE_WINDOW failure");
pureFree(error);
xcb_intern_atom_cookie_t c_minimize = xcb_intern_atom(window.conn, 0, 20, "_NET_WM_STATE_HIDDEN");
xcb_intern_atom_reply_t *r_minimize = xcb_intern_atom_reply(window.conn, c_minimize, &error);
CheckErr(&window, &cookie, error, "xcb_intern_atom _NET_WM_STATE_HIDDEN failure");
pureFree(error); pureFree(error);
pureFree(reply);
}
window.selections[CBM.Clipboard].xmode = window.atoms[Atoms.Clipboard];
window.selections[CBM.Primary].xmode = XCB_ATOM_PRIMARY;
window.selections[CBM.Secondary].xmode = XCB_ATOM_SECONDARY;
cookie = xcb_change_property_checked( cookie = xcb_change_property_checked(
window.conn, window.conn,
XCB_PROP_MODE_REPLACE, XCB_PROP_MODE_REPLACE,
window.window, window.window,
r_proto.atom, window.atoms[Atoms.WMProtocols],
XCB_ATOM_ATOM, XCB_ATOM_ATOM,
32, 32,
1, 1,
&r_close.atom &window.atoms[Atoms.DeleteWindow]
); );
error = xcb_request_check(window.conn, cookie); error = xcb_request_check(window.conn, cookie);
CheckErr(&window, &cookie, error, "xcb_change_property_checked failure"); CheckErr(&window, &cookie, error, "xcb_change_property_checked failure");
pureFree(error); pureFree(error);
window.close_event = r_close.atom;
window.minimize_event = r_minimize.atom;
pureFree(r_proto);
pureFree(r_close);
pureFree(r_minimize);
xcb_map_window(window.conn, window.window); xcb_map_window(window.conn, window.window);
i32 stream_result = xcb_flush(window.conn); i32 stream_result = xcb_flush(window.conn);
@ -699,6 +770,333 @@ FlushEvents(PlatformWindow* window)
} while (e); } while (e);
} }
bool
TransmitSelection(PlatformWindow* w, xcb_selection_request_event_t* ev)
{
bool result;
if(ev.property == XCB_NONE)
{
ev.property = ev.target;
}
if(ev.target == w.atoms[Atoms.Targets])
{
xcb_atom_t[3] targets = [
w.atoms[Atoms.Timestamp],
w.atoms[Atoms.Targets],
w.atoms[Atoms.Utf8String]
];
xcb_change_property(
w.conn,
XCB_PROP_MODE_REPLACE,
ev.requestor,
ev.property,
XCB_ATOM_ATOM,
xcb_atom_t.sizeof * 8,
targets.length,
targets.ptr
);
result = true;
}
else if(ev.target == w.atoms[Atoms.Timestamp])
{
xcb_timestamp_t cur = XCB_CURRENT_TIME;
xcb_change_property(
w.conn,
XCB_PROP_MODE_REPLACE,
ev.requestor,
ev.property,
XCB_ATOM_INTEGER,
cur.sizeof * 8,
1,
&cur
);
result = true;
}
else if(ev.target == w.atoms[Atoms.Utf8String])
{
Selection* sel = null;
if(TryLock(&w.cb_mut))
{
foreach(i; CBM.min .. CBM.max)
{
if(w.selections[i].xmode == ev.selection)
{
sel = &w.selections[i];
break;
}
}
if(sel != null && sel.owned && sel.data.length > 0 && sel.target == ev.target)
{
xcb_change_property(
w.conn,
XCB_PROP_MODE_REPLACE,
ev.requestor,
ev.property,
ev.target,
8,
cast(u32)sel.data.length,
sel.data.ptr
);
result = true;
}
Unlock(&w.cb_mut);
}
}
return result;
}
bool
ClipboardOwns(PlatformWindow* w, ClipboardMode mode)
{
bool result;
if(TryLock(&w.cb_mut))
{
result = w.selections[mode].owned;
Unlock(&w.cb_mut);
}
return result;
}
u8[]
ClipboardText(PlatformWindow* w, ClipboardMode mode)
{
u8[] buf;
if(TryLock(&w.cb_mut))
{
scope(exit) Unlock(&w.cb_mut);
Selection* sel = &w.selections[mode];
if(sel.owned)
{
buf = GetClipboardSelection(w, sel);
}
else
{
timeval now;
timespec timeout;
int pret = 0;
auto owner = xcb_get_selection_owner_reply(w.conn, xcb_get_selection_owner(w.conn, sel.xmode), null);
scope(exit) pureFree(owner);
if(owner != null && owner.owner != 0)
{
FreeArray(sel.data);
sel.data = [];
sel.target = w.atoms[Atoms.Utf8String];
xcb_convert_selection(w.conn, w.window, sel.xmode, sel.target, sel.xmode, XCB_CURRENT_TIME);
xcb_flush(w.conn);
gettimeofday(&now, null);
timeout.tv_sec = now.tv_sec + (X11_TIMEOUT_DEFAULT / 1000);
timeout.tv_nsec = (now.tv_usec * 1000UL) * ((X11_TIMEOUT_DEFAULT % 1000) * 1000000UL);
if(timeout.tv_nsec >= 1000000000UL)
{
timeout.tv_sec += timeout.tv_nsec / 1000000000UL;
timeout.tv_nsec = timeout.tv_nsec % 1000000000UL;
}
while(pret == 0 && sel.data.length == 0)
{
pret = PThreadCondTimedWait(&w.thread.cond, &w.thread.mut, &timeout);
}
GetClipboardSelection(w, sel);
}
}
}
return buf;
}
u8[]
ClipboardText(PlatformWindow* w)
{
return ClipboardText(w, CBM.Clipboard);
}
bool
SetClipboard(PlatformWindow* w, u8[] data, ClipboardMode mode)
{
bool result = true;
if(data.length > 0 && TryLock(&w.cb_mut))
{
Selection* sel = &w.selections[mode];
if(sel.data.length > 0)
{
FreeArray(sel.data);
sel.data = [];
}
sel.data = Alloc!(u8)(data.length+1);
if(sel.data.length > 0)
{
MemCpy(sel.data.ptr, data.ptr, data.length);
sel.data[sel.data.length-1] = '\0';
sel.owned = true;
sel.target = w.atoms[Atoms.Utf8String];
xcb_set_selection_owner(w.conn, w.window, sel.xmode, XCB_CURRENT_TIME);
xcb_flush(w.conn);
}
else
{
result = false;
}
}
return result;
}
bool
SetClipboard(PlatformWindow* w, u8[] data)
{
return SetClipboard(w, data, CBM.Clipboard);
}
u8[]
GetClipboardSelection(PlatformWindow* w, Selection* sel)
{
u8[] buf;
if(sel.data.length > 0 && sel.target == w.atoms[Atoms.Utf8String])
{
buf = ScratchAllocCopy(sel.data);
}
return buf;
}
void
RetrieveSelection(PlatformWindow* w, xcb_selection_notify_event_t* ev)
{
u8[] buf;
u64 buf_size;
u64 bytes_after = 1;
xcb_get_property_reply_t* reply;
xcb_atom_t actual_type;
u8 actual_format;
if(ev.property == XCB_ATOM_PRIMARY || ev.property == XCB_ATOM_SECONDARY || ev.property == w.atoms[Atoms.Clipboard])
{
while(bytes_after > 0)
{
scope(exit) pureFree(reply);
xcb_get_property_cookie_t cookie = xcb_get_property(
w.conn,
true,
w.window,
ev.property,
XCB_ATOM_ANY,
cast(u32)(buf_size/4),
cast(u32)(w.cb_transfer_size/4)
);
reply = xcb_get_property_reply(w.conn, cookie, null);
if(reply == null || (buf_size > 0 && (reply.format != actual_format || reply.type != actual_type)) || reply.format%8 != 0)
{
Errf("RetrieveSelection failure: Invalid return value from xcb_get_property_reply");
break;
}
if(buf_size == 0)
{
actual_type = reply.type;
actual_format = reply.format;
}
int nitems = xcb_get_property_value_length(reply);
if(nitems > 0)
{
if(buf_size%4 != 0)
{
Errf("RetrieveSelection failure: Data size is not a multiple of 4");
break;
}
u64 unit_size = reply.format/8;
buf = Alloc!(u8)(unit_size * (buf_size + nitems));
MemCpy(buf.ptr + buf_size, xcb_get_property_value(reply), nitems * unit_size);
buf_size += nitems * unit_size;
}
bytes_after = reply.bytes_after;
}
}
if(buf != null && TryLock(&w.cb_mut))
{
Selection* sel;
foreach(i; CBM.min .. CBM.max)
{
if(w.selections[i].xmode == ev.property)
{
sel = &w.selections[i];
break;
}
}
if(sel != null && sel.target == actual_type)
{
FreeArray(sel.data);
sel.data = buf;
buf = [];
}
else
{
Errf("RetrieveSelection failure: mismatched selection actual_type: %s", actual_type);
}
Unlock(&w.cb_mut);
}
else
{
FreeArray(buf);
}
}
void
ClearSelection(PlatformWindow* w, xcb_selection_clear_event_t* ev)
{
if(ev.owner == w.window)
{
foreach(i; CBM.min .. CBM.max)
{
Selection* sel = &w.selections[i];
if(sel.xmode == ev.selection && TryLock(&w.cb_mut))
{
FreeArray(sel.data);
sel.data = [];
sel.owned = false;
sel.target = XCB_NONE;
Unlock(&w.cb_mut);
break;
}
}
}
}
void void
HandleEvents(void* window_ptr) HandleEvents(void* window_ptr)
{ {
@ -782,7 +1180,7 @@ HandleEvents(void* window_ptr)
break; break;
} }
if(msg.data.data32[0] == w.close_event) if(msg.data.data32[0] == w.atoms[Atoms.DeleteWindow])
{ {
w.close = true; w.close = true;
} }
@ -878,6 +1276,29 @@ HandleEvents(void* window_ptr)
w.h = config_event.height; w.h = config_event.height;
} }
} break; } break;
case XCB_SELECTION_CLEAR:
{
ClearSelection(w, cast(xcb_selection_clear_event_t*)e);
} break;
case XCB_SELECTION_NOTIFY:
{
RetrieveSelection(w, cast(xcb_selection_notify_event_t*)e);
} break;
case XCB_SELECTION_REQUEST:
{
auto req = cast(xcb_selection_request_event_t*)e;
xcb_selection_notify_event_t notify = {
response_type: XCB_SELECTION_NOTIFY,
time: XCB_CURRENT_TIME,
requestor: req.requestor,
selection: req.selection,
target: req.target,
property: TransmitSelection(w, req) ? req.property : XCB_NONE,
};
xcb_send_event(w.conn, false, req.requestor, XCB_EVENT_MASK_PROPERTY_CHANGE, cast(char*)&notify);
xcb_flush(w.conn);
} break;
default: default:
break; break;
} }

17
util.d
View File

@ -6,7 +6,7 @@ import dlib.alloc;
import xxhash3; import xxhash3;
import includes; import includes;
import std.stdio; import std.stdio : write, writeln, writefln, stderr;
import std.conv; import std.conv;
import std.string; import std.string;
import std.traits; import std.traits;
@ -43,6 +43,7 @@ Logf(Args...)(string fmt, Args args)
{ {
try try
{ {
write("[INFO]: ");
writefln(fmt, args); writefln(fmt, args);
} }
catch (Exception e) catch (Exception e)
@ -51,6 +52,20 @@ Logf(Args...)(string fmt, Args args)
} }
} }
void
Errf(Args...)(string fmt, Args args)
{
try
{
stderr.write("[ERROR]: ");
stderr.writef(fmt, args, '\n');
}
catch(Exception e)
{
assert(false, "Incompatible format type");
}
}
void void
Log(string str) Log(string str)
{ {