转载地址:自带梯子 https://www.uninformativ.de/blog/postings/2017-04-02/0/POSTING-en.html
X11:“剪贴板”如何工作?
如果您在切换到运行X11的东西之前使用过其他操作系统,您会注意到有多个剪贴板:
有时,您可以使用鼠标选择一些文本,切换到另一个窗口,然后点击鼠标中键来粘贴文本。
有时,您可以选择文本,然后点击一些热键,例如Ctrl + C,切换到另一个窗口,点击另一个热键,例如Ctrl + V,然后粘贴所述文本。
有时,你可以做到这两点。
这两个剪贴板通常不会干扰。您可以使用“中间鼠标剪贴板”保留“Ctrl + C剪贴板”的内容,以复制和粘贴其他内容。
这是如何运作的?有多个剪贴板吗?那里有多少?所有X11客户端都支持所有形式的剪贴板吗?
以下是ICCCM关于此主题的适当部分。
选择作为IPC的一种形式
首先,在X11土地上,“剪贴板”被称为“选择”。
是的,有多个选择,它们都是独立工作的。实际上,您可以根据需要使用尽可能多的选择。从理论上讲,就是这样。使用选择时,可以使不同的客户端相互通信。这意味着那些客户必须同意使用哪些选项。你不能只是创造自己的选择,然后期望Firefox与它兼容。
从非常高的高度看它,它是这样的:
(1)意味着每个客户可以随时要求拥有任何选择权。它只通知X服务器 - 没有数据传输。这是一个重要的事情要理解。X服务器只不过是一个经纪人。它记录了哪个客户拥有哪个选择。
在(2)中,另一个客户端要求X服务器向其发送选择“FOO”的内容。X服务器只是将该请求中继到该选择的当前所有者。然后,客户端A负责将数据实际传输到客户端B.
如何识别选择?
上面,我只称它为“选择FOO”,这意味着它是一个相当随意的标识符,你可以选择。如果您之前使用过X11,那么您就不会感到惊讶:选择由原子识别。
快速回顾:原子是一种识别X11中的东西的方法,它们基本上是字符串。在内部,为每个原子分配一个数字,但你很少需要问X服务器,“原子序号42的名称是什么?”
有三个“标准”选择名称:
PRIMARY:“中鼠剪贴板”
SECONDARY:这几天几乎没用过
CLIPBOARD:“Ctrl + C剪贴板”
“标准”是指它们由ICCCM 2.6.1规定。是的,令人困惑的是其中一个选项被命名为“剪贴板”。
程序1:查询选择所有者
知道我们现在知道什么,我们可以要求X服务器告诉我们谁拥有哪个选择。这是xowners.c:
#include <stdio.h>
#include <X11/Xlib.h>
int
main()
{
Display *dpy;
Window owner;
Atom sel;
char *selections[] = { "PRIMARY", "SECONDARY", "CLIPBOARD", "FOOBAR" };
size_t i;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
for (i = 0; i < sizeof selections / sizeof selections[0]; i++)
{
sel = XInternAtom(dpy, selections[i], False);
owner = XGetSelectionOwner(dpy, sel);
printf("Owner of '%s': 0x%lX\n", selections[i], owner);
}
return 0;
}
编译此程序(以及以下所有类似的方式):
cc -Wall -Wextra -o xowners xowners.c -lX11
FOOBAR是一个非标准的选择名称。使用它是完全有效的,但不要指望它适用于所有客户端。:-)
如您所见,程序打印了Windows的ID:
$ ./xowners
Owner of 'PRIMARY': 0x60080F
Owner of 'SECONDARY': 0x0
Owner of 'CLIPBOARD': 0x1E00024
Owner of 'FOOBAR': 0x0
Windows是客户端之间另一种基本的通信形式,这意味着它们不一定像“像素盒”那样工作。未映射的窗口可以很好地存在于X11会话中(并且通常有许多窗口)。
我们可以使用该xwininfo工具找到有关这两个窗口的更多信息:
$ xwininfo -id 0x60080F | grep '^xwininfo'
xwininfo: Window id: 0x60080f "xiate"
$ xwininfo -id 0x1E00024 | grep '^xwininfo'
xwininfo: Window id: 0x1e00024 "lariza"
啊哈,原来xiate持有的PRIMARY选择,同时lariza拥有 CLIPBOARD。
让我们看看其中一个命令的完整输出:
$ xwininfo -id 0x60080F
xwininfo: Window id: 0x60080f "xiate"
Absolute upper-left X: -100
Absolute upper-left Y: -100
Relative upper-left X: -100
Relative upper-left Y: -100
Width: 10
Height: 10
Depth: 0
...
Map State: IsUnMapped
...
实际上,这是一个未映射的窗口。客户经常这样做。他们创建一个窗口,其唯一目的是管理选择。客户可以 使用他们的可见窗口,但这是有问题的。有时,可见窗口是短暂的,当窗口死亡时,选择的所有权就会丢失。
内容类型和转换
到现在为止还挺好。这么简单。
一旦你意识到某些客户可能会使用剪贴板来处理文本,其他人可能会将它用于图像,有些可能会将其用于音频数据,而其他一些客户可能会将其用于您从未听说过的某种形式的数据,事情就开始变得复杂。
而再有,你可以以不同的形式提供相同的数据的情况。为了说明这一点,只需在Web浏览器中选择一些文本即可。复制并粘贴到Vim中。你会得到纯文本。但是,如果将相同的选择粘贴到LibreOffice Writer等程序中,您不仅会获得文本,还会获得文本属性,例如“这是粗体,这是代码块”,等等。
回想一下上面的图表。步骤2说:客户端B告诉X服务器将选择“FOO”写入“BAR”。(我们还没有涵盖“BAR”是什么,但我们很快就会到达那里。)实际上,它更像是这样:“将选择'FOO' 写成 'BAR' 作为内容类型'BAZ'。”换句话说,客户端B可以请求选择“FOO”的当前内容作为文本。或者作为图像。或者作为别的东西。
这就是为什么库调用“获取”选择的当前内容XConvertSelection()而不是XGetSelection()。
程序2:获取剪贴板为UTF-8
这是“客户B”的一个例子:
#include <stdio.h>
#include <X11/Xlib.h>
void
show_utf8_prop(Display *dpy, Window w, Atom p)
{
Atom da, incr, type;
int di;
unsigned long size, dul;
unsigned char *prop_ret = NULL;
/* Dummy call to get type and size. */
XGetWindowProperty(dpy, w, p, 0, 0, False, AnyPropertyType,
&type, &di, &dul, &size, &prop_ret);
XFree(prop_ret);
incr = XInternAtom(dpy, "INCR", False);
if (type == incr)
{
printf("Data too large and INCR mechanism not implemented\n");
return;
}
/* Read the data in one go. */
printf("Property size: %lu\n", size);
XGetWindowProperty(dpy, w, p, 0, size, False, AnyPropertyType,
&da, &di, &dul, &dul, &prop_ret);
printf("%s", prop_ret);
fflush(stdout);
XFree(prop_ret);
/* Signal the selection owner that we have successfully read the
* data. */
XDeleteProperty(dpy, w, p);
}
int
main()
{
Display *dpy;
Window owner, target_window, root;
int screen;
Atom sel, target_property, utf8;
XEvent ev;
XSelectionEvent *sev;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
sel = XInternAtom(dpy, "CLIPBOARD", False);
utf8 = XInternAtom(dpy, "UTF8_STRING", False);
owner = XGetSelectionOwner(dpy, sel);
if (owner == None)
{
printf("'CLIPBOARD' has no owner\n");
return 1;
}
printf("0x%lX\n", owner);
/* The selection owner will store the data in a property on this
* window: */
target_window = XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0);
/* That's the property used by the owner. Note that it's completely
* arbitrary. */
target_property = XInternAtom(dpy, "PENGUIN", False);
/* Request conversion to UTF-8. Not all owners will be able to
* fulfill that request. */
XConvertSelection(dpy, sel, utf8, target_property, target_window,
CurrentTime);
for (;;)
{
XNextEvent(dpy, &ev);
switch (ev.type)
{
case SelectionNotify:
sev = (XSelectionEvent*)&ev.xselection;
if (sev->property == None)
{
printf("Conversion could not be performed.\n");
return 1;
}
else
{
show_utf8_prop(dpy, target_window, target_property);
return 0;
}
break;
}
}
}
这比你预期的代码多吗?对。但请忍受我。我们将逐步完成它。
首先,让我们揭开“BAR”的含义。你看到上面的代码创建了一个target_window和一个原子target_property。这两件事一起是“BAR”。当客户端A将选择的内容发送给客户端B时,它通过将数据写入窗口上的属性来实现。这实际上是两个X11客户端通过X服务器传递任意数据的唯一方式 。
请记住,X11是网络透明的。客户端A和B无需在同一主机上运行。他们甚至不需要使用相同的网络协议。一个可能使用TCP / IP,另一个可能使用......无论如何。ICCCM使用DECnet作为一个例子,可能今天没人用了。因此,它们不能直接通信,而只能通过X服务器进行通信。
好的。我们的目标“BAR”是一个窗口和一个属性。
我们还需要一种内容类型。在这里,我用过UTF8_STRING。您将无法在ICCCM中找到此原子名称。ICCCM首次发布时,UTF-8甚至不存在。不过,较新的客户支持它。
然后我们要求X服务器“执行”转换: XConvertSelection()。现在仔细查看本文顶部的第一个图表。没有立即回应 XConvertSelection()。X服务器必须首先将该请求中继到客户端A,前提是现在甚至还有一个选择所有者。然后,在将来的某个时刻,客户A决定做它的工作 - 或许不是。这意味着我们只能等待某个X事件发生。这就是代码底部的循环所针对的。该事件 SelectionNotify告诉我们转换已经发生或失败。然后我们可以继续在我们自己的窗口阅读该物业; 客户端A应该已将其数据写入该属性。
有些事情需要注意:
客户端A可能无法提供其数据。它可能已经崩溃了。管他呢。客户端B不得阻止并等待数据传输完成。
客户端A可能无法转换数据。例如,当您要求GIMP从剪贴板向您提供UTF-8时,GIMP实际存储了图像数据时会发生这种情况。
调用XDeleteProperty()告诉客户端A我们已成功读取数据。
它不是必需的要求转化前,要求选择的当前所有者。我只是这样检查现在是否有选择所有者。(如果你不这样做,你只会得到“转换失败”。)
计划3:拥有一个选择
这是另一个方向。CLIPBOARD如果要求输入类型,则声明所有权并提供数据的客户 UTF8_STRING。所以,这是客户A:
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <X11/Xlib.h>
void
send_no(Display *dpy, XSelectionRequestEvent *sev)
{
XSelectionEvent ssev;
char *an;
an = XGetAtomName(dpy, sev->target);
printf("Denying request of type '%s'\n", an);
if (an)
XFree(an);
/* All of these should match the values of the request. */
ssev.type = SelectionNotify;
ssev.requestor = sev->requestor;
ssev.selection = sev->selection;
ssev.target = sev->target;
ssev.property = None; /* signifies "nope" */
ssev.time = sev->time;
XSendEvent(dpy, sev->requestor, True, NoEventMask, (XEvent *)&ssev);
}
void
send_utf8(Display *dpy, XSelectionRequestEvent *sev, Atom utf8)
{
XSelectionEvent ssev;
time_t now_tm;
char *now, *an;
now_tm = time(NULL);
now = ctime(&now_tm);
an = XGetAtomName(dpy, sev->property);
printf("Sending data to window 0x%lx, property '%s'\n", sev->requestor, an);
if (an)
XFree(an);
XChangeProperty(dpy, sev->requestor, sev->property, utf8, 8, PropModeReplace,
(unsigned char *)now, strlen(now));
ssev.type = SelectionNotify;
ssev.requestor = sev->requestor;
ssev.selection = sev->selection;
ssev.target = sev->target;
ssev.property = sev->property;
ssev.time = sev->time;
XSendEvent(dpy, sev->requestor, True, NoEventMask, (XEvent *)&ssev);
}
int
main()
{
Display *dpy;
Window owner, root;
int screen;
Atom sel, utf8;
XEvent ev;
XSelectionRequestEvent *sev;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
/* We need a window to receive messages from other clients. */
owner = XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0);
sel = XInternAtom(dpy, "CLIPBOARD", False);
utf8 = XInternAtom(dpy, "UTF8_STRING", False);
/* Claim ownership of the clipboard. */
XSetSelectionOwner(dpy, sel, owner, CurrentTime);
for (;;)
{
XNextEvent(dpy, &ev);
switch (ev.type)
{
case SelectionClear:
printf("Lost selection ownership\n");
return 1;
break;
case SelectionRequest:
sev = (XSelectionRequestEvent*)&ev.xselectionrequest;
printf("Requestor: 0x%lx\n", sev->requestor);
/* Property is set to None by "obsolete" clients. */
if (sev->target != utf8 || sev->property == None)
send_no(dpy, sev);
else
send_utf8(dpy, sev, utf8);
break;
}
}
}
它创建了一个不可见的窗口,然后声称拥有CLIPBOARD。正如您所看到的,不是“客户端”拥有选择,而是窗口。
程序然后等待事件。SelectionClear很简单:其他一些客户声称拥有剪贴板的所有权。是的,这可能随时发生。
SelectionRequest由X服务器发送给客户端A. 这是X服务器由于XConvertSelection()客户端B 的调用而生成的事件。我们现在只需检查是否target存在UTF8_STRING。如果不是,我们拒绝该请求。但如果是,我们调用 XChangeProperty()改变给定目标窗口上的给定属性。完成后,我们生成一个SelectionNotify事件并将其发送给客户B.
此客户端将当前日期和时间发送给请求者。我这样做是为了进一步说明选择如何不在X服务器中存储数据。仅当另一个客户端请求数据时才转换(并可能生成)数据。
计划4:内容类型 TARGETS
有一些特殊的内容类型。您可以要求选择的所有者将选择转换为类型TARGETS。这听起来有点奇怪,但很简单。客户端A不会响应实际数据,而是使用原子列表。每个原子都是当前数据的有效目标。
#include <stdio.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
void
show_targets(Display *dpy, Window w, Atom p)
{
Atom type, *targets;
int di;
unsigned long i, nitems, dul;
unsigned char *prop_ret = NULL;
char *an = NULL;
/* Read the first 1024 atoms from this list of atoms. We don't
* expect the selection owner to be able to convert to more than
* 1024 different targets. :-) */
XGetWindowProperty(dpy, w, p, 0, 1024 * sizeof (Atom), False, XA_ATOM,
&type, &di, &nitems, &dul, &prop_ret);
printf("Targets:\n");
targets = (Atom *)prop_ret;
for (i = 0; i < nitems; i++)
{
an = XGetAtomName(dpy, targets[i]);
printf(" '%s'\n", an);
if (an)
XFree(an);
}
XFree(prop_ret);
XDeleteProperty(dpy, w, p);
}
int
main()
{
Display *dpy;
Window target_window, root;
int screen;
Atom sel, targets, target_property;
XEvent ev;
XSelectionEvent *sev;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
sel = XInternAtom(dpy, "CLIPBOARD", False);
targets = XInternAtom(dpy, "TARGETS", False);
target_property = XInternAtom(dpy, "PENGUIN", False);
target_window = XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0);
XConvertSelection(dpy, sel, targets, target_property, target_window,
CurrentTime);
for (;;)
{
XNextEvent(dpy, &ev);
switch (ev.type)
{
case SelectionNotify:
sev = (XSelectionEvent*)&ev.xselection;
if (sev->property == None)
{
printf("Conversion could not be performed.\n");
return 1;
}
else
{
show_targets(dpy, target_window, target_property);
return 0;
}
break;
}
}
}
当一个典型的GTK客户端目前拥有一个简单的文本选择时运行它会显示一些有趣的东西
$ ./xtargets
Targets:
'TIMESTAMP'
'TARGETS'
'MULTIPLE'
'SAVE_TARGETS'
'UTF8_STRING'
'COMPOUND_TEXT'
'TEXT'
'STRING'
'text/plain;charset=utf-8'
'text/plain'
X11是旧的,并且存在许多关于如何指定数据类型的约定。其中一些是遗留的,一些是模糊的,许多甚至没有被ICCCM提及。MIME类型今天很好,但ICCCM不以任何方式讨论MIME类型。
这感觉有点乱,是的。与30年前的当今客户和客户兼容并不容易。
使用处理二进制数据 xclip
我一直想知道为什么我无法使用粘贴图像xclip。应该很简单:xclip -o >foo.img。好吧,不。知道我现在所知道的,终于很简单了。:-)
首先,使用像GIMP这样的工具复制图像。
xclip可以查询TARGETS:
$ xclip -o -target TARGETS -selection clipboard
TIMESTAMP
TARGETS
MULTIPLE
SAVE_TARGETS
image/png
image/tiff
image/x-icon
image/x-ico
image/x-win-bitmap
image/vnd.microsoft.icon
application/ico
image/ico
image/icon
text/ico
image/bmp
image/x-bmp
image/x-MS-bmp
image/jpeg
选择你喜欢的东西。然后询问数据:
$ xclip -o -target image/png -selection clipboard >foo.png
$ file foo.png
foo.png: PNG image data, 373 x 309, 8-bit/color RGBA, non-interlaced
没有大碍。使用xclip以复制图像数据的作品以同样的方式,只是用指定的MIME类型-t。
大量数据
您可能已经注意到,如果涉及的内容被调用,程序2将中止INCR。这是X11选择世界中众多黑客中的一个。
Windows上的属性只能容纳有限数量的数据,因为它们存在于X服务器的内存中。如果您想通过使用选择传输几兆字节,您仍然可以这样做。您只需要对数据进行分块,客户端B必须以块的形式读取数据。通常,每个块的大小约为256 kB。不是那么多,但在大多数情况下都足够了。但是,它使客户端变得更加复杂,因为每个客户端都必须实现该组块机制。
剪贴板管理员
在日常工作中,您可能已经注意到这一点:打开一个窗口,选择一些文本,按Ctrl + C,然后关闭窗口。怎么了?选择丢失了。当然,拥有选择的客户端窗口已经消失。这与其他操作系统不同。即使所有操作系统都像这样工作,它仍然会很烦人。
X11中没有针对此问题的“干净”解决方案。相反,ICCCM建议使用剪贴板管理器。他们的工作方式如下:
剪贴板管理器应声明选择的所有权。
一旦失去所有权,它将:
向当前所有者询问内容。
提供内容本身。
重新拥有所有权。
这感觉就像涉及很多种族条件。当客户端不支持TARGETS目标时,它也会中断。是的,ICCCM要求支持这一目标,因此它“应该”起作用。
摘要
我认为理解X服务器只是一个经纪人是很重要的。客户端(通过服务器)相互交谈,交换内容。服务器中没有“内部”剪贴板。数据即时转换。您可以根据需要选择多个选项,但并非所有客户端都支持所有选择。
最后要注意的是:乍一看,X11中的选择看起来很简单。但我担心它们几乎和时区一样复杂。即使是“标准”实用程序xclip也不是严格符合ICCCM标准,并且偶尔包含“FIXME”。有许多种族条件和许多角落案件。
TL; 博士:如果可能的话,使用图书馆。
附录,2019-07-28
2017年的原始代码包含了一些调用XSelectInput()。我想告诉X服务器我的窗口要接收像这样的事件 SelectionNotify。但这样做是错误的。这些事件根本没有掩码,您无需为它们选择。
谢谢,乌尔里希!