Compare commits

..

No commits in common. "master" and "v0.19.0" have entirely different histories.

1285 changed files with 46856 additions and 144027 deletions

View file

@ -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"
}
]
}
]
}
}

View file

@ -1,11 +0,0 @@
FROM mcr.microsoft.com/devcontainers/go:bullseye
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
RUN go install mvdan.cc/gofumpt@latest
ENV PATH="/root/go/bin:${PATH}"
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.0
RUN golangci-lint --version

View file

@ -1,54 +0,0 @@
{
"name": "Docker in Docker",
"build": {
"dockerfile": "./Dockerfile",
"context": "."
},
"features": {
"ghcr.io/devcontainers/features/common-utils:1": {
"installZsh": "true",
"upgradePackages": "false",
"uid": "1000",
"gid": "1000",
"installOhMyZsh": "true",
"nonFreePackages": "true"
},
"ghcr.io/devcontainers/features/docker-from-docker:1": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/go:1": {}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "~/go",
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"go.lintTool": "golangci-lint",
"gopls": {
"formatting.gofumpt": true,
"usePlaceholders": false // add parameter placeholders when completing a function
},
"files.eol": "\n"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"golang.Go"
]
}
}
// TODO: make this work.
// "postStartCommand": "echo \"alias gr=\\\"go run /workspaces/lazydocker/main.go\\\"\" >> ~/.zhsrc"
}

View file

@ -3,7 +3,7 @@ name: Continuous Delivery
on: on:
push: push:
tags: tags:
- "v*" - 'v*'
jobs: jobs:
goreleaser: goreleaser:
@ -14,14 +14,18 @@ jobs:
- name: Unshallow repo - name: Unshallow repo
run: git fetch --prune --unshallow run: git fetch --prune --unshallow
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v1
with: with:
go-version: 1.24.x go-version: 1.18.x
- name: Run goreleaser - name: Run goreleaser
uses: goreleaser/goreleaser-action@v1 uses: goreleaser/goreleaser-action@v1
with:
distribution: goreleaser
version: v1.17.2
args: release --clean
env: env:
GITHUB_TOKEN: ${{secrets.TOKEN_GITHUB}} GITHUB_TOKEN: ${{secrets.GORELEASER_TOKEN}}
homebrew:
runs-on: ubuntu-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.HOMEBREW_TOKEN}}
formula: lazydocker

View file

@ -1,7 +1,7 @@
name: Continuous Integration name: Continuous Integration
env: env:
GO_VERSION: 1.24 GO_VERSION: 1.18
on: on:
push: push:
@ -25,11 +25,11 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v1
with: with:
go-version: 1.24.x go-version: 1.18.x
- name: Cache build - name: Cache build
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
@ -47,11 +47,11 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v1
with: with:
go-version: 1.24.x go-version: 1.18.x
- name: Cache build - name: Cache build
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
@ -66,34 +66,6 @@ jobs:
- name: Build darwin binary - name: Build darwin binary
run: | run: |
GOOS=darwin go build GOOS=darwin go build
check-codebase:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.24.x
- name: Cache build
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Check Cheatsheet
run: |
go run scripts/cheatsheet/main.go check
- name: Check Vendor Directory
# ensure our vendor directory matches up with our go modules
run: |
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
@ -102,11 +74,11 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v1
with: with:
go-version: 1.24.x go-version: 1.18.x
- name: Cache build - name: Cache build
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test

View file

@ -9,20 +9,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Generate Sponsors 💖 - name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1.2.2 uses: JamesIves/github-sponsors-readme-action@v1.0.8
with: with:
token: ${{ secrets.TOKEN_GITHUB }} token: ${{ secrets.SPONSORS_TOKEN }}
file: "README.md" file: 'README.md'
if: ${{ github.repository == 'jesseduffield/lazydocker' }}
- name: Create Pull Request 🚀 - name: Commit and push if changed
uses: peter-evans/create-pull-request@v6 run: |-
with: git diff
commit-message: "README.md: Update Sponsors" git config --global user.email "actions@users.noreply.github.com"
title: "README.md: Update Sponsors" git config --global user.name "README-bot"
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" git add README.md
labels: "ignore-for-release" git commit -m "Updated README.md" || exit 0
delete-branch: true git push

1
.gitignore vendored
View file

@ -2,4 +2,3 @@ lazydocker*
TODO.md TODO.md
Lazydocker.code-workspace Lazydocker.code-workspace
.vscode .vscode
.idea

View file

@ -1,10 +1,13 @@
linters: linters:
disable:
- structcheck # gives false positives
enable: enable:
- gofumpt - gofumpt
- thelper - thelper
- goimports - goimports
- tparallel - tparallel
- wastedassign - wastedassign
- exportloopref
- unparam - unparam
- prealloc - prealloc
- unconvert - unconvert
@ -23,5 +26,4 @@ linters-settings:
max-func-lines: 0 max-func-lines: 0
run: run:
go: '1.21' go: 1.18
timeout: 10m

View file

@ -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 ./...`

View file

@ -1,6 +1,6 @@
ARG BASE_IMAGE_BUILDER=golang ARG BASE_IMAGE_BUILDER=golang
ARG ALPINE_VERSION=3.20 ARG ALPINE_VERSION=3.15
ARG GO_VERSION=1.23 ARG GO_VERSION=1.18
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
ARG GOARCH=amd64 ARG GOARCH=amd64
@ -19,7 +19,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} GOARM=${GOARM} \
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS docker-builder FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS docker-builder
ARG GOARCH=amd64 ARG GOARCH=amd64
ARG GOARM ARG GOARM
ARG DOCKER_VERSION=v27.0.3 ARG DOCKER_VERSION=v20.10.13
RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev
WORKDIR /go/src/github.com/docker/cli WORKDIR /go/src/github.com/docker/cli
RUN git clone --branch ${DOCKER_VERSION} --single-branch --depth 1 https://github.com/docker/cli.git . > /dev/null 2>&1 RUN git clone --branch ${DOCKER_VERSION} --single-branch --depth 1 https://github.com/docker/cli.git . > /dev/null 2>&1

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

View file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
version: '3'
services: services:
lazydocker: lazydocker:
build: build:

View file

@ -10,38 +10,20 @@ Changes to the user config will only take place after closing and re-opening laz
- OSX: `~/Library/Application Support/jesseduffield/lazydocker/config.yml` - OSX: `~/Library/Application Support/jesseduffield/lazydocker/config.yml`
- Linux: `~/.config/lazydocker/config.yml` - Linux: `~/.config/lazydocker/config.yml`
- Windows: `C:\Users\<User>\AppData\Roaming\lazydocker\config.yml` - Windows: `C:\\Users\\<User>\\AppData\\Roaming\\jesseduffield\\lazydocker\\config.yml` (I think)
JSON schema is available for `config.yml` so that IntelliSense in Visual Studio Code
(completion and error checking) is automatically enabled when the [YAML Red Hat][yaml]
extension is installed. However, note that automatic schema detection only works
if your config file is in one of the standard paths mentioned above. If you
override the path to the file, you can still make IntelliSense work by adding
```yaml
# yaml-language-server: $schema=https://json.schemastore.org/lazydocker.json
```
to the top of your config file or via [Visual Studio Code settings.json config][settings].
[yaml]: https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml
[settings]: https://github.com/redhat-developer/vscode-yaml#associating-a-schema-to-a-glob-pattern-via-yamlschemas
## Default: ## Default:
```yml ```yml
gui: gui:
scrollHeight: 2 scrollHeight: 2
language: "auto" # one of 'auto' | 'en' | 'pl' | 'nl' | 'de' | 'tr' language: 'auto' # one of 'auto' | 'en' | 'pl' | 'nl' | 'de' | 'tr'
border: "rounded" # one of 'rounded' | 'single' | 'double' | 'hidden'
theme: theme:
activeBorderColor: activeBorderColor:
- green - green
- bold - bold
inactiveBorderColor: inactiveBorderColor:
- white - white
selectedLineBgColor:
- blue
optionsTextColor: optionsTextColor:
- blue - blue
returnImmediately: false returnImmediately: false
@ -54,23 +36,12 @@ gui:
# When true, increases vertical space used by focused side panel, # When true, increases vertical space used by focused side panel,
# creating an accordion effect # creating an accordion effect
expandFocusedSidePanel: false expandFocusedSidePanel: false
# Determines which screen mode will be used on startup
screenMode: "normal" # one of 'normal' | 'half' | 'fullscreen'
# Determines the style of the container status and container health display in the
# containers panel. "long": full words (default), "short": one or two characters,
# "icon": unicode emoji.
containerStatusHealthStyle: "long"
logs: logs:
timestamps: false timestamps: false
since: '60m' # set to '' to show all logs since: '60m'
tail: '' # set to 200 to show last 200 lines of logs
commandTemplates: commandTemplates:
dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates dockerCompose: docker-compose
restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}' restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
up: '{{ .DockerCompose }} up -d'
down: '{{ .DockerCompose }} down'
downWithVolumes: '{{ .DockerCompose }} down --volumes'
upService: '{{ .DockerCompose }} up -d {{ .Service.Name }}'
startService: '{{ .DockerCompose }} start {{ .Service.Name }}' startService: '{{ .DockerCompose }} start {{ .Service.Name }}'
stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}' stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}'
serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}' serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}'
@ -128,11 +99,6 @@ customCommands:
serviceNames: [] serviceNames: []
``` ```
You may use the following go templates (such as `{{ .Container.ID }}` above) in your commands:
- `{{ .DockerCompose }}`: the docker compose command (default: `docker-compose`)
- [`{{ .Container }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Container) and its fields. For example: `{{ .Container.Container.ImageID }}`
- [`{{ .Service }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Service) and its fields. For example: `{{ .Service.Name }}`
## Replacements ## Replacements
You can add replacements like so: You can add replacements like so:

View file

@ -1,4 +1,4 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ _This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._
# Lazydocker menü # Lazydocker menü
@ -7,16 +7,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>e</kbd>: bearbeite lazydocker Konfiguration <kbd>e</kbd>: bearbeite lazydocker Konfiguration
<kbd>o</kbd>: öffne lazydocker Konfiguration <kbd>o</kbd>: öffne lazydocker Konfiguration
<kbd>m</kbd>: zeige Protokolle
<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 <kbd>m</kbd>: zeige Protokolle
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
</pre> </pre>
## Container ## Container
<pre> <pre>
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>d</kbd>: entfernen <kbd>d</kbd>: entfernen
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -29,15 +30,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: fokussieren aufs Hauptpanel <kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Dienste ## Dienste
<pre> <pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: entferne Container <kbd>d</kbd>: entferne Container
<kbd>s</kbd>: anhalten <kbd>s</kbd>: anhalten
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -45,53 +42,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>S</kbd>: start <kbd>S</kbd>: start
<kbd>a</kbd>: anbinden <kbd>a</kbd>: anbinden
<kbd>m</kbd>: zeige Protokolle <kbd>m</kbd>: zeige Protokolle
<kbd>U</kbd>: up project <kbd>[</kbd>: vorheriges Tab
<kbd>D</kbd>: down project <kbd>]</kbd>: nächstes Tab
<kbd>R</kbd>: zeige Neustartoptionen <kbd>R</kbd>: zeige Neustartoptionen
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus <kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell <kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: fokussieren aufs Hauptpanel <kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Images ## Images
<pre> <pre>
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus <kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>d</kbd>: entferne Image <kbd>d</kbd>: entferne Image
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: fokussieren aufs Hauptpanel <kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Volumes ## Volumes
<pre> <pre>
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus <kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>d</kbd>: entferne Volume <kbd>d</kbd>: entferne Volume
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: fokussieren aufs Hauptpanel <kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre>
## Netzwerk
<pre>
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>d</kbd>: entferne Netzwerk
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Haupt ## Haupt
@ -105,10 +85,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen) <kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode <kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre> </pre>

View file

@ -1,4 +1,4 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ _This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._
# Lazydocker menu # Lazydocker menu
@ -7,16 +7,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>e</kbd>: edit lazydocker config <kbd>e</kbd>: edit lazydocker config
<kbd>o</kbd>: open lazydocker config <kbd>o</kbd>: open lazydocker config
<kbd>m</kbd>: view logs
<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 <kbd>m</kbd>: view logs
<kbd>enter</kbd>: focus main panel
</pre> </pre>
## Containers ## Containers
<pre> <pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove <kbd>d</kbd>: remove
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -29,15 +30,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus main panel <kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Services ## Services
<pre> <pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: remove containers <kbd>d</kbd>: remove containers
<kbd>s</kbd>: stop <kbd>s</kbd>: stop
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -45,53 +42,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>S</kbd>: start <kbd>S</kbd>: start
<kbd>a</kbd>: attach <kbd>a</kbd>: attach
<kbd>m</kbd>: view logs <kbd>m</kbd>: view logs
<kbd>U</kbd>: up project <kbd>[</kbd>: previous tab
<kbd>D</kbd>: down project <kbd>]</kbd>: next tab
<kbd>R</kbd>: view restart options <kbd>R</kbd>: view restart options
<kbd>c</kbd>: run predefined custom command <kbd>c</kbd>: run predefined custom command
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell <kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus main panel <kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Images ## Images
<pre> <pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>c</kbd>: run predefined custom command <kbd>c</kbd>: run predefined custom command
<kbd>d</kbd>: remove image <kbd>d</kbd>: remove image
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus main panel <kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Volumes ## Volumes
<pre> <pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>c</kbd>: run predefined custom command <kbd>c</kbd>: run predefined custom command
<kbd>d</kbd>: remove volume <kbd>d</kbd>: remove volume
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus main panel <kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre>
## Networks
<pre>
<kbd>c</kbd>: run predefined custom command
<kbd>d</kbd>: remove network
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Main ## Main
@ -105,10 +85,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen) <kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode <kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre> </pre>

View file

@ -1,114 +0,0 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazydocker menú
## Proyecto
<pre>
<kbd>e</kbd>: editar configuración de lazydocker
<kbd>o</kbd>: abrir configuración de lazydocker
<kbd>m</kbd>: ver logs
<kbd>enter</kbd>: enfocar panel principal
<kbd>[</kbd>: anterior pestaña
<kbd>]</kbd>: siguiente pestaña
<kbd>/</kbd>: filtrar lista
</pre>
## Contenedores
<pre>
<kbd>d</kbd>: borrar
<kbd>e</kbd>: esconder/mostrar contenedores parados
<kbd>p</kbd>: pausa
<kbd>s</kbd>: parar
<kbd>r</kbd>: reiniciar
<kbd>a</kbd>: attach
<kbd>m</kbd>: ver logs
<kbd>E</kbd>: ejecutar shell
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>b</kbd>: ver comandos masivos
<kbd>w</kbd>: abrir en navegador (first port is http)
<kbd>enter</kbd>: enfocar panel principal
<kbd>[</kbd>: anterior pestaña
<kbd>]</kbd>: siguiente pestaña
<kbd>/</kbd>: filtrar lista
</pre>
## Servicios
<pre>
<kbd>u</kbd>: levantar servicio
<kbd>d</kbd>: borrar contenedores
<kbd>s</kbd>: parar
<kbd>p</kbd>: pausa
<kbd>r</kbd>: reiniciar
<kbd>S</kbd>: iniciar
<kbd>a</kbd>: attach
<kbd>m</kbd>: ver logs
<kbd>U</kbd>: levantar proyecto
<kbd>D</kbd>: dar de baja el proyecto
<kbd>R</kbd>: ver opciones de reinicio
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>b</kbd>: ver comandos masivos
<kbd>E</kbd>: ejecutar shell
<kbd>w</kbd>: abrir en navegador (first port is http)
<kbd>enter</kbd>: enfocar panel principal
<kbd>[</kbd>: anterior pestaña
<kbd>]</kbd>: siguiente pestaña
<kbd>/</kbd>: filtrar lista
</pre>
## Imágenes
<pre>
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>d</kbd>: limpiar imagen
<kbd>b</kbd>: ver comandos masivos
<kbd>enter</kbd>: enfocar panel principal
<kbd>[</kbd>: anterior pestaña
<kbd>]</kbd>: siguiente pestaña
<kbd>/</kbd>: filtrar lista
</pre>
## Volúmenes
<pre>
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>d</kbd>: limpiar volúmen
<kbd>b</kbd>: ver comandos masivos
<kbd>enter</kbd>: enfocar panel principal
<kbd>[</kbd>: anterior pestaña
<kbd>]</kbd>: siguiente pestaña
<kbd>/</kbd>: filtrar lista
</pre>
## Redes
<pre>
<kbd>c</kbd>: ejecutar comando personalizado
<kbd>d</kbd>: limpiar red
<kbd>b</kbd>: ver comandos masivos
<kbd>enter</kbd>: enfocar panel principal
<kbd>[</kbd>: anterior pestaña
<kbd>]</kbd>: siguiente pestaña
<kbd>/</kbd>: filtrar lista
</pre>
## Inicio
<pre>
<kbd>esc</kbd>: regresar
</pre>
## Global
<pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre>

View file

@ -1,114 +0,0 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazydocker menu
## Projet
<pre>
<kbd>e</kbd>: modifier la configuration lazydocker
<kbd>o</kbd>: ouvrir la configuration lazydocker
<kbd>m</kbd>: voir les enregistrements
<kbd>enter</kbd>: focus panneau principal
<kbd>[</kbd>: onglet précédent
<kbd>]</kbd>: onglet suivant
<kbd>/</kbd>: filter list
</pre>
## Conteneurs
<pre>
<kbd>d</kbd>: supprimer
<kbd>e</kbd>: cacher/montrer les conteneurs arrêtés
<kbd>p</kbd>: pause
<kbd>s</kbd>: arrêter
<kbd>r</kbd>: redémarrer
<kbd>a</kbd>: attacher
<kbd>m</kbd>: voir les enregistrements
<kbd>E</kbd>: exécuter le shell
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>b</kbd>: voir les commandes groupées
<kbd>w</kbd>: ouvrir dans le navigateur (le premier port est http)
<kbd>enter</kbd>: focus panneau principal
<kbd>[</kbd>: onglet précédent
<kbd>]</kbd>: onglet suivant
<kbd>/</kbd>: filter list
</pre>
## Services
<pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: supprimer les conteneurs
<kbd>s</kbd>: arrêter
<kbd>p</kbd>: pause
<kbd>r</kbd>: redémarrer
<kbd>S</kbd>: démarrer
<kbd>a</kbd>: attacher
<kbd>m</kbd>: voir les enregistrements
<kbd>U</kbd>: up project
<kbd>D</kbd>: down project
<kbd>R</kbd>: voir les options de redémarrage
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>b</kbd>: voir les commandes groupées
<kbd>E</kbd>: exécuter le shell
<kbd>w</kbd>: ouvrir dans le navigateur (le premier port est http)
<kbd>enter</kbd>: focus panneau principal
<kbd>[</kbd>: onglet précédent
<kbd>]</kbd>: onglet suivant
<kbd>/</kbd>: filter list
</pre>
## Images
<pre>
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>d</kbd>: supprimer l'image
<kbd>b</kbd>: voir les commandes groupées
<kbd>enter</kbd>: focus panneau principal
<kbd>[</kbd>: onglet précédent
<kbd>]</kbd>: onglet suivant
<kbd>/</kbd>: filter list
</pre>
## Volumes
<pre>
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>d</kbd>: supprimer le volume
<kbd>b</kbd>: voir les commandes groupées
<kbd>enter</kbd>: focus panneau principal
<kbd>[</kbd>: onglet précédent
<kbd>]</kbd>: onglet suivant
<kbd>/</kbd>: filter list
</pre>
## Réseaux
<pre>
<kbd>c</kbd>: exécuter une commande prédéfinie
<kbd>d</kbd>: supprimer le réseau
<kbd>b</kbd>: voir les commandes groupées
<kbd>enter</kbd>: focus panneau principal
<kbd>[</kbd>: onglet précédent
<kbd>]</kbd>: onglet suivant
<kbd>/</kbd>: filter list
</pre>
## Principal
<pre>
<kbd>esc</kbd>: retour
</pre>
## Global
<pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre>

View file

@ -1,4 +1,4 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ _This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._
# Lazydocker menu # Lazydocker menu
@ -7,16 +7,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>e</kbd>: verander de lazydocker configuratie <kbd>e</kbd>: verander de lazydocker configuratie
<kbd>o</kbd>: open de lazydocker configuratie <kbd>o</kbd>: open de lazydocker configuratie
<kbd>m</kbd>: bekijk logs
<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 <kbd>m</kbd>: bekijk logs
<kbd>enter</kbd>: focus hoofdpaneel
</pre> </pre>
## Containers ## Containers
<pre> <pre>
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>d</kbd>: verwijder <kbd>d</kbd>: verwijder
<kbd>e</kbd>: verberg gestopte containers <kbd>e</kbd>: verberg gestopte containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -29,15 +30,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus hoofdpaneel <kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Diensten ## Diensten
<pre> <pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: verwijder containers <kbd>d</kbd>: verwijder containers
<kbd>s</kbd>: stop <kbd>s</kbd>: stop
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -45,53 +42,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>S</kbd>: start <kbd>S</kbd>: start
<kbd>a</kbd>: verbinden <kbd>a</kbd>: verbinden
<kbd>m</kbd>: bekijk logs <kbd>m</kbd>: bekijk logs
<kbd>U</kbd>: up project <kbd>[</kbd>: vorige tab
<kbd>D</kbd>: down project <kbd>]</kbd>: volgende tab
<kbd>R</kbd>: bekijk herstart opties <kbd>R</kbd>: bekijk herstart opties
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht <kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell <kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus hoofdpaneel <kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Images ## Images
<pre> <pre>
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht <kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>d</kbd>: verwijder image <kbd>d</kbd>: verwijder image
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus hoofdpaneel <kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Volumes ## Volumes
<pre> <pre>
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht <kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>d</kbd>: verwijder volume <kbd>d</kbd>: verwijder volume
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus hoofdpaneel <kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre>
## Networks
<pre>
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>d</kbd>: verwijder network
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre> </pre>
## Hoofd ## Hoofd
@ -105,10 +85,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen) <kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode <kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre> </pre>

View file

@ -1,4 +1,4 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ _This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._
# Lazydocker menu # Lazydocker menu
@ -7,16 +7,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>e</kbd>: edytuj konfigurację <kbd>e</kbd>: edytuj konfigurację
<kbd>o</kbd>: otwórz konfigurację <kbd>o</kbd>: otwórz konfigurację
<kbd>m</kbd>: pokaż logi
<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 <kbd>m</kbd>: pokaż logi
<kbd>enter</kbd>: skup na głównym panelu
</pre> </pre>
## Kontenery ## Kontenery
<pre> <pre>
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>d</kbd>: usuń <kbd>d</kbd>: usuń
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -29,15 +30,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: skup na głównym panelu <kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre> </pre>
## Serwisy ## Serwisy
<pre> <pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: usuń kontenery <kbd>d</kbd>: usuń kontenery
<kbd>s</kbd>: zatrzymaj <kbd>s</kbd>: zatrzymaj
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -45,53 +42,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>S</kbd>: start <kbd>S</kbd>: start
<kbd>a</kbd>: przyczep <kbd>a</kbd>: przyczep
<kbd>m</kbd>: pokaż logi <kbd>m</kbd>: pokaż logi
<kbd>U</kbd>: up project <kbd>[</kbd>: poprzednia zakładka
<kbd>D</kbd>: down project <kbd>]</kbd>: następna zakładka
<kbd>R</kbd>: pokaż opcje restartu <kbd>R</kbd>: pokaż opcje restartu
<kbd>c</kbd>: wykonaj predefiniowaną własną komende <kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell <kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: skup na głównym panelu <kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre> </pre>
## Obrazy ## Obrazy
<pre> <pre>
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>c</kbd>: wykonaj predefiniowaną własną komende <kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>d</kbd>: usuń obraz <kbd>d</kbd>: usuń obraz
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: skup na głównym panelu <kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre> </pre>
## Wolumeny ## Wolumeny
<pre> <pre>
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>c</kbd>: wykonaj predefiniowaną własną komende <kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>d</kbd>: usuń wolumen <kbd>d</kbd>: usuń wolumen
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: skup na głównym panelu <kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre>
## Sieci
<pre>
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>d</kbd>: usuń sieci
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre> </pre>
## Główne ## Główne
@ -105,10 +85,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen) <kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode <kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre> </pre>

View file

@ -1,114 +0,0 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazydocker menu
## Projeto
<pre>
<kbd>e</kbd>: editar configuração do lazydocker
<kbd>o</kbd>: abrir configuração do lazydocker
<kbd>m</kbd>: ver logs
<kbd>enter</kbd>: focar no painel principal
<kbd>[</kbd>: aba anterior
<kbd>]</kbd>: próxima aba
<kbd>/</kbd>: filtrar lista
</pre>
## Contêineres
<pre>
<kbd>d</kbd>: remover
<kbd>e</kbd>: ocultar/mostrar contêineres parados
<kbd>p</kbd>: pausar
<kbd>s</kbd>: parar
<kbd>r</kbd>: reiniciar
<kbd>a</kbd>: anexar
<kbd>m</kbd>: ver logs
<kbd>E</kbd>: executar shell
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>b</kbd>: ver comandos em massa
<kbd>w</kbd>: abrir no navegador (primeira porta é http)
<kbd>enter</kbd>: focar no painel principal
<kbd>[</kbd>: aba anterior
<kbd>]</kbd>: próxima aba
<kbd>/</kbd>: filtrar lista
</pre>
## Serviços
<pre>
<kbd>u</kbd>: subir serviço
<kbd>d</kbd>: remover contêineres
<kbd>s</kbd>: parar
<kbd>p</kbd>: pausar
<kbd>r</kbd>: reiniciar
<kbd>S</kbd>: iniciar
<kbd>a</kbd>: anexar
<kbd>m</kbd>: ver logs
<kbd>U</kbd>: subir projeto
<kbd>D</kbd>: derrubar projeto
<kbd>R</kbd>: ver opções de reinício
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>b</kbd>: ver comandos em massa
<kbd>E</kbd>: executar shell
<kbd>w</kbd>: abrir no navegador (primeira porta é http)
<kbd>enter</kbd>: focar no painel principal
<kbd>[</kbd>: aba anterior
<kbd>]</kbd>: próxima aba
<kbd>/</kbd>: filtrar lista
</pre>
## Imagens
<pre>
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>d</kbd>: remover imagem
<kbd>b</kbd>: ver comandos em massa
<kbd>enter</kbd>: focar no painel principal
<kbd>[</kbd>: aba anterior
<kbd>]</kbd>: próxima aba
<kbd>/</kbd>: filtrar lista
</pre>
## Volumes
<pre>
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>d</kbd>: remover volume
<kbd>b</kbd>: ver comandos em massa
<kbd>enter</kbd>: focar no painel principal
<kbd>[</kbd>: aba anterior
<kbd>]</kbd>: próxima aba
<kbd>/</kbd>: filtrar lista
</pre>
## Redes
<pre>
<kbd>c</kbd>: executar comando personalizado predefinido
<kbd>d</kbd>: remover rede
<kbd>b</kbd>: ver comandos em massa
<kbd>enter</kbd>: focar no painel principal
<kbd>[</kbd>: aba anterior
<kbd>]</kbd>: próxima aba
<kbd>/</kbd>: filtrar lista
</pre>
## Principal
<pre>
<kbd>esc</kbd>: retornar
</pre>
## Global
<pre>
<kbd>+</kbd>: modo de tela seguinte (normal/meia/tela cheia)
<kbd>_</kbd>: modo de tela anterior
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre>

View file

@ -1,4 +1,4 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._ _This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/generate_cheatsheet.go` from the project root._
# Lazydocker menü # Lazydocker menü
@ -7,16 +7,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>e</kbd>: lazzydocker ayarlarını düzenle <kbd>e</kbd>: lazzydocker ayarlarını düzenle
<kbd>o</kbd>: lazydocker ayarlarını <kbd>o</kbd>: lazydocker ayarlarını
<kbd>m</kbd>: kayıt defterini görüntüle
<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 <kbd>m</kbd>: kayıt defterini görüntüle
<kbd>enter</kbd>: ana panele odaklan
</pre> </pre>
## Konteynerler ## Konteynerler
<pre> <pre>
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>d</kbd>: kaldır <kbd>d</kbd>: kaldır
<kbd>e</kbd>: hide/show stopped containers <kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -29,15 +30,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: ana panele odaklan <kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre> </pre>
## Servisler ## Servisler
<pre> <pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: konteynerleri kaldır <kbd>d</kbd>: konteynerleri kaldır
<kbd>s</kbd>: durdur <kbd>s</kbd>: durdur
<kbd>p</kbd>: pause <kbd>p</kbd>: pause
@ -45,53 +42,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>S</kbd>: start <kbd>S</kbd>: start
<kbd>a</kbd>: bağlan/iliştir <kbd>a</kbd>: bağlan/iliştir
<kbd>m</kbd>: kayıt defterini görüntüle <kbd>m</kbd>: kayıt defterini görüntüle
<kbd>U</kbd>: up project <kbd>[</kbd>: önceki sekme
<kbd>D</kbd>: down project <kbd>]</kbd>: sonraki sekme
<kbd>R</kbd>: yeniden başlatma seçeneklerini görüntüle <kbd>R</kbd>: yeniden başlatma seçeneklerini görüntüle
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır <kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell <kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http) <kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: ana panele odaklan <kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre> </pre>
## Imajlar ## Imajlar
<pre> <pre>
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır <kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>d</kbd>: imajı kaldır <kbd>d</kbd>: imajı kaldır
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: ana panele odaklan <kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre> </pre>
## Alanlar ## Alanlar
<pre> <pre>
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır <kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>d</kbd>: alanı kaldır <kbd>d</kbd>: alanı kaldır
<kbd>b</kbd>: view bulk commands <kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: ana panele odaklan <kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre>
## Ağları
<pre>
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>d</kbd>: ağı kaldır
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre> </pre>
## Ana ## Ana
@ -105,10 +85,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre> <pre>
<kbd>+</kbd>: next screen mode (normal/half/fullscreen) <kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode <kbd>_</kbd>: prev screen mode
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre> </pre>

View file

@ -1,114 +0,0 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazydocker 菜单
## 项目
<pre>
<kbd>e</kbd>: 编辑lazydocker配置
<kbd>o</kbd>: 打开lazydocker配置
<kbd>m</kbd>: 查看日志
<kbd>enter</kbd>: 聚焦主面板
<kbd>[</kbd>: 上一个选项卡
<kbd>]</kbd>: 下一个选项卡
<kbd>/</kbd>: 过滤列表
</pre>
## 容器
<pre>
<kbd>d</kbd>: 移除
<kbd>e</kbd>: 隐藏/显示已停止的容器
<kbd>p</kbd>: 暂停
<kbd>s</kbd>: 停止
<kbd>r</kbd>: 重新启动
<kbd>a</kbd>: attach
<kbd>m</kbd>: 查看日志
<kbd>E</kbd>: 执行shell
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>b</kbd>: 查看批量命令
<kbd>w</kbd>: 在浏览器中打开(第一个端口为http)
<kbd>enter</kbd>: 聚焦主面板
<kbd>[</kbd>: 上一个选项卡
<kbd>]</kbd>: 下一个选项卡
<kbd>/</kbd>: 过滤列表
</pre>
## 服务
<pre>
<kbd>u</kbd>: 启动服务
<kbd>d</kbd>: 移除容器
<kbd>s</kbd>: 停止
<kbd>p</kbd>: 暂停
<kbd>r</kbd>: 重新启动
<kbd>S</kbd>: 启动项目
<kbd>a</kbd>: attach
<kbd>m</kbd>: 查看日志
<kbd>U</kbd>: 创建并启动容器
<kbd>D</kbd>: 停止并移除容器
<kbd>R</kbd>: 查看重启选项
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>b</kbd>: 查看批量命令
<kbd>E</kbd>: 执行shell
<kbd>w</kbd>: 在浏览器中打开(第一个端口为http)
<kbd>enter</kbd>: 聚焦主面板
<kbd>[</kbd>: 上一个选项卡
<kbd>]</kbd>: 下一个选项卡
<kbd>/</kbd>: 过滤列表
</pre>
## 镜像
<pre>
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>d</kbd>: 移除镜像
<kbd>b</kbd>: 查看批量命令
<kbd>enter</kbd>: 聚焦主面板
<kbd>[</kbd>: 上一个选项卡
<kbd>]</kbd>: 下一个选项卡
<kbd>/</kbd>: 过滤列表
</pre>
## 卷
<pre>
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>d</kbd>: 移除卷
<kbd>b</kbd>: 查看批量命令
<kbd>enter</kbd>: 聚焦主面板
<kbd>[</kbd>: 上一个选项卡
<kbd>]</kbd>: 下一个选项卡
<kbd>/</kbd>: 过滤列表
</pre>
## 网络
<pre>
<kbd>c</kbd>: 运行预定义的自定义命令
<kbd>d</kbd>: 移除网络
<kbd>b</kbd>: 查看批量命令
<kbd>enter</kbd>: 聚焦主面板
<kbd>[</kbd>: 上一个选项卡
<kbd>]</kbd>: 下一个选项卡
<kbd>/</kbd>: 过滤列表
</pre>
## 主要
<pre>
<kbd>esc</kbd>: 返回
</pre>
## 全局
<pre>
<kbd>+</kbd>: 下一个屏幕模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一个屏幕模式
<kbd>1</kbd>: focus projects panel
<kbd>2</kbd>: focus services panel
<kbd>3</kbd>: focus containers panel
<kbd>4</kbd>: focus images panel
<kbd>5</kbd>: focus volumes panel
<kbd>6</kbd>: focus networks panel
</pre>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

92
go.mod
View file

@ -1,85 +1,63 @@
module github.com/jesseduffield/lazydocker module github.com/jesseduffield/lazydocker
go 1.22 go 1.18
toolchain go1.23.6
require ( require (
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/docker/cli v27.1.1+incompatible github.com/docker/docker v20.10.15+incompatible
github.com/docker/docker v28.5.2+incompatible github.com/fatih/color v1.7.0
github.com/fatih/color v1.10.0 github.com/go-errors/errors v1.4.2
github.com/go-errors/errors v1.5.1 github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/gookit/color v1.5.0 github.com/gookit/color v1.5.0
github.com/imdario/mergo v0.3.16 github.com/imdario/mergo v0.3.8
github.com/integrii/flaggy v1.4.0 github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513 github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996 github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
github.com/mattn/go-runewidth v0.0.15 github.com/mattn/go-runewidth v0.0.13
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767
github.com/mgutz/str v1.2.0 github.com/mgutz/str v1.2.0
github.com/pmezard/go-difflib v1.0.0
github.com/samber/lo v1.31.0 github.com/samber/lo v1.31.0
github.com/sasha-s/go-deadlock v0.3.1 github.com/sirupsen/logrus v1.4.2
github.com/sirupsen/logrus v1.9.3
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.8.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
) )
require ( require (
github.com/containerd/errdefs v1.0.0 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gdamore/tcell/v2 v2.5.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect github.com/gogo/protobuf v1.3.1 // indirect
github.com/gdamore/encoding v1.0.1 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/gdamore/tcell/v2 v2.7.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-yaml v1.11.0
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-isatty v0.0.11 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect github.com/onsi/gomega v1.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect golang.org/x/text v0.3.7 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
golang.org/x/crypto v0.24.0 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect gotest.tools/v3 v3.2.0 // indirect
) )

270
go.sum
View file

@ -1,247 +1,189 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c h1:YDsGA6tou+tAxVe0Dre29iSbQ8TrWdWfwOisKArJT5E= github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c h1:YDsGA6tou+tAxVe0Dre29iSbQ8TrWdWfwOisKArJT5E=
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo= github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 h1:1fx+RA5lk1ZkzPAUP7DEgZnVHYxEcHO77vQO/V8z/2Q= github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 h1:1fx+RA5lk1ZkzPAUP7DEgZnVHYxEcHO77vQO/V8z/2Q=
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1/go.mod h1:z0nyIb42Zs97wyX1V+8MbEFhHeTw1OgFQfR6q57ZuHc= github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1/go.mod h1:z0nyIb42Zs97wyX1V+8MbEFhHeTw1OgFQfR6q57ZuHc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= github.com/docker/docker v20.10.15+incompatible h1:dk9FewY/9Xwm4ay/HViEEHSQuM/kL4F+JaG6GQdgmGo=
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v20.10.15+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw=
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM= github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI= github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee h1:7Zi/OQlGbMz4MT2V1+prN/gv1C64NDyVb/MbJnS0ZfA= github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee h1:7Zi/OQlGbMz4MT2V1+prN/gv1C64NDyVb/MbJnS0ZfA=
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee/go.mod h1:Z9UKHveKXXgyo8ME7R8yxh/BUTFOK+FgfWKlhy8oOAg= github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee/go.mod h1:Z9UKHveKXXgyo8ME7R8yxh/BUTFOK+FgfWKlhy8oOAg=
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513 h1:Y1bw5iItrsDCumATc/rklIJ/6K+68ieiWZJedhrNuXo= github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21taUpXiIbFkjjIIcn0E5GKwp5UFRuXaOiGQ=
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8= github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996 h1:CH1en6GpXSwnXl5Ehc4WX1NpS3uw9qbi7o9A4T2YYmA= github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 h1:UzTg0utG+cLS94Qjb/ZFDzfMeZDFXErSbNgJOyj70EA=
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ= github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg=
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 h1:BrhJNdEFWGuiJk/3/SwsG5Rex3zjFxYsDi2bpd7382Y= github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 h1:BrhJNdEFWGuiJk/3/SwsG5Rex3zjFxYsDi2bpd7382Y=
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767/go.mod h1:ct+byCpkFokm4J0tiuAvB8cf2ttm6GcCe89Yr25nGKg= github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767/go.mod h1:ct+byCpkFokm4J0tiuAvB8cf2ttm6GcCe89Yr25nGKg=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw= github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w= github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM= github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=

