tmux на практиці: інтеграція із системним буфером обміну

Як побудувати міст між буфером копіювання tmux та системним буфером обміну, а також зберігати виділений текст у системному буфері обміну OSX або Linux таким чином, щоб розглядати сценарії локального та віддаленого використання

Це 4-та частина мого tmux на практиці.

У попередній частині серії «tmux на практиці» ми говорили про такі речі, як буфер прокрутки, режим копіювання та трохи торкнулися теми копіювання тексту в буфер копіювання tmux.

Рано чи пізно ви зрозумієте, що все, що ви копіюєте в tmux, зберігається лише в буфері копіювання tmux, але не передається в системний буфер обміну. Копіювання та вставлення є настільки типовими операціями, що самого цього обмеження досить, щоб перетворити tmux у марну цеглу, незважаючи на інші смаколики.

У цьому дописі ми розглянемо, як побудувати міст між буфером копіювання tmux та системним буфером обміну, щоб зберігати скопійований текст у системному буфері обміну таким чином, щоб розглядати як локальний, так і віддалений сценарії використання.

Ми обговоримо наступні методи:

  1. Лише для OSX, діліться текстом із буфером обміну, використовуючи “pbcopy”
  2. Лише для OSX, використовуючи обгортку “reattach-to-user-namespace”, щоб pbcopy працював належним чином у середовищі tmux
  3. Лише для Linux, діліться текстом із виділенням X за допомогою команд xclipабоxsel

Методи, наведені вище, стосуються лише місцевих сценаріїв.

Для підтримки віддалених сценаріїв існує 2 додаткові методи:

  1. Використовуйте вхідну послідовність ANSI OSC 52 для спілкування з контрольним / батьківським терміналом для управління та зберігання тексту в буфері обміну локальної машини.
  2. Налаштуйте прослуховувач локальної мережі, який передає вхід до pbcopyабо xclipабо xsel. Пайп скопіював вибраний текст із віддаленої машини на слухач на локальній машині за допомогою віддаленого тунелювання SSH. Це досить задіяно, і я присвячу спеціальний пост, щоб описати це.

OSX. команди pbcopy та pbpaste

pbcopyі pbpasteкоманди дозволяють взаємодіяти та маніпулювати системним буфером обміну з командного рядка.

pbcopy зчитує дані stdinта зберігає їх у буфері обміну. pbpasteробить навпаки і ставить скопійований текст stdout.

Ідея полягає в тому, щоб підключити різні команди tmux, яким вдається копіювати текст, перебуваючи в режимі копіювання.

Давайте перелічимо їх:

$ tmux -f /dev/null list-keys -T copy-mode-vi
bind-key -T copy-mode-vi Enter send-keys -X copy-selection-and-cancelbind-key -T copy-mode-vi C-j send-keys -X copy-selection-and-cancelbind-key -T copy-mode-vi D send-keys -X copy-end-of-linebind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection-and-cancelbind-key -T copy-mode-vi A send-keys -X append-selection-and-cancel

copy-selection-and-cancelі copy-end-of-lineє спеціальними командами tmux, які tmux розуміє, коли панель перебуває в режимі копіювання. Існує два варіанти команди копіювання: copy-selectionі copy-pipe.

Давайте перепишемо Enterприв'язку клавіш за допомогою команди copy-pipe:

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "pbcopy"

copy-pipeкоманда зберігає виділений текст у буфері tmux однаково copy-selection, плюс додає виділений текст до заданої команди pbcopy. Отже, ми отримуємо текст, який зберігається у двох місцях: буфер копіювання tmux та системний буфер обміну.

OSX. обгортка reattach-to-user-namespace

Все йде нормально. Однак, в деяких версіях OSX, pbcopyі pbpaste НЕ в змозі функціонувати належним чином при запуску під tmux.

Прочитайте більше деталей від Кріса Джонсена про те, чому це відбувається:

tmux використовує функцію бібліотеки демона (3) під час запуску процесу сервера. У Mac OS X 10.5 Apple змінила демон (3), щоб перемістити отриманий процес із початкового простору імен bootstrap у кореневий простір імен bootstrap. Це означає, що сервер tmux та його дочірні пристрої автоматично та неконтрольовано втратять доступ до того, що було б їхнім початковим простором імен bootstrap (тобто до того, який має доступ до служби картону).

Загальним рішенням є використання обгортки reattach-to-user-namespace. Це дозволяє нам запустити процес і приєднати цей процес до простору імен bootstrap для кожного користувача, що змушує програму поводитися так, як ми очікуємо. Вам потрібно правильно змінити прив'язку клавіш:

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel “reattach-to-user-namespace pbcopy”

Крім того, вам потрібно буде сказати tmux запустити вашу оболонку (bash, zsh, ...) всередині обгортки, встановивши default-commandпараметр:

