From 172ceffa9ec6722076074348c21d7069212a112e Mon Sep 17 00:00:00 2001
From: Přemysl Eric Janouch
Date: Thu, 25 Aug 2022 06:06:19 +0200
Subject: X11: support copying text to CLIPBOARD
Use the right mouse button.
---
nncmpp.c | 217 ++++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 167 insertions(+), 50 deletions(-)
diff --git a/nncmpp.c b/nncmpp.c
index 84192ee..836f2ba 100644
--- a/nncmpp.c
+++ b/nncmpp.c
@@ -109,6 +109,7 @@ enum
// Elementary port of the TUI to X11.
#ifdef WITH_X11
+#include
#include
#include
#include
@@ -1152,6 +1153,9 @@ struct widget;
/// Draw a widget on the window
typedef void (*widget_render_fn) (struct widget *self);
+/// Extract the contents of container widgets
+typedef struct widget *(*widget_sublayout_fn) (struct widget *self);
+
/// A minimal abstraction appropriate for both TUI and GUI widgets.
/// Units for the widget's region are frontend-specific.
/// Having this as a linked list simplifies layouting and memory management.
@@ -1165,6 +1169,7 @@ struct widget
int height; ///< Height, initialized by UI methods
widget_render_fn on_render; ///< Render callback
+ widget_sublayout_fn on_sublayout; ///< Optional sublayout callback
chtype attrs; ///< Rendition, in Curses terms
short id; ///< Post-layouting identification
@@ -1362,6 +1367,7 @@ static struct app_context
XftDraw *xft_draw; ///< Xft rendering context
XftFont *xft_regular; ///< Regular font
XftFont *xft_bold; ///< Bold font
+ char *x11_selection; ///< CLIPBOARD selection
XRenderColor x_fg[ATTRIBUTE_COUNT]; ///< Foreground per attribute
XRenderColor x_bg[ATTRIBUTE_COUNT]; ///< Background per attribute
@@ -1736,14 +1742,14 @@ app_invalidate (void)
}
static void
-app_flush_layout (struct layout *l)
+app_flush_layout_to (struct layout *l, int width, struct layout *dest)
{
hard_assert (l != NULL && l->head != NULL);
- widget_redistribute (l->head, g.ui_width);
+ widget_redistribute (l->head, width);
- struct widget *last = g.widgets.tail;
+ struct widget *last = dest->tail;
if (!last)
- g.widgets = *l;
+ *dest = *l;
else
{
// Assuming there is no unclaimed vertical space.
@@ -1752,10 +1758,16 @@ app_flush_layout (struct layout *l)
last->next = l->head;
l->head->prev = last;
- g.widgets.tail = l->tail;
+ dest->tail = l->tail;
}
}
+static void
+app_flush_layout (struct layout *l)
+{
+ app_flush_layout_to (l, g.ui_width, &g.widgets);
+}
+
static struct widget *
app_push (struct layout *l, struct widget *w)
{
@@ -2041,7 +2053,7 @@ app_compute_scrollbar (struct tab *tab, long visible, long s)
return (struct scrollbar) { length, offset };
}
-static struct widget *
+static struct layout
app_layout_row (struct tab *tab, int item_index)
{
int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
@@ -2075,6 +2087,30 @@ app_layout_row (struct tab *tab, int item_index)
else
*attrs |= row_attrs;
}
+ return l;
+}
+
+// XXX: This isn't a very clean design, in that part of layouting
+// is done during the rendering stage.
+static struct widget *
+app_sublayout_list (struct widget *list)
+{
+ struct tab *tab = g.active_tab;
+ int to_show = MIN ((int) tab->item_count - tab->item_top,
+ list->height / g.ui_vunit);
+
+ struct layout l = {};
+ for (int row = 0; row < to_show; row++)
+ {
+ int item_index = tab->item_top + row;
+ struct layout subl = app_layout_row (tab, item_index);
+ app_flush_layout_to (&subl, list->width, &l);
+ }
+ LIST_FOR_EACH (struct widget, w, l.head)
+ {
+ w->x += list->x;
+ w->y += list->y;
+ }
return l.head;
}
@@ -5124,28 +5160,10 @@ tui_make_scrollbar (chtype attrs)
static void
tui_render_list (struct widget *self)
{
- struct tab *tab = g.active_tab;
- int to_show =
- MIN (app_visible_items (), (int) tab->item_count - tab->item_top);
- for (int row = 0; row < to_show; row++)
+ LIST_FOR_EACH (struct widget, w, self->on_sublayout (self))
{
- int item_index = tab->item_top + row;
- struct widget *head = app_layout_row (tab, item_index);
- widget_redistribute (head, self->width);
-
- int x = self->x;
- int y = self->y + row * g.ui_vunit;
- LIST_FOR_EACH (struct widget, w, head)
- {
- w->x += x;
- w->y += y;
- }
-
- LIST_FOR_EACH (struct widget, w, head)
- {
- w->on_render (w);
- free (w);
- }
+ w->on_render (w);
+ free (w);
}
}
@@ -5156,6 +5174,7 @@ tui_make_list (void)
w->width = -1;
w->height = g.active_tab->item_count;
w->on_render = tui_render_list;
+ w->on_sublayout = app_sublayout_list;
return w;
}
@@ -5749,28 +5768,10 @@ x11_render_list (struct widget *self)
{
x11_render_padding (self);
- struct tab *tab = g.active_tab;
- int to_show =
- MIN (app_visible_items (), (int) tab->item_count - tab->item_top);
- for (int row = 0; row < to_show; row++)
+ LIST_FOR_EACH (struct widget, w, self->on_sublayout (self))
{
- int item_index = tab->item_top + row;
- struct widget *head = app_layout_row (tab, item_index);
- widget_redistribute (head, self->width);
-
- int x = self->x;
- int y = self->y + row * g.ui_vunit;
- LIST_FOR_EACH (struct widget, w, head)
- {
- w->x += x;
- w->y += y;
- }
-
- LIST_FOR_EACH (struct widget, w, head)
- {
- w->on_render (w);
- free (w);
- }
+ w->on_render (w);
+ free (w);
}
}
@@ -5779,6 +5780,7 @@ x11_make_list (void)
{
struct widget *w = xcalloc (1, sizeof *w + 1);
w->on_render = x11_render_list;
+ w->on_sublayout = app_sublayout_list;
return w;
}
@@ -5862,6 +5864,7 @@ x11_destroy (void)
XftDrawDestroy (g.xft_draw);
XftFontClose (g.dpy, g.xft_regular);
XftFontClose (g.dpy, g.xft_bold);
+ cstr_set (&g.x11_selection, NULL);
poller_fd_reset (&g.x11_event);
XCloseDisplay (g.dpy);
@@ -6035,6 +6038,57 @@ x11_init_pixmap (void)
= XRenderCreatePicture (g.dpy, g.x11_pixmap, format, 0, NULL);
}
+static char *
+x11_find_text (struct widget *list, int x, int y)
+{
+ struct widget *target = NULL;
+ LIST_FOR_EACH (struct widget, w, list)
+ if (x >= w->x && x < w->x + w->width
+ && y >= w->y && y < w->y + w->height)
+ target = w;
+ if (!target)
+ return NULL;
+
+ if (target->on_sublayout)
+ {
+ struct widget *sublist = target->on_sublayout (target);
+ char *result = x11_find_text (sublist, x, y);
+ LIST_FOR_EACH (struct widget, w, sublist)
+ free (w);
+ if (result)
+ return result;
+ }
+ return xstrdup (target->text);
+}
+
+// TODO: OSC 52 exists for terminals, so make it possible to enable that there.
+static bool
+x11_process_press (int x, int y, int button, int modifiers)
+{
+ if (button != Button3)
+ goto out;
+
+ char *text = x11_find_text (g.widgets.head, x, y);
+ if (!text || !*(cstr_strip_in_place (text, " \t")))
+ {
+ free (text);
+ goto out;
+ }
+
+ cstr_set (&g.x11_selection, text);
+ XSetSelectionOwner (g.dpy, XInternAtom (g.dpy, "CLIPBOARD", False),
+ g.x11_window, CurrentTime);
+
+ cstr_set (&g.message,
+ xstrdup_printf ("Text copied to clipboard: %s", g.x11_selection));
+ poller_timer_set (&g.message_timer, 5000);
+ app_invalidate ();
+ return true;
+
+out:
+ return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers);
+}
+
static int
x11_state_to_modifiers (unsigned int state)
{
@@ -6079,14 +6133,66 @@ on_x11_input_event (XEvent *ev)
last_press_event = *ev;
if (ev->type == ButtonPress)
- return app_process_mouse
- (TERMO_MOUSE_PRESS, x, y, button, modifiers);
+ return x11_process_press (x, y, button, modifiers);
if (ev->type == ButtonRelease)
return app_process_mouse
(TERMO_MOUSE_RELEASE, x, y, button, modifiers);
return false;
}
+static void
+on_x11_selection_request (XSelectionRequestEvent *ev)
+{
+ Atom xa_targets = XInternAtom (g.dpy, "TARGETS", False);
+ Atom xa_compound_text = XInternAtom (g.dpy, "COMPOUND_TEXT", False);
+ Atom xa_utf8 = XInternAtom (g.dpy, "UTF8_STRING", False);
+ Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 };
+
+ bool ok = false;
+ Atom property = ev->property ? ev->property : ev->target;
+ if (!g.x11_selection)
+ goto out;
+
+ XICCEncodingStyle style = 0;
+ if ((ok = ev->target == xa_targets))
+ {
+ XChangeProperty (g.dpy, ev->requestor, property,
+ XA_ATOM, 32, PropModeReplace,
+ (const unsigned char *) targets, N_ELEMENTS (targets));
+ goto out;
+ }
+ else if (ev->target == XA_STRING)
+ style = XStringStyle;
+ else if (ev->target == xa_compound_text)
+ style = XCompoundTextStyle;
+ else if (ev->target == xa_utf8)
+ style = XUTF8StringStyle;
+ else
+ goto out;
+
+ // XXX: We let it crash us with BadLength, but we may, e.g., use INCR.
+ XTextProperty text = {};
+ if ((ok = !Xutf8TextListToTextProperty
+ (g.dpy, &g.x11_selection, 1, style, &text)))
+ {
+ XChangeProperty (g.dpy, ev->requestor, property,
+ text.encoding, text.format, PropModeReplace,
+ text.value, text.nitems);
+ }
+ XFree (text.value);
+
+out:
+ XEvent response = {};
+ response.xselection.type = SelectionNotify;
+ // XXX: We should check it against the event causing XSetSelectionOwner().
+ response.xselection.time = ev->time;
+ response.xselection.requestor = ev->requestor;
+ response.xselection.selection = ev->selection;
+ response.xselection.target = ev->target;
+ response.xselection.property = ok ? property : None;
+ XSendEvent (g.dpy, ev->requestor, False, 0, &response);
+}
+
static void
on_x11_event (XEvent *ev)
{
@@ -6115,6 +6221,12 @@ on_x11_event (XEvent *ev)
XftDrawChange (g.xft_draw, g.x11_pixmap);
app_invalidate ();
break;
+ case SelectionRequest:
+ on_x11_selection_request (&ev->xselectionrequest);
+ break;
+ case SelectionClear:
+ cstr_set (&g.x11_selection, NULL);
+ break;
case UnmapNotify:
app_quit ();
break;
@@ -6173,6 +6285,11 @@ on_x11_error (Display *dpy, XErrorEvent *event)
|| (event->error_code == BadDrawable && event->resourceid == g.x11_window))
return app_quit (), 0;
+ // XXX: The simplest possible way of discarding selection management errors.
+ // XCB would be a small win here, but it is a curse at the same time.
+ if (event->error_code == BadWindow && event->resourceid != g.x11_window)
+ return 0;
+
return x11_default_error_handler (dpy, event);
}
--
cgit v1.2.3-70-g09d2