mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 07:11:01 +00:00
Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e7aadc207 | ||
|
|
f5ff116af9 | ||
|
|
3974f6fec1 | ||
|
|
697cd441aa | ||
|
|
9134abeefd | ||
|
|
8106125441 | ||
|
|
b17d474148 | ||
|
|
e3c1c8630a | ||
|
|
ebce4fc631 | ||
|
|
2ebe3848d6 | ||
|
|
b65137addf | ||
|
|
26ce871368 | ||
|
|
577797d9ed | ||
|
|
3c9d134d05 |
30 changed files with 592 additions and 138 deletions
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
CLAUDE.md
Normal file
5
CLAUDE.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# 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 ./...`
|
||||||
0
config/config.yml
Normal file
0
config/config.yml
Normal file
|
|
@ -1,4 +1,3 @@
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
lazydocker:
|
lazydocker:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +11,7 @@ _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,6 +29,7 @@ var (
|
||||||
configFlag = false
|
configFlag = false
|
||||||
debuggingFlag = false
|
debuggingFlag = false
|
||||||
composeFiles []string
|
composeFiles []string
|
||||||
|
projectName string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -51,6 +52,7 @@ 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()
|
||||||
|
|
@ -71,7 +73,7 @@ func main() {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir)
|
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
|
||||||
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,6 +57,12 @@ 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,6 +9,8 @@ import (
|
||||||
ogLog "log"
|
ogLog "log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -39,9 +41,11 @@ type DockerCommand struct {
|
||||||
Config *config.AppConfig
|
Config *config.AppConfig
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
InDockerComposeProject bool
|
InDockerComposeProject bool
|
||||||
ErrorChan chan error
|
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
|
||||||
ContainerMutex deadlock.Mutex
|
LocalProjectName string
|
||||||
ServiceMutex deadlock.Mutex
|
ErrorChan chan error
|
||||||
|
ContainerMutex deadlock.Mutex
|
||||||
|
ServiceMutex deadlock.Mutex
|
||||||
|
|
||||||
Closers []io.Closer
|
Closers []io.Closer
|
||||||
}
|
}
|
||||||
|
|
@ -61,15 +65,40 @@ 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()
|
||||||
|
|
@ -96,7 +125,7 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
|
||||||
dockerHost = dockerHostFromEnv
|
dockerHost = dockerHostFromEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithHost(dockerHost))
|
cli, err := newDockerClient(dockerHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ogLog.Fatal(err)
|
ogLog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
@ -125,9 +154,25 @@ 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
|
||||||
|
|
@ -178,7 +223,7 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
|
||||||
container.MonitoringStats = false
|
container.MonitoringStats = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) {
|
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||||
c.ServiceMutex.Lock()
|
c.ServiceMutex.Lock()
|
||||||
defer c.ServiceMutex.Unlock()
|
defer c.ServiceMutex.Unlock()
|
||||||
|
|
||||||
|
|
@ -187,27 +232,136 @@ func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service,
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var services []*Service
|
// Derive services from container labels (covers all projects)
|
||||||
// we only need to get these services once because they won't change in the runtime of the program
|
services := c.GetServicesFromContainers(containers)
|
||||||
if currentServices != nil {
|
|
||||||
services = currentServices
|
var composeServices []*Service
|
||||||
} else {
|
if c.InDockerComposeProject {
|
||||||
services, err = c.GetServices()
|
composeServices, err = c.GetServices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
c.Log.Warn("Failed to get compose services: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
|
||||||
service.Container = ctr
|
service.Container = ctr
|
||||||
continue L
|
continue L
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +450,8 @@ 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: str,
|
ID: c.LocalProjectName + "-" + str,
|
||||||
|
ProjectName: c.LocalProjectName,
|
||||||
OSCommand: c.OSCommand,
|
OSCommand: c.OSCommand,
|
||||||
Log: c.Log,
|
Log: c.Log,
|
||||||
DockerCommand: c,
|
DockerCommand: c,
|
||||||
|
|
@ -336,11 +491,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() (*exec.Cmd, error) {
|
func (c *DockerCommand) ViewAllLogs(project *Project) (*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{}),
|
c.NewCommandObject(CommandObject{Project: project}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -351,10 +506,15 @@ func (c *DockerCommand) ViewAllLogs() (*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{}),
|
c.NewCommandObject(CommandObject{Project: project}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
91
pkg/commands/docker_test.go
Normal file
91
pkg/commands/docker_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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,6 +13,7 @@ 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,10 +488,11 @@ 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) (*AppConfig, error) {
|
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
|
||||||
configDir, err := findOrCreateConfigDir(name)
|
configDir, err := findOrCreateConfigDir(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -517,6 +518,7 @@ 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,14 +175,31 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||||
return defaultBox
|
return defaultBox
|
||||||
}
|
}
|
||||||
|
|
||||||
return append([]*boxlayout.Box{
|
// The project panel is compact (Size: 3) when not focused, but expands
|
||||||
{
|
// 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,
|
||||||
},
|
}
|
||||||
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
if currentWindow == sideWindowNames[0] {
|
||||||
|
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
|
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,15 +83,35 @@ 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 {
|
||||||
// Note that this is O(N*M) time complexity where N is the number of services
|
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
// When project-scoped, apply project and standalone filtering.
|
||||||
return false
|
// Otherwise all containers are shown in a flat list regardless
|
||||||
|
// 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
|
||||||
|
|
@ -256,7 +276,6 @@ 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 {
|
||||||
|
|
@ -283,10 +302,8 @@ func (gui *Gui) refreshContainersAndServices() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderContainersAndServices() error {
|
func (gui *Gui) renderContainersAndServices() error {
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
if err := gui.Panels.Services.RerenderList(); err != nil {
|
||||||
if err := gui.Panels.Services.RerenderList(); err != nil {
|
return err
|
||||||
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() {
|
||||||
if err := gui.refreshProject(); err != nil {
|
// Refresh containers/services first, then projects (which depend on
|
||||||
|
// 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.InDockerComposeProject {
|
if gui.DockerCommand.IsProjectScoped() {
|
||||||
return "services"
|
return "services"
|
||||||
}
|
}
|
||||||
return "containers"
|
return "containers"
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,11 @@ func (gui *Gui) focusPointInView(view *gocui.View) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPanel, ok := gui.currentListPanel()
|
for _, panel := range gui.allListPanels() {
|
||||||
if ok {
|
if panel.GetView() == view {
|
||||||
currentPanel.Refocus()
|
panel.Refocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ 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
|
||||||
|
|
@ -116,6 +119,12 @@ 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,7 +3,6 @@ package gui
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
|
@ -16,34 +15,21 @@ 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,
|
||||||
|
|
@ -64,31 +50,91 @@ 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 false
|
return a.Name < b.Name
|
||||||
},
|
},
|
||||||
GetTableCells: presentation.GetProjectDisplayStrings,
|
GetTableCells: presentation.GetProjectDisplayStrings,
|
||||||
// It doesn't make sense to filter a list of only one item.
|
OnSelect: func(project *commands.Project) error {
|
||||||
DisableFilter: true,
|
// When a different project is selected, re-filter services and
|
||||||
|
// 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 {
|
||||||
gui.Panels.Projects.SetItems([]*commands.Project{{Name: gui.getProjectName()}})
|
projects := gui.getDiscoveredProjects()
|
||||||
return gui.Panels.Projects.RerenderList()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) getProjectName() string {
|
// Preserve the current selection across refreshes. On the first refresh,
|
||||||
projectName := path.Base(gui.Config.ProjectDir)
|
// select the project specified via -p flag, or fall back to the local project.
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
selectedName := gui.getSelectedProjectName()
|
||||||
for _, service := range gui.Panels.Services.List.GetAllItems() {
|
if selectedName == "" {
|
||||||
container := service.Container
|
if gui.Config.ProjectName != "" {
|
||||||
if container != nil && container.DetailsLoaded() {
|
selectedName = gui.Config.ProjectName
|
||||||
return container.Details.Config.Labels["com.docker.compose.project"]
|
} 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 projectName
|
return gui.Panels.Projects.RerenderList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
||||||
|
// The local project (from docker-compose.yml in the current directory, or from -p) is
|
||||||
|
// included even when it has no running containers, so the user always sees the project
|
||||||
|
// they explicitly scoped to.
|
||||||
|
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
|
||||||
|
containers := gui.Panels.Containers.List.GetAllItems()
|
||||||
|
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
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 {
|
||||||
|
|
@ -112,7 +158,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,
|
||||||
|
|
@ -122,7 +168,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{}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -144,9 +190,19 @@ 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.DockerComposeConfig())
|
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfigForProject(project))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,7 +229,8 @@ 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 {
|
||||||
c, err := gui.DockerCommand.ViewAllLogs()
|
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||||
|
c, err := gui.DockerCommand.ViewAllLogs(project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,23 @@ 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.InDockerComposeProject
|
return !gui.DockerCommand.IsProjectScoped()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,13 +154,24 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose
|
if !gui.isServiceFromLocalProject(service) {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeCommand := gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}).DockerCompose
|
||||||
|
|
||||||
options := []*commandOption{
|
options := []*commandOption{
|
||||||
{
|
{
|
||||||
|
|
@ -202,10 +225,15 @@ 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)
|
||||||
|
|
@ -217,6 +245,10 @@ 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())
|
||||||
|
|
@ -233,10 +265,15 @@ 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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -247,11 +284,19 @@ 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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -289,10 +334,14 @@ 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{}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
||||||
|
|
@ -305,14 +354,18 @@ 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{}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
)
|
)
|
||||||
|
|
||||||
downWithVolumesCommand := utils.ApplyTemplate(
|
downWithVolumesCommand := utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
)
|
)
|
||||||
|
|
||||||
options := []*commandOption{
|
options := []*commandOption{
|
||||||
|
|
@ -361,6 +414,10 @@ 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}),
|
||||||
|
|
@ -465,8 +522,9 @@ 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{})
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project})
|
||||||
|
|
||||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ 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
|
||||||
|
|
@ -128,7 +130,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.InDockerComposeProject {
|
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.IsProjectScoped() {
|
||||||
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,17 +110,18 @@ 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
|
||||||
|
|
@ -217,30 +218,31 @@ 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