diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index dc9dd08e8..b1c35000f 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: zeige Protokolle E: exec shell c: führe vordefinierten benutzerdefinierten Befehl aus + y: Container-ID kopieren b: view bulk commands w: open in browser (first port is http) enter: fokussieren aufs Hauptpanel @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
c: führe vordefinierten benutzerdefinierten Befehl aus + y: Image-Namen kopieren d: entferne Image b: view bulk commands enter: fokussieren aufs Hauptpanel diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index ae21c4dcd..e428d776b 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: view logs E: exec shell c: run predefined custom command + y: copy container id b: view bulk commands w: open in browser (first port is http) enter: focus main panel @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: run predefined custom command + y: copy image name d: remove image b: view bulk commands enter: focus main panel diff --git a/docs/keybindings/Keybindings_es.md b/docs/keybindings/Keybindings_es.md index 6b4eb9210..6d9f1274c 100644 --- a/docs/keybindings/Keybindings_es.md +++ b/docs/keybindings/Keybindings_es.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: ver logs E: ejecutar shell c: ejecutar comando personalizado + y: Copiar ID del contenedor b: ver comandos masivos w: abrir en navegador (first port is http) enter: enfocar panel principal @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: ejecutar comando personalizado + y: Copiar nombre de la imagen d: limpiar imagen b: ver comandos masivos enter: enfocar panel principal diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md index 109078b23..6f7a49b41 100644 --- a/docs/keybindings/Keybindings_fr.md +++ b/docs/keybindings/Keybindings_fr.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: voir les enregistrements E: exécuter le shell c: exécuter une commande prédéfinie + y: Copier l'ID du conteneur b: voir les commandes groupées w: ouvrir dans le navigateur (le premier port est http) enter: focus panneau principal @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: exécuter une commande prédéfinie + y: Copier le nom de l'image d: supprimer l'image b: voir les commandes groupées enter: focus panneau principal diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 24477006a..5029d253c 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: bekijk logs E: exec shell c: draai een vooraf bedacht aangepaste opdracht + y: Kopieer container-id b: view bulk commands w: open in browser (first port is http) enter: focus hoofdpaneel @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: draai een vooraf bedacht aangepaste opdracht + y: Kopieer image-naam d: verwijder image b: view bulk commands enter: focus hoofdpaneel diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 2f4cb93f9..44495599f 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: pokaż logi E: exec shell c: wykonaj predefiniowaną własną komende + y: Kopiuj identyfikator kontenera b: view bulk commands w: open in browser (first port is http) enter: skup na głównym panelu @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: wykonaj predefiniowaną własną komende + y: Kopiuj nazwę obrazu d: usuń obraz b: view bulk commands enter: skup na głównym panelu diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md index 32a557b53..c0a30cd73 100644 --- a/docs/keybindings/Keybindings_pt.md +++ b/docs/keybindings/Keybindings_pt.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: ver logs E: executar shell c: executar comando personalizado predefinido + y: Copiar ID do contêiner b: ver comandos em massa w: abrir no navegador (primeira porta é http) enter: focar no painel principal @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: executar comando personalizado predefinido + y: Copiar nome da imagem d: remover imagem b: ver comandos em massa enter: focar no painel principal diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md index 3192a7c20..be9ee2107 100644 --- a/docs/keybindings/Keybindings_tr.md +++ b/docs/keybindings/Keybindings_tr.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: kayıt defterini görüntüle E: exec shell c: önceden tanımlanmış özel komutu çalıştır + y: Konteyner kimliğini kopyala b: view bulk commands w: open in browser (first port is http) enter: ana panele odaklan @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: önceden tanımlanmış özel komutu çalıştır + y: Görüntü adını kopyala d: imajı kaldır b: view bulk commands enter: ana panele odaklan diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index 0c75fbcb9..1a49f9bbe 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -26,6 +26,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct m: 查看日志 E: 执行shell c: 运行预定义的自定义命令 + y: 复制容器 ID b: 查看批量命令 w: 在浏览器中打开(第一个端口为http) enter: 聚焦主面板 @@ -62,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n directc: 运行预定义的自定义命令 + y: 复制镜像名称 d: 移除镜像 b: 查看批量命令 enter: 聚焦主面板 diff --git a/pkg/commands/clipboard.go b/pkg/commands/clipboard.go new file mode 100644 index 000000000..d03eeb7e8 --- /dev/null +++ b/pkg/commands/clipboard.go @@ -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() +} diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index f806f72c1..ce21659bd 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -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) + } + } + }() +} diff --git a/pkg/gui/clipboard.go b/pkg/gui/clipboard.go new file mode 100644 index 000000000..a3c727ce8 --- /dev/null +++ b/pkg/gui/clipboard.go @@ -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 +} diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index a638ee290..df1d3916f 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -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 { diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 7b0d56437..da57db609 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -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 +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df7060241..485fcc842 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -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', diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index aa125f0b4..dbdadfcdf 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -131,6 +131,11 @@ func chineseSet() TranslationSet { LcNextScreenMode: "下一个屏幕模式(正常/半屏/全屏)", LcPrevScreenMode: "上一个屏幕模式", - FilterPrompt: "筛选", + // copy action defaults + CopyContainerID: "复制容器 ID", + CopyImageName: "复制镜像名称", + CopiedFmt: "已复制: %s", + + FilterPrompt: "筛选", } } diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 8a8601c23..6f1a7a2b8 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -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", } } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d86b0c84f..a9703349e 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -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", } } diff --git a/pkg/i18n/french.go b/pkg/i18n/french.go index 29ab5be27..d5a60c2a1 100644 --- a/pkg/i18n/french.go +++ b/pkg/i18n/french.go @@ -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", } } diff --git a/pkg/i18n/german.go b/pkg/i18n/german.go index ac23d6b19..eff6f9a34 100644 --- a/pkg/i18n/german.go +++ b/pkg/i18n/german.go @@ -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", } } diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 1025bf47c..bffc31a03 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -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", } } diff --git a/pkg/i18n/portuguese.go b/pkg/i18n/portuguese.go index 36d9bcfdb..984fa433a 100644 --- a/pkg/i18n/portuguese.go +++ b/pkg/i18n/portuguese.go @@ -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", diff --git a/pkg/i18n/spanish.go b/pkg/i18n/spanish.go index b9148c3ae..5a603f9b0 100644 --- a/pkg/i18n/spanish.go +++ b/pkg/i18n/spanish.go @@ -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", } } diff --git a/pkg/i18n/turkish.go b/pkg/i18n/turkish.go index fccb3d911..eb9d44d76 100644 --- a/pkg/i18n/turkish.go +++ b/pkg/i18n/turkish.go @@ -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", } }