Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
PATCH: support OSC52 paste sequence
- X-seq: zsh-workers 53377
- From: Oliver Kiddle <opk@xxxxxxx>
- To: Zsh workers <zsh-workers@xxxxxxx>
- Subject: PATCH: support OSC52 paste sequence
- Date: Mon, 24 Feb 2025 23:30:42 +0100
- Archived-at: <https://zsh.org/workers/53377>
- List-id: <zsh-workers.zsh.org>
We already have support for copying to the system clipboard via the
OSC52 sequence. Some terminals also allow the system clipboard or
primary selection to be retrieved (with \e]52;p;?\e\\). You may find
that that's disabled by default for security/privacy reasons but can be
enabled. Mostly, Shift-Insert is more convenient but I sometimes miss it
when using vi-style editing.
This patch adds support for this with the "* and "+ vim registers. Much
of the patch involves moving some of the existing copy code to the new
termquery.c source file.
Oliver
diff --git a/Doc/Zsh/zle.yo b/Doc/Zsh/zle.yo
index 31eb3f3ba..316d232c3 100644
--- a/Doc/Zsh/zle.yo
+++ b/Doc/Zsh/zle.yo
@@ -2483,9 +2483,8 @@ appended to the buffer instead of overwriting it. When using the tt("_)
buffer, nothing happens. This can be useful for deleting text without
affecting any buffers.
-Updating the system clipboard relies on specific support from the terminal.
-Reading it is not possible so a paste command with tt("*) or tt("+) will do
-nothing.
+Accessing and updating the system clipboard relies on specific support from
+the terminal.
If no buffer is specified for a cut or change command, tt("1) is used, and
the contents of tt("1) to tt("8) are each shifted along one buffer;
diff --git a/Src/Zle/termquery.c b/Src/Zle/termquery.c
index ccd6eaef0..1232a96bf 100644
--- a/Src/Zle/termquery.c
+++ b/Src/Zle/termquery.c
@@ -79,6 +79,7 @@ typedef const unsigned char seqstate_t;
#define TINFO "\xc3"
#define XTID "\xc4"
#define XTVER "\xc5"
+#define CLIP "\xc6"
/* Deterministic finite state automata for parsing terminal response sequences.
* - alternatives decided by the first character (no back-tracking)
@@ -122,6 +123,12 @@ typedef const unsigned char seqstate_t;
OR MATCH("u", KITTY ) \
OR MATCH("c", DA) )))
+#define OSC52_STATES \
+ "\033]52;" EITHER("p" OR "c") ";" RECORD \
+ REPEAT( \
+ CAPTURE EITHER( "\033" MATCH("\\", CLIP) OR MATCH("\007", CLIP) ) \
+ OR WILDCARD )
+
static char *EXTVAR = ".term.extensions";
static char *IDVAR = ".term.id";
static char *VERVAR = ".term.version";
@@ -191,7 +198,7 @@ find_matching(seqstate_t* pos, int direction)
static void
probe_terminal(const char *tquery, seqstate_t *states,
void (*handle_seq) (int seq, int *numbers, int len,
- char *capture, int clen))
+ char *capture, int clen, void *output), void *output)
{
size_t blen = 256, nlen = 16;
char *buf = zhalloc(blen);
@@ -201,11 +208,12 @@ probe_terminal(const char *tquery, seqstate_t *states,
int *num = numbers;
int finish = 0, number = 0;
int ch;
- struct ttyinfo ti;
+ struct ttyinfo ti, torig;
seqstate_t *curstate = states;
gettyinfo(&ti);
+ memcpy(&torig, &ti, sizeof(torig));
#ifdef HAS_TIO
ti.tio.c_lflag &= (~ECHO & ~ICANON & ~ISIG);
ti.tio.c_iflag &= ~ICRNL;
@@ -389,7 +397,7 @@ probe_terminal(const char *tquery, seqstate_t *states,
} else {
if (sequence && !(finish = sequence == SEQ))
handle_seq(sequence & ~SEQ, numbers, num - numbers,
- buf + record, capture - record);
+ buf + record, capture - record, output);
if ((sequence || (action & 1)) &&
(current = start) && /* drop input from sequence */
@@ -409,7 +417,7 @@ probe_terminal(const char *tquery, seqstate_t *states,
if (current > buf)
ungetbytes(buf, current - buf);
- settyinfo(&shttyinfo);
+ settyinfo(&torig);
}
static void
@@ -440,7 +448,8 @@ static const char *queries[] =
{ TQ_BGCOLOR, TQ_FGCOLOR, TQ_KITTYKB, TQ_RGB, TQ_XTVERSION, TQ_DA };
static void
-handle_query(int sequence, int *numbers, int len, char *capture, int clen)
+handle_query(int sequence, int *numbers, int len, char *capture, int clen,
+ UNUSED(void *output))
{
char **feat;
@@ -499,12 +508,85 @@ query_terminal(void) {
((cterm = getsparam("COLORTERM")) &&
(!strcmp(cterm, "truecolor") ||
!strcmp(cterm, "24bit")))))
- handle_query(3, 0, 0, 0, 0);
+ handle_query(3, NULL, 0, NULL, 0, NULL);
else
struncpy(&tqend, (char *) queries[i], /* collate escape sequences */
sizeof(tquery) - (tqend - tquery));
}
if (tqend != tquery) /* unless nothing left after filtering */
- probe_terminal(tquery, states, &handle_query);
+ probe_terminal(tquery, states, &handle_query, NULL);
+}
+
+static char*
+base64_encode(const char *src, size_t len) {
+ static const char* base64_table =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ const unsigned char *end = (unsigned char *)src + len;
+ const unsigned char *in = (unsigned char *)src;
+ char *ret = zhalloc(1 + 4 * ((len + 2) / 3)); /* 4 bytes out for 3 in */
+ char *cur = ret;
+
+ for (; end - in > 0; in += 3, cur += 4) {
+ unsigned int n = *in << 16;
+ cur[3] = end - in > 2 ? base64_table[(n |= in[2]) & 0x3f] : '=';
+ cur[2] = end - in > 1 ? base64_table[((n |= in[1]<<8) >> 6) & 0x3f] : '=';
+ cur[1] = base64_table[(n >> 12) & 0x3f];
+ cur[0] = base64_table[n >> 18];
+ }
+ *cur = '\0';
+
+ return ret;
+}
+
+static char*
+base64_decode(const char *src, size_t len)
+{
+ int i = 0;
+ unsigned int n;
+ char *buf = hcalloc((3 * len) / 4 + 1);
+ char *b = buf;
+ char c;
+
+ while (len && (c = src[i]) != '=') {
+ n = isdigit(c) ? c - '0' + 52 :
+ islower(c) ? c - 'a' + 26 :
+ isupper(c) ? c - 'A' :
+ (c == '+') ? 62 :
+ (c == '/') ? 63 : 0;
+ if (i % 4)
+ *b++ |= n >> (2 * (3 - (i % 4)));
+ if (++i >= len)
+ break;
+ *b = n << (2 * (i % 4));
+ }
+ return buf;
+}
+
+static void
+handle_paste(UNUSED(int sequence), UNUSED(int *numbers), UNUSED(int len),
+ char *capture, int clen, void *output)
+{
+ *(char**) output = base64_decode(capture, clen);
+}
+
+/**/
+char *
+system_clipget(char clip)
+{
+ static seqstate_t osc52[] = OSC52_STATES;
+ char seq[] = "\033]52;.;?\033\\";
+ char *contents;
+ seq[5] = clip;
+ probe_terminal(seq, osc52, &handle_paste, &contents);
+ return contents;
+}
+
+/**/
+void
+system_clipput(char clip, char *content, size_t clen)
+{
+ char *encoded = base64_encode(content, clen);
+ fprintf(shout, "\033]52;%c;%s\a", clip, encoded);
}
diff --git a/Src/Zle/zle_misc.c b/Src/Zle/zle_misc.c
index e17a08d53..a2d07ee03 100644
--- a/Src/Zle/zle_misc.c
+++ b/Src/Zle/zle_misc.c
@@ -554,7 +554,8 @@ yank(UNUSED(char **args))
}
/* position: 0 is before, 1 after, 2 split the line */
-static void pastebuf(Cutbuffer buf, int mult, int position)
+static void
+pastebuf(Cutbuffer buf, int mult, int position)
{
int cc;
if (buf->flags & CUTBUFFER_LINE) {
@@ -613,7 +614,20 @@ viputbefore(UNUSED(char **args))
return 1;
if (zmod.flags & MOD_NULL)
return 0;
- if (zmod.flags & MOD_VIBUF)
+ if (zmod.flags & MOD_OSSEL) {
+ struct cutbuffer kbuf;
+ int x;
+ char *pbuf = system_clipget(zmod.flags & MOD_CLIP ? 'c' : 'p');
+ if (!pbuf || !*pbuf)
+ return 1;
+ kbuf.buf = stringaszleline(pbuf, 0, &x, NULL, NULL);
+ kbuf.len = x;
+ kbuf.flags = 0;
+ kct = -1;
+ yankcs = zlecs;
+ pastebuf(&kbuf, n, 0);
+ return 0;
+ } else if (zmod.flags & MOD_VIBUF)
kctbuf = &vibuf[zmod.vibuf];
else
kctbuf = &cutbuf;
@@ -636,7 +650,20 @@ viputafter(UNUSED(char **args))
return 1;
if (zmod.flags & MOD_NULL)
return 0;
- if (zmod.flags & MOD_VIBUF)
+ if (zmod.flags & MOD_OSSEL) {
+ struct cutbuffer kbuf;
+ int x;
+ char *pbuf = system_clipget(zmod.flags & MOD_CLIP ? 'c' : 'p');
+ if (!pbuf || !*pbuf)
+ return 1;
+ kbuf.buf = stringaszleline(pbuf, 0, &x, NULL, NULL);
+ kbuf.len = x;
+ kbuf.flags = 0;
+ kct = -1;
+ yankcs = zlecs;
+ pastebuf(&kbuf, n, 1);
+ return 0;
+ } else if (zmod.flags & MOD_VIBUF)
kctbuf = &vibuf[zmod.vibuf];
else
kctbuf = &cutbuf;
@@ -661,15 +688,25 @@ putreplaceselection(UNUSED(char **args))
startvichange(-1);
if (n < 0 || zmod.flags & MOD_NULL)
return 1;
- putbuf = (zmod.flags & MOD_VIBUF) ? &vibuf[zmod.vibuf] : &cutbuf;
- if (!putbuf->buf)
- return 1;
- memcpy(&prevbuf, putbuf, sizeof(prevbuf));
+ if (zmod.flags & MOD_OSSEL) {
+ int x;
+ char *pbuf = system_clipget(zmod.flags & MOD_CLIP ? 'c' : 'p');
+ if (!pbuf || !*pbuf)
+ return 1;
+ prevbuf.buf = stringaszleline(pbuf, 0, &x, NULL, NULL);
+ prevbuf.len = x;
+ prevbuf.flags = 0;
+ } else {
+ putbuf = (zmod.flags & MOD_VIBUF) ? &vibuf[zmod.vibuf] : &cutbuf;
+ if (!putbuf->buf)
+ return 1;
+ memcpy(&prevbuf, putbuf, sizeof(prevbuf));
- /* if "9 was specified, prevent killregion from freeing it */
- if (zmod.vibuf == 35) {
- putbuf->buf = 0;
- clear = 1;
+ /* if "9 was specified, prevent killregion from freeing it */
+ if (zmod.vibuf == 35) {
+ putbuf->buf = 0;
+ clear = 1;
+ }
}
zmod.flags = 0; /* flags apply to paste not kill */
diff --git a/Src/Zle/zle_utils.c b/Src/Zle/zle_utils.c
index e2b86e863..6e2456b1f 100644
--- a/Src/Zle/zle_utils.c
+++ b/Src/Zle/zle_utils.c
@@ -937,28 +937,6 @@ cut(int i, int ct, int flags)
cuttext(zleline + i, ct, flags);
}
-static char*
-base64_encode(const char *src, size_t len) {
- static const char* base64_table =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-
- const unsigned char *end = (unsigned char *)src + len;
- const unsigned char *in = (unsigned char *)src;
- char *ret = zhalloc(1 + 4 * ((len + 2) / 3)); /* 4 bytes out for 3 in */
- char *cur = ret;
-
- for (; end - in > 0; in += 3, cur += 4) {
- unsigned int n = *in << 16;
- cur[3] = end - in > 2 ? base64_table[(n |= in[2]) & 0x3f] : '=';
- cur[2] = end - in > 1 ? base64_table[((n |= in[1]<<8) >> 6) & 0x3f] : '=';
- cur[1] = base64_table[(n >> 12) & 0x3f];
- cur[0] = base64_table[n >> 18];
- }
- *cur = '\0';
-
- return ret;
-}
-
/*
* As cut, but explicitly supply the text together with its length.
*/
@@ -975,10 +953,7 @@ cuttext(ZLE_STRING_T line, int ct, int flags)
int cutll;
char *mbcut = zlelineasstring(line, ct, 0, &cutll, NULL, 1);
unmetafy(mbcut, &cutll);
- mbcut = base64_encode(mbcut, cutll);
-
- fprintf(shout, "\033]52;%c;%s\a", zmod.flags & MOD_CLIP ? 'c' : 'p',
- mbcut);
+ system_clipput(zmod.flags & MOD_CLIP ? 'c' : 'p', mbcut, cutll);
} else if (zmod.flags & MOD_VIBUF) {
struct cutbuffer *b = &vibuf[zmod.vibuf];
diff --git a/Test/X06termquery.ztst b/Test/X06termquery.ztst
index 52f39ddf1..cce4e626f 100644
--- a/Test/X06termquery.ztst
+++ b/Test/X06termquery.ztst
@@ -84,6 +84,20 @@
termresp ''
0:no response - timeout
+# Following three vi-put tests also cover 0, 1 and 2 `=' padding
+# characters in the base64 decoding.
+ termresp $'\e[?0;cbindkey -v\necho \e"*p\e]52;p;YWZ0ZXI=\aa\n'
+0:paste after from clipboard
+>after
+
+ termresp $'\e[?0;cbindkey -v\necho X\e"*P\e]52;p;YmVmb3Jl\aa\n'
+0:paste before from clipboard
+>beforeX
+
+ termresp $'\e[?0;cbindkey -v\necho X\ev"*p\e]52;p;cmVwbGFjZQ==\aa\n'
+0:paste over from clipboard
+>replace
+
%clean
zmodload -ui zsh/zpty
Messages sorted by:
Reverse Date,
Date,
Thread,
Author