copy container id/ image name

This commit is contained in:
tkkkd 2026-05-05 18:17:00 +09:00
parent 7e7aadc207
commit 1cbd2582dd
24 changed files with 294 additions and 1 deletions

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: zeige Protokolle
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>y</kbd>: Container-ID kopieren
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>y</kbd>: Image-Namen kopieren
<kbd>d</kbd>: entferne Image
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: fokussieren aufs Hauptpanel

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view logs
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: run predefined custom command
<kbd>y</kbd>: copy container id
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus main panel
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: run predefined custom command
<kbd>y</kbd>: copy image name
<kbd>d</kbd>: remove image
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus main panel

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: ver logs
<kbd>E</kbd>: ejecutar shell
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>y</kbd>: Copiar ID del contenedor
<kbd>b</kbd>: ver comandos masivos
<kbd>w</kbd>: abrir en navegador (first port is http)
<kbd>enter</kbd>: enfocar panel principal
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>y</kbd>: Copiar nombre de la imagen
<kbd>d</kbd>: limpiar imagen
<kbd>b</kbd>: ver comandos masivos
<kbd>enter</kbd>: enfocar panel principal

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: voir les enregistrements
<kbd>E</kbd>: exécuter le shell
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>y</kbd>: Copier l'ID du conteneur
<kbd>b</kbd>: voir les commandes groupées
<kbd>w</kbd>: ouvrir dans le navigateur (le premier port est http)
<kbd>enter</kbd>: focus panneau principal
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>y</kbd>: Copier le nom de l'image
<kbd>d</kbd>: supprimer l'image
<kbd>b</kbd>: voir les commandes groupées
<kbd>enter</kbd>: focus panneau principal

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: bekijk logs
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>y</kbd>: Kopieer container-id
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus hoofdpaneel
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>y</kbd>: Kopieer image-naam
<kbd>d</kbd>: verwijder image
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus hoofdpaneel

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: pokaż logi
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>y</kbd>: Kopiuj identyfikator kontenera
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: skup na głównym panelu
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>y</kbd>: Kopiuj nazwę obrazu
<kbd>d</kbd>: usuń obraz
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: skup na głównym panelu

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: ver logs
<kbd>E</kbd>: executar shell
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>y</kbd>: Copiar ID do contêiner
<kbd>b</kbd>: ver comandos em massa
<kbd>w</kbd>: abrir no navegador (primeira porta é http)
<kbd>enter</kbd>: focar no painel principal
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>y</kbd>: Copiar nome da imagem
<kbd>d</kbd>: remover imagem
<kbd>b</kbd>: ver comandos em massa
<kbd>enter</kbd>: focar no painel principal

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: kayıt defterini görüntüle
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>y</kbd>: Konteyner kimliğini kopyala
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: ana panele odaklan
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>y</kbd>: Görüntü adını kopyala
<kbd>d</kbd>: imajı kaldır
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: ana panele odaklan

View file

@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: 查看日志
<kbd>E</kbd>: 执行shell
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>y</kbd>: 复制容器 ID
<kbd>b</kbd>: 查看批量命令
<kbd>w</kbd>: 在浏览器中打开(第一个端口为http)
<kbd>enter</kbd>: 聚焦主面板
@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>y</kbd>: 复制镜像名称
<kbd>d</kbd>: 移除镜像
<kbd>b</kbd>: 查看批量命令
<kbd>enter</kbd>: 聚焦主面板

78
pkg/commands/clipboard.go Normal file
View file

@ -0,0 +1,78 @@
package commands
import (
"context"
"fmt"
"io"
"os/exec"
"time"
)
const clipboardTimeout = 1500 * time.Millisecond
// CopyToClipboard copies the given string to the system clipboard.
func (c *OSCommand) CopyToClipboard(s string) error {
if s == "" {
return fmt.Errorf("nothing to copy")
}
switch c.Platform.os {
case "darwin":
ctx, cancel := context.WithTimeout(context.Background(), clipboardTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "pbcopy")
return runCmdWithInput(cmd, s)
case "linux":
if p, err := exec.LookPath("wl-copy"); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), clipboardTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, p)
return runCmdWithInput(cmd, s)
}
if p, err := exec.LookPath("xclip"); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), clipboardTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, p, "-selection", "clipboard")
return runCmdWithInput(cmd, s)
}
if p, err := exec.LookPath("xsel"); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), clipboardTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, p, "--clipboard", "--input")
return runCmdWithInput(cmd, s)
}
return fmt.Errorf("no clipboard utility found: try installing wl-clipboard (wl-copy) or xclip/xsel")
case "windows":
ctx, cancel := context.WithTimeout(context.Background(), clipboardTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command", "Set-Clipboard")
return runCmdWithInput(cmd, s)
default:
return fmt.Errorf("unsupported platform: %s", c.Platform.os)
}
}
func runCmdWithInput(cmd *exec.Cmd, s string) error {
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
_ = stdin.Close()
return err
}
if _, err := io.WriteString(stdin, s); err != nil {
_ = cmd.Process.Kill()
_ = stdin.Close()
return err
}
if err := stdin.Close(); err != nil {
_ = cmd.Process.Kill()
return err
}
return cmd.Wait()
}

