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
    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
    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
    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
    Feb 21, 2023 at 0:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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