├── .github └── workflows │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── pbcopy │ └── main.go ├── go.mod ├── misc └── preferences.png ├── pbcopy └── pbcopy.py /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.16.x 14 | - uses: goreleaser/goreleaser-action@v2 15 | with: 16 | version: latest 17 | args: release --rm-dist 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/pbcopy 3 | binary: pbcopy 4 | env: 5 | - CGO_ENABLED=0 6 | goos: 7 | - linux 8 | - darwin 9 | goarch: 10 | - amd64 11 | - arm64 12 | checksum: 13 | name_template: 'pbcopy-checksums.txt' 14 | archives: 15 | - name_template: "pbcopy-{{ .Os }}-{{ .Arch }}" 16 | changelog: 17 | sort: asc 18 | filters: 19 | exclude: 20 | - '^docs:' 21 | - '^test:' 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Shoichi Kaji 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remote pbcopy for iTerm2 2 | 3 | `pbcopy` is a well-known macOS tool that copies data to the clipboard. 4 | It's very useful, but available only in your local machine, not in remote machines. 5 | 6 | Fortunately, with OSC52 escape sequence, 7 | we can access the local machine clipboard via a remote machine. 8 | 9 | I prepared a simple tool that is `pbcopy` for remote machines. 10 | 11 | ## Install 12 | 13 | 1. First, make sure you use iTerm2 version 3.0.0 or later. 14 | 2. Copy a preferred `pbcopy` to a directory where `$PATH` is set. 15 | 16 | [local] $ ssh remote 17 | 18 | # If you prefer a self-contained binary, then 19 | [remote] $ curl -fsSLo pbcopy-linux-amd64.tar.gz https://github.com/skaji/remote-pbcopy-iterm2/releases/latest/download/pbcopy-linux-amd64.tar.gz 20 | [remote] $ tar xf pbcopy-linux-amd64.tar.gz 21 | [remote] $ mv pbcopy /path/to/bin/ 22 | 23 | # If you prefer a perl script, then 24 | [remote] $ curl -fsSLo pbcopy https://raw.githubusercontent.com/skaji/remote-pbcopy-iterm2/master/pbcopy 25 | [remote] $ chmod +x pbcopy 26 | [remote] $ mv pbcopy /path/to/bin/ 27 | 28 | # If you prefer a python script, then 29 | [remote] $ curl -fsSLo pbcopy https://raw.githubusercontent.com/skaji/remote-pbcopy-iterm2/master/pbcopy.py 30 | [remote] $ chmod +x pbcopy 31 | [remote] $ mv pbcopy /path/to/bin/ 32 | 33 | 3. Check "Applications in terminal may access clipboard" in iTerm2 Preferences: 34 | 35 | ![preferences.png](https://raw.githubusercontent.com/skaji/remote-pbcopy-iterm2/master/misc/preferences.png) 36 | 37 | ## Usage 38 | 39 | Just like the normal `pbcopy`: 40 | 41 | [local] $ ssh remote 42 | [remote] $ date | pbcopy 43 | [remote] $ exit 44 | [local] $ pbpaste 45 | Sun Jan 18 20:28:03 JST 2015 46 | 47 | ## How about `pbpaste`? 48 | 49 | Currently iTerm2 does not allow OSC 52 read access for security reasons. 50 | But we can just use command+V key to paste content from clipboard. 51 | 52 | If you want to save the content of clipboard to a remote file, try this: 53 | 54 | [remote] cat > out.txt 55 | # press command+V to paste content of clipboard, 56 | # and press control+D which indicats EOF 57 | 58 | ## See also 59 | 60 | For OSC52 61 | 62 | * http://doda.b.sourceforge.jp/2011/12/15/tmux-set-clipboard/ 63 | * http://qiita.com/kefir_/items/1f635fe66b778932e278 64 | * http://qiita.com/kefir_/items/515ed5264fce40dec522 65 | * https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm/etc/osc52.vim 66 | * https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm/etc/osc52.el 67 | * https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm/etc/osc52.sh 68 | 69 | ## Author 70 | 71 | Shoichi Kaji 72 | 73 | ## License 74 | 75 | MIT 76 | -------------------------------------------------------------------------------- /cmd/pbcopy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | func normalEsc(b64 string) string { 14 | return "\x1B]52;;" + b64 + "\x1B\x5C" 15 | } 16 | 17 | func tmuxEsc(b64 string) string { 18 | return "\x1BPtmux;\x1B\x1B]52;;" + b64 + "\x1B\x1B\x5C\x5C\x1B\x5C" 19 | } 20 | 21 | func screenEsc(b64 string) string { 22 | out := []string{} 23 | for i := 0; ; i++ { 24 | begin, end := i*76, (i+1)*76 25 | if end > len(b64) { 26 | end = len(b64) 27 | } 28 | if begin == 0 { 29 | out = append(out, "\x1BP\x1B]52;;"+b64[begin:end]) 30 | } else { 31 | out = append(out, "\x1B\x5C\x1BP"+b64[begin:end]) 32 | } 33 | if end == len(b64) { 34 | break 35 | } 36 | } 37 | out = append(out, "\x07\x1B\x5C") 38 | return strings.Join(out, "") 39 | } 40 | 41 | func isTmuxCC(pid string) bool { 42 | out, err := exec.Command("ps", "-p", pid, "-o", "command=").Output() 43 | if err != nil { 44 | return false 45 | } 46 | out = bytes.TrimRight(out, "\n\r") 47 | for _, argv := range strings.Split(string(out), " ") { 48 | if argv == "-CC" { 49 | return true 50 | } 51 | } 52 | return false 53 | } 54 | 55 | func chooseEsc() func(string) string { 56 | if env := os.Getenv("TMUX"); env != "" { 57 | envs := strings.Split(env, ",") 58 | if len(envs) > 1 { 59 | pid := envs[1] 60 | if isTmuxCC(pid) { 61 | return normalEsc 62 | } 63 | } 64 | return tmuxEsc 65 | } else if env := os.Getenv("TERM"); strings.HasPrefix(env, "screen") { 66 | return screenEsc 67 | } 68 | return normalEsc 69 | } 70 | 71 | func run() error { 72 | var b []byte 73 | var err error 74 | if len(os.Args) == 1 { 75 | b, err = ioutil.ReadAll(os.Stdin) 76 | } else { 77 | if os.Args[1] == "-h" || os.Args[1] == "--help" { 78 | fmt.Print("Usage:\n pbcopy FILE\n some-command | pbcopy\n") 79 | os.Exit(1) 80 | } 81 | b, err = ioutil.ReadFile(os.Args[1]) 82 | } 83 | if err != nil { 84 | return err 85 | } 86 | b = bytes.TrimRight(b, "\n\r") 87 | if len(b) == 0 { 88 | return nil 89 | } 90 | 91 | esc := chooseEsc() 92 | b64 := base64.RawStdEncoding.EncodeToString(b) 93 | fmt.Print(esc(b64)) 94 | return nil 95 | } 96 | 97 | func main() { 98 | if err := run(); err != nil { 99 | fmt.Fprintln(os.Stderr, err) 100 | os.Exit(1) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skaji/remote-pbcopy-iterm2 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /misc/preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skaji/remote-pbcopy-iterm2/513f4b90f7e8dd9fabadf1b40d1207ec8d5d03b8/misc/preferences.png -------------------------------------------------------------------------------- /pbcopy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use MIME::Base64 qw(encode_base64); 5 | 6 | my $b = "\\"; # 1 backslash, not 2 7 | 8 | my $normal = sub { 9 | my $base64 = shift; 10 | print "\e]52;;", $base64, "\e$b"; 11 | }; 12 | 13 | my $tmux = sub { 14 | my $base64 = shift; 15 | print "\ePtmux;\e\e]52;;", $base64, "\e\e$b$b\e$b"; 16 | }; 17 | 18 | my $screen = sub { 19 | my $base64 = shift; 20 | my $len = 76; 21 | my $first = 1; 22 | while (1) { 23 | my $chunk = substr $base64, 0, $len, ""; 24 | last if $chunk eq ""; 25 | if ($first) { 26 | print "\eP\e]52;;", $chunk; 27 | undef $first; 28 | } else { 29 | print "\e$b\eP", $chunk; 30 | } 31 | } 32 | print "\a\e$b"; 33 | }; 34 | 35 | my $input = do { local $/; <> } || ""; 36 | $input =~ s/\n+\z//sm; 37 | length $input or exit; 38 | 39 | my $print = $normal; # default 40 | CHOOSE: { 41 | if (my $tmux_env = $ENV{TMUX}) { 42 | (undef, my $pid) = split /,/, $tmux_env, 3; 43 | if ($pid) { 44 | my $ps = `ps -p $pid -o command= 2>/dev/null` || ""; 45 | chomp $ps; 46 | (undef, my @option) = split /\s+/, $ps; 47 | last CHOOSE if grep { $_ eq "-CC" } @option; 48 | } 49 | $print = $tmux; 50 | } elsif ( ($ENV{TERM} || "") =~ /^screen/ ) { 51 | $print = $screen; 52 | } 53 | } 54 | 55 | $print->( encode_base64($input, "") ); 56 | 57 | __END__ 58 | 59 | =head1 NAME 60 | 61 | pbcopy - remote pbcopy for iTerm2 62 | 63 | =head1 AUTHOR 64 | 65 | Shoichi Kaji 66 | 67 | =head1 LICENSE 68 | 69 | MIT 70 | 71 | =cut 72 | -------------------------------------------------------------------------------- /pbcopy.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import base64 4 | import os 5 | import subprocess 6 | import sys 7 | from typing import Callable 8 | 9 | 10 | def normal_esc(b64: str) -> str: 11 | return "\x1B]52;;" + b64 + "\x1B\x5C" 12 | 13 | 14 | def tmux_esc(b64: str) -> str: 15 | return "\x1BPtmux;\x1B\x1B]52;;" + b64 + "\x1B\x1B\x5C\x5C\x1B\x5C" 16 | 17 | 18 | def screen_esc(b64: str) -> str: 19 | out = [] 20 | for i in range(sys.maxsize): 21 | begin, end = i * 76, min((i + 1) * 76, len(b64)) 22 | out.append(("\x1BP\x1B]52;;" if begin == 0 else "\x1B\x5C\x1BP") + b64[begin:end]) 23 | if end == len(b64): 24 | break 25 | out.append("\x07\x1B\x5C") 26 | 27 | return "".join(out) 28 | 29 | 30 | def is_tmux_cc(pid: str) -> bool: 31 | try: 32 | out = subprocess.check_output(["ps", "-p", pid, "-o", "command="]) 33 | out = out.rstrip(b"\n\r") 34 | for arg in out.split(b" "): 35 | if arg == b"-CC": 36 | return True 37 | return False 38 | except subprocess.CalledProcessError: 39 | return False 40 | 41 | 42 | def choose_esc() -> Callable[[str], str]: 43 | env = os.getenv("TMUX") 44 | if env: 45 | envs = env.split(",") 46 | if len(envs) > 1 and is_tmux_cc(envs[1]): 47 | return normal_esc 48 | else: 49 | return tmux_esc 50 | 51 | env = os.getenv("TERM") 52 | if env and env.startswith("screen"): 53 | return screen_esc 54 | 55 | return normal_esc 56 | 57 | 58 | def run(): 59 | if len(sys.argv) == 1: 60 | b = sys.stdin.buffer.read() 61 | else: 62 | if sys.argv[1] == "-h" or sys.argv[1] == "--help": 63 | print("Usage:\n pbcopy FILE\n some-command | pbcopy\n", end="") 64 | sys.exit(1) 65 | b = open(sys.argv[1], "rb").read() 66 | 67 | b = b.rstrip(b"\n\r") 68 | if b: 69 | b64 = base64.b64encode(b).decode(encoding="UTF-8") 70 | print(choose_esc()(b64), end="") 71 | 72 | 73 | if __name__ == "__main__": 74 | run() 75 | --------------------------------------------------------------------------------