pax_global_header00006660000000000000000000000064144013772220014514gustar00rootroot0000000000000052 comment=ce73587a0f72cf077e55f163e71ea1b5496a133c go-osc52-2.0.1/000077500000000000000000000000001440137722200130525ustar00rootroot00000000000000go-osc52-2.0.1/.github/000077500000000000000000000000001440137722200144125ustar00rootroot00000000000000go-osc52-2.0.1/.github/workflows/000077500000000000000000000000001440137722200164475ustar00rootroot00000000000000go-osc52-2.0.1/.github/workflows/build.yml000066400000000000000000000014531440137722200202740ustar00rootroot00000000000000name: build on: push: pull_request: branches: - master jobs: build: strategy: matrix: go-version: [^1] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - name: Checkout code uses: actions/checkout@v3 - name: Install Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} - uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Download Go modules run: go mod download - name: Build run: go build -v ./... - name: Test run: go test ./... go-osc52-2.0.1/LICENSE000066400000000000000000000020561440137722200140620ustar00rootroot00000000000000MIT License Copyright (c) 2022 Ayman Bagabas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-osc52-2.0.1/README.md000066400000000000000000000045021440137722200143320ustar00rootroot00000000000000 # go-osc52

Latest Release GoDoc

A Go library to work with the [ANSI OSC52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) terminal sequence. ## Usage You can use this small library to construct an ANSI OSC52 sequence suitable for your terminal. ### Example ```go import ( "os" "fmt" "github.com/aymanbagabas/go-osc52/v2" ) func main() { s := "Hello World!" // Copy `s` to system clipboard osc52.New(s).WriteTo(os.Stderr) // Copy `s` to primary clipboard (X11) osc52.New(s).Primary().WriteTo(os.Stderr) // Query the clipboard osc52.Query().WriteTo(os.Stderr) // Clear system clipboard osc52.Clear().WriteTo(os.Stderr) // Use the fmt.Stringer interface to copy `s` to system clipboard fmt.Fprint(os.Stderr, osc52.New(s)) // Or to primary clipboard fmt.Fprint(os.Stderr, osc52.New(s).Primary()) } ``` ## SSH Example You can use this over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) for instance: ```go var sshSession ssh.Session seq := osc52.New("Hello awesome!") // Check if term is screen or tmux pty, _, _ := s.Pty() if pty.Term == "screen" { seq = seq.Screen() } else if isTmux { seq = seq.Tmux() } seq.WriteTo(sshSession.Stderr()) ``` ## Tmux Make sure you have `set-clipboard on` in your config, otherwise, tmux won't allow your application to access the clipboard [^1]. Using the tmux option, `osc52.TmuxMode` or `osc52.New(...).Tmux()`, wraps the OSC52 sequence in a special tmux DCS sequence and pass it to the outer terminal. This requires `allow-passthrough on` in your config. `allow-passthrough` is no longer enabled by default [since tmux 3.3a](https://github.com/tmux/tmux/issues/3218#issuecomment-1153089282) [^2]. [^1]: See [tmux clipboard](https://github.com/tmux/tmux/wiki/Clipboard) [^2]: [What is allow-passthrough](https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it) ## Credits * [vim-oscyank](https://github.com/ojroques/vim-oscyank) this is heavily inspired by vim-oscyank. go-osc52-2.0.1/_examples/000077500000000000000000000000001440137722200150275ustar00rootroot00000000000000go-osc52-2.0.1/_examples/local/000077500000000000000000000000001440137722200161215ustar00rootroot00000000000000go-osc52-2.0.1/_examples/local/main.go000066400000000000000000000002341440137722200173730ustar00rootroot00000000000000package main import ( "fmt" "github.com/aymanbagabas/go-osc52" ) func main() { str := "hello world" osc52.Copy(str) fmt.Printf("Copied %q!", str) } go-osc52-2.0.1/_examples/ssh/000077500000000000000000000000001440137722200156245ustar00rootroot00000000000000go-osc52-2.0.1/_examples/ssh/main.go000066400000000000000000000014471440137722200171050ustar00rootroot00000000000000package main import ( "fmt" "log" "github.com/aymanbagabas/go-osc52" "github.com/charmbracelet/wish" "github.com/gliderlabs/ssh" ) func main() { s, err := wish.NewServer( wish.WithAddress(":2222"), wish.WithHostKeyPath("ssh_host_key"), wish.WithMiddleware( middleware(), ), ) if err != nil { log.Fatal(err) } fmt.Printf("SSH into %s\n", s.Addr) s.ListenAndServe() } func middleware() wish.Middleware { return func(h ssh.Handler) ssh.Handler { return func(s ssh.Session) { environ := s.Environ() pty, _, _ := s.Pty() // Put TERM environment variable into environ. environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term)) out := osc52.NewOutput(s, environ) str := "hello world" out.Copy(str) s.Write([]byte(fmt.Sprintf("Copied %q!\n", str))) } } } go-osc52-2.0.1/go.mod000077500000000000000000000000641440137722200141630ustar00rootroot00000000000000module github.com/aymanbagabas/go-osc52/v2 go 1.16 go-osc52-2.0.1/go.sum000066400000000000000000000000001440137722200141730ustar00rootroot00000000000000go-osc52-2.0.1/osc52.go000077500000000000000000000174521440137722200143500ustar00rootroot00000000000000// OSC52 is a terminal escape sequence that allows copying text to the clipboard. // // The sequence consists of the following: // // OSC 52 ; Pc ; Pd BEL // // Pc is the clipboard choice: // // c: clipboard // p: primary // q: secondary (not supported) // s: select (not supported) // 0-7: cut-buffers (not supported) // // Pd is the data to copy to the clipboard. This string should be encoded in // base64 (RFC-4648). // // If Pd is "?", the terminal replies to the host with the current contents of // the clipboard. // // If Pd is neither a base64 string nor "?", the terminal clears the clipboard. // // See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands // where Ps = 52 => Manipulate Selection Data. // // Examples: // // // copy "hello world" to the system clipboard // fmt.Fprint(os.Stderr, osc52.New("hello world")) // // // copy "hello world" to the primary Clipboard // fmt.Fprint(os.Stderr, osc52.New("hello world").Primary()) // // // limit the size of the string to copy 10 bytes // fmt.Fprint(os.Stderr, osc52.New("0123456789").Limit(10)) // // // escape the OSC52 sequence for screen using DCS sequences // fmt.Fprint(os.Stderr, osc52.New("hello world").Screen()) // // // escape the OSC52 sequence for Tmux // fmt.Fprint(os.Stderr, osc52.New("hello world").Tmux()) // // // query the system Clipboard // fmt.Fprint(os.Stderr, osc52.Query()) // // // query the primary clipboard // fmt.Fprint(os.Stderr, osc52.Query().Primary()) // // // clear the system Clipboard // fmt.Fprint(os.Stderr, osc52.Clear()) // // // clear the primary Clipboard // fmt.Fprint(os.Stderr, osc52.Clear().Primary()) package osc52 import ( "encoding/base64" "fmt" "io" "strings" ) // Clipboard is the clipboard buffer to use. type Clipboard rune const ( // SystemClipboard is the system clipboard buffer. SystemClipboard Clipboard = 'c' // PrimaryClipboard is the primary clipboard buffer (X11). PrimaryClipboard = 'p' ) // Mode is the mode to use for the OSC52 sequence. type Mode uint const ( // DefaultMode is the default OSC52 sequence mode. DefaultMode Mode = iota // ScreenMode escapes the OSC52 sequence for screen using DCS sequences. ScreenMode // TmuxMode escapes the OSC52 sequence for tmux. Not needed if tmux // clipboard is set to `set-clipboard on` TmuxMode ) // Operation is the OSC52 operation. type Operation uint const ( // SetOperation is the copy operation. SetOperation Operation = iota // QueryOperation is the query operation. QueryOperation // ClearOperation is the clear operation. ClearOperation ) // Sequence is the OSC52 sequence. type Sequence struct { str string limit int op Operation mode Mode clipboard Clipboard } var _ fmt.Stringer = Sequence{} var _ io.WriterTo = Sequence{} // String returns the OSC52 sequence. func (s Sequence) String() string { var seq strings.Builder // mode escape sequences start seq.WriteString(s.seqStart()) // actual OSC52 sequence start seq.WriteString(fmt.Sprintf("\x1b]52;%c;", s.clipboard)) switch s.op { case SetOperation: str := s.str if s.limit > 0 && len(str) > s.limit { return "" } b64 := base64.StdEncoding.EncodeToString([]byte(str)) switch s.mode { case ScreenMode: // Screen doesn't support OSC52 but will pass the contents of a DCS // sequence to the outer terminal unchanged. // // Here, we split the encoded string into 76 bytes chunks and then // join the chunks with sequences. Finally, // wrap the whole thing in // . // s := strings.SplitN(b64, "", 76) s := make([]string, 0, len(b64)/76+1) for i := 0; i < len(b64); i += 76 { end := i + 76 if end > len(b64) { end = len(b64) } s = append(s, b64[i:end]) } seq.WriteString(strings.Join(s, "\x1b\\\x1bP")) default: seq.WriteString(b64) } case QueryOperation: // OSC52 queries the clipboard using "?" seq.WriteString("?") case ClearOperation: // OSC52 clears the clipboard if the data is neither a base64 string nor "?" // we're using "!" as a default seq.WriteString("!") } // actual OSC52 sequence end seq.WriteString("\x07") // mode escape end seq.WriteString(s.seqEnd()) return seq.String() } // WriteTo writes the OSC52 sequence to the writer. func (s Sequence) WriteTo(out io.Writer) (int64, error) { n, err := out.Write([]byte(s.String())) return int64(n), err } // Mode sets the mode for the OSC52 sequence. func (s Sequence) Mode(m Mode) Sequence { s.mode = m return s } // Tmux sets the mode to TmuxMode. // Used to escape the OSC52 sequence for `tmux`. // // Note: this is not needed if tmux clipboard is set to `set-clipboard on`. If // TmuxMode is used, tmux must have `allow-passthrough on` set. // // This is a syntactic sugar for s.Mode(TmuxMode). func (s Sequence) Tmux() Sequence { return s.Mode(TmuxMode) } // Screen sets the mode to ScreenMode. // Used to escape the OSC52 sequence for `screen`. // // This is a syntactic sugar for s.Mode(ScreenMode). func (s Sequence) Screen() Sequence { return s.Mode(ScreenMode) } // Clipboard sets the clipboard buffer for the OSC52 sequence. func (s Sequence) Clipboard(c Clipboard) Sequence { s.clipboard = c return s } // Primary sets the clipboard buffer to PrimaryClipboard. // This is the X11 primary clipboard. // // This is a syntactic sugar for s.Clipboard(PrimaryClipboard). func (s Sequence) Primary() Sequence { return s.Clipboard(PrimaryClipboard) } // Limit sets the limit for the OSC52 sequence. // The default limit is 0 (no limit). // // Strings longer than the limit get ignored. Settting the limit to 0 or a // negative value disables the limit. Each terminal defines its own escapse // sequence limit. func (s Sequence) Limit(l int) Sequence { if l < 0 { s.limit = 0 } else { s.limit = l } return s } // Operation sets the operation for the OSC52 sequence. // The default operation is SetOperation. func (s Sequence) Operation(o Operation) Sequence { s.op = o return s } // Clear sets the operation to ClearOperation. // This clears the clipboard. // // This is a syntactic sugar for s.Operation(ClearOperation). func (s Sequence) Clear() Sequence { return s.Operation(ClearOperation) } // Query sets the operation to QueryOperation. // This queries the clipboard contents. // // This is a syntactic sugar for s.Operation(QueryOperation). func (s Sequence) Query() Sequence { return s.Operation(QueryOperation) } // SetString sets the string for the OSC52 sequence. Strings are joined with a // space character. func (s Sequence) SetString(strs ...string) Sequence { s.str = strings.Join(strs, " ") return s } // New creates a new OSC52 sequence with the given string(s). Strings are // joined with a space character. func New(strs ...string) Sequence { s := Sequence{ str: strings.Join(strs, " "), limit: 0, mode: DefaultMode, clipboard: SystemClipboard, op: SetOperation, } return s } // Query creates a new OSC52 sequence with the QueryOperation. // This returns a new OSC52 sequence to query the clipboard contents. // // This is a syntactic sugar for New().Query(). func Query() Sequence { return New().Query() } // Clear creates a new OSC52 sequence with the ClearOperation. // This returns a new OSC52 sequence to clear the clipboard. // // This is a syntactic sugar for New().Clear(). func Clear() Sequence { return New().Clear() } func (s Sequence) seqStart() string { switch s.mode { case TmuxMode: // Write the start of a tmux escape sequence. return "\x1bPtmux;\x1b" case ScreenMode: // Write the start of a DCS sequence. return "\x1bP" default: return "" } } func (s Sequence) seqEnd() string { switch s.mode { case TmuxMode: // Terminate the tmux escape sequence. return "\x1b\\" case ScreenMode: // Write the end of a DCS sequence. return "\x1b\x5c" default: return "" } } go-osc52-2.0.1/osc52_test.go000066400000000000000000000123551440137722200154010ustar00rootroot00000000000000package osc52 import ( "bytes" "testing" ) func TestCopy(t *testing.T) { cases := []struct { name string str string clipboard Clipboard mode Mode limit int expected string }{ { name: "hello world", str: "hello world", clipboard: SystemClipboard, mode: DefaultMode, limit: 0, expected: "\x1b]52;c;aGVsbG8gd29ybGQ=\x07", }, { name: "empty string", str: "", clipboard: SystemClipboard, mode: DefaultMode, limit: 0, expected: "\x1b]52;c;\x07", }, { name: "hello world primary", str: "hello world", clipboard: PrimaryClipboard, mode: DefaultMode, limit: 0, expected: "\x1b]52;p;aGVsbG8gd29ybGQ=\x07", }, { name: "hello world tmux mode", str: "hello world", clipboard: SystemClipboard, mode: TmuxMode, limit: 0, expected: "\x1bPtmux;\x1b\x1b]52;c;aGVsbG8gd29ybGQ=\x07\x1b\\", }, { name: "hello world screen mode", str: "hello world", clipboard: SystemClipboard, mode: ScreenMode, limit: 0, expected: "\x1bP\x1b]52;c;aGVsbG8gd29ybGQ=\x07\x1b\\", }, { name: "hello world screen mode longer than 76 bytes string", str: "hello world hello world hello world hello world hello world hello world hello world hello world", clipboard: SystemClipboard, mode: ScreenMode, limit: 0, expected: "\x1bP\x1b]52;c;aGVsbG8gd29ybGQgaGVsbG8gd29ybGQgaGVsbG8gd29ybGQgaGVsbG8gd29ybGQgaGVsbG8gd29y\x1b\\\x1bPbGQgaGVsbG8gd29ybGQgaGVsbG8gd29ybGQgaGVsbG8gd29ybGQ=\a\x1b\\", }, { name: "hello world with limit 11", str: "hello world", clipboard: SystemClipboard, mode: DefaultMode, limit: 11, expected: "\x1b]52;c;aGVsbG8gd29ybGQ=\x07", }, { name: "hello world with limit 10", str: "hello world", clipboard: SystemClipboard, mode: DefaultMode, limit: 10, expected: "", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { s := New(c.str) s = s.Clipboard(c.clipboard) s = s.Mode(c.mode) s = s.Limit(c.limit) if s.String() != c.expected { t.Errorf("expected %q, got %q", c.expected, s.String()) } }) } } func TestQuery(t *testing.T) { cases := []struct { name string mode Mode clipboard Clipboard expected string }{ { name: "query system clipboard", mode: DefaultMode, clipboard: SystemClipboard, expected: "\x1b]52;c;?\x07", }, { name: "query primary clipboard", mode: DefaultMode, clipboard: PrimaryClipboard, expected: "\x1b]52;p;?\x07", }, { name: "query system clipboard tmux mode", mode: TmuxMode, clipboard: SystemClipboard, expected: "\x1bPtmux;\x1b\x1b]52;c;?\x07\x1b\\", }, { name: "query system clipboard screen mode", mode: ScreenMode, clipboard: SystemClipboard, expected: "\x1bP\x1b]52;c;?\x07\x1b\\", }, { name: "query primary clipboard tmux mode", mode: TmuxMode, clipboard: PrimaryClipboard, expected: "\x1bPtmux;\x1b\x1b]52;p;?\x07\x1b\\", }, { name: "query primary clipboard screen mode", mode: ScreenMode, clipboard: PrimaryClipboard, expected: "\x1bP\x1b]52;p;?\x07\x1b\\", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { s := New().Query().Clipboard(c.clipboard).Mode(c.mode) if s.String() != c.expected { t.Errorf("expected %q, got %q", c.expected, s.Query()) } }) } } func TestClear(t *testing.T) { cases := []struct { name string mode Mode clipboard Clipboard expected string }{ { name: "clear system clipboard", mode: DefaultMode, clipboard: SystemClipboard, expected: "\x1b]52;c;!\x07", }, { name: "clear system clipboard tmux mode", mode: TmuxMode, clipboard: SystemClipboard, expected: "\x1bPtmux;\x1b\x1b]52;c;!\x07\x1b\\", }, { name: "clear system clipboard screen mode", mode: ScreenMode, clipboard: SystemClipboard, expected: "\x1bP\x1b]52;c;!\x07\x1b\\", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { s := New().Clear().Clipboard(c.clipboard).Mode(c.mode) if s.String() != c.expected { t.Errorf("expected %q, got %q", c.expected, s.Clear()) } }) } } func TestWriteTo(t *testing.T) { var buf bytes.Buffer cases := []struct { name string str string clipboard Clipboard mode Mode limit int expected string }{ { name: "hello world", str: "hello world", clipboard: SystemClipboard, mode: DefaultMode, limit: 0, expected: "\x1b]52;c;aGVsbG8gd29ybGQ=\x07", }, { name: "empty string", str: "", clipboard: SystemClipboard, mode: DefaultMode, limit: 0, expected: "\x1b]52;c;\x07", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { buf.Reset() s := New(c.str) s.Clipboard(c.clipboard) s.Mode(c.mode) s.Limit(c.limit) if _, err := s.WriteTo(&buf); err != nil { t.Errorf("expected nil, got %v", err) } if buf.String() != c.expected { t.Errorf("expected %q, got %q", c.expected, buf.String()) } }) } }