if -b "command -v reattach-to-user-namespace > /dev/null 2>&1" \ "run 'tmux set -g default-command \"exec $(tmux show -gv default-shell) 2>/dev/null & reattach-to-user-namespace -l $(tmux show -gv default-shell)\"'"

Примітка : деякі версії OSX чудово працюють навіть без цього злому (OSX 10.11.5 El Capitan), тоді як користувачі OSX Sierra повідомляють, що цей злом все ще потрібен.

Linux. Взаємодійте з виділенням X за допомогою xclip та xsel

Ми можемо використовувати xclipабо xselкоманди на Linux для зберігання тексту в буфері обміну, як і pbcopyна OSX. У Linux існує декілька видів вибору буфера обміну, що підтримуються сервером X: основний, вторинний та буфер обміну. Ми маємо справу лише з основним та буфером обміну. Вторинна була задумана як альтернатива первинній.

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xclip -i -f -selection primary | xclip -i -selection clipboard"

Або при використанні xsel:

bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "xsel -i --clipboard"

Прочитайте тут про порівняння xclipпроти xsel, якщо вам цікаво. Також перегляньте цю публікацію щодо xclipвикористання та прикладів. І не забудьте встановити одну з цих утиліт, оскільки вони можуть не бути частиною вашого дистрибутива.

Використання екрануючої послідовності ANSI OSC 52, щоб термінал зберігав текст у буфері обміну

Поки що ми розглядали лише місцеві сценарії. При SSH з віддаленим комп'ютером, і почати tmux сесій, Ви не можете використовувати pbcopy, xclipабо xsel, тому що текст буде записаний в буфер обміну віддаленого комп'ютера, а не в вашій локальній. Вам потрібен спосіб перенести скопійований текст у буфер обміну локальної машини.

Екранна послідовність ANSI - це послідовність байтів, відправлених на термінал, які перемежовуються звичайними символами для друку та використовуються для управління різними аспектами терміналу: такими як кольори тексту, положення курсору, текстові ефекти, екран очищення. Термінал здатний виявляти таку керуючу послідовність байтів, яка змушує його запускати певні дії, а не друкувати ці символи на виході.

ESCЕкранну послідовність ANSI можна виявити, коли вона починається з символу ASCII (0x1b шістнадцяткове, 027 десяткове, \ 033 в восьмеричному). Наприклад, коли термінал побачить \033[2Aпослідовність, він перемістить позицію курсора на 2 рядки вгору.

Цих відомих послідовностей насправді багато. Деякі з них однакові для різних типів терміналів, тоді як інші можуть відрізнятися і бути дуже специфічними для вашого емулятора терміналу. Використовуйте infocmpкоманду для запиту terminfoбази даних для вихідних послідовностей, що підтримуються різними типами терміналів.

Гаразд, чудово, але як це може допомогти нам щодо буфера обміну? Виявляється, існує спеціальна категорія екранованих послідовностей: «Елементи керування операційною системою» (OSC) та послідовність екранів «OSC 52», яка дозволяє програмам взаємодіяти з буфером обміну.

Якщо ви використовуєте iTerm, спробуйте виконати наступну команду, а потім “ ⌘V”, щоб переглянути вміст системного буфера обміну. Обов’язково увімкніть обробку послідовності виходу OSC 52: «Налаштування -> Загальне -> Програми в терміналі можуть отримати доступ до буфера обміну».

printf "\033]52;c;$(printf "%s" "blabla" | base64)\a"

Висновок полягає в тому, що ми можемо зберігати текст у системному буфері обміну, надсилаючи на наш термінал спеціально створену послідовність виходів ANSI.

Давайте напишемо сценарій оболонки yank.sh:

#!/bin/bash
set -eu
# get data either form stdin or from filebuf=$(cat "[email protected]")
# Get buffer lengthbuflen=$( printf %s "$buf" | wc -c )
maxlen=74994
# warn if exceeds maxlenif [ "$buflen" -gt "$maxlen" ]; then printf "input is %d bytes too long" "$(( buflen - maxlen ))" >&2fi
# build up OSC 52 ANSI escape sequenceesc="\033]52;c;$( printf %s "$buf" | head -c $maxlen | base64 | tr -d '\r\n' )\a"

Отже, ми читаємо текст для копіювання stdin, а потім перевіряємо, чи перевищує його довжина максимальну довжину 74994 байт. Якщо це так, ми обрізаємо його, і, нарешті, перетворюємо дані в base64 і обертаємо в послідовність OSC 52:\033]53;c;${data_in_base64}\a

Тоді давайте зв’яжемо це нашими клавіатурами tmux. Це досить просто: просто додайте вибраний текст до нашого yank.shсценарію, як і ми до pbcopyабо xclip.

