Rework static notification handling

This commit is contained in:
Clément PÉAU 2025-05-03 20:32:36 +02:00
parent 59f39d76cc
commit 70e96c6a49
15 changed files with 73 additions and 59 deletions

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Container ## Container
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: entfernen <kbd>d</kbd>: entfernen
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Containers ## Containers
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: remove <kbd>d</kbd>: remove
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Contenedores ## Contenedores
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: borrar <kbd>d</kbd>: borrar
<kbd>e</kbd>: esconder/mostrar contenedores parados <kbd>e</kbd>: esconder/mostrar contenedores parados
<kbd>p</kbd>: pausa <kbd>p</kbd>: pausa

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Conteneurs ## Conteneurs
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: supprimer <kbd>d</kbd>: supprimer
<kbd>e</kbd>: cacher/montrer les conteneurs arrêtés <kbd>e</kbd>: cacher/montrer les conteneurs arrêtés
<kbd>p</kbd>: pause <kbd>p</kbd>: pause

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Containers ## Containers
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: verwijder <kbd>d</kbd>: verwijder
<kbd>e</kbd>: verberg gestopte containers <kbd>e</kbd>: verberg gestopte containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Kontenery ## Kontenery
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: usuń <kbd>d</kbd>: usuń
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Contêineres ## Contêineres
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: remover <kbd>d</kbd>: remover
<kbd>e</kbd>: ocultar/mostrar contêineres parados <kbd>e</kbd>: ocultar/mostrar contêineres parados
<kbd>p</kbd>: pausar <kbd>p</kbd>: pausar

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Konteynerler ## Konteynerler
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: kaldır <kbd>d</kbd>: kaldır
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause

View file

@ -16,7 +16,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 容器 ## 容器
<pre> <pre>
<kbd></kbd>: copy container id <kbd>y</kbd>: copy container ID
<kbd>d</kbd>: 移除 <kbd>d</kbd>: 移除
<kbd>e</kbd>: 隐藏/显示已停止的容器 <kbd>e</kbd>: 隐藏/显示已停止的容器
<kbd>p</kbd>: 暂停 <kbd>p</kbd>: 暂停

View file

@ -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})) 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) return clipboard.WriteAll(str)
} }

View file

