mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
feature: add pod support in the container pane
This commit is contained in:
parent
71c53a5c5e
commit
e5c209de93
16 changed files with 1460 additions and 622 deletions
1284
coverage.txt
1284
coverage.txt
File diff suppressed because it is too large
Load diff
66
pkg/commands/container_list_item.go
Normal file
66
pkg/commands/container_list_item.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package commands
|
||||
|
||||
// ContainerListItem represents either a pod or a container in the unified list view.
|
||||
// This allows the container panel to display both pods and containers.
|
||||
type ContainerListItem struct {
|
||||
IsPod bool
|
||||
Pod *Pod // Set if IsPod is true
|
||||
Container *Container // Set if IsPod is false
|
||||
Indent int // 0 for pods/standalone containers, 2 for containers in pods
|
||||
}
|
||||
|
||||
// ID returns the unique ID for the item.
|
||||
func (c *ContainerListItem) ID() string {
|
||||
if c.IsPod {
|
||||
return c.Pod.ID
|
||||
}
|
||||
return c.Container.ID
|
||||
}
|
||||
|
||||
// Name returns the display name for the item.
|
||||
func (c *ContainerListItem) Name() string {
|
||||
if c.IsPod {
|
||||
return c.Pod.Name
|
||||
}
|
||||
return c.Container.Name
|
||||
}
|
||||
|
||||
// State returns the state for the item.
|
||||
func (c *ContainerListItem) State() string {
|
||||
if c.IsPod {
|
||||
return c.Pod.State()
|
||||
}
|
||||
return c.Container.Summary.State
|
||||
}
|
||||
|
||||
// GetContainers returns the containers if this is a pod, nil otherwise.
|
||||
func (c *ContainerListItem) GetContainers() []*Container {
|
||||
if c.IsPod {
|
||||
return c.Pod.Containers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsInPod returns true if this is a container that belongs to a pod.
|
||||
func (c *ContainerListItem) IsInPod() bool {
|
||||
if c.IsPod {
|
||||
return false
|
||||
}
|
||||
return c.Container.Summary.Pod != ""
|
||||
}
|
||||
|
||||
// PodID returns the pod ID if this container is in a pod, empty string otherwise.
|
||||
func (c *ContainerListItem) PodID() string {
|
||||
if c.IsPod {
|
||||
return c.Pod.ID
|
||||
}
|
||||
return c.Container.Summary.Pod
|
||||
}
|
||||
|
||||
// PodName returns the pod name if this container is in a pod, empty string otherwise.
|
||||
func (c *ContainerListItem) PodName() string {
|
||||
if c.IsPod {
|
||||
return c.Pod.Name
|
||||
}
|
||||
return c.Container.Summary.PodName
|
||||
}
|
||||
36
pkg/commands/pod.go
Normal file
36
pkg/commands/pod.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Pod represents a Podman pod with its containers.
|
||||
type Pod struct {
|
||||
ID string
|
||||
Name string
|
||||
Summary PodSummary
|
||||
Containers []*Container // Non-infra containers in this pod
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
// State returns the pod state.
|
||||
func (p *Pod) State() string {
|
||||
return p.Summary.Status
|
||||
}
|
||||
|
||||
// HasContainers returns true if the pod has non-infra containers.
|
||||
func (p *Pod) HasContainers() bool {
|
||||
return len(p.Containers) > 0
|
||||
}
|
||||
|
||||
// GetRunningContainerCount returns the number of running containers in the pod.
|
||||
func (p *Pod) GetRunningContainerCount() int {
|
||||
count := 0
|
||||
for _, c := range p.Containers {
|
||||
if c.Summary.State == "running" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
|
@ -347,15 +347,35 @@ func calculateMemoryPercentageFromEntry(stats ContainerStatsEntry) float64 {
|
|||
return 0.0
|
||||
}
|
||||
|
||||
func (c *PodmanCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||
func (c *PodmanCommand) RefreshContainersAndServices(currentServices []*Service, currentItems []*ContainerListItem) ([]*ContainerListItem, []*Service, error) {
|
||||
c.ServiceMutex.Lock()
|
||||
defer c.ServiceMutex.Unlock()
|
||||
|
||||
// Extract existing containers from current items
|
||||
var currentContainers []*Container
|
||||
for _, item := range currentItems {
|
||||
if !item.IsPod && item.Container != nil {
|
||||
currentContainers = append(currentContainers, item.Container)
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := c.GetContainers(currentContainers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get pods
|
||||
ctx := context.Background()
|
||||
podSummaries, err := c.Runtime.ListPods(ctx)
|
||||
if err != nil {
|
||||
// Don't fail if pods can't be listed, just log and continue without pods
|
||||
c.Log.Warnf("Failed to list pods: %v", err)
|
||||
podSummaries = nil
|
||||
}
|
||||
|
||||
// Build the unified list
|
||||
items := c.buildContainerListItems(containers, podSummaries)
|
||||
|
||||
var services []*Service
|
||||
// we only need to get these services once because they won't change in the runtime of the program
|
||||
if currentServices != nil {
|
||||
|
|
@ -367,9 +387,88 @@ func (c *PodmanCommand) RefreshContainersAndServices(currentServices []*Service,
|
|||
}
|
||||
}
|
||||
|
||||
c.assignContainersToServices(containers, services)
|
||||
// Extract just containers for service assignment
|
||||
var containersForServices []*Container
|
||||
for _, item := range items {
|
||||
if !item.IsPod && item.Container != nil {
|
||||
containersForServices = append(containersForServices, item.Container)
|
||||
}
|
||||
}
|
||||
c.assignContainersToServices(containersForServices, services)
|
||||
|
||||
return containers, services, nil
|
||||
return items, services, nil
|
||||
}
|
||||
|
||||
// buildContainerListItems creates a unified list of pods and containers
|
||||
func (c *PodmanCommand) buildContainerListItems(containers []*Container, podSummaries []PodSummary) []*ContainerListItem {
|
||||
var items []*ContainerListItem
|
||||
|
||||
// Create a map of pod ID -> pod summary
|
||||
podMap := make(map[string]PodSummary)
|
||||
for _, ps := range podSummaries {
|
||||
podMap[ps.ID] = ps
|
||||
}
|
||||
|
||||
// Create a map of pod ID -> containers in that pod
|
||||
podContainers := make(map[string][]*Container)
|
||||
var standaloneContainers []*Container
|
||||
|
||||
for _, ctr := range containers {
|
||||
// Skip infra containers
|
||||
if ctr.Summary.IsInfra {
|
||||
continue
|
||||
}
|
||||
|
||||
if ctr.Summary.Pod != "" {
|
||||
podContainers[ctr.Summary.Pod] = append(podContainers[ctr.Summary.Pod], ctr)
|
||||
} else {
|
||||
standaloneContainers = append(standaloneContainers, ctr)
|
||||
}
|
||||
}
|
||||
|
||||
// Add pods and their containers
|
||||
for podID, ps := range podMap {
|
||||
// Create pod object
|
||||
pod := &Pod{
|
||||
ID: ps.ID,
|
||||
Name: ps.Name,
|
||||
Summary: ps,
|
||||
Containers: podContainers[podID],
|
||||
OSCommand: c.OSCommand,
|
||||
Log: c.Log,
|
||||
}
|
||||
|
||||
// Add pod item
|
||||
items = append(items, &ContainerListItem{
|
||||
IsPod: true,
|
||||
Pod: pod,
|
||||
Indent: 0,
|
||||
})
|
||||
|
||||
// Add containers in this pod with indent
|
||||
for _, ctr := range podContainers[podID] {
|
||||
// Set pod name on container if not already set
|
||||
if ctr.Summary.PodName == "" {
|
||||
ctr.Summary.PodName = ps.Name
|
||||
}
|
||||
items = append(items, &ContainerListItem{
|
||||
IsPod: false,
|
||||
Container: ctr,
|
||||
Indent: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add standalone containers
|
||||
for _, ctr := range standaloneContainers {
|
||||
items = append(items, &ContainerListItem{
|
||||
IsPod: false,
|
||||
Container: ctr,
|
||||
Indent: 0,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (c *PodmanCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ type ContainerRuntime interface {
|
|||
RemoveNetwork(ctx context.Context, name string) error
|
||||
PruneNetworks(ctx context.Context) error
|
||||
|
||||
// Pod operations
|
||||
ListPods(ctx context.Context) ([]PodSummary, error)
|
||||
|
||||
// Events streams container/image/volume/network events
|
||||
Events(ctx context.Context) (<-chan Event, <-chan error)
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ func (r *LibpodRuntime) ContainerTop(ctx context.Context, id string) ([]string,
|
|||
// Parse the result: first line is headers, rest are process rows
|
||||
// Each line is space-separated fields
|
||||
headers := splitFields(result[0])
|
||||
var processes [][]string
|
||||
processes := make([][]string, 0, len(result)-1)
|
||||
for _, line := range result[1:] {
|
||||
processes = append(processes, splitFields(line))
|
||||
}
|
||||
|
|
@ -309,6 +309,37 @@ func (r *LibpodRuntime) PruneNetworks(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Pod operations
|
||||
|
||||
// ListPods returns all pods.
|
||||
func (r *LibpodRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
|
||||
pods, err := r.runtime.GetAllPods()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]PodSummary, len(pods))
|
||||
for i, pod := range pods {
|
||||
status, err := pod.GetPodStatus()
|
||||
if err != nil {
|
||||
status = "unknown"
|
||||
}
|
||||
config := pod.Config()
|
||||
infraID := ""
|
||||
if config != nil {
|
||||
infraID = config.InfraContainerID
|
||||
}
|
||||
result[i] = PodSummary{
|
||||
ID: pod.ID(),
|
||||
Name: pod.Name(),
|
||||
Status: status,
|
||||
Created: pod.CreatedTime(),
|
||||
InfraID: infraID,
|
||||
Labels: pod.Labels(),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Events streams container runtime events.
|
||||
// For libpod, we use a polling approach since direct event streaming requires more setup.
|
||||
func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
|
||||
|
|
@ -361,6 +392,14 @@ func convertLibpodContainerList(ctrs []*libpod.Container) ([]ContainerSummary, e
|
|||
if len(config.Command) > 0 {
|
||||
command = config.Command[0]
|
||||
}
|
||||
// Get pod name if container is in a pod
|
||||
podName := ""
|
||||
if podID := ctr.PodID(); podID != "" {
|
||||
// Try to get pod name from config labels or leave empty
|
||||
if pn, ok := config.Labels["io.kubernetes.pod.name"]; ok {
|
||||
podName = pn
|
||||
}
|
||||
}
|
||||
result[i] = ContainerSummary{
|
||||
ID: ctr.ID(),
|
||||
Names: []string{ctr.Name()},
|
||||
|
|
@ -372,6 +411,8 @@ func convertLibpodContainerList(ctrs []*libpod.Container) ([]ContainerSummary, e
|
|||
Status: state.String(),
|
||||
Labels: config.Labels,
|
||||
Pod: ctr.PodID(),
|
||||
PodName: podName,
|
||||
IsInfra: ctr.IsInfra(),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ func (r *LibpodRuntime) PruneNetworks(ctx context.Context) error {
|
|||
return ErrLibpodNotAvailable
|
||||
}
|
||||
|
||||
// Pod operations - all return ErrLibpodNotAvailable
|
||||
|
||||
func (r *LibpodRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
|
||||
return nil, ErrLibpodNotAvailable
|
||||
}
|
||||
|
||||
// Events returns an error channel on non-Linux platforms.
|
||||
func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
|
||||
errChan := make(chan error, 1)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ type MockRuntime struct {
|
|||
RemoveNetworkFunc func(ctx context.Context, name string) error
|
||||
PruneNetworksFunc func(ctx context.Context) error
|
||||
|
||||
// Pod operation mocks
|
||||
ListPodsFunc func(ctx context.Context) ([]PodSummary, error)
|
||||
|
||||
// Event mock
|
||||
EventsFunc func(ctx context.Context) (<-chan Event, <-chan error)
|
||||
|
||||
|
|
@ -251,6 +254,16 @@ func (m *MockRuntime) PruneNetworks(ctx context.Context) error {
|
|||
return ErrMockNotImplemented
|
||||
}
|
||||
|
||||
// Pod operations
|
||||
|
||||
func (m *MockRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
|
||||
m.recordCall("ListPods")
|
||||
if m.ListPodsFunc != nil {
|
||||
return m.ListPodsFunc(ctx)
|
||||
}
|
||||
return nil, ErrMockNotImplemented
|
||||
}
|
||||
|
||||
// Events
|
||||
|
||||
func (m *MockRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package commands
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -275,6 +276,45 @@ func (r *SocketRuntime) PruneNetworks(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// podPsJSON represents the JSON output of `podman pod ps --format json`
|
||||
type podPsJSON struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Status string `json:"Status"`
|
||||
Created string `json:"Created"`
|
||||
InfraID string `json:"InfraId"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
// ListPods returns all pods.
|
||||
func (r *SocketRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
|
||||
// Use podman CLI since there's no pods binding package
|
||||
cmd := exec.CommandContext(ctx, "podman", "pod", "ps", "--format", "json")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pods []podPsJSON
|
||||
if err := json.Unmarshal(output, &pods); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]PodSummary, len(pods))
|
||||
for i, p := range pods {
|
||||
created, _ := time.Parse(time.RFC3339, p.Created)
|
||||
result[i] = PodSummary{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Status: p.Status,
|
||||
Created: created,
|
||||
InfraID: p.InfraID,
|
||||
Labels: p.Labels,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Events streams container runtime events.
|
||||
func (r *SocketRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
|
||||
eventChan := make(chan Event)
|
||||
|
|
@ -346,6 +386,7 @@ func convertPodmanContainerList(podmanContainers []entities.ListContainer) []Con
|
|||
SizeRootFs: sizeRootFs,
|
||||
Pod: c.Pod,
|
||||
PodName: c.PodName,
|
||||
IsInfra: c.IsInfra,
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type ContainerSummary struct {
|
|||
// Podman-specific fields
|
||||
Pod string
|
||||
PodName string
|
||||
IsInfra bool
|
||||
}
|
||||
|
||||
// PortMapping represents a container port mapping.
|
||||
|
|
@ -424,3 +425,13 @@ type EventActor struct {
|
|||
ID string
|
||||
Attributes map[string]string
|
||||
}
|
||||
|
||||
// PodSummary provides runtime-agnostic pod information.
|
||||
type PodSummary struct {
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
Created time.Time
|
||||
InfraID string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,3 +142,74 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Pod logs rendering
|
||||
|
||||
func (gui *Gui) renderPodLogsToMain(pod *commands.Pod) tasks.TaskFunc {
|
||||
return gui.NewTickerTask(TickerTaskOpts{
|
||||
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||
gui.renderPodLogsToMainAux(pod, ctx, notifyStopped)
|
||||
},
|
||||
Duration: time.Millisecond * 200,
|
||||
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||
Autoscroll: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderPodLogsToMainAux(pod *commands.Pod, ctx context.Context, notifyStopped chan struct{}) {
|
||||
gui.clearMainView()
|
||||
defer func() {
|
||||
notifyStopped <- struct{}{}
|
||||
}()
|
||||
|
||||
mainView := gui.Views.Main
|
||||
|
||||
if err := gui.writePodLogs(pod, ctx, mainView); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
// Wait for context cancellation
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (gui *Gui) writePodLogs(pod *commands.Pod, ctx context.Context, writer io.Writer) error {
|
||||
// Build podman pod logs command
|
||||
args := []string{"pod", "logs", "--follow", "--color"}
|
||||
|
||||
if gui.Config.UserConfig.Logs.Timestamps {
|
||||
args = append(args, "--timestamps")
|
||||
}
|
||||
if gui.Config.UserConfig.Logs.Since != "" {
|
||||
args = append(args, "--since", gui.Config.UserConfig.Logs.Since)
|
||||
}
|
||||
if gui.Config.UserConfig.Logs.Tail != "" {
|
||||
args = append(args, "--tail", gui.Config.UserConfig.Logs.Tail)
|
||||
}
|
||||
args = append(args, pod.Name)
|
||||
|
||||
cmd := pod.OSCommand.NewCmd("podman", args...)
|
||||
cmd.Stdout = writer
|
||||
cmd.Stderr = writer
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for context cancellation or command completion
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if err := pod.OSCommand.Kill(cmd); err != nil {
|
||||
gui.Log.Warn(err)
|
||||
}
|
||||
return nil
|
||||
case err := <-done:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] {
|
||||
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.ContainerListItem] {
|
||||
// Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context.
|
||||
isStandaloneContainer := func(container *commands.Container) bool {
|
||||
if container.OneOff || container.ServiceName == "" {
|
||||
|
|
@ -30,58 +30,60 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
})
|
||||
}
|
||||
|
||||
return &panels.SideListPanel[*commands.Container]{
|
||||
ContextState: &panels.ContextState[*commands.Container]{
|
||||
GetMainTabs: func() []panels.MainTab[*commands.Container] {
|
||||
return []panels.MainTab[*commands.Container]{
|
||||
return &panels.SideListPanel[*commands.ContainerListItem]{
|
||||
ContextState: &panels.ContextState[*commands.ContainerListItem]{
|
||||
GetMainTabs: func() []panels.MainTab[*commands.ContainerListItem] {
|
||||
return []panels.MainTab[*commands.ContainerListItem]{
|
||||
{
|
||||
Key: "logs",
|
||||
Title: gui.Tr.LogsTitle,
|
||||
Render: gui.renderContainerLogsToMain,
|
||||
Render: gui.renderContainerListItemLogs,
|
||||
},
|
||||
{
|
||||
Key: "stats",
|
||||
Title: gui.Tr.StatsTitle,
|
||||
Render: gui.renderContainerStats,
|
||||
Render: gui.renderContainerListItemStats,
|
||||
},
|
||||
{
|
||||
Key: "env",
|
||||
Title: gui.Tr.EnvTitle,
|
||||
Render: gui.renderContainerEnv,
|
||||
Render: gui.renderContainerListItemEnv,
|
||||
},
|
||||
{
|
||||
Key: "config",
|
||||
Title: gui.Tr.ConfigTitle,
|
||||
Render: gui.renderContainerConfig,
|
||||
Render: gui.renderContainerListItemConfig,
|
||||
},
|
||||
{
|
||||
Key: "top",
|
||||
Title: gui.Tr.TopTitle,
|
||||
Render: gui.renderContainerTop,
|
||||
Render: gui.renderContainerListItemTop,
|
||||
},
|
||||
}
|
||||
},
|
||||
GetItemContextCacheKey: func(container *commands.Container) string {
|
||||
// Including the container state in the cache key so that if the container
|
||||
// restarts we re-read the logs. In the past we've had some glitchiness
|
||||
// where a container restarts but the new logs don't get read.
|
||||
// Note that this might be jarring if we have a lot of logs and the container
|
||||
// restarts a lot, so let's keep an eye on it.
|
||||
return "containers-" + container.ID + "-" + container.Summary.State
|
||||
GetItemContextCacheKey: func(item *commands.ContainerListItem) string {
|
||||
// Including the state in the cache key so that if the container/pod
|
||||
// restarts we re-read the logs.
|
||||
return "containers-" + item.ID() + "-" + item.State()
|
||||
},
|
||||
},
|
||||
ListPanel: panels.ListPanel[*commands.Container]{
|
||||
List: panels.NewFilteredList[*commands.Container](),
|
||||
ListPanel: panels.ListPanel[*commands.ContainerListItem]{
|
||||
List: panels.NewFilteredList[*commands.ContainerListItem](),
|
||||
View: gui.Views.Containers,
|
||||
},
|
||||
NoItemsMessage: gui.Tr.NoContainers,
|
||||
Gui: gui.intoInterface(),
|
||||
// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created)
|
||||
// and sorted by name if c.SortContainersByState is false
|
||||
Sort: func(a *commands.Container, b *commands.Container) bool {
|
||||
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
||||
// Sort items: pods first (with their containers grouped), then standalone containers
|
||||
Sort: func(a *commands.ContainerListItem, b *commands.ContainerListItem) bool {
|
||||
return sortContainerListItems(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
||||
},
|
||||
Filter: func(container *commands.Container) bool {
|
||||
Filter: func(item *commands.ContainerListItem) bool {
|
||||
// Pods are always shown
|
||||
if item.IsPod {
|
||||
return true
|
||||
}
|
||||
|
||||
container := item.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.
|
||||
|
|
@ -95,8 +97,8 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
|
||||
return true
|
||||
},
|
||||
GetTableCells: func(container *commands.Container) []string {
|
||||
return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container)
|
||||
GetTableCells: func(item *commands.ContainerListItem) []string {
|
||||
return presentation.GetContainerListItemDisplayStrings(&gui.Config.UserConfig.Gui, item)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +123,137 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo
|
|||
return containerStates[a.Summary.State] < containerStates[b.Summary.State]
|
||||
}
|
||||
|
||||
// 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
|
||||
aInPod := a.IsPod || a.PodID() != ""
|
||||
bInPod := b.IsPod || b.PodID() != ""
|
||||
|
||||
if aInPod && !bInPod {
|
||||
return true // Pods/pod containers before standalone
|
||||
}
|
||||
if !aInPod && bInPod {
|
||||
return false
|
||||
}
|
||||
|
||||
// Both in same category (pod-related or standalone)
|
||||
if legacySort {
|
||||
return aKey < bKey
|
||||
}
|
||||
|
||||
// Sort by state, then by key
|
||||
stateA := containerStates[a.State()]
|
||||
stateB := containerStates[b.State()]
|
||||
if stateA == stateB {
|
||||
return aKey < bKey
|
||||
}
|
||||
return stateA < stateB
|
||||
}
|
||||
|
||||
// Wrapper functions that delegate to container or pod rendering
|
||||
|
||||
func (gui *Gui) renderContainerListItemLogs(item *commands.ContainerListItem) tasks.TaskFunc {
|
||||
if item.IsPod {
|
||||
return gui.renderPodLogsToMain(item.Pod)
|
||||
}
|
||||
return gui.renderContainerLogsToMain(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerListItemStats(item *commands.ContainerListItem) tasks.TaskFunc {
|
||||
if item.IsPod {
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return fmt.Sprintf("Pod: %s\nStatus: %s\nContainers: %d\n\nStats not available for pods. Select a container to view stats.",
|
||||
item.Pod.Name, item.Pod.State(), len(item.Pod.Containers))
|
||||
})
|
||||
}
|
||||
return gui.renderContainerStats(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerListItemEnv(item *commands.ContainerListItem) tasks.TaskFunc {
|
||||
if item.IsPod {
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return "Environment variables not available for pods. Select a container to view environment."
|
||||
})
|
||||
}
|
||||
return gui.renderContainerEnv(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerListItemConfig(item *commands.ContainerListItem) tasks.TaskFunc {
|
||||
if item.IsPod {
|
||||
return gui.renderPodConfig(item.Pod)
|
||||
}
|
||||
return gui.renderContainerConfig(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerListItemTop(item *commands.ContainerListItem) tasks.TaskFunc {
|
||||
if item.IsPod {
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
return "Process list not available for pods. Select a container to view processes."
|
||||
})
|
||||
}
|
||||
return gui.renderContainerTop(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderPodConfig(pod *commands.Pod) tasks.TaskFunc {
|
||||
return gui.NewSimpleRenderStringTask(func() string {
|
||||
padding := 10
|
||||
output := ""
|
||||
output += utils.ColoredString("Pod Information\n\n", color.FgCyan)
|
||||
output += utils.WithPadding("ID: ", padding) + pod.ID + "\n"
|
||||
output += utils.WithPadding("Name: ", padding) + pod.Name + "\n"
|
||||
output += utils.WithPadding("Status: ", padding) + pod.State() + "\n"
|
||||
output += utils.WithPadding("Created: ", padding) + pod.Summary.Created.Format(time.RFC3339) + "\n"
|
||||
output += fmt.Sprintf("%s%d\n", utils.WithPadding("Containers: ", padding), len(pod.Containers))
|
||||
|
||||
if len(pod.Containers) > 0 {
|
||||
output += "\n" + utils.ColoredString("Containers in this pod:\n", color.FgYellow)
|
||||
for _, c := range pod.Containers {
|
||||
stateColor := color.FgWhite
|
||||
switch c.Summary.State {
|
||||
case "running":
|
||||
stateColor = color.FgGreen
|
||||
case "exited":
|
||||
stateColor = color.FgRed
|
||||
}
|
||||
output += fmt.Sprintf(" - %s (%s)\n", c.Name, utils.ColoredString(c.Summary.State, stateColor))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc {
|
||||
return gui.NewSimpleRenderStringTask(func() string { return gui.containerEnv(container) })
|
||||
}
|
||||
|
|
@ -179,9 +312,9 @@ func (gui *Gui) containerConfigStr(container *commands.Container) string {
|
|||
output += "\n"
|
||||
for _, mount := range container.Details.Mounts {
|
||||
if mount.Type == "volume" {
|
||||
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name)
|
||||
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Name)
|
||||
} else {
|
||||
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination)
|
||||
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Source, mount.Destination)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -302,11 +435,16 @@ func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Pod operations not yet supported")
|
||||
}
|
||||
|
||||
ctr := item.Container
|
||||
handleMenuPress := func(force bool, removeVolumes bool) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if err := ctr.Remove(force, removeVolumes); err != nil {
|
||||
|
|
@ -357,20 +495,29 @@ func (gui *Gui) PauseContainer(container *commands.Container) error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.PauseContainer(ctr)
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Pod operations not yet supported")
|
||||
}
|
||||
|
||||
return gui.PauseContainer(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Pod operations not yet supported")
|
||||
}
|
||||
|
||||
ctr := item.Container
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||
if err := ctr.Stop(); err != nil {
|
||||
|
|
@ -383,11 +530,16 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Pod operations not yet supported")
|
||||
}
|
||||
|
||||
ctr := item.Container
|
||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||
if err := ctr.Restart(); err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
|
|
@ -398,11 +550,16 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Pod operations not yet supported")
|
||||
}
|
||||
|
||||
ctr := item.Container
|
||||
c, err := ctr.Attach()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
|
|
@ -424,23 +581,32 @@ func (gui *Gui) handlePruneContainers() error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.renderLogsToStdout(ctr)
|
||||
if item.IsPod {
|
||||
// TODO: implement pod logs to stdout
|
||||
return gui.createErrorPanel("Pod logs to stdout not yet supported")
|
||||
}
|
||||
|
||||
gui.renderLogsToStdout(item.Container)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.containerExecShell(ctr)
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Cannot exec into a pod. Select a container instead.")
|
||||
}
|
||||
|
||||
return gui.containerExecShell(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) containerExecShell(container *commands.Container) error {
|
||||
|
|
@ -456,13 +622,17 @@ func (gui *Gui) containerExecShell(container *commands.Container) error {
|
|||
}
|
||||
|
||||
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Custom commands not yet supported for pods")
|
||||
}
|
||||
|
||||
commandObject := gui.PodmanCommand.NewCommandObject(commands.CommandObject{
|
||||
Container: ctr,
|
||||
Container: item.Container,
|
||||
})
|
||||
|
||||
customCommands := gui.Config.UserConfig.CustomCommands.Containers
|
||||
|
|
@ -473,11 +643,13 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
|
|||
func (gui *Gui) handleStopContainers() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if err := ctr.Stop(); err != nil {
|
||||
for _, item := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if !item.IsPod && item.Container != nil {
|
||||
if err := item.Container.Stop(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
|
@ -487,11 +659,13 @@ func (gui *Gui) handleStopContainers() error {
|
|||
func (gui *Gui) handleRemoveContainers() error {
|
||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if err := ctr.Remove(true, false); err != nil {
|
||||
for _, item := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if !item.IsPod && item.Container != nil {
|
||||
if err := item.Container.Remove(true, false); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
|
@ -522,12 +696,16 @@ func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
|||
|
||||
// Open first port in browser
|
||||
func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||
item, err := gui.Panels.Containers.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.openContainerInBrowser(ctr)
|
||||
if item.IsPod {
|
||||
return gui.createErrorPanel("Cannot open pod in browser. Select a container instead.")
|
||||
}
|
||||
|
||||
return gui.openContainerInBrowser(item.Container)
|
||||
}
|
||||
|
||||
func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ type Gui struct {
|
|||
type Panels struct {
|
||||
Projects *panels.SideListPanel[*commands.Project]
|
||||
Services *panels.SideListPanel[*commands.Service]
|
||||
Containers *panels.SideListPanel[*commands.Container]
|
||||
Containers *panels.SideListPanel[*commands.ContainerListItem]
|
||||
Images *panels.SideListPanel[*commands.Image]
|
||||
Volumes *panels.SideListPanel[*commands.Volume]
|
||||
Networks *panels.SideListPanel[*commands.Network]
|
||||
|
|
@ -290,7 +290,14 @@ func (gui *Gui) setPanels() {
|
|||
}
|
||||
|
||||
func (gui *Gui) updateContainerDetails() error {
|
||||
return gui.PodmanCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems())
|
||||
// Extract containers from ContainerListItems
|
||||
var containers []*commands.Container
|
||||
for _, item := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if !item.IsPod && item.Container != nil {
|
||||
containers = append(containers, item.Container)
|
||||
}
|
||||
}
|
||||
return gui.PodmanCommand.RefreshContainerDetails(containers)
|
||||
}
|
||||
|
||||
func (gui *Gui) refresh() {
|
||||
|
|
@ -469,9 +476,9 @@ func (gui *Gui) monitorContainerStats(ctx context.Context) {
|
|||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
for _, container := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if !container.MonitoringStats {
|
||||
go gui.PodmanCommand.CreateClientStatMonitor(container)
|
||||
for _, item := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if !item.IsPod && item.Container != nil && !item.Container.MonitoringStats {
|
||||
go gui.PodmanCommand.CreateClientStatMonitor(item.Container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/asciigraph"
|
||||
"github.com/christophe-duc/lazypodman/pkg/commands"
|
||||
"github.com/christophe-duc/lazypodman/pkg/config"
|
||||
"github.com/christophe-duc/lazypodman/pkg/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/asciigraph"
|
||||
"github.com/mcuadros/go-lookup"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,91 @@ 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 {
|
||||
if item.IsPod {
|
||||
return GetPodDisplayStrings(guiConfig, item.Pod)
|
||||
}
|
||||
|
||||
// Add indentation for containers in pods
|
||||
strings := GetContainerDisplayStrings(guiConfig, item.Container)
|
||||
if item.Indent > 0 {
|
||||
strings[2] = fmt.Sprintf("%s%s", createIndent(item.Indent), strings[2])
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
// GetPodDisplayStrings returns display strings for a pod
|
||||
func GetPodDisplayStrings(guiConfig *config.GuiConfig, pod *commands.Pod) []string {
|
||||
return []string{
|
||||
getPodDisplayStatus(guiConfig, pod),
|
||||
"", // No substatus for pods
|
||||
utils.ColoredString(pod.Name, color.FgCyan),
|
||||
"", // No CPU% for pods
|
||||
"", // No ports for pods
|
||||
utils.ColoredString(fmt.Sprintf("(%d containers)", len(pod.Containers)), color.FgMagenta),
|
||||
}
|
||||
}
|
||||
|
||||
func getPodDisplayStatus(guiConfig *config.GuiConfig, pod *commands.Pod) string {
|
||||
shortStatusMap := map[string]string{
|
||||
"Running": "R",
|
||||
"Degraded": "D",
|
||||
"Exited": "X",
|
||||
"Dead": "!",
|
||||
"Created": "C",
|
||||
}
|
||||
|
||||
iconStatusMap := map[string]rune{
|
||||
"Running": '▶',
|
||||
"Degraded": '◐',
|
||||
"Exited": '⨯',
|
||||
"Dead": '!',
|
||||
"Created": '+',
|
||||
}
|
||||
|
||||
var podState string
|
||||
switch guiConfig.ContainerStatusHealthStyle {
|
||||
case "short":
|
||||
if s, ok := shortStatusMap[pod.State()]; ok {
|
||||
podState = s
|
||||
} else {
|
||||
podState = "?"
|
||||
}
|
||||
case "icon":
|
||||
if r, ok := iconStatusMap[pod.State()]; ok {
|
||||
podState = string(r)
|
||||
} else {
|
||||
podState = "?"
|
||||
}
|
||||
default:
|
||||
podState = pod.State()
|
||||
}
|
||||
|
||||
return utils.ColoredString(podState, getPodColor(pod))
|
||||
}
|
||||
|
||||
func getPodColor(pod *commands.Pod) color.Attribute {
|
||||
switch pod.State() {
|
||||
case "Running":
|
||||
return color.FgGreen
|
||||
case "Degraded":
|
||||
return color.FgYellow
|
||||
case "Exited":
|
||||
return color.FgRed
|
||||
case "Dead":
|
||||
return color.FgRed
|
||||
case "Created":
|
||||
return color.FgCyan
|
||||
default:
|
||||
return color.FgWhite
|
||||
}
|
||||
}
|
||||
|
||||
func createIndent(spaces int) string {
|
||||
return fmt.Sprintf("%*s", spaces, "")
|
||||
}
|
||||
|
||||
func displayContainerImage(container *commands.Container) string {
|
||||
return strings.TrimPrefix(container.Summary.Image, "sha256:")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ type TranslationSet struct {
|
|||
FilterList string
|
||||
OpenInBrowser string
|
||||
SortContainersByState string
|
||||
|
||||
LogsTitle string
|
||||
ConfigTitle string
|
||||
EnvTitle string
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue