diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index 2d306a2d..7b680e86 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## Container
+  y: copy container ID
   d: entfernen
   e: hide/show stopped containers
   p: pause
diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md
index 13470d1d..1179da36 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Containers
 
 
+  y: copy container ID
   d: remove
   e: hide/show stopped containers
   p: pause
diff --git a/docs/keybindings/Keybindings_es.md b/docs/keybindings/Keybindings_es.md
index 145c7a22..4c3e0138 100644
--- a/docs/keybindings/Keybindings_es.md
+++ b/docs/keybindings/Keybindings_es.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Contenedores
 
 
+  y: copy container ID
   d: borrar
   e: esconder/mostrar contenedores parados
   p: pausa
diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md
index 260fe0bd..c33a9275 100644
--- a/docs/keybindings/Keybindings_fr.md
+++ b/docs/keybindings/Keybindings_fr.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Conteneurs
 
 
+  y: copy container ID
   d: supprimer
   e: cacher/montrer les conteneurs arrêtés
   p: pause
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index dca57f64..f8963d88 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Containers
 
 
+  y: copy container ID
   d: verwijder
   e: verberg gestopte containers
   p: pause
diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md
index c290fdc2..533d8691 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Kontenery
 
 
+  y: copy container ID
   d: usuń
   e: hide/show stopped containers
   p: pause
diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md
index 73544cc9..76c73583 100644
--- a/docs/keybindings/Keybindings_pt.md
+++ b/docs/keybindings/Keybindings_pt.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Contêineres
 
 
+  y: copy container ID
   d: remover
   e: ocultar/mostrar contêineres parados
   p: pausar
diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md
index 787d7d15..9c012d48 100644
--- a/docs/keybindings/Keybindings_tr.md
+++ b/docs/keybindings/Keybindings_tr.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Konteynerler
 
 
+  y: copy container ID
   d: kaldır
   e: hide/show stopped containers
   p: pause
diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md
index 0e47a44e..57af67a8 100644
--- a/docs/keybindings/Keybindings_zh.md
+++ b/docs/keybindings/Keybindings_zh.md
@@ -16,6 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## 容器
 
 
+  y: copy container ID
   d: 移除
   e: 隐藏/显示已停止的容器
   p: 暂停