View file

@ -37,6 +37,23 @@ func (m *statusManager) addWaitingStatus(name string) {
m.statuses = append([]appStatus{newStatus}, m.statuses...)
}
// addMessage adds a transient message status that will be removed after the given duration.
func (m *statusManager) addMessage(name string, duration time.Duration) {
m.removeStatus(name)
newStatus := appStatus{
name: name,
statusType: "message",
duration: int(duration / time.Millisecond),
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
// schedule removal after duration
go func() {
time.Sleep(duration)
m.removeStatus(name)
}()
}
func (m *statusManager) getStatusString() string {
if len(m.statuses) == 0 {
return ""
@ -80,3 +97,30 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
return nil
}
// AddMessage shows a transient message in the appStatus area for the given duration.
func (gui *Gui) AddMessage(name string, duration time.Duration) {
if gui.statusManager == nil {
return
}
gui.statusManager.addMessage(name, duration)
// ensure the UI is updated immediately
go func() {
// repeatedly render until the message expires so layout picks it up
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
start := time.Now()
for range ticker.C {
if time.Since(start) > duration+time.Millisecond*100 {
return
}
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
return
}
if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
gui.Log.Warn(err)
}
}
}()
}

45
pkg/gui/clipboard.go Normal file
View file

@ -0,0 +1,45 @@
package gui
import (
"fmt"
"time"
)
// CopyToClipboard copies s to the system clipboard and shows a temporary status message
func (gui *Gui) CopyToClipboard(s string) error {
if err := gui.OSCommand.CopyToClipboard(s); err != nil {
return err
}
// show a short status message in the appStatus area
// try to be slightly more informative if possible
msg := gui.Tr.CopiedFmt
copiedContent := ""
if len(s) > 0 {
// show up to 50 chars of the copied content
short := s
if len(short) > 50 {
short = short[:50] + "..."
}
copiedContent = short
}
// format using translation (if CopiedFmt is empty fall back)
if gui.Tr.CopiedFmt != "" {
if copiedContent != "" {
msg = fmt.Sprintf(gui.Tr.CopiedFmt, copiedContent)
} else {
msg = fmt.Sprintf(gui.Tr.CopiedFmt, "")
}
} else {
if copiedContent != "" {
msg = "Copied: " + copiedContent
} else {
msg = "Copied to clipboard"
}
}
// show the message through statusManager so it doesn't get clobbered by other status updates
gui.AddMessage(msg, 2*time.Second)
return nil
}

View file

@ -453,6 +453,24 @@ func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) handleCopyContainerID(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
id := ctr.ID
if len(id) >= 12 {
id = id[:12]
}
if err := gui.CopyToClipboard(id); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
}
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {

View file

@ -219,3 +219,29 @@ func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}
func (gui *Gui) handleCopyImageName(g *gocui.Gui, v *gocui.View) error {
img, err := gui.Panels.Images.GetSelectedItem()
if err != nil {
return nil
}
name := img.Name
if img.Tag != "" {
name = name + ":" + img.Tag
}
if name == "none" || name == ":" {
if len(img.ID) >= 12 {
name = img.ID[:12]
} else {
name = img.ID
}
}
if err := gui.CopyToClipboard(name); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
}

View file

@ -248,6 +248,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainersCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "containers",
Key: 'y',
Modifier: gocui.ModNone,
Handler: gui.handleCopyContainerID,
Description: gui.Tr.CopyContainerID,
},
{
ViewName: "containers",
Key: 'b',
@ -374,6 +381,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleImagesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "images",
Key: 'y',
Modifier: gocui.ModNone,
Handler: gui.handleCopyImageName,
Description: gui.Tr.CopyImageName,
},
{
ViewName: "images",
Key: 'd',

View file

@ -131,6 +131,11 @@ func chineseSet() TranslationSet {
LcNextScreenMode: "下一个屏幕模式(正常/半屏/全屏)",
LcPrevScreenMode: "上一个屏幕模式",
FilterPrompt: "筛选",
// copy action defaults
CopyContainerID: "复制容器 ID",
CopyImageName: "复制镜像名称",
CopiedFmt: "已复制: %s",
FilterPrompt: "筛选",
}
}

