From 81c9d51b1e8d733c84c3dde5d42205b5d576490d Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 23 Feb 2023 14:35:58 -0500 Subject: [PATCH 1/3] ref: modular api Construct a sequence using `New` or `Sequence{}`. Set the string to be converted to a OSC52 sequence, clipboard, mode, and operation. Mode can be default, tmux, or screen to escape the sequence based on. --- osc52.go | 374 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 226 insertions(+), 148 deletions(-) diff --git a/osc52.go b/osc52.go index 104e4a7..adb5e78 100755 --- a/osc52.go +++ b/osc52.go @@ -22,206 +22,284 @@ // // 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" - "os" "strings" ) // Clipboard is the clipboard buffer to use. -type Clipboard uint +type Clipboard rune const ( // SystemClipboard is the system clipboard buffer. - SystemClipboard Clipboard = iota + SystemClipboard Clipboard = 'c' // PrimaryClipboard is the primary clipboard buffer (X11). - PrimaryClipboard + 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 ) -// String implements the fmt.Stringer interface for [Clipboard]. -func (c Clipboard) String() string { - return []string{ - "c", "p", - }[c] +// Sequence is the OSC52 sequence. +type Sequence struct { + str string + limit int + op Operation + mode Mode + clipboard Clipboard } -// output is the default output for Copy which uses os.Stdout and os.Environ. -var output = NewOutput(os.Stdout, os.Environ()) +var _ fmt.Stringer = (*Sequence)(nil) -// envs is a map of environment variables. -type envs map[string]string +var _ io.WriterTo = (*Sequence)(nil) -// Get returns the value of the environment variable named by the key. -func (e envs) Get(key string) string { - v, ok := e[key] - if !ok { - return "" +// 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("!") } - return v + // actual OSC52 sequence end + seq.WriteString("\x07") + // mode escape end + seq.WriteString(s.seqEnd()) + return seq.String() } -// Output is where the OSC52 string should be written. -type Output struct { - out io.Writer - envs envs +// 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 } -// NewOutput returns a new Output. -func NewOutput(out io.Writer, envs []string) *Output { - e := make(map[string]string, 0) - for _, env := range envs { - s := strings.Split(env, "=") - k := s[0] - v := strings.Join(s[1:], "=") - e[k] = v - } - o := &Output{ - out: out, - envs: e, - } - return o +// Mode sets the mode for the OSC52 sequence. +func (s *Sequence) Mode(m Mode) *Sequence { + s.mode = m + return s } -// DefaultOutput returns the default output for Copy. -func DefaultOutput() *Output { - return output +// 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) } -// Copy copies the OSC52 string to the output. This uses the system clipboard buffer. -func Copy(str string) { - output.Copy(str) +// 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) } -// Copy copies the OSC52 string to the output. This uses the system clipboard buffer. -func (o *Output) Copy(str string) { - o.CopyClipboard(str, SystemClipboard) +// Clipboard sets the clipboard buffer for the OSC52 sequence. +func (s *Sequence) Clipboard(c Clipboard) *Sequence { + s.clipboard = c + return s } -// CopyPrimary copies the OSC52 string to the output. This uses the primary clipboard buffer. -func CopyPrimary(str string) { - output.CopyPrimary(str) +// 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) } -// CopyPrimary copies the OSC52 string to the output. This uses the primary clipboard buffer. -func (o *Output) CopyPrimary(str string) { - o.CopyClipboard(str, 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 } -// CopyClipboard copies the OSC52 string to the output. This uses the passed clipboard buffer. -func CopyClipboard(str string, c Clipboard) { - output.CopyClipboard(str, c) +// 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 } -// CopyClipboard copies the OSC52 string to the output. This uses the passed clipboard buffer. -func (o *Output) CopyClipboard(str string, c Clipboard) { - o.osc52Write(str, c) +// 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) } -func (o *Output) osc52Write(str string, c Clipboard) { - var seq string - term := strings.ToLower(o.envs.Get("TERM")) - switch { - case o.envs.Get("TMUX") != "", strings.HasPrefix(term, "tmux"): - seq = Sequence(str, "tmux", c) - case strings.HasPrefix(term, "screen"): - seq = Sequence(str, "screen", c) - case strings.Contains(term, "kitty"): - // First, we flush the keyboard before copying, this is required for - // Kitty < 0.22.0. - o.out.Write([]byte(Clear(term, c))) - seq = Sequence(str, "kitty", c) - default: - seq = Sequence(str, term, c) - } - o.out.Write([]byte(seq)) +// 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) } -func seqStart(term string, c Clipboard) string { - var seq strings.Builder - switch { - case strings.Contains(term, "tmux"): - // Write the start of a tmux escape sequence. - seq.WriteString("\x1bPtmux;\x1b") - case strings.Contains(term, "screen"): - // Write the start of a DCS sequence. - seq.WriteString("\x1bP") - } - // OSC52 sequence start. - seq.WriteString(fmt.Sprintf("\x1b]52;%s;", c)) - return seq.String() +// 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 } -func seqEnd(term string) string { - var seq strings.Builder - // OSC52 sequence end. - seq.WriteString("\x07") - switch { - case strings.Contains(term, "tmux"): - // Terminate the tmux escape sequence. - seq.WriteString("\x1b\\") - case strings.Contains(term, "screen"): - // Write the end of a DCS sequence. - seq.WriteString("\x1b\x5c") +// 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 seq.String() + return s } -// sequence returns the OSC52 sequence for the passed content. -// Beware that the string here is not base64 encoded. -func sequence(contents string, term string, c Clipboard) string { - var seq strings.Builder - term = strings.ToLower(term) - seq.WriteString(seqStart(term, c)) - switch { - case strings.Contains(term, "screen"): - // 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 := make([]string, 0, len(contents)/76+1) - for i := 0; i < len(contents); i += 76 { - end := i + 76 - if end > len(contents) { - end = len(contents) - } - s = append(s, contents[i:end]) - } - - seq.WriteString(strings.Join(s, "\x1b\\\x1bP")) - default: - seq.WriteString(contents) - } - seq.WriteString(seqEnd(term)) - return seq.String() +// 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() } -// Sequence returns the OSC52 sequence for the given string, terminal, and clipboard choice. -func Sequence(str string, term string, c Clipboard) string { - b64 := base64.StdEncoding.EncodeToString([]byte(str)) - return sequence(b64, term, c) +// 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() } -// Contents returns the contents of the clipboard. -func Contents(term string, c Clipboard) string { - var seq strings.Builder - seq.WriteString(seqStart(term, c)) - seq.WriteString("?") - seq.WriteString(seqEnd(term)) - return seq.String() +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 "" + } } -// Clear returns the OSC52 sequence to clear the clipboard. -func Clear(term string, c Clipboard) string { - var seq strings.Builder - seq.WriteString(seqStart(term, c)) - // Clear the clipboard - seq.WriteString("!") - seq.WriteString(seqEnd(term)) - return seq.String() +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 "" + } } From 89356c4e2e3007664cab19220a462d93118ec1e7 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Sat, 25 Feb 2023 21:38:02 -0500 Subject: [PATCH 2/3] chore: add tests --- osc52_test.go | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 osc52_test.go diff --git a/osc52_test.go b/osc52_test.go new file mode 100644 index 0000000..edebecb --- /dev/null +++ b/osc52_test.go @@ -0,0 +1,227 @@ +package 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.Clipboard(c.clipboard) + s.Mode(c.mode) + 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()) + } + }) + } +} From 0e44c74ffabc321ff53176dda4c78f8e7149640a Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 27 Feb 2023 17:02:14 -0500 Subject: [PATCH 3/3] docs: update docs --- README.md | 88 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 18a3cef..defde50 100644 --- a/README.md +++ b/README.md @@ -8,56 +8,76 @@ A Go library to work with the [ANSI OSC52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) terminal sequence. -## Example +## Usage -```go -str := "Hello World!" -osc52.Copy(str) // Copies str to system clipboard -osc52.CopyPrimary(str) // Copies str to primary clipboard (X11 only) -``` +You can use this small library to construct an ANSI OSC52 sequence suitable for +your terminal. -## SSH Example -You can use this over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) for instance: +### Example ```go -envs := sshSession.Environ() -pty, _, _ := s.Pty() -envs = append(envs, "TERM="+pty.Term) -output := NewOutput(sshSession, envs) -// Copy text in your application -output.Copy("Hello awesome!") -``` +import ( + "os" + "fmt" -If you're using tmux, you could pass the `TMUX` environment variable to help detect tmux: + "github.com/aymanbagabas/go-osc52" +) -```sh -ssh -o SendEnv=TMUX -``` +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) -### Tmux users + // Query the clipboard + osc52.Query().WriteTo(os.Stderr) -If you're using tmux, make sure you set `set -g default-terminal` in your tmux -config, to a value that starts with `tmux-`. `tmux-256color` for instance. See -[this](https://github.com/tmux/tmux/wiki/FAQ#why-do-you-use-the-screen-terminal-description-inside-tmux) -for more details. + // Clear system clipboard + osc52.Clear().WriteTo(os.Stderr) -`go-osc52` will wrap the OSC52 sequence in a `tmux` escape sequence if tmux is -detected. If you're running tmux >= 3.3, OSC52 won't work and you'll need to set -the `set -g allow-passthrough on` in your tmux config. + // Use the fmt.Stringer interface to copy `s` to system clipboard + fmt.Fprint(os.Stderr, osc52.New(s)) -```tmux -set -g allow-passthrough on + // Or to primary clipboard + fmt.Fprint(os.Stderr, osc52.New(s).Primary()) +} ``` -or set `set -g set-clipboard on` in your tmux config and use your outer terminal in your code instead: +## SSH Example + +You can use this over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) for instance: ```go -// Assuming this code is running in tmux >= 3.3 in kitty -seq := osc52.Sequence("Hello awesome!", "xterm-kitty", osc52.ClipboardC) -os.Stderr.WriteString(seq) +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 2.4](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. \ No newline at end of file +* [vim-oscyank](https://github.com/ojroques/vim-oscyank) this is heavily inspired by vim-oscyank.