liberty-xui: add shell out and XEmbed support
Alpine 3.24 Success
OpenBSD 7.8 Success

To allow for program composition.
This commit is contained in:
2026-08-07 08:02:55 +02:00
parent 7f2156c3d7
commit c0ea24d92d
+396 -5
View File
@@ -772,6 +772,7 @@ struct xui
struct poller_timer tk_timer; ///< termo timeout timer
bool locale_is_utf8; ///< The locale is Unicode
bool use_partial_boxes; ///< Use Unicode box drawing chars
bool tui_suspended; ///< TUI is released to a child process
int tui_clip_top; ///< Top row of the current clip region
int tui_clip_bottom; ///< Bottom of the current clip region
@@ -785,6 +786,15 @@ struct xui
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
bool x11_embed_mapped; ///< Plug is mapped by XEmbed request
unsigned long x11_embed_version; ///< Negotiated XEmbed protocol version
int x11_embed_left; ///< Embedded socket's left border
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
@@ -823,7 +833,8 @@ g_xui;
static void
xui_invalidate (void)
{
poller_idle_set (&g_xui.refresh_event);
if (!g_xui.tui_suspended)
poller_idle_set (&g_xui.refresh_event);
}
static bool
@@ -1114,6 +1125,34 @@ static struct ui tui_ui =
.destroy = tui_destroy,
};
static void
xui_tui_suspend (void)
{
hard_assert (g_xui.ui == &tui_ui && !g_xui.tui_suspended);
g_xui.tui_suspended = true;
poller_idle_reset (&g_xui.refresh_event);
poller_idle_reset (&g_xui.flip_event);
poller_fd_reset (&g_xui.tty_event);
poller_timer_reset (&g_xui.tk_timer);
endwin ();
termo_stop (g_xui.tk);
}
static void
xui_tui_resume (void)
{
hard_assert (g_xui.ui == &tui_ui && g_xui.tui_suspended);
if (!termo_start (g_xui.tk))
exit_fatal ("failed to set up the terminal");
g_xui.tui_suspended = false;
poller_fd_set (&g_xui.tty_event, POLLIN);
poller_timer_set (&g_xui.tk_timer, 0);
tui_winch ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
@@ -1166,7 +1205,11 @@ tui_on_tty_readable (const struct pollfd *fd, void *user_data)
int64_t event_ts = clock_msec (CLOCK_BEST);
termo_result_t res;
while ((res = termo_getkey (g_xui.tk, &event)) == TERMO_RES_KEY)
{
tui_on_tty_event (&event, event_ts);
if (!termo_is_started (g_xui.tk))
return;
}
if (res == TERMO_RES_AGAIN)
poller_timer_set (&g_xui.tk_timer, termo_get_waittime (g_xui.tk));
@@ -1784,6 +1827,8 @@ x11_flip (void)
static void
x11_destroy (void)
{
if (g_xui.x11_embed_socket)
XDestroyWindow (g_xui.dpy, g_xui.x11_embed_socket);
XDestroyIC (g_xui.x11_ic);
XCloseIM (g_xui.x11_im);
XDestroyRegion (g_xui.x11_clip);
@@ -2122,9 +2167,341 @@ out:
XSendEvent (g_xui.dpy, ev->requestor, False, 0, &response);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Simple XEmbed integration that takes over all input.
// It seems to work and that's enough.
enum
{
X11_XEMBED_VERSION = 1,
X11_XEMBED_MAPPED = 1 << 0,
X11_XEMBED_FOCUS_WRAPAROUND = 1 << 0,
};
enum
{
X11_XEMBED_EMBEDDED_NOTIFY,
X11_XEMBED_WINDOW_ACTIVATE,
X11_XEMBED_WINDOW_DEACTIVATE,
X11_XEMBED_REQUEST_FOCUS,
X11_XEMBED_FOCUS_IN,
X11_XEMBED_FOCUS_OUT,
X11_XEMBED_FOCUS_NEXT,
X11_XEMBED_FOCUS_PREV,
};
enum
{
X11_XEMBED_FOCUS_CURRENT,
X11_XEMBED_FOCUS_FIRST,
X11_XEMBED_FOCUS_LAST,
};
static void
x11_embed_send (long message, long detail, long data1, long data2)
{
XEvent event = {};
event.xclient.type = ClientMessage;
event.xclient.window = g_xui.x11_embed_plug;
event.xclient.message_type = XInternAtom (g_xui.dpy, "_XEMBED", False);
event.xclient.format = 32;
event.xclient.data.l[0] = CurrentTime;
event.xclient.data.l[1] = message;
event.xclient.data.l[2] = detail;
event.xclient.data.l[3] = data1;
event.xclient.data.l[4] = data2;
XSendEvent (g_xui.dpy, g_xui.x11_embed_plug, False, NoEventMask, &event);
}
static bool
x11_embed_info (Window plug, unsigned long *version, unsigned long *flags)
{
Atom xembed_info = XInternAtom (g_xui.dpy, "_XEMBED_INFO", False);
Atom type = None;
int format = 0;
unsigned long nitems = 0, after = 0;
unsigned char *data = NULL;
int status = XGetWindowProperty (g_xui.dpy, plug, xembed_info,
0, 2, False, xembed_info, &type, &format, &nitems, &after, &data);
if (status != Success || type != xembed_info || format != 32 || nitems < 2)
{
if (data)
XFree (data);
return false;
}
if (version)
*version = ((unsigned long *) data)[0];
if (flags)
*flags = ((unsigned long *) data)[1];
XFree (data);
return true;
}
static void
x11_embed_set_active (bool active)
{
if (!g_xui.x11_embed_plug || active == g_xui.x11_embed_active)
return;
g_xui.x11_embed_active = active;
x11_embed_send (active
? X11_XEMBED_WINDOW_ACTIVATE
: X11_XEMBED_WINDOW_DEACTIVATE, 0, 0, 0);
}
static void
x11_embed_focus (long detail, long flags)
{
if (!g_xui.x11_embed_plug)
return;
XSetInputFocus (g_xui.dpy, g_xui.x11_window, RevertToParent, CurrentTime);
x11_embed_set_active (true);
x11_embed_send (X11_XEMBED_FOCUS_IN, detail, flags, 0);
}
static void
x11_embed_resize (void)
{
int width = MAX (1, g_xui.width -
g_xui.x11_embed_left - g_xui.x11_embed_right);
int height = MAX (1, g_xui.height -
g_xui.x11_embed_top - g_xui.x11_embed_bottom);
if (g_xui.x11_embed_socket)
XMoveResizeWindow (g_xui.dpy, g_xui.x11_embed_socket,
g_xui.x11_embed_left, g_xui.x11_embed_top, width, height);
if (g_xui.x11_embed_plug)
XMoveResizeWindow (g_xui.dpy, g_xui.x11_embed_plug,
0, 0, width, height);
}
static void
x11_embed_set_mapped (bool mapped)
{
if (!g_xui.x11_embed_plug || mapped == g_xui.x11_embed_mapped)
return;
g_xui.x11_embed_mapped = mapped;
if (mapped)
{
XMapWindow (g_xui.dpy, g_xui.x11_embed_socket);
XMapWindow (g_xui.dpy, g_xui.x11_embed_plug);
x11_embed_resize ();
x11_embed_focus (X11_XEMBED_FOCUS_FIRST, 0);
}
else
XUnmapWindow (g_xui.dpy, g_xui.x11_embed_plug);
}
static void
x11_embed_send_configure (void)
{
if (!g_xui.x11_embed_plug)
return;
Window child = None;
int x = 0, y = 0;
XTranslateCoordinates (g_xui.dpy, g_xui.x11_embed_plug,
RootWindow (g_xui.dpy, DefaultScreen (g_xui.dpy)),
0, 0, &x, &y, &child);
XEvent event = {};
event.xconfigure.type = ConfigureNotify;
event.xconfigure.event = g_xui.x11_embed_plug;
event.xconfigure.window = g_xui.x11_embed_plug;
event.xconfigure.x = x;
event.xconfigure.y = y;
event.xconfigure.width = MAX (1, g_xui.width -
g_xui.x11_embed_left - g_xui.x11_embed_right);
event.xconfigure.height = MAX (1, g_xui.height -
g_xui.x11_embed_top - g_xui.x11_embed_bottom);
event.xconfigure.border_width = 0;
event.xconfigure.above = None;
event.xconfigure.override_redirect = False;
XSendEvent (g_xui.dpy, g_xui.x11_embed_plug,
False, NoEventMask, &event);
}
static void
x11_embed_end (void)
{
Window socket = g_xui.x11_embed_socket;
g_xui.x11_embed_socket = None;
g_xui.x11_embed_plug = None;
g_xui.x11_embed_active = false;
g_xui.x11_embed_mapped = false;
g_xui.x11_embed_version = 0;
if (socket)
XDestroyWindow (g_xui.dpy, socket);
XSetInputFocus (g_xui.dpy, g_xui.x11_window, RevertToParent, CurrentTime);
xui_invalidate ();
}
/// Prepare an XEmbed socket within the given application window borders.
/// Returns None when an existing embedded client was merely focused.
static Window
x11_embed_start (int left, int top, int right, int bottom)
{
if (g_xui.x11_embed_plug)
{
x11_embed_focus (X11_XEMBED_FOCUS_CURRENT, 0);
return None;
}
if (g_xui.x11_embed_socket)
x11_embed_end ();
g_xui.x11_embed_left = left;
g_xui.x11_embed_top = top;
g_xui.x11_embed_right = right;
g_xui.x11_embed_bottom = bottom;
int width = MAX (1, g_xui.width - left - right);
int height = MAX (1, g_xui.height - top - bottom);
g_xui.x11_embed_socket = XCreateSimpleWindow (g_xui.dpy,
g_xui.x11_window, left, top, width, height, 0, 0, 0);
XSelectInput (g_xui.dpy, g_xui.x11_embed_socket,
SubstructureNotifyMask | SubstructureRedirectMask);
XFlush (g_xui.dpy);
return g_xui.x11_embed_socket;
}
static void
x11_embed_attach (Window plug)
{
if (g_xui.x11_embed_plug)
return;
g_xui.x11_embed_plug = plug;
XSelectInput (g_xui.dpy, plug, StructureNotifyMask | PropertyChangeMask);
x11_embed_resize ();
unsigned long version = X11_XEMBED_VERSION, flags = 0;
bool has_info = x11_embed_info (plug, &version, &flags);
g_xui.x11_embed_version = MIN (version, X11_XEMBED_VERSION);
x11_embed_send (X11_XEMBED_EMBEDDED_NOTIFY,
0, g_xui.x11_embed_socket, g_xui.x11_embed_version);
if (has_info)
x11_embed_set_mapped (flags & X11_XEMBED_MAPPED);
}
static bool
x11_embed_process_event (XEvent *event)
{
if (!g_xui.x11_embed_socket)
return false;
switch (event->type)
{
case CreateNotify:
if (event->xcreatewindow.parent == g_xui.x11_embed_socket)
x11_embed_attach (event->xcreatewindow.window);
return true;
case MapRequest:
if (event->xmaprequest.parent != g_xui.x11_embed_socket)
return false;
x11_embed_attach (event->xmaprequest.window);
if (event->xmaprequest.window != g_xui.x11_embed_plug)
return true;
unsigned long flags = 0;
bool mapped = !x11_embed_info (g_xui.x11_embed_plug, NULL, &flags)
|| (flags & X11_XEMBED_MAPPED);
x11_embed_set_mapped (mapped);
return true;
case ConfigureRequest:
if (event->xconfigurerequest.parent != g_xui.x11_embed_socket)
return false;
x11_embed_attach (event->xconfigurerequest.window);
if (event->xconfigurerequest.window != g_xui.x11_embed_plug)
return true;
x11_embed_resize ();
x11_embed_send_configure ();
return true;
case ConfigureNotify:
if (event->xconfigure.window != g_xui.x11_embed_plug
&& event->xconfigure.window != g_xui.x11_embed_socket)
return false;
return true;
case PropertyNotify:
if (event->xproperty.window != g_xui.x11_embed_plug)
return false;
if (event->xproperty.atom ==
XInternAtom (g_xui.dpy, "_XEMBED_INFO", False))
{
unsigned long flags = 0;
bool mapped = x11_embed_info (g_xui.x11_embed_plug, NULL, &flags)
&& (flags & X11_XEMBED_MAPPED);
x11_embed_set_mapped (mapped);
}
return true;
case DestroyNotify:
if (event->xdestroywindow.window != g_xui.x11_embed_plug)
return false;
x11_embed_end ();
return true;
case ReparentNotify:
if (!g_xui.x11_embed_plug)
{
if (event->xreparent.parent != g_xui.x11_embed_socket)
return false;
x11_embed_attach (event->xreparent.window);
return true;
}
if (event->xreparent.window != g_xui.x11_embed_plug)
return false;
if (event->xreparent.parent != g_xui.x11_embed_socket)
x11_embed_end ();
return true;
case ClientMessage:
if (event->xclient.window != g_xui.x11_embed_socket
|| event->xclient.message_type !=
XInternAtom (g_xui.dpy, "_XEMBED", False))
return false;
long message = event->xclient.data.l[1];
if (message == X11_XEMBED_REQUEST_FOCUS)
x11_embed_focus (X11_XEMBED_FOCUS_CURRENT, 0);
else if (g_xui.x11_embed_version < 1)
/* Not GTK's version with X11_XEMBED_FOCUS_WRAPAROUND. */;
else if (message == X11_XEMBED_FOCUS_NEXT
&& !(event->xclient.data.l[3] & X11_XEMBED_FOCUS_WRAPAROUND))
x11_embed_focus (X11_XEMBED_FOCUS_FIRST,
X11_XEMBED_FOCUS_WRAPAROUND);
else if (message == X11_XEMBED_FOCUS_PREV
&& !(event->xclient.data.l[3] & X11_XEMBED_FOCUS_WRAPAROUND))
x11_embed_focus (X11_XEMBED_FOCUS_LAST,
X11_XEMBED_FOCUS_WRAPAROUND);
return true;
case KeyPress:
case KeyRelease:
if (event->xkey.window != g_xui.x11_window)
return false;
event->xkey.window = g_xui.x11_embed_plug;
event->xkey.subwindow = None;
event->xkey.x = event->xkey.y = 0;
XSendEvent (g_xui.dpy, g_xui.x11_embed_plug, False, NoEventMask, event);
return true;
default:
return false;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
on_x11_event (XEvent *ev)
{
if (x11_embed_process_event (ev))
return;
termo_key_t key = {};
switch (ev->type)
{
@@ -2143,6 +2520,7 @@ on_x11_event (XEvent *ev)
g_xui.width = ev->xconfigure.width;
g_xui.height = ev->xconfigure.height;
x11_embed_resize ();
XRenderFreePicture (g_xui.dpy, g_xui.x11_pixmap_picture);
XFreePixmap (g_xui.dpy, g_xui.x11_pixmap);
@@ -2160,14 +2538,25 @@ on_x11_event (XEvent *ev)
// Should this turn out to be unreliable (window not destroyed by WM
// upon closing), opt for the WM_DELETE_WINDOW protocol as well.
case DestroyNotify:
app_quit ();
if (ev->xdestroywindow.window == g_xui.x11_window)
app_quit ();
break;
case FocusIn:
if (ev->xfocus.window == g_xui.x11_window
&& ev->xfocus.detail != NotifyInferior
&& ev->xfocus.mode != NotifyGrab
&& ev->xfocus.mode != NotifyUngrab)
x11_embed_set_active (true);
key.type = TERMO_TYPE_FOCUS;
key.code.focused = true;
xui_process_termo_event (&key);
break;
case FocusOut:
if (ev->xfocus.window == g_xui.x11_window
&& ev->xfocus.detail != NotifyInferior
&& ev->xfocus.mode != NotifyGrab
&& ev->xfocus.mode != NotifyUngrab)
x11_embed_set_active (false);
key.type = TERMO_TYPE_FOCUS;
key.code.focused = false;
xui_process_termo_event (&key);
@@ -2191,7 +2580,9 @@ on_x11_pending (void *user_data)
{
if (XNextEvent (g_xui.dpy, &ev.core))
exit_fatal ("XNextEvent returned non-zero");
if (XFilterEvent (&ev.core, None))
if ((!g_xui.x11_embed_plug
|| (ev.core.type != KeyPress && ev.core.type != KeyRelease))
&& XFilterEvent (&ev.core, None))
continue;
on_x11_event (&ev.core);
@@ -2343,8 +2734,8 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
XSetWindowAttributes attrs =
{
.event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask
| KeyPressMask | ButtonPressMask | ButtonReleaseMask
| Button1MotionMask,
| KeyPressMask | KeyReleaseMask
| ButtonPressMask | ButtonReleaseMask | Button1MotionMask,
.bit_gravity = NorthWestGravity,
.background_pixel = default_bg.pixel,
};