diff --git a/go.mod b/go.mod
index f1f302bf..6b679ae0 100644
--- a/go.mod
+++ b/go.mod
@@ -31,6 +31,7 @@ require (
 )
 
 require (
+	github.com/atotto/clipboard v0.1.4 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/containerd/log v0.1.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 05ef54bf..9822f768 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
 github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c h1:YDsGA6tou+tAxVe0Dre29iSbQ8TrWdWfwOisKArJT5E=
 github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 h1:1fx+RA5lk1ZkzPAUP7DEgZnVHYxEcHO77vQO/V8z/2Q=
 github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1/go.mod h1:z0nyIb42Zs97wyX1V+8MbEFhHeTw1OgFQfR6q57ZuHc=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
diff --git a/pkg/commands/os.go b/pkg/commands/os.go
index b8dec4a0..620de164 100644
--- a/pkg/commands/os.go
+++ b/pkg/commands/os.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/go-errors/errors"
 
+	"github.com/atotto/clipboard"
 	"github.com/jesseduffield/kill"
 	"github.com/jesseduffield/lazydocker/pkg/config"
 	"github.com/jesseduffield/lazydocker/pkg/utils"
@@ -373,3 +374,20 @@ func (c *OSCommand) Kill(cmd *exec.Cmd) error {
 func (c *OSCommand) PrepareForChildren(cmd *exec.Cmd) {
 	kill.PrepareForChildren(cmd)
 }
+
+func (c *OSCommand) CopyToClipboard(str string) error {
+	escaped := strings.Replace(str, "\n", "\\n", -1)
+	truncated := utils.TruncateWithEllipsis(escaped, 40)
+
+	c.Log.Debug(utils.ResolvePlaceholderString("Copying '{{str}}' to clipboard", map[string]string{"str": truncated}))
+
+	// Not needed yet
+	//if c.UserConfig().OS.CopyToClipboardCmd != "" {
+	//	cmdStr := utils.ResolvePlaceholderString(c.UserConfig().OS.CopyToClipboardCmd, map[string]string{
+	//		"text": c.Cmd.Quote(str),
+	//	})
+	//	return c.Cmd.NewShell(cmdStr).Run()
+	//}
+
+	return clipboard.WriteAll(str)
+}
diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go
index f806f72c..e6e4ec09 100644
--- a/pkg/gui/app_status_manager.go
+++ b/pkg/gui/app_status_manager.go
@@ -17,6 +17,10 @@ type statusManager struct {
 	statuses []appStatus
 }
 
+const (
+	TickIntervalMs = 50
+)
+
 func (m *statusManager) removeStatus(name string) {
 	newStatuses := []appStatus{}
 	for _, status := range m.statuses {
@@ -27,12 +31,12 @@ func (m *statusManager) removeStatus(name string) {
 	m.statuses = newStatuses
 }
 
-func (m *statusManager) addWaitingStatus(name string) {
+func (m *statusManager) addStatus(name string, statusType string, duration int) {
 	m.removeStatus(name)
 	newStatus := appStatus{
 		name:       name,
-		statusType: "waiting",
-		duration:   0,
+		statusType: statusType,
+		duration:   duration,
 	}
 	m.statuses = append([]appStatus{newStatus}, m.statuses...)
 }
@@ -41,36 +45,26 @@ func (m *statusManager) getStatusString() string {
 	if len(m.statuses) == 0 {
 		return ""
 	}
+
 	topStatus := m.statuses[0]
 	if topStatus.statusType == "waiting" {
 		return topStatus.name + " " + utils.Loader()
+	} else if topStatus.statusType == "info" {
+		return topStatus.name
 	}
+
 	return topStatus.name
 }
 
 // WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
 func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
 	go func() {
-		gui.statusManager.addWaitingStatus(name)
+		go gui.Notify(name, "waiting", 0)()
 
 		defer func() {
 			gui.statusManager.removeStatus(name)
 		}()
 
-		go func() {
-			ticker := time.NewTicker(time.Millisecond * 50)
-			defer ticker.Stop()
-			for range ticker.C {
-				appStatus := gui.statusManager.getStatusString()
-				if appStatus == "" {
-					return
-				}
-				if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
-					gui.Log.Warn(err)
-				}
-			}
-		}()
-
 		if err := f(); err != nil {
 			gui.g.Update(func(g *gocui.Gui) error {
 				return gui.createErrorPanel(err.Error())
@@ -80,3 +74,37 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
 
 	return nil
 }
+
+// Notify sends static notification to the user.
+// duration of 0 will disable the self-cleaning of the notification
+func (gui *Gui) Notify(name string, statusType string, duration int) func() {
+	return func() {
+		gui.statusManager.addStatus(name, statusType, duration)
+
+		defer func() {
+			gui.statusManager.removeStatus(name)
+		}()
+
+		ticker := time.NewTicker(time.Millisecond * TickIntervalMs)
+		tickCount := 0
+		endTick := duration * 1000 / TickIntervalMs
+
+		defer ticker.Stop()
+		for range ticker.C {
+			tickCount++
+			// If no duration, don't terminate early
+			if duration > 0 && tickCount >= endTick {
+				return
+			}
+
+			appStatus := gui.statusManager.getStatusString()
+			if appStatus == "" {
+				return
+			}
+
+			if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
+				gui.Log.Warn(err)
+			}
+		}
+	}
+}
diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go
index 92aa95b3..6006c467 100644
--- a/pkg/gui/containers_panel.go
+++ b/pkg/gui/containers_panel.go
@@ -252,8 +252,10 @@ func (gui *Gui) refreshContainersAndServices() error {
 	}
 
 	// keep track of current service selected so that we can reposition our cursor if it moves position in the list
+	gui.DockerCommand.ServiceMutex.Lock()
 	originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
 	selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
+	gui.DockerCommand.ServiceMutex.Unlock()
 
 	containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
 		gui.Panels.Services.List.GetAllItems(),
@@ -358,6 +360,18 @@ func (gui *Gui) PauseContainer(container *commands.Container) error {
 	})
 }
 
+func (gui *Gui) handleCopyContainerId(g *gocui.Gui, v *gocui.View) error {
+	ctr, err := gui.Panels.Containers.GetSelectedItem()
+	if err != nil {
+		return nil
+	}
+
+	formattedStatusText := fmt.Sprintf("Copied %s to clipboard", utils.TruncateWithEllipsis(ctr.ID, 10))
+	go gui.Notify(formattedStatusText, "info", 3)()
+
+	return gui.OSCommand.CopyToClipboard(ctr.ID)
+}
+
 func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
 	ctr, err := gui.Panels.Containers.GetSelectedItem()
 	if err != nil {
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index df706024..fda0de25 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -185,6 +185,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 			Modifier: gocui.ModNone,
 			Handler:  gui.handleDonate,
 		},
+		{
+			ViewName:    "containers",
+			Key:         'y',
+			Modifier:    gocui.ModNone,
+			Handler:     gui.handleCopyContainerId,
+			Description: gui.Tr.CopyContainerId,
+		},
 		{
 			ViewName:    "containers",
 			Key:         'd',
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index d1ed0320..36ac5e68 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -109,6 +109,7 @@ type TranslationSet struct {
 	FilterList                  string
 	OpenInBrowser               string
 	SortContainersByState       string
+	CopyContainerId             string
 
 	LogsTitle                 string
 	ConfigTitle               string
@@ -216,6 +217,7 @@ func englishSet() TranslationSet {
 		FilterList:                  "filter list",
 		OpenInBrowser:               "open in browser (first port is http)",
 		SortContainersByState:       "sort containers by state",
+		CopyContainerId:             "copy container ID",
 
 		GlobalTitle:               "Global",
 		MainTitle:                 "Main",
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 0cd9a7ca..33b01e49 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -11,6 +11,7 @@ import (
 	"sort"
 	"strings"
 	"time"
+	"unicode"
 
 	"github.com/go-errors/errors"
 	"github.com/jesseduffield/gocui"
@@ -410,3 +411,23 @@ func marshalIntoFormat(data interface{}, format string) ([]byte, error) {
 		return nil, errors.New(fmt.Sprintf("Unsupported detailization format: %s", format))
 	}
 }
+
+func StringWidth(s string) int {
+	// We are intentionally not using a range loop here, because that would
+	// convert the characters to runes, which is unnecessary work in this case.
+	for i := 0; i < len(s); i++ {
+		if s[i] > unicode.MaxASCII {
+			return runewidth.StringWidth(s)
+		}
+	}
+
+	return len(s)
+}
+
+// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
+func TruncateWithEllipsis(str string, limit int) string {
+	if StringWidth(str) > limit && limit <= 2 {
+		return strings.Repeat(".", limit)
+	}
+	return runewidth.Truncate(str, limit, "…")
+}
diff --git a/vendor/github.com/atotto/clipboard/.travis.yml b/vendor/github.com/atotto/clipboard/.travis.yml
new file mode 100644
index 00000000..23f21d83
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/.travis.yml
@@ -0,0 +1,22 @@
+language: go
+
+os:
+ - linux
+ - osx
+ - windows
+
+go:
+ - go1.13.x
+ - go1.x
+
+services:
+ - xvfb
+
+before_install:
+ - export DISPLAY=:99.0
+
+script:
+ - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xsel; fi
+ - go test -v .
+ - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xclip; fi
+ - go test -v .
diff --git a/vendor/github.com/atotto/clipboard/LICENSE b/vendor/github.com/atotto/clipboard/LICENSE
new file mode 100644
index 00000000..dee3257b
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Ato Araki. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of @atotto. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/atotto/clipboard/README.md b/vendor/github.com/atotto/clipboard/README.md
new file mode 100644
index 00000000..41fdd57b
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/README.md
@@ -0,0 +1,48 @@
+[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard)
+
+[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard)
+
+# Clipboard for Go
+
+Provide copying and pasting to the Clipboard for Go.
+
+Build:
+
+    $ go get github.com/atotto/clipboard
+
+Platforms:
+
+* OSX
+* Windows 7 (probably work on other Windows)
+* Linux, Unix (requires 'xclip' or 'xsel' command to be installed)
+
+
+Document: 
+
+* http://godoc.org/github.com/atotto/clipboard
+
+Notes:
+
+* Text string only
+* UTF-8 text encoding only (no conversion)
+
+TODO:
+
+* Clipboard watcher(?)
+
+## Commands:
+
+paste shell command:
+
+    $ go get github.com/atotto/clipboard/cmd/gopaste
+    $ # example:
+    $ gopaste > document.txt
+
+copy shell command:
+
+    $ go get github.com/atotto/clipboard/cmd/gocopy
+    $ # example:
+    $ cat document.txt | gocopy
+
+
+
diff --git a/vendor/github.com/atotto/clipboard/clipboard.go b/vendor/github.com/atotto/clipboard/clipboard.go
new file mode 100644
index 00000000..d7907d3a
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard.go
@@ -0,0 +1,20 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package clipboard read/write on clipboard
+package clipboard
+
+// ReadAll read string from clipboard
+func ReadAll() (string, error) {
+	return readAll()
+}
+
+// WriteAll write string to clipboard
+func WriteAll(text string) error {
+	return writeAll(text)
+}
+
+// Unsupported might be set true during clipboard init, to help callers decide
+// whether or not to offer clipboard options.
+var Unsupported bool
diff --git a/vendor/github.com/atotto/clipboard/clipboard_darwin.go b/vendor/github.com/atotto/clipboard/clipboard_darwin.go
new file mode 100644
index 00000000..6f33078d
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_darwin.go
@@ -0,0 +1,52 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package clipboard
+
+import (
+	"os/exec"
+)
+
+var (
+	pasteCmdArgs = "pbpaste"
+	copyCmdArgs  = "pbcopy"
+)
+
+func getPasteCommand() *exec.Cmd {
+	return exec.Command(pasteCmdArgs)
+}
+
+func getCopyCommand() *exec.Cmd {
+	return exec.Command(copyCmdArgs)
+}
+
+func readAll() (string, error) {
+	pasteCmd := getPasteCommand()
+	out, err := pasteCmd.Output()
+	if err != nil {
+		return "", err
+	}
+	return string(out), nil
+}
+
+func writeAll(text string) error {
+	copyCmd := getCopyCommand()
+	in, err := copyCmd.StdinPipe()
+	if err != nil {
+		return err
+	}
+
+	if err := copyCmd.Start(); err != nil {
+		return err
+	}
+	if _, err := in.Write([]byte(text)); err != nil {
+		return err
+	}
+	if err := in.Close(); err != nil {
+		return err
+	}
+	return copyCmd.Wait()
+}
diff --git a/vendor/github.com/atotto/clipboard/clipboard_plan9.go b/vendor/github.com/atotto/clipboard/clipboard_plan9.go
new file mode 100644
index 00000000..9d2fef4e
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_plan9.go
@@ -0,0 +1,42 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build plan9
+
+package clipboard
+
+import (
+	"os"
+	"io/ioutil"
+)
+
+func readAll() (string, error) {
+	f, err := os.Open("/dev/snarf")
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+
+	str, err := ioutil.ReadAll(f)
+	if err != nil {
+		return "", err
+	}
+	
+	return string(str), nil
+}
+
+func writeAll(text string) error {
+	f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	
+	_, err = f.Write([]byte(text))
+	if err != nil {
+		return err
+	}
+	
+	return nil
+}
diff --git a/vendor/github.com/atotto/clipboard/clipboard_unix.go b/vendor/github.com/atotto/clipboard/clipboard_unix.go
new file mode 100644
index 00000000..d9f6a561
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_unix.go
@@ -0,0 +1,149 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build freebsd linux netbsd openbsd solaris dragonfly
+
+package clipboard
+
+import (
+	"errors"
+	"os"
+	"os/exec"
+)
+
+const (
+	xsel               = "xsel"
+	xclip              = "xclip"
+	powershellExe      = "powershell.exe"
+	clipExe            = "clip.exe"
+	wlcopy             = "wl-copy"
+	wlpaste            = "wl-paste"
+	termuxClipboardGet = "termux-clipboard-get"
+	termuxClipboardSet = "termux-clipboard-set"
+)
+
+var (
+	Primary bool
+	trimDos bool
+
+	pasteCmdArgs []string
+	copyCmdArgs  []string
+
+	xselPasteArgs = []string{xsel, "--output", "--clipboard"}
+	xselCopyArgs  = []string{xsel, "--input", "--clipboard"}
+
+	xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
+	xclipCopyArgs  = []string{xclip, "-in", "-selection", "clipboard"}
+
+	powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"}
+	clipExeCopyArgs        = []string{clipExe}
+
+	wlpasteArgs = []string{wlpaste, "--no-newline"}
+	wlcopyArgs  = []string{wlcopy}
+
+	termuxPasteArgs = []string{termuxClipboardGet}
+	termuxCopyArgs  = []string{termuxClipboardSet}
+
+	missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.")
+)
+
+func init() {
+	if os.Getenv("WAYLAND_DISPLAY") != "" {
+		pasteCmdArgs = wlpasteArgs
+		copyCmdArgs = wlcopyArgs
+
+		if _, err := exec.LookPath(wlcopy); err == nil {
+			if _, err := exec.LookPath(wlpaste); err == nil {
+				return
+			}
+		}
+	}
+
+	pasteCmdArgs = xclipPasteArgs
+	copyCmdArgs = xclipCopyArgs
+
+	if _, err := exec.LookPath(xclip); err == nil {
+		return
+	}
+
+	pasteCmdArgs = xselPasteArgs
+	copyCmdArgs = xselCopyArgs
+
+	if _, err := exec.LookPath(xsel); err == nil {
+		return
+	}
+
+	pasteCmdArgs = termuxPasteArgs
+	copyCmdArgs = termuxCopyArgs
+
+	if _, err := exec.LookPath(termuxClipboardSet); err == nil {
+		if _, err := exec.LookPath(termuxClipboardGet); err == nil {
+			return
+		}
+	}
+
+	pasteCmdArgs = powershellExePasteArgs
+	copyCmdArgs = clipExeCopyArgs
+	trimDos = true
+
+	if _, err := exec.LookPath(clipExe); err == nil {
+		if _, err := exec.LookPath(powershellExe); err == nil {
+			return
+		}
+	}
+
+	Unsupported = true
+}
+
+func getPasteCommand() *exec.Cmd {
+	if Primary {
+		pasteCmdArgs = pasteCmdArgs[:1]
+	}
+	return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...)
+}
+
+func getCopyCommand() *exec.Cmd {
+	if Primary {
+		copyCmdArgs = copyCmdArgs[:1]
+	}
+	return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...)
+}
+
+func readAll() (string, error) {
+	if Unsupported {
+		return "", missingCommands
+	}
+	pasteCmd := getPasteCommand()
+	out, err := pasteCmd.Output()
+	if err != nil {
+		return "", err
+	}
+	result := string(out)
+	if trimDos && len(result) > 1 {
+		result = result[:len(result)-2]
+	}
+	return result, nil
+}
+
+func writeAll(text string) error {
+	if Unsupported {
+		return missingCommands
+	}
+	copyCmd := getCopyCommand()
+	in, err := copyCmd.StdinPipe()
+	if err != nil {
+		return err
+	}
+
+	if err := copyCmd.Start(); err != nil {
+		return err
+	}
+	if _, err := in.Write([]byte(text)); err != nil {
+		return err
+	}
+	if err := in.Close(); err != nil {
+		return err
+	}
+	return copyCmd.Wait()
+}
diff --git a/vendor/github.com/atotto/clipboard/clipboard_windows.go b/vendor/github.com/atotto/clipboard/clipboard_windows.go
new file mode 100644
index 00000000..253bb932
--- /dev/null
+++ b/vendor/github.com/atotto/clipboard/clipboard_windows.go
@@ -0,0 +1,157 @@
+// Copyright 2013 @atotto. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build windows
+
+package clipboard
+
+import (
+	"runtime"
+	"syscall"
+	"time"
+	"unsafe"
+)
+
+const (
+	cfUnicodetext = 13
+	gmemMoveable  = 0x0002
+)
+
+var (
+	user32                     = syscall.MustLoadDLL("user32")
+	isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
+	openClipboard              = user32.MustFindProc("OpenClipboard")
+	closeClipboard             = user32.MustFindProc("CloseClipboard")
+	emptyClipboard             = user32.MustFindProc("EmptyClipboard")
+	getClipboardData           = user32.MustFindProc("GetClipboardData")
+	setClipboardData           = user32.MustFindProc("SetClipboardData")
+
+	kernel32     = syscall.NewLazyDLL("kernel32")
+	globalAlloc  = kernel32.NewProc("GlobalAlloc")
+	globalFree   = kernel32.NewProc("GlobalFree")
+	globalLock   = kernel32.NewProc("GlobalLock")
+	globalUnlock = kernel32.NewProc("GlobalUnlock")
+	lstrcpy      = kernel32.NewProc("lstrcpyW")
+)
+
+// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
+func waitOpenClipboard() error {
+	started := time.Now()
+	limit := started.Add(time.Second)
+	var r uintptr
+	var err error
+	for time.Now().Before(limit) {
+		r, _, err = openClipboard.Call(0)
+		if r != 0 {
+			return nil
+		}
+		time.Sleep(time.Millisecond)
+	}
+	return err
+}
+
+func readAll() (string, error) {
+	// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
+	// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+	if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
+		return "", err
+	}
+	err := waitOpenClipboard()
+	if err != nil {
+		return "", err
+	}
+
+	h, _, err := getClipboardData.Call(cfUnicodetext)
+	if h == 0 {
+		_, _, _ = closeClipboard.Call()
+		return "", err
+	}
+
+	l, _, err := globalLock.Call(h)
+	if l == 0 {
+		_, _, _ = closeClipboard.Call()
+		return "", err
+	}
+
+	text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
+
+	r, _, err := globalUnlock.Call(h)
+	if r == 0 {
+		_, _, _ = closeClipboard.Call()
+		return "", err
+	}
+
+	closed, _, err := closeClipboard.Call()
+	if closed == 0 {
+		return "", err
+	}
+	return text, nil
+}
+
+func writeAll(text string) error {
+	// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
+	// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	err := waitOpenClipboard()
+	if err != nil {
+		return err
+	}
+
+	r, _, err := emptyClipboard.Call(0)
+	if r == 0 {
+		_, _, _ = closeClipboard.Call()
+		return err
+	}
+
+	data := syscall.StringToUTF16(text)
+
+	// "If the hMem parameter identifies a memory object, the object must have
+	// been allocated using the function with the GMEM_MOVEABLE flag."
+	h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
+	if h == 0 {
+		_, _, _ = closeClipboard.Call()
+		return err
+	}
+	defer func() {
+		if h != 0 {
+			globalFree.Call(h)
+		}
+	}()
+
+	l, _, err := globalLock.Call(h)
+	if l == 0 {
+		_, _, _ = closeClipboard.Call()
+		return err
+	}
+
+	r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
+	if r == 0 {
+		_, _, _ = closeClipboard.Call()
+		return err
+	}
+
+	r, _, err = globalUnlock.Call(h)
+	if r == 0 {
+		if err.(syscall.Errno) != 0 {
+			_, _, _ = closeClipboard.Call()
+			return err
+		}
+	}
+
+	r, _, err = setClipboardData.Call(cfUnicodetext, h)
+	if r == 0 {
+		_, _, _ = closeClipboard.Call()
+		return err
+	}
+	h = 0 // suppress deferred cleanup
+	closed, _, err := closeClipboard.Call()
+	if closed == 0 {
+		return err
+	}
+	return nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 511cfdbc..31207b1d 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -8,6 +8,9 @@ github.com/Microsoft/go-winio/pkg/guid
 # github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
 ## explicit
 github.com/OpenPeeDeeP/xdg
+# github.com/atotto/clipboard v0.1.4
+## explicit
+github.com/atotto/clipboard
 # github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1
 ## explicit
 github.com/boz/go-throttle