1

Xterm has a feature where if you put in .Xresources

XTerm.vt100.translations: #override \
    Shift Ctrl<Key>V: insert-selection(CLIPBOARD)

then ctrl+shift+V will paste from clipboard.

I notice that when I paste from the clipboard with this shortcut then the pasted text will not trigger a new command and wrapped in "white background":

screenshot of the terminal

After some research I figure out that this is the bracketed paste mode supported by xterm. I want to make a vim key binding that does the bracketed paste into an embedded terminal in vim (opened by :ter for example):

set termwinkey=<c-j>
" then <c-j>"+ pastes from "+ clipboard to the terminal
tnore <c-j>"+p  <esc>[200~<c-j>"+<esc>[201~

This works, unless the command running inside the terminal does not support bracketed paste...

terminal output

From my understanding xterm's ctrl+shift+V (if defined as above) can automatically detect whether the shell inside supports bracketed paste because the shell sends ^[[?2004h to xterm, and if it's not seen then xterm will not send the ^[[200~.

Question: how can the vim key binding above be defined so that it does the same thing as xterm's ctrl+shift+V?

(motivation: I want to paste from vim's @" selection instead, while ctrl+shift+V only allows pasting from @+)


Update: I notice that xterm has a feature to query whether bracketed paste is currently enabled...

Documentation: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html

We have

CSI ? Pm h
          DEC Private Mode Set (DECSET).
            [...]
            Ps = 2 0 0 4  ⇒  Set bracketed paste mode, xterm.


CSI ? Ps $ p
          Request DEC private mode (DECRQM).  For VT300 and up, reply
          DECRPM is
            CSI ? Ps; Pm $ y
          where Ps is the mode number as in DECSET/DECSET, Pm is the
          mode value as in the ANSI DECRQM.
          Two private modes are read-only (i.e., 1 3  and 1 4 ),
          provided only for reporting their values using this control
          sequence.  They correspond to the resources cursorBlink and
          cursorBlinkXOR.

CSI Ps $ p
          Request ANSI mode (DECRQM).  For VT300 and up, reply DECRPM is
            CSI Ps; Pm $ y
          where Ps is the mode number as in SM/RM, and Pm is the mode
          value:
            0 - not recognized
            1 - set
            2 - reset
            3 - permanently set
            4 - permanently reset

In other words, if the application inside prints to the terminal ^[[?2004$p, the terminal will return ^[[?2004;answer$y where answer is one of {1, 2, 3, 4} as described above.

So I try doing something like this:

call writefile(["\e[?2004$p"], "/dev/tty", "b")
for s in "\e[?2004;"
    if getcharstr()!=s
        throw "Error"
    endif
endfor
let response=getcharstr()
for s in "$y"
    if getcharstr()!=s
        throw "Error"
    endif
endfor
echom response

it works, but always return 1 even if it's inside a terminal.

I'm not sure how TTY/PTY works unfortunately, so I don't know what to do next.

(but let's say term_gettty(bufnr()) returns /dev/pts/27, then I can

  • printf '\e[?2004$p' > /dev/pts/27 to get as-if the response typed into the command running inside it, and
  • printf '\e]51;["call", "Tapi_log", ["123"]]\x07' > /dev/pts/27 to call vim terminal API. )
3
  • I'm not entirely sure I follow your question, but try :help xterm-bracketed-paste, and also note that you can copy the contents of the default register @" to one of the system clipboard registers @+, @* to make it pasteable by the system (better: you can yank directly to such registers).
    – D. Ben Knoble
    Commented Jan 3, 2023 at 14:20
  • 1
    @D.BenKnoble Even if I copy @" to @+, I still need to "simulate" a mouse click or ctrl+shift+V etc. to make xterm paste into the vim terminal, which is suboptimal; besides, that will destroy the existing content in @+. (xterm-bracketed-paste only explain how vim should be configured to handle xterm properly, nothing about how to paste things from vim into embedded terminal)
    – user202729
    Commented Jan 3, 2023 at 17:06
  • Is there a reason <C-w>"" doesn't work? Edit: ah, this probably just inserts without any bracketed-pasting…
    – D. Ben Knoble
    Commented Jan 3, 2023 at 18:08

1 Answer 1

1

I figured out a way, although it needs some wrapper script...

First, save this wrapper script as a.py:

#!/usr/bin/env python3

import sys
import os
import pty
import tty
import select
import subprocess

STDIN_FILENO = sys.stdin.fileno()
STDOUT_FILENO = sys.stdout.fileno()
STDERR_FILENO = sys.stderr.fileno()

def _writen(fd, data):
    while data:
        n = os.write(fd, data)
        data = data[n:]

def main_loop(master_fd):
    fds = [master_fd, STDIN_FILENO]

    data_processed = bytearray()

    while fds:
        rfds, _, _ = select.select(fds, [], [])
        if master_fd in rfds:
            data = os.read(master_fd, 1024)
            if not data:
                fds.remove(master_fd)
            else:
                last_pos = len(data_processed)
                for b in data:
                    data_processed.append(b)

                    if data_processed.endswith(b'\x1b[?2004h'):
                        # set bracketed paste mode
                        data_processed += b'\x1b]51;["call","Tapi_bracketed_paste",1]\x07'

                    if data_processed.endswith(b'\x1b[?2004l'):
                        # reset bracketed paste mode
                        data_processed += b'\x1b]51;["call","Tapi_bracketed_paste",0]\x07'

                    # TODO what about \x9b? https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-C1-_8-Bit_-Control-Characters

                _writen(STDOUT_FILENO, data_processed[last_pos:])

                # truncate to save memory
                if(len(data_processed)>=1024+30):
                    data_processed=data_processed[1024:]
                last_pos=len(data_processed)
                
        if STDIN_FILENO in rfds:
            data = os.read(STDIN_FILENO, 1024)
            if not data:
                fds.remove(STDIN_FILENO)
            else:
                _writen(master_fd, data)

def main():
    interactive_command = sys.argv[1:]

    if hasattr(os, "fsencode"):
        # convert them back to bytes
        # http://bugs.python.org/issue8776
        interactive_command = [*map(os.fsencode, interactive_command)]

    pid, master_fd = pty.fork()

    if pid == 0:
        os.execlp(interactive_command[0], *interactive_command)

    try:
        mode = tty.tcgetattr(STDIN_FILENO)
        tty.setraw(STDIN_FILENO)
        restore = True
    except tty.error:    # This is the same as termios.error
        restore = False

    try:
        main_loop(master_fd)
    except OSError:
        if restore:
            tty.tcsetattr(0, tty.TCSAFLUSH, mode)

    os.close(master_fd)
    return os.waitpid(pid, 0)[1]

if __name__ == "__main__":
    main()

This stands between vim and the child process, to rewrite the content as necessary -- whenever the child process sends CSI?2004h the wrapper appends ESC[51;["call","Tapi_bracketed_paste",1]\x07 to it to inform vim about the change, similarly for CSI?2004l.

Then put this in vimrc:

function Tapi_bracketed_paste(bufn, args)
    call setbufvar(a:bufn, "bracketed_paste_enabled", a:args)
endfunction

Then the "bracketed paste" command can simply be defined as follows:

function TermBracketedPaste()
    if b:bracketed_paste_enabled
        return "\<esc>[200~\<c-j>\"\"\<esc>[201~"
    else
        return "\<c-j>\"\""
    endif
endfunction
tnore <expr> <c-j>p TermBracketedPaste()

or, more simply,

tnore <expr> <c-j>p b:bracketed_paste_enabled ? "\<esc>[200~\<c-j>\"\"\<esc>[201~" : "\<c-j>\"\""

If you run :term a.py bash in vim, it will open a terminal but optional bracketed paste works.

References:

1
  • Although the linked answer states that the code may have a deadlock (I have not been able to confirm that)
    – user202729
    Commented Feb 21, 2023 at 0:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.