@ -1,7 +1,6 @@
package gui package gui
import ( import (
"sync"
"time" "time"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
@ -16,15 +15,14 @@ type appStatus struct {
type statusManager struct { type statusManager struct {
statuses []appStatus statuses []appStatus
lock *sync.Mutex
} }
const (
TickIntervalMs = 50
)
func (m *statusManager) removeStatus(name string) { func (m *statusManager) removeStatus(name string) {
newStatuses := []appStatus{} newStatuses := []appStatus{}
m.lock.Lock()
defer m.lock.Unlock()
for _, status := range m.statuses { for _, status := range m.statuses {
if status.name != name { if status.name != name {
newStatuses = append(newStatuses, status) newStatuses = append(newStatuses, status)
@ -33,61 +31,40 @@ func (m *statusManager) removeStatus(name string) {
m.statuses = newStatuses m.statuses = newStatuses
} }
func (m *statusManager) addWaitingStatus(name string) { func (m *statusManager) addStatus(name string, statusType string, duration int) {
m.lock.Lock()
defer m.lock.Unlock()
m.removeStatus(name) m.removeStatus(name)
newStatus := appStatus{ newStatus := appStatus{
name: name, name: name,
statusType: "waiting", statusType: statusType,
duration: 0, duration: duration,
} }
m.statuses = append([]appStatus{newStatus}, m.statuses...) m.statuses = append([]appStatus{newStatus}, m.statuses...)
} }
func (m *statusManager) getStatusString() string { func (m *statusManager) getStatusString() string {
m.lock.Lock()
defer m.lock.Unlock()
if len(m.statuses) == 0 { if len(m.statuses) == 0 {
return "" return ""
} }
topStatus := m.statuses[0] topStatus := m.statuses[0]
if topStatus.statusType == "waiting" { if topStatus.statusType == "waiting" {
return topStatus.name + " " + utils.Loader() 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 return topStatus.name
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 // 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 { func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go func() { go func() {
gui.statusManager.addWaitingStatus(name) go gui.Notify(name, "waiting", 0)()
defer func() { defer func() {
gui.statusManager.removeStatus(name) 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 { if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
@ -97,3 +74,37 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
return nil 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)
}
}
}
}

View file

@ -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 // 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 originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx) selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
gui.DockerCommand.ServiceMutex.Unlock()
containers, services, err := gui.DockerCommand.RefreshContainersAndServices( containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
gui.Panels.Services.List.GetAllItems(), gui.Panels.Services.List.GetAllItems(),
@ -364,10 +366,8 @@ func (gui *Gui) handleCopyContainerId(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
err = gui.WithStaticWaitingStatus(fmt.Sprintf(gui.Tr.CopyContainerIdStatus, utils.TruncateWithEllipsis(ctr.ID, 10)), time.Second*2) formattedStatusText := fmt.Sprintf("Copied %s to clipboard", utils.TruncateWithEllipsis(ctr.ID, 10))
if err != nil { go gui.Notify(formattedStatusText, "info", 3)()
return err
}
return gui.OSCommand.CopyToClipboard(ctr.ID) return gui.OSCommand.CopyToClipboard(ctr.ID)
} }

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
@ -147,11 +146,9 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
State: initialState, State: initialState,
Config: config, Config: config,
Tr: tr, Tr: tr,
statusManager: &statusManager{ statusManager: &statusManager{},
lock: &sync.Mutex{}, taskManager: tasks.NewTaskManager(log, tr),
}, ErrorChan: errorChan,
taskManager: tasks.NewTaskManager(log, tr),
ErrorChan: errorChan,
} }
deadlock.Opts.Disable = !gui.Config.Debug deadlock.Opts.Disable = !gui.Config.Debug

View file

@ -187,7 +187,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}, },
{ {
ViewName: "containers", ViewName: "containers",
Key: gocui.KeyCtrlO, Key: 'y',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleCopyContainerId, Handler: gui.handleCopyContainerId,
Description: gui.Tr.CopyContainerId, Description: gui.Tr.CopyContainerId,

View file

@ -67,8 +67,6 @@ type TranslationSet struct {
ViewLogs string ViewLogs string
UpProject string UpProject string
DownProject string DownProject string
CopyContainerId string
CopyContainerIdStatus string
ServicesTitle string ServicesTitle string
ContainersTitle string ContainersTitle string
StandaloneContainersTitle string StandaloneContainersTitle string
@ -111,6 +109,7 @@ type TranslationSet struct {
FilterList string FilterList string
OpenInBrowser string OpenInBrowser string
SortContainersByState string SortContainersByState string
CopyContainerId string
LogsTitle string LogsTitle string
ConfigTitle string ConfigTitle string
@ -199,8 +198,6 @@ func englishSet() TranslationSet {
ViewLogs: "view logs", ViewLogs: "view logs",
UpProject: "up project", UpProject: "up project",
DownProject: "down project", DownProject: "down project",
CopyContainerId: "copy container id",
CopyContainerIdStatus: "Copied %s to clipboard",
RemoveImage: "remove image", RemoveImage: "remove image",
RemoveVolume: "remove volume", RemoveVolume: "remove volume",
RemoveNetwork: "remove network", RemoveNetwork: "remove network",
@ -220,6 +217,7 @@ func englishSet() TranslationSet {
FilterList: "filter list", FilterList: "filter list",
OpenInBrowser: "open in browser (first port is http)", OpenInBrowser: "open in browser (first port is http)",
SortContainersByState: "sort containers by state", SortContainersByState: "sort containers by state",
CopyContainerId: "copy container ID",
GlobalTitle: "Global", GlobalTitle: "Global",
MainTitle: "Main", MainTitle: "Main",