View file

@ -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())
} }

View file

@ -1,87 +0,0 @@
package cheatsheet
import (
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"regexp"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/pmezard/go-difflib/difflib"
)
func Check() {
dir := GetKeybindingsDir()
tmpDir := filepath.Join(os.TempDir(), "lazydocker_cheatsheet")
err := os.RemoveAll(tmpDir)
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
defer os.RemoveAll(tmpDir)
if err = os.Mkdir(tmpDir, 0o700); err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
generateAtDir(tmpDir)
actualContent := obtainContent(dir)
expectedContent := obtainContent(tmpDir)
if expectedContent == "" {
log.Fatal("empty expected content")
}
if actualContent != expectedContent {
if err := difflib.WriteUnifiedDiff(os.Stdout, difflib.UnifiedDiff{
A: difflib.SplitLines(expectedContent),
B: difflib.SplitLines(actualContent),
FromFile: "Expected",
FromDate: "",
ToFile: "Actual",
ToDate: "",
Context: 1,
}); err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
fmt.Printf(
"\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes. "+
"If you run the script and no keybindings files are updated as a result, try rebasing onto master"+
"and trying again.\n",
generateCheatsheetCmd,
)
os.Exit(1)
}
fmt.Println("\nCheatsheets are up to date")
}
func GetKeybindingsDir() string {
return utils.GetLazyRootDirectory() + "/docs/keybindings"
}
func obtainContent(dir string) string {
re := regexp.MustCompile(`Keybindings_\w+\.md$`)
content := ""
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if re.MatchString(path) {
bytes, err := os.ReadFile(path)
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
content += fmt.Sprintf("\n%s\n\n", filepath.Base(path))
content += string(bytes)
}
return nil
})
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
return content
}

View file

@ -4,15 +4,21 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"sync"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/samber/lo"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/fatih/color"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
@ -27,21 +33,143 @@ type Container struct {
OneOff bool OneOff bool
ProjectName string ProjectName string
ID string ID string
Container container.Summary Container types.Container
DisplayString string
Client *client.Client Client *client.Client
OSCommand *OSCommand OSCommand *OSCommand
Config *config.AppConfig
Log *logrus.Entry Log *logrus.Entry
StatHistory []*RecordedStats StatHistory []*RecordedStats
Details container.InspectResponse Details types.ContainerJSON
MonitoringStats bool MonitoringStats bool
DockerCommand LimitedDockerCommand DockerCommand LimitedDockerCommand
Tr *i18n.TranslationSet Tr *i18n.TranslationSet
StatsMutex deadlock.Mutex StatsMutex sync.Mutex
}
// GetDisplayStrings returns the dispaly string of Container
func (c *Container) GetDisplayStrings(isFocused bool) []string {
image := strings.TrimPrefix(c.Container.Image, "sha256:")
return []string{c.GetDisplayStatus(), c.GetDisplaySubstatus(), c.Name, c.GetDisplayCPUPerc(), utils.ColoredString(image, color.FgMagenta), c.displayPorts()}
}
func (c *Container) displayPorts() string {
portStrings := lo.Map(c.Container.Ports, func(port types.Port, _ int) string {
// docker ps will show '0.0.0.0:80->80/tcp' but we'll show
// '80->80/tcp' instead to save space (unless the IP is something other than
// 0.0.0.0)
ipString := ""
if port.IP != "0.0.0.0" {
ipString = port.IP + ":"
}
return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type)
})
return strings.Join(portStrings, ", ")
}
// GetDisplayStatus returns the colored status of the container
func (c *Container) GetDisplayStatus() string {
return utils.ColoredString(c.Container.State, c.GetColor())
}
// GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check)
func (c *Container) GetDisplaySubstatus() string {
if !c.DetailsLoaded() {
return ""
}
switch c.Container.State {
case "exited":
return utils.ColoredString(
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), c.GetColor(),
)
case "running":
return c.getHealthStatus()
default:
return ""
}
}
func (c *Container) getHealthStatus() string {
if !c.DetailsLoaded() {
return ""
}
healthStatusColorMap := map[string]color.Attribute{
"healthy": color.FgGreen,
"unhealthy": color.FgRed,
"starting": color.FgYellow,
}
if c.Details.State.Health == nil {
return ""
}
healthStatus := c.Details.State.Health.Status
if healthStatusColor, ok := healthStatusColorMap[healthStatus]; ok {
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
}
return ""
}
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
func (c *Container) GetDisplayCPUPerc() string {
stats, ok := c.getLastStats()
if !ok {
return ""
}
percentage := stats.DerivedStats.CPUPercentage
formattedPercentage := fmt.Sprintf("%.2f%%", stats.DerivedStats.CPUPercentage)
var clr color.Attribute
if percentage > 90 {
clr = color.FgRed
} else if percentage > 50 {
clr = color.FgYellow
} else {
clr = color.FgWhite
}
return utils.ColoredString(formattedPercentage, clr)
}
// ProducingLogs tells us whether we should bother checking a container's logs
func (c *Container) ProducingLogs() bool {
return c.Container.State == "running" && c.DetailsLoaded() && c.Details.HostConfig.LogConfig.Type != "none"
}
// GetColor Container color
func (c *Container) GetColor() color.Attribute {
switch c.Container.State {
case "exited":
// This means the colour may be briefly yellow and then switch to red upon starting
// Not sure what a better alternative is.
if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 {
return color.FgYellow
}
return color.FgRed
case "created":
return color.FgCyan
case "running":
return color.FgGreen
case "paused":
return color.FgYellow
case "dead":
return color.FgRed
case "restarting":
return color.FgBlue
case "removing":
return color.FgMagenta
default:
return color.FgWhite
}
} }
// Remove removes the container // Remove removes the container
func (c *Container) Remove(options container.RemoveOptions) error { func (c *Container) Remove(options types.ContainerRemoveOptions) error {
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name)) c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil { if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") { if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
@ -57,16 +185,10 @@ 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))
return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{}) return c.Client.ContainerStop(context.Background(), c.ID, nil)
} }
// Pause pauses the container // Pause pauses the container
@ -84,7 +206,7 @@ func (c *Container) Unpause() error {
// Restart restarts the container // Restart restarts the container
func (c *Container) Restart() error { func (c *Container) Restart() error {
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name)) c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{}) return c.Client.ContainerRestart(context.Background(), c.ID, nil)
} }
// Attach attaches the container // Attach attaches the container
@ -104,23 +226,23 @@ func (c *Container) Attach() (*exec.Cmd, error) {
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name)) c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
// TODO: use SDK // TODO: use SDK
cmd := c.OSCommand.NewCmd("docker", "attach", "--sig-proxy=false", c.ID) cmd := c.OSCommand.PrepareSubProcess("docker", "attach", "--sig-proxy=false", c.ID)
return cmd, nil return cmd, nil
} }
// Top returns process information // Top returns process information
func (c *Container) Top(ctx context.Context) (container.TopResponse, error) { func (c *Container) Top() (container.ContainerTopOKBody, error) {
detail, err := c.Inspect() detail, err := c.Inspect()
if err != nil { if err != nil {
return container.TopResponse{}, err return container.ContainerTopOKBody{}, err
} }
// check container status // check container status
if !detail.State.Running { if !detail.State.Running {
return container.TopResponse{}, errors.New("container is not running") return container.ContainerTopOKBody{}, errors.New("container is not running")
} }
return c.Client.ContainerTop(ctx, c.ID, []string{}) return c.Client.ContainerTop(context.Background(), c.ID, []string{})
} }
// PruneContainers prunes containers // PruneContainers prunes containers
@ -130,13 +252,13 @@ func (c *DockerCommand) PruneContainers() error {
} }
// Inspect returns details about the container // Inspect returns details about the container
func (c *Container) Inspect() (container.InspectResponse, error) { func (c *Container) Inspect() (types.ContainerJSON, error) {
return c.Client.ContainerInspect(context.Background(), c.ID) return c.Client.ContainerInspect(context.Background(), c.ID)
} }
// RenderTop returns details about the container // RenderTop returns details about the container
func (c *Container) RenderTop(ctx context.Context) (string, error) { func (c *Container) RenderTop() (string, error) {
result, err := c.Top(ctx) result, err := c.Top()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -144,9 +266,7 @@ func (c *Container) RenderTop(ctx context.Context) (string, error) {
return utils.RenderTable(append([][]string{result.Titles}, result.Processes...)) return utils.RenderTable(append([][]string{result.Titles}, result.Processes...))
} }
// DetailsLoaded tells us whether we have yet loaded the details for a container. // DetailsLoaded tells us whether we have yet loaded the details for a container. Because this is an asynchronous operation, sometimes we have the container before we have its details.
// Sometimes it takes some time for a container to have its details loaded
// after it starts.
func (c *Container) DetailsLoaded() bool { func (c *Container) DetailsLoaded() bool {
return c.Details.ContainerJSONBase != nil return c.Details.ContainerJSONBase != nil
} }

View file

@ -1,8 +1,19 @@
package commands package commands
import ( import (
"encoding/json"
"fmt"
"math" "math"
"reflect"
"strconv"
"strings"
"time" "time"
"github.com/fatih/color"
"github.com/jesseduffield/asciigraph"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/mcuadros/go-lookup"
) )
// RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct // RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct
@ -155,29 +166,66 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
return value return value
} }
func (c *Container) appendStats(stats *RecordedStats, maxDuration time.Duration) { // RenderStats returns a string containing the rendered stats of the container
func (c *Container) RenderStats(viewWidth int) (string, error) {
stats, ok := c.getLastStats()
if !ok {
return "", nil
}
graphSpecs := c.Config.UserConfig.Stats.Graphs
graphs := make([]string, len(graphSpecs))
for i, spec := range graphSpecs {
graph, err := c.PlotGraph(spec, viewWidth-10)
if err != nil {
return "", err
}
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
}
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
originalJSON, err := json.MarshalIndent(stats, "", " ")
if err != nil {
return "", err
}
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
pidsCount,
dataReceived,
dataSent,
string(originalJSON),
)
return contents, nil
}
func (c *Container) appendStats(stats *RecordedStats) {
c.StatsMutex.Lock() c.StatsMutex.Lock()
defer c.StatsMutex.Unlock() defer c.StatsMutex.Unlock()
c.StatHistory = append(c.StatHistory, stats) c.StatHistory = append(c.StatHistory, stats)
c.eraseOldHistory(maxDuration) c.eraseOldHistory()
} }
// eraseOldHistory removes any history before the user-specified max duration // eraseOldHistory removes any history before the user-specified max duration
func (c *Container) eraseOldHistory(maxDuration time.Duration) { func (c *Container) eraseOldHistory() {
if maxDuration == 0 { if c.Config.UserConfig.Stats.MaxDuration == 0 {
return return
} }
for i, stat := range c.StatHistory { for i, stat := range c.StatHistory {
if time.Since(stat.RecordedAt) < maxDuration { if time.Since(stat.RecordedAt) < c.Config.UserConfig.Stats.MaxDuration {
c.StatHistory = c.StatHistory[i:] c.StatHistory = c.StatHistory[i:]
return return
} }
} }
} }
func (c *Container) GetLastStats() (*RecordedStats, bool) { func (c *Container) getLastStats() (*RecordedStats, bool) {
c.StatsMutex.Lock() c.StatsMutex.Lock()
defer c.StatsMutex.Unlock() defer c.StatsMutex.Unlock()
history := c.StatHistory history := c.StatHistory
@ -186,3 +234,95 @@ func (c *Container) GetLastStats() (*RecordedStats, bool) {
} }
return history[len(history)-1], true return history[len(history)-1], true
} }
// PlotGraph returns the plotted graph based on the graph spec and the stat history
func (c *Container) PlotGraph(spec config.GraphConfig, width int) (string, error) {
c.StatsMutex.Lock()
defer c.StatsMutex.Unlock()
data := make([]float64, len(c.StatHistory))
max := spec.Max
min := spec.Min
for i, stats := range c.StatHistory {
value, err := lookup.LookupString(stats, spec.StatPath)
if err != nil {
return "Could not find key: " + spec.StatPath, nil
}
floatValue, err := getFloat(value.Interface())
if err != nil {
return "", err
}
if spec.MinType == "" {
if i == 0 {
min = floatValue
} else if floatValue < min {
min = floatValue
}
}
if spec.MaxType == "" {
if i == 0 {
max = floatValue
} else if floatValue > max {
max = floatValue
}
}
data[i] = floatValue
}
height := 10
if spec.Height > 0 {
height = spec.Height
}
return asciigraph.Plot(
data,
asciigraph.Height(height),
asciigraph.Width(width),
asciigraph.Min(min),
asciigraph.Max(max),
asciigraph.Caption(fmt.Sprintf("%s: %0.2f (%v)", spec.Caption, data[len(data)-1], time.Since(c.StatHistory[0].RecordedAt).Round(time.Second))),
), nil
}
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
func getFloat(unk interface{}) (float64, error) {
floatType := reflect.TypeOf(float64(0))
stringType := reflect.TypeOf("")
switch i := unk.(type) {
case float64:
return i, nil
case float32:
return float64(i), nil
case int64:
return float64(i), nil
case int32:
return float64(i), nil
case int:
return float64(i), nil
case uint64:
return float64(i), nil
case uint32:
return float64(i), nil
case uint:
return float64(i), nil
case string:
return strconv.ParseFloat(i, 64)
default:
v := reflect.ValueOf(unk)
v = reflect.Indirect(v)
if v.Type().ConvertibleTo(floatType) {
fv := v.Convert(floatType)
return fv.Float(), nil
} else if v.Type().ConvertibleTo(stringType) {
sv := v.Convert(stringType)
s := sv.String()
return strconv.ParseFloat(s, 64)
} else {
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
}
}
}

View file

@ -7,30 +7,24 @@ import (
"fmt" "fmt"
"io" "io"
ogLog "log" ogLog "log"
"os"
"os/exec" "os/exec"
"path/filepath"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
cliconfig "github.com/docker/cli/cli/config" "github.com/docker/docker/api/types"
ddocker "github.com/docker/cli/cli/context/docker"
ctxstore "github.com/docker/cli/cli/context/store"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"github.com/jesseduffield/lazydocker/pkg/commands/ssh" "github.com/jesseduffield/lazydocker/pkg/commands/ssh"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const ( const (
dockerHostEnvKey = "DOCKER_HOST" APIVersion = "1.25"
) )
// DockerCommand is our main docker interface // DockerCommand is our main docker interface
@ -41,12 +35,16 @@ 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. ShowExited bool
LocalProjectName string
ErrorChan chan error ErrorChan chan error
ContainerMutex deadlock.Mutex ContainerMutex sync.Mutex
ServiceMutex deadlock.Mutex ServiceMutex sync.Mutex
Services []*Service
Containers []*Container
// DisplayContainers is the array of containers we will display in the containers panel. If Gui.ShowAllContainers is false, this will only be those containers which aren't based on a service. This reduces clutter and duplication in the UI
DisplayContainers []*Container
Images []*Image
Volumes []*Volume
Closers []io.Closer Closers []io.Closer
} }
@ -64,68 +62,23 @@ type CommandObject struct {
Container *Container Container *Container
Image *Image Image *Image
Volume *Volume Volume *Volume
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()
if err != nil {
ogLog.Printf("> could not determine host %v", err)
}
// NOTE: Inject the determined docker host to the environment. This allows the
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
// over SSH to the specified ssh host.
if strings.HasPrefix(dockerHost, "ssh://") {
os.Setenv(dockerHostEnvKey, dockerHost)
}
tunnelCloser, err := ssh.NewSSHHandler(osCommand).HandleSSHDockerHost() tunnelCloser, err := ssh.NewSSHHandler(osCommand).HandleSSHDockerHost()
if err != nil { if err != nil {
ogLog.Fatal(err) ogLog.Fatal(err)
} }
// Retrieve the docker host from the environment which could have been set by cli, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(APIVersion))
// the `SSHHandler.HandleSSHDockerHost()` and override `dockerHost`.
dockerHostFromEnv := os.Getenv(dockerHostEnvKey)
if dockerHostFromEnv != "" {
dockerHost = dockerHostFromEnv
}
cli, err := newDockerClient(dockerHost)
if err != nil { if err != nil {
ogLog.Fatal(err) ogLog.Fatal(err)
} }
@ -137,11 +90,17 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
Config: config, Config: config,
Client: cli, Client: cli,
ErrorChan: errorChan, ErrorChan: errorChan,
ShowExited: true,
InDockerComposeProject: true, InDockerComposeProject: true,
Closers: []io.Closer{tunnelCloser}, Closers: []io.Closer{tunnelCloser},
} }
dockerCommand.setDockerComposeCommand(config) command := utils.ApplyTemplate(
config.UserConfig.CommandTemplates.CheckDockerComposeConfig,
dockerCommand.NewCommandObject(CommandObject{}),
)
log.Warn(command)
err = osCommand.RunCommand( err = osCommand.RunCommand(
utils.ApplyTemplate( utils.ApplyTemplate(
@ -154,42 +113,33 @@ 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) {
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
return
}
// it's possible that a user is still using docker-compose, so we'll check if 'docker comopose' is available, and if not, we'll fall back to 'docker-compose'
err := c.OSCommand.RunCommand("docker compose version")
if err != nil {
config.UserConfig.CommandTemplates.DockerCompose = "docker-compose"
}
}
func (c *DockerCommand) Close() error { func (c *DockerCommand) Close() error {
return utils.CloseMany(c.Closers) return utils.CloseMany(c.Closers)
} }
func (c *DockerCommand) CreateClientStatMonitor(container *Container) { func (c *DockerCommand) MonitorContainerStats(ctx context.Context) {
// periodically loop through running containers and see if we need to create a monitor goroutine for any
// every second we check if we need to spawn a new goroutine
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
for _, container := range c.Containers {
if !container.MonitoringStats {
go c.createClientStatMonitor(container)
}
}
}
}
}
func (c *DockerCommand) createClientStatMonitor(container *Container) {
container.MonitoringStats = true container.MonitoringStats = true
stream, err := c.Client.ContainerStats(context.Background(), container.ID, true) stream, err := c.Client.ContainerStats(context.Background(), container.ID, true)
if err != nil { if err != nil {
@ -217,152 +167,69 @@ func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
RecordedAt: time.Now(), RecordedAt: time.Now(),
} }
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration) container.appendStats(recordedStats)
} }
container.MonitoringStats = false container.MonitoringStats = false
} }
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) { // RefreshContainersAndServices returns a slice of docker containers
func (c *DockerCommand) RefreshContainersAndServices() error {
c.ServiceMutex.Lock() c.ServiceMutex.Lock()
defer c.ServiceMutex.Unlock() defer c.ServiceMutex.Unlock()
containers, err := c.GetContainers(currentContainers) currentServices := c.Services
containers, err := c.GetContainers()
if err != nil { if err != nil {
return nil, nil, err return 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 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 displayContainers := containers
} if !c.Config.UserConfig.Gui.ShowAllContainers {
displayContainers = c.obtainStandaloneContainers(containers, services)
}
// GetServicesFromContainers derives services from container labels for all projects // sort services first by whether they have a linked container, and second by alphabetical order
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service { sort.Slice(services, func(i, j int) bool {
// Use project+service as key to avoid duplicates if services[i].Container != nil && services[j].Container == nil {
type serviceKey struct { return true
project string
service string
} }
seen := make(map[serviceKey]bool)
services := make([]*Service, 0, len(containers))
for _, ctr := range containers { if services[i].Container == nil && services[j].Container != nil {
if ctr.ServiceName == "" || ctr.OneOff { return false
continue
} }
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
if seen[key] { return services[i].Name < services[j].Name
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 c.Containers = containers
} c.Services = services
c.DisplayContainers = c.filterOutExited(displayContainers)
c.DisplayContainers = c.sortedContainers(c.DisplayContainers)
// mergeServices merges compose services (which may lack ProjectName) with return nil
// 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 _, container := range containers {
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName { if !container.OneOff && container.ServiceName == service.Name {
service.Container = ctr service.Container = container
continue L continue L
} }
} }
@ -370,24 +237,77 @@ L:
} }
} }
// filterOutExited filters out the exited containers if c.ShowExited is false
func (c *DockerCommand) filterOutExited(containers []*Container) []*Container {
if c.ShowExited {
return containers
}
toReturn := []*Container{}
for _, container := range containers {
if container.Container.State != "exited" {
toReturn = append(toReturn, container)
}
}
return toReturn
}
// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created)
// and sorted by name if c.SortContainersByState is false
func (c *DockerCommand) sortedContainers(containers []*Container) []*Container {
if !c.Config.UserConfig.Gui.LegacySortContainers {
states := map[string]int{
"running": 1,
"exited": 2,
"created": 3,
}
sort.Slice(containers, func(i, j int) bool {
stateLeft := states[containers[i].Container.State]
stateRight := states[containers[j].Container.State]
if stateLeft == stateRight {
return containers[i].Name < containers[j].Name
}
return states[containers[i].Container.State] < states[containers[j].Container.State]
})
}
return containers
}
// obtainStandaloneContainers returns standalone containers. Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context
func (c *DockerCommand) obtainStandaloneContainers(containers []*Container, services []*Service) []*Container {
standaloneContainers := []*Container{}
L:
for _, container := range containers {
for _, service := range services {
if !container.OneOff && container.ServiceName != "" && container.ServiceName == service.Name {
continue L
}
}
standaloneContainers = append(standaloneContainers, container)
}
return standaloneContainers
}
// GetContainers gets the docker containers // GetContainers gets the docker containers
func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) { func (c *DockerCommand) GetContainers() ([]*Container, error) {
c.ContainerMutex.Lock() c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock() defer c.ContainerMutex.Unlock()
containers, err := c.Client.ContainerList(context.Background(), container.ListOptions{All: true}) existingContainers := c.Containers
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil { if err != nil {
return nil, err return nil, err
} }
ownContainers := make([]*Container, len(containers)) ownContainers := make([]*Container, len(containers))
for i, ctr := range containers { for i, container := range containers {
var newContainer *Container var newContainer *Container
// check if we already have data stored against the container // check if we already data stored against the container
for _, existingContainer := range existingContainers { for _, existingContainer := range existingContainers {
if existingContainer.ID == ctr.ID { if existingContainer.ID == container.ID {
newContainer = existingContainer newContainer = existingContainer
break break
} }
@ -396,36 +316,31 @@ func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Conta
// initialise the container if it's completely new // initialise the container if it's completely new
if newContainer == nil { if newContainer == nil {
newContainer = &Container{ newContainer = &Container{
ID: ctr.ID, ID: container.ID,
Client: c.Client, Client: c.Client,
OSCommand: c.OSCommand, OSCommand: c.OSCommand,
Log: c.Log, Log: c.Log,
Config: c.Config,
DockerCommand: c, DockerCommand: c,
Tr: c.Tr, Tr: c.Tr,
} }
} }
newContainer.Container = ctr newContainer.Container = container
// if the container is made with a name label we will use that // if the container is made with a name label we will use that
if name, ok := ctr.Labels["name"]; ok { if name, ok := container.Labels["name"]; ok {
newContainer.Name = name newContainer.Name = name
} else { } else {
if len(ctr.Names) > 0 { newContainer.Name = strings.TrimLeft(container.Names[0], "/")
newContainer.Name = strings.TrimLeft(ctr.Names[0], "/")
} else {
newContainer.Name = ctr.ID
} }
} newContainer.ServiceName = container.Labels["com.docker.compose.service"]
newContainer.ServiceName = ctr.Labels["com.docker.compose.service"] newContainer.ProjectName = container.Labels["com.docker.compose.project"]
newContainer.ProjectName = ctr.Labels["com.docker.compose.project"] newContainer.ContainerNumber = container.Labels["com.docker.compose.container"]
newContainer.ContainerNumber = ctr.Labels["com.docker.compose.container"] newContainer.OneOff = container.Labels["com.docker.compose.oneoff"] == "True"
newContainer.OneOff = ctr.Labels["com.docker.compose.oneoff"] == "True"
ownContainers[i] = newContainer ownContainers[i] = newContainer
} }
c.SetContainerDetails(ownContainers)
return ownContainers, nil return ownContainers, nil
} }
@ -436,22 +351,22 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
} }
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand)) output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// output looks like: // output looks like:
// service1 // service1 998d6d286b0499e0ff23d66302e720991a2asdkf9c30d0542034f610daf8a971
// service2 // service2 asdld98asdklasd9bccd02438de0994f8e19cbe691feb3755336ec5ca2c55971
lines := utils.SplitLines(output) lines := utils.SplitLines(output)
services := make([]*Service, len(lines)) services := make([]*Service, len(lines))
for i, str := range lines { for i, str := range lines {
arr := strings.Split(str, " ")
services[i] = &Service{ services[i] = &Service{
Name: str, Name: arr[0],
ID: c.LocalProjectName + "-" + str, ID: arr[1],
ProjectName: c.LocalProjectName,
OSCommand: c.OSCommand, OSCommand: c.OSCommand,
Log: c.Log, Log: c.Log,
DockerCommand: c, DockerCommand: c,
@ -461,41 +376,30 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
return services, nil return services, nil
} }
func (c *DockerCommand) RefreshContainerDetails(containers []*Container) error { // UpdateContainerDetails attaches the details returned from docker inspect to each of the containers
// this contains a bit more info than what you get from the go-docker client
func (c *DockerCommand) UpdateContainerDetails() error {
c.ContainerMutex.Lock() c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock() defer c.ContainerMutex.Unlock()
c.SetContainerDetails(containers) for _, container := range c.Containers {
details, err := c.Client.ContainerInspect(context.Background(), container.ID)
if err != nil {
c.Log.Error(err)
} else {
container.Details = details
}
}
return nil return nil
} }
// Attaches the details returned from docker inspect to each of the containers
// this contains a bit more info than what you get from the go-docker client
func (c *DockerCommand) SetContainerDetails(containers []*Container) {
wg := sync.WaitGroup{}
for _, ctr := range containers {
ctr := ctr
wg.Add(1)
go func() {
details, err := c.Client.ContainerInspect(context.Background(), ctr.ID)
if err != nil {
c.Log.Error(err)
} else {
ctr.Details = details
}
wg.Done()
}()
}
wg.Wait()
}
// 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 +410,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 {
@ -522,64 +421,3 @@ func (c *DockerCommand) DockerComposeConfigForProject(project *Project) string {
} }
return output return output
} }
// determineDockerHost tries to the determine the docker host that we should connect to
// in the following order of decreasing precedence:
// - value of "DOCKER_HOST" environment variable
// - host retrieved from the current context (specified via DOCKER_CONTEXT)
// - "default docker host" for the host operating system, otherwise
func determineDockerHost() (string, error) {
// If the docker host is explicitly set via the "DOCKER_HOST" environment variable,
// then its a no-brainer :shrug:
if os.Getenv("DOCKER_HOST") != "" {
return os.Getenv("DOCKER_HOST"), nil
}
currentContext := os.Getenv("DOCKER_CONTEXT")
if currentContext == "" {
cf, err := cliconfig.Load(cliconfig.Dir())
if err != nil {
return "", err
}
currentContext = cf.CurrentContext
}
// On some systems (windows) `default` is stored in the docker config as the currentContext.
if currentContext == "" || currentContext == "default" {
// If a docker context is neither specified via the "DOCKER_CONTEXT" environment variable nor via the
// $HOME/.docker/config file, then we fall back to connecting to the "default docker host" meant for
// the host operating system.
return defaultDockerHost, nil
}
storeConfig := ctxstore.NewConfig(
func() interface{} { return &ddocker.EndpointMeta{} },
ctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),
)
st := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)
md, err := st.GetMetadata(currentContext)
if err != nil {
return "", err
}
dockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]
if !ok {
return "", err
}
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
if !ok {
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
}
if dockerEPMeta.Host != "" {
return dockerEPMeta.Host, nil
}
// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').
// For example:
// ```sh
// docker context create foo --docker "host="
// ```
// In such scenario, we mimic the `docker` cli and try to connect to the "default docker host".
return defaultDockerHost, nil
}

View file

@ -1,7 +0,0 @@
//go:build !windows
package commands
const (
defaultDockerHost = "unix:///var/run/docker.sock"
)

View file

@ -1,5 +0,0 @@
package commands
const (
defaultDockerHost = "npipe:////./pipe/docker_engine"
)

View file

@ -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())
})
}
}

View file

@ -1,7 +1,7 @@
package commands package commands
import ( import (
"io" "io/ioutil"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
@ -31,7 +31,7 @@ func NewDummyAppConfig() *config.AppConfig {
// NewDummyLog creates a new dummy Log for testing // NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry { func NewDummyLog() *logrus.Entry {
log := logrus.New() log := logrus.New()
log.Out = io.Discard log.Out = ioutil.Discard
return log.WithField("test", "test") return log.WithField("test", "test")
} }

View file

@ -2,14 +2,16 @@ package commands
import ( import (
"context" "context"
"sort"
"strings" "strings"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -18,15 +20,20 @@ type Image struct {
Name string Name string
Tag string Tag string
ID string ID string
Image image.Summary Image types.ImageSummary
Client *client.Client Client *client.Client
OSCommand *OSCommand OSCommand *OSCommand
Log *logrus.Entry Log *logrus.Entry
DockerCommand LimitedDockerCommand DockerCommand LimitedDockerCommand
} }
// GetDisplayStrings returns the display string of Image
func (i *Image) GetDisplayStrings(isFocused bool) []string {
return []string{i.Name, i.Tag, utils.FormatDecimalBytes(int(i.Image.Size))}
}
// Remove removes the image // Remove removes the image
func (i *Image) Remove(options image.RemoveOptions) error { func (i *Image) Remove(options types.ImageRemoveOptions) error {
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil { if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
return err return err
} }
@ -34,13 +41,19 @@ func (i *Image) Remove(options image.RemoveOptions) error {
return nil return nil
} }
func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []string { // Layer is a layer in an image's history
type Layer struct {
image.HistoryResponseItem
}
// GetDisplayStrings returns the array of strings describing the layer
func (l *Layer) GetDisplayStrings(isFocused bool) []string {
tag := "" tag := ""
if len(layer.Tags) > 0 { if len(l.Tags) > 0 {
tag = layer.Tags[0] tag = l.Tags[0]
} }
id := strings.TrimPrefix(layer.ID, "sha256:") id := strings.TrimPrefix(l.ID, "sha256:")
if len(id) > 10 { if len(id) > 10 {
id = id[0:10] id = id[0:10]
} }
@ -50,16 +63,16 @@ func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []str
} }
dockerFileCommandPrefix := "/bin/sh -c #(nop) " dockerFileCommandPrefix := "/bin/sh -c #(nop) "
createdBy := layer.CreatedBy createdBy := l.CreatedBy
if strings.Contains(layer.CreatedBy, dockerFileCommandPrefix) { if strings.Contains(l.CreatedBy, dockerFileCommandPrefix) {
createdBy = strings.Trim(strings.TrimPrefix(layer.CreatedBy, dockerFileCommandPrefix), " ") createdBy = strings.Trim(strings.TrimPrefix(l.CreatedBy, dockerFileCommandPrefix), " ")
split := strings.Split(createdBy, " ") split := strings.Split(createdBy, " ")
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ") createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
} }
createdBy = strings.Replace(createdBy, "\t", " ", -1) createdBy = strings.Replace(createdBy, "\t", " ", -1)
size := utils.FormatBinaryBytes(int(layer.Size)) size := utils.FormatBinaryBytes(int(l.Size))
sizeColor := color.FgWhite sizeColor := color.FgWhite
if size == "0B" { if size == "0B" {
sizeColor = color.FgBlue sizeColor = color.FgBlue
@ -80,28 +93,26 @@ func (i *Image) RenderHistory() (string, error) {
return "", err return "", err
} }
tableBody := lo.Map(history, func(layer image.HistoryResponseItem, _ int) []string { layers := make([]*Layer, len(history))
return getHistoryResponseItemDisplayStrings(layer) for i, layer := range history {
}) layers[i] = &Layer{layer}
}
headers := [][]string{{"ID", "TAG", "SIZE", "COMMAND"}} return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"}))
table := append(headers, tableBody...)
return utils.RenderTable(table)
} }
// RefreshImages returns a slice of docker images // RefreshImages returns a slice of docker images
func (c *DockerCommand) RefreshImages() ([]*Image, error) { func (c *DockerCommand) RefreshImages() ([]*Image, error) {
images, err := c.Client.ImageList(context.Background(), image.ListOptions{}) images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
ownImages := make([]*Image, len(images)) ownImages := make([]*Image, len(images))
for i, img := range images { for i, image := range images {
firstTag := "" firstTag := ""
tags := img.RepoTags tags := image.RepoTags
if len(tags) > 0 { if len(tags) > 0 {
firstTag = tags[0] firstTag = tags[0]
} }
@ -122,10 +133,10 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) {
} }
ownImages[i] = &Image{ ownImages[i] = &Image{
ID: img.ID, ID: image.ID,
Name: name, Name: name,
Tag: tag, Tag: tag,
Image: img, Image: image,
Client: c.Client, Client: c.Client,
OSCommand: c.OSCommand, OSCommand: c.OSCommand,
Log: c.Log, Log: c.Log,
@ -133,6 +144,20 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) {
} }
} }
noneLabel := "<none>"
sort.Slice(ownImages, func(i, j int) bool {
if ownImages[i].Name == noneLabel && ownImages[j].Name != noneLabel {
return false
}
if ownImages[i].Name != noneLabel && ownImages[j].Name == noneLabel {
return true
}
return ownImages[i].Name < ownImages[j].Name
})
return ownImages, nil return ownImages, nil
} }

View file

@ -1,54 +0,0 @@
package commands
import (
"context"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// Network : A docker Network
type Network struct {
Name string
Network network.Inspect
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
DockerCommand LimitedDockerCommand
}
// RefreshNetworks gets the networks and stores them
func (c *DockerCommand) RefreshNetworks() ([]*Network, error) {
networks, err := c.Client.NetworkList(context.Background(), network.ListOptions{})
if err != nil {
return nil, err
}
ownNetworks := make([]*Network, len(networks))
for i, nw := range networks {
ownNetworks[i] = &Network{
Name: nw.Name,
Network: nw,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
}
}
return ownNetworks, nil
}
// PruneNetworks prunes networks
func (c *DockerCommand) PruneNetworks() error {
_, err := c.Client.NetworksPrune(context.Background(), filters.Args{})
return err
}
// Remove removes the network
func (v *Network) Remove() error {
return v.Client.NetworkRemove(context.Background(), v.Name)
}

View file

@ -1,9 +1,8 @@
package commands package commands
import ( import (
"context"
"fmt" "fmt"
"io" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -25,8 +24,10 @@ type Platform struct {
os string os string
shell string shell string
shellArg string shellArg string
escapedQuote string
openCommand string openCommand string
openLinkCommand string openLinkCommand string
fallbackEscapedQuote string
} }
// OSCommand holds all the os commands // OSCommand holds all the os commands
@ -64,15 +65,6 @@ func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
return output, err return output, err
} }
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutputContext(ctx context.Context, command string) (string, error) {
cmd := c.ExecutableFromStringContext(ctx, command)
before := time.Now()
output, err := sanitisedCommandOutput(cmd.Output())
c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before)))
return output, err
}
// RunExecutableWithOutput runs an executable file and returns its output // RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) { func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
return sanitisedCommandOutput(cmd.CombinedOutput()) return sanitisedCommandOutput(cmd.CombinedOutput())
@ -87,38 +79,8 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
// ExecutableFromString takes a string like `docker ps -a` and returns an executable command for it // ExecutableFromString takes a string like `docker ps -a` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd { func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr) splitCmd := str.ToArgv(commandStr)
return c.NewCmd(splitCmd[0], splitCmd[1:]...) // c.Log.Info(splitCmd)
} return c.command(splitCmd[0], splitCmd[1:]...)
// Same as ExecutableFromString but cancellable via a context
func (c *OSCommand) ExecutableFromStringContext(ctx context.Context, commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
return exec.CommandContext(ctx, splitCmd[0], splitCmd[1:]...)
}
func (c *OSCommand) NewCmd(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.command(cmdName, commandArgs...)
cmd.Env = os.Environ()
return cmd
}
func (c *OSCommand) NewCommandStringWithShell(commandStr string) string {
var quotedCommand string
// Windows does not seem to like quotes around the command
if c.Platform.os == "windows" {
quotedCommand = strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
} else {
quotedCommand = c.Quote(commandStr)
}
return fmt.Sprintf("%s %s %s", c.Platform.shell, c.Platform.shellArg, quotedCommand)
} }
// RunCommand runs a command and just returns the error // RunCommand runs a command and just returns the error
@ -139,6 +101,16 @@ func (c *OSCommand) FileType(path string) string {
return "file" return "file"
} }
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
return sanitisedCommandOutput(
c.command(c.Platform.shell, c.Platform.shellArg, command).
CombinedOutput(),
)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) { func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output) outputString := string(output)
if err != nil { if err != nil {
@ -193,28 +165,22 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
return nil, errors.New("No editor defined in $VISUAL or $EDITOR") return nil, errors.New("No editor defined in $VISUAL or $EDITOR")
} }
return c.NewCmd(editor, filename), nil return c.PrepareSubProcess(editor, filename), nil
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
return c.command(cmdName, commandArgs...)
} }
// Quote wraps a message in platform-specific quotation marks // Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string { func (c *OSCommand) Quote(message string) string {
var quote string message = strings.Replace(message, "`", "\\`", -1)
if c.Platform.os == "windows" { escapedQuote := c.Platform.escapedQuote
quote = `\"` if strings.Contains(message, c.Platform.escapedQuote) {
message = strings.NewReplacer( escapedQuote = c.Platform.fallbackEscapedQuote
`"`, `"'"'"`,
`\"`, `\\"`,
).Replace(message)
} else {
quote = `"`
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
} }
return quote + message + quote return escapedQuote + message + escapedQuote
} }
// Unquote removes wrapping quotations marks if they are present // Unquote removes wrapping quotations marks if they are present
@ -240,7 +206,7 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
// CreateTempFile writes a string to a new temp file and returns the file's name // CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := os.CreateTemp("", filename) tmpfile, err := ioutil.TempFile("", filename)
if err != nil { if err != nil {
c.Log.Error(err) c.Log.Error(err)
return "", WrapError(err) return "", WrapError(err)
@ -302,7 +268,7 @@ func (c *OSCommand) GetLazydockerPath() string {
// RunCustomCommand returns the pointer to a custom command // RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd { func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.NewCmd(c.Platform.shell, c.Platform.shellArg, command) return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
} }
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C // PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
@ -342,7 +308,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
c.Log.Error(err) c.Log.Error(err)
} }
if b, err := io.ReadAll(stderr); err == nil { if b, err := ioutil.ReadAll(stderr); err == nil {
if len(b) > 0 { if len(b) > 0 {
finalErrors = append(finalErrors, string(b)) finalErrors = append(finalErrors, string(b))
} }

View file

@ -12,7 +12,9 @@ func getPlatform() *Platform {
os: runtime.GOOS, os: runtime.GOOS,
shell: "bash", shell: "bash",
shellArg: "-c", shellArg: "-c",
escapedQuote: "'",
openCommand: "open {{filename}}", openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}", openLinkCommand: "open {{link}}",
fallbackEscapedQuote: "\"",
} }
} }

