mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 07:11:01 +00:00
Compare commits
No commits in common. "master" and "v0.24.3" have entirely different histories.
30 changed files with 136 additions and 590 deletions
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"hooks": {
|
|
||||||
"PostToolUse": [
|
|
||||||
{
|
|
||||||
"matcher": "Edit|Write|MultiEdit",
|
|
||||||
"hooks": [
|
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; case \"$f\" in *.go) gofumpt -w \"$f\" ;; esac; } 2>/dev/null || true"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
## Build & test
|
|
||||||
- All Go commands need `GOFLAGS=-mod=vendor` (deps are vendored, including the `jesseduffield/gocui` fork and the Docker SDK).
|
|
||||||
- Unit tests: `GOFLAGS=-mod=vendor go test ./...`
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
lazydocker:
|
lazydocker:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
<kbd>[</kbd>: vorheriges Tab
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
<kbd>]</kbd>: nächstes Tab
|
<kbd>]</kbd>: nächstes Tab
|
||||||
<kbd>/</kbd>: filter list
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Container
|
## Container
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: focus main panel
|
<kbd>enter</kbd>: focus main panel
|
||||||
<kbd>[</kbd>: previous tab
|
<kbd>[</kbd>: previous tab
|
||||||
<kbd>]</kbd>: next tab
|
<kbd>]</kbd>: next tab
|
||||||
<kbd>/</kbd>: filter list
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Containers
|
## Containers
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: enfocar panel principal
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
<kbd>[</kbd>: anterior pestaña
|
<kbd>[</kbd>: anterior pestaña
|
||||||
<kbd>]</kbd>: siguiente pestaña
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
<kbd>/</kbd>: filtrar lista
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Contenedores
|
## Contenedores
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: focus panneau principal
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
<kbd>[</kbd>: onglet précédent
|
<kbd>[</kbd>: onglet précédent
|
||||||
<kbd>]</kbd>: onglet suivant
|
<kbd>]</kbd>: onglet suivant
|
||||||
<kbd>/</kbd>: filter list
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Conteneurs
|
## Conteneurs
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: focus hoofdpaneel
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
<kbd>[</kbd>: vorige tab
|
<kbd>[</kbd>: vorige tab
|
||||||
<kbd>]</kbd>: volgende tab
|
<kbd>]</kbd>: volgende tab
|
||||||
<kbd>/</kbd>: filter list
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Containers
|
## Containers
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: skup na głównym panelu
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
<kbd>[</kbd>: poprzednia zakładka
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
<kbd>]</kbd>: następna zakładka
|
<kbd>]</kbd>: następna zakładka
|
||||||
<kbd>/</kbd>: filter list
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Kontenery
|
## Kontenery
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: focar no painel principal
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
<kbd>[</kbd>: aba anterior
|
<kbd>[</kbd>: aba anterior
|
||||||
<kbd>]</kbd>: próxima aba
|
<kbd>]</kbd>: próxima aba
|
||||||
<kbd>/</kbd>: filtrar lista
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Contêineres
|
## Contêineres
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: ana panele odaklan
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
<kbd>[</kbd>: önceki sekme
|
<kbd>[</kbd>: önceki sekme
|
||||||
<kbd>]</kbd>: sonraki sekme
|
<kbd>]</kbd>: sonraki sekme
|
||||||
<kbd>/</kbd>: filter list
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Konteynerler
|
## Konteynerler
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||||
<kbd>enter</kbd>: 聚焦主面板
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
<kbd>[</kbd>: 上一个选项卡
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
<kbd>]</kbd>: 下一个选项卡
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
<kbd>/</kbd>: 过滤列表
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## 容器
|
## 容器
|
||||||
|
|
|
||||||
4
main.go
4
main.go
|
|
@ -29,7 +29,6 @@ var (
|
||||||
configFlag = false
|
configFlag = false
|
||||||
debuggingFlag = false
|
debuggingFlag = false
|
||||||
composeFiles []string
|
composeFiles []string
|
||||||
projectName string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -52,7 +51,6 @@ func main() {
|
||||||
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
||||||
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
|
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
|
||||||
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
|
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
|
||||||
flaggy.String(&projectName, "p", "project", "Specify a docker compose project name")
|
|
||||||
flaggy.SetVersion(info)
|
flaggy.SetVersion(info)
|
||||||
|
|
||||||
flaggy.Parse()
|
flaggy.Parse()
|
||||||
|
|
@ -73,7 +71,7 @@ func main() {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
|
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func Generate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateAtDir(dir string) {
|
func generateAtDir(dir string) {
|
||||||
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
|
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,12 +57,6 @@ func (c *Container) Remove(options container.RemoveOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the container
|
|
||||||
func (c *Container) Start() error {
|
|
||||||
c.Log.Warn(fmt.Sprintf("starting container %s", c.Name))
|
|
||||||
return c.Client.ContainerStart(context.Background(), c.ID, container.StartOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the container
|
// Stop stops the container
|
||||||
func (c *Container) Stop() error {
|
func (c *Container) Stop() error {
|
||||||
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
|
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ import (
|
||||||
ogLog "log"
|
ogLog "log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -41,11 +39,9 @@ type DockerCommand struct {
|
||||||
Config *config.AppConfig
|
Config *config.AppConfig
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
InDockerComposeProject bool
|
InDockerComposeProject bool
|
||||||
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
|
ErrorChan chan error
|
||||||
LocalProjectName string
|
ContainerMutex deadlock.Mutex
|
||||||
ErrorChan chan error
|
ServiceMutex deadlock.Mutex
|
||||||
ContainerMutex deadlock.Mutex
|
|
||||||
ServiceMutex deadlock.Mutex
|
|
||||||
|
|
||||||
Closers []io.Closer
|
Closers []io.Closer
|
||||||
}
|
}
|
||||||
|
|
@ -65,40 +61,15 @@ type CommandObject struct {
|
||||||
Image *Image
|
Image *Image
|
||||||
Volume *Volume
|
Volume *Volume
|
||||||
Network *Network
|
Network *Network
|
||||||
Project *Project
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
|
// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
|
||||||
func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||||
defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose}
|
defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose}
|
||||||
_ = mergo.Merge(&defaultObj, obj)
|
_ = mergo.Merge(&defaultObj, obj)
|
||||||
|
|
||||||
// When operating on a specific project, include -p flag so that
|
|
||||||
// docker compose targets the correct project.
|
|
||||||
if obj.Service != nil && obj.Service.ProjectName != "" {
|
|
||||||
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Service.ProjectName)
|
|
||||||
} else if obj.Project != nil && obj.Project.Name != "" {
|
|
||||||
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Project.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultObj
|
return defaultObj
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDockerClient creates a Docker client with the given host.
|
|
||||||
// We avoid using client.FromEnv because it includes WithVersionFromEnv() which
|
|
||||||
// sets manualOverride=true when DOCKER_API_VERSION is set, preventing API version
|
|
||||||
// negotiation even when WithAPIVersionNegotiation() is specified.
|
|
||||||
// Instead, we explicitly configure only what we need, and rely on proper
|
|
||||||
// API version negotiation to support older Docker daemons.
|
|
||||||
// See https://github.com/jesseduffield/lazydocker/issues/715
|
|
||||||
func newDockerClient(dockerHost string) (*client.Client, error) {
|
|
||||||
return client.NewClientWithOpts(
|
|
||||||
client.WithTLSClientConfigFromEnv(),
|
|
||||||
client.WithAPIVersionNegotiation(),
|
|
||||||
client.WithHost(dockerHost),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDockerCommand it runs docker commands
|
// NewDockerCommand it runs docker commands
|
||||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
||||||
dockerHost, err := determineDockerHost()
|
dockerHost, err := determineDockerHost()
|
||||||
|
|
@ -125,7 +96,7 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
|
||||||
dockerHost = dockerHostFromEnv
|
dockerHost = dockerHostFromEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := newDockerClient(dockerHost)
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithHost(dockerHost))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ogLog.Fatal(err)
|
ogLog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -154,25 +125,9 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
|
||||||
log.Warn(err.Error())
|
log.Warn(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the user passes -p outside of a compose directory, treat it as the
|
|
||||||
// local project so the project/services panels still appear and filtering
|
|
||||||
// is applied. Inside a compose dir, LocalProjectName is derived from
|
|
||||||
// container labels later in RefreshContainersAndServices.
|
|
||||||
if !dockerCommand.InDockerComposeProject && config.ProjectName != "" {
|
|
||||||
dockerCommand.LocalProjectName = config.ProjectName
|
|
||||||
}
|
|
||||||
|
|
||||||
return dockerCommand, nil
|
return dockerCommand, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsProjectScoped reports whether lazydocker should be scoped to a single
|
|
||||||
// compose project — either because we're inside a compose directory or
|
|
||||||
// because the user passed -p. When false, the project/services panels are
|
|
||||||
// hidden and all containers are shown in a flat list.
|
|
||||||
func (c *DockerCommand) IsProjectScoped() bool {
|
|
||||||
return c.InDockerComposeProject || c.Config.ProjectName != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
|
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
|
||||||
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
|
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
|
||||||
return
|
return
|
||||||
|
|
@ -223,7 +178,7 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
|
||||||
container.MonitoringStats = false
|
container.MonitoringStats = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
|
func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||||
c.ServiceMutex.Lock()
|
c.ServiceMutex.Lock()
|
||||||
defer c.ServiceMutex.Unlock()
|
defer c.ServiceMutex.Unlock()
|
||||||
|
|
||||||
|
|
@ -232,136 +187,27 @@ func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Contai
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive services from container labels (covers all projects)
|
var services []*Service
|
||||||
services := c.GetServicesFromContainers(containers)
|
// we only need to get these services once because they won't change in the runtime of the program
|
||||||
|
if currentServices != nil {
|
||||||
var composeServices []*Service
|
services = currentServices
|
||||||
if c.InDockerComposeProject {
|
} else {
|
||||||
composeServices, err = c.GetServices()
|
services, err = c.GetServices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log.Warn("Failed to get compose services: " + err.Error())
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the local project name before merging services, since
|
|
||||||
// mergeServices needs it. We match compose service names against container
|
|
||||||
// labels to handle cases where the project name differs from the directory
|
|
||||||
// name (e.g. a `name:` directive in the compose file).
|
|
||||||
if c.LocalProjectName == "" && c.InDockerComposeProject && composeServices != nil {
|
|
||||||
for _, ctr := range containers {
|
|
||||||
if ctr.ProjectName == "" || ctr.ServiceName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, svc := range composeServices {
|
|
||||||
if ctr.ServiceName == svc.Name {
|
|
||||||
c.LocalProjectName = ctr.ProjectName
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.LocalProjectName != "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fall back to directory name
|
|
||||||
if c.LocalProjectName == "" && c.Config.ProjectDir != "" {
|
|
||||||
c.LocalProjectName = filepath.Base(c.Config.ProjectDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge compose services (which include stopped services) with
|
|
||||||
// container-derived services from all projects
|
|
||||||
if composeServices != nil {
|
|
||||||
services = c.mergeServices(services, composeServices)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.assignContainersToServices(containers, services)
|
c.assignContainersToServices(containers, services)
|
||||||
|
|
||||||
return containers, services, nil
|
return containers, services, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServicesFromContainers derives services from container labels for all projects
|
|
||||||
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service {
|
|
||||||
// Use project+service as key to avoid duplicates
|
|
||||||
type serviceKey struct {
|
|
||||||
project string
|
|
||||||
service string
|
|
||||||
}
|
|
||||||
seen := make(map[serviceKey]bool)
|
|
||||||
services := make([]*Service, 0, len(containers))
|
|
||||||
|
|
||||||
for _, ctr := range containers {
|
|
||||||
if ctr.ServiceName == "" || ctr.OneOff {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
|
|
||||||
if seen[key] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[key] = true
|
|
||||||
services = append(services, &Service{
|
|
||||||
Name: ctr.ServiceName,
|
|
||||||
ID: ctr.ProjectName + "-" + ctr.ServiceName,
|
|
||||||
ProjectName: ctr.ProjectName,
|
|
||||||
OSCommand: c.OSCommand,
|
|
||||||
Log: c.Log,
|
|
||||||
DockerCommand: c,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return services
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeServices merges compose services (which may lack ProjectName) with
|
|
||||||
// container-derived services. Compose services take priority because they
|
|
||||||
// include services without running containers.
|
|
||||||
func (c *DockerCommand) mergeServices(containerServices []*Service, composeServices []*Service) []*Service {
|
|
||||||
// Set project name on compose services
|
|
||||||
for _, svc := range composeServices {
|
|
||||||
if svc.ProjectName == "" {
|
|
||||||
svc.ProjectName = c.LocalProjectName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a set of compose service names for the local project
|
|
||||||
composeServiceNames := make(map[string]bool)
|
|
||||||
for _, svc := range composeServices {
|
|
||||||
composeServiceNames[svc.Name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start with compose services, then add container-derived services
|
|
||||||
// that aren't already covered by compose (i.e. from other projects)
|
|
||||||
result := make([]*Service, 0, len(composeServices)+len(containerServices))
|
|
||||||
result = append(result, composeServices...)
|
|
||||||
|
|
||||||
for _, svc := range containerServices {
|
|
||||||
if svc.ProjectName == c.LocalProjectName && composeServiceNames[svc.Name] {
|
|
||||||
continue // already covered by compose service
|
|
||||||
}
|
|
||||||
result = append(result, svc)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProjectNames returns all unique project names from containers
|
|
||||||
func (c *DockerCommand) GetProjectNames(containers []*Container) []string {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
var names []string
|
|
||||||
for _, ctr := range containers {
|
|
||||||
if ctr.ProjectName != "" && !seen[ctr.ProjectName] {
|
|
||||||
seen[ctr.ProjectName] = true
|
|
||||||
names = append(names, ctr.ProjectName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
||||||
L:
|
L:
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
|
if !ctr.OneOff && ctr.ServiceName == service.Name {
|
||||||
service.Container = ctr
|
service.Container = ctr
|
||||||
continue L
|
continue L
|
||||||
}
|
}
|
||||||
|
|
@ -450,8 +296,7 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
|
||||||
for i, str := range lines {
|
for i, str := range lines {
|
||||||
services[i] = &Service{
|
services[i] = &Service{
|
||||||
Name: str,
|
Name: str,
|
||||||
ID: c.LocalProjectName + "-" + str,
|
ID: str,
|
||||||
ProjectName: c.LocalProjectName,
|
|
||||||
OSCommand: c.OSCommand,
|
OSCommand: c.OSCommand,
|
||||||
Log: c.Log,
|
Log: c.Log,
|
||||||
DockerCommand: c,
|
DockerCommand: c,
|
||||||
|
|
@ -491,11 +336,11 @@ func (c *DockerCommand) SetContainerDetails(containers []*Container) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
||||||
func (c *DockerCommand) ViewAllLogs(project *Project) (*exec.Cmd, error) {
|
func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
|
||||||
cmd := c.OSCommand.ExecutableFromString(
|
cmd := c.OSCommand.ExecutableFromString(
|
||||||
utils.ApplyTemplate(
|
utils.ApplyTemplate(
|
||||||
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
|
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
|
||||||
c.NewCommandObject(CommandObject{Project: project}),
|
c.NewCommandObject(CommandObject{}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -506,15 +351,10 @@ func (c *DockerCommand) ViewAllLogs(project *Project) (*exec.Cmd, error) {
|
||||||
|
|
||||||
// DockerComposeConfig returns the result of 'docker-compose config'
|
// DockerComposeConfig returns the result of 'docker-compose config'
|
||||||
func (c *DockerCommand) DockerComposeConfig() string {
|
func (c *DockerCommand) DockerComposeConfig() string {
|
||||||
return c.DockerComposeConfigForProject(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DockerComposeConfigForProject returns the result of 'docker-compose config' for a specific project
|
|
||||||
func (c *DockerCommand) DockerComposeConfigForProject(project *Project) string {
|
|
||||||
output, err := c.OSCommand.RunCommandWithOutput(
|
output, err := c.OSCommand.RunCommandWithOutput(
|
||||||
utils.ApplyTemplate(
|
utils.ApplyTemplate(
|
||||||
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
|
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
|
||||||
c.NewCommandObject(CommandObject{Project: project}),
|
c.NewCommandObject(CommandObject{}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestNewDockerClientVersionNegotiation verifies that newDockerClient allows
|
|
||||||
// API version negotiation even when DOCKER_API_VERSION is set.
|
|
||||||
//
|
|
||||||
// This is a regression test for https://github.com/jesseduffield/lazydocker/issues/715
|
|
||||||
// where users got "client version 1.25 is too old" errors because FromEnv()
|
|
||||||
// includes WithVersionFromEnv() which sets manualOverride=true, preventing
|
|
||||||
// API version negotiation.
|
|
||||||
func TestNewDockerClientVersionNegotiation(t *testing.T) {
|
|
||||||
// Save original env var and restore after test
|
|
||||||
originalAPIVersion := os.Getenv("DOCKER_API_VERSION")
|
|
||||||
defer func() {
|
|
||||||
if originalAPIVersion == "" {
|
|
||||||
os.Unsetenv("DOCKER_API_VERSION")
|
|
||||||
} else {
|
|
||||||
os.Setenv("DOCKER_API_VERSION", originalAPIVersion)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set DOCKER_API_VERSION to an old version that would cause
|
|
||||||
// "client version 1.25 is too old" errors if negotiation is disabled
|
|
||||||
os.Setenv("DOCKER_API_VERSION", "1.25")
|
|
||||||
|
|
||||||
t.Run("FromEnv locks version preventing negotiation", func(t *testing.T) {
|
|
||||||
// This demonstrates the problematic behavior we're avoiding.
|
|
||||||
// When using FromEnv with DOCKER_API_VERSION set, the client
|
|
||||||
// version gets locked to 1.25 and negotiation is disabled.
|
|
||||||
cli, err := client.NewClientWithOpts(
|
|
||||||
client.FromEnv,
|
|
||||||
client.WithAPIVersionNegotiation(),
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer cli.Close()
|
|
||||||
|
|
||||||
// Version is locked to the env var value
|
|
||||||
assert.Equal(t, "1.25", cli.ClientVersion())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("newDockerClient allows version negotiation", func(t *testing.T) {
|
|
||||||
// Test the actual production function.
|
|
||||||
// Use DefaultDockerHost for cross-platform compatibility
|
|
||||||
// (unix socket on Linux/macOS, named pipe on Windows).
|
|
||||||
cli, err := newDockerClient(client.DefaultDockerHost)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer cli.Close()
|
|
||||||
|
|
||||||
// Version is NOT locked to the env var value (1.25).
|
|
||||||
// Instead, it uses the library's default version and will negotiate
|
|
||||||
// with the server on first request. This is the key difference that
|
|
||||||
// fixes the "version too old" error.
|
|
||||||
assert.NotEqual(t, "1.25", cli.ClientVersion(),
|
|
||||||
"client version should not be locked to DOCKER_API_VERSION env var")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIsProjectScoped covers the predicate that drives whether the
|
|
||||||
// project/services panels appear and whether the containers panel filters by
|
|
||||||
// project. The "outside compose dir + -p" case is the regression we fixed
|
|
||||||
// after PR #776 silently disabled it.
|
|
||||||
func TestIsProjectScoped(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
inDockerComposeProject bool
|
|
||||||
projectName string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"inside compose dir, no -p", true, "", true},
|
|
||||||
{"inside compose dir, with -p", true, "myproject", true},
|
|
||||||
{"outside compose dir, no -p", false, "", false},
|
|
||||||
{"outside compose dir, with -p", false, "myproject", true},
|
|
||||||
}
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
c := &DockerCommand{
|
|
||||||
InDockerComposeProject: tc.inDockerComposeProject,
|
|
||||||
Config: &config.AppConfig{ProjectName: tc.projectName},
|
|
||||||
}
|
|
||||||
assert.Equal(t, tc.want, c.IsProjectScoped())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string
|
Name string
|
||||||
ID string
|
ID string
|
||||||
ProjectName string
|
|
||||||
OSCommand *OSCommand
|
OSCommand *OSCommand
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
Container *Container
|
Container *Container
|
||||||
|
|
|
||||||
|
|
@ -488,11 +488,10 @@ type AppConfig struct {
|
||||||
UserConfig *UserConfig
|
UserConfig *UserConfig
|
||||||
ConfigDir string
|
ConfigDir string
|
||||||
ProjectDir string
|
ProjectDir string
|
||||||
ProjectName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAppConfig makes a new app config
|
// NewAppConfig makes a new app config
|
||||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
|
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string) (*AppConfig, error) {
|
||||||
configDir, err := findOrCreateConfigDir(name)
|
configDir, err := findOrCreateConfigDir(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -518,7 +517,6 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||||
UserConfig: userConfig,
|
UserConfig: userConfig,
|
||||||
ConfigDir: configDir,
|
ConfigDir: configDir,
|
||||||
ProjectDir: projectDir,
|
ProjectDir: projectDir,
|
||||||
ProjectName: projectName,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return appConfig, nil
|
return appConfig, nil
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func TestDockerComposeCommandNoFiles(t *testing.T) {
|
func TestDockerComposeCommandNoFiles(t *testing.T) {
|
||||||
composeFiles := []string{}
|
composeFiles := []string{}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
|
||||||
|
|
||||||
func TestDockerComposeCommandSingleFile(t *testing.T) {
|
func TestDockerComposeCommandSingleFile(t *testing.T) {
|
||||||
composeFiles := []string{"one.yml"}
|
composeFiles := []string{"one.yml"}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
|
||||||
|
|
||||||
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
||||||
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
|
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
||||||
func TestWritingToConfigFile(t *testing.T) {
|
func TestWritingToConfigFile(t *testing.T) {
|
||||||
// init the AppConfig
|
// init the AppConfig
|
||||||
emptyComposeFiles := []string{}
|
emptyComposeFiles := []string{}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,31 +175,14 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||||
return defaultBox
|
return defaultBox
|
||||||
}
|
}
|
||||||
|
|
||||||
// The project panel is compact (Size: 3) when not focused, but expands
|
return append([]*boxlayout.Box{
|
||||||
// when focused to show the list of projects. This only applies when the
|
{
|
||||||
// project panel is actually visible (i.e. we are inside a compose project).
|
|
||||||
if len(sideWindowNames) > 0 && sideWindowNames[0] == "project" {
|
|
||||||
projectBox := &boxlayout.Box{
|
|
||||||
Window: sideWindowNames[0],
|
Window: sideWindowNames[0],
|
||||||
Size: 3,
|
Size: 3,
|
||||||
}
|
},
|
||||||
if currentWindow == sideWindowNames[0] {
|
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
||||||
projectBox = &boxlayout.Box{
|
|
||||||
Window: sideWindowNames[0],
|
|
||||||
Weight: 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return append([]*boxlayout.Box{
|
|
||||||
projectBox,
|
|
||||||
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
|
||||||
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
|
||||||
})...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
|
||||||
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||||
})
|
})...)
|
||||||
} else {
|
} else {
|
||||||
squashedHeight := 1
|
squashedHeight := 1
|
||||||
if height >= 21 {
|
if height >= 21 {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
||||||
}
|
}
|
||||||
|
|
||||||
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool {
|
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool {
|
||||||
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName
|
return service.Name == container.ServiceName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,35 +83,15 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
||||||
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
||||||
},
|
},
|
||||||
Filter: func(container *commands.Container) bool {
|
Filter: func(container *commands.Container) bool {
|
||||||
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
// 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.
|
||||||
|
if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// When project-scoped, apply project and standalone filtering.
|
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
||||||
// Otherwise all containers are shown in a flat list regardless
|
return false
|
||||||
// of which compose project they belong to.
|
|
||||||
if gui.DockerCommand.IsProjectScoped() {
|
|
||||||
// This check must be inside the IsProjectScoped guard: when
|
|
||||||
// not project-scoped, services are still derived from container
|
|
||||||
// labels, so compose-managed containers from other projects
|
|
||||||
// would be incorrectly hidden.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by selected project. Containers with no project (truly
|
|
||||||
// standalone, not from any compose project) are always shown.
|
|
||||||
selectedProject := gui.getSelectedProjectName()
|
|
||||||
if selectedProject == "" {
|
|
||||||
selectedProject = gui.DockerCommand.LocalProjectName
|
|
||||||
}
|
|
||||||
if selectedProject != "" && container.ProjectName != "" && container.ProjectName != selectedProject {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
@ -276,6 +256,7 @@ func (gui *Gui) refreshContainersAndServices() error {
|
||||||
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
|
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
|
||||||
|
|
||||||
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
|
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
|
||||||
|
gui.Panels.Services.List.GetAllItems(),
|
||||||
gui.Panels.Containers.List.GetAllItems(),
|
gui.Panels.Containers.List.GetAllItems(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -302,8 +283,10 @@ func (gui *Gui) refreshContainersAndServices() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderContainersAndServices() error {
|
func (gui *Gui) renderContainersAndServices() error {
|
||||||
if err := gui.Panels.Services.RerenderList(); err != nil {
|
if gui.DockerCommand.InDockerComposeProject {
|
||||||
return err
|
if err := gui.Panels.Services.RerenderList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gui.Panels.Containers.RerenderList(); err != nil {
|
if err := gui.Panels.Containers.RerenderList(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -297,12 +297,12 @@ func (gui *Gui) updateContainerDetails() error {
|
||||||
|
|
||||||
func (gui *Gui) refresh() {
|
func (gui *Gui) refresh() {
|
||||||
go func() {
|
go func() {
|
||||||
// Refresh containers/services first, then projects (which depend on
|
if err := gui.refreshProject(); err != nil {
|
||||||
// container labels to discover projects).
|
|
||||||
if err := gui.refreshContainersAndServices(); err != nil {
|
|
||||||
gui.Log.Error(err)
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
if err := gui.refreshProject(); err != nil {
|
}()
|
||||||
|
go func() {
|
||||||
|
if err := gui.refreshContainersAndServices(); err != nil {
|
||||||
gui.Log.Error(err)
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
@ -459,7 +459,7 @@ func (gui *Gui) ShouldRefresh(key string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) initiallyFocusedViewName() string {
|
func (gui *Gui) initiallyFocusedViewName() string {
|
||||||
if gui.DockerCommand.IsProjectScoped() {
|
if gui.DockerCommand.InDockerComposeProject {
|
||||||
return "services"
|
return "services"
|
||||||
}
|
}
|
||||||
return "containers"
|
return "containers"
|
||||||
|
|
|
||||||
|
|
@ -120,11 +120,9 @@ func (gui *Gui) focusPointInView(view *gocui.View) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, panel := range gui.allListPanels() {
|
currentPanel, ok := gui.currentListPanel()
|
||||||
if panel.GetView() == view {
|
if ok {
|
||||||
panel.Refocus()
|
currentPanel.Refocus()
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,6 @@ type SideListPanel[T comparable] struct {
|
||||||
// a callback to invoke when the item is clicked
|
// a callback to invoke when the item is clicked
|
||||||
OnClick func(T) error
|
OnClick func(T) error
|
||||||
|
|
||||||
// a callback to invoke when a new item is selected (e.g. keyboard navigation)
|
|
||||||
OnSelect func(T) error
|
|
||||||
|
|
||||||
// returns the cells that we render to the view in a table format. The cells will
|
// returns the cells that we render to the view in a table format. The cells will
|
||||||
// be rendered with padding.
|
// be rendered with padding.
|
||||||
GetTableCells func(T) []string
|
GetTableCells func(T) []string
|
||||||
|
|
@ -119,12 +116,6 @@ func (self *SideListPanel[T]) HandleSelect() error {
|
||||||
|
|
||||||
self.Refocus()
|
self.Refocus()
|
||||||
|
|
||||||
if self.OnSelect != nil {
|
|
||||||
if err := self.OnSelect(item); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.renderContext(item)
|
return self.renderContext(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package gui
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
|
@ -15,21 +16,34 @@ import (
|
||||||
"github.com/jesseduffield/yaml"
|
"github.com/jesseduffield/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Although at the moment we'll only have one project, in future we could have
|
||||||
|
// a list of projects in the project panel.
|
||||||
|
|
||||||
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
||||||
return &panels.SideListPanel[*commands.Project]{
|
return &panels.SideListPanel[*commands.Project]{
|
||||||
ContextState: &panels.ContextState[*commands.Project]{
|
ContextState: &panels.ContextState[*commands.Project]{
|
||||||
GetMainTabs: func() []panels.MainTab[*commands.Project] {
|
GetMainTabs: func() []panels.MainTab[*commands.Project] {
|
||||||
|
if gui.DockerCommand.InDockerComposeProject {
|
||||||
|
return []panels.MainTab[*commands.Project]{
|
||||||
|
{
|
||||||
|
Key: "logs",
|
||||||
|
Title: gui.Tr.LogsTitle,
|
||||||
|
Render: gui.renderAllLogs,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "config",
|
||||||
|
Title: gui.Tr.DockerComposeConfigTitle,
|
||||||
|
Render: gui.renderDockerComposeConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "credits",
|
||||||
|
Title: gui.Tr.CreditsTitle,
|
||||||
|
Render: gui.renderCredits,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return []panels.MainTab[*commands.Project]{
|
return []panels.MainTab[*commands.Project]{
|
||||||
{
|
|
||||||
Key: "logs",
|
|
||||||
Title: gui.Tr.LogsTitle,
|
|
||||||
Render: gui.renderAllLogs,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "config",
|
|
||||||
Title: gui.Tr.DockerComposeConfigTitle,
|
|
||||||
Render: gui.renderDockerComposeConfig,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Key: "credits",
|
Key: "credits",
|
||||||
Title: gui.Tr.CreditsTitle,
|
Title: gui.Tr.CreditsTitle,
|
||||||
|
|
@ -50,91 +64,31 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
||||||
Gui: gui.intoInterface(),
|
Gui: gui.intoInterface(),
|
||||||
|
|
||||||
Sort: func(a *commands.Project, b *commands.Project) bool {
|
Sort: func(a *commands.Project, b *commands.Project) bool {
|
||||||
return a.Name < b.Name
|
return false
|
||||||
},
|
},
|
||||||
GetTableCells: presentation.GetProjectDisplayStrings,
|
GetTableCells: presentation.GetProjectDisplayStrings,
|
||||||
OnSelect: func(project *commands.Project) error {
|
// It doesn't make sense to filter a list of only one item.
|
||||||
// When a different project is selected, re-filter services and
|
DisableFilter: true,
|
||||||
// containers to show only those belonging to the selected project.
|
|
||||||
return gui.renderContainersAndServices()
|
|
||||||
},
|
|
||||||
Hide: func() bool {
|
|
||||||
return !gui.DockerCommand.IsProjectScoped()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshProject() error {
|
func (gui *Gui) refreshProject() error {
|
||||||
projects := gui.getDiscoveredProjects()
|
gui.Panels.Projects.SetItems([]*commands.Project{{Name: gui.getProjectName()}})
|
||||||
|
|
||||||
// Preserve the current selection across refreshes. On the first refresh,
|
|
||||||
// select the project specified via -p flag, or fall back to the local project.
|
|
||||||
selectedName := gui.getSelectedProjectName()
|
|
||||||
if selectedName == "" {
|
|
||||||
if gui.Config.ProjectName != "" {
|
|
||||||
selectedName = gui.Config.ProjectName
|
|
||||||
} else {
|
|
||||||
selectedName = gui.DockerCommand.LocalProjectName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.Panels.Projects.SetItems(projects)
|
|
||||||
|
|
||||||
if selectedName != "" {
|
|
||||||
for i, p := range gui.Panels.Projects.List.GetItems() {
|
|
||||||
if p.Name == selectedName {
|
|
||||||
gui.Panels.Projects.SetSelectedLineIdx(i)
|
|
||||||
gui.Panels.Projects.Refocus()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.Panels.Projects.RerenderList()
|
return gui.Panels.Projects.RerenderList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
func (gui *Gui) getProjectName() string {
|
||||||
// The local project (from docker-compose.yml in the current directory, or from -p) is
|
projectName := path.Base(gui.Config.ProjectDir)
|
||||||
// included even when it has no running containers, so the user always sees the project
|
if gui.DockerCommand.InDockerComposeProject {
|
||||||
// they explicitly scoped to.
|
for _, service := range gui.Panels.Services.List.GetAllItems() {
|
||||||
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
|
container := service.Container
|
||||||
containers := gui.Panels.Containers.List.GetAllItems()
|
if container != nil && container.DetailsLoaded() {
|
||||||
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
return container.Details.Config.Labels["com.docker.compose.project"]
|
||||||
|
|
||||||
// If we're scoped to a project but it has no running containers, still
|
|
||||||
// include it. We don't fall back to the directory name here to avoid
|
|
||||||
// briefly flashing the wrong project name on startup.
|
|
||||||
localName := gui.DockerCommand.LocalProjectName
|
|
||||||
|
|
||||||
if gui.DockerCommand.IsProjectScoped() && localName != "" {
|
|
||||||
found := false
|
|
||||||
for _, name := range projectNames {
|
|
||||||
if name == localName {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
|
||||||
projectNames = append([]string{localName}, projectNames...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
projects := make([]*commands.Project, len(projectNames))
|
return projectName
|
||||||
for i, name := range projectNames {
|
|
||||||
projects[i] = &commands.Project{Name: name}
|
|
||||||
}
|
|
||||||
|
|
||||||
return projects
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSelectedProjectName returns the name of the currently selected project,
|
|
||||||
// or empty string if none is selected.
|
|
||||||
func (gui *Gui) getSelectedProjectName() string {
|
|
||||||
project, err := gui.Panels.Projects.GetSelectedItem()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return project.Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc {
|
func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc {
|
||||||
|
|
@ -158,7 +112,7 @@ func (gui *Gui) creditsStr() string {
|
||||||
}, "\n\n")
|
}, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
|
func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc {
|
||||||
return gui.NewTask(TaskOpts{
|
return gui.NewTask(TaskOpts{
|
||||||
Autoscroll: true,
|
Autoscroll: true,
|
||||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||||
|
|
@ -168,7 +122,7 @@ func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
|
||||||
cmd := gui.OSCommand.RunCustomCommand(
|
cmd := gui.OSCommand.RunCustomCommand(
|
||||||
utils.ApplyTemplate(
|
utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.AllLogs,
|
gui.Config.UserConfig.CommandTemplates.AllLogs,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -190,19 +144,9 @@ func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc {
|
func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) tasks.TaskFunc {
|
||||||
if !gui.DockerCommand.InDockerComposeProject {
|
|
||||||
return gui.NewSimpleRenderStringTask(func() string {
|
|
||||||
return "Compose config is only available when launched from a docker-compose project directory"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
|
||||||
return gui.NewSimpleRenderStringTask(func() string {
|
|
||||||
return "Compose config is not available for non-local projects"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return gui.NewSimpleRenderStringTask(func() string {
|
return gui.NewSimpleRenderStringTask(func() string {
|
||||||
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfigForProject(project))
|
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfig())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,8 +173,7 @@ func lazydockerTitle() string {
|
||||||
|
|
||||||
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
|
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
|
||||||
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
|
||||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
c, err := gui.DockerCommand.ViewAllLogs()
|
||||||
c, err := gui.DockerCommand.ViewAllLogs(project)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,23 +74,11 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
|
||||||
|
|
||||||
return a.Name < b.Name
|
return a.Name < b.Name
|
||||||
},
|
},
|
||||||
Filter: func(service *commands.Service) bool {
|
|
||||||
selectedProject := gui.getSelectedProjectName()
|
|
||||||
if selectedProject == "" {
|
|
||||||
// Before any project is selected (e.g. startup), default to
|
|
||||||
// the local project so we don't briefly flash all services.
|
|
||||||
selectedProject = gui.DockerCommand.LocalProjectName
|
|
||||||
}
|
|
||||||
if selectedProject == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return service.ProjectName == selectedProject
|
|
||||||
},
|
|
||||||
GetTableCells: func(service *commands.Service) []string {
|
GetTableCells: func(service *commands.Service) []string {
|
||||||
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
|
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
|
||||||
},
|
},
|
||||||
Hide: func() bool {
|
Hide: func() bool {
|
||||||
return !gui.DockerCommand.IsProjectScoped()
|
return !gui.DockerCommand.InDockerComposeProject
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,24 +142,13 @@ func (r *commandOption) getDisplayStrings() []string {
|
||||||
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
|
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isServiceFromLocalProject returns true if the given service belongs to the
|
|
||||||
// local compose project (the one whose compose file is in the current directory).
|
|
||||||
// Compose commands like up/stop/restart only work for local project services.
|
|
||||||
func (gui *Gui) isServiceFromLocalProject(service *commands.Service) bool {
|
|
||||||
return service.ProjectName == gui.DockerCommand.LocalProjectName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.Panels.Services.GetSelectedItem()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gui.isServiceFromLocalProject(service) {
|
composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
|
|
||||||
composeCommand := gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}).DockerCompose
|
|
||||||
|
|
||||||
options := []*commandOption{
|
options := []*commandOption{
|
||||||
{
|
{
|
||||||
|
|
@ -225,15 +202,10 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
|
||||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||||
if !gui.isServiceFromLocalProject(service) {
|
|
||||||
if service.Container == nil {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
return service.Container.Stop()
|
|
||||||
}
|
|
||||||
if err := service.Stop(); err != nil {
|
if err := service.Stop(); err != nil {
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
@ -245,10 +217,6 @@ func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gui.isServiceFromLocalProject(service) {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
|
||||||
if err := service.Up(); err != nil {
|
if err := service.Up(); err != nil {
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
|
|
@ -265,15 +233,10 @@ func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||||
if !gui.isServiceFromLocalProject(service) {
|
|
||||||
if service.Container == nil {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
return service.Container.Restart()
|
|
||||||
}
|
|
||||||
if err := service.Restart(); err != nil {
|
if err := service.Restart(); err != nil {
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -284,19 +247,11 @@ func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gui.isServiceFromLocalProject(service) {
|
|
||||||
if service.Container == nil {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
|
||||||
return service.Container.Start()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
|
||||||
if err := service.Start(); err != nil {
|
if err := service.Start(); err != nil {
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -334,14 +289,10 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
|
||||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
cmdStr := utils.ApplyTemplate(
|
cmdStr := utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.Up,
|
gui.Config.UserConfig.CommandTemplates.Up,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
||||||
|
|
@ -354,18 +305,14 @@ func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
|
||||||
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
downCommand := utils.ApplyTemplate(
|
downCommand := utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.Down,
|
gui.Config.UserConfig.CommandTemplates.Down,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||||
)
|
)
|
||||||
|
|
||||||
downWithVolumesCommand := utils.ApplyTemplate(
|
downWithVolumesCommand := utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
||||||
)
|
)
|
||||||
|
|
||||||
options := []*commandOption{
|
options := []*commandOption{
|
||||||
|
|
@ -414,10 +361,6 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gui.isServiceFromLocalProject(service) {
|
|
||||||
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuildCommand := utils.ApplyTemplate(
|
rebuildCommand := utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||||
|
|
@ -522,9 +465,8 @@ L:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
project, _ := gui.Panels.Projects.GetSelectedItem()
|
|
||||||
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
|
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
|
||||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project})
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||||
|
|
||||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,8 +120,6 @@ func (gui *Gui) createAllViews() error {
|
||||||
|
|
||||||
gui.Views.Project.Title = gui.Tr.ProjectTitle
|
gui.Views.Project.Title = gui.Tr.ProjectTitle
|
||||||
gui.Views.Project.TitlePrefix = "[1]"
|
gui.Views.Project.TitlePrefix = "[1]"
|
||||||
gui.Views.Project.Highlight = true
|
|
||||||
gui.Views.Project.SelBgColor = selectedLineBgColor
|
|
||||||
|
|
||||||
gui.Views.Services.Highlight = true
|
gui.Views.Services.Highlight = true
|
||||||
gui.Views.Services.Title = gui.Tr.ServicesTitle
|
gui.Views.Services.Title = gui.Tr.ServicesTitle
|
||||||
|
|
@ -130,7 +128,7 @@ func (gui *Gui) createAllViews() error {
|
||||||
|
|
||||||
gui.Views.Containers.Highlight = true
|
gui.Views.Containers.Highlight = true
|
||||||
gui.Views.Containers.SelBgColor = selectedLineBgColor
|
gui.Views.Containers.SelBgColor = selectedLineBgColor
|
||||||
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.IsProjectScoped() {
|
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
|
||||||
gui.Views.Containers.Title = gui.Tr.ContainersTitle
|
gui.Views.Containers.Title = gui.Tr.ContainersTitle
|
||||||
} else {
|
} else {
|
||||||
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
|
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
|
||||||
|
|
|
||||||
|
|
@ -110,18 +110,17 @@ type TranslationSet struct {
|
||||||
OpenInBrowser string
|
OpenInBrowser string
|
||||||
SortContainersByState string
|
SortContainersByState string
|
||||||
|
|
||||||
LogsTitle string
|
LogsTitle string
|
||||||
ConfigTitle string
|
ConfigTitle string
|
||||||
EnvTitle string
|
EnvTitle string
|
||||||
DockerComposeConfigTitle string
|
DockerComposeConfigTitle string
|
||||||
StatsTitle string
|
StatsTitle string
|
||||||
CreditsTitle string
|
CreditsTitle string
|
||||||
ContainerConfigTitle string
|
ContainerConfigTitle string
|
||||||
ContainerEnvTitle string
|
ContainerEnvTitle string
|
||||||
NothingToDisplay string
|
NothingToDisplay string
|
||||||
NoContainerForService string
|
NoContainerForService string
|
||||||
CannotDisplayEnvVariables string
|
CannotDisplayEnvVariables string
|
||||||
CannotManageNonLocalService string
|
|
||||||
|
|
||||||
No string
|
No string
|
||||||
Yes string
|
Yes string
|
||||||
|
|
@ -218,31 +217,30 @@ func englishSet() TranslationSet {
|
||||||
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",
|
||||||
|
|
||||||
GlobalTitle: "Global",
|
GlobalTitle: "Global",
|
||||||
MainTitle: "Main",
|
MainTitle: "Main",
|
||||||
ProjectTitle: "Project",
|
ProjectTitle: "Project",
|
||||||
ServicesTitle: "Services",
|
ServicesTitle: "Services",
|
||||||
ContainersTitle: "Containers",
|
ContainersTitle: "Containers",
|
||||||
StandaloneContainersTitle: "Standalone Containers",
|
StandaloneContainersTitle: "Standalone Containers",
|
||||||
ImagesTitle: "Images",
|
ImagesTitle: "Images",
|
||||||
VolumesTitle: "Volumes",
|
VolumesTitle: "Volumes",
|
||||||
NetworksTitle: "Networks",
|
NetworksTitle: "Networks",
|
||||||
CustomCommandTitle: "Custom Command:",
|
CustomCommandTitle: "Custom Command:",
|
||||||
BulkCommandTitle: "Bulk Command:",
|
BulkCommandTitle: "Bulk Command:",
|
||||||
ErrorTitle: "Error",
|
ErrorTitle: "Error",
|
||||||
LogsTitle: "Logs",
|
LogsTitle: "Logs",
|
||||||
ConfigTitle: "Config",
|
ConfigTitle: "Config",
|
||||||
EnvTitle: "Env",
|
EnvTitle: "Env",
|
||||||
DockerComposeConfigTitle: "Docker-Compose Config",
|
DockerComposeConfigTitle: "Docker-Compose Config",
|
||||||
TopTitle: "Top",
|
TopTitle: "Top",
|
||||||
StatsTitle: "Stats",
|
StatsTitle: "Stats",
|
||||||
CreditsTitle: "About",
|
CreditsTitle: "About",
|
||||||
ContainerConfigTitle: "Container Config",
|
ContainerConfigTitle: "Container Config",
|
||||||
ContainerEnvTitle: "Container Env",
|
ContainerEnvTitle: "Container Env",
|
||||||
NothingToDisplay: "Nothing to display",
|
NothingToDisplay: "Nothing to display",
|
||||||
NoContainerForService: "No logs to show; service is not associated with a container",
|
NoContainerForService: "No logs to show; service is not associated with a container",
|
||||||
CannotDisplayEnvVariables: "Something went wrong while displaying environment variables",
|
CannotDisplayEnvVariables: "Something went wrong while displaying environment variables",
|
||||||
CannotManageNonLocalService: "This service belongs to a different compose project. Run lazydocker from that project's directory to manage it.",
|
|
||||||
|
|
||||||
NoContainers: "No containers",
|
NoContainers: "No containers",
|
||||||
NoContainer: "No container",
|
NoContainer: "No container",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue