Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSC 52 clipboard reading #1477

Closed
IngoHeimbach opened this issue Sep 14, 2018 · 35 comments
Closed

OSC 52 clipboard reading #1477

IngoHeimbach opened this issue Sep 14, 2018 · 35 comments

Comments

@IngoHeimbach
Copy link

@IngoHeimbach IngoHeimbach commented Sep 14, 2018

Tmux supports clipboard setting with OSC 52 control sequences. Original XTerm additionally supports reading the clipboard with a special ? parameter (instead of a Base64 encoded string). Currently, tmux (version 2.7) ignores OSC 52 with a ? parameter completely. Would it be possible to handle a clipboard reading request? This feature would be very helpful when working with tmux on remote machines.

OSC 52 reading can be tested with the following Python script (tested in XTerm):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import base64
import copy
import fcntl
import os
import select
import subprocess
import sys
import termios

try:
    from typing import List, IO, Optional  # noqa: F401 pylint: disable=unused-import
except ImportError:
    pass

PY2 = sys.version_info.major < 3  # is needed for correct mypy checking

if PY2:
    str = unicode  # pylint: disable=redefined-builtin
    stdin_bytes = sys.stdin
    stdout_bytes = sys.stdout
else:
    stdin_bytes = sys.stdin.buffer
    stdout_bytes = sys.stdout.buffer


class NoTtyException(Exception):
    pass


def get_active_tmux_tty():
    # type: () -> str
    try:
        tmux_active_tty = [
            tty
            for is_active, tty in (
                line.split()
                for line in subprocess.check_output(["tmux", "list-panes", "-F", "#{pane_active} #{pane_tty}"])
                .strip()
                .split(b"\n")
            )
            if is_active
        ][0]
    except (subprocess.CalledProcessError, IndexError):
        raise NoTtyException
    return tmux_active_tty


def get_active_tty():
    # type: () -> str
    if "TMUX" in os.environ:
        active_tty = get_active_tmux_tty()
    else:
        try:
            active_tty = subprocess.check_output(["tty"]).strip()
        except subprocess.CalledProcessError:
            raise NoTtyException
    return active_tty


def toggle_terminal_for_osc_query():
    # type: () -> None
    self = toggle_terminal_for_osc_query
    active_tty = get_active_tty()
    with open(active_tty, "wb") as tty:
        if not hasattr(self, "terminal_attrs"):
            self.terminal_attrs = termios.tcgetattr(tty)  # type: ignore
            modified_terminal_attrs = copy.deepcopy(self.terminal_attrs)  # type: ignore
            modified_terminal_attrs[3] &= ~(  # type: ignore
                termios.ICANON | termios.ECHO
            )  # Disable echo of input bytes and line buffering
            termios.tcsetattr(tty, termios.TCSANOW, modified_terminal_attrs)
        else:
            termios.tcsetattr(tty, termios.TCSANOW, self.terminal_attrs)  # type: ignore
            del self.terminal_attrs  # type: ignore


def set_non_blocking_mode(f):
    # type: (IO) -> None
    fl = fcntl.fcntl(f, fcntl.F_GETFL)
    fcntl.fcntl(f, fcntl.F_SETFL, fl | os.O_NONBLOCK)


def osc_paste():
    # type: () -> bytes
    toggle_terminal_for_osc_query()
    try:
        osc_sequence = b"\033]52;c;?\a"  # type: bytes
        active_tty = get_active_tty()
        with open(active_tty, "wb") as tty:
            tty.write(osc_sequence)
        with open(active_tty, "rb") as tty:
            set_non_blocking_mode(tty)
            next_bytes = b""
            response_bytes = []  # type: List[bytes]
            response_parameter_id = 0
            read_sequence_terminator = False
            while not read_sequence_terminator and tty in select.select([tty], [], [], 0.01)[0]:
                next_bytes = tty.read()
                while True:
                    next_delimiter_index = next_bytes.find(b";")
                    if next_delimiter_index < 0:
                        break
                    response_parameter_id += 1
                    next_bytes = next_bytes[next_delimiter_index + 1 :]
                sequence_terminator_index = next_bytes.find(b"\a")
                if sequence_terminator_index >= 0:
                    next_bytes = next_bytes[:sequence_terminator_index]
                    read_sequence_terminator = True
                if response_parameter_id == 2:
                    response_bytes.append(next_bytes)
    finally:
        toggle_terminal_for_osc_query()
    response = base64.b64decode(b"".join(response_bytes))
    return response


def main():
    # type: () -> None
    try:
        paste_buffer = osc_paste()
        if paste_buffer:
            stdout_bytes.write(paste_buffer)
            sys.exit(0)
        else:
            # Assume that the terminal does not support OSC 52 reading if the response was empty
            sys.exit(1)
    except NoTtyException:
        sys.exit(2)


if __name__ == "__main__":
    main()
@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

There is no reason tmux couldn't support this, although I don't want it to use it to read the X clipboard.

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Sep 14, 2018

I am aware that many people consider such a feature as a potential security risk when connecting to other machines. In my opinion, clipboard reading is ok if one only connects to trusted machines. OSC52 reading could be activated with an extra option.
I would be glad to contribute to this great project but I couldn't find the line in the code which blocks reading requests / broken base64 encodings.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

I don't know what you are asking. I don't want tmux to read the X clipboard from the external terminal, at least not without the user requesting it, it would need to be a flag to set-buffer or something.

If you want to make tmux report its own top buffer with OSC 52 ? then you can do that by extending input.c:input_osc_52. Look at input_reply for how to respond to a query.

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Sep 14, 2018

Why is it a problem to pass the OSC query to the outer terminal?

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

How would it work? tmux has no way of automatically knowing when the clipboard is changed, it can only ask for it. I don't want it to poll because it would be too much data. So tmux needs to do it when the user asks for it.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

I think this needs two changes:

  • A flag to set-buffer or load-buffer that makes tmux read the X clipboard using OSC 52 ? into a buffer. It would need to send the escape sequence and set a flag or a callback to tell the input parser what to do when it sees the response. I think it would need a timeout as well because IIRC you don't get a response at all if it is disabled in the terminal.

  • Support for OSC 52 ? for applications inside tmux to reply with tmux's own top buffer, this should be pretty simple.

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Sep 14, 2018

Ah sorry, maybe I did not explain enough. I think it is not necessary to poll for X clipboard changes. Instead, everytime when an application requests the clipboard content (with \\E]52;c;?\a) tmux could simply pass this escape sequence to the outer terminal, read the response (setting the tmux paste buffer optionally) and passing the answer back to the application.
You are right, many terminals do not respond to OSC 52 read requests, so a timeout is mandatory. Some terminals (like st) even clear the X clipboard because ? is interpreted as a bad parameter value.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

Yes that could work but it would need to be an option, it could be a tristate like set-clipboard - off, external (get external clipboard into top buffer and return it), internal (get top buffer only).

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

Actually there is a problem - how should tmux work out which terminal you want the clipboard from?

I think it is better if the user has to request it explicitly, then they can specify a client or it can follow the normal rules.

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Sep 14, 2018

Ah, I didn't think about the case when multiple clients are connected....

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Sep 14, 2018

Btw: What does normal rules mean?

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 14, 2018

How the command works out the client if you do not specify -t.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Sep 28, 2018

Here is the ? part: tmux-osc-52.diff.txt

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 3, 2018

Try this please which also adds "refresh-client -c" to read the clipboard from the external terminal and store it as a new paste buffer: tmux-osc52-2.diff.txt

I'm not entirely sure refresh-client is the best place but it has -c already. I don't think we need a timeout because no response will do nothing.

@nicm nicm closed this Oct 7, 2018
@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 7, 2018

Why is this issue closed?
Sorry, I have been quite busy for the last days. I can test your patch tomorrow evening.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 7, 2018

OK let me know.

@nicm nicm reopened this Oct 7, 2018
@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 8, 2018

I tried your first patch (looks good and compiles fine) but I cannot apply your second patch. When running

git apply -3 tmux-osc52-2.diff

I get

error: patch failed: cmd-refresh-client.c:31
error: repository lacks the necessary blob to fall back on 3-way merge.
error: cmd-refresh-client.c: patch does not apply

Is anything missing in the public repository?

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 8, 2018

Try this instead please. I forgot I'd already used -c in master so it is refresh-client -l instead: tmux-ul-zz.diff.txt

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 9, 2018

Thanks for your new patch!
I did some tests and have these results:

  • Sending a clipboard reading request to tmux works in some cases and in some cases it doesn't. It seems to be dependent on the text that was put into the paste buffer before. I think I have some more time tonight to dig a bit deeper.
  • Running tmux refresh-client -l writes the base64 encoded data to the tty. The code for setting the paste buffer is still missing here, isn't it?
@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 9, 2018

No it all works for me, what terminal are you using?

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 9, 2018

Strange, I have tried it in XTerm. Maybe I did something wrong?

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 9, 2018

Are you sure you have both "Allow Window Ops" on in xterm and set-clipboard on in tmux?

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 9, 2018

I have set -g set-clipboard on in my ~/.tmux.conf and XTerm*allowWindowOps: true in my ~/.Xresources.

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 9, 2018

I have tested the OSC reading part with the Python script of my first post, maybe there is still an error in that script...

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 9, 2018

I see a problem now...

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 9, 2018

In input_osc_52 change the outlen calculation on line input.c:2357 to:

outlen = 4 * ((len + 2) / 3) + 1;
@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 10, 2018

👍 Works perfectly.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 11, 2018

Applied now, thanks!

@nicm nicm closed this Oct 11, 2018
@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 12, 2018

Thank you very much for your work!
I think I have found one small issue: When pressing <prefix>: and running refresh-client -l a new paste buffer is created with the current X clipboard contents. But when running tmux refresh-client -l on the command line, the XTerm answer is printed to the tty directly and no paste buffer is created:

heimbach@debian:~$ tmux refresh-client -l
^[]52;s0;V2VsY29tZSB0byBJRkZHaXQsIA==^G%
@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 12, 2018

It definitely works for me because I always use the shell, can you show me tmux logs from doing this?

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 12, 2018

Ok, I have started a fresh tmux server with tmux -v and run tmux refresh-client -l. These are my logs:
tmux-logs.zip
The server log is very large. Can you make use of it anyway or is it possible to set a kind of filter for the verbose mode?

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 12, 2018

You have escape-time set to 0 which is not enough time for tmux to process the response when it comes in several parts, you will need to increase escape-time.

@nicm

This comment has been minimized.

Copy link
Member

@nicm nicm commented Oct 12, 2018

Setting it to 25 milliseconds or so should be enough for most clipboards I would guess.

@IngoHeimbach

This comment has been minimized.

Copy link
Author

@IngoHeimbach IngoHeimbach commented Oct 12, 2018

Ah, the escape time is set by the tmux-sensible plugin.... Ok, I will change that. Thanks for your help and time.

@lock

This comment has been minimized.

Copy link

@lock lock bot commented Feb 15, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked and limited conversation to collaborators Feb 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants
You can’t perform that action at this time.