View file

@ -1,7 +1,7 @@
package commands package commands
import ( import (
"fmt" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"testing" "testing"
@ -89,7 +89,7 @@ func TestOSCommandEditFile(t *testing.T) {
assert.EqualValues(t, "nano", name) assert.EqualValues(t, "nano", name)
return exec.Command("exit", "0") return nil
}, },
func(env string) string { func(env string) string {
if env == "VISUAL" { if env == "VISUAL" {
@ -111,7 +111,7 @@ func TestOSCommandEditFile(t *testing.T) {
assert.EqualValues(t, "emacs", name) assert.EqualValues(t, "emacs", name)
return exec.Command("exit", "0") return nil
}, },
func(env string) string { func(env string) string {
if env == "EDITOR" { if env == "EDITOR" {
@ -133,7 +133,7 @@ func TestOSCommandEditFile(t *testing.T) {
assert.EqualValues(t, "vi", name) assert.EqualValues(t, "vi", name)
return exec.Command("exit", "0") return nil
}, },
func(env string) string { func(env string) string {
return "" return ""
@ -153,14 +153,13 @@ func TestOSCommandEditFile(t *testing.T) {
} }
} }
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) { func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand() osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello `test`") actual := osCommand.Quote("hello `test`")
expected := "\"hello \\`test\\`\"" expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual) assert.EqualValues(t, expected, actual)
} }
@ -170,10 +169,12 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := NewDummyOSCommand() osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux" osCommand.Platform.os = "linux"
osCommand.Platform.fallbackEscapedQuote = "\""
osCommand.Platform.escapedQuote = "'"
actual := osCommand.Quote("hello 'test'") actual := osCommand.Quote("hello 'test'")
expected := `"hello 'test'"` expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
assert.EqualValues(t, expected, actual) assert.EqualValues(t, expected, actual)
} }
@ -186,20 +187,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
actual := osCommand.Quote(`hello "test"`) actual := osCommand.Quote(`hello "test"`)
expected := `"hello \"test\""` expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteWindows tests the quote function for Windows
func TestOSCommandQuoteWindows(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "windows"
actual := osCommand.Quote(`hello "test" 'test2'`)
expected := `\"hello "'"'"test"'"'" 'test2'\"`
assert.EqualValues(t, expected, actual) assert.EqualValues(t, expected, actual)
} }
@ -289,7 +277,7 @@ func TestOSCommandCreateTempFile(t *testing.T) {
func(path string, err error) { func(path string, err error) {
assert.NoError(t, err) assert.NoError(t, err)
content, err := os.ReadFile(path) content, err := ioutil.ReadFile(path)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "content", string(content)) assert.Equal(t, "content", string(content))
@ -303,53 +291,3 @@ func TestOSCommandCreateTempFile(t *testing.T) {
}) })
} }
} }
func TestOSCommandExecutableFromStringWithShellLinux(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
tests := []struct {
name string
commandStr string
want string
}{
{
"success",
"pwd",
fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "\"pwd\""),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := osCommand.NewCommandStringWithShell(tt.commandStr)
assert.Equal(t, tt.want, got)
})
}
}
func TestOSCommandNewCommandStringWithShellWindows(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "windows"
tests := []struct {
name string
commandStr string
want string
}{
{
"success",
"pwd",
fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "pwd"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := osCommand.NewCommandStringWithShell(tt.commandStr)
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -5,5 +5,7 @@ func getPlatform() *Platform {
os: "windows", os: "windows",
shell: "cmd", shell: "cmd",
shellArg: "/c", shellArg: "/c",
escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
} }
} }

View file

@ -1,5 +0,0 @@
package commands
type Project struct {
Name string
}

View file

@ -1,10 +1,12 @@
package commands package commands
import ( import (
"context"
"os/exec" "os/exec"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -13,41 +15,52 @@ 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
DockerCommand LimitedDockerCommand DockerCommand LimitedDockerCommand
} }
// GetDisplayStrings returns the dispaly string of Container
func (s *Service) GetDisplayStrings(isFocused bool) []string {
if s.Container == nil {
return []string{utils.ColoredString("none", color.FgBlue), "", s.Name, "", ""}
}
cont := s.Container
return []string{cont.GetDisplayStatus(), cont.GetDisplaySubstatus(), s.Name, cont.GetDisplayCPUPerc(), utils.ColoredString(cont.displayPorts(), color.FgYellow)}
}
// Remove removes the service's containers // Remove removes the service's containers
func (s *Service) Remove(options container.RemoveOptions) error { func (s *Service) Remove(options types.ContainerRemoveOptions) error {
return s.Container.Remove(options) return s.Container.Remove(options)
} }
// Stop stops the service's containers // Stop stops the service's containers
func (s *Service) Stop() error { func (s *Service) Stop() error {
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService) templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StopService
} command := utils.ApplyTemplate(
templateString,
// Up up's the service s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
func (s *Service) Up() error { )
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.UpService) return s.OSCommand.RunCommand(command)
} }
// Restart restarts the service // Restart restarts the service
func (s *Service) Restart() error { func (s *Service) Restart() error {
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.RestartService) templateString := s.OSCommand.Config.UserConfig.CommandTemplates.RestartService
}
// Start starts the service
func (s *Service) Start() error {
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StartService)
}
func (s *Service) runCommand(templateCmdStr string) error {
command := utils.ApplyTemplate( command := utils.ApplyTemplate(
templateCmdStr, templateString,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
return s.OSCommand.RunCommand(command)
}
// Restart starts the service
func (s *Service) Start() error {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StartService
command := utils.ApplyTemplate(
templateString,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}), s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
) )
return s.OSCommand.RunCommand(command) return s.OSCommand.RunCommand(command)
@ -58,6 +71,11 @@ func (s *Service) Attach() (*exec.Cmd, error) {
return s.Container.Attach() return s.Container.Attach()
} }
// Top returns process information
func (s *Service) Top() (container.ContainerTopOKBody, error) {
return s.Container.Top()
}
// ViewLogs attaches to a subprocess viewing the service's logs // ViewLogs attaches to a subprocess viewing the service's logs
func (s *Service) ViewLogs() (*exec.Cmd, error) { func (s *Service) ViewLogs() (*exec.Cmd, error) {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs
@ -73,12 +91,12 @@ func (s *Service) ViewLogs() (*exec.Cmd, error) {
} }
// RenderTop renders the process list of the service // RenderTop renders the process list of the service
func (s *Service) RenderTop(ctx context.Context) (string, error) { func (s *Service) RenderTop() (string, error) {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop
command := utils.ApplyTemplate( command := utils.ApplyTemplate(
templateString, templateString,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}), s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
) )
return s.OSCommand.RunCommandWithOutputContext(ctx, command) return s.OSCommand.RunCommandWithOutput(command)
} }

View file

@ -0,0 +1,173 @@
package commands
import (
"testing"
"github.com/docker/docker/api/types"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/stretchr/testify/assert"
)
func sampleContainers(userConfig *config.AppConfig) []*Container {
return []*Container{
{
ID: "1",
Name: "1",
Container: types.Container{
State: "exited",
},
Config: userConfig,
},
{
ID: "2",
Name: "2",
Container: types.Container{
State: "running",
},
Config: userConfig,
},
{
ID: "3",
Name: "3",
Container: types.Container{
State: "running",
},
Config: userConfig,
},
{
ID: "4",
Name: "4",
Container: types.Container{
State: "created",
},
Config: userConfig,
},
}
}
func expectedPerStatusContainers(appConfig *config.AppConfig) []*Container {
return []*Container{
{
ID: "2",
Name: "2",
Container: types.Container{
State: "running",
},
Config: appConfig,
},
{
ID: "3",
Name: "3",
Container: types.Container{
State: "running",
},
Config: appConfig,
},
{
ID: "1",
Name: "1",
Container: types.Container{
State: "exited",
},
Config: appConfig,
},
{
ID: "4",
Name: "4",
Container: types.Container{
State: "created",
},
Config: appConfig,
},
}
}
func expectedLegacySortedContainers(appConfig *config.AppConfig) []*Container {
return []*Container{
{
ID: "1",
Name: "1",
Container: types.Container{
State: "exited",
},
Config: appConfig,
},
{
ID: "2",
Name: "2",
Container: types.Container{
State: "running",
},
Config: appConfig,
},
{
ID: "3",
Name: "3",
Container: types.Container{
State: "running",
},
Config: appConfig,
},
{
ID: "4",
Name: "4",
Container: types.Container{
State: "created",
},
Config: appConfig,
},
}
}
func assertEqualContainers(t *testing.T, left *Container, right *Container) {
t.Helper()
assert.Equal(t, left.Container.State, right.Container.State)
assert.Equal(t, left.Container.ID, right.Container.ID)
assert.Equal(t, left.Name, right.Name)
}
func TestSortContainers(t *testing.T) {
appConfig := NewDummyAppConfig()
appConfig.UserConfig = &config.UserConfig{
Gui: config.GuiConfig{
LegacySortContainers: false,
},
}
command := &DockerCommand{
Config: appConfig,
}
containers := sampleContainers(appConfig)
sorted := expectedPerStatusContainers(appConfig)
ct := command.sortedContainers(containers)
assert.Equal(t, len(ct), len(sorted))
for i := 0; i < len(ct); i++ {
assertEqualContainers(t, sorted[i], ct[i])
}
}
func TestLegacySortedContainers(t *testing.T) {
appConfig := NewDummyAppConfig()
appConfig.UserConfig = &config.UserConfig{
Gui: config.GuiConfig{
LegacySortContainers: true,
},
}
command := &DockerCommand{
Config: appConfig,
}
containers := sampleContainers(appConfig)
sorted := expectedLegacySortedContainers(appConfig)
ct := command.sortedContainers(containers)
for i := 0; i < len(ct); i++ {
assertEqualContainers(t, sorted[i], ct[i])
}
}

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/url" "net/url"
"os" "os"
@ -36,7 +37,7 @@ func NewSSHHandler(oSCommand CmdKiller) *SSHHandler {
return (&net.Dialer{}).DialContext(ctx, network, addr) return (&net.Dialer{}).DialContext(ctx, network, addr)
}, },
startCmd: func(cmd *exec.Cmd) error { return cmd.Start() }, startCmd: func(cmd *exec.Cmd) error { return cmd.Start() },
tempDir: os.MkdirTemp, tempDir: ioutil.TempDir,
getenv: os.Getenv, getenv: os.Getenv,
setenv: os.Setenv, setenv: os.Setenv,
} }

View file

@ -2,9 +2,10 @@ package commands
import ( import (
"context" "context"
"sort"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -12,28 +13,46 @@ import (
// Volume : A docker Volume // Volume : A docker Volume
type Volume struct { type Volume struct {
Name string Name string
Volume *volume.Volume Volume *types.Volume
Client *client.Client Client *client.Client
OSCommand *OSCommand OSCommand *OSCommand
Log *logrus.Entry Log *logrus.Entry
DockerCommand LimitedDockerCommand DockerCommand LimitedDockerCommand
} }
// GetDisplayStrings returns the dispaly string of Container
func (v *Volume) GetDisplayStrings(isFocused bool) []string {
return []string{v.Volume.Driver, v.Name}
}
// RefreshVolumes gets the volumes and stores them // RefreshVolumes gets the volumes and stores them
func (c *DockerCommand) RefreshVolumes() ([]*Volume, error) { func (c *DockerCommand) RefreshVolumes() error {
result, err := c.Client.VolumeList(context.Background(), volume.ListOptions{}) result, err := c.Client.VolumeList(context.Background(), filters.Args{})
if err != nil { if err != nil {
return nil, err return err
} }
volumes := result.Volumes volumes := result.Volumes
ownVolumes := make([]*Volume, len(volumes)) ownVolumes := make([]*Volume, len(volumes))
for i, vol := range volumes { // we're sorting these volumes based on whether they have labels defined,
// because those are the ones you typically care about.
// Within that, we also sort them alphabetically
sort.Slice(volumes, func(i, j int) bool {
if len(volumes[i].Labels) == 0 && len(volumes[j].Labels) > 0 {
return false
}
if len(volumes[i].Labels) > 0 && len(volumes[j].Labels) == 0 {
return true
}
return volumes[i].Name < volumes[j].Name
})
for i, volume := range volumes {
ownVolumes[i] = &Volume{ ownVolumes[i] = &Volume{
Name: vol.Name, Name: volume.Name,
Volume: vol, Volume: volume,
Client: c.Client, Client: c.Client,
OSCommand: c.OSCommand, OSCommand: c.OSCommand,
Log: c.Log, Log: c.Log,
@ -41,7 +60,9 @@ func (c *DockerCommand) RefreshVolumes() ([]*Volume, error) {
} }
} }
return ownVolumes, nil c.Volumes = ownVolumes
return nil
} }
// PruneVolumes prunes volumes // PruneVolumes prunes volumes

View file

@ -13,6 +13,7 @@
package config package config
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -59,18 +60,12 @@ type UserConfig struct {
// Replacements determines how we render an item's info // Replacements determines how we render an item's info
Replacements Replacements `yaml:"replacements,omitempty"` Replacements Replacements `yaml:"replacements,omitempty"`
// For demo purposes: any list item with one of these strings as a substring
// will be filtered out and not displayed.
// Not documented because it's subject to change
Ignore []string `yaml:"ignore,omitempty"`
} }
// ThemeConfig is for setting the colors of panels and some text. // ThemeConfig is for setting the colors of panels and some text.
type ThemeConfig struct { type ThemeConfig struct {
ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"` ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"`
InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"` InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor,omitempty"`
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"` OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
} }
@ -130,18 +125,6 @@ type GuiConfig struct {
// When true, increases vertical space used by focused side panel, // When true, increases vertical space used by focused side panel,
// creating an accordion effect // creating an accordion effect
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
// ScreenMode allow user to specify which screen mode will be used on startup
ScreenMode string `yaml:"screenMode,omitempty"`
// Determines the style of the container status and container health display in the
// containers panel. "long": full words (default), "short": one or two characters,
// "icon": unicode emoji.
ContainerStatusHealthStyle string `yaml:"containerStatusHealthStyle"`
// Window border style.
// One of 'rounded' (default) | 'single' | 'double' | 'hidden'
Border string `yaml:"border"`
} }
// CommandTemplatesConfig determines what commands actually get called when we // CommandTemplatesConfig determines what commands actually get called when we
@ -155,20 +138,9 @@ type CommandTemplatesConfig struct {
// StartService is just like the above but for starting // StartService is just like the above but for starting
StartService string `yaml:"startService,omitempty"` StartService string `yaml:"startService,omitempty"`
// UpService ups the service (creates and starts)
UpService string `yaml:"upService,omitempty"`
// Runs "docker-compose up -d"
Up string `yaml:"up,omitempty"`
// downs everything
Down string `yaml:"down,omitempty"`
// downs and removes volumes
DownWithVolumes string `yaml:"downWithVolumes,omitempty"`
// DockerCompose is for your docker-compose command. You may want to combine a // DockerCompose is for your docker-compose command. You may want to combine a
// few different docker-compose.yml files together, in which case you can set // few different docker-compose.yml files together, in which case you can set
// this to "docker compose -f foo/docker-compose.yml -f // this to "docker-compose -f foo/docker-compose.yml -f
// bah/docker-compose.yml". The reason that the other docker-compose command // bah/docker-compose.yml". The reason that the other docker-compose command
// templates all start with {{ .DockerCompose }} is so that they can make use // templates all start with {{ .DockerCompose }} is so that they can make use
// of whatever you've set in this value rather than you having to copy and // of whatever you've set in this value rather than you having to copy and
@ -200,7 +172,7 @@ type CommandTemplatesConfig struct {
// and ensure they're running before trying to run the service at hand // and ensure they're running before trying to run the service at hand
RecreateService string `yaml:"recreateService,omitempty"` RecreateService string `yaml:"recreateService,omitempty"`
// AllLogs is for showing what you get from doing `docker compose logs`. It // AllLogs is for showing what you get from doing `docker-compose logs`. It
// combines all the logs together // combines all the logs together
AllLogs string `yaml:"allLogs,omitempty"` AllLogs string `yaml:"allLogs,omitempty"`
@ -298,9 +270,6 @@ type CustomCommands struct {
// Volumes contains the custom commands for volumes // Volumes contains the custom commands for volumes
Volumes []CustomCommand `yaml:"volumes,omitempty"` Volumes []CustomCommand `yaml:"volumes,omitempty"`
// Networks contains the custom commands for networks
Networks []CustomCommand `yaml:"networks,omitempty"`
} }
// Replacements contains the stuff relating to rendering a container's info // Replacements contains the stuff relating to rendering a container's info
@ -321,10 +290,6 @@ type CustomCommand struct {
// option where the output plays in the main panel. // option where the output plays in the main panel.
Attach bool `yaml:"attach"` Attach bool `yaml:"attach"`
// Shell indicates whether to invoke the Command on a shell or not.
// Example of a bash invoked command: `/bin/bash -c "{Command}".
Shell bool `yaml:"shell"`
// Command is the command we want to run. We can use the go templates here as // Command is the command we want to run. We can use the go templates here as
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }} // well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
// /bin/sh` // /bin/sh`
@ -343,7 +308,6 @@ type CustomCommand struct {
type LogsConfig struct { type LogsConfig struct {
Timestamps bool `yaml:"timestamps,omitempty"` Timestamps bool `yaml:"timestamps,omitempty"`
Since string `yaml:"since,omitempty"` Since string `yaml:"since,omitempty"`
Tail string `yaml:"tail,omitempty"`
} }
// GetDefaultConfig returns the application default configuration NOTE (to // GetDefaultConfig returns the application default configuration NOTE (to
@ -365,7 +329,6 @@ func GetDefaultConfig() UserConfig {
Theme: ThemeConfig{ Theme: ThemeConfig{
ActiveBorderColor: []string{"green", "bold"}, ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"default"}, InactiveBorderColor: []string{"default"},
SelectedLineBgColor: []string{"blue"},
OptionsTextColor: []string{"blue"}, OptionsTextColor: []string{"blue"},
}, },
ShowAllContainers: false, ShowAllContainers: false,
@ -375,23 +338,16 @@ func GetDefaultConfig() UserConfig {
SidePanelWidth: 0.3333, SidePanelWidth: 0.3333,
ShowBottomLine: true, ShowBottomLine: true,
ExpandFocusedSidePanel: false, ExpandFocusedSidePanel: false,
ScreenMode: "normal",
ContainerStatusHealthStyle: "long",
}, },
ConfirmOnQuit: false, ConfirmOnQuit: false,
Logs: LogsConfig{ Logs: LogsConfig{
Timestamps: false, Timestamps: false,
Since: "60m", Since: "60m",
Tail: "",
}, },
CommandTemplates: CommandTemplatesConfig{ CommandTemplates: CommandTemplatesConfig{
DockerCompose: "docker compose", DockerCompose: "docker-compose",
RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}", RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}",
StartService: "{{ .DockerCompose }} start {{ .Service.Name }}", StartService: "{{ .DockerCompose }} start {{ .Service.Name }}",
Up: "{{ .DockerCompose }} up -d",
Down: "{{ .DockerCompose }} down",
DownWithVolumes: "{{ .DockerCompose }} down --volumes",
UpService: "{{ .DockerCompose }} up -d {{ .Service.Name }}",
RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}", RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}", RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}", StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}",
@ -488,11 +444,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
@ -503,7 +458,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
return nil, err return nil, err
} }
// Pass compose files as individual -f flags to docker compose // Pass compose files as individual -f flags to docker-compose
if len(composeFiles) > 0 { if len(composeFiles) > 0 {
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ") userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
} }
@ -518,7 +473,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
@ -574,7 +528,7 @@ func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
} }
} }
content, err := os.ReadFile(fileName) content, err := ioutil.ReadFile(fileName)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -9,13 +9,13 @@ 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)
} }
actual := conf.UserConfig.CommandTemplates.DockerCompose actual := conf.UserConfig.CommandTemplates.DockerCompose
expected := "docker compose" expected := "docker-compose"
if actual != expected { if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual) t.Fatalf("Expected %s but got %s", expected, actual)
} }
@ -23,13 +23,13 @@ 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)
} }
actual := conf.UserConfig.CommandTemplates.DockerCompose actual := conf.UserConfig.CommandTemplates.DockerCompose
expected := "docker compose -f one.yml" expected := "docker-compose -f one.yml"
if actual != expected { if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual) t.Fatalf("Expected %s but got %s", expected, actual)
} }
@ -37,13 +37,13 @@ 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)
} }
actual := conf.UserConfig.CommandTemplates.DockerCompose actual := conf.UserConfig.CommandTemplates.DockerCompose
expected := "docker compose -f one.yml -f two.yml -f three.yml" expected := "docker-compose -f one.yml -f two.yml -f three.yml"
if actual != expected { if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual) t.Fatalf("Expected %s but got %s", expected, actual)
} }
@ -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)
} }

View file

@ -2,7 +2,6 @@ package gui
import ( import (
"github.com/jesseduffield/lazycore/pkg/boxlayout" "github.com/jesseduffield/lazycore/pkg/boxlayout"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/samber/lo" "github.com/samber/lo"
@ -29,7 +28,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
sidePanelsDirection = boxlayout.ROW sidePanelsDirection = boxlayout.ROW
} }
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine
infoSectionSize := 0 infoSectionSize := 0
if showInfoSection { if showInfoSection {
infoSectionSize = 1 infoSectionSize = 1
@ -99,19 +98,6 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
) )
} }
if gui.State.Filter.active {
return append(result, []*boxlayout.Box{
{
Window: "filterPrefix",
Size: runewidth.StringWidth(gui.filterPrompt()),
},
{
Window: "filter",
Weight: 1,
},
}...)
}
result = append(result, result = append(result,
[]*boxlayout.Box{ []*boxlayout.Box{
{ {
@ -130,13 +116,11 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
} }
func (gui *Gui) sideViewNames() []string { func (gui *Gui) sideViewNames() []string {
visibleSidePanels := lo.Filter(gui.allSidePanels(), func(panel panels.ISideListPanel, _ int) bool { if gui.DockerCommand.InDockerComposeProject {
return !panel.IsHidden() return []string{"project", "services", "containers", "images", "volumes"}
}) } else {
return []string{"project", "containers", "images", "volumes"}
return lo.Map(visibleSidePanels, func(panel panels.ISideListPanel, _ int) string { }
return panel.GetView().Name()
})
} }
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
@ -175,31 +159,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] {
projectBox = &boxlayout.Box{
Window: sideWindowNames[0],
Weight: 2,
}
}
return append([]*boxlayout.Box{
projectBox,
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box { }, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
return accordionBox(&boxlayout.Box{Window: window, Weight: 1}) 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})
})
} else { } else {
squashedHeight := 1 squashedHeight := 1
if height >= 21 { if height >= 21 {

View file

@ -52,8 +52,8 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
return lineCount return lineCount
} }
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) { func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
width, height := gui.g.Size() width, height := g.Size()
panelWidth := width / 2 panelWidth := width / 2
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return width/2 - panelWidth/2, return width/2 - panelWidth/2,
@ -64,7 +64,7 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel() gui.onNewPopupPanel()
err := gui.prepareConfirmationPanel(title, "") err := gui.prepareConfirmationPanel(title, "", false)
if err != nil { if err != nil {
return err return err
} }
@ -72,13 +72,17 @@ func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *
return gui.setKeyBindings(gui.g, handleConfirm, nil) return gui.setKeyBindings(gui.g, handleConfirm, nil)
} }
func (gui *Gui) prepareConfirmationPanel(title, prompt string) error { func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt) x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView := gui.Views.Confirmation confirmationView := gui.Views.Confirmation
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) _, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil { if err != nil {
return err return err
} }
confirmationView.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
}
confirmationView.Title = title confirmationView.Title = title
confirmationView.Visible = true confirmationView.Visible = true
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
@ -96,10 +100,10 @@ func (gui *Gui) onNewPopupPanel() {
// The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil. // The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil.
// nolint:unparam // nolint:unparam
func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(title, prompt, handleConfirm, handleClose) return gui.createPopupPanel(title, prompt, false, handleConfirm, handleClose)
} }
func (gui *Gui) createPopupPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { func (gui *Gui) createPopupPanel(title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel() gui.onNewPopupPanel()
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
if gui.currentViewName() == "confirmation" { if gui.currentViewName() == "confirmation" {
@ -107,7 +111,7 @@ func (gui *Gui) createPopupPanel(title, prompt string, handleConfirm, handleClos
gui.Log.Error(err.Error()) gui.Log.Error(err.Error())
} }
} }
err := gui.prepareConfirmationPanel(title, prompt) err := gui.prepareConfirmationPanel(title, prompt, hasLoader)
if err != nil { if err != nil {
return err return err
} }
@ -122,17 +126,17 @@ func (gui *Gui) createPopupPanel(title, prompt string, handleConfirm, handleClos
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
// would use a loop here but because the function takes an interface{} and slices of interfaces require even more boilerplate // would use a loop here but because the function takes an interface{} and slices of interfaces require even more boilerplate
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err return err
} }
if err := g.SetKeybinding("confirmation", 'y', gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { if err := g.SetKeybinding("confirmation", nil, 'y', gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err return err
} }
if err := g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose)); err != nil { if err := g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose)); err != nil {
return err return err
} }
if err := g.SetKeybinding("confirmation", 'n', gocui.ModNone, gui.wrappedConfirmationFunction(handleClose)); err != nil { if err := g.SetKeybinding("confirmation", nil, 'n', gocui.ModNone, gui.wrappedConfirmationFunction(handleClose)); err != nil {
return err return err
} }

View file