yank="~/.tmux/yank.sh"
bind -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "$yank"

Однак для завершення головоломки залишився один шматок. Куди слід надіслати втечу? Мабуть, просто відправити його не stdoutбуде працювати. Ціль має бути нашим емулятором батьківського терміналу, але ми не знаємо правильного tty. Отже, ми надішлемо його в активну область ttytmux і скажемо tmux, щоб надалі відправити його в емулятор батьківського терміналу:

# build up OSC 52 ANSI escape sequenceesc="\033]52;c;$( printf %s "$buf" | head -c $maxlen | base64 | tr -d '\r\n' )\a"esc="\033Ptmux;\033$esc\033\\"
pane_active_tty=$(tmux list-panes -F "#{pane_active} #{pane_tty}" | awk '$1=="1" { print $2 }')
printf "$esc" > "$pane_active_tty"

Ми використовуємо tmux list-panesкоманду для запиту для активної панелі, і це tty. Ми також помістили нашу послідовність OSC 52 в додаткову послідовність екранування обгортки (Device Control String, ESC P), тому tmux розгортає цей конверт і передає OSC 52 батьківському терміналу.

In newer versions of tmux, you can tell tmux to handle interactions with the clipboard for you. Seeset-clipboard tmux option. on — tmux will create an inner buffer and attempt to set the terminal clipboard using OSC 52. external — do not create a buffer, but still attempt to set the terminal clipboard.

Just make sure it’s either external or on:

set -g set-clipboard on

So, if tmux is already capable of this feature, why we need to bother ourselves with manual wiring OSC 52 stuff? That’s because set-clipboard does not work when you have a remote tmux session nested in a local one. And it only works in those terminals which supports OSC 52 escape sequence handling.

The trick for nested remote sessions is to bypass the remote session and send our OSC 52 escape sequence directly to the local session, so it hits our local terminal emulator (iTerm).

Use $SSH_TTY for this purpose:

# resolve target terminal to send escape sequence# if we are on remote machine, send directly to SSH_TTY to transport escape sequence# to terminal on local machine, so data lands in clipboard on our local machinepane_active_tty=$(tmux list-panes -F "#{pane_active} #{pane_tty}" | awk '$1=="1" { print $2 }')target_tty="${SSH_TTY:-$pane_active_tty}"
printf "$esc" > "$target_tty"

That’s it. Now we have a completely working solution, be it a local session, remote or both, nested in each other. Credits to this great post, where I first read about this approach.

The major drawback of using OSC escape sequences,is that despite being declared in spec, only a few terminals support this in practice: iTerm and xterm do, whereas OSX Terminal, Terminator, and Gnome terminal does not. So, an otherwise great solution (especially in remote scenarios, when you cannot just pipe to xclip or pbcopy) lacks wider terminal support.

You might want to checkout complete version of yank.sh script.

There is yet another solution to support remote scenarios, which is rather crazy, and I’ll describe it in another dedicated post. The idea is to setup a local network listener which pipes input to pbcopy or xclipor xsel; and pipes copied selected text from a remote machine to a listener on the local machine through SSH remote tunneling. Stay tuned.

Resources and links

ANSI escape code — Wikipedia — //en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences

What are OSC terminal control sequences / escape codes? | ivucica blog — //blog.vucica.net/2017/07/what-are-osc-terminal-control-sequences-escape-codes.html

Copying to clipboard from tmux and Vim using OSC 52 — The Terminal Programmer — //sunaku.github.io/tmux-yank-osc52.html

Copy Shell Prompt Output To Linux / UNIX X Clipboard Directly — nixCraft — //www.cyberciti.biz/faq/xclip-linux-insert-files-command-output-intoclipboard/

software recommendation — ‘xclip’ vs. ‘xsel’ — Ask Ubuntu — //askubuntu.com/questions/705620/xclip-vs-xsel

Everything you need to know about Tmux copy paste · rushiagr — //www.rushiagr.com/blog/2016/06/16/everything-you-need-to-know-about-tmux-copy-pasting/

macos — Synchronize pasteboard between remote tmux session and local Mac OS pasteboard — Super User — //superuser.com/questions/407888/synchronize-pasteboard-between-remote-tmux-session-and-local-mac-os-pasteboard/408374#408374

linux — Getting Items on the Local Clipboard from a Remote SSH Session — Stack Overflow — //stackoverflow.com/questions/1152362/getting-items-on-the-local-clipboard-from-a-remote-ssh-session

Use tmux set-clipboard in gnome-terminal (XTerm’s disallowedWindowOps) — Ask Ubuntu — //askubuntu.com/questions/621522/use-tmux-set-clipboard-in-gnome-terminal-xterms-disallowedwindowops/621646