From 70856b683c9f8dae4b5db753fd22153e1b9890c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=89AU?= Date: Sat, 5 Oct 2024 19:29:05 +0200 Subject: [PATCH 1/3] Add clipboard support & feature to copy container id --- go.mod | 1 + go.sum | 2 + pkg/commands/os.go | 10 ++ pkg/gui/app_status_manager.go | 20 ++- pkg/gui/containers_panel.go | 14 ++ pkg/gui/gui.go | 9 +- pkg/gui/keybindings.go | 7 + pkg/i18n/english.go | 4 + pkg/utils/utils.go | 21 +++ .../github.com/atotto/clipboard/.travis.yml | 22 +++ vendor/github.com/atotto/clipboard/LICENSE | 27 +++ vendor/github.com/atotto/clipboard/README.md | 48 ++++++ .../github.com/atotto/clipboard/clipboard.go | 20 +++ .../atotto/clipboard/clipboard_darwin.go | 52 ++++++ .../atotto/clipboard/clipboard_plan9.go | 42 +++++ .../atotto/clipboard/clipboard_unix.go | 149 +++++++++++++++++ .../atotto/clipboard/clipboard_windows.go | 157 ++++++++++++++++++ vendor/modules.txt | 3 + 18 files changed, 604 insertions(+), 4 deletions(-) create mode 100644 vendor/github.com/atotto/clipboard/.travis.yml create mode 100644 vendor/github.com/atotto/clipboard/LICENSE create mode 100644 vendor/github.com/atotto/clipboard/README.md create mode 100644 vendor/github.com/atotto/clipboard/clipboard.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_darwin.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_plan9.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_unix.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_windows.go 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..5128b1bf 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,12 @@ 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})) + + return clipboard.WriteAll(str) +} diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index f806f72c..6a967a0c 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -1,6 +1,7 @@ package gui import ( + "sync" "time" "github.com/jesseduffield/gocui" @@ -15,10 +16,15 @@ type appStatus struct { type statusManager struct { statuses []appStatus + lock *sync.Mutex } func (m *statusManager) removeStatus(name string) { newStatuses := []appStatus{} + + m.lock.Lock() + defer m.lock.Unlock() + for _, status := range m.statuses { if status.name != name { newStatuses = append(newStatuses, status) @@ -28,9 +34,13 @@ func (m *statusManager) removeStatus(name string) { } func (m *statusManager) addWaitingStatus(name string) { + m.lock.Lock() + defer m.lock.Unlock() + m.removeStatus(name) newStatus := appStatus{ - name: name, + name: name, + //TODO: add a different enum for information statuses statusType: "waiting", duration: 0, } @@ -38,6 +48,9 @@ func (m *statusManager) addWaitingStatus(name string) { } func (m *statusManager) getStatusString() string { + m.lock.Lock() + defer m.lock.Unlock() + if len(m.statuses) == 0 { return "" } @@ -48,6 +61,11 @@ func (m *statusManager) getStatusString() string { return topStatus.name } +// WithStaticWaitingStatus shows a waiting status for a specific duration +func (gui *Gui) WithStaticWaitingStatus(name string, duration time.Duration) error { + return gui.WithWaitingStatus(name, func() error { time.Sleep(duration); return nil }) +} + // 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() { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 92aa95b3..f8dc9b08 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -358,6 +358,20 @@ 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 + } + + err = gui.WithStaticWaitingStatus(fmt.Sprintf(gui.Tr.CopyContainerIdStatus, utils.TruncateWithEllipsis(ctr.ID, 10)), time.Second*2) + if err != nil { + return err + } + + 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/gui.go b/pkg/gui/gui.go index fa6199f3..8bfebf76 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -4,6 +4,7 @@ import ( "context" "os" "strings" + "sync" "time" "github.com/docker/docker/api/types/events" @@ -146,9 +147,11 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand State: initialState, Config: config, Tr: tr, - statusManager: &statusManager{}, - taskManager: tasks.NewTaskManager(log, tr), - ErrorChan: errorChan, + statusManager: &statusManager{ + lock: &sync.Mutex{}, + }, + taskManager: tasks.NewTaskManager(log, tr), + ErrorChan: errorChan, } deadlock.Opts.Disable = !gui.Config.Debug diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df706024..56c37845 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: gocui.KeyCtrlO, + 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..88828752 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -67,6 +67,8 @@ type TranslationSet struct { ViewLogs string UpProject string DownProject string + CopyContainerId string + CopyContainerIdStatus string ServicesTitle string ContainersTitle string StandaloneContainersTitle string @@ -197,6 +199,8 @@ func englishSet() TranslationSet { ViewLogs: "view logs", UpProject: "up project", DownProject: "down project", + CopyContainerId: "copy container id", + CopyContainerIdStatus: "Copied %s to clipboard", RemoveImage: "remove image", RemoveVolume: "remove volume", RemoveNetwork: "remove network", 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 From 59f39d76ccc929769222d2d2236247d1ed8cb2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=89AU?= Date: Mon, 14 Oct 2024 22:56:18 +0200 Subject: [PATCH 2/3] add cheatcheat gofumpt --- docs/keybindings/Keybindings_de.md | 1 + docs/keybindings/Keybindings_en.md | 1 + docs/keybindings/Keybindings_es.md | 1 + docs/keybindings/Keybindings_fr.md | 1 + docs/keybindings/Keybindings_nl.md | 1 + docs/keybindings/Keybindings_pl.md | 1 + docs/keybindings/Keybindings_pt.md | 1 + docs/keybindings/Keybindings_tr.md | 1 + docs/keybindings/Keybindings_zh.md | 1 + pkg/gui/app_status_manager.go | 3 +-- 10 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index 2d306a2d..da130058 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
+  : 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..09679c2d 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
 
 
+  : 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..02065ea0 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
 
 
+  : 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..c69006ce 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
 
 
+  : 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..d17b91d3 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
 
 
+  : 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..974c52aa 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
 
 
+  : 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..079be7cd 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
 
 
+  : 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..e9f5e1e2 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
 
 
+  : 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..1c3553c0 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
 ## 容器
 
 
+  : copy container id
   d: 移除
   e: 隐藏/显示已停止的容器
   p: 暂停
diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go
index 6a967a0c..f58b6915 100644
--- a/pkg/gui/app_status_manager.go
+++ b/pkg/gui/app_status_manager.go
@@ -39,8 +39,7 @@ func (m *statusManager) addWaitingStatus(name string) {
 
 	m.removeStatus(name)
 	newStatus := appStatus{
-		name: name,
-		//TODO: add a different enum for information statuses
+		name:       name,
 		statusType: "waiting",
 		duration:   0,
 	}

From 70e96c6a49030be6d461b63c9fbfaf4e7d92350b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=89AU?= 
Date: Sat, 3 May 2025 20:32:36 +0200
Subject: [PATCH 3/3] Rework static notification handling

---
 docs/keybindings/Keybindings_de.md |  2 +-
 docs/keybindings/Keybindings_en.md |  2 +-
 docs/keybindings/Keybindings_es.md |  2 +-
 docs/keybindings/Keybindings_fr.md |  2 +-
 docs/keybindings/Keybindings_nl.md |  2 +-
 docs/keybindings/Keybindings_pl.md |  2 +-
 docs/keybindings/Keybindings_pt.md |  2 +-
 docs/keybindings/Keybindings_tr.md |  2 +-
 docs/keybindings/Keybindings_zh.md |  2 +-
 pkg/commands/os.go                 |  8 +++
 pkg/gui/app_status_manager.go      | 81 +++++++++++++++++-------------
 pkg/gui/containers_panel.go        |  8 +--
 pkg/gui/gui.go                     |  9 ++--
 pkg/gui/keybindings.go             |  2 +-
 pkg/i18n/english.go                |  6 +--
 15 files changed, 73 insertions(+), 59 deletions(-)

diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md
index da130058..7b680e86 100644
--- a/docs/keybindings/Keybindings_de.md
+++ b/docs/keybindings/Keybindings_de.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Container
 
 
-  : copy container id
+  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 09679c2d..1179da36 100644
--- a/docs/keybindings/Keybindings_en.md
+++ b/docs/keybindings/Keybindings_en.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Containers
 
 
-  : copy container id
+  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 02065ea0..4c3e0138 100644
--- a/docs/keybindings/Keybindings_es.md
+++ b/docs/keybindings/Keybindings_es.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Contenedores
 
 
-  : copy container id
+  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 c69006ce..c33a9275 100644
--- a/docs/keybindings/Keybindings_fr.md
+++ b/docs/keybindings/Keybindings_fr.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Conteneurs
 
 
-  : copy container id
+  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 d17b91d3..f8963d88 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Containers
 
 
-  : copy container id
+  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 974c52aa..533d8691 100644
--- a/docs/keybindings/Keybindings_pl.md
+++ b/docs/keybindings/Keybindings_pl.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Kontenery
 
 
-  : copy container id
+  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 079be7cd..76c73583 100644
--- a/docs/keybindings/Keybindings_pt.md
+++ b/docs/keybindings/Keybindings_pt.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Contêineres
 
 
-  : copy container id
+  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 e9f5e1e2..9c012d48 100644
--- a/docs/keybindings/Keybindings_tr.md
+++ b/docs/keybindings/Keybindings_tr.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## Konteynerler
 
 
-  : copy container id
+  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 1c3553c0..57af67a8 100644
--- a/docs/keybindings/Keybindings_zh.md
+++ b/docs/keybindings/Keybindings_zh.md
@@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 ## 容器
 
 
-  : copy container id
+  y: copy container ID
   d: 移除
   e: 隐藏/显示已停止的容器
   p: 暂停
diff --git a/pkg/commands/os.go b/pkg/commands/os.go
index 5128b1bf..620de164 100644
--- a/pkg/commands/os.go
+++ b/pkg/commands/os.go
@@ -381,5 +381,13 @@ func (c *OSCommand) CopyToClipboard(str string) error {
 
 	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 f58b6915..e6e4ec09 100644
--- a/pkg/gui/app_status_manager.go
+++ b/pkg/gui/app_status_manager.go
@@ -1,7 +1,6 @@
 package gui
 
 import (
-	"sync"
 	"time"
 
 	"github.com/jesseduffield/gocui"
@@ -16,15 +15,14 @@ type appStatus struct {
 
 type statusManager struct {
 	statuses []appStatus
-	lock     *sync.Mutex
 }
 
+const (
+	TickIntervalMs = 50
+)
+
 func (m *statusManager) removeStatus(name string) {
 	newStatuses := []appStatus{}
-
-	m.lock.Lock()
-	defer m.lock.Unlock()
-
 	for _, status := range m.statuses {
 		if status.name != name {
 			newStatuses = append(newStatuses, status)
@@ -33,61 +31,40 @@ func (m *statusManager) removeStatus(name string) {
 	m.statuses = newStatuses
 }
 
-func (m *statusManager) addWaitingStatus(name string) {
-	m.lock.Lock()
-	defer m.lock.Unlock()
-
+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...)
 }
 
 func (m *statusManager) getStatusString() string {
-	m.lock.Lock()
-	defer m.lock.Unlock()
-
 	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
-}
 
-// WithStaticWaitingStatus shows a waiting status for a specific duration
-func (gui *Gui) WithStaticWaitingStatus(name string, duration time.Duration) error {
-	return gui.WithWaitingStatus(name, func() error { time.Sleep(duration); return nil })
+	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())
@@ -97,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 f8dc9b08..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(),
@@ -364,10 +366,8 @@ func (gui *Gui) handleCopyContainerId(g *gocui.Gui, v *gocui.View) error {
 		return nil
 	}
 
-	err = gui.WithStaticWaitingStatus(fmt.Sprintf(gui.Tr.CopyContainerIdStatus, utils.TruncateWithEllipsis(ctr.ID, 10)), time.Second*2)
-	if err != nil {
-		return err
-	}
+	formattedStatusText := fmt.Sprintf("Copied %s to clipboard", utils.TruncateWithEllipsis(ctr.ID, 10))
+	go gui.Notify(formattedStatusText, "info", 3)()
 
 	return gui.OSCommand.CopyToClipboard(ctr.ID)
 }
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 8bfebf76..fa6199f3 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"os"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/docker/docker/api/types/events"
@@ -147,11 +146,9 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
 		State:         initialState,
 		Config:        config,
 		Tr:            tr,
-		statusManager: &statusManager{
-			lock: &sync.Mutex{},
-		},
-		taskManager: tasks.NewTaskManager(log, tr),
-		ErrorChan:   errorChan,
+		statusManager: &statusManager{},
+		taskManager:   tasks.NewTaskManager(log, tr),
+		ErrorChan:     errorChan,
 	}
 
 	deadlock.Opts.Disable = !gui.Config.Debug
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 56c37845..fda0de25 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -187,7 +187,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 		},
 		{
 			ViewName:    "containers",
-			Key:         gocui.KeyCtrlO,
+			Key:         'y',
 			Modifier:    gocui.ModNone,
 			Handler:     gui.handleCopyContainerId,
 			Description: gui.Tr.CopyContainerId,
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 88828752..36ac5e68 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -67,8 +67,6 @@ type TranslationSet struct {
 	ViewLogs                    string
 	UpProject                   string
 	DownProject                 string
-	CopyContainerId             string
-	CopyContainerIdStatus       string
 	ServicesTitle               string
 	ContainersTitle             string
 	StandaloneContainersTitle   string
@@ -111,6 +109,7 @@ type TranslationSet struct {
 	FilterList                  string
 	OpenInBrowser               string
 	SortContainersByState       string
+	CopyContainerId             string
 
 	LogsTitle                 string
 	ConfigTitle               string
@@ -199,8 +198,6 @@ func englishSet() TranslationSet {
 		ViewLogs:                    "view logs",
 		UpProject:                   "up project",
 		DownProject:                 "down project",
-		CopyContainerId:             "copy container id",
-		CopyContainerIdStatus:       "Copied %s to clipboard",
 		RemoveImage:                 "remove image",
 		RemoveVolume:                "remove volume",
 		RemoveNetwork:               "remove network",
@@ -220,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",