@ -8,34 +8,36 @@ import (
"os/signal" "os/signal"
"time" "time"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
) )
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc { func (gui *Gui) renderContainerLogsToMain(container *commands.Container) error {
return gui.NewTickerTask(TickerTaskOpts{ mainView := gui.getMainView()
Func: func(ctx context.Context, notifyStopped chan struct{}) { mainView.Autoscroll = true
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped) mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
},
Duration: time.Millisecond * 200, return gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) {
// TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed) gui.renderContainerLogsToMainAux(container, stop, notifyStopped)
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: true,
}) })
} }
func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx context.Context, notifyStopped chan struct{}) { func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, stop, notifyStopped chan struct{}) {
gui.clearMainView() gui.clearMainView()
defer func() { defer func() {
notifyStopped <- struct{}{} notifyStopped <- struct{}{}
}() }()
mainView := gui.Views.Main ctx, ctxCancel := context.WithCancel(context.Background())
go func() {
<-stop
ctxCancel()
}()
mainView := gui.getMainView()
if err := gui.writeContainerLogs(container, ctx, mainView); err != nil { if err := gui.writeContainerLogs(container, ctx, mainView); err != nil {
gui.Log.Error(err) gui.Log.Error(err)
@ -47,7 +49,7 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-stop:
return return
case <-ticker.C: case <-ticker.C:
result, err := container.Inspect() result, err := container.Inspect()
@ -104,39 +106,19 @@ func (gui *Gui) promptToReturn() {
} }
} }
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error { func (gui *Gui) writeContainerLogs(container *commands.Container, ctx context.Context, writer io.Writer) error {
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{ readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, container.ID, types.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps, Timestamps: gui.Config.UserConfig.Logs.Timestamps,
Since: gui.Config.UserConfig.Logs.Since, Since: gui.Config.UserConfig.Logs.Since,
Tail: gui.Config.UserConfig.Logs.Tail,
Follow: true, Follow: true,
}) })
if err != nil { if err != nil {
gui.Log.Error(err)
return err return err
} }
defer readCloser.Close()
if !ctr.DetailsLoaded() { if container.DetailsLoaded() && container.Details.Config.Tty {
// loop until the details load or context is cancelled, using timer
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
outer:
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if ctr.DetailsLoaded() {
break outer
}
}
}
}
if ctr.Details.Config.Tty {
_, err = io.Copy(writer, readCloser) _, err = io.Copy(writer, readCloser)
if err != nil { if err != nil {
return err return err

View file

@ -1,191 +1,146 @@
package gui package gui
import ( import (
"context" "encoding/json"
"fmt" "fmt"
"strings" "strings"
"time" "time"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
) )
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] { // list panel functions
// Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context.
isStandaloneContainer := func(container *commands.Container) bool { func (gui *Gui) getContainerContexts() []string {
if container.OneOff || container.ServiceName == "" { return []string{"logs", "stats", "env", "config", "top"}
return true }
func (gui *Gui) getContainerContextTitles() []string {
return []string{gui.Tr.LogsTitle, gui.Tr.StatsTitle, gui.Tr.EnvTitle, gui.Tr.ConfigTitle, gui.Tr.TopTitle}
}
func (gui *Gui) getSelectedContainer() (*commands.Container, error) {
selectedLine := gui.State.Panels.Containers.SelectedLine
if selectedLine == -1 {
return &commands.Container{}, gui.Errors.ErrNoContainers
} }
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool { return gui.DockerCommand.DisplayContainers[selectedLine], nil
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName }
func (gui *Gui) handleContainersClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.DockerCommand.DisplayContainers)
handleSelect := gui.handleContainerSelect
selectedLine := &gui.State.Panels.Containers.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
if err != nil {
if err != gui.Errors.ErrNoContainers {
return err
}
return nil
}
gui.focusY(gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v)
key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
mainView := gui.getMainView()
mainView.Tabs = gui.getContainerContextTitles()
mainView.TabIndex = gui.State.Panels.Containers.ContextIndex
gui.clearMainView()
switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] {
case "logs":
if err := gui.renderContainerLogsToMain(container); err != nil {
return err
}
case "config":
if err := gui.renderContainerConfig(container); err != nil {
return err
}
case "env":
if err := gui.renderContainerEnv(container); err != nil {
return err
}
case "stats":
if err := gui.renderContainerStats(container); err != nil {
return err
}
case "top":
if err := gui.renderContainerTop(container); err != nil {
return err
}
default:
return errors.New("Unknown context for containers panel")
}
return nil
}
func (gui *Gui) renderContainerEnv(container *commands.Container) error {
if !container.DetailsLoaded() {
return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", gui.Tr.WaitingForContainerInfo)
}) })
} }
return &panels.SideListPanel[*commands.Container]{ mainView := gui.getMainView()
ContextState: &panels.ContextState[*commands.Container]{ mainView.Autoscroll = false
GetMainTabs: func() []panels.MainTab[*commands.Container] { mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return []panels.MainTab[*commands.Container]{ envVariablesList := [][]string{}
{ renderedTable := gui.Tr.NothingToDisplay
Key: "logs", if len(container.Details.Config.Env) > 0 {
Title: gui.Tr.LogsTitle, var err error
Render: gui.renderContainerLogsToMain, for _, env := range container.Details.Config.Env {
}, splitEnv := strings.SplitN(env, "=", 2)
{
Key: "stats",
Title: gui.Tr.StatsTitle,
Render: gui.renderContainerStats,
},
{
Key: "env",
Title: gui.Tr.EnvTitle,
Render: gui.renderContainerEnv,
},
{
Key: "config",
Title: gui.Tr.ConfigTitle,
Render: gui.renderContainerConfig,
},
{
Key: "top",
Title: gui.Tr.TopTitle,
Render: gui.renderContainerTop,
},
}
},
GetItemContextCacheKey: func(container *commands.Container) string {
// Including the container state in the cache key so that if the container
// restarts we re-read the logs. In the past we've had some glitchiness
// where a container restarts but the new logs don't get read.
// Note that this might be jarring if we have a lot of logs and the container
// restarts a lot, so let's keep an eye on it.
return "containers-" + container.ID + "-" + container.Container.State
},
},
ListPanel: panels.ListPanel[*commands.Container]{
List: panels.NewFilteredList[*commands.Container](),
View: gui.Views.Containers,
},
NoItemsMessage: gui.Tr.NoContainers,
Gui: gui.intoInterface(),
// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created)
// and sorted by name if c.SortContainersByState is false
Sort: func(a *commands.Container, b *commands.Container) bool {
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
},
Filter: func(container *commands.Container) bool {
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
return false
}
// When project-scoped, apply project and standalone filtering.
// 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
},
GetTableCells: func(container *commands.Container) []string {
return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container)
},
}
}
var containerStates = map[string]int{
"running": 1,
"exited": 2,
"created": 3,
}
func sortContainers(a *commands.Container, b *commands.Container, legacySort bool) bool {
if legacySort {
return a.Name < b.Name
}
stateLeft := containerStates[a.Container.State]
stateRight := containerStates[b.Container.State]
if stateLeft == stateRight {
return a.Name < b.Name
}
return containerStates[a.Container.State] < containerStates[b.Container.State]
}
func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.containerEnv(container) })
}
func (gui *Gui) containerEnv(container *commands.Container) string {
if !container.DetailsLoaded() {
return gui.Tr.WaitingForContainerInfo
}
if len(container.Details.Config.Env) == 0 {
return gui.Tr.NothingToDisplay
}
envVarsList := lo.Map(container.Details.Config.Env, func(envVar string, _ int) []string {
splitEnv := strings.SplitN(envVar, "=", 2)
key := splitEnv[0] key := splitEnv[0]
value := "" value := ""
if len(splitEnv) > 1 { if len(splitEnv) > 1 {
value = splitEnv[1] value = splitEnv[1]
} }
return []string{ envVariablesList = append(envVariablesList,
[]string{
utils.ColoredString(key+":", color.FgGreen), utils.ColoredString(key+":", color.FgGreen),
utils.ColoredString(value, color.FgYellow), utils.ColoredString(value, color.FgYellow),
}
}) })
}
output, err := utils.RenderTable(envVarsList) renderedTable, err = utils.RenderTable(envVariablesList)
if err != nil { if err != nil {
gui.Log.Error(err) gui.Log.Error(err)
return gui.Tr.CannotDisplayEnvVariables renderedTable = gui.Tr.CannotDisplayEnvVariables
} }
}
return output return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", renderedTable)
})
} }
func (gui *Gui) renderContainerConfig(container *commands.Container) tasks.TaskFunc { func (gui *Gui) renderContainerConfig(container *commands.Container) error {
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfigStr(container) })
}
func (gui *Gui) containerConfigStr(container *commands.Container) string {
if !container.DetailsLoaded() { if !container.DetailsLoaded() {
return gui.Tr.WaitingForContainerInfo return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", gui.Tr.WaitingForContainerInfo)
})
} }
mainView := gui.getMainView()
mainView.Autoscroll = false
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
padding := 10 padding := 10
output := "" output := ""
output += utils.WithPadding("ID: ", padding) + container.ID + "\n" output += utils.WithPadding("ID: ", padding) + container.ID + "\n"
@ -221,118 +176,242 @@ func (gui *Gui) containerConfigStr(container *commands.Container) string {
output += "none\n" output += "none\n"
} }
data, err := utils.MarshalIntoYaml(&container.Details) data, err := json.MarshalIndent(&container.Details, "", " ")
if err != nil { if err != nil {
return fmt.Sprintf("Error marshalling container details: %v", err) return err
} }
output += fmt.Sprintf("\nFull details:\n\n%s", string(data))
output += fmt.Sprintf("\nFull details:\n\n%s", utils.ColoredYamlString(string(data))) return gui.T.NewTask(func(stop chan struct{}) {
_ = gui.renderString(gui.g, "main", output)
return output })
} }
func (gui *Gui) renderContainerStats(container *commands.Container) tasks.TaskFunc { func (gui *Gui) renderContainerStats(container *commands.Container) error {
return gui.NewTickerTask(TickerTaskOpts{ mainView := gui.getMainView()
Func: func(ctx context.Context, notifyStopped chan struct{}) { mainView.Autoscroll = false
contents, err := presentation.RenderStats(gui.Config.UserConfig, container, gui.Views.Main.Width()) mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
width, _ := mainView.Size()
contents, err := container.RenderStats(width)
if err != nil { if err != nil {
_ = gui.createErrorPanel(err.Error()) _ = gui.createErrorPanel(err.Error())
} }
gui.reRenderStringMain(contents) gui.reRenderStringMain(contents)
},
Duration: time.Second,
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: false, // wrapping looks bad here so we're overriding the config value
Autoscroll: false,
}) })
} }
func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc { func (gui *Gui) renderContainerTop(container *commands.Container) error {
return gui.NewTickerTask(TickerTaskOpts{ mainView := gui.getMainView()
Func: func(ctx context.Context, notifyStopped chan struct{}) { mainView.Autoscroll = false
contents, err := container.RenderTop(ctx) mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
contents, err := container.RenderTop()
if err != nil { if err != nil {
gui.RenderStringMain(err.Error()) gui.reRenderStringMain(err.Error())
} }
gui.reRenderStringMain(contents) gui.reRenderStringMain(contents)
},
Duration: time.Second,
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: false,
}) })
} }
func (gui *Gui) refreshContainersAndServices() error { func (gui *Gui) refreshContainersAndServices() error {
if gui.Views.Containers == nil { containersView := gui.getContainersView()
if containersView == nil {
// if the containersView hasn't been instantiated yet we just return // if the containersView hasn't been instantiated yet we just return
return nil return nil
} }
// keep track of current service selected so that we can reposition our cursor if it moves position in the list // keep track of current service selected so that we can reposition our cursor if it moves position in the list
originalSelectedLineIdx := gui.Panels.Services.SelectedIdx sl := gui.State.Panels.Services.SelectedLine
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx) var selectedService *commands.Service
if len(gui.DockerCommand.Services) > 0 {
selectedService = gui.DockerCommand.Services[sl]
}
containers, services, err := gui.DockerCommand.RefreshContainersAndServices( if err := gui.DockerCommand.RefreshContainersAndServices(); err != nil {
gui.Panels.Containers.List.GetAllItems(), return err
) }
// see if our selected service has moved
if selectedService != nil {
for i, service := range gui.DockerCommand.Services {
if service.ID == selectedService.ID {
if i == sl {
break
}
gui.State.Panels.Services.SelectedLine = i
gui.focusY(i, len(gui.DockerCommand.Services), gui.getServicesView())
}
}
}
if len(gui.DockerCommand.DisplayContainers) > 0 && gui.State.Panels.Containers.SelectedLine == -1 {
gui.State.Panels.Containers.SelectedLine = 0
}
if len(gui.DockerCommand.DisplayContainers)-1 < gui.State.Panels.Containers.SelectedLine {
gui.State.Panels.Containers.SelectedLine = len(gui.DockerCommand.DisplayContainers) - 1
}
// doing the exact same thing for services
if len(gui.DockerCommand.Services) > 0 && gui.State.Panels.Services.SelectedLine == -1 {
gui.State.Panels.Services.SelectedLine = 0
}
if len(gui.DockerCommand.Services)-1 < gui.State.Panels.Services.SelectedLine {
gui.State.Panels.Services.SelectedLine = len(gui.DockerCommand.Services) - 1
}
gui.renderContainersAndServices()
return nil
}
func (gui *Gui) renderContainersAndServices() {
gui.g.Update(func(g *gocui.Gui) error {
containersView := gui.getContainersView()
containersView.Clear()
isFocused := gui.g.CurrentView().Name() == "containers"
list, err := utils.RenderList(gui.DockerCommand.DisplayContainers, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return err return err
} }
fmt.Fprint(containersView, list)
gui.Panels.Services.SetItems(services) if containersView == g.CurrentView() {
gui.Panels.Containers.SetItems(containers) if err := gui.handleContainerSelect(g, containersView); err != nil {
return err
// see if our selected service has moved
if isServiceSelected {
for i, service := range gui.Panels.Services.List.GetItems() {
if service.ID == selectedService.ID {
if i == originalSelectedLineIdx {
break
}
gui.Panels.Services.SetSelectedLineIdx(i)
gui.Panels.Services.Refocus()
}
} }
} }
return gui.renderContainersAndServices() // doing the exact same thing for services
if !gui.DockerCommand.InDockerComposeProject {
return nil
}
servicesView := gui.getServicesView()
servicesView.Clear()
isFocused = gui.g.CurrentView().Name() == "services"
list, err = utils.RenderList(gui.DockerCommand.Services, utils.IsFocused(isFocused))
if err != nil {
return err
}
fmt.Fprint(servicesView, list)
if servicesView == g.CurrentView() {
return gui.handleServiceSelect(g, servicesView)
}
return nil
})
} }
func (gui *Gui) renderContainersAndServices() error { func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error {
if err := gui.Panels.Services.RerenderList(); err != nil { if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return err return nil
} }
if err := gui.Panels.Containers.RerenderList(); err != nil { panelState := gui.State.Panels.Containers
return err gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.DisplayContainers), false)
return gui.handleContainerSelect(gui.g, v)
}
func (gui *Gui) handleContainersPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
} }
panelState := gui.State.Panels.Containers
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.DisplayContainers), true)
return gui.handleContainerSelect(gui.g, v)
}
func (gui *Gui) handleContainersNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getContainerContexts()
if gui.State.Panels.Containers.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Containers.ContextIndex = 0
} else {
gui.State.Panels.Containers.ContextIndex++
}
_ = gui.handleContainerSelect(gui.g, v)
return nil return nil
} }
func (gui *Gui) handleContainersPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getContainerContexts()
if gui.State.Panels.Containers.ContextIndex <= 0 {
gui.State.Panels.Containers.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Containers.ContextIndex--
}
_ = gui.handleContainerSelect(gui.g, v)
return nil
}
type removeContainerOption struct {
description string
command string
configOptions types.ContainerRemoveOptions
}
// GetDisplayStrings is a function.
func (r *removeContainerOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
gui.State.ShowExitedContainers = !gui.State.ShowExitedContainers gui.DockerCommand.ShowExited = !gui.DockerCommand.ShowExited
return gui.Panels.Containers.RerenderList() if err := gui.refreshContainersAndServices(); err != nil {
return err
}
return nil
} }
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
handleMenuPress := func(configOptions container.RemoveOptions) error { options := []*removeContainerOption{
{
description: gui.Tr.Remove,
command: "docker rm " + container.ID[1:10],
configOptions: types.ContainerRemoveOptions{},
},
{
description: gui.Tr.RemoveWithVolumes,
command: "docker rm --volumes " + container.ID[1:10],
configOptions: types.ContainerRemoveOptions{RemoveVolumes: true},
},
{
description: gui.Tr.Cancel,
},
}
handleMenuPress := func(index int) error {
if options[index].command == "" {
return nil
}
configOptions := options[index].configOptions
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := ctr.Remove(configOptions); err != nil { if err := container.Remove(configOptions); err != nil {
if commands.HasErrorCode(err, commands.MustStopContainer) { if commands.HasErrorCode(err, commands.MustStopContainer) {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
configOptions.Force = true configOptions.Force = true
return ctr.Remove(configOptions) return container.Remove(configOptions)
}) })
}, nil) }, nil)
} }
@ -342,21 +421,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
}) })
} }
menuItems := []*types.MenuItem{ return gui.createMenu("", options, len(options), handleMenuPress)
{
LabelColumns: []string{gui.Tr.Remove, "docker rm " + ctr.ID[1:10]},
OnPress: func() error { return handleMenuPress(container.RemoveOptions{}) },
},
{
LabelColumns: []string{gui.Tr.RemoveWithVolumes, "docker rm --volumes " + ctr.ID[1:10]},
OnPress: func() error { return handleMenuPress(container.RemoveOptions{RemoveVolumes: true}) },
},
}
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
} }
func (gui *Gui) PauseContainer(container *commands.Container) error { func (gui *Gui) PauseContainer(container *commands.Container) error {
@ -376,23 +441,23 @@ func (gui *Gui) PauseContainer(container *commands.Container) error {
} }
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
return gui.PauseContainer(ctr) return gui.PauseContainer(container)
} }
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := ctr.Stop(); err != nil { if err := container.Stop(); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
@ -402,13 +467,13 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := ctr.Restart(); err != nil { if err := container.Restart(); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
@ -417,17 +482,17 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
c, err := ctr.Attach() c, err := container.Attach()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
return gui.runSubprocessWithMessage(c, gui.Tr.DetachFromContainerShortCut) return gui.runSubprocess(c)
} }
func (gui *Gui) handlePruneContainers() error { func (gui *Gui) handlePruneContainers() error {
@ -443,23 +508,23 @@ func (gui *Gui) handlePruneContainers() error {
} }
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
gui.renderLogsToStdout(ctr) gui.renderLogsToStdout(container)
return nil return nil
} }
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
return gui.containerExecShell(ctr) return gui.containerExecShell(container)
} }
func (gui *Gui) containerExecShell(container *commands.Container) error { func (gui *Gui) containerExecShell(container *commands.Container) error {
@ -475,13 +540,13 @@ func (gui *Gui) containerExecShell(container *commands.Container) error {
} }
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Container: ctr, Container: container,
}) })
customCommands := gui.Config.UserConfig.CustomCommands.Containers customCommands := gui.Config.UserConfig.CustomCommands.Containers
@ -492,10 +557,8 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
func (gui *Gui) handleStopContainers() error { func (gui *Gui) handleStopContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
for _, ctr := range gui.Panels.Containers.List.GetAllItems() { for _, container := range gui.DockerCommand.Containers {
if err := ctr.Stop(); err != nil { _ = container.Stop()
gui.Log.Error(err)
}
} }
return nil return nil
@ -506,10 +569,8 @@ func (gui *Gui) handleStopContainers() error {
func (gui *Gui) handleRemoveContainers() error { func (gui *Gui) handleRemoveContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
for _, ctr := range gui.Panels.Containers.List.GetAllItems() { for _, container := range gui.DockerCommand.Containers {
if err := ctr.Remove(container.RemoveOptions{Force: true}); err != nil { _ = container.Remove(types.ContainerRemoveOptions{Force: true})
gui.Log.Error(err)
}
} }
return nil return nil
@ -541,21 +602,21 @@ func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
// Open first port in browser // Open first port in browser
func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() container, err := gui.getSelectedContainer()
if err != nil { if err != nil {
return nil return nil
} }
return gui.openContainerInBrowser(ctr) return gui.openContainerInBrowser(container)
} }
func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error { func (gui *Gui) openContainerInBrowser(container *commands.Container) error {
// skip if no any ports // skip if no any ports
if len(ctr.Container.Ports) == 0 { if len(container.Container.Ports) == 0 {
return nil return nil
} }
// skip if the first port is not published // skip if the first port is not published
port := ctr.Container.Ports[0] port := container.Container.Ports[0]
if port.IP == "" { if port.IP == "" {
return nil return nil
} }

View file

@ -4,50 +4,67 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
) )
type customCommandOption struct {
customCommand config.CustomCommand
description string
command string
name string
runCommand bool
attach bool
}
// GetDisplayStrings is a function.
func (r *customCommandOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.name, utils.ColoredString(r.description, color.FgCyan)}
}
func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error { func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error {
menuItems := lo.Map(customCommands, func(command config.CustomCommand, _ int) *types.MenuItem { options := make([]*customCommandOption, len(customCommands)+1)
for i, command := range customCommands {
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject) resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
onPress := func() error { options[i] = &customCommandOption{
if command.InternalFunction != nil { customCommand: command,
return command.InternalFunction() description: utils.WithShortSha(resolvedCommand),
command: resolvedCommand,
runCommand: true,
attach: command.Attach,
name: command.Name,
}
}
options[len(options)-1] = &customCommandOption{
name: gui.Tr.Cancel,
runCommand: false,
} }
if command.Shell { handleMenuPress := func(index int) error {
resolvedCommand = gui.OSCommand.NewCommandStringWithShell(resolvedCommand) option := options[index]
if !option.runCommand {
return nil
}
if option.customCommand.InternalFunction != nil {
return option.customCommand.InternalFunction()
} }
// if we have a command for attaching, we attach and return the subprocess error // if we have a command for attaching, we attach and return the subprocess error
if command.Attach { if option.customCommand.Attach {
return gui.runSubprocess(gui.OSCommand.ExecutableFromString(resolvedCommand)) cmd := gui.OSCommand.ExecutableFromString(option.command)
return gui.runSubprocess(cmd)
} }
return gui.WithWaitingStatus(waitingStatus, func() error { return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.OSCommand.RunCommand(resolvedCommand); err != nil { if err := gui.OSCommand.RunCommand(option.command); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })
} }
return &types.MenuItem{ return gui.createMenu(title, options, len(options), handleMenuPress)
LabelColumns: []string{
command.Name,
utils.ColoredString(utils.WithShortSha(resolvedCommand), color.FgCyan),
},
OnPress: onPress,
}
})
return gui.Menu(CreateMenuOptions{
Title: title,
Items: menuItems,
})
} }
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error { func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {

View file

@ -1,80 +0,0 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleOpenFilter() error {
panel, ok := gui.currentListPanel()
if !ok {
return nil
}
if panel.IsFilterDisabled() {
return nil
}
gui.State.Filter.active = true
gui.State.Filter.panel = panel
return gui.switchFocus(gui.Views.Filter)
}
func (gui *Gui) onNewFilterNeedle(value string) error {
gui.State.Filter.needle = value
gui.ResetOrigin(gui.State.Filter.panel.GetView())
return gui.State.Filter.panel.RerenderList()
}
func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
matched := f(v, key, ch, mod)
if matched {
if err := gui.onNewFilterNeedle(v.TextArea.GetContent()); err != nil {
gui.Log.Error(err)
}
}
return matched
}
}
func (gui *Gui) escapeFilterPrompt() error {
if err := gui.clearFilter(); err != nil {
return err
}
return gui.returnFocus()
}
func (gui *Gui) clearFilter() error {
gui.State.Filter.needle = ""
gui.State.Filter.active = false
panel := gui.State.Filter.panel
gui.State.Filter.panel = nil
gui.Views.Filter.ClearTextArea()
if panel == nil {
return nil
}
gui.ResetOrigin(panel.GetView())
return panel.RerenderList()
}
// returns to the list view with the filter still applied
func (gui *Gui) commitFilter() error {
if gui.State.Filter.needle == "" {
if err := gui.clearFilter(); err != nil {
return err
}
}
return gui.returnFocus()
}
func (gui *Gui) filterPrompt() string {
return fmt.Sprintf("%s: ", gui.Tr.FilterPrompt)
}

View file

@ -1,146 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/samber/lo"
)
func (gui *Gui) newLineFocused(v *gocui.View) error {
if v == nil {
return nil
}
currentListPanel, ok := gui.currentListPanel()
if ok {
return currentListPanel.HandleSelect()
}
switch v.Name() {
case "confirmation":
return nil
case "main":
v.Highlight = false
return nil
case "filter":
return nil
default:
panic(gui.Tr.NoViewMachingNewLineFocusedSwitchStatement)
}
}
// TODO: move some of this logic into our onFocusLost and onFocus hooks
func (gui *Gui) switchFocus(newView *gocui.View) error {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
return gui.switchFocusAux(newView)
}
func (gui *Gui) switchFocusAux(newView *gocui.View) error {
gui.pushView(newView.Name())
gui.Log.Info("setting highlight to true for view " + newView.Name())
gui.Log.Info("new focused view is " + newView.Name())
if _, err := gui.g.SetCurrentView(newView.Name()); err != nil {
return err
}
gui.g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
newViewStack := gui.State.ViewStack
if gui.State.Filter.panel != nil && !lo.Contains(newViewStack, gui.State.Filter.panel.GetView().Name()) {
if err := gui.clearFilter(); err != nil {
return err
}
}
// TODO: add 'onFocusLost' hook
if !lo.Contains(newViewStack, "menu") {
gui.Views.Menu.Visible = false
}
return gui.newLineFocused(newView)
}
func (gui *Gui) returnFocus() error {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
if len(gui.State.ViewStack) <= 1 {
return nil
}
previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2]
previousView, err := gui.g.View(previousViewName)
if err != nil {
return err
}
return gui.switchFocusAux(previousView)
}
func (gui *Gui) removeViewFromStack(view *gocui.View) {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
return viewName != view.Name()
})
}
// Not to be called directly. Use `switchFocus` instead
func (gui *Gui) pushView(name string) {
// No matter what view we're pushing, we first remove all popup panels from the stack
// (unless it's the search view because we may be searching the menu panel)
if name != "filter" {
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
return !gui.isPopupPanel(viewName)
})
}
// If we're pushing a side panel, we remove all other panels
if lo.Contains(gui.sideViewNames(), name) {
gui.State.ViewStack = []string{}
}
// If we're pushing a panel that's already in the stack, we remove it
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
return viewName != name
})
gui.State.ViewStack = append(gui.State.ViewStack, name)
}
// excludes popups
func (gui *Gui) currentStaticViewName() string {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
for i := len(gui.State.ViewStack) - 1; i >= 0; i-- {
if !lo.Contains(gui.popupViewNames(), gui.State.ViewStack[i]) {
return gui.State.ViewStack[i]
}
}
return gui.initiallyFocusedViewName()
}
func (gui *Gui) currentSideViewName() string {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
// we expect that there is a side window somewhere in the view stack, so we will search from top to bottom
for idx := range gui.State.ViewStack {
reversedIdx := len(gui.State.ViewStack) - 1 - idx
viewName := gui.State.ViewStack[reversedIdx]
if lo.Contains(gui.sideViewNames(), viewName) {
return viewName
}
}
return gui.initiallyFocusedViewName()
}

View file

@ -2,27 +2,52 @@ package gui
import ( import (
"context" "context"
"os"
"strings" "strings"
"sync"
"time" "time"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types"
"github.com/go-errors/errors" "github.com/go-errors/errors"
throttle "github.com/boz/go-throttle" throttle "github.com/boz/go-throttle"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
// SentinelErrors are the errors that have special meaning and need to be checked
// by calling functions. The less of these, the better
type SentinelErrors struct {
ErrNoContainers error
ErrNoImages error
ErrNoVolumes error
}
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
// because we can't do package-scoped errors with localization, and also because
// it seems like package-scoped variables are bad in general
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
// In the future it would be good to implement some of the recommendations of
// that article. For now, if we don't need an error to be a sentinel, we will just
// define it inline. This has implications for error messages that pop up everywhere
// in that we'll be duplicating the default values. We may need to look at
// having a default localisation bundle defined, and just using keys-only when
// localising things in the code.
func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{
ErrNoContainers: errors.New(gui.Tr.NoContainers),
ErrNoImages: errors.New(gui.Tr.NoImages),
ErrNoVolumes: errors.New(gui.Tr.NoVolumes),
}
}
// Gui wraps the gocui Gui object which handles rendering and events // Gui wraps the gocui Gui object which handles rendering and events
type Gui struct { type Gui struct {
g *gocui.Gui g *gocui.Gui
@ -32,10 +57,16 @@ type Gui struct {
State guiState State guiState
Config *config.AppConfig Config *config.AppConfig
Tr *i18n.TranslationSet Tr *i18n.TranslationSet
Errors SentinelErrors
statusManager *statusManager statusManager *statusManager
taskManager *tasks.TaskManager waitForIntro sync.WaitGroup
T *tasks.TaskManager
ErrorChan chan error ErrorChan chan error
CyclableViews []string
Views Views Views Views
// returns true if our views have been created and assigned to gui.Views.
// Views are setup only once, upon application start.
ViewsSetup bool
// if we've suspended the gui (e.g. because we've switched to a subprocess) // if we've suspended the gui (e.g. because we've switched to a subprocess)
// we typically want to pause some things that are running like background // we typically want to pause some things that are running like background
@ -43,23 +74,30 @@ type Gui struct {
PauseBackgroundThreads bool PauseBackgroundThreads bool
Mutexes Mutexes
Panels Panels
}
type Panels struct {
Projects *panels.SideListPanel[*commands.Project]
Services *panels.SideListPanel[*commands.Service]
Containers *panels.SideListPanel[*commands.Container]
Images *panels.SideListPanel[*commands.Image]
Volumes *panels.SideListPanel[*commands.Volume]
Networks *panels.SideListPanel[*commands.Network]
Menu *panels.SideListPanel[*types.MenuItem]
} }
type Mutexes struct { type Mutexes struct {
SubprocessMutex deadlock.Mutex SubprocessMutex sync.Mutex
ViewStackMutex deadlock.Mutex ViewStackMutex sync.Mutex
}
type servicePanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
}
type containerPanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
}
type projectState struct {
ContextIndex int // for specifying if you are looking at credits/logs
}
type menuPanelState struct {
SelectedLine int
OnPress func(*gocui.Gui, *gocui.View) error
} }
type mainPanelState struct { type mainPanelState struct {
@ -67,11 +105,28 @@ type mainPanelState struct {
ObjectKey string ObjectKey string
} }
type imagePanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
}
type volumePanelState struct {
SelectedLine int
ContextIndex int
}
type panelStates struct { type panelStates struct {
Services *servicePanelState
Containers *containerPanelState
Menu *menuPanelState
Main *mainPanelState Main *mainPanelState
Images *imagePanelState
Volumes *volumePanelState
Project *projectState
} }
type guiState struct { type guiState struct {
MenuItemCount int // can't store the actual list because it's of interface{} type
// the names of views in the current focus stack (last item is the current view) // the names of views in the current focus stack (last item is the current view)
ViewStack []string ViewStack []string
Platform commands.Platform Platform commands.Platform
@ -79,24 +134,7 @@ type guiState struct {
SubProcessOutput string SubProcessOutput string
Stats map[string]commands.ContainerStats Stats map[string]commands.ContainerStats
// if true, we show containers with an 'exited' status in the containers panel
ShowExitedContainers bool
ScreenMode WindowMaximisation ScreenMode WindowMaximisation
// Maintains the state of manual filtering i.e. typing in a substring
// to filter on in the current panel.
Filter filterState
}
type filterState struct {
// If true then we're either currently inside the filter view
// or we've committed the filter and we're back in the list view
active bool
// The panel that we're filtering.
panel panels.ISideListPanel
// The string that we're filtering on
needle string
} }
// screen sizing determines how much space your selected window takes up (window // screen sizing determines how much space your selected window takes up (window
@ -111,48 +149,44 @@ const (
SCREEN_FULL SCREEN_FULL
) )
func getScreenMode(config *config.AppConfig) WindowMaximisation {
switch config.UserConfig.Gui.ScreenMode {
case "normal":
return SCREEN_NORMAL
case "half":
return SCREEN_HALF
case "fullscreen":
return SCREEN_FULL
default:
return SCREEN_NORMAL
}
}
// NewGui builds a new gui handler // NewGui builds a new gui handler
func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) { func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
initialState := guiState{ initialState := guiState{
Platform: *oSCommand.Platform, Platform: *oSCommand.Platform,
Panels: &panelStates{ Panels: &panelStates{
Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0},
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
Volumes: &volumePanelState{SelectedLine: -1, ContextIndex: 0},
Menu: &menuPanelState{SelectedLine: 0},
Main: &mainPanelState{ Main: &mainPanelState{
ObjectKey: "", ObjectKey: "",
}, },
Project: &projectState{ContextIndex: 0},
}, },
ViewStack: []string{}, ViewStack: []string{},
}
ShowExitedContainers: true, cyclableViews := []string{"project", "containers", "images", "volumes"}
ScreenMode: getScreenMode(config), if dockerCommand.InDockerComposeProject {
cyclableViews = []string{"project", "services", "containers", "images", "volumes"}
} }
gui := &Gui{ gui := &Gui{
Log: log, Log: log,
DockerCommand: dockerCommand, DockerCommand: dockerCommand,
OSCommand: oSCommand, OSCommand: oSCommand,
// TODO: look into this warning
State: initialState, State: initialState,
Config: config, Config: config,
Tr: tr, Tr: tr,
statusManager: &statusManager{}, statusManager: &statusManager{},
taskManager: tasks.NewTaskManager(log, tr), T: tasks.NewTaskManager(log, tr),
ErrorChan: errorChan, ErrorChan: errorChan,
CyclableViews: cyclableViews,
} }
deadlock.Opts.Disable = !gui.Config.Debug gui.GenerateSentinelErrors()
deadlock.Opts.DeadlockTimeout = 10 * time.Second
return gui, nil return gui, nil
} }
@ -161,7 +195,7 @@ func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{ return gui.renderOptionsMap(map[string]string{
"PgUp/PgDn": gui.Tr.Scroll, "PgUp/PgDn": gui.Tr.Scroll,
"← → ↑ ↓": gui.Tr.Navigate, "← → ↑ ↓": gui.Tr.Navigate,
"q": gui.Tr.Quit, "esc/q": gui.Tr.Close,
"b": gui.Tr.ViewBulkCommands, "b": gui.Tr.ViewBulkCommands,
"x": gui.Tr.Menu, "x": gui.Tr.Menu,
}) })
@ -173,9 +207,11 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
if !gui.PauseBackgroundThreads { if gui.PauseBackgroundThreads {
_ = function() return
} }
_ = function()
} }
}() }()
} }
@ -183,12 +219,9 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) {
// Run setup the gui with keybindings and start the mainloop // Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error { func (gui *Gui) Run() error {
// closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker // closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker
defer gui.taskManager.Close() defer gui.T.Close()
g, err := gocui.NewGui(gocui.NewGuiOpts{ g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, gocui.NORMAL, false, map[rune]string{})
OutputMode: gocui.OutputTrue,
RuneReplacements: map[rune]string{},
})
if err != nil { if err != nil {
return err return err
} }
@ -201,19 +234,31 @@ func (gui *Gui) Run() error {
gui.g = g // TODO: always use gui.g rather than passing g around everywhere gui.g = g // TODO: always use gui.g rather than passing g around everywhere
// if the deadlock package wants to report a deadlock, we first need to
// close the gui so that we can actually read what it prints.
deadlock.Opts.LogBuf = lcUtils.NewOnceWriter(os.Stderr, func() {
gui.g.Close()
})
if err := gui.SetColorScheme(); err != nil { if err := gui.SetColorScheme(); err != nil {
return err return err
} }
gui.waitForIntro.Add(1)
throttledRefresh := throttle.ThrottleFunc(time.Millisecond*50, true, gui.refresh) throttledRefresh := throttle.ThrottleFunc(time.Millisecond*50, true, gui.refresh)
defer throttledRefresh.Stop() defer throttledRefresh.Stop()
ctx, finish := context.WithCancel(context.Background())
defer finish()
go gui.listenForEvents(ctx, throttledRefresh.Trigger)
go gui.DockerCommand.MonitorContainerStats(ctx)
go func() {
gui.waitForIntro.Wait()
throttledRefresh.Trigger()
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails)
gui.goEvery(time.Millisecond*1000, gui.checkForContextChange)
gui.goEvery(time.Millisecond*1000, gui.rerenderContainersAndServices)
}()
go func() { go func() {
for err := range gui.ErrorChan { for err := range gui.ErrorChan {
if err == nil { if err == nil {
@ -230,48 +275,10 @@ func (gui *Gui) Run() error {
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout())) g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
if err := gui.createAllViews(); err != nil {
return err
}
if err := gui.setInitialViewContent(); err != nil {
return err
}
// TODO: see if we can avoid the circular dependency
gui.setPanels()
if err = gui.keybindings(g); err != nil { if err = gui.keybindings(g); err != nil {
return err return err
} }
if gui.g.CurrentView() == nil {
viewName := gui.initiallyFocusedViewName()
view, err := gui.g.View(viewName)
if err != nil {
return err
}
if err := gui.switchFocus(view); err != nil {
return err
}
}
ctx, finish := context.WithCancel(context.Background())
defer finish()
go gui.listenForEvents(ctx, throttledRefresh.Trigger)
go gui.monitorContainerStats(ctx)
go func() {
throttledRefresh.Trigger()
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
gui.goEvery(time.Millisecond*1000, gui.updateContainerDetails)
gui.goEvery(time.Millisecond*1000, gui.checkForContextChange)
// we need to regularly re-render these because their stats will be changed in the background
gui.goEvery(time.Millisecond*1000, gui.renderContainersAndServices)
}()
err = g.MainLoop() err = g.MainLoop()
if err == gocui.ErrQuit { if err == gocui.ErrQuit {
return nil return nil
@ -279,45 +286,26 @@ func (gui *Gui) Run() error {
return err return err
} }
func (gui *Gui) setPanels() { func (gui *Gui) rerenderContainersAndServices() error {
gui.Panels = Panels{ // we need to regularly re-render these because their stats will be changed in the background
Projects: gui.getProjectPanel(), gui.renderContainersAndServices()
Services: gui.getServicesPanel(), return nil
Containers: gui.getContainersPanel(),
Images: gui.getImagesPanel(),
Volumes: gui.getVolumesPanel(),
Networks: gui.getNetworksPanel(),
Menu: gui.getMenuPanel(),
}
}
func (gui *Gui) updateContainerDetails() error {
return gui.DockerCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems())
} }
func (gui *Gui) refresh() { func (gui *Gui) refresh() {
go gui.refreshProject()
go func() { go func() {
// Refresh containers/services first, then projects (which depend on
// container labels to discover projects).
if err := gui.refreshContainersAndServices(); err != nil { 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.refreshVolumes(); err != nil {
gui.Log.Error(err) gui.Log.Error(err)
} }
}() }()
go func() { go func() {
if err := gui.reloadVolumes(); err != nil { if err := gui.refreshImages(); err != nil {
gui.Log.Error(err)
}
}()
go func() {
if err := gui.reloadNetworks(); err != nil {
gui.Log.Error(err)
}
}()
go func() {
if err := gui.reloadImages(); err != nil {
gui.Log.Error(err) gui.Log.Error(err)
} }
}() }()
@ -336,7 +324,7 @@ func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
outer: outer:
for { for {
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), events.ListOptions{}) messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), types.EventsOptions{})
if errorCount > 0 { if errorCount > 0 {
select { select {
@ -383,7 +371,7 @@ func (gui *Gui) checkForContextChange() error {
} }
func (gui *Gui) reRenderMain() error { func (gui *Gui) reRenderMain() error {
mainView := gui.Views.Main mainView := gui.getMainView()
if mainView == nil { if mainView == nil {
return nil return nil
} }
@ -404,16 +392,6 @@ func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit return gocui.ErrQuit
} }
// this handler is executed when we press escape when there is only one view
// on the stack.
func (gui *Gui) escape() error {
if gui.State.Filter.active {
return gui.clearFilter()
}
return nil
}
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
if !gui.g.Mouse { if !gui.g.Mouse {
return nil return nil
@ -449,7 +427,7 @@ func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
}) })
} }
func (gui *Gui) ShouldRefresh(key string) bool { func (gui *Gui) shouldRefresh(key string) bool {
if gui.State.Panels.Main.ObjectKey == key { if gui.State.Panels.Main.ObjectKey == key {
return false return false
} }
@ -459,56 +437,8 @@ 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"
} }
func (gui *Gui) IgnoreStrings() []string {
return gui.Config.UserConfig.Ignore
}
func (gui *Gui) Update(f func() error) {
gui.g.Update(func(*gocui.Gui) error { return f() })
}
func (gui *Gui) monitorContainerStats(ctx context.Context) {
// periodically loop through running containers and see if we need to create a monitor goroutine for any
// every second we check if we need to spawn a new goroutine
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
for _, container := range gui.Panels.Containers.List.GetAllItems() {
if !container.MonitoringStats {
go gui.DockerCommand.CreateClientStatMonitor(container)
}
}
}
}
}
// this is used by our cheatsheet code to generate keybindings. We need some views
// and panels to exist for us to know what keybindings there are, so we invoke
// gocui in headless mode and create them.
func (gui *Gui) SetupFakeGui() {
g, err := gocui.NewGui(gocui.NewGuiOpts{
OutputMode: gocui.OutputTrue,
RuneReplacements: map[rune]string{},
Headless: true,
})
if err != nil {
panic(err)
}
gui.g = g
defer g.Close()
if err := gui.createAllViews(); err != nil {
panic(err)
}
gui.setPanels()
}

View file

@ -5,75 +5,76 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
) )
func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] { // list panel functions
noneLabel := "<none>"
return &panels.SideListPanel[*commands.Image]{ func (gui *Gui) getImageContexts() []string {
ContextState: &panels.ContextState[*commands.Image]{ return []string{"config"}
GetMainTabs: func() []panels.MainTab[*commands.Image] {
return []panels.MainTab[*commands.Image]{
{
Key: "config",
Title: gui.Tr.ConfigTitle,
Render: gui.renderImageConfigTask,
},
}
},
GetItemContextCacheKey: func(image *commands.Image) string {
return "images-" + image.ID
},
},
ListPanel: panels.ListPanel[*commands.Image]{
List: panels.NewFilteredList[*commands.Image](),
View: gui.Views.Images,
},
NoItemsMessage: gui.Tr.NoImages,
Gui: gui.intoInterface(),
Sort: func(a *commands.Image, b *commands.Image) bool {
if a.Name == noneLabel && b.Name != noneLabel {
return false
}
if a.Name != noneLabel && b.Name == noneLabel {
return true
}
if a.Name != b.Name {
return a.Name < b.Name
}
if a.Tag != b.Tag {
return a.Tag < b.Tag
}
return a.ID < b.ID
},
GetTableCells: presentation.GetImageDisplayStrings,
}
} }
func (gui *Gui) renderImageConfigTask(image *commands.Image) tasks.TaskFunc { func (gui *Gui) getImageContextTitles() []string {
return gui.NewRenderStringTask(RenderStringTaskOpts{ return []string{gui.Tr.ConfigTitle}
GetStrContent: func() string { return gui.imageConfigStr(image) },
Autoscroll: false,
Wrap: false, // don't care what your config is this page is ugly without wrapping
})
} }
func (gui *Gui) imageConfigStr(image *commands.Image) string { func (gui *Gui) getSelectedImage() (*commands.Image, error) {
selectedLine := gui.State.Panels.Images.SelectedLine
if selectedLine == -1 {
return &commands.Image{}, gui.Errors.ErrNoImages
}
return gui.DockerCommand.Images[selectedLine], nil
}
func (gui *Gui) handleImagesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.DockerCommand.Images)
handleSelect := gui.handleImageSelect
selectedLine := &gui.State.Panels.Images.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
Image, err := gui.getSelectedImage()
if err != nil {
if err != gui.Errors.ErrNoImages {
return err
}
return gui.renderString(g, "main", gui.Tr.NoImages)
}
gui.focusY(gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v)
key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
mainView := gui.getMainView()
mainView.Tabs = gui.getImageContextTitles()
mainView.TabIndex = gui.State.Panels.Images.ContextIndex
switch gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] {
case "config":
if err := gui.renderImageConfig(mainView, Image); err != nil {
return err
}
default:
return errors.New("Unknown context for Images panel")
}
return nil
}
func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) error {
return gui.T.NewTask(func(stop chan struct{}) {
padding := 10 padding := 10
output := "" output := ""
output += utils.WithPadding("Name: ", padding) + image.Name + "\n" output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
@ -89,94 +90,173 @@ func (gui *Gui) imageConfigStr(image *commands.Image) string {
output += "\n\n" + history output += "\n\n" + history
return output mainView.Autoscroll = false
mainView.Wrap = false // don't care what your config is this page is ugly without wrapping
_ = gui.renderString(gui.g, "main", output)
})
} }
func (gui *Gui) reloadImages() error { func (gui *Gui) refreshImages() error {
ImagesView := gui.getImagesView()
if ImagesView == nil {
// if the ImagesView hasn't been instantiated yet we just return
return nil
}
if err := gui.refreshStateImages(); err != nil { if err := gui.refreshStateImages(); err != nil {
return err return err
} }
return gui.Panels.Images.RerenderList() if len(gui.DockerCommand.Images) > 0 && gui.State.Panels.Images.SelectedLine == -1 {
gui.State.Panels.Images.SelectedLine = 0
}
if len(gui.DockerCommand.Images)-1 < gui.State.Panels.Images.SelectedLine {
gui.State.Panels.Images.SelectedLine = len(gui.DockerCommand.Images) - 1
}
gui.g.Update(func(g *gocui.Gui) error {
ImagesView.Clear()
isFocused := gui.g.CurrentView() == gui.Views.Images
list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused))
if err != nil {
return err
}
fmt.Fprint(ImagesView, list)
if ImagesView == g.CurrentView() {
return gui.handleImageSelect(g, ImagesView)
}
return nil
})
return nil
} }
// TODO: leave this to DockerCommand
func (gui *Gui) refreshStateImages() error { func (gui *Gui) refreshStateImages() error {
images, err := gui.DockerCommand.RefreshImages() Images, err := gui.DockerCommand.RefreshImages()
if err != nil { if err != nil {
return err return err
} }
gui.Panels.Images.SetItems(images) gui.DockerCommand.Images = Images
return nil return nil
} }
func (gui *Gui) FilterString(view *gocui.View) string { func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() != view { if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return "" return nil
} }
return gui.State.Filter.needle panelState := gui.State.Panels.Images
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), false)
return gui.handleImageSelect(gui.g, v)
}
func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
panelState := gui.State.Panels.Images
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), true)
return gui.handleImageSelect(gui.g, v)
}
func (gui *Gui) handleImagesNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getImageContexts()
if gui.State.Panels.Images.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Images.ContextIndex = 0
} else {
gui.State.Panels.Images.ContextIndex++
}
_ = gui.handleImageSelect(gui.g, v)
return nil
}
func (gui *Gui) handleImagesPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getImageContexts()
if gui.State.Panels.Images.ContextIndex <= 0 {
gui.State.Panels.Images.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Images.ContextIndex--
}
_ = gui.handleImageSelect(gui.g, v)
return nil
}
type removeImageOption struct {
description string
command string
configOptions types.ImageRemoveOptions
runCommand bool
}
// GetDisplayStrings is a function.
func (r *removeImageOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
} }
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
type removeImageOption struct { Image, err := gui.getSelectedImage()
description string
command string
configOptions image.RemoveOptions
}
img, err := gui.Panels.Images.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
shortSha := img.ID[7:17] shortSha := Image.ID[7:17]
// TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item // TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item
options := []*removeImageOption{ options := []*removeImageOption{
{ {
description: gui.Tr.Remove, description: gui.Tr.Remove,
command: "docker image rm " + shortSha, command: "docker image rm " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: true, Force: false}, configOptions: types.ImageRemoveOptions{PruneChildren: true, Force: false},
runCommand: true,
}, },
{ {
description: gui.Tr.RemoveWithoutPrune, description: gui.Tr.RemoveWithoutPrune,
command: "docker image rm --no-prune " + shortSha, command: "docker image rm --no-prune " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: false, Force: false}, configOptions: types.ImageRemoveOptions{PruneChildren: false, Force: false},
runCommand: true,
}, },
{ {
description: gui.Tr.RemoveWithForce, description: gui.Tr.RemoveWithForce,
command: "docker image rm --force " + shortSha, command: "docker image rm --force " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: true, Force: true}, configOptions: types.ImageRemoveOptions{PruneChildren: true, Force: true},
runCommand: true,
}, },
{ {
description: gui.Tr.RemoveWithoutPruneWithForce, description: gui.Tr.RemoveWithoutPruneWithForce,
command: "docker image rm --no-prune --force " + shortSha, command: "docker image rm --no-prune --force " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: false, Force: true}, configOptions: types.ImageRemoveOptions{PruneChildren: false, Force: true},
runCommand: true,
},
{
description: gui.Tr.Cancel,
runCommand: false,
}, },
} }
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *types.MenuItem { handleMenuPress := func(index int) error {
return &types.MenuItem{ if !options[index].runCommand {
LabelColumns: []string{ return nil
option.description, }
color.New(color.FgRed).Sprint(option.command), configOptions := options[index].configOptions
}, if cerr := Image.Remove(configOptions); cerr != nil {
OnPress: func() error { return gui.createErrorPanel(cerr.Error())
if err := img.Remove(option.configOptions); err != nil {
return gui.createErrorPanel(err.Error())
} }
return nil return nil
},
} }
})
return gui.Menu(CreateMenuOptions{ return gui.createMenu("", options, len(options), handleMenuPress)
Title: "",
Items: menuItems,
})
} }
func (gui *Gui) handlePruneImages() error { func (gui *Gui) handlePruneImages() error {
@ -186,19 +266,19 @@ func (gui *Gui) handlePruneImages() error {
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
} }
return gui.reloadImages() return gui.refreshImages()
}) })
}, nil) }, nil)
} }
func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
img, err := gui.Panels.Images.GetSelectedItem() image, err := gui.getSelectedImage()
if err != nil { if err != nil {
return nil return nil
} }
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Image: img, Image: image,
}) })
customCommands := gui.Config.UserConfig.CustomCommands.Images customCommands := gui.Config.UserConfig.CustomCommands.Images

