From 2f69b2e1db09fcb660c1c9b084b9193ef7a90f75 Mon Sep 17 00:00:00 2001 From: christophe-duc Date: Wed, 7 Jan 2026 19:22:32 -0400 Subject: [PATCH 1/4] added expand/collapse feature --- README.md | 2 + pkg/commands/podman.go | 11 ++- pkg/gui/containers_panel.go | 108 ++++++++++++++----------- pkg/gui/gui.go | 5 ++ pkg/gui/keybindings.go | 7 ++ pkg/gui/presentation/containers.go | 18 ++++- pkg/gui/sort_container_test.go | 122 +++++++++++++++++++++++++++++ pkg/i18n/chinese.go | 1 + pkg/i18n/dutch.go | 5 +- pkg/i18n/english.go | 2 + pkg/i18n/french.go | 1 + pkg/i18n/german.go | 5 +- pkg/i18n/polish.go | 5 +- pkg/i18n/portuguese.go | 1 + pkg/i18n/spanish.go | 1 + pkg/i18n/turkish.go | 5 +- 16 files changed, 241 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 083ee925..edaa4296 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ A simple terminal UI for Podman and podman-compose, written in Go with the [gocu Again, this is a fork! and probably with reduced functionality as the original from Jesse Duffield. It was created to resolve a simple problem, work fully with podman and don't depend on how docker works and the socket. +Lazypodman DOES support pods + This is published as is. Compilation works and lazypodman runs on Linux without needing a socket present to monitor your containers. Original elevator pitch below: diff --git a/pkg/commands/podman.go b/pkg/commands/podman.go index 7d1da7ee..57a3ab03 100644 --- a/pkg/commands/podman.go +++ b/pkg/commands/podman.go @@ -440,12 +440,19 @@ func (c *PodmanCommand) buildContainerListItems(containers []*Container, podSumm // Add pods and their containers for _, podID := range podIDs { ps := podMap[podID] + + // Sort containers within this pod alphabetically + podCtrs := podContainers[podID] + sort.Slice(podCtrs, func(i, j int) bool { + return podCtrs[i].Name < podCtrs[j].Name + }) + // Create pod object pod := &Pod{ ID: ps.ID, Name: ps.Name, Summary: ps, - Containers: podContainers[podID], + Containers: podCtrs, OSCommand: c.OSCommand, Log: c.Log, } @@ -458,7 +465,7 @@ func (c *PodmanCommand) buildContainerListItems(containers []*Container, podSumm }) // Add containers in this pod with indent - for _, ctr := range podContainers[podID] { + for _, ctr := range podCtrs { // Set pod name on container if not already set if ctr.Summary.PodName == "" { ctr.Summary.PodName = ps.Name diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 6131ecf4..fff66416 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -84,6 +84,14 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.ContainerLi } container := item.Container + + // Hide containers in collapsed pods + if container.Summary.Pod != "" { + if !gui.State.ExpandedPods[container.Summary.Pod] { + return false // Pod is collapsed, hide this container + } + } + // Note that this is O(N*M) time complexity where N is the number of services // and M is the number of containers. We expect N to be small but M may be large, // so we will need to keep an eye on this. @@ -98,7 +106,11 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.ContainerLi return true }, GetTableCells: func(item *commands.ContainerListItem) []string { - return presentation.GetContainerListItemDisplayStrings(&gui.Config.UserConfig.Gui, item) + expanded := false + if item.IsPod && item.Pod != nil { + expanded = gui.State.ExpandedPods[item.Pod.ID] + } + return presentation.GetContainerListItemDisplayStrings(&gui.Config.UserConfig.Gui, item, expanded) }, } } @@ -124,39 +136,10 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo } // sortContainerListItems sorts items to group pods with their containers. -// Order: pods first (sorted by state/name), then their containers indented, -// then standalone containers (sorted by state/name). -func sortContainerListItems(a *commands.ContainerListItem, b *commands.ContainerListItem, legacySort bool) bool { - // If both are in the same pod, sort by indent (pod first) then by name - if a.PodID() != "" && a.PodID() == b.PodID() { - // Pod comes before its containers - if a.IsPod && !b.IsPod { - return true - } - if !a.IsPod && b.IsPod { - return false - } - // Both are containers in the same pod, sort by name - return a.Name() < b.Name() - } - - // Get the effective sort key (pod name for items in pods, own name for standalone) - aKey := a.Name() - bKey := b.Name() - if a.PodID() != "" && !a.IsPod { - aKey = a.PodName() + "\x00" + a.Name() // Sort after the pod - } - if b.PodID() != "" && !b.IsPod { - bKey = b.PodName() + "\x00" + b.Name() - } - if a.IsPod { - aKey = a.Name() + "\x00" // Pod sorts before its containers - } - if b.IsPod { - bKey = b.Name() + "\x00" - } - - // Pods and their containers sort together, standalone containers at the end +// Order: pods first (sorted alphabetically), then their containers indented (sorted alphabetically), +// then standalone containers (sorted alphabetically). +func sortContainerListItems(a *commands.ContainerListItem, b *commands.ContainerListItem, _ bool) bool { + // Pods and their containers sort before standalone containers aInPod := a.IsPod || a.PodID() != "" bInPod := b.IsPod || b.PodID() != "" @@ -167,18 +150,39 @@ func sortContainerListItems(a *commands.ContainerListItem, b *commands.Container return false } - // Both in same category (pod-related or standalone) - if legacySort { - return aKey < bKey + // Both are in the same category (pod-related or standalone) + + // For pod-related items, sort by pod name first, then by type (pod before containers), then by container name + if aInPod && bInPod { + // Get effective pod name for comparison + aPodName := a.PodName() + if a.IsPod { + aPodName = a.Name() + } + bPodName := b.PodName() + if b.IsPod { + bPodName = b.Name() + } + + // Different pods: sort by pod name + if aPodName != bPodName { + return aPodName < bPodName + } + + // Same pod: pod comes first, then containers alphabetically + if a.IsPod && !b.IsPod { + return true + } + if !a.IsPod && b.IsPod { + return false + } + + // Both are containers in the same pod: sort by name + return a.Name() < b.Name() } - // Sort by state, then by key - stateA := containerStates[a.State()] - stateB := containerStates[b.State()] - if stateA == stateB { - return aKey < bKey - } - return stateA < stateB + // Both are standalone containers: sort alphabetically + return a.Name() < b.Name() } // Wrapper functions that delegate to container or pod rendering @@ -725,3 +729,19 @@ func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error { link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort) return gui.OSCommand.OpenLink(link) } + +func (gui *Gui) handleTogglePodExpansion(g *gocui.Gui, v *gocui.View) error { + item, err := gui.Panels.Containers.GetSelectedItem() + if err != nil { + return nil + } + + if !item.IsPod { + return nil // Only works on pods + } + + podID := item.Pod.ID + gui.State.ExpandedPods[podID] = !gui.State.ExpandedPods[podID] + + return gui.Panels.Containers.RerenderList() +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 6867abda..ba79b0da 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -80,6 +80,10 @@ type guiState struct { // if true, we show containers with an 'exited' status in the containers panel ShowExitedContainers bool + // ExpandedPods tracks which pods are expanded (showing their containers) + // Key is pod ID, value is true if expanded. Pods start collapsed by default. + ExpandedPods map[string]bool + ScreenMode WindowMaximisation // Maintains the state of manual filtering i.e. typing in a substring @@ -134,6 +138,7 @@ func NewGui(log *logrus.Entry, podmanCommand *commands.PodmanCommand, oSCommand ViewStack: []string{}, ShowExitedContainers: true, + ExpandedPods: make(map[string]bool), ScreenMode: getScreenMode(config), } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df706024..85b7c455 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -262,6 +262,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleContainersOpenInBrowserCommand, Description: gui.Tr.OpenInBrowser, }, + { + ViewName: "containers", + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleTogglePodExpansion, + Description: gui.Tr.TogglePodExpansion, + }, { ViewName: "services", Key: 'u', diff --git a/pkg/gui/presentation/containers.go b/pkg/gui/presentation/containers.go index c96486f8..6729daa5 100644 --- a/pkg/gui/presentation/containers.go +++ b/pkg/gui/presentation/containers.go @@ -25,9 +25,9 @@ func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands } // GetContainerListItemDisplayStrings returns display strings for a ContainerListItem (pod or container) -func GetContainerListItemDisplayStrings(guiConfig *config.GuiConfig, item *commands.ContainerListItem) []string { +func GetContainerListItemDisplayStrings(guiConfig *config.GuiConfig, item *commands.ContainerListItem, expanded bool) []string { if item.IsPod && item.Pod != nil { - return GetPodDisplayStrings(guiConfig, item.Pod) + return GetPodDisplayStrings(guiConfig, item.Pod, expanded) } if item.Container == nil { @@ -44,11 +44,21 @@ func GetContainerListItemDisplayStrings(guiConfig *config.GuiConfig, item *comma } // GetPodDisplayStrings returns display strings for a pod -func GetPodDisplayStrings(guiConfig *config.GuiConfig, pod *commands.Pod) []string { +func GetPodDisplayStrings(guiConfig *config.GuiConfig, pod *commands.Pod, expanded bool) []string { + // Add expand/collapse indicator to pod name + var indicator string + if len(pod.Containers) > 0 { + if expanded { + indicator = "- " + } else { + indicator = "+ " + } + } + return []string{ getPodDisplayStatus(guiConfig, pod), "", // No substatus for pods - utils.ColoredString(pod.Name, color.FgCyan), + utils.ColoredString(indicator+pod.Name, color.FgCyan), "", // No CPU% for pods "", // No ports for pods utils.ColoredString(fmt.Sprintf("(%d containers)", len(pod.Containers)), color.FgMagenta), diff --git a/pkg/gui/sort_container_test.go b/pkg/gui/sort_container_test.go index 1de7dbef..813da1d6 100644 --- a/pkg/gui/sort_container_test.go +++ b/pkg/gui/sort_container_test.go @@ -145,3 +145,125 @@ func TestLegacySortedContainers(t *testing.T) { assertEqualContainers(t, expected[i], actual[i]) } } + +func TestSortContainerListItems(t *testing.T) { + // Create test data: 2 pods with containers and 2 standalone containers + items := []*commands.ContainerListItem{ + // Standalone container "zebra" + { + IsPod: false, + Container: &commands.Container{ + ID: "standalone-z", + Name: "zebra", + Summary: commands.ContainerSummary{ + State: "running", + }, + }, + Indent: 0, + }, + // Pod "beta" with containers + { + IsPod: true, + Pod: &commands.Pod{ + ID: "pod-beta", + Name: "beta", + }, + Indent: 0, + }, + { + IsPod: false, + Container: &commands.Container{ + ID: "ctr-beta-y", + Name: "yak", + Summary: commands.ContainerSummary{ + State: "running", + Pod: "pod-beta", + PodName: "beta", + }, + }, + Indent: 2, + }, + // Standalone container "apple" + { + IsPod: false, + Container: &commands.Container{ + ID: "standalone-a", + Name: "apple", + Summary: commands.ContainerSummary{ + State: "exited", + }, + }, + Indent: 0, + }, + // Pod "alpha" with containers + { + IsPod: true, + Pod: &commands.Pod{ + ID: "pod-alpha", + Name: "alpha", + }, + Indent: 0, + }, + { + IsPod: false, + Container: &commands.Container{ + ID: "ctr-alpha-b", + Name: "bear", + Summary: commands.ContainerSummary{ + State: "running", + Pod: "pod-alpha", + PodName: "alpha", + }, + }, + Indent: 2, + }, + { + IsPod: false, + Container: &commands.Container{ + ID: "ctr-alpha-a", + Name: "ant", + Summary: commands.ContainerSummary{ + State: "exited", + Pod: "pod-alpha", + PodName: "alpha", + }, + }, + Indent: 2, + }, + { + IsPod: false, + Container: &commands.Container{ + ID: "ctr-beta-x", + Name: "xray", + Summary: commands.ContainerSummary{ + State: "exited", + Pod: "pod-beta", + PodName: "beta", + }, + }, + Indent: 2, + }, + } + + // Sort the items + sort.Slice(items, func(i, j int) bool { + return sortContainerListItems(items[i], items[j], false) + }) + + // Expected order: + // 1. pod alpha (alphabetically first pod) + // 2. ant (container in alpha, alphabetically first) + // 3. bear (container in alpha) + // 4. pod beta (alphabetically second pod) + // 5. xray (container in beta, alphabetically first) + // 6. yak (container in beta) + // 7. apple (standalone, alphabetically first) + // 8. zebra (standalone) + + expectedOrder := []string{"alpha", "ant", "bear", "beta", "xray", "yak", "apple", "zebra"} + + assert.Equal(t, len(expectedOrder), len(items)) + for i, item := range items { + assert.Equal(t, expectedOrder[i], item.Name(), "Item at index %d should be %s but was %s", i, expectedOrder[i], item.Name()) + } +} diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index 4b0a4367..d8a427d4 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -78,6 +78,7 @@ func chineseSet() TranslationSet { ViewBulkCommands: "查看批量命令", FilterList: "过滤列表", OpenInBrowser: "在浏览器中打开(第一个端口为http)", + TogglePodExpansion: "展开/折叠 pod", SortContainersByState: "按状态排序容器", GlobalTitle: "全局", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index e079b10e..d844eb18 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -51,8 +51,9 @@ func dutchSet() TranslationSet { PruneVolumes: "vernietig ongebruikte volumes", PruneNetworks: "vernietig ongebruikte networks", PruneImages: "vernietig ongebruikte images", - ViewRestartOptions: "bekijk herstart opties", - RunCustomCommand: "draai een vooraf bedacht aangepaste opdracht", + ViewRestartOptions: "bekijk herstart opties", + RunCustomCommand: "draai een vooraf bedacht aangepaste opdracht", + TogglePodExpansion: "pod uitvouwen/invouwen", GlobalTitle: "Globaal", MainTitle: "Hoofd", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 27f8d0ac..31f482f7 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -108,6 +108,7 @@ type TranslationSet struct { ViewBulkCommands string FilterList string OpenInBrowser string + TogglePodExpansion string SortContainersByState string LogsTitle string ConfigTitle string @@ -214,6 +215,7 @@ func englishSet() TranslationSet { ViewBulkCommands: "view bulk commands", FilterList: "filter list", OpenInBrowser: "open in browser (first port is http)", + TogglePodExpansion: "expand/collapse pod", SortContainersByState: "sort containers by state", GlobalTitle: "Global", diff --git a/pkg/i18n/french.go b/pkg/i18n/french.go index c146428b..ceef5a3f 100644 --- a/pkg/i18n/french.go +++ b/pkg/i18n/french.go @@ -68,6 +68,7 @@ func frenchSet() TranslationSet { RunCustomCommand: "exécuter une commande prédéfinie", ViewBulkCommands: "voir les commandes groupées", OpenInBrowser: "ouvrir dans le navigateur (le premier port est http)", + TogglePodExpansion: "ouvrir/réduire les conteneurs du pod", SortContainersByState: "ordonner les conteneurs par état", GlobalTitle: "Global", diff --git a/pkg/i18n/german.go b/pkg/i18n/german.go index 2c794ccb..b034d5f5 100644 --- a/pkg/i18n/german.go +++ b/pkg/i18n/german.go @@ -50,8 +50,9 @@ func germanSet() TranslationSet { PruneVolumes: "entferne unbenutzte Volumes", PruneNetworks: "entferne unbenutzte Netzwerk", PruneImages: "entferne unbenutzte Images", - ViewRestartOptions: "zeige Neustartoptionen", - RunCustomCommand: "führe vordefinierten benutzerdefinierten Befehl aus", + ViewRestartOptions: "zeige Neustartoptionen", + RunCustomCommand: "führe vordefinierten benutzerdefinierten Befehl aus", + TogglePodExpansion: "Pod erweitern/reduzieren", GlobalTitle: "Global", MainTitle: "Haupt", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index eca2b5fc..6e32ccfe 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -50,8 +50,9 @@ func polishSet() TranslationSet { PruneVolumes: "wyczyść nieużywane wolumeny", PruneNetworks: "wyczyść nieużywane sieci", PruneImages: "wyczyść nieużywane obrazy", - ViewRestartOptions: "pokaż opcje restartu", - RunCustomCommand: "wykonaj predefiniowaną własną komende", + ViewRestartOptions: "pokaż opcje restartu", + RunCustomCommand: "wykonaj predefiniowaną własną komende", + TogglePodExpansion: "rozwiń/zwiń pod", GlobalTitle: "Globalne", MainTitle: "Główne", diff --git a/pkg/i18n/portuguese.go b/pkg/i18n/portuguese.go index 655321fd..bd9cbad0 100644 --- a/pkg/i18n/portuguese.go +++ b/pkg/i18n/portuguese.go @@ -78,6 +78,7 @@ func portugueseSet() TranslationSet { ViewBulkCommands: "ver comandos em massa", FilterList: "filtrar lista", OpenInBrowser: "abrir no navegador (primeira porta é http)", + TogglePodExpansion: "expandir/recolher pod", SortContainersByState: "ordenar contêineres por estado", GlobalTitle: "Global", diff --git a/pkg/i18n/spanish.go b/pkg/i18n/spanish.go index 8c0374c7..1a238edf 100644 --- a/pkg/i18n/spanish.go +++ b/pkg/i18n/spanish.go @@ -73,6 +73,7 @@ func spanishSet() TranslationSet { ViewBulkCommands: "ver comandos masivos", FilterList: "filtar list", OpenInBrowser: "abrir en navegador (first port is http)", + TogglePodExpansion: "expandir/colapsar pod", SortContainersByState: "ordenar contenedores por estado", GlobalTitle: "Global", diff --git a/pkg/i18n/turkish.go b/pkg/i18n/turkish.go index cef41e4c..4e0d846a 100644 --- a/pkg/i18n/turkish.go +++ b/pkg/i18n/turkish.go @@ -50,8 +50,9 @@ func turkishSet() TranslationSet { PruneVolumes: "kullanılmayan alanları temizle", PruneNetworks: "kullanılmayan ağları temizle", PruneImages: "kullanılmayan imajları temizle", - ViewRestartOptions: "yeniden başlatma seçeneklerini görüntüle", - RunCustomCommand: "önceden tanımlanmış özel komutu çalıştır", + ViewRestartOptions: "yeniden başlatma seçeneklerini görüntüle", + RunCustomCommand: "önceden tanımlanmış özel komutu çalıştır", + TogglePodExpansion: "pod'u genişlet/daralt", GlobalTitle: "Global", MainTitle: "Ana", From f28b9a5a13ad1d9aea928a9b711dfa369d56a049 Mon Sep 17 00:00:00 2001 From: christophe-duc Date: Wed, 7 Jan 2026 19:25:31 -0400 Subject: [PATCH 2/4] fixed linting --- pkg/i18n/dutch.go | 6 +++--- pkg/i18n/german.go | 6 +++--- pkg/i18n/polish.go | 6 +++--- pkg/i18n/turkish.go | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index d844eb18..0e3e3516 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -51,9 +51,9 @@ func dutchSet() TranslationSet { PruneVolumes: "vernietig ongebruikte volumes", PruneNetworks: "vernietig ongebruikte networks", PruneImages: "vernietig ongebruikte images", - ViewRestartOptions: "bekijk herstart opties", - RunCustomCommand: "draai een vooraf bedacht aangepaste opdracht", - TogglePodExpansion: "pod uitvouwen/invouwen", + ViewRestartOptions: "bekijk herstart opties", + RunCustomCommand: "draai een vooraf bedacht aangepaste opdracht", + TogglePodExpansion: "pod uitvouwen/invouwen", GlobalTitle: "Globaal", MainTitle: "Hoofd", diff --git a/pkg/i18n/german.go b/pkg/i18n/german.go index b034d5f5..93c540b9 100644 --- a/pkg/i18n/german.go +++ b/pkg/i18n/german.go @@ -50,9 +50,9 @@ func germanSet() TranslationSet { PruneVolumes: "entferne unbenutzte Volumes", PruneNetworks: "entferne unbenutzte Netzwerk", PruneImages: "entferne unbenutzte Images", - ViewRestartOptions: "zeige Neustartoptionen", - RunCustomCommand: "führe vordefinierten benutzerdefinierten Befehl aus", - TogglePodExpansion: "Pod erweitern/reduzieren", + ViewRestartOptions: "zeige Neustartoptionen", + RunCustomCommand: "führe vordefinierten benutzerdefinierten Befehl aus", + TogglePodExpansion: "Pod erweitern/reduzieren", GlobalTitle: "Global", MainTitle: "Haupt", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 6e32ccfe..273c9695 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -50,9 +50,9 @@ func polishSet() TranslationSet { PruneVolumes: "wyczyść nieużywane wolumeny", PruneNetworks: "wyczyść nieużywane sieci", PruneImages: "wyczyść nieużywane obrazy", - ViewRestartOptions: "pokaż opcje restartu", - RunCustomCommand: "wykonaj predefiniowaną własną komende", - TogglePodExpansion: "rozwiń/zwiń pod", + ViewRestartOptions: "pokaż opcje restartu", + RunCustomCommand: "wykonaj predefiniowaną własną komende", + TogglePodExpansion: "rozwiń/zwiń pod", GlobalTitle: "Globalne", MainTitle: "Główne", diff --git a/pkg/i18n/turkish.go b/pkg/i18n/turkish.go index 4e0d846a..b8e0c3aa 100644 --- a/pkg/i18n/turkish.go +++ b/pkg/i18n/turkish.go @@ -50,9 +50,9 @@ func turkishSet() TranslationSet { PruneVolumes: "kullanılmayan alanları temizle", PruneNetworks: "kullanılmayan ağları temizle", PruneImages: "kullanılmayan imajları temizle", - ViewRestartOptions: "yeniden başlatma seçeneklerini görüntüle", - RunCustomCommand: "önceden tanımlanmış özel komutu çalıştır", - TogglePodExpansion: "pod'u genişlet/daralt", + ViewRestartOptions: "yeniden başlatma seçeneklerini görüntüle", + RunCustomCommand: "önceden tanımlanmış özel komutu çalıştır", + TogglePodExpansion: "pod'u genişlet/daralt", GlobalTitle: "Global", MainTitle: "Ana", From 7f67c6a271fbdbe7b02d0bfbcb4c8669def004bc Mon Sep 17 00:00:00 2001 From: christophe-duc Date: Wed, 7 Jan 2026 19:38:03 -0400 Subject: [PATCH 3/4] corrected a problem reported by CLAUDE Reviewer --- CLAUDE.md | 13 ++++++++++--- pkg/gui/containers_panel.go | 15 ++++++++++++--- pkg/gui/sort_container_test.go | 8 ++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4b7448f1..c9cd3505 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,15 +149,22 @@ GetEvents() - Streaming (socket) or polling (libpod) - Custom commands configurable via YAML - Command templates use Go template syntax +### GUI Features +- **Pod expand/collapse**: Pods can be expanded/collapsed in the containers panel. State tracked in `gui.State.ExpandedPods` map +- **Container sorting**: Controlled by `LegacySortContainers` config option: + - `false` (default): Sort by state (running → exited → created), then alphabetically + - `true`: Sort alphabetically by name only +- **Container list items**: `ContainerListItem` wraps both pods and containers for unified list display + ## Testing ```bash # Run all tests with coverage ./test.sh -# Run specific package tests -go test -mod=vendor ./pkg/commands/... -go test -mod=vendor ./pkg/gui/... +# Run specific package tests (requires build tags to avoid CGO dependencies) +go test -tags=containers_image_openpgp,exclude_graphdriver_btrfs -mod=vendor ./pkg/gui/... +go test -tags=containers_image_openpgp,exclude_graphdriver_btrfs -mod=vendor ./pkg/commands/... ``` ## Module Info diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index fff66416..00afbc63 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -138,7 +138,7 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo // sortContainerListItems sorts items to group pods with their containers. // Order: pods first (sorted alphabetically), then their containers indented (sorted alphabetically), // then standalone containers (sorted alphabetically). -func sortContainerListItems(a *commands.ContainerListItem, b *commands.ContainerListItem, _ bool) bool { +func sortContainerListItems(a *commands.ContainerListItem, b *commands.ContainerListItem, legacySort bool) bool { // Pods and their containers sort before standalone containers aInPod := a.IsPod || a.PodID() != "" bInPod := b.IsPod || b.PodID() != "" @@ -181,8 +181,17 @@ func sortContainerListItems(a *commands.ContainerListItem, b *commands.Container return a.Name() < b.Name() } - // Both are standalone containers: sort alphabetically - return a.Name() < b.Name() + // Both are standalone containers + if legacySort { + return a.Name() < b.Name() + } + // Sort by state, then by name + stateA := containerStates[a.State()] + stateB := containerStates[b.State()] + if stateA == stateB { + return a.Name() < b.Name() + } + return stateA < stateB } // Wrapper functions that delegate to container or pod rendering diff --git a/pkg/gui/sort_container_test.go b/pkg/gui/sort_container_test.go index 813da1d6..794292b8 100644 --- a/pkg/gui/sort_container_test.go +++ b/pkg/gui/sort_container_test.go @@ -250,17 +250,17 @@ func TestSortContainerListItems(t *testing.T) { return sortContainerListItems(items[i], items[j], false) }) - // Expected order: + // Expected order (with legacySort=false, sorts by state then name): // 1. pod alpha (alphabetically first pod) // 2. ant (container in alpha, alphabetically first) // 3. bear (container in alpha) // 4. pod beta (alphabetically second pod) // 5. xray (container in beta, alphabetically first) // 6. yak (container in beta) - // 7. apple (standalone, alphabetically first) - // 8. zebra (standalone) + // 7. zebra (standalone, running - state 1) + // 8. apple (standalone, exited - state 2) - expectedOrder := []string{"alpha", "ant", "bear", "beta", "xray", "yak", "apple", "zebra"} + expectedOrder := []string{"alpha", "ant", "bear", "beta", "xray", "yak", "zebra", "apple"} assert.Equal(t, len(expectedOrder), len(items)) for i, item := range items { From 562276ab79bcc727bacba876a912e0607a3cd626 Mon Sep 17 00:00:00 2001 From: christophe-duc Date: Wed, 7 Jan 2026 19:42:14 -0400 Subject: [PATCH 4/4] regenerated keybindings --- 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 + 9 files changed, 9 insertions(+) diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index 6aabaff4..e2b8ff55 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: führe vordefinierten benutzerdefinierten Befehl aus b: view bulk commands w: open in browser (first port is http) + space: Pod erweitern/reduzieren enter: fokussieren aufs Hauptpanel [: vorheriges Tab ]: nächstes Tab diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 5330d3b4..f5839dce 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: run predefined custom command b: view bulk commands w: open in browser (first port is http) + space: expand/collapse pod enter: focus main panel [: previous tab ]: next tab diff --git a/docs/keybindings/Keybindings_es.md b/docs/keybindings/Keybindings_es.md index ce2c1711..935f9eca 100644 --- a/docs/keybindings/Keybindings_es.md +++ b/docs/keybindings/Keybindings_es.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: ejecutar comando personalizado b: ver comandos masivos w: abrir en navegador (first port is http) + space: expandir/colapsar pod enter: enfocar panel principal [: anterior pestaña ]: siguiente pestaña diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md index 708b3245..6fc40803 100644 --- a/docs/keybindings/Keybindings_fr.md +++ b/docs/keybindings/Keybindings_fr.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: exécuter une commande prédéfinie b: voir les commandes groupées w: ouvrir dans le navigateur (le premier port est http) + space: ouvrir/réduire les conteneurs du pod enter: focus panneau principal [: onglet précédent ]: onglet suivant diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 93d3e89b..6208a948 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: draai een vooraf bedacht aangepaste opdracht b: view bulk commands w: open in browser (first port is http) + space: pod uitvouwen/invouwen enter: focus hoofdpaneel [: vorige tab ]: volgende tab diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 112e5ca1..a1773983 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: wykonaj predefiniowaną własną komende b: view bulk commands w: open in browser (first port is http) + space: rozwiń/zwiń pod enter: skup na głównym panelu [: poprzednia zakładka ]: następna zakładka diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md index 4b87a18a..88c7f9d6 100644 --- a/docs/keybindings/Keybindings_pt.md +++ b/docs/keybindings/Keybindings_pt.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: executar comando personalizado predefinido b: ver comandos em massa w: abrir no navegador (primeira porta é http) + space: expandir/recolher pod enter: focar no painel principal [: aba anterior ]: próxima aba diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md index 934d39b8..5fb47652 100644 --- a/docs/keybindings/Keybindings_tr.md +++ b/docs/keybindings/Keybindings_tr.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: önceden tanımlanmış özel komutu çalıştır b: view bulk commands w: open in browser (first port is http) + space: pod'u genişlet/daralt enter: ana panele odaklan [: önceki sekme ]: sonraki sekme diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index 3582ee62..3a855dfb 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -27,6 +27,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct c: 运行预定义的自定义命令 b: 查看批量命令 w: 在浏览器中打开(第一个端口为http) + space: 展开/折叠 pod enter: 聚焦主面板 [: 上一个选项卡 ]: 下一个选项卡