View file

@ -94,5 +94,10 @@ func dutchSet() TranslationSet {
StopContainer: "Weet je zeker dat je deze container wil stoppen?",
PressEnterToReturn: "Druk op enter om terug te gaan naar lazydocker (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Als u wilt loskoppelen van de container, drukt u standaard op ctrl-p en vervolgens op ctrl-q",
// copy action defaults
CopyContainerID: "Kopieer container-id",
CopyImageName: "Kopieer image-naam",
CopiedFmt: "Gekopieerd: %s",
}
}

View file

@ -136,6 +136,11 @@ type TranslationSet struct {
FocusImages string
FocusVolumes string
FocusNetworks string
// new translations for copy actions
CopyContainerID string
CopyImageName string
CopiedFmt string // format string, e.g. "Copied: %s"
}
func englishSet() TranslationSet {
@ -279,5 +284,10 @@ func englishSet() TranslationSet {
FocusImages: "focus images panel",
FocusVolumes: "focus volumes panel",
FocusNetworks: "focus networks panel",
// copy action defaults
CopyContainerID: "Copy container id",
CopyImageName: "Copy image name",
CopiedFmt: "Copied: %s",
}
}

View file

@ -116,5 +116,10 @@ func frenchSet() TranslationSet {
No: "non",
Yes: "oui",
// copy action defaults
CopyContainerID: "Copier l'ID du conteneur",
CopyImageName: "Copier le nom de l'image",
CopiedFmt: "Copié : %s",
}
}

View file

@ -93,5 +93,10 @@ func germanSet() TranslationSet {
StopContainer: "Bist du dir sicher, dass du den Container anhalten möchtest?",
PressEnterToReturn: "Drücke Eingabe um zu lazydocker zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Um sich vom Container zu trennen, drücken Sie standardmäßig ctrl-p und dann ctrl-q",
// copy action defaults
CopyContainerID: "Container-ID kopieren",
CopyImageName: "Image-Namen kopieren",
CopiedFmt: "Kopiert: %s",
}
}

View file

@ -93,5 +93,10 @@ func polishSet() TranslationSet {
StopContainer: "Na pewno zatrzymać ten kontener?",
PressEnterToReturn: "Wciśnij enter aby powrócić do lazydockera (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Domyślnie, aby odłączyć się od kontenera, naciśnij ctrl-p, a następnie ctrl-q",
// copy action defaults
CopyContainerID: "Kopiuj identyfikator kontenera",
CopyImageName: "Kopiuj nazwę obrazu",
CopiedFmt: "Skopiowano: %s",
}
}

View file

@ -130,6 +130,11 @@ func portugueseSet() TranslationSet {
No: "não",
Yes: "sim",
// copy action defaults
CopyContainerID: "Copiar ID do contêiner",
CopyImageName: "Copiar nome da imagem",
CopiedFmt: "Copiado: %s",
LcNextScreenMode: "modo de tela seguinte (normal/meia/tela cheia)",
LcPrevScreenMode: "modo de tela anterior",
FilterPrompt: "filtro",

View file

@ -124,6 +124,11 @@ func spanishSet() TranslationSet {
No: "no",
Yes: "sí",
// copy action defaults
CopyContainerID: "Copiar ID del contenedor",
CopyImageName: "Copiar nombre de la imagen",
CopiedFmt: "Copiado: %s",
FilterPrompt: "filtrar",
}
}

View file

@ -93,5 +93,10 @@ func turkishSet() TranslationSet {
StopContainer: "Bu konteyneri durdurmak istediğinize emin misiniz?",
PressEnterToReturn: "lazydocker' a geri dönmek için enter tuşuna basın ( Bu uyarı, `gui.return Immediately: true` ayarıyla devre dışı bırakılabilir)",
DetachFromContainerShortCut: "Varsayılan olarak, kaptan ayırmak için ctrl-p ve ardından ctrl-q tuşlarına basın",
// copy action defaults
CopyContainerID: "Konteyner kimliğini kopyala",
CopyImageName: "Görüntü adını kopyala",
CopiedFmt: "Kopyalandı: %s",
}
}