View file

@ -17,6 +17,11 @@ type Binding struct {
Description string Description string
} }
// GetDisplayStrings returns the display string of a file
func (b *Binding) GetDisplayStrings(isFocused bool) []string {
return []string{b.GetKey(), b.Description}
}
// GetKey is a function. // GetKey is a function.
func (b *Binding) GetKey() string { func (b *Binding) GetKey() string {
key := 0 key := 0
@ -56,12 +61,6 @@ func (b *Binding) GetKey() string {
// GetInitialKeybindings is a function. // GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding { func (gui *Gui) GetInitialKeybindings() []*Binding {
bindings := []*Binding{ bindings := []*Binding{
{
ViewName: "",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.escape),
},
{ {
ViewName: "", ViewName: "",
Key: 'q', Key: 'q',
@ -74,29 +73,35 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.quit, Handler: gui.quit,
}, },
{
ViewName: "",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.quit,
},
{ {
ViewName: "", ViewName: "",
Key: gocui.KeyPgup, Key: gocui.KeyPgup,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.scrollUpMain), Handler: gui.scrollUpMain,
}, },
{ {
ViewName: "", ViewName: "",
Key: gocui.KeyPgdn, Key: gocui.KeyPgdn,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.scrollDownMain), Handler: gui.scrollDownMain,
}, },
{ {
ViewName: "", ViewName: "",
Key: gocui.KeyCtrlU, Key: gocui.KeyCtrlU,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.scrollUpMain), Handler: gui.scrollUpMain,
}, },
{ {
ViewName: "", ViewName: "",
Key: gocui.KeyCtrlD, Key: gocui.KeyCtrlD,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.scrollDownMain), Handler: gui.scrollDownMain,
}, },
{ {
ViewName: "", ViewName: "",
@ -104,12 +109,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.autoScrollMain, Handler: gui.autoScrollMain,
}, },
{
ViewName: "",
Key: gocui.KeyHome,
Modifier: gocui.ModNone,
Handler: gui.jumpToTopMain,
},
{ {
ViewName: "", ViewName: "",
Key: 'x', Key: 'x',
@ -142,6 +141,26 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleOpenConfig, Handler: gui.handleOpenConfig,
Description: gui.Tr.OpenConfig, Description: gui.Tr.OpenConfig,
}, },
{
ViewName: "project",
Key: '[',
Modifier: gocui.ModNone,
Handler: gui.handleProjectPrevContext,
Description: gui.Tr.PreviousContext,
},
{
ViewName: "project",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleProjectNextContext,
Description: gui.Tr.NextContext,
},
{
ViewName: "project",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleProjectClick,
},
{ {
ViewName: "project", ViewName: "project",
Key: 'm', Key: 'm',
@ -149,35 +168,23 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleViewAllLogs, Handler: gui.handleViewAllLogs,
Description: gui.Tr.ViewLogs, Description: gui.Tr.ViewLogs,
}, },
{
ViewName: "project",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleProjectSelect,
},
{ {
ViewName: "menu", ViewName: "menu",
Key: gocui.KeyEsc, Key: gocui.KeyEsc,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.handleMenuClose), Handler: gui.handleMenuClose,
}, },
{ {
ViewName: "menu", ViewName: "menu",
Key: 'q', Key: 'q',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.handleMenuClose), Handler: gui.handleMenuClose,
},
{
ViewName: "menu",
Key: ' ',
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.handleMenuPress),
},
{
ViewName: "menu",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.handleMenuPress),
},
{
ViewName: "menu",
Key: 'y',
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.handleMenuPress),
}, },
{ {
ViewName: "information", ViewName: "information",
@ -185,6 +192,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleDonate, Handler: gui.handleDonate,
}, },
{
ViewName: "containers",
Key: '[',
Modifier: gocui.ModNone,
Handler: gui.handleContainersPrevContext,
Description: gui.Tr.PreviousContext,
},
{
ViewName: "containers",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleContainersNextContext,
Description: gui.Tr.NextContext,
},
{ {
ViewName: "containers", ViewName: "containers",
Key: 'd', Key: 'd',
@ -262,13 +283,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainersOpenInBrowserCommand, Handler: gui.handleContainersOpenInBrowserCommand,
Description: gui.Tr.OpenInBrowser, Description: gui.Tr.OpenInBrowser,
}, },
{
ViewName: "services",
Key: 'u',
Modifier: gocui.ModNone,
Handler: gui.handleServiceUp,
Description: gui.Tr.UpService,
},
{ {
ViewName: "services", ViewName: "services",
Key: 'd', Key: 'd',
@ -320,17 +334,17 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}, },
{ {
ViewName: "services", ViewName: "services",
Key: 'U', Key: '[',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleProjectUp, Handler: gui.handleServicesPrevContext,
Description: gui.Tr.UpProject, Description: gui.Tr.PreviousContext,
}, },
{ {
ViewName: "services", ViewName: "services",
Key: 'D', Key: ']',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleProjectDown, Handler: gui.handleServicesNextContext,
Description: gui.Tr.DownProject, Description: gui.Tr.NextContext,
}, },
{ {
ViewName: "services", ViewName: "services",
@ -367,6 +381,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleServicesOpenInBrowserCommand, Handler: gui.handleServicesOpenInBrowserCommand,
Description: gui.Tr.OpenInBrowser, Description: gui.Tr.OpenInBrowser,
}, },
{
ViewName: "images",
Key: '[',
Modifier: gocui.ModNone,
Handler: gui.handleImagesPrevContext,
Description: gui.Tr.PreviousContext,
},
{
ViewName: "images",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleImagesNextContext,
Description: gui.Tr.NextContext,
},
{ {
ViewName: "images", ViewName: "images",
Key: 'c', Key: 'c',
@ -388,6 +416,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleImagesBulkCommand, Handler: gui.handleImagesBulkCommand,
Description: gui.Tr.ViewBulkCommands, Description: gui.Tr.ViewBulkCommands,
}, },
{
ViewName: "volumes",
Key: '[',
Modifier: gocui.ModNone,
Handler: gui.handleVolumesPrevContext,
Description: gui.Tr.PreviousContext,
},
{
ViewName: "volumes",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleVolumesNextContext,
Description: gui.Tr.NextContext,
},
{ {
ViewName: "volumes", ViewName: "volumes",
Key: 'c', Key: 'c',
@ -409,27 +451,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleVolumesBulkCommand, Handler: gui.handleVolumesBulkCommand,
Description: gui.Tr.ViewBulkCommands, Description: gui.Tr.ViewBulkCommands,
}, },
{
ViewName: "networks",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleNetworksCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "networks",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleNetworksRemoveMenu,
Description: gui.Tr.RemoveNetwork,
},
{
ViewName: "networks",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handleNetworksBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{ {
ViewName: "main", ViewName: "main",
Key: gocui.KeyEsc, Key: gocui.KeyEsc,
@ -461,29 +482,17 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.scrollRightMain, Handler: gui.scrollRightMain,
}, },
{
ViewName: "filter",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.commitFilter),
},
{
ViewName: "filter",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.escapeFilterPrompt),
},
{ {
ViewName: "", ViewName: "",
Key: 'J', Key: 'J',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.scrollDownMain), Handler: gui.scrollDownMain,
}, },
{ {
ViewName: "", ViewName: "",
Key: 'K', Key: 'K',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.scrollUpMain), Handler: gui.scrollUpMain,
}, },
{ {
ViewName: "", ViewName: "",
@ -511,81 +520,52 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}, },
} }
for _, panel := range gui.allSidePanels() { // TODO: add more views here
for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "menu"} {
bindings = append(bindings, []*Binding{ bindings = append(bindings, []*Binding{
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: panel.GetView().Name(), Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyBacktab, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gocui.KeyBacktab, Modifier: gocui.ModNone, Handler: gui.previousView},
}...) }...)
} }
setUpDownClickBindings := func(viewName string, onUp func() error, onDown func() error, onClick func() error) { panelMap := map[string]struct {
onKeyUpPress func(*gocui.Gui, *gocui.View) error
onKeyDownPress func(*gocui.Gui, *gocui.View) error
onClick func(*gocui.Gui, *gocui.View) error
}{
"menu": {onKeyUpPress: gui.handleMenuPrevLine, onKeyDownPress: gui.handleMenuNextLine, onClick: gui.handleMenuClick},
"services": {onKeyUpPress: gui.handleServicesPrevLine, onKeyDownPress: gui.handleServicesNextLine, onClick: gui.handleServicesClick},
"containers": {onKeyUpPress: gui.handleContainersPrevLine, onKeyDownPress: gui.handleContainersNextLine, onClick: gui.handleContainersClick},
"images": {onKeyUpPress: gui.handleImagesPrevLine, onKeyDownPress: gui.handleImagesNextLine, onClick: gui.handleImagesClick},
"volumes": {onKeyUpPress: gui.handleVolumesPrevLine, onKeyDownPress: gui.handleVolumesNextLine, onClick: gui.handleVolumesClick},
"main": {onKeyUpPress: gui.scrollUpMain, onKeyDownPress: gui.scrollDownMain, onClick: gui.handleMainClick},
}
for viewName, functions := range panelMap {
bindings = append(bindings, []*Binding{ bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)}, {ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)}, {ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)}, {ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)}, {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)}, {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)}, {ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: wrappedHandler(onClick)}, {ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.onClick},
}...) }...)
} }
bindings = append(bindings, []*Binding{ for _, viewName := range []string{"project", "services", "containers", "images", "volumes"} {
{Handler: gui.handleGoTo(gui.Panels.Projects.View), Key: '1', Description: gui.Tr.FocusProjects}, bindings = append(bindings, &Binding{
{Handler: gui.handleGoTo(gui.Panels.Services.View), Key: '2', Description: gui.Tr.FocusServices}, ViewName: viewName,
{Handler: gui.handleGoTo(gui.Panels.Containers.View), Key: '3', Description: gui.Tr.FocusContainers},
{Handler: gui.handleGoTo(gui.Panels.Images.View), Key: '4', Description: gui.Tr.FocusImages},
{Handler: gui.handleGoTo(gui.Panels.Volumes.View), Key: '5', Description: gui.Tr.FocusVolumes},
{Handler: gui.handleGoTo(gui.Panels.Networks.View), Key: '6', Description: gui.Tr.FocusNetworks},
}...)
for _, panel := range gui.allListPanels() {
setUpDownClickBindings(panel.GetView().Name(), panel.HandlePrevLine, panel.HandleNextLine, panel.HandleClick)
}
setUpDownClickBindings("main", gui.scrollUpMain, gui.scrollDownMain, gui.handleMainClick)
for _, panel := range gui.allSidePanels() {
bindings = append(bindings,
&Binding{
ViewName: panel.GetView().Name(),
Key: gocui.KeyEnter, Key: gocui.KeyEnter,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleEnterMain, Handler: gui.handleEnterMain,
Description: gui.Tr.FocusMain, Description: gui.Tr.FocusMain,
},
&Binding{
ViewName: panel.GetView().Name(),
Key: '[',
Modifier: gocui.ModNone,
Handler: wrappedHandler(panel.HandlePrevMainTab),
Description: gui.Tr.PreviousContext,
},
&Binding{
ViewName: panel.GetView().Name(),
Key: ']',
Modifier: gocui.ModNone,
Handler: wrappedHandler(panel.HandleNextMainTab),
Description: gui.Tr.NextContext,
},
)
}
for _, panel := range gui.allListPanels() {
if !panel.IsFilterDisabled() {
bindings = append(bindings, &Binding{
ViewName: panel.GetView().Name(),
Key: '/',
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.handleOpenFilter),
Description: gui.Tr.LcFilter,
}) })
} }
}
return bindings return bindings
} }
@ -594,7 +574,7 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := gui.GetInitialKeybindings() bindings := gui.GetInitialKeybindings()
for _, binding := range bindings { for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil { if err := g.SetKeybinding(binding.ViewName, nil, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err return err
} }
} }

View file

@ -59,6 +59,14 @@ func (gui *Gui) onFocus(v *gocui.View) {
// layout is called for every screen re-render e.g. when the screen is resized // layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error { func (gui *Gui) layout(g *gocui.Gui) error {
if !gui.ViewsSetup {
if err := gui.createAllViews(); err != nil {
return err
}
gui.ViewsSetup = true
}
g.Highlight = true g.Highlight = true
width, height := g.Size() width, height := g.Size()
@ -101,13 +109,25 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return view, err return view, err
} }
for _, viewName := range gui.autoPositionedViewNames() { for _, viewName := range gui.controlledBoundsViewNames() {
_, err := setViewFromDimensions(viewName, viewName) _, err := setViewFromDimensions(viewName, viewName)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err return err
} }
} }
if gui.g.CurrentView() == nil {
viewName := gui.initiallyFocusedViewName()
view, err := gui.g.View(viewName)
if err != nil {
return err
}
if err := gui.switchFocus(view); err != nil {
return err
}
}
// here is a good place log some stuff // here is a good place log some stuff
// if you download humanlog and do tail -f development.log | humanlog // if you download humanlog and do tail -f development.log | humanlog
// this will let you see these branches as prettified json // this will let you see these branches as prettified json
@ -115,16 +135,26 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return gui.resizeCurrentPopupPanel(g) return gui.resizeCurrentPopupPanel(g)
} }
type listViewState struct {
selectedLine int
lineCount int
}
func (gui *Gui) focusPointInView(view *gocui.View) { func (gui *Gui) focusPointInView(view *gocui.View) {
if view == nil { if view == nil {
return return
} }
for _, panel := range gui.allListPanels() { listViews := map[string]listViewState{
if panel.GetView() == view { "containers": {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.DisplayContainers)},
panel.Refocus() "images": {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
return "volumes": {selectedLine: gui.State.Panels.Volumes.SelectedLine, lineCount: len(gui.DockerCommand.Volumes)},
"services": {selectedLine: gui.State.Panels.Services.SelectedLine, lineCount: len(gui.DockerCommand.Services)},
"menu": {selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount},
} }
if state, ok := listViews[view.Name()]; ok {
gui.focusY(state.selectedLine, state.lineCount, view)
} }
} }

View file

@ -6,16 +6,16 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
) )
func (gui *Gui) scrollUpMain() error { func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.Views.Main mainView := gui.getMainView()
mainView.Autoscroll = false mainView.Autoscroll = false
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight))) newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight)))
return mainView.SetOrigin(ox, newOy) return mainView.SetOrigin(ox, newOy)
} }
func (gui *Gui) scrollDownMain() error { func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.Views.Main mainView := gui.getMainView()
mainView.Autoscroll = false mainView.Autoscroll = false
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
@ -34,7 +34,7 @@ func (gui *Gui) scrollDownMain() error {
} }
func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.Views.Main mainView := gui.getMainView()
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
newOx := int(math.Max(0, float64(ox-gui.Config.UserConfig.Gui.ScrollHeight))) newOx := int(math.Max(0, float64(ox-gui.Config.UserConfig.Gui.ScrollHeight)))
@ -42,7 +42,7 @@ func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.Views.Main mainView := gui.getMainView()
ox, oy := mainView.Origin() ox, oy := mainView.Origin()
content := mainView.ViewBufferLines() content := mainView.ViewBufferLines()
@ -62,32 +62,43 @@ func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
gui.Views.Main.Autoscroll = true gui.getMainView().Autoscroll = true
return nil
}
func (gui *Gui) jumpToTopMain(g *gocui.Gui, v *gocui.View) error {
gui.Views.Main.Autoscroll = false
_ = gui.Views.Main.SetOrigin(0, 0)
_ = gui.Views.Main.SetCursor(0, 0)
return nil return nil
} }
func (gui *Gui) onMainTabClick(tabIndex int) error { func (gui *Gui) onMainTabClick(tabIndex int) error {
gui.Log.Warn(tabIndex) gui.Log.Warn(tabIndex)
currentSidePanel, ok := gui.currentSidePanel() viewName := gui.currentViewName()
if !ok { mainView := gui.getMainView()
return nil if viewName == "main" && mainView.ParentView != nil {
viewName = mainView.ParentView.Name()
} }
currentSidePanel.SetMainTabIndex(tabIndex) switch viewName {
return currentSidePanel.HandleSelect() case "project":
gui.State.Panels.Project.ContextIndex = tabIndex
return gui.handleProjectSelect(gui.g, gui.getProjectView())
case "services":
gui.State.Panels.Services.ContextIndex = tabIndex
return gui.handleServiceSelect(gui.g, gui.getServicesView())
case "containers":
gui.State.Panels.Containers.ContextIndex = tabIndex
return gui.handleContainerSelect(gui.g, gui.getContainersView())
case "images":
gui.State.Panels.Images.ContextIndex = tabIndex
return gui.handleImageSelect(gui.g, gui.getImagesView())
case "volumes":
gui.State.Panels.Volumes.ContextIndex = tabIndex
return gui.handleVolumeSelect(gui.g, gui.getVolumesView())
}
return nil
} }
func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.Views.Main mainView := gui.getMainView()
mainView.ParentView = v mainView.ParentView = v
return gui.switchFocus(mainView) return gui.switchFocus(mainView)
@ -98,16 +109,18 @@ func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error {
return gui.returnFocus() return gui.returnFocus()
} }
func (gui *Gui) handleMainClick() error { func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() { if gui.popupPanelFocused() {
return nil return nil
} }
currentView := gui.g.CurrentView() currentView := gui.g.CurrentView()
if currentView.Name() != "main" { if currentView != nil && currentView.Name() == "main" {
gui.Views.Main.ParentView = currentView currentView = nil
} else {
v.ParentView = currentView
} }
return gui.switchFocus(gui.Views.Main) return gui.switchFocus(v)
} }

View file

@ -1,133 +1,112 @@
package gui package gui
import ( import (
"github.com/jesseduffield/lazydocker/pkg/gui/panels" "fmt"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/gui/types" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
) )
type CreateMenuOptions struct { // list panel functions
Title string
Items []*types.MenuItem func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
HideCancel bool gui.focusY(gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
return nil
} }
func (gui *Gui) getMenuPanel() *panels.SideListPanel[*types.MenuItem] { func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
return &panels.SideListPanel[*types.MenuItem]{ panelState := gui.State.Panels.Menu
ListPanel: panels.ListPanel[*types.MenuItem]{ gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
List: panels.NewFilteredList[*types.MenuItem](),
View: gui.Views.Menu, return gui.handleMenuSelect(g, v)
},
NoItemsMessage: "",
Gui: gui.intoInterface(),
OnClick: gui.onMenuPress,
Sort: nil,
GetTableCells: presentation.GetMenuItemDisplayStrings,
OnRerender: func() error {
return gui.resizePopupPanel(gui.Views.Menu)
},
// so that we can avoid some UI trickiness, the menu will not have filtering
// abillity yet. To support it, we would need to have filter state against
// each panel (e.g. for when you filter the images panel, then bring up
// the options menu, then try to filter that too.
DisableFilter: true,
}
} }
func (gui *Gui) onMenuPress(menuItem *types.MenuItem) error { func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
if err := gui.handleMenuClose(); err != nil { itemCount := gui.State.MenuItemCount
handleSelect := gui.handleMenuSelect
selectedLine := &gui.State.Panels.Menu.SelectedLine
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
return err return err
} }
if menuItem.OnPress != nil { return gui.State.Panels.Menu.OnPress(g, v)
return menuItem.OnPress()
}
return nil
} }
func (gui *Gui) handleMenuPress() error { func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
selectedMenuItem, err := gui.Panels.Menu.GetSelectedItem() panelState := gui.State.Panels.Menu
if err != nil { gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
return nil
}
return gui.onMenuPress(selectedMenuItem) return gui.handleMenuSelect(g, v)
}
func (gui *Gui) Menu(opts CreateMenuOptions) error {
if !opts.HideCancel {
// this is mutative but I'm okay with that for now
opts.Items = append(opts.Items, &types.MenuItem{
LabelColumns: []string{gui.Tr.Cancel},
OnPress: func() error {
return nil
},
})
}
maxColumnSize := 1
for _, item := range opts.Items {
if item.LabelColumns == nil {
item.LabelColumns = []string{item.Label}
}
if item.OpensMenu {
item.LabelColumns[0] = utils.OpensMenuStyle(item.LabelColumns[0])
}
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
}
for _, item := range opts.Items {
if len(item.LabelColumns) < maxColumnSize {
// we require that each item has the same number of columns so we're padding out with blank strings
// if this item has too few
item.LabelColumns = append(item.LabelColumns, make([]string, maxColumnSize-len(item.LabelColumns))...)
}
}
gui.Panels.Menu.SetItems(opts.Items)
gui.Panels.Menu.SetSelectedLineIdx(0)
if err := gui.Panels.Menu.RerenderList(); err != nil {
return err
}
gui.Views.Menu.Title = opts.Title
gui.Views.Menu.Visible = true
return gui.switchFocus(gui.Views.Menu)
} }
// specific functions // specific functions
func (gui *Gui) renderMenuOptions() error { func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{ optionsMap := map[string]string{
"esc": gui.Tr.Close, "esc/q": gui.Tr.Close,
"↑ ↓": gui.Tr.Navigate, "↑ ↓": gui.Tr.Navigate,
"enter": gui.Tr.Execute, "enter": gui.Tr.Execute,
} }
return gui.renderOptionsMap(optionsMap) return gui.renderOptionsMap(optionsMap)
} }
func (gui *Gui) handleMenuClose() error { func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
return err
}
}
gui.Views.Menu.Visible = false gui.Views.Menu.Visible = false
return gui.returnFocus()
}
// this code is here for when we do add filter ability to the menu panel, func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
// though it's currently disabled isFocused := gui.g.CurrentView().Name() == "menu"
if gui.State.Filter.panel == gui.Panels.Menu { gui.State.MenuItemCount = itemCount
if err := gui.clearFilter(); err != nil { list, err := utils.RenderList(items, utils.IsFocused(isFocused))
if err != nil {
return err return err
} }
// we need to remove the view from the view stack because we're about to x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
// return focus and don't want to land in the search view when it was searching _, _ = gui.g.SetView("menu", x0, y0, x1, y1, 0)
// the menu in the first place menuView := gui.Views.Menu
gui.removeViewFromStack(gui.Views.Filter) menuView.Title = title
menuView.FgColor = gocui.ColorDefault
menuView.Clear()
fmt.Fprint(menuView, list)
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
menuView.Visible = false
err := gui.returnFocus()
if err != nil {
return err
} }
return gui.returnFocus() if err := handlePress(selectedLine); err != nil {
return err
}
return nil
}
gui.State.Panels.Menu.OnPress = wrappedHandlePress
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
if err := gui.g.SetKeybinding("menu", nil, key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
menuView.Visible = true
return gui.switchFocus(menuView)
})
return nil
} }

View file

@ -1,179 +0,0 @@
package gui
import (
"strconv"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
)
func (gui *Gui) getNetworksPanel() *panels.SideListPanel[*commands.Network] {
return &panels.SideListPanel[*commands.Network]{
ContextState: &panels.ContextState[*commands.Network]{
GetMainTabs: func() []panels.MainTab[*commands.Network] {
return []panels.MainTab[*commands.Network]{
{
Key: "config",
Title: gui.Tr.ConfigTitle,
Render: gui.renderNetworkConfig,
},
}
},
GetItemContextCacheKey: func(network *commands.Network) string {
return "networks-" + network.Name
},
},
ListPanel: panels.ListPanel[*commands.Network]{
List: panels.NewFilteredList[*commands.Network](),
View: gui.Views.Networks,
},
NoItemsMessage: gui.Tr.NoNetworks,
Gui: gui.intoInterface(),
// we're sorting these networks based on whether they have labels defined,
// because those are the ones you typically care about.
// Within that, we also sort them alphabetically
Sort: func(a *commands.Network, b *commands.Network) bool {
return a.Name < b.Name
},
GetTableCells: presentation.GetNetworkDisplayStrings,
}
}
func (gui *Gui) renderNetworkConfig(network *commands.Network) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.networkConfigStr(network) })
}
func (gui *Gui) networkConfigStr(network *commands.Network) string {
padding := 15
output := ""
output += utils.WithPadding("ID: ", padding) + network.Network.ID + "\n"
output += utils.WithPadding("Name: ", padding) + network.Name + "\n"
output += utils.WithPadding("Driver: ", padding) + network.Network.Driver + "\n"
output += utils.WithPadding("Scope: ", padding) + network.Network.Scope + "\n"
output += utils.WithPadding("EnabledIPV6: ", padding) + strconv.FormatBool(network.Network.EnableIPv6) + "\n"
output += utils.WithPadding("Internal: ", padding) + strconv.FormatBool(network.Network.Internal) + "\n"
output += utils.WithPadding("Attachable: ", padding) + strconv.FormatBool(network.Network.Attachable) + "\n"
output += utils.WithPadding("Ingress: ", padding) + strconv.FormatBool(network.Network.Ingress) + "\n"
output += utils.WithPadding("Containers: ", padding)
if len(network.Network.Containers) > 0 {
output += "\n"
for _, v := range network.Network.Containers {
output += utils.FormatMapItem(padding, v.Name, v.EndpointID)
}
} else {
output += "none\n"
}
output += "\n"
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, network.Network.Labels) + "\n"
output += utils.WithPadding("Options: ", padding) + utils.FormatMap(padding, network.Network.Options)
return output
}
func (gui *Gui) reloadNetworks() error {
if err := gui.refreshStateNetworks(); err != nil {
return err
}
return gui.Panels.Networks.RerenderList()
}
func (gui *Gui) refreshStateNetworks() error {
networks, err := gui.DockerCommand.RefreshNetworks()
if err != nil {
return err
}
gui.Panels.Networks.SetItems(networks)
return nil
}
func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
network, err := gui.Panels.Networks.GetSelectedItem()
if err != nil {
return nil
}
type removeNetworkOption struct {
description string
command string
}
options := []*removeNetworkOption{
{
description: gui.Tr.Remove,
command: utils.WithShortSha("docker network rm " + network.Name),
},
}
menuItems := lo.Map(options, func(option *removeNetworkOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: []string{option.description, color.New(color.FgRed).Sprint(option.command)},
OnPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := network.Remove(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
}
})
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
}
func (gui *Gui) handlePruneNetworks() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneNetworks, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneNetworks()
if err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}, nil)
}
func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
network, err := gui.Panels.Networks.GetSelectedItem()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Network: network,
})
customCommands := gui.Config.UserConfig.CustomCommands.Networks
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleNetworksBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.PruneNetworks,
InternalFunction: gui.handlePruneNetworks,
},
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Networks...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -1,10 +1,9 @@
package gui package gui
import ( import (
"github.com/samber/lo" "github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
) )
func (gui *Gui) getBindings(v *gocui.View) []*Binding { func (gui *Gui) getBindings(v *gocui.View) []*Binding {
@ -41,6 +40,10 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
} }
} }
// } else if v.ParentView != nil && binding.ViewName == v.ParentView.Name() {
// // only add this if we don't have our own matching binding
// bindingsPanel = append(bindingsPanel, binding)
// append dummy element to have a separator between // append dummy element to have a separator between
// panel and global keybindings // panel and global keybindings
bindingsPanel = append(bindingsPanel, &Binding{}) bindingsPanel = append(bindingsPanel, &Binding{})
@ -48,26 +51,22 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
} }
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if gui.isPopupPanel(v.Name()) { if v.Name() == "menu" || v.Name() == "confirmation" {
return nil return nil
} }
menuItems := lo.Map(gui.getBindings(v), func(binding *Binding, _ int) *types.MenuItem { bindings := gui.getBindings(v)
return &types.MenuItem{
LabelColumns: []string{binding.GetKey(), binding.Description}, handleMenuPress := func(index int) error {
OnPress: func() error { if bindings[index].Key == nil {
if binding.Key == nil {
return nil return nil
} }
if index >= len(bindings) {
return binding.Handler(g, v) return errors.New("Index is greater than size of bindings")
},
} }
})
return gui.Menu(CreateMenuOptions{ return bindings[index].Handler(g, v)
Title: gui.Tr.MenuTitle, }
Items: menuItems,
HideCancel: true, return gui.createMenu(gui.Tr.MenuTitle, bindings, len(bindings), handleMenuPress)
})
} }

View file

@ -1,7 +0,0 @@
package gui
import "github.com/jesseduffield/lazydocker/pkg/gui/panels"
func (gui *Gui) intoInterface() panels.IGui {
return gui
}

View file

@ -1,69 +0,0 @@
package panels
import (
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/samber/lo"
)
// A 'context' generally corresponds to an item and the tab in the main panel that we're
// displaying. So if we switch to a new item, or change the tab in the panel panel
// for the current item, we end up with a new context. When we have a new context,
// we render new content to the main panel.
type ContextState[T any] struct {
// index of the currently selected tab in the main view.
mainTabIdx int
// this function returns the tabs that we can display for an item (the tabs
// are shown on the main view)
GetMainTabs func() []MainTab[T]
// This tells us whether we need to re-render to the main panel for a given item.
// This should include the item's ID and if you want to invalidate the cache for
// some other reason, you can add that to the key as well (e.g. the container's state).
GetItemContextCacheKey func(item T) string
}
type MainTab[T any] struct {
// key used as part of the context cache key
Key string
// title of the tab, rendered in the main view
Title string
// function to render the content of the tab
Render func(item T) tasks.TaskFunc
}
func (self *ContextState[T]) GetMainTabTitles() []string {
return lo.Map(self.GetMainTabs(), func(tab MainTab[T], _ int) string {
return tab.Title
})
}
func (self *ContextState[T]) GetCurrentContextKey(item T) string {
return self.GetItemContextCacheKey(item) + "-" + self.GetCurrentMainTab().Key
}
func (self *ContextState[T]) GetCurrentMainTab() MainTab[T] {
return self.GetMainTabs()[self.mainTabIdx]
}
func (self *ContextState[T]) HandleNextMainTab() {
tabs := self.GetMainTabs()
if len(tabs) == 0 {
return
}
self.mainTabIdx = (self.mainTabIdx + 1) % len(tabs)
}
func (self *ContextState[T]) HandlePrevMainTab() {
tabs := self.GetMainTabs()
if len(tabs) == 0 {
return
}
self.mainTabIdx = (self.mainTabIdx - 1 + len(tabs)) % len(tabs)
}
func (self *ContextState[T]) SetMainTabIndex(index int) {
self.mainTabIdx = index
}

View file

@ -1,111 +0,0 @@
package panels
import (
"sort"
"sync"
)
type FilteredList[T comparable] struct {
allItems []T
// indices of items in the allItems slice that are included in the filtered list
indices []int
mutex sync.RWMutex
}
func NewFilteredList[T comparable]() *FilteredList[T] {
return &FilteredList[T]{}
}
func (self *FilteredList[T]) SetItems(items []T) {
self.mutex.Lock()
defer self.mutex.Unlock()
self.allItems = items
self.indices = make([]int, len(items))
for i := range self.indices {
self.indices[i] = i
}
}
func (self *FilteredList[T]) Filter(filter func(T, int) bool) {
self.mutex.Lock()
defer self.mutex.Unlock()
self.indices = self.indices[:0]
for i, item := range self.allItems {
if filter(item, i) {
self.indices = append(self.indices, i)
}
}
}
func (self *FilteredList[T]) Sort(less func(T, T) bool) {
self.mutex.Lock()
defer self.mutex.Unlock()
if less == nil {
return
}
sort.Slice(self.indices, func(i, j int) bool {
return less(self.allItems[self.indices[i]], self.allItems[self.indices[j]])
})
}
func (self *FilteredList[T]) Get(index int) T {
self.mutex.RLock()
defer self.mutex.RUnlock()
return self.allItems[self.indices[index]]
}
func (self *FilteredList[T]) TryGet(index int) (T, bool) {
self.mutex.RLock()
defer self.mutex.RUnlock()
if index < 0 || index >= len(self.indices) {
var zero T
return zero, false
}
return self.allItems[self.indices[index]], true
}
// returns the length of the filtered list
func (self *FilteredList[T]) Len() int {
self.mutex.RLock()
defer self.mutex.RUnlock()
return len(self.indices)
}
func (self *FilteredList[T]) GetIndex(item T) int {
self.mutex.RLock()
defer self.mutex.RUnlock()
for i, index := range self.indices {
if self.allItems[index] == item {
return i
}
}
return -1
}
func (self *FilteredList[T]) GetItems() []T {
self.mutex.RLock()
defer self.mutex.RUnlock()
result := make([]T, len(self.indices))
for i, index := range self.indices {
result[i] = self.allItems[index]
}
return result
}
func (self *FilteredList[T]) GetAllItems() []T {
self.mutex.RLock()
defer self.mutex.RUnlock()
return self.allItems
}

View file

@ -1,183 +0,0 @@
package panels
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilteredListGet(t *testing.T) {
tests := []struct {
f *FilteredList[int]
args int
want int
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 1,
want: 2,
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 2,
want: 3,
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
args: 0,
want: 2,
},
}
for _, tt := range tests {
if got := tt.f.Get(tt.args); got != tt.want {
t.Errorf("FilteredList.Get() = %v, want %v", got, tt.want)
}
}
}
func TestFilteredListLen(t *testing.T) {
tests := []struct {
f *FilteredList[int]
want int
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
want: 3,
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
want: 1,
},
}
for _, tt := range tests {
if got := tt.f.Len(); got != tt.want {
t.Errorf("FilteredList.Len() = %v, want %v", got, tt.want)
}
}
}
func TestFilteredListFilter(t *testing.T) {
tests := []struct {
f *FilteredList[int]
args func(int, int) bool
want *FilteredList[int]
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, _ int) bool { return i%2 == 0 },
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, _ int) bool { return i%2 == 1 },
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 2}},
},
}
for _, tt := range tests {
tt.f.Filter(tt.args)
assert.EqualValues(t, tt.f.indices, tt.want.indices)
}
}
func TestFilteredListSort(t *testing.T) {
tests := []struct {
f *FilteredList[int]
args func(int, int) bool
want *FilteredList[int]
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, j int) bool { return i < j },
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: func(i int, j int) bool { return i > j },
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{2, 1, 0}},
},
}
for _, tt := range tests {
tt.f.Sort(tt.args)
assert.EqualValues(t, tt.f.indices, tt.want.indices)
}
}
func TestFilteredListGetIndex(t *testing.T) {
tests := []struct {
f *FilteredList[int]
args int
want int
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 1,
want: 0,
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: 2,
want: 1,
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
args: 0,
want: -1,
},
}
for _, tt := range tests {
if got := tt.f.GetIndex(tt.args); got != tt.want {
t.Errorf("FilteredList.GetIndex() = %v, want %v", got, tt.want)
}
}
}
func TestFilteredListGetItems(t *testing.T) {
tests := []struct {
f *FilteredList[int]
want []int
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
want: []int{1, 2, 3},
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
want: []int{2},
},
}
for _, tt := range tests {
got := tt.f.GetItems()
assert.EqualValues(t, got, tt.want)
}
}
func TestFilteredListSetItems(t *testing.T) {
tests := []struct {
f *FilteredList[int]
args []int
want *FilteredList[int]
}{
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
args: []int{4, 5, 6},
want: &FilteredList[int]{allItems: []int{4, 5, 6}, indices: []int{0, 1, 2}},
},
{
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
args: []int{4},
want: &FilteredList[int]{allItems: []int{4}, indices: []int{0}},
},
}
for _, tt := range tests {
tt.f.SetItems(tt.args)
assert.EqualValues(t, tt.f.indices, tt.want.indices)
assert.EqualValues(t, tt.f.allItems, tt.want.allItems)
}
}

