liberty-xui: add some clipboard pasting abilities
Alpine 3.24 Success
OpenBSD 7.8 Success

This commit is contained in:
2026-08-11 05:30:53 +02:00
parent bb75c8e4fa
commit 285dacb785
+219 -17
View File
@@ -87,6 +87,7 @@ static bool app_process_mouse (termo_mouse_event_t type,
int x, int y, int button, int modifiers);
static bool app_on_insufficient_color (void);
static void app_on_clipboard_copy (const char *text);
static void app_on_clipboard_paste (const char *text);
// This could be overridable, however thus far row_buffer and line_editor both
// depend on XUI being initialized.
@@ -732,6 +733,8 @@ struct ui
struct widget *(*scrollbar)
(chtype attrs, long top, long total);
void (*request_clipboard) (void);
void (*beep) (void);
void (*render) (void);
void (*flip) (void);
@@ -798,17 +801,32 @@ struct xui
// X11:
#ifdef LIBERTY_XUI_WANT_X11
Display *dpy; ///< X display handle
struct poller_fd x11_event; ///< X11 events on wire
struct poller_idle xpending_event; ///< X11 events possibly in I/O queues
int xkb_base_event_code; ///< Xkb base event code
Window x11_window; ///< Application window
Time x11_timestamp; ///< Most recent X server timestamp
Pixmap x11_pixmap; ///< Off-screen bitmap
Region x11_clip; ///< Invalidated region
Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap
XftDraw *xft_draw; ///< Xft rendering context
struct x11_font *xft_fonts; ///< Font collection
char *x11_selection; ///< CLIPBOARD selection contents we own
struct str x11_paste_buffer; ///< Incoming CLIPBOARD UTF-8
Time x11_paste_in_progress; ///< Timestamp of active paste request
bool x11_paste_incr; ///< ICCCM INCR transfer running
bool x11_paste_incr_failure; ///< INCR failure indicator
XIM x11_im; ///< Input method
XIC x11_ic; ///< Input method context
XFontSet x11_im_fontset; ///< Fonts for positional preediting
XIMStyle x11_im_style; ///< Negotiated input method style
XPoint x11_im_spot; ///< Last reported input caret baseline
unsigned x11_im_spot_set; ///< x11_im_spot has been reported
Display *dpy; ///< X display handle
struct poller_fd x11_event; ///< X11 events on wire
struct poller_idle xpending_event; ///< X11 events possibly in I/O queues
int xkb_base_event_code; ///< Xkb base event code
Window x11_window; ///< Application window
Window x11_embed_socket; ///< Socket for an embedded X11 client
Window x11_embed_plug; ///< Embedded X11 client window
bool x11_embed_active; ///< Outer X11 window is active
@@ -818,14 +836,8 @@ struct xui
int x11_embed_top; ///< Embedded socket's top border
int x11_embed_right; ///< Embedded socket's right border
int x11_embed_bottom; ///< Embedded socket's bottom border
Pixmap x11_pixmap; ///< Off-screen bitmap
Region x11_clip; ///< Invalidated region
Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap
XftDraw *xft_draw; ///< Xft rendering context
struct x11_font *xft_fonts; ///< Font collection
char *x11_selection; ///< CLIPBOARD selection
struct xdg_xsettings x11_xsettings; ///< XSETTINGS
struct xdg_xsettings x11_xsettings; ///< XSETTINGS
int32_t x11_double_click_time; ///< Maximum delay for double clicks
int32_t x11_double_click_distance; ///< Maximum distance for double clicks
const char *x11_fontname; ///< Fontconfig font name
@@ -1157,6 +1169,8 @@ static struct ui tui_ui =
.gauge = tui_make_gauge,
.scrollbar = tui_make_scrollbar,
// TODO(p): We can handle bracketed paste via app_on_clipboard_paste.
.beep = tui_beep,
.render = tui_render,
.flip = tui_flip,
@@ -1836,6 +1850,32 @@ x11_render_widget (struct widget *w, const XRectangle *clip)
x11_render_widget (child, &subclip);
}
#define X11_TIME_DIFFERENCE(a, b) ((uint32_t) ((a) - (b)))
static void
x11_request_clipboard (void)
{
// Don't try to process two things at once. Each request gets a few seconds
// to finish, then we move on, hoping that a property race doesn't commence.
if (!g_xui.x11_timestamp || (g_xui.x11_paste_in_progress &&
X11_TIME_DIFFERENCE
(g_xui.x11_timestamp, g_xui.x11_paste_in_progress) < 5000))
return;
Atom xa_clipboard = XInternAtom (g_xui.dpy, "CLIPBOARD", False);
// Most modern applications support this, though Xlib can easily convert.
Atom xa_utf8 = XInternAtom (g_xui.dpy, "UTF8_STRING", False);
// ICCCM says we should ensure the named property doesn't exist
XDeleteProperty (g_xui.dpy, g_xui.x11_window, xa_clipboard);
XConvertSelection (g_xui.dpy, xa_clipboard, xa_utf8, xa_clipboard,
g_xui.x11_window, g_xui.x11_timestamp);
g_xui.x11_paste_in_progress = g_xui.x11_timestamp;
g_xui.x11_paste_incr = false;
}
static void
x11_beep (void)
{
@@ -1924,6 +1964,7 @@ x11_destroy (void)
LIST_FOR_EACH (struct x11_font, font, g_xui.xft_fonts)
x11_font_destroy (font);
cstr_set (&g_xui.x11_selection, NULL);
str_free (&g_xui.x11_paste_buffer);
xdg_xsettings_free (&g_xui.x11_xsettings);
free (g_xui.x_fg);
@@ -1944,6 +1985,8 @@ static struct ui x11_ui =
.gauge = x11_make_gauge,
.scrollbar = x11_make_scrollbar,
.request_clipboard = x11_request_clipboard,
.beep = x11_beep,
.render = x11_render,
.flip = x11_flip,
@@ -2127,7 +2170,7 @@ x11_find_text (struct widget *list, int x, int y)
static bool
x11_process_press (int x, int y, int button, int modifiers)
{
if (button != Button3)
if (button != Button3 || !g_xui.x11_timestamp)
goto out;
char *text = x11_find_text (g_xui.widgets, x, y);
@@ -2139,7 +2182,7 @@ x11_process_press (int x, int y, int button, int modifiers)
cstr_set (&g_xui.x11_selection, text);
XSetSelectionOwner (g_xui.dpy, XInternAtom (g_xui.dpy, "CLIPBOARD", False),
g_xui.x11_window, CurrentTime);
g_xui.x11_window, g_xui.x11_timestamp);
app_on_clipboard_copy (g_xui.x11_selection);
return true;
@@ -2199,6 +2242,106 @@ on_x11_input_event (XEvent *ev)
return false;
}
static bool
x11_read_selection_property (Window wid, Atom property, bool *empty)
{
Atom xa_utf8 = XInternAtom (g_xui.dpy, "UTF8_STRING", False);
long offset = 0;
bool more_data = true, ok = true;
while (ok && more_data)
{
Atom type = None;
int format = 0;
unsigned long nitems = 0, bytes_after = 0;
unsigned char *data = NULL;
if (XGetWindowProperty (g_xui.dpy, wid, property, offset, 0x8000,
False /* delete */, AnyPropertyType, &type, &format,
&nitems, &bytes_after, &data) != Success)
return false;
if (offset == 0 && nitems == 0 && empty)
*empty = true;
more_data = bytes_after != 0;
if ((ok = type == xa_utf8 && format == 8))
{
offset += (long) (nitems >> 2);
str_append_data (&g_xui.x11_paste_buffer, data, nitems);
}
XFree (data);
}
return ok;
}
static void
on_x11_selection_notify (XSelectionEvent *ev)
{
Atom xa_clipboard = XInternAtom (g_xui.dpy, "CLIPBOARD", False);
Atom xa_incr = XInternAtom (g_xui.dpy, "INCR", False);
if (ev->selection != xa_clipboard
|| ev->time != g_xui.x11_paste_in_progress)
return;
// TODO(p): Perhaps report failures to the application as well.
g_xui.x11_paste_in_progress = 0;
if (ev->property == None)
return;
Atom type = None;
int format = 0;
unsigned long nitems = 0, bytes_after = 0;
unsigned char *data = NULL;
if (XGetWindowProperty (g_xui.dpy, ev->requestor, ev->property,
0, 0, False /* delete */, AnyPropertyType, &type, &format,
&nitems, &bytes_after, &data) != Success)
goto out;
XFree (data);
str_reset (&g_xui.x11_paste_buffer);
// When you select a lot of text in VIM, it starts the ICCCM INCR mechanism,
// from which there is no opt-out.
if (type == xa_incr)
{
g_xui.x11_paste_in_progress = ev->time;
g_xui.x11_paste_incr = true;
g_xui.x11_paste_incr_failure = false;
}
else if (x11_read_selection_property (ev->requestor, ev->property, NULL))
app_on_clipboard_paste (g_xui.x11_paste_buffer.str);
out:
XDeleteProperty (g_xui.dpy, g_xui.x11_window, ev->property);
}
static void
on_x11_property_notify (XPropertyEvent *ev)
{
Atom xa_clipboard = XInternAtom (g_xui.dpy, "CLIPBOARD", False);
if (!g_xui.x11_paste_incr
|| ev->window != g_xui.x11_window
|| ev->state != PropertyNewValue
|| ev->atom != xa_clipboard)
return;
bool empty = false;
if (!x11_read_selection_property (ev->window, ev->atom, &empty))
// We need to keep deleting the property.
g_xui.x11_paste_incr_failure = true;
// Once it's empty, we've consumed everything and can move on undisturbed.
if (empty)
{
if (!g_xui.x11_paste_incr_failure)
app_on_clipboard_paste (g_xui.x11_paste_buffer.str);
g_xui.x11_paste_in_progress = 0;
g_xui.x11_paste_incr = false;
}
XDeleteProperty (g_xui.dpy, ev->window, ev->atom);
}
static void
on_x11_selection_request (XSelectionRequestEvent *ev)
{
@@ -2581,9 +2724,41 @@ x11_embed_process_event (XEvent *event)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static Time
x11_event_timestamp (const XEvent *ev)
{
switch (ev->type)
{
case KeyPress:
case KeyRelease:
return ev->xkey.time;
case ButtonPress:
case ButtonRelease:
return ev->xbutton.time;
case MotionNotify:
return ev->xmotion.time;
case EnterNotify:
case LeaveNotify:
return ev->xcrossing.time;
case PropertyNotify:
return ev->xproperty.time;
case SelectionClear:
return ev->xselectionclear.time;
default:
return 0;
}
}
static void
on_x11_event (XEvent *ev)
{
// X11 timestamps are wrapping 32-bit values. Ignore CurrentTime and
// events older than the latest one we've already processed.
Time timestamp = x11_event_timestamp (ev);
if (timestamp && (!g_xui.x11_timestamp ||
X11_TIME_DIFFERENCE (timestamp, g_xui.x11_timestamp) < UINT32_MAX / 2))
g_xui.x11_timestamp = timestamp;
if (x11_embed_process_event (ev))
return;
@@ -2613,12 +2788,18 @@ on_x11_event (XEvent *ev)
XftDrawChange (g_xui.xft_draw, g_xui.x11_pixmap);
xui_invalidate ();
break;
case SelectionRequest:
on_x11_selection_request (&ev->xselectionrequest);
case PropertyNotify:
on_x11_property_notify (&ev->xproperty);
break;
case SelectionClear:
cstr_set (&g_xui.x11_selection, NULL);
break;
case SelectionRequest:
on_x11_selection_request (&ev->xselectionrequest);
break;
case SelectionNotify:
on_x11_selection_notify (&ev->xselection);
break;
// UnmapNotify can be received when restarting the window manager.
// Should this turn out to be unreliable (window not destroyed by WM
// upon closing), opt for the WM_DELETE_WINDOW protocol as well.
@@ -2891,7 +3072,8 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
{
.event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask
| KeyPressMask | KeyReleaseMask
| ButtonPressMask | ButtonReleaseMask | Button1MotionMask,
| ButtonPressMask | ButtonReleaseMask | Button1MotionMask
| PropertyChangeMask,
.bit_gravity = NorthWestGravity,
.background_pixel = default_bg.pixel,
};
@@ -2948,6 +3130,7 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
x11_init_pixmap ();
g_xui.xft_draw = XftDrawCreate (g_xui.dpy, g_xui.x11_pixmap, visual, cmap);
g_xui.x11_paste_buffer = str_make ();
g_xui.ui = &x11_ui;
XMapWindow (g_xui.dpy, g_xui.x11_window);
@@ -3242,6 +3425,23 @@ appkit_render_widget (struct widget *w, NSRect clip)
appkit_render_widget (child, subclip);
}
static void
appkit_request_clipboard (void)
{
@autoreleasepool
{
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSString *string = [pasteboard stringForType:NSPasteboardTypeString];
// TODO(p): Perhaps report failures to the application as well.
if (!string)
return;
const char *text = [string UTF8String];
if (text)
app_on_clipboard_paste (text);
}
}
static void
appkit_beep (void)
{
@@ -3306,6 +3506,8 @@ static struct ui appkit_ui =
.gauge = appkit_make_gauge,
.scrollbar = appkit_make_scrollbar,
.request_clipboard = appkit_request_clipboard,
.beep = appkit_beep,
.render = appkit_render,
.flip = appkit_flip,