View file

@ -1,42 +0,0 @@
package panels
import (
"github.com/jesseduffield/gocui"
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
)
type ListPanel[T comparable] struct {
SelectedIdx int
List *FilteredList[T]
View *gocui.View
}
func (self *ListPanel[T]) SetSelectedLineIdx(value int) {
clampedValue := 0
if self.List.Len() > 0 {
clampedValue = lcUtils.Clamp(value, 0, self.List.Len()-1)
}
self.SelectedIdx = clampedValue
}
func (self *ListPanel[T]) clampSelectedLineIdx() {
clamped := lcUtils.Clamp(self.SelectedIdx, 0, self.List.Len()-1)
if clamped != self.SelectedIdx {
self.SelectedIdx = clamped
}
}
// moves the cursor up or down by the given amount (up for negative values)
func (self *ListPanel[T]) moveSelectedLine(delta int) {
self.SetSelectedLineIdx(self.SelectedIdx + delta)
}
func (self *ListPanel[T]) SelectNextLine() {
self.moveSelectedLine(1)
}
func (self *ListPanel[T]) SelectPrevLine() {
self.moveSelectedLine(-1)
}

View file

@ -1,280 +0,0 @@
package panels
import (
"context"
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
)
type ISideListPanel interface {
SetMainTabIndex(int)
HandleSelect() error
GetView() *gocui.View
Refocus()
RerenderList() error
IsFilterDisabled() bool
IsHidden() bool
HandleNextLine() error
HandlePrevLine() error
HandleClick() error
HandlePrevMainTab() error
HandleNextMainTab() error
}
// list panel at the side of the screen that renders content to the main panel
type SideListPanel[T comparable] struct {
ContextState *ContextState[T]
ListPanel[T]
// message to render in the main view if there are no items in the panel
// and it has focus. Leave empty if you don't want to render anything
NoItemsMessage string
// a representation of the gui
Gui IGui
// this Filter is applied on top of additional default filters
Filter func(T) bool
Sort func(a, b T) bool
// a callback to invoke when the item is clicked
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
// be rendered with padding.
GetTableCells func(T) []string
// function to be called after re-rendering list. Can be nil
OnRerender func() error
// set this to true if you don't want to allow manual filtering via '/'
DisableFilter bool
// This can be nil if you want to always show the panel
Hide func() bool
}
var _ ISideListPanel = &SideListPanel[int]{}
type IGui interface {
HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error
NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc
FocusY(selectedLine int, itemCount int, view *gocui.View)
ShouldRefresh(contextKey string) bool
GetMainView() *gocui.View
IsCurrentView(*gocui.View) bool
FilterString(view *gocui.View) string
IgnoreStrings() []string
Update(func() error)
QueueTask(f func(ctx context.Context)) error
}
func (self *SideListPanel[T]) HandleClick() error {
itemCount := self.List.Len()
handleSelect := self.HandleSelect
selectedLine := &self.SelectedIdx
if err := self.Gui.HandleClick(self.View, itemCount, selectedLine, handleSelect); err != nil {
return err
}
if self.OnClick != nil {
selectedItem, err := self.GetSelectedItem()
if err == nil {
return self.OnClick(selectedItem)
}
}
return nil
}
func (self *SideListPanel[T]) GetView() *gocui.View {
return self.View
}
func (self *SideListPanel[T]) HandleSelect() error {
item, err := self.GetSelectedItem()
if err != nil {
if err.Error() != self.NoItemsMessage {
return err
}
if self.NoItemsMessage != "" {
self.Gui.NewSimpleRenderStringTask(func() string { return self.NoItemsMessage })
}
return nil
}
self.Refocus()
if self.OnSelect != nil {
if err := self.OnSelect(item); err != nil {
return err
}
}
return self.renderContext(item)
}
func (self *SideListPanel[T]) renderContext(item T) error {
if self.ContextState == nil {
return nil
}
key := self.ContextState.GetCurrentContextKey(item)
if !self.Gui.ShouldRefresh(key) {
return nil
}
mainView := self.Gui.GetMainView()
mainView.Tabs = self.ContextState.GetMainTabTitles()
mainView.TabIndex = self.ContextState.mainTabIdx
task := self.ContextState.GetCurrentMainTab().Render(item)
return self.Gui.QueueTask(task)
}
func (self *SideListPanel[T]) GetSelectedItem() (T, error) {
var zero T
item, ok := self.List.TryGet(self.SelectedIdx)
if !ok {
// could probably have a better error here
return zero, errors.New(self.NoItemsMessage)
}
return item, nil
}
func (self *SideListPanel[T]) HandleNextLine() error {
self.SelectNextLine()
return self.HandleSelect()
}
func (self *SideListPanel[T]) HandlePrevLine() error {
self.SelectPrevLine()
return self.HandleSelect()
}
func (self *SideListPanel[T]) HandleNextMainTab() error {
if self.ContextState == nil {
return nil
}
self.ContextState.HandleNextMainTab()
return self.HandleSelect()
}
func (self *SideListPanel[T]) HandlePrevMainTab() error {
if self.ContextState == nil {
return nil
}
self.ContextState.HandlePrevMainTab()
return self.HandleSelect()
}
func (self *SideListPanel[T]) Refocus() {
self.Gui.FocusY(self.SelectedIdx, self.List.Len(), self.View)
}
func (self *SideListPanel[T]) SetItems(items []T) {
self.List.SetItems(items)
self.FilterAndSort()
}
func (self *SideListPanel[T]) FilterAndSort() {
filterString := self.Gui.FilterString(self.View)
self.List.Filter(func(item T, index int) bool {
if self.Filter != nil && !self.Filter(item) {
return false
}
if lo.SomeBy(self.Gui.IgnoreStrings(), func(ignore string) bool {
return lo.SomeBy(self.GetTableCells(item), func(searchString string) bool {
return strings.Contains(searchString, ignore)
})
}) {
return false
}
if filterString != "" {
return lo.SomeBy(self.GetTableCells(item), func(searchString string) bool {
return strings.Contains(searchString, filterString)
})
}
return true
})
self.List.Sort(self.Sort)
self.clampSelectedLineIdx()
}
func (self *SideListPanel[T]) RerenderList() error {
self.FilterAndSort()
self.Gui.Update(func() error {
self.View.Clear()
table := lo.Map(self.List.GetItems(), func(item T, index int) []string {
return self.GetTableCells(item)
})
renderedTable, err := utils.RenderTable(table)
if err != nil {
return err
}
fmt.Fprint(self.View, renderedTable)
if self.OnRerender != nil {
if err := self.OnRerender(); err != nil {
return err
}
}
if self.Gui.IsCurrentView(self.View) {
return self.HandleSelect()
}
return nil
})
return nil
}
func (self *SideListPanel[T]) SetMainTabIndex(index int) {
if self.ContextState == nil {
return
}
self.ContextState.SetMainTabIndex(index)
}
func (self *SideListPanel[T]) IsFilterDisabled() bool {
return self.DisableFilter
}
func (self *SideListPanel[T]) IsHidden() bool {
if self.Hide == nil {
return false
}
return self.Hide()
}

View file

@ -1,146 +0,0 @@
package presentation
import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/asciigraph"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/mcuadros/go-lookup"
"github.com/samber/lo"
)
func RenderStats(userConfig *config.UserConfig, container *commands.Container, viewWidth int) (string, error) {
stats, ok := container.GetLastStats()
if !ok {
return "", nil
}
graphSpecs := userConfig.Stats.Graphs
graphs := make([]string, len(graphSpecs))
for i, spec := range graphSpecs {
graph, err := plotGraph(container, spec, viewWidth-10)
if err != nil {
return "", err
}
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
}
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
originalStats, err := utils.MarshalIntoYaml(stats)
if err != nil {
return "", err
}
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
pidsCount,
dataReceived,
dataSent,
utils.ColoredYamlString(string(originalStats)),
)
return contents, nil
}
// plotGraph returns the plotted graph based on the graph spec and the stat history
func plotGraph(container *commands.Container, spec config.GraphConfig, width int) (string, error) {
container.StatsMutex.Lock()
defer container.StatsMutex.Unlock()
data := make([]float64, len(container.StatHistory))
for i, stats := range container.StatHistory {
value, err := lookup.LookupString(stats, spec.StatPath)
if err != nil {
return "Could not find key: " + spec.StatPath, nil
}
floatValue, err := getFloat(value.Interface())
if err != nil {
return "", err
}
data[i] = floatValue
}
max := spec.Max
if spec.MaxType == "" {
max = lo.Max(data)
}
min := spec.Min
if spec.MinType == "" {
min = lo.Min(data)
}
height := 10
if spec.Height > 0 {
height = spec.Height
}
caption := fmt.Sprintf(
"%s: %0.2f (%v)",
spec.Caption,
data[len(data)-1],
time.Since(container.StatHistory[0].RecordedAt).Round(time.Second),
)
return asciigraph.Plot(
data,
asciigraph.Height(height),
asciigraph.Width(width),
asciigraph.Min(min),
asciigraph.Max(max),
asciigraph.Caption(caption),
), nil
}
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
func getFloat(unk interface{}) (float64, error) {
floatType := reflect.TypeOf(float64(0))
stringType := reflect.TypeOf("")
switch i := unk.(type) {
case float64:
return i, nil
case float32:
return float64(i), nil
case int64:
return float64(i), nil
case int32:
return float64(i), nil
case int:
return float64(i), nil
case uint64:
return float64(i), nil
case uint32:
return float64(i), nil
case uint:
return float64(i), nil
case string:
return strconv.ParseFloat(i, 64)
default:
v := reflect.ValueOf(unk)
v = reflect.Indirect(v)
if v.Type().ConvertibleTo(floatType) {
fv := v.Convert(floatType)
return fv.Float(), nil
} else if v.Type().ConvertibleTo(stringType) {
sv := v.Convert(stringType)
s := sv.String()
return strconv.ParseFloat(s, 64)
} else {
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
}
}
}

View file

@ -1,202 +0,0 @@
package presentation
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
)
func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container) []string {
return []string{
getContainerDisplayStatus(guiConfig, container),
getContainerDisplaySubstatus(guiConfig, container),
container.Name,
getDisplayCPUPerc(container),
utils.ColoredString(displayPorts(container), color.FgYellow),
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
}
}
func displayContainerImage(container *commands.Container) string {
return strings.TrimPrefix(container.Container.Image, "sha256:")
}
func displayPorts(c *commands.Container) string {
portStrings := lo.Map(c.Container.Ports, func(port container.Port, _ int) string {
if port.PublicPort == 0 {
return fmt.Sprintf("%d/%s", port.PrivatePort, port.Type)
}
// docker ps will show '0.0.0.0:80->80/tcp' but we'll show
// '80->80/tcp' instead to save space (unless the IP is something other than
// 0.0.0.0)
ipString := ""
if port.IP != "0.0.0.0" {
ipString = port.IP + ":"
}
return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type)
})
// sorting because the order of the ports is not deterministic
// and we don't want to have them constantly swapping
sort.Strings(portStrings)
return strings.Join(portStrings, ", ")
}
// getContainerDisplayStatus returns the colored status of the container
func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
shortStatusMap := map[string]string{
"paused": "P",
"exited": "X",
"created": "C",
"removing": "RM",
"restarting": "RS",
"running": "R",
"dead": "D",
}
iconStatusMap := map[string]rune{
"paused": '◫',
"exited": '',
"created": '+',
"removing": '',
"restarting": '⟳',
"running": '▶',
"dead": '!',
}
var containerState string
switch guiConfig.ContainerStatusHealthStyle {
case "short":
containerState = shortStatusMap[c.Container.State]
case "icon":
containerState = string(iconStatusMap[c.Container.State])
case "long":
fallthrough
default:
containerState = c.Container.State
}
return utils.ColoredString(containerState, getContainerColor(c))
}
// GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check)
func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Container) string {
if !c.DetailsLoaded() {
return ""
}
switch c.Container.State {
case "exited":
return utils.ColoredString(
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c),
)
case "running":
return getHealthStatus(guiConfig, c)
default:
return ""
}
}
func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
if !c.DetailsLoaded() {
return ""
}
healthStatusColorMap := map[string]color.Attribute{
"healthy": color.FgGreen,
"unhealthy": color.FgRed,
"starting": color.FgYellow,
}
if c.Details.State.Health == nil {
return ""
}
shortHealthStatusMap := map[string]string{
"healthy": "H",
"unhealthy": "U",
"starting": "S",
}
iconHealthStatusMap := map[string]rune{
"healthy": '✔',
"unhealthy": '?',
"starting": '…',
}
var healthStatus string
switch guiConfig.ContainerStatusHealthStyle {
case "short":
healthStatus = shortHealthStatusMap[c.Details.State.Health.Status]
case "icon":
healthStatus = string(iconHealthStatusMap[c.Details.State.Health.Status])
case "long":
fallthrough
default:
healthStatus = c.Details.State.Health.Status
}
if healthStatusColor, ok := healthStatusColorMap[c.Details.State.Health.Status]; ok {
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
}
return ""
}
// getDisplayCPUPerc colors the cpu percentage based on how extreme it is
func getDisplayCPUPerc(c *commands.Container) string {
stats, ok := c.GetLastStats()
if !ok {
return ""
}
percentage := stats.DerivedStats.CPUPercentage
formattedPercentage := fmt.Sprintf("%.2f%%", stats.DerivedStats.CPUPercentage)
var clr color.Attribute
if percentage > 90 {
clr = color.FgRed
} else if percentage > 50 {
clr = color.FgYellow
} else {
clr = color.FgWhite
}
return utils.ColoredString(formattedPercentage, clr)
}
// getContainerColor Container color
func getContainerColor(c *commands.Container) color.Attribute {
switch c.Container.State {
case "exited":
// This means the colour may be briefly yellow and then switch to red upon starting
// Not sure what a better alternative is.
if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 {
return color.FgYellow
}
return color.FgRed
case "created":
return color.FgCyan
case "running":
return color.FgGreen
case "paused":
return color.FgYellow
case "dead":
return color.FgRed
case "restarting":
return color.FgBlue
case "removing":
return color.FgMagenta
default:
return color.FgWhite
}
}

View file

@ -1,14 +0,0 @@
package presentation
import (
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
func GetImageDisplayStrings(image *commands.Image) []string {
return []string{
image.Name,
image.Tag,
utils.FormatDecimalBytes(int(image.Image.Size)),
}
}

View file

@ -1,7 +0,0 @@
package presentation
import "github.com/jesseduffield/lazydocker/pkg/gui/types"
func GetMenuItemDisplayStrings(menuItem *types.MenuItem) []string {
return menuItem.LabelColumns
}

View file

@ -1,7 +0,0 @@
package presentation
import "github.com/jesseduffield/lazydocker/pkg/commands"
func GetNetworkDisplayStrings(network *commands.Network) []string {
return []string{network.Network.Driver, network.Name}
}

View file

@ -1,7 +0,0 @@
package presentation
import "github.com/jesseduffield/lazydocker/pkg/commands"
func GetProjectDisplayStrings(project *commands.Project) []string {
return []string{project.Name}
}

View file

@ -1,43 +0,0 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
func GetServiceDisplayStrings(guiConfig *config.GuiConfig, service *commands.Service) []string {
if service.Container == nil {
var containerState string
switch guiConfig.ContainerStatusHealthStyle {
case "short":
containerState = "n"
case "icon":
containerState = "."
case "long":
fallthrough
default:
containerState = "none"
}
return []string{
utils.ColoredString(containerState, color.FgBlue),
"",
service.Name,
"",
"",
"",
}
}
container := service.Container
return []string{
getContainerDisplayStatus(guiConfig, container),
getContainerDisplaySubstatus(guiConfig, container),
service.Name,
getDisplayCPUPerc(container),
utils.ColoredString(displayPorts(container), color.FgYellow),
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
}
}

View file

@ -1,7 +0,0 @@
package presentation
import "github.com/jesseduffield/lazydocker/pkg/commands"
func GetVolumeDisplayStrings(volume *commands.Volume) []string {
return []string{volume.Volume.Driver, volume.Name}
}

View file

@ -2,207 +2,171 @@ package gui
import ( import (
"bytes" "bytes"
"context" "fmt"
"path"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/yaml" "github.com/jesseduffield/yaml"
) )
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] { func (gui *Gui) getProjectContexts() []string {
return &panels.SideListPanel[*commands.Project]{ if gui.DockerCommand.InDockerComposeProject {
ContextState: &panels.ContextState[*commands.Project]{ return []string{"logs", "config", "credits"}
GetMainTabs: func() []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",
Title: gui.Tr.CreditsTitle,
Render: gui.renderCredits,
},
}
},
GetItemContextCacheKey: func(project *commands.Project) string {
return "projects-" + project.Name
},
},
ListPanel: panels.ListPanel[*commands.Project]{
List: panels.NewFilteredList[*commands.Project](),
View: gui.Views.Project,
},
NoItemsMessage: "",
Gui: gui.intoInterface(),
Sort: func(a *commands.Project, b *commands.Project) bool {
return a.Name < b.Name
},
GetTableCells: presentation.GetProjectDisplayStrings,
OnSelect: func(project *commands.Project) error {
// 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()
},
} }
return []string{"credits"}
} }
func (gui *Gui) refreshProject() error { func (gui *Gui) getProjectContextTitles() []string {
projects := gui.getDiscoveredProjects() if gui.DockerCommand.InDockerComposeProject {
return []string{gui.Tr.LogsTitle, gui.Tr.DockerComposeConfigTitle, gui.Tr.CreditsTitle}
// 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
} }
} return []string{gui.Tr.CreditsTitle}
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()
} }
// getDiscoveredProjects returns all docker compose projects by examining container labels. func (gui *Gui) refreshProject() {
// The local project (from docker-compose.yml in the current directory, or from -p) is v := gui.getProjectView()
// 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 projectName := gui.getProjectName()
// 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 != "" { gui.g.Update(func(*gocui.Gui) error {
found := false v.Clear()
for _, name := range projectNames { fmt.Fprint(v, projectName)
if name == localName { return nil
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, func (gui *Gui) getProjectName() string {
// or empty string if none is selected. projectName := path.Base(gui.Config.ProjectDir)
func (gui *Gui) getSelectedProjectName() string { if gui.DockerCommand.InDockerComposeProject {
project, err := gui.Panels.Projects.GetSelectedItem() for _, service := range gui.DockerCommand.Services {
if err != nil { container := service.Container
return "" if container != nil && container.DetailsLoaded() {
return container.Details.Config.Labels["com.docker.compose.project"]
} }
return project.Name }
}
return projectName
} }
func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc { func (gui *Gui) handleProjectClick(g *gocui.Gui, v *gocui.View) error {
return gui.NewSimpleRenderStringTask(func() string { return gui.creditsStr() }) if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
return gui.handleProjectSelect(g, v)
} }
func (gui *Gui) creditsStr() string { func (gui *Gui) handleProjectSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
key := gui.getProjectContexts()[gui.State.Panels.Project.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
gui.clearMainView()
mainView := gui.getMainView()
mainView.Tabs = gui.getProjectContextTitles()
mainView.TabIndex = gui.State.Panels.Project.ContextIndex
switch gui.getProjectContexts()[gui.State.Panels.Project.ContextIndex] {
case "credits":
if err := gui.renderCredits(); err != nil {
return err
}
case "logs":
if err := gui.renderAllLogs(); err != nil {
return err
}
case "config":
if err := gui.renderDockerComposeConfig(); err != nil {
return err
}
default:
return errors.New("Unknown context for status panel")
}
return nil
}
func (gui *Gui) renderCredits() error {
return gui.T.NewTask(func(stop chan struct{}) {
mainView := gui.getMainView()
mainView.Autoscroll = false
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
var configBuf bytes.Buffer var configBuf bytes.Buffer
_ = yaml.NewEncoder(&configBuf, yaml.IncludeOmitted).Encode(gui.Config.UserConfig) _ = yaml.NewEncoder(&configBuf, yaml.IncludeOmitted).Encode(gui.Config.UserConfig)
return strings.Join( dashboardString := strings.Join(
[]string{ []string{
lazydockerTitle(), lazydockerTitle(),
"Copyright (c) 2019 Jesse Duffield", "Copyright (c) 2019 Jesse Duffield",
"Keybindings: https://github.com/jesseduffield/lazydocker/blob/master/docs/keybindings", "Keybindings: https://github.com/jesseduffield/lazydocker/blob/master/docs/keybindings",
"Config Options: https://github.com/jesseduffield/lazydocker/blob/master/docs/Config.md", "Config Options: https://github.com/jesseduffield/lazydocker/blob/master/docs/Config.md",
"Raise an Issue: https://github.com/jesseduffield/lazydocker/issues", "Raise an Issue: https://github.com/jesseduffield/lazydocker/issues",
utils.ColoredString("Buy Jesse a coffee: https://github.com/sponsors/jesseduffield", color.FgMagenta), // caffeine ain't free utils.ColoredString("Buy Jesse a coffee: https://donorbox.org/lazydocker", color.FgMagenta), // caffeine ain't free
"Here's your lazydocker config when merged in with the defaults (you can open your config by pressing 'o'):", "Here's your lazydocker config when merged in with the defaults (you can open your config by pressing 'o'):",
utils.ColoredYamlString(configBuf.String()), configBuf.String(),
}, "\n\n") }, "\n\n")
_ = gui.renderString(gui.g, "main", dashboardString)
})
} }
func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc { func (gui *Gui) renderAllLogs() error {
return gui.NewTask(TaskOpts{ return gui.T.NewTask(func(stop chan struct{}) {
Autoscroll: true, mainView := gui.getMainView()
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, mainView.Autoscroll = true
Func: func(ctx context.Context) { mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
gui.clearMainView() gui.clearMainView()
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{}),
), ),
) )
cmd.Stdout = gui.Views.Main cmd.Stdout = mainView
cmd.Stderr = gui.Views.Main cmd.Stderr = mainView
gui.OSCommand.PrepareForChildren(cmd) gui.OSCommand.PrepareForChildren(cmd)
_ = cmd.Start() _ = cmd.Start()
go func() { go func() {
<-ctx.Done() <-stop
if err := gui.OSCommand.Kill(cmd); err != nil { if err := gui.OSCommand.Kill(cmd); err != nil {
gui.Log.Error(err) gui.Log.Error(err)
} }
}() }()
_ = cmd.Wait() _ = cmd.Wait()
},
}) })
} }
func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc { func (gui *Gui) renderDockerComposeConfig() error {
if !gui.DockerCommand.InDockerComposeProject { return gui.T.NewTask(func(stop chan struct{}) {
return gui.NewSimpleRenderStringTask(func() string { mainView := gui.getMainView()
return "Compose config is only available when launched from a docker-compose project directory" mainView.Autoscroll = false
}) mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
}
if project != nil && project.Name != gui.DockerCommand.LocalProjectName { config := gui.DockerCommand.DockerComposeConfig()
return gui.NewSimpleRenderStringTask(func() string { _ = gui.renderString(gui.g, "main", config)
return "Compose config is not available for non-local projects"
})
}
return gui.NewSimpleRenderStringTask(func() string {
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfigForProject(project))
}) })
} }
@ -227,10 +191,35 @@ func lazydockerTitle() string {
` `
} }
func (gui *Gui) handleProjectNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getProjectContexts()
if gui.State.Panels.Project.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Project.ContextIndex = 0
} else {
gui.State.Panels.Project.ContextIndex++
}
_ = gui.handleProjectSelect(gui.g, v)
return nil
}
func (gui *Gui) handleProjectPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getProjectContexts()
if gui.State.Panels.Project.ContextIndex <= 0 {
gui.State.Panels.Project.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Project.ContextIndex--
}
_ = gui.handleProjectSelect(gui.g, v)
return nil
}
// 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())
} }

View file

@ -1,177 +1,206 @@
package gui package gui
import ( import (
"context"
"fmt" "fmt"
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
) )
func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] { // list panel functions
return &panels.SideListPanel[*commands.Service]{
ContextState: &panels.ContextState[*commands.Service]{
GetMainTabs: func() []panels.MainTab[*commands.Service] {
return []panels.MainTab[*commands.Service]{
{
Key: "logs",
Title: gui.Tr.LogsTitle,
Render: gui.renderServiceLogs,
},
{
Key: "stats",
Title: gui.Tr.StatsTitle,
Render: gui.renderServiceStats,
},
{
Key: "container-env",
Title: gui.Tr.ContainerEnvTitle,
Render: gui.renderServiceContainerEnv,
},
{
Key: "container-config",
Title: gui.Tr.ContainerConfigTitle,
Render: gui.renderServiceContainerConfig,
},
{
Key: "top",
Title: gui.Tr.TopTitle,
Render: gui.renderServiceTop,
},
}
},
GetItemContextCacheKey: func(service *commands.Service) string {
if service.Container == nil {
return "services-" + service.ID
}
return "services-" + service.ID + "-" + service.Container.ID + "-" + service.Container.Container.State
},
},
ListPanel: panels.ListPanel[*commands.Service]{
List: panels.NewFilteredList[*commands.Service](),
View: gui.Views.Services,
},
NoItemsMessage: gui.Tr.NoServices,
Gui: gui.intoInterface(),
// sort services first by whether they have a linked container, and second by alphabetical order
Sort: func(a *commands.Service, b *commands.Service) bool {
if a.Container != nil && b.Container == nil {
return true
}
if a.Container == nil && b.Container != nil { func (gui *Gui) getServiceContexts() []string {
return false return []string{"logs", "stats", "container-env", "container-config", "top"}
} }
return a.Name < b.Name func (gui *Gui) getServiceContextTitles() []string {
}, return []string{
Filter: func(service *commands.Service) bool { gui.Tr.LogsTitle,
selectedProject := gui.getSelectedProjectName() gui.Tr.StatsTitle,
if selectedProject == "" { gui.Tr.ContainerEnvTitle,
// Before any project is selected (e.g. startup), default to gui.Tr.ContainerConfigTitle,
// the local project so we don't briefly flash all services. gui.Tr.TopTitle,
selectedProject = gui.DockerCommand.LocalProjectName
}
if selectedProject == "" {
return true
}
return service.ProjectName == selectedProject
},
GetTableCells: func(service *commands.Service) []string {
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
},
Hide: func() bool {
return !gui.DockerCommand.IsProjectScoped()
},
} }
} }
func (gui *Gui) renderServiceContainerConfig(service *commands.Service) tasks.TaskFunc { func (gui *Gui) getSelectedService() (*commands.Service, error) {
if service.Container == nil { selectedLine := gui.State.Panels.Services.SelectedLine
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer }) if selectedLine == -1 {
return &commands.Service{}, errors.New("no service selected")
} }
return gui.renderContainerConfig(service.Container) return gui.DockerCommand.Services[selectedLine], nil
} }
func (gui *Gui) renderServiceContainerEnv(service *commands.Service) tasks.TaskFunc { func (gui *Gui) handleServicesClick(g *gocui.Gui, v *gocui.View) error {
if service.Container == nil { itemCount := len(gui.DockerCommand.Services)
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer }) handleSelect := gui.handleServiceSelect
selectedLine := &gui.State.Panels.Services.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
} }
return gui.renderContainerEnv(service.Container) containerID := ""
if service.Container != nil {
containerID = service.Container.ID
}
gui.focusY(gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v)
key := "services-" + service.ID + "-" + containerID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
mainView := gui.getMainView()
mainView.Tabs = gui.getServiceContextTitles()
mainView.TabIndex = gui.State.Panels.Services.ContextIndex
switch gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] {
case "logs":
if err := gui.renderServiceLogs(service); err != nil {
return err
}
case "stats":
if err := gui.renderServiceStats(service); err != nil {
return err
}
case "container-env":
if service.Container == nil {
return gui.renderString(gui.g, "main", gui.Tr.NoContainer)
}
if err := gui.renderContainerEnv(service.Container); err != nil {
return err
}
case "container-config":
if service.Container == nil {
return gui.renderString(gui.g, "main", gui.Tr.NoContainer)
}
if err := gui.renderContainerConfig(service.Container); err != nil {
return err
}
case "top":
if err := gui.renderServiceTop(service); err != nil {
return err
}
default:
return errors.New("Unknown context for services panel")
}
return nil
} }
func (gui *Gui) renderServiceStats(service *commands.Service) tasks.TaskFunc { func (gui *Gui) renderServiceStats(service *commands.Service) error {
if service.Container == nil { if service.Container == nil {
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer }) return nil
} }
return gui.renderContainerStats(service.Container) return gui.renderContainerStats(service.Container)
} }
func (gui *Gui) renderServiceTop(service *commands.Service) tasks.TaskFunc { func (gui *Gui) renderServiceTop(service *commands.Service) error {
return gui.NewTickerTask(TickerTaskOpts{ mainView := gui.getMainView()
Func: func(ctx context.Context, notifyStopped chan struct{}) { mainView.Autoscroll = false
contents, err := service.RenderTop(ctx) mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
contents, err := service.RenderTop()
if err != nil { if err != nil {
gui.RenderStringMain(err.Error()) gui.reRenderStringMain(err.Error())
} }
gui.reRenderStringMain(contents) gui.reRenderStringMain(contents)
},
Duration: time.Second,
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: false,
}) })
} }
func (gui *Gui) renderServiceLogs(service *commands.Service) tasks.TaskFunc { func (gui *Gui) renderServiceLogs(service *commands.Service) error {
if service.Container == nil { if service.Container == nil {
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainerForService }) return gui.T.NewTask(func(stop chan struct{}) {
gui.clearMainView()
})
} }
return gui.renderContainerLogsToMain(service.Container) return gui.renderContainerLogsToMain(service.Container)
} }
func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
panelState := gui.State.Panels.Services
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), false)
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServicesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
panelState := gui.State.Panels.Services
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), true)
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServicesNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getServiceContexts()
if gui.State.Panels.Services.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Services.ContextIndex = 0
} else {
gui.State.Panels.Services.ContextIndex++
}
_ = gui.handleServiceSelect(gui.g, v)
return nil
}
func (gui *Gui) handleServicesPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getServiceContexts()
if gui.State.Panels.Services.ContextIndex <= 0 {
gui.State.Panels.Services.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Services.ContextIndex--
}
_ = gui.handleServiceSelect(gui.g, v)
return nil
}
type commandOption struct { type commandOption struct {
description string description string
command string command string
onPress func() error f func() error
} }
func (r *commandOption) getDisplayStrings() []string { // GetDisplayStrings is a function.
func (r *commandOption) GetDisplayStrings(isFocused bool) []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.getSelectedService()
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{
{ {
@ -182,31 +211,16 @@ func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.RemoveWithVolumes, description: gui.Tr.RemoveWithVolumes,
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name), command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
}, },
} {
description: gui.Tr.Cancel,
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: option.getDisplayStrings(),
OnPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := gui.OSCommand.RunCommand(option.command); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}, },
} }
})
return gui.Menu(CreateMenuOptions{ return gui.createServiceCommandMenu(options, gui.Tr.RemovingStatus)
Title: "",
Items: menuItems,
})
} }
func (gui *Gui) handleServicePause(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServicePause(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
@ -218,91 +232,54 @@ func (gui *Gui) handleServicePause(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
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)
} }
func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if !gui.isServiceFromLocalProject(service) {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
if err := service.Up(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
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
}) })
} }
func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
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
}) })
} }
func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
@ -320,7 +297,7 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
@ -333,91 +310,12 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
return gui.runSubprocess(c) return gui.runSubprocess(c)
} }
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 {
cmdStr := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Up,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
)
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}, nil)
}
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(
gui.Config.UserConfig.CommandTemplates.Down,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
)
downWithVolumesCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
)
options := []*commandOption{
{
description: gui.Tr.Down,
command: downCommand,
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
if err := gui.OSCommand.RunCommand(downCommand); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
},
{
description: gui.Tr.DownWithVolumes,
command: downWithVolumesCommand,
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
},
}
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: option.getDisplayStrings(),
OnPress: option.onPress,
}
})
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
}
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
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}),
@ -435,7 +333,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
gui.Config.UserConfig.CommandTemplates.RestartService, gui.Config.UserConfig.CommandTemplates.RestartService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
onPress: func() error { f: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := service.Restart(); err != nil { if err := service.Restart(); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
@ -450,7 +348,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
gui.Config.UserConfig.CommandTemplates.RecreateService, gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
onPress: func() error { f: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil { if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
@ -465,27 +363,40 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
gui.Config.UserConfig.CommandTemplates.RebuildService, gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
onPress: func() error { f: func() error {
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand)) return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
}, },
}, },
{
description: gui.Tr.Cancel,
f: func() error { return nil },
},
} }
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem { handleMenuPress := func(index int) error { return options[index].f() }
return &types.MenuItem{
LabelColumns: option.getDisplayStrings(),
OnPress: option.onPress,
}
})
return gui.Menu(CreateMenuOptions{ return gui.createMenu("", options, len(options), handleMenuPress)
Title: "", }
Items: menuItems,
func (gui *Gui) createServiceCommandMenu(options []*commandOption, status string) error {
handleMenuPress := func(index int) error {
if options[index].command == "" {
return nil
}
return gui.WithWaitingStatus(status, func() error {
if err := gui.OSCommand.RunCommand(options[index].command); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
}) })
}
return gui.createMenu("", options, len(options), handleMenuPress)
} }
func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
@ -522,15 +433,14 @@ 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)
} }
func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }
@ -544,7 +454,7 @@ func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem() service, err := gui.getSelectedService()
if err != nil { if err != nil {
return nil return nil
} }

View file

@ -1,148 +0,0 @@
package gui
import (
"sort"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/stretchr/testify/assert"
)
func sampleContainers() []*commands.Container {
return []*commands.Container{
{
ID: "1",
Name: "1",
Container: container.Summary{
State: "exited",
},
},
{
ID: "2",
Name: "2",
Container: container.Summary{
State: "running",
},
},
{
ID: "3",
Name: "3",
Container: container.Summary{
State: "running",
},
},
{
ID: "4",
Name: "4",
Container: container.Summary{
State: "created",
},
},
}
}
func expectedPerStatusContainers() []*commands.Container {
return []*commands.Container{
{
ID: "2",
Name: "2",
Container: container.Summary{
State: "running",
},
},
{
ID: "3",
Name: "3",
Container: container.Summary{
State: "running",
},
},
{
ID: "1",
Name: "1",
Container: container.Summary{
State: "exited",
},
},
{
ID: "4",
Name: "4",
Container: container.Summary{
State: "created",
},
},
}
}
func expectedLegacySortedContainers() []*commands.Container {
return []*commands.Container{
{
ID: "1",
Name: "1",
Container: container.Summary{
State: "exited",
},
},
{
ID: "2",
Name: "2",
Container: container.Summary{
State: "running",
},
},
{
ID: "3",
Name: "3",
Container: container.Summary{
State: "running",
},
},
{
ID: "4",
Name: "4",
Container: container.Summary{
State: "created",
},
},
}
}
func assertEqualContainers(t *testing.T, left *commands.Container, right *commands.Container) {
t.Helper()
assert.Equal(t, left.Container.State, right.Container.State)
assert.Equal(t, left.Container.ID, right.Container.ID)
assert.Equal(t, left.Name, right.Name)
}
func TestSortContainers(t *testing.T) {
actual := sampleContainers()
expected := expectedPerStatusContainers()
sort.Slice(actual, func(i, j int) bool {
return sortContainers(actual[i], actual[j], false)
})
assert.Equal(t, len(actual), len(expected))
for i := 0; i < len(actual); i++ {
assertEqualContainers(t, expected[i], actual[i])
}
}
func TestLegacySortedContainers(t *testing.T) {
actual := sampleContainers()
expected := expectedLegacySortedContainers()
sort.Slice(actual, func(i, j int) bool {
return sortContainers(actual[i], actual[j], true)
})
assert.Equal(t, len(actual), len(expected))
for i := 0; i < len(actual); i++ {
assertEqualContainers(t, expected[i], actual[i])
}
}

View file

@ -2,7 +2,7 @@ package gui
import ( import (
"fmt" "fmt"
"io" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
@ -13,10 +13,6 @@ import (
) )
func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { func (gui *Gui) runSubprocess(cmd *exec.Cmd) error {
return gui.runSubprocessWithMessage(cmd, "")
}
func (gui *Gui) runSubprocessWithMessage(cmd *exec.Cmd, msg string) error {
gui.Mutexes.SubprocessMutex.Lock() gui.Mutexes.SubprocessMutex.Lock()
defer gui.Mutexes.SubprocessMutex.Unlock() defer gui.Mutexes.SubprocessMutex.Unlock()
@ -26,7 +22,7 @@ func (gui *Gui) runSubprocessWithMessage(cmd *exec.Cmd, msg string) error {
gui.PauseBackgroundThreads = true gui.PauseBackgroundThreads = true
gui.runCommand(cmd, msg) gui.runCommand(cmd)
if err := gui.g.Resume(); err != nil { if err := gui.g.Resume(); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
@ -37,7 +33,7 @@ func (gui *Gui) runSubprocessWithMessage(cmd *exec.Cmd, msg string) error {
return nil return nil
} }
func (gui *Gui) runCommand(cmd *exec.Cmd, msg string) { func (gui *Gui) runCommand(cmd *exec.Cmd) {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout cmd.Stderr = os.Stdout
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
@ -55,9 +51,7 @@ func (gui *Gui) runCommand(cmd *exec.Cmd, msg string) {
}() }()
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(cmd.Args, " "), color.FgBlue)) fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(cmd.Args, " "), color.FgBlue))
if msg != "" {
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString(msg, color.FgGreen))
}
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
// not handling the error explicitly because usually we're going to see it // not handling the error explicitly because usually we're going to see it
// in the output anyway // in the output anyway
@ -65,8 +59,8 @@ func (gui *Gui) runCommand(cmd *exec.Cmd, msg string) {
} }
cmd.Stdin = nil cmd.Stdin = nil
cmd.Stdout = io.Discard cmd.Stdout = ioutil.Discard
cmd.Stderr = io.Discard cmd.Stderr = ioutil.Discard
gui.promptToReturn() gui.promptToReturn()
} }

View file

@ -1,101 +0,0 @@
package gui
import (
"context"
"time"
"github.com/jesseduffield/lazydocker/pkg/tasks"
)
func (gui *Gui) QueueTask(f func(ctx context.Context)) error {
return gui.taskManager.NewTask(f)
}
type RenderStringTaskOpts struct {
Autoscroll bool
Wrap bool
GetStrContent func() string
}
type TaskOpts struct {
Autoscroll bool
Wrap bool
Func func(ctx context.Context)
}
type TickerTaskOpts struct {
Duration time.Duration
Before func(ctx context.Context)
Func func(ctx context.Context, notifyStopped chan struct{})
Autoscroll bool
Wrap bool
}
func (gui *Gui) NewRenderStringTask(opts RenderStringTaskOpts) tasks.TaskFunc {
taskOpts := TaskOpts{
Autoscroll: opts.Autoscroll,
Wrap: opts.Wrap,
Func: func(ctx context.Context) {
gui.RenderStringMain(opts.GetStrContent())
},
}
return gui.NewTask(taskOpts)
}
// assumes it's cheap to obtain the content (otherwise we would pass a function that returns the content)
func (gui *Gui) NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc {
return gui.NewRenderStringTask(RenderStringTaskOpts{
GetStrContent: getContent,
Autoscroll: false,
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
})
}
func (gui *Gui) NewTask(opts TaskOpts) tasks.TaskFunc {
return func(ctx context.Context) {
mainView := gui.Views.Main
mainView.Autoscroll = opts.Autoscroll
mainView.Wrap = opts.Wrap
opts.Func(ctx)
}
}
// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second
// the before function gets called after the lock is obtained, but before the ticker starts.
// if you handle a message on the stop channel in f() you need to send a message on the notifyStopped channel because returning is not sufficient. Here, unlike in a regular task, simply returning means we're now going to wait till the next tick to run again.
func (gui *Gui) NewTickerTask(opts TickerTaskOpts) tasks.TaskFunc {
notifyStopped := make(chan struct{}, 10)
task := func(ctx context.Context) {
if opts.Before != nil {
opts.Before(ctx)
}
tickChan := time.NewTicker(opts.Duration)
defer tickChan.Stop()
// calling f first so that we're not waiting for the first tick
opts.Func(ctx, notifyStopped)
for {
select {
case <-notifyStopped:
gui.Log.Info("exiting ticker task due to notifyStopped channel")
return
case <-ctx.Done():
gui.Log.Info("exiting ticker task due to stopped channel")
return
case <-tickChan.C:
gui.Log.Info("running ticker task again")
opts.Func(ctx, notifyStopped)
}
}
}
taskOpts := TaskOpts{
Autoscroll: opts.Autoscroll,
Wrap: opts.Wrap,
Func: task,
}
return gui.NewTask(taskOpts)
}

View file

@ -1,13 +0,0 @@
package types
type MenuItem struct {
Label string
// alternative to Label. Allows specifying columns which will be auto-aligned
LabelColumns []string
OnPress func() error
// Only applies when Label is used
OpensMenu bool
}

View file

@ -6,32 +6,23 @@ import (
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/spkg/bom" "github.com/spkg/bom"
) )
func (gui *Gui) handleGoTo(view *gocui.View) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
gui.resetMainView()
return gui.switchFocus(view)
}
}
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
sideViewNames := gui.sideViewNames()
var focusedViewName string var focusedViewName string
if v == nil || v.Name() == sideViewNames[len(sideViewNames)-1] { if v == nil || v.Name() == gui.CyclableViews[len(gui.CyclableViews)-1] {
focusedViewName = sideViewNames[0] focusedViewName = gui.CyclableViews[0]
} else { } else {
viewName := v.Name() viewName := v.Name()
for i := range sideViewNames { for i := range gui.CyclableViews {
if viewName == sideViewNames[i] { if viewName == gui.CyclableViews[i] {
focusedViewName = sideViewNames[i+1] focusedViewName = gui.CyclableViews[i+1]
break break
} }
if i == len(sideViewNames)-1 { if i == len(gui.CyclableViews)-1 {
gui.Log.Info("not in list of views") gui.Log.Info("not in list of views")
return nil return nil
} }
@ -46,18 +37,17 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
sideViewNames := gui.sideViewNames()
var focusedViewName string var focusedViewName string
if v == nil || v.Name() == sideViewNames[0] { if v == nil || v.Name() == gui.CyclableViews[0] {
focusedViewName = sideViewNames[len(sideViewNames)-1] focusedViewName = gui.CyclableViews[len(gui.CyclableViews)-1]
} else { } else {
viewName := v.Name() viewName := v.Name()
for i := range sideViewNames { for i := range gui.CyclableViews {
if viewName == sideViewNames[i] { if viewName == gui.CyclableViews[i] {
focusedViewName = sideViewNames[i-1] focusedViewName = gui.CyclableViews[i-1]
break break
} }
if i == len(sideViewNames)-1 { if i == len(gui.CyclableViews)-1 {
gui.Log.Info("not in list of views") gui.Log.Info("not in list of views")
return nil return nil
} }
@ -73,7 +63,126 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) resetMainView() { func (gui *Gui) resetMainView() {
gui.State.Panels.Main.ObjectKey = "" gui.State.Panels.Main.ObjectKey = ""
gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel gui.getMainView().Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
}
func (gui *Gui) newLineFocused(v *gocui.View) error {
if v == nil {
return nil
}
switch v.Name() {
case "menu":
return gui.handleMenuSelect(gui.g, v)
case "project":
return gui.handleProjectSelect(gui.g, v)
case "services":
return gui.handleServiceSelect(gui.g, v)
case "containers":
return gui.handleContainerSelect(gui.g, v)
case "images":
return gui.handleImageSelect(gui.g, v)
case "volumes":
return gui.handleVolumeSelect(gui.g, v)
case "confirmation":
return nil
case "main":
v.Highlight = false
return nil
default:
panic(gui.Tr.NoViewMachingNewLineFocusedSwitchStatement)
}
}
// TODO: move some of this logic into our onFocusLost and onFocus hooks
func (gui *Gui) switchFocus(newView *gocui.View) error {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
return gui.switchFocusAux(newView)
}
func (gui *Gui) switchFocusAux(newView *gocui.View) error {
gui.pushView(newView.Name())
gui.Log.Info("setting highlight to true for view " + newView.Name())
gui.Log.Info("new focused view is " + newView.Name())
if _, err := gui.g.SetCurrentView(newView.Name()); err != nil {
return err
}
gui.g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
return gui.newLineFocused(newView)
}
func (gui *Gui) returnFocus() error {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
if len(gui.State.ViewStack) <= 1 {
return nil
}
previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2]
previousView, err := gui.g.View(previousViewName)
if err != nil {
return err
}
return gui.switchFocusAux(previousView)
}
// Not to be called directly. Use `switchFocus` instead
func (gui *Gui) pushView(name string) {
// No matter what view we're pushing, we first remove all popup panels from the stack
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
return viewName != "confirmation" && viewName != "menu"
})
// If we're pushing a side panel, we remove all other panels
if lo.Contains(gui.sideViewNames(), name) {
gui.State.ViewStack = []string{}
}
// If we're pushing a panel that's already in the stack, we remove it
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
return viewName != name
})
gui.State.ViewStack = append(gui.State.ViewStack, name)
}
// excludes popups
func (gui *Gui) currentStaticViewName() string {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
for i := len(gui.State.ViewStack) - 1; i >= 0; i-- {
if !lo.Contains(gui.popupViewNames(), gui.State.ViewStack[i]) {
return gui.State.ViewStack[i]
}
}
return gui.initiallyFocusedViewName()
}
func (gui *Gui) currentSideViewName() string {
gui.Mutexes.ViewStackMutex.Lock()
defer gui.Mutexes.ViewStackMutex.Unlock()
// we expect that there is a side window somewhere in the view stack, so we will search from top to bottom
for idx := range gui.State.ViewStack {
reversedIdx := len(gui.State.ViewStack) - 1 - idx
viewName := gui.State.ViewStack[reversedIdx]
if lo.Contains(gui.sideViewNames(), viewName) {
return viewName
}
}
return gui.initiallyFocusedViewName()
} }
// if the cursor down past the last item, move it to the last line // if the cursor down past the last item, move it to the last line
@ -114,15 +223,10 @@ func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui
} }
} }
func (gui *Gui) FocusY(selectedY int, lineCount int, v *gocui.View) { func (gui *Gui) focusY(selectedY int, lineCount int, v *gocui.View) {
gui.focusPoint(0, selectedY, lineCount, v) gui.focusPoint(0, selectedY, lineCount, v)
} }
func (gui *Gui) ResetOrigin(v *gocui.View) {
_ = v.SetOrigin(0, 0)
_ = v.SetCursor(0, 0)
}
func (gui *Gui) cleanString(s string) string { func (gui *Gui) cleanString(s string) string {
output := string(bom.Clean([]byte(s))) output := string(bom.Clean([]byte(s)))
return utils.NormalizeLinefeeds(output) return utils.NormalizeLinefeeds(output)
@ -152,10 +256,6 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
return nil return nil
} }
func (gui *Gui) RenderStringMain(s string) {
_ = gui.renderString(gui.g, "main", s)
}
// reRenderString sets the main view's content, without changing its origin // reRenderString sets the main view's content, without changing its origin
func (gui *Gui) reRenderStringMain(s string) { func (gui *Gui) reRenderStringMain(s string) {
gui.reRenderString("main", s) gui.reRenderString("main", s)
@ -185,8 +285,34 @@ func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap)) return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
} }
func (gui *Gui) GetMainView() *gocui.View { func (gui *Gui) getProjectView() *gocui.View {
return gui.Views.Main v, _ := gui.g.View("project")
return v
}
func (gui *Gui) getServicesView() *gocui.View {
v, _ := gui.g.View("services")
return v
}
func (gui *Gui) getContainersView() *gocui.View {
v, _ := gui.g.View("containers")
return v
}
func (gui *Gui) getImagesView() *gocui.View {
v, _ := gui.g.View("images")
return v
}
func (gui *Gui) getVolumesView() *gocui.View {
v, _ := gui.g.View("volumes")
return v
}
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v
} }
func (gui *Gui) trimmedContent(v *gocui.View) string { func (gui *Gui) trimmedContent(v *gocui.View) string {
@ -205,24 +331,40 @@ func (gui *Gui) currentViewName() string {
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error { func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
v := g.CurrentView() v := g.CurrentView()
if gui.isPopupPanel(v.Name()) { if gui.isPopupPanel(v.Name()) {
return gui.resizePopupPanel(v) return gui.resizePopupPanel(g, v)
} }
return nil return nil
} }
func (gui *Gui) resizePopupPanel(v *gocui.View) error { func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width, // If the confirmation panel is already displayed, just resize the width,
// otherwise continue // otherwise continue
content := v.Buffer() content := v.Buffer()
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content) x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions() vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 { if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil return nil
} }
_, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0) _, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err return err
} }
func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
if up {
if *line == -1 || *line == 0 {
return
}
*line -= 1
} else {
if *line == -1 || *line == total-1 {
return
}
*line += 1
}
}
func (gui *Gui) renderPanelOptions() error { func (gui *Gui) renderPanelOptions() error {
currentView := gui.g.CurrentView() currentView := gui.g.CurrentView()
switch currentView.Name() { switch currentView.Name() {
@ -235,7 +377,7 @@ func (gui *Gui) renderPanelOptions() error {
} }
func (gui *Gui) isPopupPanel(viewName string) bool { func (gui *Gui) isPopupPanel(viewName string) bool {
return lo.Contains(gui.popupViewNames(), viewName) return viewName == "confirmation" || viewName == "menu"
} }
func (gui *Gui) popupPanelFocused() bool { func (gui *Gui) popupPanelFocused() bool {
@ -243,24 +385,21 @@ func (gui *Gui) popupPanelFocused() bool {
} }
func (gui *Gui) clearMainView() { func (gui *Gui) clearMainView() {
mainView := gui.Views.Main mainView := gui.getMainView()
mainView.Clear() mainView.Clear()
_ = mainView.SetOrigin(0, 0) _ = mainView.SetOrigin(0, 0)
_ = mainView.SetCursor(0, 0) _ = mainView.SetCursor(0, 0)
} }
func (gui *Gui) HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error { func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error {
wrappedHandleSelect := func(g *gocui.Gui, v *gocui.View) error {
return handleSelect()
}
return gui.handleClickAux(v, itemCount, selectedLine, wrappedHandleSelect)
}
func (gui *Gui) handleClickAux(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error {
if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) { if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) {
return nil return nil
} }
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
_, cy := v.Cursor() _, cy := v.Cursor()
_, oy := v.Origin() _, oy := v.Origin()
@ -276,12 +415,6 @@ func (gui *Gui) handleClickAux(v *gocui.View, itemCount int, selectedLine *int,
*selectedLine = newSelectedLine *selectedLine = newSelectedLine
if gui.currentViewName() != v.Name() {
if err := gui.switchFocus(v); err != nil {
return err
}
}
return handleSelect(gui.g, v) return handleSelect(gui.g, v)
} }
@ -332,51 +465,3 @@ func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowM
} }
return sl[len(sl)-1] return sl[len(sl)-1]
} }
func (gui *Gui) CurrentView() *gocui.View {
return gui.g.CurrentView()
}
func (gui *Gui) currentSidePanel() (panels.ISideListPanel, bool) {
viewName := gui.currentViewName()
for _, sidePanel := range gui.allSidePanels() {
if sidePanel.GetView().Name() == viewName {
return sidePanel, true
}
}
return nil, false
}
// returns the current list panel. If no list panel is focused, returns false.
func (gui *Gui) currentListPanel() (panels.ISideListPanel, bool) {
viewName := gui.currentViewName()
for _, sidePanel := range gui.allListPanels() {
if sidePanel.GetView().Name() == viewName {
return sidePanel, true
}
}
return nil, false
}
func (gui *Gui) allSidePanels() []panels.ISideListPanel {
return []panels.ISideListPanel{
gui.Panels.Projects,
gui.Panels.Services,
gui.Panels.Containers,
gui.Panels.Images,
gui.Panels.Volumes,
gui.Panels.Networks,
}
}
func (gui *Gui) allListPanels() []panels.ISideListPanel {
return append(gui.allSidePanels(), gui.Panels.Menu)
}
func (gui *Gui) IsCurrentView(view *gocui.View) bool {
return view == gui.CurrentView()
}

View file

@ -1,157 +1,96 @@
package gui package gui
import ( import (
"os"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/samber/lo"
) )
// See https://github.com/xtermjs/xterm.js/issues/4238
// VSCode is soon to fix this in an upcoming update.
// Once that's done, we can scrap the HIDE_UNDERSCORES variable
var (
underscoreEnvChecked bool
hideUnderscores bool
)
func hideUnderScores() bool {
if !underscoreEnvChecked {
hideUnderscores = os.Getenv("TERM_PROGRAM") == "vscode"
underscoreEnvChecked = true
}
return hideUnderscores
}
type Views struct { type Views struct {
// side panels
Project *gocui.View Project *gocui.View
Services *gocui.View Services *gocui.View
Containers *gocui.View Containers *gocui.View
Images *gocui.View Images *gocui.View
Volumes *gocui.View Volumes *gocui.View
Networks *gocui.View
// main panel
Main *gocui.View Main *gocui.View
// bottom line
Options *gocui.View Options *gocui.View
Information *gocui.View
AppStatus *gocui.View
// text that prompts you to enter text in the Filter view
FilterPrefix *gocui.View
// appears next to the SearchPrefix view, it's where you type in the search string
Filter *gocui.View
// popups
Confirmation *gocui.View Confirmation *gocui.View
Menu *gocui.View Menu *gocui.View
Information *gocui.View
// will cover everything when it appears AppStatus *gocui.View
Limit *gocui.View Limit *gocui.View
} }
type viewNameMapping struct { type viewNameMapping struct {
viewPtr **gocui.View viewPtr **gocui.View
name string name string
// if true, we handle the position/size of the view in arrangement.go. Otherwise
// we handle it manually.
autoPosition bool
} }
func (gui *Gui) orderedViewNameMappings() []viewNameMapping { func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
return []viewNameMapping{ return []viewNameMapping{
// first layer. Ordering within this layer does not matter because there are // first layer. Ordering within this layer does not matter because there are
// no overlapping views // no overlapping views
{viewPtr: &gui.Views.Project, name: "project", autoPosition: true}, {viewPtr: &gui.Views.Project, name: "project"},
{viewPtr: &gui.Views.Services, name: "services", autoPosition: true}, {viewPtr: &gui.Views.Services, name: "services"},
{viewPtr: &gui.Views.Containers, name: "containers", autoPosition: true}, {viewPtr: &gui.Views.Containers, name: "containers"},
{viewPtr: &gui.Views.Images, name: "images", autoPosition: true}, {viewPtr: &gui.Views.Images, name: "images"},
{viewPtr: &gui.Views.Volumes, name: "volumes", autoPosition: true}, {viewPtr: &gui.Views.Volumes, name: "volumes"},
{viewPtr: &gui.Views.Networks, name: "networks", autoPosition: true},
{viewPtr: &gui.Views.Main, name: "main", autoPosition: true}, {viewPtr: &gui.Views.Main, name: "main"},
// bottom line // bottom line
{viewPtr: &gui.Views.Options, name: "options", autoPosition: true}, {viewPtr: &gui.Views.Options, name: "options"},
{viewPtr: &gui.Views.AppStatus, name: "appStatus", autoPosition: true}, {viewPtr: &gui.Views.AppStatus, name: "appStatus"},
{viewPtr: &gui.Views.Information, name: "information", autoPosition: true}, {viewPtr: &gui.Views.Information, name: "information"},
{viewPtr: &gui.Views.Filter, name: "filter", autoPosition: true},
{viewPtr: &gui.Views.FilterPrefix, name: "filterPrefix", autoPosition: true},
// popups. // popups.
{viewPtr: &gui.Views.Menu, name: "menu", autoPosition: false}, {viewPtr: &gui.Views.Menu, name: "menu"},
{viewPtr: &gui.Views.Confirmation, name: "confirmation", autoPosition: false}, {viewPtr: &gui.Views.Confirmation, name: "confirmation"},
// this guy will cover everything else when it appears // this guy will cover everything else when it appears
{viewPtr: &gui.Views.Limit, name: "limit", autoPosition: true}, {viewPtr: &gui.Views.Limit, name: "limit"},
} }
} }
func (gui *Gui) createAllViews() error { func (gui *Gui) createAllViews() error {
frameRunes := []rune{'─', '│', '╭', '╮', '╰', '╯'}
switch gui.Config.UserConfig.Gui.Border {
case "single":
frameRunes = []rune{'─', '│', '┌', '┐', '└', '┘'}
case "double":
frameRunes = []rune{'═', '║', '╔', '╗', '╚', '╝'}
case "hidden":
frameRunes = []rune{' ', ' ', ' ', ' ', ' ', ' '}
}
var err error var err error
for _, mapping := range gui.orderedViewNameMappings() { for _, mapping := range gui.orderedViewNameMappings() {
*mapping.viewPtr, err = gui.prepareView(mapping.name) *mapping.viewPtr, err = gui.prepareView(mapping.name)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err return err
} }
(*mapping.viewPtr).FrameRunes = frameRunes
(*mapping.viewPtr).FgColor = gocui.ColorDefault (*mapping.viewPtr).FgColor = gocui.ColorDefault
} }
selectedLineBgColor := GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.SelectedLineBgColor) selectedLineBgColor := gocui.ColorBlue
gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
gui.Views.Main.IgnoreCarriageReturns = true gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Project.Title = gui.Tr.ProjectTitle gui.Views.Project.Title = gui.Tr.ProjectTitle
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
gui.Views.Services.TitlePrefix = "[2]"
gui.Views.Services.SelBgColor = selectedLineBgColor gui.Views.Services.SelBgColor = selectedLineBgColor
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
} }
gui.Views.Containers.TitlePrefix = "[3]"
gui.Views.Images.Highlight = true gui.Views.Images.Highlight = true
gui.Views.Images.Title = gui.Tr.ImagesTitle gui.Views.Images.Title = gui.Tr.ImagesTitle
gui.Views.Images.SelBgColor = selectedLineBgColor gui.Views.Images.SelBgColor = selectedLineBgColor
gui.Views.Images.TitlePrefix = "[4]"
gui.Views.Volumes.Highlight = true gui.Views.Volumes.Highlight = true
gui.Views.Volumes.Title = gui.Tr.VolumesTitle gui.Views.Volumes.Title = gui.Tr.VolumesTitle
gui.Views.Volumes.TitlePrefix = "[5]"
gui.Views.Volumes.SelBgColor = selectedLineBgColor gui.Views.Volumes.SelBgColor = selectedLineBgColor
gui.Views.Networks.Highlight = true
gui.Views.Networks.Title = gui.Tr.NetworksTitle
gui.Views.Networks.TitlePrefix = "[6]"
gui.Views.Networks.SelBgColor = selectedLineBgColor
gui.Views.Options.Frame = false gui.Views.Options.Frame = false
gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor() gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor()
@ -161,6 +100,10 @@ func (gui *Gui) createAllViews() error {
gui.Views.Information.Frame = false gui.Views.Information.Frame = false
gui.Views.Information.FgColor = gocui.ColorGreen gui.Views.Information.FgColor = gocui.ColorGreen
if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil {
return err
}
gui.Views.Confirmation.Visible = false gui.Views.Confirmation.Visible = false
gui.Views.Confirmation.Wrap = true gui.Views.Confirmation.Wrap = true
gui.Views.Menu.Visible = false gui.Views.Menu.Visible = false
@ -170,25 +113,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true gui.Views.Limit.Wrap = true
gui.Views.FilterPrefix.BgColor = gocui.ColorDefault gui.waitForIntro.Done()
gui.Views.FilterPrefix.FgColor = gocui.ColorGreen
gui.Views.FilterPrefix.Frame = false
gui.Views.Filter.BgColor = gocui.ColorDefault
gui.Views.Filter.FgColor = gocui.ColorGreen
gui.Views.Filter.Editable = true
gui.Views.Filter.Frame = false
gui.Views.Filter.Editor = gocui.EditorFunc(gui.wrapEditor(gocui.SimpleEditor))
return nil
}
func (gui *Gui) setInitialViewContent() error {
if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil {
return err
}
_ = gui.setViewContent(gui.Views.FilterPrefix, gui.filterPrompt())
return nil return nil
} }
@ -199,12 +124,7 @@ func (gui *Gui) getInformationContent() string {
return informationStr return informationStr
} }
attrs := []color.Attribute{color.FgMagenta} donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
if !hideUnderScores() {
attrs = append(attrs, color.Underline)
}
donate := color.New(attrs...).Sprint(gui.Tr.Donate)
return donate + " " + informationStr return donate + " " + informationStr
} }
@ -213,12 +133,6 @@ func (gui *Gui) popupViewNames() []string {
} }
// these views have their position and size determined by arrangement.go // these views have their position and size determined by arrangement.go
func (gui *Gui) autoPositionedViewNames() []string { func (gui *Gui) controlledBoundsViewNames() []string {
views := lo.Filter(gui.orderedViewNameMappings(), func(viewNameMapping viewNameMapping, _ int) bool { return []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"}
return viewNameMapping.autoPosition
})
return lo.Map(views, func(viewNameMapping viewNameMapping, _ int) string {
return viewNameMapping.name
})
} }

View file

@ -4,60 +4,77 @@ import (
"fmt" "fmt"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
) )
func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] { // list panel functions
return &panels.SideListPanel[*commands.Volume]{
ContextState: &panels.ContextState[*commands.Volume]{ func (gui *Gui) getVolumeContexts() []string {
GetMainTabs: func() []panels.MainTab[*commands.Volume] { return []string{"config"}
return []panels.MainTab[*commands.Volume]{
{
Key: "config",
Title: gui.Tr.ConfigTitle,
Render: gui.renderVolumeConfig,
},
}
},
GetItemContextCacheKey: func(volume *commands.Volume) string {
return "volumes-" + volume.Name
},
},
ListPanel: panels.ListPanel[*commands.Volume]{
List: panels.NewFilteredList[*commands.Volume](),
View: gui.Views.Volumes,
},
NoItemsMessage: gui.Tr.NoVolumes,
Gui: gui.intoInterface(),
// we're sorting these volumes based on whether they have labels defined,
// because those are the ones you typically care about.
// Within that, we also sort them alphabetically
Sort: func(a *commands.Volume, b *commands.Volume) bool {
if len(a.Volume.Labels) == 0 && len(b.Volume.Labels) > 0 {
return false
}
if len(a.Volume.Labels) > 0 && len(b.Volume.Labels) == 0 {
return true
}
return a.Name < b.Name
},
GetTableCells: presentation.GetVolumeDisplayStrings,
}
} }
func (gui *Gui) renderVolumeConfig(volume *commands.Volume) tasks.TaskFunc { func (gui *Gui) getVolumeContextTitles() []string {
return gui.NewSimpleRenderStringTask(func() string { return gui.volumeConfigStr(volume) }) return []string{gui.Tr.ConfigTitle}
} }
func (gui *Gui) volumeConfigStr(volume *commands.Volume) string { func (gui *Gui) getSelectedVolume() (*commands.Volume, error) {
selectedLine := gui.State.Panels.Volumes.SelectedLine
if selectedLine == -1 {
return nil, gui.Errors.ErrNoVolumes
}
return gui.DockerCommand.Volumes[selectedLine], nil
}
func (gui *Gui) handleVolumesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.DockerCommand.Volumes)
handleSelect := gui.handleVolumeSelect
selectedLine := &gui.State.Panels.Volumes.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleVolumeSelect(g *gocui.Gui, v *gocui.View) error {
volume, err := gui.getSelectedVolume()
if err != nil {
if err != gui.Errors.ErrNoVolumes {
return err
}
return gui.renderString(g, "main", gui.Tr.NoVolumes)
}
gui.focusY(gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v)
key := "volumes-" + volume.Name + "-" + gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
mainView := gui.getMainView()
mainView.Tabs = gui.getVolumeContextTitles()
mainView.TabIndex = gui.State.Panels.Volumes.ContextIndex
switch gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex] {
case "config":
if err := gui.renderVolumeConfig(mainView, volume); err != nil {
return err
}
default:
return errors.New("Unknown context for Volumes panel")
}
return nil
}
func (gui *Gui) renderVolumeConfig(mainView *gocui.View, volume *commands.Volume) error {
return gui.T.NewTask(func(stop chan struct{}) {
mainView.Autoscroll = false
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
padding := 15 padding := 15
output := "" output := ""
output += utils.WithPadding("Name: ", padding) + volume.Name + "\n" output += utils.WithPadding("Name: ", padding) + volume.Name + "\n"
@ -82,38 +99,109 @@ func (gui *Gui) volumeConfigStr(volume *commands.Volume) string {
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n" output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n"
} }
return output _ = gui.renderString(gui.g, "main", output)
})
} }
func (gui *Gui) reloadVolumes() error { func (gui *Gui) refreshVolumes() error {
if err := gui.refreshStateVolumes(); err != nil { volumesView := gui.getVolumesView()
if volumesView == nil {
// if the volumesView hasn't been instantiated yet we just return
return nil
}
if err := gui.DockerCommand.RefreshVolumes(); err != nil {
return err return err
} }
return gui.Panels.Volumes.RerenderList() if len(gui.DockerCommand.Volumes) > 0 && gui.State.Panels.Volumes.SelectedLine == -1 {
} gui.State.Panels.Volumes.SelectedLine = 0
}
if len(gui.DockerCommand.Volumes)-1 < gui.State.Panels.Volumes.SelectedLine {
gui.State.Panels.Volumes.SelectedLine = len(gui.DockerCommand.Volumes) - 1
}
func (gui *Gui) refreshStateVolumes() error { gui.g.Update(func(g *gocui.Gui) error {
volumes, err := gui.DockerCommand.RefreshVolumes() volumesView.Clear()
isFocused := gui.g.CurrentView().Name() == "volumes"
list, err := utils.RenderList(gui.DockerCommand.Volumes, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return err return err
} }
fmt.Fprint(volumesView, list)
gui.Panels.Volumes.SetItems(volumes) if volumesView == g.CurrentView() {
return gui.handleVolumeSelect(g, volumesView)
}
return nil
})
return nil return nil
} }
func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleVolumesNextLine(g *gocui.Gui, v *gocui.View) error {
volume, err := gui.Panels.Volumes.GetSelectedItem() if gui.popupPanelFocused() || gui.g.CurrentView() != v {
if err != nil {
return nil return nil
} }
type removeVolumeOption struct { panelState := gui.State.Panels.Volumes
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Volumes), false)
return gui.handleVolumeSelect(gui.g, v)
}
func (gui *Gui) handleVolumesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
panelState := gui.State.Panels.Volumes
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Volumes), true)
return gui.handleVolumeSelect(gui.g, v)
}
func (gui *Gui) handleVolumesNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getVolumeContexts()
if gui.State.Panels.Volumes.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Volumes.ContextIndex = 0
} else {
gui.State.Panels.Volumes.ContextIndex++
}
_ = gui.handleVolumeSelect(gui.g, v)
return nil
}
func (gui *Gui) handleVolumesPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getVolumeContexts()
if gui.State.Panels.Volumes.ContextIndex <= 0 {
gui.State.Panels.Volumes.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Volumes.ContextIndex--
}
_ = gui.handleVolumeSelect(gui.g, v)
return nil
}
type removeVolumeOption struct {
description string description string
command string command string
force bool force bool
runCommand bool
}
// GetDisplayStrings is a function.
func (r *removeVolumeOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
volume, err := gui.getSelectedVolume()
if err != nil {
return nil
} }
options := []*removeVolumeOption{ options := []*removeVolumeOption{
@ -121,32 +209,33 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.Remove, description: gui.Tr.Remove,
command: utils.WithShortSha("docker volume rm " + volume.Name), command: utils.WithShortSha("docker volume rm " + volume.Name),
force: false, force: false,
runCommand: true,
}, },
{ {
description: gui.Tr.ForceRemove, description: gui.Tr.ForceRemove,
command: utils.WithShortSha("docker volume rm --force " + volume.Name), command: utils.WithShortSha("docker volume rm --force " + volume.Name),
force: true, force: true,
runCommand: true,
},
{
description: gui.Tr.Cancel,
runCommand: false,
}, },
} }
menuItems := lo.Map(options, func(option *removeVolumeOption, _ int) *types.MenuItem { handleMenuPress := func(index int) error {
return &types.MenuItem{ if !options[index].runCommand {
LabelColumns: []string{option.description, color.New(color.FgRed).Sprint(option.command)}, return nil
OnPress: func() error { }
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := volume.Remove(option.force); err != nil { if cerr := volume.Remove(options[index].force); cerr != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(cerr.Error())
} }
return nil return nil
}) })
},
} }
})
return gui.Menu(CreateMenuOptions{ return gui.createMenu("", options, len(options), handleMenuPress)
Title: "",
Items: menuItems,
})
} }
func (gui *Gui) handlePruneVolumes() error { func (gui *Gui) handlePruneVolumes() error {
@ -162,7 +251,7 @@ func (gui *Gui) handlePruneVolumes() error {
} }
func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error {
volume, err := gui.Panels.Volumes.GetSelectedItem() volume, err := gui.getSelectedVolume()
if err != nil { if err != nil {
return nil return nil
} }

View file

@ -1,136 +0,0 @@
package i18n
func chineseSet() TranslationSet {
return TranslationSet{
PruningStatus: "修剪中",
RemovingStatus: "移除中",
RestartingStatus: "重启中",
StartingStatus: "启动中",
StoppingStatus: "停止中",
UppingServiceStatus: "升级服务中",
UppingProjectStatus: "升级项目中",
DowningStatus: "下架中",
PausingStatus: "暂停中",
RunningCustomCommandStatus: "正在运行自定义命令",
RunningBulkCommandStatus: "正在运行批量命令",
NoViewMachingNewLineFocusedSwitchStatement: "没有匹配 newLineFocused switch 语句的视图",
ErrorOccurred: "发生错误!请在 https://github.com/jesseduffield/lazydocker/issues 上创建一个问题",
ConnectionFailed: "无法连接到 Docker 客户端。您可能需要重新启动 Docker 客户端",
UnattachableContainerError: "容器不支持 attaching。您必须使用“-it”标志运行服务或者在docker-compose.yml文件中使用`stdin_open: truetty: true`",
WaitingForContainerInfo: "在 Docker 给我们更多关于容器的信息之前,无法继续。请几分钟后重试。",
CannotAttachStoppedContainerError: "您不能 attach 到已停止的容器,您需要先启动它(您可以用 'r' 键来执行此操作)(是的,我懒得为您自动执行此操作)(很酷的是,我可以通过错误消息与您进行一对一的通讯)",
CannotAccessDockerSocketError: "无法访问 Docker 套接字unix:///var/run/docker.sock\n请以 root 用户身份运行 lazydocker 或阅读https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "等待三秒钟以停止子进程。可能有一个孤儿进程在您的系统上继续运行。",
Donate: "捐赠",
Confirm: "确认",
Return: "返回",
FocusMain: "聚焦主面板",
LcFilter: "过滤列表",
Navigate: "导航",
Execute: "执行",
Close: "关闭",
Quit: "退出",
Menu: "菜单",
MenuTitle: "菜单",
Scroll: "滚动",
OpenConfig: "打开lazydocker配置",
EditConfig: "编辑lazydocker配置",
Cancel: "取消",
Remove: "移除",
HideStopped: "隐藏/显示已停止的容器",
ForceRemove: "强制移除",
RemoveWithVolumes: "移除并删除卷",
RemoveService: "移除容器",
UpService: "启动服务",
Stop: "停止",
Pause: "暂停",
Restart: "重新启动",
Down: "关闭项目",
DownWithVolumes: "关闭包括卷的项目",
Start: "启动项目",
Rebuild: "重建",
Recreate: "重新创建",
PreviousContext: "上一个选项卡",
NextContext: "下一个选项卡",
// Attach: "连接/附加",
ViewLogs: "查看日志",
UpProject: "创建并启动容器",
DownProject: "停止并移除容器",
RemoveImage: "移除镜像",
RemoveVolume: "移除卷",
RemoveNetwork: "移除网络",
RemoveWithoutPrune: "移除但不删除未标记的父级",
RemoveWithoutPruneWithForce: "移除(强制)但不删除未标记的父级",
RemoveWithForce: "移除(强制)",
PruneContainers: "删除退出的容器",
PruneVolumes: "删除未使用的卷",
PruneNetworks: "删除未使用的网络",
PruneImages: "删除未使用的镜像",
StopAllContainers: "停止所有容器",
RemoveAllContainers: "删除所有容器(强制)",
ViewRestartOptions: "查看重启选项",
ExecShell: "执行shell",
RunCustomCommand: "运行预定义的自定义命令",
ViewBulkCommands: "查看批量命令",
FilterList: "过滤列表",
OpenInBrowser: "在浏览器中打开(第一个端口为http)",
SortContainersByState: "按状态排序容器",
GlobalTitle: "全局",
MainTitle: "主要",
ProjectTitle: "项目",
ServicesTitle: "服务",
ContainersTitle: "容器",
StandaloneContainersTitle: "独立容器",
ImagesTitle: "镜像",
VolumesTitle: "卷",
NetworksTitle: "网络",
CustomCommandTitle: "自定义命令:",
BulkCommandTitle: "批量命令:",
ErrorTitle: "错误",
LogsTitle: "日志",
ConfigTitle: "配置",
EnvTitle: "环境变量",
DockerComposeConfigTitle: "Docker-Compose配置",
TopTitle: "系统资源管理",
StatsTitle: "统计信息",
CreditsTitle: "关于我们",
ContainerConfigTitle: "容器配置",
ContainerEnvTitle: "容器环境变量",
NothingToDisplay: "无内容显示",
NoContainerForService: "没有日志可以展示;该服务未关联任何容器",
CannotDisplayEnvVariables: "展示环境变量时出现问题",
NoContainers: "没有容器",
NoContainer: "没有容器",
NoImages: "没有镜像",
NoVolumes: "没有卷",
NoNetworks: "没有网络",
NoServices: "没有服务",
ConfirmQuit: "您确定要退出吗?",
ConfirmUpProject: "您确定要“up”的docker compose项目吗",
MustForceToRemoveContainer: "您无法删除正在运行的容器,除非您强制执行。您想强制执行吗?",
NotEnoughSpace: "空间不足,无法渲染面板",
ConfirmPruneImages: "您确定要删除所有未使用的镜像吗?",
ConfirmPruneContainers: "您确定要删除所有停止的容器吗?",
ConfirmStopContainers: "您确定要停止所有容器吗?",
ConfirmRemoveContainers: "您确定要删除所有容器吗?",
ConfirmPruneVolumes: "您确定要删除所有未使用的卷吗?",
ConfirmPruneNetworks: "您确定要删除所有未使用的网络吗?",
StopService: "您确定要停止此服务的容器吗?",
StopContainer: "您确定要停止此容器吗?",
PressEnterToReturn: "按 enter 返回 lazydocker您可以在配置文件中设置 `gui.returnImmediately: true` 来禁用此提示)",
No: "否",
Yes: "是",
LcNextScreenMode: "下一个屏幕模式(正常/半屏/全屏)",
LcPrevScreenMode: "上一个屏幕模式",
FilterPrompt: "筛选",
}
}

View file

@ -45,11 +45,9 @@ func dutchSet() TranslationSet {
ViewLogs: "bekijk logs", ViewLogs: "bekijk logs",
RemoveImage: "verwijder image", RemoveImage: "verwijder image",
RemoveVolume: "verwijder volume", RemoveVolume: "verwijder volume",
RemoveNetwork: "verwijder network",
RemoveWithoutPrune: "verwijder zonder de ongelabeld ouders te verwijderen", RemoveWithoutPrune: "verwijder zonder de ongelabeld ouders te verwijderen",
PruneContainers: "vernietig bestaande containers", PruneContainers: "vernietig bestaande containers",
PruneVolumes: "vernietig ongebruikte volumes", PruneVolumes: "vernietig ongebruikte volumes",
PruneNetworks: "vernietig ongebruikte networks",
PruneImages: "vernietig ongebruikte images", PruneImages: "vernietig ongebruikte images",
ViewRestartOptions: "bekijk herstart opties", ViewRestartOptions: "bekijk herstart opties",
RunCustomCommand: "draai een vooraf bedacht aangepaste opdracht", RunCustomCommand: "draai een vooraf bedacht aangepaste opdracht",
@ -62,7 +60,6 @@ func dutchSet() TranslationSet {
StandaloneContainersTitle: "Alleenstaande Containers", StandaloneContainersTitle: "Alleenstaande Containers",
ImagesTitle: "Images", ImagesTitle: "Images",
VolumesTitle: "Volumes", VolumesTitle: "Volumes",
NetworksTitle: "Networks",
CustomCommandTitle: "Aangepast commando:", CustomCommandTitle: "Aangepast commando:",
ErrorTitle: "Fout", ErrorTitle: "Fout",
LogsTitle: "Logs", LogsTitle: "Logs",
@ -81,7 +78,6 @@ func dutchSet() TranslationSet {
NoContainer: "Geen container", NoContainer: "Geen container",
NoImages: "Geen images", NoImages: "Geen images",
NoVolumes: "Geen volumes", NoVolumes: "Geen volumes",
NoNetworks: "Geen networks",
ConfirmQuit: "Weet je zeker dat je weg wil gaan?", ConfirmQuit: "Weet je zeker dat je weg wil gaan?",
MustForceToRemoveContainer: "Je kan geen draaiende container verwijderen tenzij je het forceert, Wil je het forceren?", MustForceToRemoveContainer: "Je kan geen draaiende container verwijderen tenzij je het forceert, Wil je het forceren?",
@ -89,10 +85,8 @@ func dutchSet() TranslationSet {
ConfirmPruneImages: "Weet je zeker dat je alle niet gebruikte images wil vernietigen?", ConfirmPruneImages: "Weet je zeker dat je alle niet gebruikte images wil vernietigen?",
ConfirmPruneContainers: "Weet je zeker dat je alle niet gestopte containers wil vernietigen?", ConfirmPruneContainers: "Weet je zeker dat je alle niet gestopte containers wil vernietigen?",
ConfirmPruneVolumes: "Weet je zeker dat je alle niet gebruikte volumes wil vernietigen?", ConfirmPruneVolumes: "Weet je zeker dat je alle niet gebruikte volumes wil vernietigen?",
ConfirmPruneNetworks: "Weet je zeker dat je alle niet gebruikte networks wil vernietigen?",
StopService: "Weet je zeker dat je deze service zijn containers wil stoppen?", StopService: "Weet je zeker dat je deze service zijn containers wil stoppen?",
StopContainer: "Weet je zeker dat je deze container wil stoppen?", StopContainer: "Weet je zeker dat je deze container wil stoppen?",
PressEnterToReturn: "Druk op enter om terug te gaan naar lazydocker (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)", PressEnterToReturn: "Druk op enter om terug te gaan naar lazydocker (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Als u wilt loskoppelen van de container, drukt u standaard op ctrl-p en vervolgens op ctrl-q",
} }
} }

View file

@ -12,13 +12,11 @@ type TranslationSet struct {
Execute string Execute string
Scroll string Scroll string
Close string Close string
Quit string
ErrorTitle string ErrorTitle string
NoViewMachingNewLineFocusedSwitchStatement string NoViewMachingNewLineFocusedSwitchStatement string
OpenConfig string OpenConfig string
EditConfig string EditConfig string
ConfirmQuit string ConfirmQuit string
ConfirmUpProject string
ErrorOccurred string ErrorOccurred string
ConnectionFailed string ConnectionFailed string
UnattachableContainerError string UnattachableContainerError string
@ -39,25 +37,18 @@ type TranslationSet struct {
Confirm string Confirm string
Return string Return string
FocusMain string FocusMain string
LcFilter string
StopContainer string StopContainer string
RestartingStatus string RestartingStatus string
StartingStatus string StartingStatus string
StoppingStatus string StoppingStatus string
UppingProjectStatus string
UppingServiceStatus string
PausingStatus string PausingStatus string
RemovingStatus string RemovingStatus string
DowningStatus string
RunningCustomCommandStatus string RunningCustomCommandStatus string
RunningBulkCommandStatus string RunningBulkCommandStatus string
RemoveService string RemoveService string
UpService string
Stop string Stop string
Pause string Pause string
Restart string Restart string
Down string
DownWithVolumes string
Start string Start string
Rebuild string Rebuild string
Recreate string Recreate string
@ -65,48 +56,38 @@ type TranslationSet struct {
NextContext string NextContext string
Attach string Attach string
ViewLogs string ViewLogs string
UpProject string
DownProject string
ServicesTitle string ServicesTitle string
ContainersTitle string ContainersTitle string
StandaloneContainersTitle string StandaloneContainersTitle string
TopTitle string TopTitle string
ImagesTitle string ImagesTitle string
VolumesTitle string VolumesTitle string
NetworksTitle string
NoContainers string NoContainers string
NoContainer string NoContainer string
NoImages string NoImages string
NoVolumes string NoVolumes string
NoNetworks string
NoServices string
RemoveImage string RemoveImage string
RemoveVolume string RemoveVolume string
RemoveNetwork string
RemoveWithoutPrune string RemoveWithoutPrune string
RemoveWithoutPruneWithForce string RemoveWithoutPruneWithForce string
RemoveWithForce string RemoveWithForce string
PruneImages string PruneImages string
PruneContainers string PruneContainers string
PruneVolumes string PruneVolumes string
PruneNetworks string
ConfirmPruneContainers string ConfirmPruneContainers string
ConfirmStopContainers string ConfirmStopContainers string
ConfirmRemoveContainers string ConfirmRemoveContainers string
ConfirmPruneImages string ConfirmPruneImages string
ConfirmPruneVolumes string ConfirmPruneVolumes string
ConfirmPruneNetworks string
PruningStatus string PruningStatus string
StopService string StopService string
PressEnterToReturn string PressEnterToReturn string
DetachFromContainerShortCut string
StopAllContainers string StopAllContainers string
RemoveAllContainers string RemoveAllContainers string
ViewRestartOptions string ViewRestartOptions string
ExecShell string ExecShell string
RunCustomCommand string RunCustomCommand string
ViewBulkCommands string ViewBulkCommands string
FilterList string
OpenInBrowser string OpenInBrowser string
SortContainersByState string SortContainersByState string
@ -119,23 +100,13 @@ type TranslationSet struct {
ContainerConfigTitle string ContainerConfigTitle string
ContainerEnvTitle string ContainerEnvTitle string
NothingToDisplay string NothingToDisplay string
NoContainerForService string
CannotDisplayEnvVariables string CannotDisplayEnvVariables string
CannotManageNonLocalService string
No string No string
Yes string Yes string
LcNextScreenMode string LcNextScreenMode string
LcPrevScreenMode string LcPrevScreenMode string
FilterPrompt string
FocusProjects string
FocusServices string
FocusContainers string
FocusImages string
FocusVolumes string
FocusNetworks string
} }
func englishSet() TranslationSet { func englishSet() TranslationSet {
@ -145,9 +116,6 @@ func englishSet() TranslationSet {
RestartingStatus: "restarting", RestartingStatus: "restarting",
StartingStatus: "starting", StartingStatus: "starting",
StoppingStatus: "stopping", StoppingStatus: "stopping",
UppingServiceStatus: "upping service",
UppingProjectStatus: "upping project",
DowningStatus: "downing",
PausingStatus: "pausing", PausingStatus: "pausing",
RunningCustomCommandStatus: "running custom command", RunningCustomCommandStatus: "running custom command",
RunningBulkCommandStatus: "running bulk command", RunningBulkCommandStatus: "running bulk command",
@ -167,11 +135,9 @@ func englishSet() TranslationSet {
Return: "return", Return: "return",
FocusMain: "focus main panel", FocusMain: "focus main panel",
LcFilter: "filter list",
Navigate: "navigate", Navigate: "navigate",
Execute: "execute", Execute: "execute",
Close: "close", Close: "close",
Quit: "quit",
Menu: "menu", Menu: "menu",
MenuTitle: "Menu", MenuTitle: "Menu",
Scroll: "scroll", Scroll: "scroll",
@ -183,12 +149,9 @@ func englishSet() TranslationSet {
ForceRemove: "force remove", ForceRemove: "force remove",
RemoveWithVolumes: "remove with volumes", RemoveWithVolumes: "remove with volumes",
RemoveService: "remove containers", RemoveService: "remove containers",
UpService: "up service",
Stop: "stop", Stop: "stop",
Pause: "pause", Pause: "pause",
Restart: "restart", Restart: "restart",
Down: "down project",
DownWithVolumes: "down project with volumes",
Start: "start", Start: "start",
Rebuild: "rebuild", Rebuild: "rebuild",
Recreate: "recreate", Recreate: "recreate",
@ -196,17 +159,13 @@ func englishSet() TranslationSet {
NextContext: "next tab", NextContext: "next tab",
Attach: "attach", Attach: "attach",
ViewLogs: "view logs", ViewLogs: "view logs",
UpProject: "up project",
DownProject: "down project",
RemoveImage: "remove image", RemoveImage: "remove image",
RemoveVolume: "remove volume", RemoveVolume: "remove volume",
RemoveNetwork: "remove network",
RemoveWithoutPrune: "remove without deleting untagged parents", RemoveWithoutPrune: "remove without deleting untagged parents",
RemoveWithoutPruneWithForce: "remove (forced) without deleting untagged parents", RemoveWithoutPruneWithForce: "remove (forced) without deleting untagged parents",
RemoveWithForce: "remove (forced)", RemoveWithForce: "remove (forced)",
PruneContainers: "prune exited containers", PruneContainers: "prune exited containers",
PruneVolumes: "prune unused volumes", PruneVolumes: "prune unused volumes",
PruneNetworks: "prune unused networks",
PruneImages: "prune unused images", PruneImages: "prune unused images",
StopAllContainers: "stop all containers", StopAllContainers: "stop all containers",
RemoveAllContainers: "remove all containers (forced)", RemoveAllContainers: "remove all containers (forced)",
@ -214,7 +173,6 @@ func englishSet() TranslationSet {
ExecShell: "exec shell", ExecShell: "exec shell",
RunCustomCommand: "run predefined custom command", RunCustomCommand: "run predefined custom command",
ViewBulkCommands: "view bulk commands", ViewBulkCommands: "view bulk commands",
FilterList: "filter list",
OpenInBrowser: "open in browser (first port is http)", OpenInBrowser: "open in browser (first port is http)",
SortContainersByState: "sort containers by state", SortContainersByState: "sort containers by state",
@ -226,7 +184,6 @@ func englishSet() TranslationSet {
StandaloneContainersTitle: "Standalone Containers", StandaloneContainersTitle: "Standalone Containers",
ImagesTitle: "Images", ImagesTitle: "Images",
VolumesTitle: "Volumes", VolumesTitle: "Volumes",
NetworksTitle: "Networks",
CustomCommandTitle: "Custom Command:", CustomCommandTitle: "Custom Command:",
BulkCommandTitle: "Bulk Command:", BulkCommandTitle: "Bulk Command:",
ErrorTitle: "Error", ErrorTitle: "Error",
@ -240,19 +197,14 @@ func englishSet() TranslationSet {
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",
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",
NoImages: "No images", NoImages: "No images",
NoVolumes: "No volumes", NoVolumes: "No volumes",
NoNetworks: "No networks",
NoServices: "No services",
ConfirmQuit: "Are you sure you want to quit?", ConfirmQuit: "Are you sure you want to quit?",
ConfirmUpProject: "Are you sure you want to 'up' your docker compose project?",
MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?", MustForceToRemoveContainer: "You cannot remove a running container unless you force it. Do you want to force it?",
NotEnoughSpace: "Not enough space to render panels", NotEnoughSpace: "Not enough space to render panels",
ConfirmPruneImages: "Are you sure you want to prune all unused images?", ConfirmPruneImages: "Are you sure you want to prune all unused images?",
@ -260,24 +212,14 @@ func englishSet() TranslationSet {
ConfirmStopContainers: "Are you sure you want to stop all containers?", ConfirmStopContainers: "Are you sure you want to stop all containers?",
ConfirmRemoveContainers: "Are you sure you want to remove all containers?", ConfirmRemoveContainers: "Are you sure you want to remove all containers?",
ConfirmPruneVolumes: "Are you sure you want to prune all unused volumes?", ConfirmPruneVolumes: "Are you sure you want to prune all unused volumes?",
ConfirmPruneNetworks: "Are you sure you want to prune all unused networks?",
StopService: "Are you sure you want to stop this service's containers?", StopService: "Are you sure you want to stop this service's containers?",
StopContainer: "Are you sure you want to stop this container?", StopContainer: "Are you sure you want to stop this container?",
PressEnterToReturn: "Press enter to return to lazydocker (this prompt can be disabled in your config by setting `gui.returnImmediately: true`)", PressEnterToReturn: "Press enter to return to lazydocker (this prompt can be disabled in your config by setting `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "By default, to detach from the container press ctrl-p then ctrl-q",
No: "no", No: "no",
Yes: "yes", Yes: "yes",
LcNextScreenMode: "next screen mode (normal/half/fullscreen)", LcNextScreenMode: "next screen mode (normal/half/fullscreen)",
LcPrevScreenMode: "prev screen mode", LcPrevScreenMode: "prev screen mode",
FilterPrompt: "filter",
FocusProjects: "focus projects panel",
FocusServices: "focus services panel",
FocusContainers: "focus containers panel",
FocusImages: "focus images panel",
FocusVolumes: "focus volumes panel",
FocusNetworks: "focus networks panel",
} }
} }

View file

@ -1,120 +0,0 @@
package i18n
func frenchSet() TranslationSet {
return TranslationSet{
PruningStatus: "destruction",
RemovingStatus: "suppression",
RestartingStatus: "redémarrage",
StartingStatus: "démarrage",
StoppingStatus: "arrêt",
PausingStatus: "mise en pause",
RunningCustomCommandStatus: "exécution de la commande personalisée",
RunningBulkCommandStatus: "exécution de la commande groupée",
NoViewMachingNewLineFocusedSwitchStatement: "Aucune vue correspondant au switch newLineFocused",
ErrorOccurred: "Une erreur s'est produite ! Veuillez créer un rapport d'erreur sur https://github.com/jesseduffield/lazydocker/issues",
ConnectionFailed: "Erreur lors de la connexion au client Docker. Essayez de redémarrer votre client Docker",
UnattachableContainerError: "Le conteneur ne peut pas être attaché. Vous devez exécuter le service avec le drapeau 'it' ou bien utiliser `stdin_open: true, tty: true` dans votre fichier docker-compose.yml",
WaitingForContainerInfo: "Le processus ne peut pas continuer avant que Docker ne fournisse plus d'informations. Veuillez réessayer dans quelques instants.",
CannotAttachStoppedContainerError: "Vous ne pouvez pas vous attacher à un conteneur arrêté, vous devez le démarrer en amont (ce que vous pouvez faire avec la touche 'r') (oui, je suis trop paresseux pour le faire automatiquement pour vous) (plutôt cool que je puisse communiquer en tête-à-tête avec vous au travers d'un message d'erreur, cependant)",
CannotAccessDockerSocketError: "Impossible d'accéder au socket Docker à : unix:///var/run/docker.sock\nLancez lazydocker en tant que root ou alors lisez https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "Trois secondes se sont écoulées depuis la demande d'arrêt des processus enfants. Il se peut qu'un processus orphelin continue à tourner sur votre système.",
Donate: "Donner",
Confirm: "Confirmer",
Return: "retour",
FocusMain: "focus panneau principal",
Navigate: "naviguer",
Execute: "exécuter",
Close: "fermer",
Menu: "menu",
MenuTitle: "Menu",
Scroll: "faire défiler",
OpenConfig: "ouvrir la configuration lazydocker",
EditConfig: "modifier la configuration lazydocker",
Cancel: "annuler",
Remove: "supprimer",
HideStopped: "cacher/montrer les conteneurs arrêtés",
ForceRemove: "forcer la suppression",
RemoveWithVolumes: "supprimer avec les volumes",
RemoveService: "supprimer les conteneurs",
Stop: "arrêter",
Pause: "pause",
Restart: "redémarrer",
Start: "démarrer",
Rebuild: "reconstruire",
Recreate: "recréer",
PreviousContext: "onglet précédent",
NextContext: "onglet suivant",
Attach: "attacher",
ViewLogs: "voir les enregistrements",
RemoveImage: "supprimer l'image",
RemoveVolume: "supprimer le volume",
RemoveNetwork: "supprimer le réseau",
RemoveWithoutPrune: "supprimer sans effacer les parents non étiquetés",
RemoveWithoutPruneWithForce: "supprimer (forcer) sans effacer les parents non étiquetés",
RemoveWithForce: "supprimer (forcer)",
PruneContainers: "détruire les conteneurs arrêtés",
PruneVolumes: "détruire les volumes non utilisés",
PruneNetworks: "détruire les réseaux non utilisés",
PruneImages: "détruire les images non utilisées",
StopAllContainers: "arrêter tous les conteneurs",
RemoveAllContainers: "supprimer tous les conteneurs (forcer)",
ViewRestartOptions: "voir les options de redémarrage",
ExecShell: "exécuter le shell",
RunCustomCommand: "exécuter une commande prédéfinie",
ViewBulkCommands: "voir les commandes groupées",
OpenInBrowser: "ouvrir dans le navigateur (le premier port est http)",
SortContainersByState: "ordonner les conteneurs par état",
GlobalTitle: "Global",
MainTitle: "Principal",
ProjectTitle: "Projet",
ServicesTitle: "Services",
ContainersTitle: "Conteneurs",
StandaloneContainersTitle: "Conteneurs autonomes",
ImagesTitle: "Images",
VolumesTitle: "Volumes",
NetworksTitle: "Réseaux",
CustomCommandTitle: "Commande personnalisée :",
BulkCommandTitle: "Commande groupée :",
ErrorTitle: "Erreur",
LogsTitle: "Journaux",
ConfigTitle: "Config",
EnvTitle: "Env",
DockerComposeConfigTitle: "Config Docker-Compose",
TopTitle: "Top",
StatsTitle: "Statistiques",
CreditsTitle: "À propos",
ContainerConfigTitle: "Config Conteneur",
ContainerEnvTitle: "Env Conteneur",
NothingToDisplay: "Rien à afficher",
CannotDisplayEnvVariables: "Quelque chose a échoué lors de l'affichage des variables d'environnement",
NoContainers: "Aucun conteneur",
NoContainer: "Aucun conteneur",
NoImages: "Aucune image",
NoVolumes: "Aucun volume",
NoNetworks: "Aucun réseau",
ConfirmQuit: "Êtes-vous certain de vouloir quitter ?",
MustForceToRemoveContainer: "Vous ne pouvez pas supprimer un conteneur qui tourne sans le forcer. Voulez-vous le forcer ?",
NotEnoughSpace: "Manque d'espace pour afficher les différent panneaux",
ConfirmPruneImages: "Êtes-vous certain de vouloir détruire toutes les images non utilisées ?",
ConfirmPruneContainers: "Êtes-vous certain de vouloir détruire tous les conteneurs arrêtés ?",
ConfirmStopContainers: "Êtes-vous certain de vouloir arrêter tous les conteneurs ?",
ConfirmRemoveContainers: "Êtes-vous certain de vouloir supprimer tous les conteneurs ?",
ConfirmPruneVolumes: "Êtes-vous certain de vouloir détruire tous les volumes non utilisés ?",
ConfirmPruneNetworks: "Êtes-vous certain de vouloir détruire tous les réseaux non utilisés ?",
StopService: "Êtes-vous certain de vouloir arrêter le conteneur de ce service ?",
StopContainer: "Êtes-vous certain de vouloir arrêter ce conteneur ?",
PressEnterToReturn: "Appuyez sur Entrée pour revenir à lazydocker (ce message peut être désactivé dans vos configurations en appliquant `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Par défaut, pour se détacher du conteneur appuyez sur CTRL-P puis CTRL-Q",
No: "non",
Yes: "oui",
}
}

View file

@ -44,11 +44,9 @@ func germanSet() TranslationSet {
ViewLogs: "zeige Protokolle", ViewLogs: "zeige Protokolle",
RemoveImage: "entferne Image", RemoveImage: "entferne Image",
RemoveVolume: "entferne Volume", RemoveVolume: "entferne Volume",
RemoveNetwork: "entferne Netzwerk",
RemoveWithoutPrune: "entfernen, ohne die unmarkierten Eltern zu entfernen", RemoveWithoutPrune: "entfernen, ohne die unmarkierten Eltern zu entfernen",
PruneContainers: "entferne verlassene Container", PruneContainers: "entferne verlassene Container",
PruneVolumes: "entferne unbenutzte Volumes", PruneVolumes: "entferne unbenutzte Volumes",
PruneNetworks: "entferne unbenutzte Netzwerk",
PruneImages: "entferne unbenutzte Images", PruneImages: "entferne unbenutzte Images",
ViewRestartOptions: "zeige Neustartoptionen", ViewRestartOptions: "zeige Neustartoptionen",
RunCustomCommand: "führe vordefinierten benutzerdefinierten Befehl aus", RunCustomCommand: "führe vordefinierten benutzerdefinierten Befehl aus",
@ -61,7 +59,6 @@ func germanSet() TranslationSet {
StandaloneContainersTitle: "Alleinstehende Container", StandaloneContainersTitle: "Alleinstehende Container",
ImagesTitle: "Images", ImagesTitle: "Images",
VolumesTitle: "Volumes", VolumesTitle: "Volumes",
NetworksTitle: "Netzwerk",
CustomCommandTitle: "Benutzerdefinierter Befehl", CustomCommandTitle: "Benutzerdefinierter Befehl",
ErrorTitle: "Fehler", ErrorTitle: "Fehler",
LogsTitle: "Protokoll", LogsTitle: "Protokoll",
@ -80,7 +77,6 @@ func germanSet() TranslationSet {
NoContainer: "Kein Container", NoContainer: "Kein Container",
NoImages: "Keine Images", NoImages: "Keine Images",
NoVolumes: "Keine Volumes", NoVolumes: "Keine Volumes",
NoNetworks: "Keine Netzwerk",
ConfirmQuit: "Bist du dir sicher, dass du verlassen möchtest?", ConfirmQuit: "Bist du dir sicher, dass du verlassen möchtest?",
MustForceToRemoveContainer: "Du kannst keinen Container entfernen, der noch ausgeführt wird außer du erzwingst es. Möchtest du es erzwingen?", MustForceToRemoveContainer: "Du kannst keinen Container entfernen, der noch ausgeführt wird außer du erzwingst es. Möchtest du es erzwingen?",
@ -88,10 +84,8 @@ func germanSet() TranslationSet {
ConfirmPruneImages: "Bist du dir sicher, dass du alle unbenutzten Images entfernen möchtest?", ConfirmPruneImages: "Bist du dir sicher, dass du alle unbenutzten Images entfernen möchtest?",
ConfirmPruneContainers: "Bist du dir sicher, dass du alle angehaltenen Container entfernen möchtes?", ConfirmPruneContainers: "Bist du dir sicher, dass du alle angehaltenen Container entfernen möchtes?",
ConfirmPruneVolumes: "Bist du dir sicher, dass du alle unbenutzen Volumes entfernen möchtest?", ConfirmPruneVolumes: "Bist du dir sicher, dass du alle unbenutzen Volumes entfernen möchtest?",
ConfirmPruneNetworks: "Bist du dir sicher, dass du alle unbenutzen Netzwerk entfernen möchtest?",
StopService: "Bist du dir sicher, dass du den Dienst dieses Containers anhalten möchtest?", StopService: "Bist du dir sicher, dass du den Dienst dieses Containers anhalten möchtest?",
StopContainer: "Bist du dir sicher, dass du den Container anhalten möchtest?", StopContainer: "Bist du dir sicher, dass du den Container anhalten möchtest?",
PressEnterToReturn: "Drücke Eingabe um zu lazydocker zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)", PressEnterToReturn: "Drücke Eingabe um zu lazydocker zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Um sich vom Container zu trennen, drücken Sie standardmäßig ctrl-p und dann ctrl-q",
} }
} }

View file

@ -53,10 +53,6 @@ func GetTranslationSets() map[string]TranslationSet {
"de": germanSet(), "de": germanSet(),
"tr": turkishSet(), "tr": turkishSet(),
"en": englishSet(), "en": englishSet(),
"fr": frenchSet(),
"zh": chineseSet(),
"es": spanishSet(),
"pt": portugueseSet(),
} }
} }

View file

@ -44,11 +44,9 @@ func polishSet() TranslationSet {
ViewLogs: "pokaż logi", ViewLogs: "pokaż logi",
RemoveImage: "usuń obraz", RemoveImage: "usuń obraz",
RemoveVolume: "usuń wolumen", RemoveVolume: "usuń wolumen",
RemoveNetwork: "usuń sieci",
RemoveWithoutPrune: "usuń bez kasowania nieoznaczonych rodziców", RemoveWithoutPrune: "usuń bez kasowania nieoznaczonych rodziców",
PruneContainers: "wyczyść kontenery", PruneContainers: "wyczyść kontenery",
PruneVolumes: "wyczyść nieużywane wolumeny", PruneVolumes: "wyczyść nieużywane wolumeny",
PruneNetworks: "wyczyść nieużywane sieci",
PruneImages: "wyczyść nieużywane obrazy", PruneImages: "wyczyść nieużywane obrazy",
ViewRestartOptions: "pokaż opcje restartu", ViewRestartOptions: "pokaż opcje restartu",
RunCustomCommand: "wykonaj predefiniowaną własną komende", RunCustomCommand: "wykonaj predefiniowaną własną komende",
@ -61,7 +59,6 @@ func polishSet() TranslationSet {
StandaloneContainersTitle: "Kontenery samodzielne", StandaloneContainersTitle: "Kontenery samodzielne",
ImagesTitle: "Obrazy", ImagesTitle: "Obrazy",
VolumesTitle: "Wolumeny", VolumesTitle: "Wolumeny",
NetworksTitle: "Sieci",
CustomCommandTitle: "Własna komenda:", CustomCommandTitle: "Własna komenda:",
ErrorTitle: "Błąd", ErrorTitle: "Błąd",
LogsTitle: "Logi", LogsTitle: "Logi",
@ -80,7 +77,6 @@ func polishSet() TranslationSet {
NoContainer: "Brak kontenera", NoContainer: "Brak kontenera",
NoImages: "Brak obrazów", NoImages: "Brak obrazów",
NoVolumes: "Brak wolumenów", NoVolumes: "Brak wolumenów",
NoNetworks: "Brak sieci",
ConfirmQuit: "Na pewno chcesz wyjść?", ConfirmQuit: "Na pewno chcesz wyjść?",
MustForceToRemoveContainer: "Nie możesz usunąć uruchomionego kontenera dopóki nie zrobisz tego siłą. Chcesz wykonać to z siłą?", MustForceToRemoveContainer: "Nie możesz usunąć uruchomionego kontenera dopóki nie zrobisz tego siłą. Chcesz wykonać to z siłą?",
@ -88,10 +84,8 @@ func polishSet() TranslationSet {
ConfirmPruneImages: "Na pewno wyczyścić wszystkie nieużywane obrazy?", ConfirmPruneImages: "Na pewno wyczyścić wszystkie nieużywane obrazy?",
ConfirmPruneContainers: "Na pewno wyczyścić wszystkie nieuruchomione kontenery?", ConfirmPruneContainers: "Na pewno wyczyścić wszystkie nieuruchomione kontenery?",
ConfirmPruneVolumes: "Na pewno wyczyścić wszystkie nieużywane wolumeny?", ConfirmPruneVolumes: "Na pewno wyczyścić wszystkie nieużywane wolumeny?",
ConfirmPruneNetworks: "Na pewno wyczyścić wszystkie nieużywane sieci?",
StopService: "Na pewno zatrzymać kontenery tego serwisu?", StopService: "Na pewno zatrzymać kontenery tego serwisu?",
StopContainer: "Na pewno zatrzymać ten kontener?", StopContainer: "Na pewno zatrzymać ten kontener?",
PressEnterToReturn: "Wciśnij enter aby powrócić do lazydockera (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)", PressEnterToReturn: "Wciśnij enter aby powrócić do lazydockera (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Domyślnie, aby odłączyć się od kontenera, naciśnij ctrl-p, a następnie ctrl-q",
} }
} }

View file

@ -1,137 +0,0 @@
package i18n
func portugueseSet() TranslationSet {
return TranslationSet{
PruningStatus: "destruindo",
RemovingStatus: "removendo",
RestartingStatus: "reiniciando",
StartingStatus: "iniciando",
StoppingStatus: "parando",
UppingServiceStatus: "subindo serviço",
UppingProjectStatus: "subindo projeto",
DowningStatus: "derrubando",
PausingStatus: "pausando",
RunningCustomCommandStatus: "executando comando personalizado",
RunningBulkCommandStatus: "executando comando em massa",
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "Um erro ocorreu! Por favor, crie uma issue em https://github.com/jesseduffield/lazydocker/issues",
ConnectionFailed: "Falha na conexão com o cliente Docker. Você pode precisar reiniciar o seu cliente Docker",
UnattachableContainerError: "O contêiner não suporta anexação. Você deve executar o serviço com a flag '-it' ou usar `stdin_open: true, tty: true` no arquivo docker-compose.yml",
WaitingForContainerInfo: "Não é possível prosseguir até que o Docker forneça mais informações sobre o contêiner. Por favor, tente novamente em alguns momentos.",
CannotAttachStoppedContainerError: "Você não pode anexar a um contêiner parado, você precisa iniciá-lo primeiro (o que você pode fazer com a tecla 'r') (sim, sou preguiçoso demais para fazer isso automaticamente para você) (aliás, bem legal que eu posso me comunicar diretamente com você na forma de uma mensagem de erro)",
CannotAccessDockerSocketError: "Não é possível acessar o sôquete docker em: unix:///var/run/docker.sock\nExecute o lazydocker como root ou leia https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "Três segundos foram esperarados para que os processos filhos parassem. Pode haver um processo órfão que continua em execução em seu sistema.",
Donate: "Doar",
Confirm: "Confirmar",
Return: "retornar",
FocusMain: "focar no painel principal",
LcFilter: "filtrar lista",
Navigate: "navegar",
Execute: "executar",
Close: "fechar",
Quit: "sair",
Menu: "menu",
MenuTitle: "Menu",
Scroll: "rolar",
OpenConfig: "abrir configuração do lazydocker",
EditConfig: "editar configuração do lazydocker",
Cancel: "cancelar",
Remove: "remover",
HideStopped: "ocultar/mostrar contêineres parados",
ForceRemove: "forçar remoção",
RemoveWithVolumes: "remover com volumes",
RemoveService: "remover contêineres",
UpService: "subir serviço",
Stop: "parar",
Pause: "pausar",
Restart: "reiniciar",
Down: "derrubar projeto",
DownWithVolumes: "derrubar projetos com volumes",
Start: "iniciar",
Rebuild: "reconstuir",
Recreate: "recriar",
PreviousContext: "aba anterior",
NextContext: "próxima aba",
Attach: "anexar",
ViewLogs: "ver logs",
UpProject: "subir projeto",
DownProject: "derrubar projeto",
RemoveImage: "remover imagem",
RemoveVolume: "remover volume",
RemoveNetwork: "remover rede",
RemoveWithoutPrune: "remover sem deletar pais não etiquetados",
RemoveWithoutPruneWithForce: "remover (forçado) sem deletar pais não etiquetados",
RemoveWithForce: "remover (forçado)",
PruneContainers: "destruir contêineres encerrados",
PruneVolumes: "destruir volumes não utilizados",
PruneNetworks: "destruir redes não utilizadas",
PruneImages: "destruir imagens não utilizadas",
StopAllContainers: "parar todos os contêineres",
RemoveAllContainers: "remover todos os contêineres (forçado)",
ViewRestartOptions: "ver opções de reinício",
ExecShell: "executar shell",
RunCustomCommand: "executar comando personalizado predefinido",
ViewBulkCommands: "ver comandos em massa",
FilterList: "filtrar lista",
OpenInBrowser: "abrir no navegador (primeira porta é http)",
SortContainersByState: "ordenar contêineres por estado",
GlobalTitle: "Global",
MainTitle: "Principal",
ProjectTitle: "Projeto",
ServicesTitle: "Serviços",
ContainersTitle: "Contêineres",
StandaloneContainersTitle: "Contêineres Avulsos",
ImagesTitle: "Imagens",
VolumesTitle: "Volumes",
NetworksTitle: "Redes",
CustomCommandTitle: "Comando Personalizado:",
BulkCommandTitle: "Comando em Massa:",
ErrorTitle: "Erro",
LogsTitle: "Registros",
ConfigTitle: "Config",
EnvTitle: "Env",
DockerComposeConfigTitle: "Docker-Compose Config",
TopTitle: "Topo",
StatsTitle: "Estatísticas",
CreditsTitle: "Sobre",
ContainerConfigTitle: "Configuração do Contêiner",
ContainerEnvTitle: "Contêiner Env",
NothingToDisplay: "Nada a exibir",
NoContainerForService: "Nenhum log para exibir; o serviço não está associado a nenhum contêiner",
CannotDisplayEnvVariables: "Algo deu errado ao exibir as variáveis de ambiente",
NoContainers: "Sem contêineres",
NoContainer: "Sem contêiner",
NoImages: "Sem imagens",
NoVolumes: "Sem volumes",
NoNetworks: "Sem redes",
NoServices: "Sem serviços",
ConfirmQuit: "Tem certeza que deseja sair?",
ConfirmUpProject: "Tem certeza que deseja 'iniciar' seu projeto docker compose?",
MustForceToRemoveContainer: "Você não pode remover um contêiner em execução a menos que o force. Deseja forçar?",
NotEnoughSpace: "Sem espaço suficiente para renderizar os painéis",
ConfirmPruneImages: "Tem certeza que deseja eliminar todas as imagens não utilizadas?",
ConfirmPruneContainers: "Tem certeza que deseja destruir todos os contêineres parados?",
ConfirmStopContainers: "Tem certeza que deseja parar todos os contêineres?",
ConfirmRemoveContainers: "Tem certeza que deseja remover todos os contêineres?",
ConfirmPruneVolumes: "Tem certeza que deseja destruir todos os volumes não utilizados?",
ConfirmPruneNetworks: "Tem certeza que deseja destruir todas as redes não utilizadas?",
StopService: "Tem certeza que deseja parar os contêineres deste serviço?",
StopContainer: "Tem certeza que deseja parar este contêiner?",
PressEnterToReturn: "Pressione enter para retornar ao lazydocker (este prompt pode ser desativado em sua configuração definindo `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Por padrão, para desanexar do contêiner, pressione ctrl-p e depois ctrl-q",
No: "não",
Yes: "sim",
LcNextScreenMode: "modo de tela seguinte (normal/meia/tela cheia)",
LcPrevScreenMode: "modo de tela anterior",
FilterPrompt: "filtro",
}
}

Some files were not shown because too many files have changed in this diff Show more