Compare commits

...

661 commits
v0.4 ... master

Author SHA1 Message Date
Jesse Duffield
7e7aadc207
Merge pull request #797 from jesseduffield/support-p-flag
Support -p flag again
2026-04-19 12:50:06 +10:00
Jesse Duffield
f5ff116af9 Use IsProjectScoped at remaining call sites + add test
Addresses Copilot review comments on PR #797:
- Containers panel title, initial focus selection, and the local-project
  inclusion in getDiscoveredProjects all switched from
  InDockerComposeProject to IsProjectScoped, so they stay consistent
  when -p is given outside a compose dir.
- Adds TestIsProjectScoped table test covering all four combinations.

InDockerComposeProject is still used at the call sites that genuinely
require a compose dir (renderDockerComposeConfig, GetServices).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:47:30 +10:00
Jesse Duffield
3974f6fec1 Support -p flag and DRY up code 2026-04-19 12:16:55 +10:00
Jesse Duffield
697cd441aa Add some claude stuff 2026-04-19 12:16:43 +10:00
Jesse Duffield
9134abeefd
Merge pull request #776 from jesseduffield/copilot/disable-forced-project-view
Hide project/services panels when not in a docker-compose project directory
2026-04-19 11:55:51 +10:00
Jesse Duffield
8106125441
Merge pull request #795 from ddibiasi/copilot/disable-forced-project-view
Fixed forced project view
2026-04-19 11:55:00 +10:00
Daniel Dibiasi
b17d474148
Fixed forced project view 2026-04-16 09:23:12 +02:00
copilot-swe-agent[bot]
e3c1c8630a Hide project/services panels when not in a docker-compose project
Co-authored-by: jesseduffield <8456633+jesseduffield@users.noreply.github.com>
2026-03-15 22:26:03 +00:00
copilot-swe-agent[bot]
ebce4fc631 Initial plan 2026-03-15 22:20:36 +00:00
Jesse Duffield
2ebe3848d6
Merge pull request #774 from jesseduffield/better-project-support
Improve projects support
2026-03-15 14:55:35 +11:00
Jesse Duffield
b65137addf Add claude settings file 2026-03-15 14:53:48 +11:00
Jesse Duffield
26ce871368 Improve projects support 2026-03-15 14:53:48 +11:00
Jesse Duffield
577797d9ed
Merge pull request #744 from jesseduffield/fix-version-lock
Fix version lock issue
2026-01-17 17:14:40 +11:00
Jesse Duffield
3c9d134d05 Fix version lock issue 2026-01-17 17:03:34 +11:00
Jesse Duffield
f4fc3669ca
Merge pull request #716 from ManManavadaria/bugfix/index-out-of-range-713
Fix index out of range panic using containerID fallback (#713)
2025-12-20 09:44:26 +11:00
Jesse Duffield
322e10dcb5 Use even newer Go version 2025-12-20 09:39:26 +11:00
Jesse Duffield
3580e04b8d Use new go version 2025-12-20 09:32:38 +11:00
Jesse Duffield
b38db60ead Use new setup-go version 2025-12-20 09:25:03 +11:00
ManManavadaria
43db69da00 Fix panic: validate Names slice length and use containerID as container name if Names slice is empty in container summary 2025-11-27 19:54:53 +05:30
Jesse Duffield
78edbf3d2e
Merge pull request #706 from nickspaargaren/upgrade-go-version-1.23
Upgrade Go version from 1.21 to 1.23 in Dockerfile
2025-11-15 14:01:07 +11:00
Nick Spaargaren
753c0269d7 Upgrade Go version from 1.21 to 1.23 in Dockerfile 2025-11-14 13:32:29 +01:00
Jesse Duffield
4137d216aa
Merge pull request #703 from jesseduffield/fix-docker-api
fix: Update Docker client to support API version 1.52
2025-11-14 21:23:17 +11:00
Jesse Duffield
826f0293f1 Fix types 2025-11-14 21:20:28 +11:00
Jesse Duffield
c7b58683ae Fix CI 2025-11-14 21:03:38 +11:00
Jesse Duffield
5e0aeec310 run go mod vendor 2025-11-14 20:59:29 +11:00
Jesse Duffield
992c138844 Clean up 2025-11-14 20:59:23 +11:00
Md. Asaduzzaman
c8d2fbdfdc fix: Update Docker client to support API version 1.52
- Upgrade Docker client from v27.1.1 to v28.5.2
- Update hardcoded API version from 1.25 to 1.52
- Enable API version negotiation for better compatibility
- Fix compatibility with Docker Engine 29.0.0 and newer versions

Resolves issues with lazydocker not working with latest Docker clients.
2025-11-12 08:49:01 +06:00
Jesse Duffield
f023b42c89
Add Subble job ad 2025-10-29 21:58:38 +11:00
Jesse Duffield
123af388b0 Add subble job ad 2025-10-29 21:56:08 +11:00
Jesse Duffield
3314ecb66c
Updated README.md
Updated the image source for the Warp logo in the README.
2025-10-11 15:51:02 +11:00
Jesse Duffield
4e2f26b21e
Update README.md
Updated image widths for Warp, Tuple, and Subble.
2025-09-06 17:11:19 +10:00
Jesse Duffield
8a1975ef4a
Update README.md 2025-09-06 17:08:36 +10:00
Jesse Duffield
4332656606 Add Subble image 2025-09-06 17:07:12 +10:00
Jesse Duffield
99bf41c873
Update README.md 2025-09-06 17:04:26 +10:00
Jesse Duffield
587bbbf4b5 Change warp logo explicitly 2025-09-06 17:04:10 +10:00
Jesse Duffield
46400c4d32
Update README.md 2025-09-06 17:01:58 +10:00
Jesse Duffield
8ee57bffaf Add tuple png 2025-09-06 16:59:05 +10:00
Jesse Duffield
bedde4a037
Merge pull request #564 from viviannevilar/patch-1
Update Config.md: location of the config in windows
2024-12-22 21:43:29 +11:00
Jesse Duffield
07558afdc8
Merge pull request #605 from A11Might/readme-add-manual-install
readme add manual install part
2024-12-22 21:40:08 +11:00
Jesse Duffield
1178381e00
Merge pull request #599 from A11Might/feat-show-panel-number
Show jump panel keybindings in panel titles
2024-12-22 21:37:21 +11:00
Jesse Duffield
c45bdea3a0
Merge pull request #540 from peauc/feature/issue-522-add-number-like-lazygit
Add navigation with numbers
2024-12-22 21:26:21 +11:00
kohath
15677ac3d2 readme add manual install part 2024-12-07 22:04:56 +08:00
kohath
b8a3ca1550 Show jump panel keybindings in panel titles 2024-11-25 21:27:27 +08:00
Jesse Duffield
be05115352
Merge pull request #568 from thaJeztah/update_docker_27.1
go.mod: github.com/docker/docker, docker/cli v27.1.1
2024-11-23 17:24:22 +11:00
Clément PÉAU
e2bc42f4bb Merge branch 'master' into feature/issue-522-add-number-like-lazygit 2024-09-02 22:35:05 +02:00
Sebastiaan van Stijn
98721a3a2c
go.mod: github.com/docker/cli v27.1.1
full diff: https://github.com/docker/cli/compare/v27.0.3...v27.1.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-31 09:55:42 +02:00
Sebastiaan van Stijn
a8e75ecfe3
go.mod: github.com/docker/docker v27.1.1
full diff: https://github.com/docker/docker/compare/v27.0.3...v27.1.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-31 09:42:45 +02:00
Clément PÉAU
86cce84955 Add navigation via numbers
A la lazygit, we can now navigate between panels using numbers
(1,2,3,4,5,6,7)

Add translation for number navigation

reformat

oops

fix translation case & generate docs

pluralize networks
2024-07-26 15:58:45 +02:00
Vivianne
eaecd2c573
Update Config.md - location of the config in Windows
Update the location of the config file in Windows in the file Config.md
2024-07-26 17:29:21 +08:00
Jesse Duffield
de40167712
Merge pull request #563 from jesseduffield/docker-compose-command-update
Use 'docker compose' by default
2024-07-21 21:49:27 +10:00
Jesse Duffield
df6c8c196e Use 'docker compose' by default
We now fall back to 'docker-compose' if need be
2024-07-21 21:32:03 +10:00
Jesse Duffield
4f375bbf36
Merge pull request #554 from hauskens/patch-1
Add hint to what .DockerCompose is in documentation
2024-07-21 21:06:04 +10:00
Jesse Duffield
0849b800ff
Merge pull request #561 from thaJeztah/update_docker
go.mod: github.com/docker/docker, docker/cli v27.0.3
2024-07-21 20:49:16 +10:00
Jesse Duffield
c73b3f7cc0
Merge pull request #552 from peauc/feature/issue-493
fix lazydocker breaking when having default context in config
2024-07-21 20:15:19 +10:00
Sebastiaan van Stijn
77a4de4819
go.mod: github.com/docker/docker, docker/cli v27.0.3
Update to v27.0, and remove uses of deprecated types.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:44:09 +02:00
Sebastiaan van Stijn
2d16389add
go.mod: github.com/docker/docker, docker/cli v26.1.4
Update to v26.1, and remove uses of deprecated types.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:37:10 +02:00
Sebastiaan van Stijn
0232610967
go.mod: github.com/docker/docker, docker/cli v25.0.5
Update to v25.0, and remove uses of deprecated types.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:25:11 +02:00
Sebastiaan van Stijn
c84bb887a4
update to go1.21
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:07:48 +02:00
Sebastiaan van Stijn
ff579d8713
go.mod: github.com/stretchr/testify v1.9.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:05:26 +02:00
Sebastiaan van Stijn
463ddfd6ee
go.mod: github.com/docker/go-connections v0.5.0
drops support for legacy Go versions, add support for TLS 1.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:05:26 +02:00
Sebastiaan van Stijn
6f85b653d4
go.mod: github.com/docker/docker, docker/cli v24.0.7
Update to v24.0, and remove uses of deprecated types.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:04:43 +02:00
Sebastiaan van Stijn
966ad05338
go.mod: github.com/docker/docker, docker/cli v23.0.10
Update to v23.0, and remove uses of deprecated types.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:03 +02:00
Sebastiaan van Stijn
8fe90830b1
go.mod: github.com/Microsoft/go-winio v0.5.3
update to a version compatible with go1.22 and up

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:03 +02:00
Sebastiaan van Stijn
bb5d70f787
go.mod: golang.org/x/time v0.5.0
no changes, but using the tagged version

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:03 +02:00
Sebastiaan van Stijn
3daea1ea4d
go.mod: github.com/docker/go-units v0.5.0
removes uses of regular expressions

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:03 +02:00
Sebastiaan van Stijn
e27d4878a2
go.mod: github.com/moby/term v0.5.0
no changes, but using the tagged version

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:03 +02:00
Sebastiaan van Stijn
61cd92bc0c
go.mod: github.com/morikuni/aec v1.0.0
no changes, but using the tagged version

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:02 +02:00
Sebastiaan van Stijn
8ca3166439
go.mod: github.com/imdario/mergo v0.3.16
update to the last version of the module under the old name, before v1.0.0
renames the module.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:02 +02:00
Sebastiaan van Stijn
6217ba0808
go.mod: github.com/sirupsen/logrus v1.9.3
update to the current version, which addresses some issues with current
Go versions, and removes the legacy konsorten/go-windows-terminal-sequences
dependency.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:02:02 +02:00
Sebastiaan van Stijn
b010ea4347
test.sh: fix file not being executable
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 11:00:44 +02:00
Sebastiaan van Stijn
ed600c7116
Dockerfile: fix alpine version
The go1.20 image is not published with alpine 3.15; updating the version.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-07-15 10:59:56 +02:00
hausken
015a1086b7
Add hint to .DockerCompose in documentation
It took me a fair bit of time before i understood that '{{ .DockerCompose }}' was defined by the dockerCompose var in commandTemplates.

In hindsight it is VERY obvious.. so i just wanted to contribute a comment that would have been useful to me when i first tried to set this up.
2024-06-26 00:15:38 +02:00
Clément PÉAU
f6c044d74f fix lazydocker breaking when having default docker explicitly mentioned in docker config 2024-06-18 23:24:00 +02:00
Jesse Duffield
06ab7b77b1
Merge pull request #511 from rajiv-k/docker-host-ssh
fix: Properly support remote docker daemon over ssh
2024-05-30 23:15:50 +10:00
Jesse Duffield
89020d6ead Update readme 2024-05-26 14:35:20 +10:00
Jesse Duffield
1cf2e0284c
Merge pull request #536 from jesseduffield/create-pull-request/patch
README.md: Update Sponsors
2024-05-26 14:33:12 +10:00
github-actions[bot]
58d5a18892 README.md: Update Sponsors 2024-05-26 04:32:45 +00:00
Jesse Duffield
1e34a9babc Remove homebrew bump action
Apparently:

"
Error: Whoops, the lazydocker formula has its version update
pull requests automatically opened by BrewTestBot!
We'd still love your contributions, though, so try another one
that's not in the autobump list
"

So looks like we don't need to have this action anymore because homebrew
will be updated automatically.
2024-05-26 14:26:10 +10:00
Jesse Duffield
152b365771 Use new github token 2024-05-26 14:19:55 +10:00
Jesse Duffield
55b21dfd30 Update sponsors action 2024-05-26 14:13:24 +10:00
Jesse Duffield
be2d80d015
Merge pull request #535 from jesseduffield/bump-gocui-2
Bump gocui
2024-05-26 14:09:04 +10:00
Jesse Duffield
cdf1d9ad11 Remove loader code for confirmation panel
It wasn't being used and if it were to be used, there would be a memory
leak
2024-05-26 14:04:25 +10:00
Jesse Duffield
968b09e6c4 Bump gocui 2024-05-26 14:00:53 +10:00
Jesse Duffield
2729aafe2c Default to rounded corners 2024-05-26 13:52:34 +10:00
Jesse Duffield
ce780b8f6d
Merge pull request #529 from scottmckendry/master
feat: add 'border' option in config
2024-05-26 13:50:55 +10:00
Jesse Duffield
7f0f4305a9
Merge pull request #508 from Vininhos/fix-typo-pt
Fix small Portuguese typo
2024-05-26 13:50:25 +10:00
Jesse Duffield
9440a10053
Merge pull request #517 from joaothallis/docs/fix-go-version-to-install
Fix Go Version to install
2024-05-26 13:49:32 +10:00
Jesse Duffield
0f1b8c9848
Merge pull request #534 from jesseduffield/fix-buggy-log-rendering
Fix buggy log rendering
2024-05-26 13:12:53 +10:00
Jesse Duffield
630ea7d324 Fix issue where logs were buggy after returning from subprocess
Turns out that upon switching to a subprocess we were cancelling ALL
periodic tasks, which includes re-rendering the main view ever 30ms.
This completely broke our log rendering and meant that it only updated
when an event occurred
2024-05-26 13:02:54 +10:00
Jesse Duffield
8a0fcf99f4 Gracefully handle situation when container details have not yet loaded
This should be very rare but it does happen sometimes
2024-05-26 12:27:01 +10:00
Jesse Duffield
623fdb4f33 Use taskID mutex properly 2024-05-26 12:25:32 +10:00
Jesse Duffield
bca15cf6b1 Get container details immediately after getting containers
Previously we separately obtained container details every second,
meaning there was a second of not knowing whether a given container was
using a TTY or not, and that meant we were using the wrong streaming
method for logs, which led to a black screen.
2024-05-26 12:19:17 +10:00
Jesse Duffield
a6ade7e78d Close stream when done with it
We should have been doing this from the start. Potential cause of a
memory leak?
2024-05-26 11:34:05 +10:00
Scott McKendry
51b3bb5418 docs: add new 'border' option to config doc 2024-05-07 07:11:10 +12:00
Scott McKendry
dbf2bced5e feat: support for different border styles 2024-05-07 07:11:10 +12:00
João Thallis
67441aa4b7
Fix Go Version to install 2024-03-02 00:01:55 -03:00
Rajiv Kushwaha
42b7ba18ea
fix: Properly support remote docker daemon over ssh 2024-02-14 14:46:56 +05:30
Vininhos
b210264c05 Fix small Portuguese typo
This commit fix a Portuguese word gender typo "todos as redes" as "redes" (network) represent a female portuguese word. The word "todos" (all) which is a male word should be "todas" (female) to follow the word "as" to match the gender.
2024-01-23 18:07:02 -03:00
Jesse Duffield
80af149b02
Update README.md 2023-10-27 04:41:39 +00:00
README-bot
8aa725f06f Updated README.md 2023-10-27 04:39:26 +00:00
Jesse Duffield
559265df4a add warp png 2023-10-27 15:39:10 +11:00
README-bot
1060e17731 Updated README.md 2023-10-12 06:50:13 +00:00
Jesse Duffield
8976f10120
Merge pull request #489 from jesseduffield/graceful-dir-check 2023-10-12 17:49:43 +11:00
Jesse Duffield
e860416ee4 Gracefully check for docker config
It's fine if the file doesn't exist
2023-10-12 17:42:34 +11:00
Jesse Duffield
cce9d40f43
Merge pull request #487 from jesseduffield/bump-things 2023-10-11 09:18:14 +11:00
Jesse Duffield
6473810fa9 Bump docker 2023-10-11 09:09:47 +11:00
Jesse Duffield
f25a5ebff7 Bump x/net 2023-10-11 09:01:06 +11:00
Jesse Duffield
b2525f4629
Merge pull request #486 from jesseduffield/add-environ-to-cmd 2023-10-11 08:47:30 +11:00
Jesse Duffield
2dda39dff0 Appease linter 2023-10-10 17:39:09 +11:00
Jesse Duffield
39e694f496 Add timeout to go linter 2023-10-09 22:03:40 +11:00
Jesse Duffield
e69449c824 Use go 1.20 2023-10-09 22:03:40 +11:00
Jesse Duffield
966570ce6c Forward env vars to commands
We want to forward env vars to commands just because it's good practice,
and probably enables things that previously weren't possible.
2023-10-09 22:03:40 +11:00
Rajiv Kushwaha
d2fa5e9e9c
feat(docker): Honor the host specified in current docker context (#464) 2023-10-09 17:13:22 +11:00
Jesse Duffield
649e5e1c90 Update cheatsheets 2023-10-09 17:06:06 +11:00
Paul Rivière
722fb88792
Fix mistakes in French translation (#427) 2023-10-09 17:05:34 +11:00
Luis Quiñones
d2dd4db582
i18n: spanish translation (#473)
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2023-10-09 17:02:04 +11:00
guangwu
953e56065d
fix: Maintenance typo (#477) 2023-10-09 17:00:52 +11:00
Bartosz Jablonski
42a9668e55
Fixes #466 (#467) 2023-10-09 16:59:45 +11:00
README-bot
8f77c0f7c0 Updated README.md 2023-10-09 05:59:33 +00:00
Emily Grace Seville
9e90d2f4f2
Mention JSON schema (#474)
Co-authored-by: Emily Grace Seville <EmilySeville7cfg@gmail.com>
2023-10-09 16:59:10 +11:00
README-bot
d68ecbf5f2 Updated README.md 2023-09-19 12:44:44 +00:00
Miguel Figueira Ferraz
50582db9b7
feat(i18n): 🌐 Portuguese translation (#479)
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2023-09-19 22:44:27 +10:00
README-bot
604ca8f0df Updated README.md 2023-08-28 11:38:02 +00:00
Jonas Helgemo
71e8e43b06
Bump version tag in installation docs for asdf (#476)
- Latest version is 0.21.1
- Update asdf install instructions to reflect latest version
2023-08-28 21:37:43 +10:00
README-bot
c635266fae Updated README.md 2023-08-03 10:39:07 +00:00
Luka Markušić
937a6a9c93
Re-enable custom command template application (#470) 2023-08-03 20:38:53 +10:00
Jesse Duffield
e8e808ff47 Set goreleaser version 2023-07-21 19:14:23 +10:00
tiagovla
454eaee2cf
feat: add selectedLineBgColor theme customization (#420) 2023-07-21 18:31:27 +10:00
dependabot[bot]
f0ee38f7f7
Bump github.com/docker/distribution from 2.8.0+incompatible to 2.8.2+incompatible (#448)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 18:30:29 +10:00
Quan Dong
7c074d25f7
feat(i18n): 🌐 Chinese translation (#441)
Co-authored-by: tabris <tabrisdong@gmail.com>
2023-07-21 18:24:47 +10:00
Martin Cross
dffe6970aa
Short status (#419)
Co-authored-by: Martin Cross <martinidc1992@gmail.com>
2023-07-21 18:22:18 +10:00
Rdkang
07f26c1e91
Allow setting default screenMode (#421) 2023-07-21 18:08:21 +10:00
John Chavez
66f5d176d3
Docs: explain go templates in custom commands (#428) 2023-07-21 18:08:06 +10:00
README-bot
ab8d87e6ae Updated README.md 2023-07-21 07:47:13 +00:00
Jesse Duffield
e5ca391291
287 Display hotkeys to detach from the container (#465)
Co-authored-by: glendsoza <glendsouza23@gmail.com>
2023-07-21 17:47:00 +10:00
README-bot
15bc01b9af Updated README.md 2023-05-21 04:07:13 +00:00
Tony Soloveyv
2a9ed70896
Allow to render container stats and configs both in json and yaml (#444) 2023-05-21 14:06:56 +10:00
Dawid Dziurla
fcc0bbe552
Merge pull request #442 from jesseduffield/dawidd6-patch-1
os: fix linter issue
2023-03-29 16:00:06 +02:00
Dawid Dziurla
f93ddeceef
Merge pull request #440 from jesseduffield/dependabot/go_modules/github.com/docker/distribution-2.8.0incompatible
Bump github.com/docker/distribution from 2.7.1+incompatible to 2.8.0+incompatible
2023-03-29 15:57:16 +02:00
Dawid Dziurla
0066c1c76c
os: fix linter issue 2023-03-29 15:55:53 +02:00
Dawid Dziurla
cd11c3e131
Merge pull request #439 from jesseduffield/dependabot/go_modules/github.com/opencontainers/image-spec-1.0.2
Bump github.com/opencontainers/image-spec from 1.0.1 to 1.0.2
2023-03-29 15:49:11 +02:00
README-bot
62095473c0 Updated README.md 2023-03-29 13:48:58 +00:00
Dawid Dziurla
cdd43478e2
Merge pull request #432 from jesseduffield/dependabot/go_modules/github.com/gogo/protobuf-1.3.2
Bump github.com/gogo/protobuf from 1.3.1 to 1.3.2
2023-03-29 15:48:37 +02:00
dependabot[bot]
afacc9be9e
Bump github.com/docker/distribution
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.7.1+incompatible to 2.8.0+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.7.1...v2.8.0)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 03:36:30 +00:00
dependabot[bot]
3c3c80d570
Bump github.com/opencontainers/image-spec from 1.0.1 to 1.0.2
Bumps [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/opencontainers/image-spec/releases)
- [Changelog](https://github.com/opencontainers/image-spec/blob/main/RELEASES.md)
- [Commits](https://github.com/opencontainers/image-spec/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: github.com/opencontainers/image-spec
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 03:36:29 +00:00
README-bot
866bcae509 Updated README.md 2023-03-06 01:36:18 +00:00
Jesse Duffield
966a266c68
Merge pull request #424 from Tony-Sol/master 2023-03-06 12:35:56 +11:00
Tony-Sol
8a095f74b1
regenerate cheatsheets 2023-03-06 02:12:24 +03:00
Tony-Sol
ce95ddea2a
Add networks panel 2023-03-06 01:25:23 +03:00
dependabot[bot]
1746bc07c7
Bump github.com/gogo/protobuf from 1.3.1 to 1.3.2
Bumps [github.com/gogo/protobuf](https://github.com/gogo/protobuf) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/gogo/protobuf/releases)
- [Commits](https://github.com/gogo/protobuf/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: github.com/gogo/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-14 22:43:48 +00:00
Jesse Duffield
17fcdb2b20
Merge pull request #407 from nakaakist/fix-service-id
Resolves https://github.com/jesseduffield/lazydocker/issues/402
2022-12-13 21:11:53 +11:00
README-bot
338b655c0a Updated README.md 2022-12-13 08:23:48 +00:00
Jesse Duffield
6025912336
Merge pull request #387 from gusandrioli/wrap-custom-cmds-on-shell 2022-12-13 19:23:31 +11:00
Gustavo Andrioli
d77b895319 Wrap custom commands on shell 2022-12-01 19:04:29 +11:00
Nariyuki Saito
8871edc1a0 use service name as service ID 2022-11-24 00:05:06 +09:00
README-bot
553b9857cd Updated README.md 2022-11-23 11:22:06 +00:00
Jesse Duffield
d777bfe655
Merge pull request #405 from jesseduffield/tail-logs-configurable 2022-11-23 22:21:52 +11:00
Jesse Duffield
821cd81b72 allow configuring number of logs to tail 2022-11-23 20:58:35 +11:00
README-bot
b8969fd55f Updated README.md 2022-11-15 08:48:31 +00:00
Jesse Duffield
83acb91d83
Merge pull request #401 from jesseduffield/add-dev-container 2022-11-15 19:48:17 +11:00
Jesse Duffield
b0cef73dc1 hide underscores on VSCode integrated terminal while they're glitchy 2022-11-15 19:44:26 +11:00
Jesse Duffield
642abae037 update demo docker compose project 2022-11-15 19:40:26 +11:00
Jesse Duffield
6fbf8c3cc5 add dev-container
allow docker-in-docker
2022-11-15 19:40:26 +11:00
README-bot
530dbbe570 Updated README.md 2022-11-10 09:18:54 +00:00
Jesse Duffield
04cf34383d
Merge pull request #392 from jesseduffield/list-panel-filtering 2022-11-10 20:18:40 +11:00
Jesse Duffield
3e2a3e608a update cheatsheet 2022-11-10 20:13:41 +11:00
Jesse Duffield
b7fea7a44d separate quit and close options 2022-11-10 19:45:32 +11:00
Jesse Duffield
d1ccf11502 small refactor 2022-11-10 19:42:02 +11:00
Jesse Duffield
b0241c79db use context instead of struct channel 2022-11-10 19:38:49 +11:00
Jesse Duffield
7f38cf2ab2 fix some things 2022-11-10 19:27:28 +11:00
Jesse Duffield
52040d1d99 better way of rendering to the main view 2022-11-10 19:14:12 +11:00
Jesse Duffield
5ffbeae3e8 preparing to refactor main render functions 2022-11-09 21:21:36 +11:00
Jesse Duffield
7324254414 better name: GetTableCells 2022-11-09 21:21:36 +11:00
Jesse Duffield
d6ad538426 refactor side list panel some more 2022-11-09 21:21:36 +11:00
Jesse Duffield
02a03ed18d move container stats stuff into presentation layer 2022-11-09 21:21:36 +11:00
Jesse Duffield
a87b698761 move things into presentation package 2022-11-09 21:21:36 +11:00
Jesse Duffield
fc592b5806 more refactoring of list panels 2022-11-09 21:21:36 +11:00
Jesse Duffield
5d498c796a move context stuff into its own struct 2022-11-09 21:21:36 +11:00
Jesse Duffield
c6d424e4a4 show loader when filtering 2022-11-09 21:21:36 +11:00
Jesse Duffield
95cee378cb restore sorting tests 2022-11-09 21:21:36 +11:00
Jesse Duffield
82516385c0 use lazycore stuff 2022-11-09 21:21:36 +11:00
Jesse Duffield
52d2c1ce8c remove unused string 2022-11-09 21:21:36 +11:00
Jesse Duffield
b453ce8940 cleaning things up 2022-11-09 21:21:36 +11:00
Jesse Duffield
0bdd0e0539 yellow ports in container panel for consistency 2022-11-09 21:21:36 +11:00
Jesse Duffield
d964c59df0 dont store cyclable views in state 2022-11-09 21:21:36 +11:00
Jesse Duffield
dfff845469 more refactoring 2022-11-09 21:21:36 +11:00
Jesse Duffield
751dbef42e refactor some more 2022-11-09 21:21:35 +11:00
Jesse Duffield
aab8bcfbcc add deadlock detection for debug mode 2022-11-09 21:21:35 +11:00
Jesse Duffield
9b58b8188a remove redundant function 2022-11-09 21:21:35 +11:00
Jesse Duffield
f7beb6e83f stop support menu filtering for simplicity 2022-11-09 21:21:35 +11:00
Jesse Duffield
e6d4a97545 better search logic 2022-11-09 21:21:35 +11:00
Jesse Duffield
702361559f allow searching in any list view 2022-11-09 21:21:35 +11:00
Jesse Duffield
b6b5373999 fix some glitchiness caused by indeterminate sorting 2022-11-09 21:21:35 +11:00
Jesse Duffield
60df208924 convert menu panel to use new struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
158ef68372 convert project panel to use new struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
5aea90c08b convert containers panel to use new struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
a4b4fee868 convert volumes panel to use new struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
79a58c8d48 convert services panel to use new struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
b80c8edcd0 convert images panel to use new struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
e46b908006 support filtered list panel without dependency on gui package 2022-11-09 21:21:35 +11:00
Jesse Duffield
dcce4c67ec create list panel struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
cb38e48add create filtered list struct 2022-11-09 21:21:35 +11:00
Jesse Duffield
57379252a5 bump gocui 2022-11-09 21:21:35 +11:00
Jesse Duffield
be347a7216 POC filtering on images panel 2022-11-09 21:21:35 +11:00
Jesse Duffield
023370e357
Update README.md 2022-11-09 21:15:28 +11:00
README-bot
50c837fc11 Updated README.md 2022-11-09 10:09:19 +00:00
Jesse Duffield
8b68bb6480
Create README.md 2022-11-09 21:09:00 +11:00
README-bot
d644d5063f Updated README.md 2022-11-09 09:01:32 +00:00
Jesse Duffield
ead759108c switch to discord 2022-11-09 19:59:33 +11:00
README-bot
53248a1a78 Updated README.md 2022-10-21 20:32:44 +00:00
Jesse Duffield
2ecfc5d0dc
Merge pull request #390 from jesseduffield/fix-port-flickering 2022-10-21 13:32:28 -07:00
Jesse Duffield
10919b4dfb deterministically display container ports 2022-10-21 13:29:50 -07:00
Jesse Duffield
2134025de4
Merge pull request #379 from jesseduffield/ignore-substrings 2022-10-13 21:46:30 -07:00
README-bot
478e2edc9d Updated README.md 2022-10-14 04:44:54 +00:00
Jesse Duffield
150adf7abc
Merge pull request #383 from clemsau-forks/master 2022-10-13 21:44:31 -07:00
Clement Sauvage
de07409d84 Generate french keybindings 2022-10-12 18:29:55 +02:00
Clement Sauvage
dddcf7c4a5 Add pkg/i18n/french.go 2022-10-12 18:28:24 +02:00
Jesse Duffield
9ec03eef45 ignore items with configured substrings 2022-10-10 21:00:41 -07:00
Jesse Duffield
950936b178
Merge pull request #378 from jesseduffield/small-improvements 2022-10-10 20:59:34 -07:00
Jesse Duffield
129402fd50 use confirmation panel for upping project 2022-10-10 20:40:51 -07:00
Jesse Duffield
0a0116b5eb update cheatsheets 2022-10-10 20:36:34 -07:00
Jesse Duffield
008ad719d6 allow jumping to top of main panel with home key 2022-10-10 20:34:05 -07:00
Jesse Duffield
0cdac0bca3 allow upping and downing whole project 2022-10-10 20:33:56 -07:00
Jesse Duffield
f948334188
Merge pull request #377 from jesseduffield/upping-v2 2022-10-10 20:33:19 -07:00
Jesse Duffield
2f52bd04e6 properly display unforwarded port 2022-10-10 20:10:36 -07:00
Jesse Duffield
15990effff wait when upping 2022-10-10 19:57:34 -07:00
Jesse Duffield
26b40a9d94
Merge pull request #376 from jesseduffield/up-services 2022-10-10 19:36:14 -07:00
Jesse Duffield
195ccf41ce allow upping a service 2022-10-10 19:19:07 -07:00
Jesse Duffield
33df6ff3b0 update sponsor link 2022-10-10 18:40:49 -07:00
Jesse Duffield
38959a0e2b
Merge pull request #374 from gusandrioli/check-cheatsheet-is-up-to-date 2022-10-10 18:31:19 -07:00
Gustavo Andrioli
574e0b6f90 Remove GetLazydockerRootDirectory function 2022-10-10 21:22:55 -03:00
Gustavo Andrioli
dd11b0ee10 Update CI jobs: Remove Check Cheatsheet from ci job 2022-10-10 18:43:18 -03:00
Gustavo Andrioli
1cb9506554 Update CI jobs: Add check-codebase 2022-10-10 18:42:14 -03:00
Gustavo Andrioli
337e57afbb Bump lazycore 2022-10-10 18:29:17 -03:00
Jesse Duffield
26858c3154
Merge pull request #375 from jesseduffield/sort-volumes-by-labels 2022-10-10 13:51:42 -07:00
Jesse Duffield
ec8dfbac06 sort volumes first on whether they have labels and secondly on name 2022-10-10 13:45:25 -07:00
Gustavo Andrioli
c56256358a Clean up 2022-10-10 17:43:32 -03:00
Gustavo Andrioli
119835c64d Check if cheatsheets are up to date on CI 2022-10-10 17:43:29 -03:00
README-bot
0e0997d41a Updated README.md 2022-10-10 20:37:20 +00:00
Jesse Duffield
33dafa04d9
Merge pull request #373 from gusandrioli/add-auto-generated-cheetsheets-message 2022-10-10 13:37:06 -07:00
Gustavo Andrioli
68ee15d24d Convert generate cmd func to const 2022-10-10 16:03:24 -03:00
Gustavo Andrioli
68f5566fd0 Add message that cheetsheets are auto-generated 2022-10-10 15:36:18 -03:00
README-bot
080512d58d Updated README.md 2022-10-10 02:07:39 +00:00
Jesse Duffield
8670bc06e6
Merge pull request #372 from jesseduffield/resizable-windows 2022-10-09 19:07:25 -07:00
Jesse Duffield
a614bd9a1d show selected line in blue 2022-10-09 18:43:19 -07:00
Jesse Duffield
46de6b365d clean up some things
remove redundant g argument
2022-10-09 18:36:42 -07:00
Jesse Duffield
a9d1694853 more concise showing of ports 2022-10-09 18:10:45 -07:00
Jesse Duffield
5988ec5745 display ports against container and service 2022-10-09 18:03:57 -07:00
Jesse Duffield
540c592b04 prevent issue where opening popup in maximised main view shows popup in front of a side view 2022-10-09 18:03:57 -07:00
Jesse Duffield
aa8504b8f6 fix focus issues 2022-10-09 18:03:57 -07:00
Jesse Duffield
fb7b28243b improve subprocess approach 2022-10-09 18:03:46 -07:00
Jesse Duffield
c6607686eb fix view stack code 2022-10-09 18:03:40 -07:00
Jesse Duffield
ae240021c1 ensure we never delete views 2022-10-09 18:03:14 -07:00
Jesse Duffield
badc43b225 allow changing window size 2022-10-09 18:02:11 -07:00
Jesse Duffield
df1a0b7c07
Merge pull request #358 from Syriiin/readme/docker-compose-faq 2022-10-08 12:53:31 -07:00
Jesse Duffield
270c9de796
Merge pull request #352 from rmtsrc/master 2022-10-08 12:02:59 -07:00
Jesse Duffield
2db5aa5ce9
Merge pull request #366 from miles170/master 2022-10-08 12:00:36 -07:00
Jesse Duffield
9c40237fae
Update pkg/config/app_config.go 2022-10-08 11:57:01 -07:00
Jesse Duffield
d556ce534a
Merge pull request #367 from blennster/master 2022-10-08 09:29:16 -07:00
Jesse Duffield
63e9d82b4f
Merge pull request #368 from jesseduffield/fix-linting 2022-10-08 09:28:57 -07:00
Jesse Duffield
cd791e576f appease linter
see if this works
2022-10-08 09:25:31 -07:00
README-bot
e8ebec193f Updated README.md 2022-10-08 15:59:39 +00:00
Jesse Duffield
79feb369e1
Merge pull request #362 from tilliwilli/change-white-colors-to-default 2022-10-08 08:59:23 -07:00
blennster
7e9bde0f41
add tab and backtab navigation 2022-09-21 00:33:28 +02:00
Miles Liu
d64a92227a
allow user to replace docker repo prefix from image names
Example usage:

```yaml
replacements:
  imageNamePrefixes:
    '123456789012.dkr.ecr.us-east-1.amazonaws.com': '<prod>'
    '923456789999.dkr.ecr.us-east-1.amazonaws.com': '<dev>'
```

Closes #316
2022-09-06 13:43:04 +08:00
KOLOSSA Tilman
49dd19b01c fixed faulty auto-formatting 2022-08-16 13:59:24 +02:00
KOLOSSA Tilman
e93cadf1d6 changed "white" colors to "default" to work in both light and dark terminals 2022-08-16 13:53:33 +02:00
rmt/src
3c607534d1
Include image name on config tab 2022-07-13 14:06:25 +01:00
Samuel Cattini-Schultz
339b1068ec Add Compose V2 instructions to FAQ 2022-06-28 23:00:21 +10:00
Jesse Duffield
0b158dfabe
Merge pull request #355 from DasFaultier/master
remove extracted lazydocker binary
2022-06-18 13:38:22 +10:00
README-bot
0da5fef551 Updated README.md 2022-06-18 03:38:02 +00:00
Jesse Duffield
7581417552
Merge pull request #356 from jesseduffield/extract-kill
extract process kill logic
2022-06-18 13:37:47 +10:00
Jesse Duffield
ea3c009305 extract process kill logic 2022-06-18 13:33:51 +10:00
Jörg Sachse
54cfd08849
remove extracted lazydocker binary
The installation method used up until now would always leave one copy of the lazydocker binary in the extraction directory. This copy is not actually needed after installation, so it should be removed. TheAxelander also pointed this out in his comment at https://github.com/jesseduffield/lazydocker/issues/270#issuecomment-1042983635.
This commit changes that behaviour and makes the install script cleanup after itself.
2022-06-14 12:03:06 +02:00
README-bot
923c91121e Updated README.md 2022-05-19 07:04:07 +00:00
Jesse Duffield
0118786fd9
Merge pull request #346 from jesseduffield/exec-shell-service
exec shell handler for services panel
2022-05-19 17:03:49 +10:00
Jesse Duffield
14af9f4547 exec shell handler for services panel 2022-05-19 15:30:39 +10:00
Jesse Duffield
3abff083ad
Merge pull request #317 from mark2185/feature/docker-pause 2022-05-14 12:31:08 +10:00
Luka Markušić
a03f550535 Refactor pausing a bit 2022-05-12 14:19:06 +02:00
Luka Markušić
0ced576d9c Add services pause functionality 2022-05-12 14:01:44 +02:00
Luka Markušić
badc92f192 Add container pause functionality 2022-05-12 14:01:44 +02:00
Jesse Duffield
2fa6a8964a
Merge pull request #342 from jesseduffield/container-logs-in-sdk 2022-05-12 19:08:05 +10:00
Jesse Duffield
d0a53fbe60 use SDK to print container logs to stdout 2022-05-12 19:03:15 +10:00
Jesse Duffield
d940f2e7ab honour context being done 2022-05-12 16:59:37 +10:00
README-bot
cc2f8032c7 Updated README.md 2022-05-11 13:07:27 +00:00
Jesse Duffield
430467711b again 2022-05-11 23:07:04 +10:00
Jesse Duffield
cd744ad699 try again 2022-05-11 22:45:51 +10:00
Jesse Duffield
5112ac775d add sponsors to readme 2022-05-11 22:41:23 +10:00
Jesse Duffield
da650f4384
Merge pull request #340 from jesseduffield/alright-ed 2022-05-11 22:13:37 +10:00
Jesse Duffield
8319aed87c deal with TTY containers gracefully 2022-05-11 22:11:01 +10:00
Jesse Duffield
92ce88e5c0 redundant file 2022-05-11 20:58:54 +10:00
Jesse Duffield
76fdbd1239
Merge pull request #339 from jesseduffield/config-updates 2022-05-11 20:37:15 +10:00
Jesse Duffield
86de6c09bd default to wrapping main panel 2022-05-11 20:24:53 +10:00
Jesse Duffield
7396dcb0b7
Merge pull request #338 from jesseduffield/no-more-docker 2022-05-11 20:01:01 +10:00
Jesse Duffield
2f08e0942a support starting a service 2022-05-11 19:48:11 +10:00
Jesse Duffield
b4509fb45e add TODO's for using the docker SDK 2022-05-11 19:41:53 +10:00
Jesse Duffield
62be3d9924 allow forcing an image removal 2022-05-11 19:38:53 +10:00
Jesse Duffield
d3e84881bb remove custom command from default config given that it is now inbuilt 2022-05-11 19:38:41 +10:00
Jesse Duffield
100fc69f85
Merge pull request #337 from jesseduffield/default-to-no-timestamps 2022-05-11 19:25:56 +10:00
Jesse Duffield
cd96b20bb2 default to no timestamps in logs 2022-05-11 19:18:58 +10:00
Jesse Duffield
c4e41b3ee2
Merge pull request #336 from jesseduffield/inspect-via-sdk 2022-05-11 19:15:00 +10:00
Jesse Duffield
0b7017150c use sdk to obtain container logs 2022-05-11 19:12:11 +10:00
Jesse Duffield
b709f9f6b1
Merge pull request #335 from jesseduffield/inspect-via-sdk 2022-05-11 18:32:57 +10:00
Jesse Duffield
a100caa0d9 use sdk to inspect containers 2022-05-11 18:22:14 +10:00
Jesse Duffield
13c0bfc0b4
Merge pull request #334 from jesseduffield/retry-if-docker-not-available 2022-05-11 18:09:09 +10:00
Jesse Duffield
d27ce19f6c simplify CPU monitoring logic 2022-05-11 17:49:00 +10:00
Jesse Duffield
b5384d6cdb retry upon losing connection to docker 2022-05-11 17:06:58 +10:00
Jesse Duffield
011824a9b7
Merge pull request #332 from jesseduffield/fixes 2022-05-10 23:22:19 +10:00
Jesse Duffield
75ed6b12e5 refresh images upon receiving an event 2022-05-10 23:14:46 +10:00
Jesse Duffield
56b3196ef9 sort images by name 2022-05-10 23:02:29 +10:00
Jesse Duffield
1924dd2bdb
Merge pull request #331 from jesseduffield/refresh-after-hiding-stopped-containers 2022-05-10 22:29:54 +10:00
Jesse Duffield
ac1e47766a ensure we refresh after hiding stopped containers 2022-05-10 22:24:19 +10:00
Jesse Duffield
4f19a66d35
Merge pull request #330 from jesseduffield/update-gocui 2022-05-10 22:24:01 +10:00
Jesse Duffield
d8777b7a59 update gocui 2022-05-10 22:11:21 +10:00
Jesse Duffield
05fa32014f
Merge pull request #329 from jesseduffield/use-updated-alpine 2022-05-10 21:30:46 +10:00
Jesse Duffield
e8c216a3f9 use alpine that supports go 1.18 2022-05-10 21:23:33 +10:00
Jesse Duffield
dc2e0ffa54
Merge pull request #328 from jesseduffield/no-more-unversioned 2022-05-10 21:10:49 +10:00
Jesse Duffield
052c2ec1d1 appease linter 2022-05-10 20:59:28 +10:00
Jesse Duffield
e68554cee9 use go's own git version info 2022-05-10 20:47:33 +10:00
Jesse Duffield
14cb013f5d update sponsorship link 2022-05-10 20:47:09 +10:00
Jesse Duffield
4f096c3c2b
Update README.md 2022-05-09 22:54:23 +10:00
Jesse Duffield
a3d3109103
Update README.md 2022-05-09 22:50:38 +10:00
Jesse Duffield
0e13712e91 fix: version bump 2022-05-09 22:27:29 +10:00
Jesse Duffield
cf87275668 fix: version bump 2022-05-09 22:20:55 +10:00
Jesse Duffield
087fd00759 swap tokens 2022-05-09 21:53:41 +10:00
Jesse Duffield
b438700d2c new tokens 2022-05-09 21:53:07 +10:00
Jesse Duffield
31435c68f5
Merge pull request #325 from jesseduffield/use-github-actions 2022-05-09 21:42:54 +10:00
Jesse Duffield
afe824a3d6 fix test 2022-05-09 21:37:53 +10:00
Jesse Duffield
6bc57b7628 appease linter 2022-05-09 21:31:24 +10:00
Jesse Duffield
212b9d0b03 use github actions instead of circle ci 2022-05-09 21:24:32 +10:00
Jesse Duffield
846cadd5fd
Merge pull request #324 from jesseduffield/less-cpu-usage 2022-05-09 20:36:46 +10:00
Jesse Duffield
1f5d31c92f
Merge pull request #310 from Tiryoh/feature/update-readme 2022-05-09 20:34:20 +10:00
Jesse Duffield
2fe7505ebc use event channel to reduce CPU usage by a LOT 2022-05-09 20:31:16 +10:00
Jesse Duffield
5de54dfb0e upgrade to go 1.18 2022-05-09 20:31:16 +10:00
Jesse Duffield
bba2feac94
Merge pull request #313 from jesseduffield/fix-goreleaser 2022-05-09 20:30:56 +10:00
Jesse Duffield
1eec32f1fb go mod tidy in docs 2022-05-09 20:23:45 +10:00
Jesse Duffield
57ac3c409d kill process directly 2022-05-09 20:23:45 +10:00
Jesse Duffield
6af9e54491 go mod tidy in docs 2022-03-23 18:46:02 +11:00
Jesse Duffield
46fb0f5e5b remove dead code 2022-02-22 18:10:03 +11:00
Daisuke Sato
32aaea973f
Fix the installed path written in the document to match the script 2022-02-09 12:06:25 +09:00
Jesse Duffield
35bd0db5f8
Merge pull request #307 from jesseduffield/fix-overflow
fix overflow bug
2022-02-02 15:15:20 +11:00
Jesse Duffield
15105d87ff fix overflow bug 2022-02-02 10:32:13 +11:00
Jesse Duffield
e9d65eda03
Merge pull request #295 from glendsoza/master
Created a seperate context to display just environment variables
2022-01-20 09:15:09 +11:00
glendsoza
2718749229 Ran gofmt to fix issues with formatting 2022-01-19 17:27:24 +05:30
glendsoza
9a0bbd1e38 Fixed the typo and removed the uncessory comment 2022-01-18 21:42:49 +05:30
glendsoza
ce5e97dd68 Merge branch 'master' of github.com:jesseduffield/lazydocker 2022-01-18 21:38:39 +05:30
Jesse Duffield
9fad29449c
Merge pull request #272 from lpessoa/lp-order-by-status 2022-01-18 12:28:09 +11:00
lpessoa
62eeaac437
Fixed code format issues 2022-01-17 21:24:02 -03:00
lpessoa
cbacd32a68
Changing SortContainersByName to LegacySortContainers
LegacySortContainers will default to legacy behaviour.
New behaviour will sort by status followed by name.
2022-01-17 19:58:51 -03:00
glendsoza
4d90a0345f Changes as requested per review 2022-01-17 19:24:32 +05:30
glendsoza
600e4af8e6 Merge branch 'master' of github.com:jesseduffield/lazydocker 2022-01-17 19:13:49 +05:30
Jesse Duffield
dd88876e53
Merge pull request #300 from jesseduffield/fix-click-scroll 2022-01-17 22:43:28 +11:00
Jesse Duffield
c5d90fd09d fix clicking on item after scrolling 2022-01-17 22:11:36 +11:00
Jesse Duffield
fa2f86fe31 update funding.yml 2022-01-17 15:19:41 +11:00
glendsoza
663928e81a Log the error 2022-01-14 12:56:58 +05:30
glendsoza
32aa08066c Used RenderTable to dispaly environemnt varialbles also added a check in RenderTable method to make sure code does not panic when empty array is passed 2022-01-14 12:27:30 +05:30
glendsoza
af2eb0bb84 Created a seperate context to display just environment variables in the container panel 2022-01-12 22:39:12 +05:30
Jesse Duffield
a78b5af56b fix crash 2022-01-12 23:24:15 +11:00
Jesse Duffield
0d825a49c7
Merge pull request #294 from jesseduffield/no-monitoring-prompt 2022-01-12 23:18:06 +11:00
Jesse Duffield
1d29a124ee
Merge pull request #292 from dhax/patch-1 2022-01-12 23:15:18 +11:00
Jesse Duffield
4c17f73750 maybe this will fix CI 2022-01-12 23:14:00 +11:00
Jesse Duffield
23bb5692e4 appease linter 2022-01-12 23:12:54 +11:00
Jesse Duffield
97772a95d0 remove anonymous reporting prompt because we disabled that globally ages ago 2022-01-12 23:11:03 +11:00
lpessoa
0279534811
moved states outside Slice call 2022-01-11 21:10:05 +00:00
dhax
71251f4f90
update docs: default linux configuration location
Hey there and hanks for this gem!
2022-01-11 16:09:37 +01:00
Jesse Duffield
048c4c9731
Merge pull request #268 from cmoog/ssh-host 2022-01-10 16:29:38 +11:00
Charlie Moog
7105cc67ae
fix: success case in utils.CloseMany 2022-01-09 10:46:29 -06:00
Jesse Duffield
a0d8803147
Merge pull request #291 from dezren39/patch-2 2022-01-09 18:11:43 +11:00
Jesse Duffield
e29e110476 refactor and add test 2022-01-09 18:05:58 +11:00
Charlie Moog
afe3d23cbd
refactor: kill tunnel cmd on Close 2022-01-08 15:09:08 -06:00
Drewry Pope
50681bc9a0
Update go install instructions
https://go.dev/doc/go-get-install-deprecation
2022-01-08 06:23:07 -06:00
lpessoa
e585277c89 fixing gofmt errors 2022-01-03 22:55:02 +00:00
lpessoa
615ce6536b adding tests for legacy and new default sort for containers 2022-01-03 22:52:51 +00:00
Charlie Moog
0bd4168dc9
override DOCKER_HOST environment variable directly
Rather than only overriding the Go docker client
configuration, we now override the process DOCKER_HOST so
as to properly configure docker-cli child processes.
2021-12-27 23:58:42 -06:00
Charlie Moog
892fc090b6
set pgid and send kill signal on exit 2021-12-26 09:41:43 -06:00
Charlie Moog
7f68d01181
Merge branch 'jesseduffield:master' into ssh-host 2021-12-26 09:22:16 -06:00
lpessoa
db288a1727 Updated sortedContainer slice ordering 2021-12-10 17:12:52 +00:00
lpessoa
1681d839fa Fixing gofmt errors 2021-12-10 16:39:42 +00:00
lpessoa
14e6ff1c1f Use sort containers by state by default
Implements #224
Use sort containers by state as default behavior

Legacy sort by name behavior can be set using a user config setting.
`sortContainersByName` is false by default.
2021-12-09 23:18:20 +00:00
Mark Kopenga
a0386618f9
Merge pull request #280 from conorbros/patch-1
Fix AUR link in docs
2021-12-03 11:10:37 +01:00
Conor
b30648c742
Update README.md 2021-12-03 09:26:17 +10:00
Mark Kopenga
2712d995cd
Merge pull request #278 from camflan/patch-1
Fix asdf install command
2021-12-02 14:57:00 +01:00
Conor
baf1759992
Update README.md 2021-12-02 18:11:25 +10:00
Conor
ae9bbdaf19
Update README.md 2021-12-02 11:55:36 +10:00
Conor
4a99c4699b
Update README.md 2021-12-02 11:55:23 +10:00
Conor
9462017eaf
Update README.md 2021-12-02 11:47:46 +10:00
Camron Flanders
1166e0ddb7
Fix asdf install command 2021-12-01 08:55:18 -06:00
Charlie Moog
33e207aeac Merge branch 'master' into ssh-host 2021-11-26 10:12:18 -06:00
Charlie Moog
a2958c7820 code review changes 2021-11-26 10:10:02 -06:00
Jesse Duffield
7b253141c3
Merge pull request #271 from Eonfge/installationScript 2021-11-26 22:04:49 +11:00
Jesse Duffield
503bf0c314
Merge pull request #265 from Dwarven/master 2021-11-26 21:49:13 +11:00
Jesse Duffield
839d4904bf
Merge pull request #256 from comdotlinux/master 2021-11-26 21:48:00 +11:00
Jesse Duffield
4391878bab
Merge pull request #252 from jtraub/browser-services 2021-11-26 21:47:12 +11:00
Jesse Duffield
52fb944bfb refactor 2021-11-26 21:38:52 +11:00
Jesse Duffield
83c9fa8e72
Merge pull request #277 from jesseduffield/some-changes 2021-11-26 21:37:36 +11:00
Jesse Duffield
3c9437f801 slightly more responsive 2021-11-26 21:16:47 +11:00
Jesse Duffield
9db6eeac9d do not panic if docker client not running 2021-11-26 20:50:06 +11:00
Kevin Degeling
8acb257abe Improved installation script that avoids sudo rights 2021-09-19 13:45:41 +02:00
Charlie Moog
4e201bb8f4 feat: support ssh:// scheme for docker client host
Check for the presence of environment variable `DOCKER_HOST`. If nonempty
and of scheme `ssh://`, open an SSH tunnel to the specified remote host,
forwarding the remote docker socket to a local socket in /tmp. Then,
construct the lazydocker client from the local socket.

Example usage: `DOCKER_HOST=ssh://my-server lazydocker`

Fixes #244, #228, #255
2021-09-05 19:15:44 -05:00
Dwarven YANG
43f83f64ec allow user to configure the gui language 2021-08-30 09:54:55 +08:00
Guruprasad Kulkarni
926ab3a40b
Moved the asdf instructions above binary releases 2021-06-18 14:21:56 +02:00
Guruprasad Kulkarni
58d1d28652
Added link to asdf-vm installation method :) 2021-05-14 13:26:02 +02:00
Konstantin Mikhailov
6c27cf3f32 Open service in browser 2021-03-27 07:15:41 +10:00
lazydocker bot
c88fba546f update docs 2021-03-24 21:11:20 +00:00
mjarkk
6fd5337cc2 Replace github with tap in brews in goreleaser config 2021-03-24 22:00:35 +01:00
Mark Kopenga
fb6b73bf5f
Merge pull request #247 from stokito/exec_shell
Exec shell: separate menu item and auto detect shell: bash or sh
2021-03-24 21:38:10 +01:00
Mark Kopenga
3663fe583b
Merge pull request #248 from stokito/browser
Open in browser
2021-03-22 17:47:09 +01:00
Sergey Ponomarev
7ede2f8297 Open in browser
Most containers expose some http port. We can open it in browser.
For simplicity we can just open first container's published port.
2021-03-22 17:43:10 +02:00
Sergey Ponomarev
2688801cbd Separate menu item for Exec shell
Login to container's shell is very popular operation and to make it easier it will be better to make it as a separate menu item with a dedicated hot key.
2020-12-12 18:45:24 +02:00
Sergey Ponomarev
430600063c Exec shell: autodetect container shell
Some containers may have bash installed but some have only basic sh (Ash/Dash).
There is other shells too: ZSh, tsh etc.

We can automatically detect what shell is configured.
See explanation how it works https://github.com/moby/moby/issues/41702
2020-12-12 18:40:31 +02:00
Dawid Dziurla
415b14fb10
Merge pull request #246 from stokito/patch-1
README.md: Add Awesome Docker to Alternatives
2020-12-09 23:00:31 +01:00
Sergey Ponomarev
c3dc8e1cd5
README.md: Add Awesome Docker to Alternatives 2020-11-30 19:29:46 +02:00
Jesse Duffield
2d37030a40
Merge pull request #245 from Liberatys/feature/add-new-config-folder-loc
Add legacy folder loc change
2020-11-28 10:46:21 +11:00
Nick Flueckiger
1bd850df12 Add legacy folder loc change 2020-11-22 15:01:57 +01:00
Jesse Duffield
ff0a53bfad
Merge pull request #241 from tylerstillwater/fix-empty-logs
Fix Logs (and others) tab always being empty
2020-11-13 13:53:13 +11:00
Tyler Stillwater
6ef3342e96 Logs PrepareForChildren 2020-10-29 09:20:32 -06:00
Jesse Duffield
d4e2fbe6a3
Merge pull request #238 from jesseduffield/more-correction
couple small fixes
2020-09-23 20:34:15 +10:00
Jesse Duffield
941c9fff05 couple small fixes 2020-09-23 20:31:21 +10:00
Jesse Duffield
272a433f68
Merge pull request #212 from biologghe/patch-1
Corrected some typos dutch
2020-09-23 20:22:31 +10:00
Jesse Duffield
197ff321d4
Merge pull request #237 from jesseduffield/health-check-alignment
better alignment for health check
2020-09-23 20:21:17 +10:00
Jesse Duffield
12e486edd8 fix ci errors 2020-09-23 20:19:36 +10:00
Jesse Duffield
1dd0875323 better alignment for health check 2020-09-23 20:16:04 +10:00
Jesse Duffield
386334b1f0
Merge pull request #201 from stirante/health-check
Add health check to container list
2020-09-23 20:15:42 +10:00
Dawid Dziurla
af22494425
Merge pull request #226 from zverev-iv/master
Added info about chocolatey package
2020-07-20 17:01:46 +02:00
zverev-iv
e07ea952fc
Added info about chocolatey package 2020-07-20 16:02:17 +03:00
biologghe
53efd63d5e
Corrected some typos dutch 2020-03-27 12:29:07 +01:00
Dawid Dziurla
10617da560
Merge pull request #209 from jesseduffield/dawidd6-patch-1
install_update_linux: allow specifying directory
2020-03-20 00:21:19 +01:00
Dawid Dziurla
86302e8ccc
README: mention DIR env variable 2020-03-15 19:08:50 +01:00
Dawid Dziurla
60640d4fe4
install_update_linux: allow specifying directory
Closes #208
2020-03-15 19:02:45 +01:00
Jesse Duffield
22ab840439
Merge pull request #204 from jesseduffield/remove-rollbar
remove rollbar
2020-02-02 11:26:48 +11:00
Jesse Duffield
6279b416bc remove rollbar 2020-02-02 11:25:11 +11:00
Jesse Duffield
fa6460b8ab
Merge pull request #203 from jesseduffield/fix-cpu
reduce CPU usage caused by tickers
2020-02-02 11:19:56 +11:00
Jesse Duffield
cea67bc570 reduce cpu usage caused by tickers 2020-02-02 11:11:00 +11:00
Jesse Duffield
012f8d6e08 bump gocui 2020-02-02 10:14:44 +11:00
Piotr Brzozowski
6adb04a364 Add changes from suggestions 2020-01-30 18:48:38 +01:00
stirante
0c2edc7c22 Run gofmt on container.go 2020-01-27 12:44:32 +01:00
stirante
305b9630a0 Add health check to container list 2020-01-27 11:53:17 +01:00
Dawid Dziurla
69f32d52d1
Merge pull request #199 from gggeek/issue_198
only use curl in install_update_linux.sh
2020-01-03 15:28:01 +01:00
Gaetano Giunta
8309068378 only use curl in install_update_linux.sh 2019-12-22 17:38:39 +00:00
Dawid Dziurla
9bf4c8bffb
Merge pull request #197 from jesseduffield/update-deps
update dependencies
2019-12-13 11:36:00 +01:00
Dawid Dziurla
0597c1e8e0
update dependencies 2019-12-13 11:33:04 +01:00
Dawid Dziurla
182d810911
Merge pull request #194 from Olshansk/patch-1
Update README.md
2019-11-22 17:51:37 +01:00
Daniel Olshansky
2e0c1e8054
Update README.md
- Link to an issue with instructions on how to copy-paste text on a Mac

* Not adding all the instructions directly into the README to avoid bloat
2019-11-22 11:48:05 -05:00
Dawid Dziurla
14f27e86c8
Merge pull request #191 from CallanTaylor/stop-ticker
Defer stop ticker
2019-11-19 10:29:10 +01:00
CallanTaylor
58d1eef502 Defer stop ticker 2019-11-19 15:14:49 +13:00
Dawid Dziurla
450d163040
commands: pass arguments inline
newer Docker releases has Opt struct instead of this func
2019-11-16 13:41:39 +01:00
Dawid Dziurla
724d501115
Merge pull request #186 from reicheltp/add-win-to-docs
Add windows release to README
2019-11-13 00:08:23 +01:00
Paul Reichelt
82d95967e5
Update docs: lazydocker is available in scoop main bucket 2019-11-12 08:30:53 +01:00
Paul Reichelt
3f976c2ad9
Add windows release to README 2019-11-11 13:01:16 +01:00
Dawid Dziurla
36075dc835
ci: build on window/i386 too 2019-11-11 12:40:56 +01:00
lazydocker bot
167feee5b1 update docs 2019-11-11 11:39:15 +00:00
Dawid Dziurla
945ad95bae
Comment out snapcraft stuff
It's failing the release pipeline and we do not use it currently.
2019-11-11 12:36:04 +01:00
Dawid Dziurla
221f2b2490
Merge pull request #185 from reicheltp/fix-windows-build
Fix #29: Windows Build
2019-11-11 12:31:00 +01:00
Dawid Dziurla
214f96d0ca
Build windows amd64 target on CI 2019-11-11 12:10:25 +01:00
Paul Reichelt
131d14f37d
Re-enable windows build in goreleaser 2019-11-11 12:04:39 +01:00
Dawid Dziurla
168f63c2d5
Add missing imports 2019-11-11 12:01:08 +01:00
Paul Reichelt
d971621e66
Kill all related processes using windows internals
Use windows kernel32.dll to fetch all related processes.

Inspired by: https://blog.csdn.net/fyxichen/article/details/51857864
2019-11-11 11:38:44 +01:00
Paul Reichelt
bfd3df5354
Move Kill and PrepareForChildren to platform specific implementation 2019-11-11 11:38:43 +01:00
Jesse Duffield
b8804d9af3
Update README.md 2019-11-10 10:17:39 +11:00
Jesse Duffield
b389f48267
Update README.md 2019-11-10 10:13:13 +11:00
Jesse Duffield
5348d2f821
Merge pull request #182 from harrywhite4/logFilter
Return stdout only from docker-compose command
2019-11-05 19:04:45 +11:00
Harry White
b22b4801af Return error with stderr value
To maintain existing behaviour when not capturing stdout and stderr
2019-10-28 22:45:37 +11:00
Harry White
5d5ca0d3c4 Return only stdout from command
docker-compose outputs warning messages to stderr
2019-10-28 22:22:29 +11:00
Dawid Dziurla
d9a94779f3
Merge pull request #181 from skovtunenko/master
Removed unconditional logging profile
2019-10-23 11:52:46 +02:00
Sergio
ab2fed3215
Merge pull request #1 from skovtunenko/Removed-unconditional-logging-profile
Removed unconditional logging profile
2019-10-23 12:42:21 +03:00
Sergio
191bf9a3dc
Removed unconditional logging profile 2019-10-23 12:41:46 +03:00
Dawid Dziurla
7e4ad3de29
Merge pull request #179 from Jamesking56/patch-1
Remove the containers once you exit
2019-10-21 11:30:47 +02:00
James King
4d121cfb0f
Remove the containers once you exit
Exiting after using `docker run` leaves behind stopped containers. Adding `--rm` tells Docker to delete them after exiting.
2019-10-21 09:24:33 +01:00
Dawid Dziurla
945eb3714d
README: clickable homebrew badge + two new github badges 2019-09-17 14:17:10 +02:00
Dawid Dziurla
d41ba0d551
README: add homebrew badge 2019-09-17 14:11:14 +02:00
Dawid Dziurla
c19c2bab15
README: add link to tag badge 2019-09-17 14:08:44 +02:00
Dawid Dziurla
a283584956
README: one badge per line 2019-09-17 14:08:06 +02:00
Dawid Dziurla
362da7abd0
Merge pull request #172 from jesseduffield/ci-goflags
CI improvements
2019-09-15 12:33:37 +02:00
Dawid Dziurla
0bb696a24e
ci: comment some stuff 2019-09-15 12:31:00 +02:00
Dawid Dziurla
74444ae328
ci: split gox step 2019-09-15 12:19:04 +02:00
Dawid Dziurla
939305ff5a
ci: global GOFLAGS with -mod=vendor 2019-09-15 12:10:13 +02:00
Dawid Dziurla
817fb5349b
ci: gox, vendor and you
seems like gox is messing with GOPATH even when -mod=vendor option is
passed

setting GOFLAGS env variable looks to give better results
2019-09-11 12:06:31 +02:00
Dawid Dziurla
ef1b2b57b0
README: append info about logs in Docker to FAQ section 2019-09-11 11:50:47 +02:00
Dawid Dziurla
dc8a624e07
gui: remove duplicate 'c' binding in images panel
closes: #169
2019-09-11 10:16:18 +02:00
Dawid Dziurla
41c46ceb3c
goreleaser: only armv6
armv7 is backward compatible with armv6, so it should run armv6 binaries just fine
2019-09-08 21:59:09 +02:00
lazydocker bot
927ec67043 update docs 2019-09-07 09:22:02 +00:00
Jesse Duffield
c8adaa920a use relative path when creating docs 2019-09-07 19:20:28 +10:00
Jesse Duffield
956b2734f4 Revert "checkout latest master before updating docs"
This reverts commit 42e029c45e.
2019-09-07 19:20:18 +10:00
Jesse Duffield
42e029c45e checkout latest master before updating docs 2019-09-07 19:08:05 +10:00
Jesse Duffield
54609a5d99
Merge pull request #148 from jesseduffield/147_child_processes
ensure the current task is stopped before exit
2019-09-07 18:08:02 +10:00
Jesse Duffield
3f28a97507 ensure the current task is stopped before exit so we don't have any child orphan processes
use stop mutex
2019-09-07 18:04:49 +10:00
Dawid Dziurla
a14e6400cb
Merge pull request #159 from jesseduffield/fix_returning
Implement PreviousViews stack
2019-09-07 09:07:21 +02:00
Dawid Dziurla
2763336f49
gui: delete comment 2019-09-07 09:04:58 +02:00
Dawid Dziurla
a3b3c94ed2
Merge pull request #164 from jesseduffield/ci-improvements
CI improvements
2019-09-04 18:19:50 +02:00
Dawid Dziurla
a901506940
Merge pull request #163 from mikhailswift/fix_1.13_builds
Update golang.org/x/xerrors package (fixes go 1.13 builds)
2019-09-04 18:19:20 +02:00
Dawid Dziurla
139d991f45
ci: proper build matrix using workflows
reference: https://discuss.circleci.com/t/build-matrix-configuration/14448
2019-09-04 18:14:39 +02:00
Mikhail Swift
1ee506f671 Update golang.org/x/xerrors package 2019-09-04 11:42:45 -04:00
Dawid Dziurla
d4b237308a
ci: improvements
- delete codecov stuff
- run simple go test
- no working directory
- no environment variables
- test on circleci/golang:latest too
- remove test.sh file
2019-09-04 17:41:02 +02:00
Michiel Roos
ea18c1b85f Update keybindings.go (#162)
* Update keybindings.go

Remove duplicate keybinding: volumes - c
2019-09-03 14:34:04 +02:00
Dawid Dziurla
cfd4b7a1cc
gui: improve and fix PreviousViews stack 2019-09-02 16:19:30 +02:00
Dawid Dziurla
f95b49cd6b
gui: actually pop stack and keep it fit 2019-09-02 15:49:13 +02:00
Dawid Dziurla
c2ea933059
vendor: add golang-collections 2019-08-31 12:06:01 +02:00
Dawid Dziurla
897d47d0cc
gui: implement PreviousViews stack 2019-08-31 12:02:27 +02:00
Dawid Dziurla
b12414907b
add golang-collections dependency 2019-08-31 11:59:31 +02:00
Dawid Dziurla
14abd94cd0
go mod tidy 2019-08-31 10:18:47 +02:00
Dawid Dziurla
133454ecca
README: change brew installation instructions 2019-08-26 20:13:14 +02:00
Dawid Dziurla
98ae4f3b0c
Merge pull request #156 from skanehira/master
Improve container top
2019-08-26 08:22:33 +02:00
skanehira
b1881e6c58 Improve container top
Check container state.
If container is not running then not call top api.
2019-08-26 08:19:03 +09:00
Dawid Dziurla
febfce995c
Merge pull request #155 from jesseduffield/docker_env
Respect Environment Variables
2019-08-25 19:20:39 +02:00
Dawid Dziurla
1258471af2
respect DOCKER_HOST 2019-08-25 19:09:24 +02:00
Dawid Dziurla
b8d477f4e5
Merge pull request #149 from ekremkaraca/patch-1
remove tap command for homebrew
2019-08-21 14:57:40 +02:00
Ekrem Karaca
91ec431a22 Fix typo... 2019-08-21 15:10:57 +03:00
Ekrem Karaca
1bc6ebc180 Update README... 2019-08-21 15:09:19 +03:00
lazydocker bot
49fe8346da update docs 2019-08-14 23:18:02 +00:00
Jesse Duffield
2b6a4b02e5
Merge pull request #150 from Duske/patch-1
fix german notion of volumes
2019-08-14 20:38:01 +10:00
Jesse Duffield
b305558f5e
Merge pull request #145 from anjiawei1991/master
add image to conatiner view.
2019-08-14 20:36:34 +10:00
Jesse Duffield
6ebddea76e
Merge pull request #130 from winhung/limit-scrolling
scrolling does not go beyond the displayed text #31
2019-08-14 20:29:50 +10:00
Dustin
42c428771a
fix german notion of volumes
Afaik, `Volumen` is not the common notion for docker volumes. Simply using `Volumes` is better.
2019-08-12 15:15:04 +02:00
Ekrem Karaca
c764c9e9e6
remove tap command for homebrew
No longer needed, it's already in core formula
2019-08-08 18:36:09 +03:00
winhung
4e25f78507 scrollDownMain now works properly with ScrollPastBottom enabled #31 2019-08-06 21:46:24 +08:00
anjiawei
9f50809bcf add image to conatiner view.
Update issue: https://github.com/jesseduffield/lazydocker/issues/144

Co-Authored-By: Jesse Duffield <jessedduffield@gmail.com>
2019-08-06 19:52:06 +08:00
winhung
b899796a52 removed scrollCount #31 2019-08-05 21:42:18 +08:00
winhung
082034d345 used ViewLinesHeight for scrolling down function #31 2019-08-04 11:07:48 +08:00
winhung
19dac181cf updated vendor for gocui #31 2019-08-04 11:05:13 +08:00
winhung
07bfb9255e implemented review comments by adding improved scrolling checking #31 2019-08-04 10:23:15 +08:00
Dawid Dziurla
8970352efc
Merge pull request #142 from pvande/fix/crash-on-credits-tab
Credits context crashes when trying to export InternalFunction
2019-08-01 16:52:16 +02:00
Pieter van de Bruggen
b5df76187b InternalFunction is not serializable to YAML 2019-07-31 20:48:15 -07:00
Dawid Dziurla
fc329ac565
README: set docker minimal version to match api
https://docs.docker.com/develop/sdk
2019-07-30 01:07:34 +02:00
lazydocker bot
b2878345b5 update docs 2019-07-29 09:36:29 +00:00
Jesse Duffield
c5996588d1 third time lucky 2019-07-29 19:34:09 +10:00
Jesse Duffield
2e58ff59fa turns out we can't use --rebase 2019-07-29 19:30:15 +10:00
Jesse Duffield
7213a312b1 fix CI update docs thing 2019-07-29 19:28:13 +10:00
Jesse Duffield
4dc8f76083
Merge pull request #107 from jesseduffield/bulk-commands
Support bulk commands in each of the panels
2019-07-29 19:01:20 +10:00
Jesse Duffield
bd3ce66234 bump api version so that we can e.g. prune containers via the sdk
update api version in readme
2019-07-29 18:59:12 +10:00
Jesse Duffield
1f9f82601b support bulk commands in each of the panels 2019-07-29 18:59:07 +10:00
Jesse Duffield
64863adc2b
Merge pull request #127 from mjarkk/origin/f115
Allow user setting the pull interval
2019-07-27 11:48:08 +10:00
mjarkk
5b3cdcd0aa
Fixed sugestion from jesse 2019-07-26 16:06:46 +02:00
winhung
42e69ac821 added test to increase coverage #31 2019-07-26 16:42:46 +08:00
Dawid Dziurla
339ee0596f
Merge pull request #138 from tobypinder/patch-1
Fix image reference
2019-07-25 11:32:05 +02:00
Toby Pinder
b2620b59a9
Fix image reference
According to rest of docs this should be "lazyteam"
2019-07-25 10:20:52 +01:00
winhung
4de994e030 scrolling does not go beyond the displayed text #31 2019-07-22 21:04:50 +08:00
Dawid Dziurla
b5f64c0a2e
README: lzd docker alias 2019-07-21 15:46:57 +02:00
Dawid Dziurla
a7ccf40d3f
Merge branch 'reworking-dockerfile' 2019-07-21 15:40:17 +02:00
Dawid Dziurla
0c82a7089c
Merge pull request #132 from muesli/gocritic-fixes
Simplified code a bit
2019-07-19 11:08:04 +02:00
Dawid Dziurla
3197f28e94
Merge pull request #133 from muesli/unconvert-fixes
Removed unnecessary string conversion
2019-07-19 11:03:07 +02:00
Dawid Dziurla
07e108d16c
Merge pull request #131 from muesli/gosimple-fixes
Simplified return and bool check code
2019-07-19 11:02:17 +02:00
Christian Muehlhaeuser
08861a53bf
Removed unnecessary string conversion
`s.expected` is already a string.
2019-07-19 05:01:29 +02:00
Christian Muehlhaeuser
78d7fda9ec
Simplified code a bit 2019-07-19 04:59:32 +02:00
Christian Muehlhaeuser
16b3eaedac
Simplified return and bool check code 2019-07-19 04:54:33 +02:00
Dawid Dziurla
857053de56
Merge pull request #129 from jesseduffield/xdg
Use xdg package instead of configdir
2019-07-18 02:59:03 +02:00
Quentin McGaw
25e877539d Removed PATH variable definition in Dockerfile
As @ibnesayeed mentionned, some environment variables are actually defined even in Scratch Docker images. `PATH` contains `/bin` so there is no need to overwrite it.
2019-07-16 05:06:35 -07:00
Quentin McGaw
80fdf3142b Switched to opencontainers.org labels for Docker image 2019-07-16 05:03:09 -07:00
Quentin McGaw
a497b56e36 Simplified readme instructions for Docker run and removed volume
- Volume did not work properly as the config would be persistent but not shared across restart of the container running interactively.
- Docker run instructions were therefore updated and simplified to bind mount the config directory as volume do not really work in this situation.
2019-07-16 05:02:48 -07:00
Quentin McGaw
84320dd4de Uses modules for Docker build 2019-07-16 05:00:52 -07:00
Quentin McGaw
dedafb3acf All binaries are in /bin directory. 2019-07-15 22:11:08 -07:00
Quentin McGaw
98697c544e Added Docker CLI binary to final image 2019-07-15 22:03:17 -07:00
Quentin McGaw
dec14665e1 Simplified docker run readme thanks to default volume 2019-07-15 22:02:04 -07:00
Dawid Dziurla
e413252d3b
mod: newer xdg package
need to use newer commit, because of BSD support
2019-07-13 13:08:01 +02:00
Dawid Dziurla
2b3d380d10
config: use xdg package instead of configdir 2019-07-13 12:56:51 +02:00
Dawid Dziurla
f384e57917
mod: tidy and vendor 2019-07-13 12:39:50 +02:00
Dawid Dziurla
f7d566b915
Merge pull request #128 from tecpromotion/patch-2
typos and sentence structure
2019-07-13 12:09:29 +02:00
Stefan Wendhausen
4b727fc24a
Update german.go 2019-07-13 11:59:32 +02:00
Stefan Wendhausen
9a558edf3b
typos and sentence structure
Corrected some typos and improved sentence structure
2019-07-13 11:56:36 +02:00
Dawid Dziurla
690a7c62f4
main: check for encoding error 2019-07-12 23:48:49 +02:00
Dawid Dziurla
8024ba77f1
main: nicer --version output 2019-07-12 23:41:40 +02:00
mjarkk
625c4a5084
Updated the config.md 2019-07-12 15:05:30 +02:00
mjarkk
63b1d871c5
Changed the sugestions 2019-07-12 15:04:07 +02:00
Jesse Duffield
c99bdaa5ff
Merge pull request #124 from jesseduffield/confirmation-options
show y/n options on confirmation panels
2019-07-12 21:01:43 +10:00
Jesse Duffield
7c87c21fa0 show y/n options on confirmation panels 2019-07-12 20:59:44 +10:00
mjarkk
fd4d4a1dc6
Made the information pulling time dynamic 2019-07-12 10:26:59 +02:00
Dawid Dziurla
0e81129024
README: set minimal docker version 2019-07-08 14:48:46 +02:00
Dawid Dziurla
bd16a78b06
Merge branch 'docker-sdk-update' 2019-07-08 14:44:51 +02:00
Jesse Duffield
06c80ec36f
Merge pull request #120 from SvenDowideit/add-image-and-volume-custom-commands
custom commands for images and volumes
2019-07-08 18:22:55 +10:00
Dowideit, Sven (O&A, St. Lucia)
cdf075b00d Initial working demo
Signed-off-by: Dowideit, Sven (O&A, St. Lucia) <Sven.Dowideit@csiro.au>
2019-07-08 17:01:15 +10:00
Jesse Duffield
cca69e64f3
Merge pull request #111 from mjarkk/f110
Added `e` keybinding for toggling visibility stopped containers
2019-07-07 22:50:44 +10:00
mjarkk
209456fdcd
Changed the keybinding to e 2019-07-07 14:44:59 +02:00
mjarkk
ec5bd8cd38
Updated keybindings file 2019-07-07 14:34:06 +02:00
Mark Kopenga
9bbd513a72
Update pkg/i18n/english.go
Co-Authored-By: Jesse Duffield <jessedduffield@gmail.com>
2019-07-07 14:33:23 +02:00
Mark Kopenga
0ca9235220
Merge branch 'master' into f110 2019-07-07 14:33:03 +02:00
mjarkk
401c6a39ed
Updated the keybindings 2019-07-07 14:30:18 +02:00
mjarkk
30a79faa49
Moved the filtering out of containers to the docker.go file 2019-07-07 14:19:35 +02:00
Jesse Duffield
a46074c581
Merge pull request #108 from jesseduffield/see-required-translations
add get_required_translations.go
2019-07-07 21:13:29 +10:00
Quentin McGaw
88896e9117 Removed terminal space check (gocui got fixed) 2019-07-07 12:19:44 +02:00
Jesse Duffield
87d657ccf2 only update docs on release 2019-07-07 13:07:13 +10:00
lazydocker bot
7e490b9ee0 update docs 2019-07-07 03:03:29 +00:00
Jesse Duffield
17fc1bd2e3
Merge pull request #114 from jesseduffield/ci-cheatsheet
update docs when merging to master
2019-07-07 13:03:15 +10:00
Jesse Duffield
018e151e1f update docs when merging to master 2019-07-07 13:01:08 +10:00
Jesse Duffield
cfd47b2877 add get_required_translations.go 2019-07-07 11:11:24 +10:00
Jesse Duffield
df7f9c4a99 update cheatsheets 2019-07-07 11:11:24 +10:00
Jesse Duffield
da6deff199
Merge pull request #112 from mjarkk/fix-git-typo
Cleaned up some code from lazygit
2019-07-07 11:05:10 +10:00
mjarkk
867459af56
Merge remote-tracking branch 'jesse/master' into fix-git-typo 2019-07-06 22:28:41 +02:00
mjarkk
fddbc7cdf8
Removed some lazygit code 2019-07-06 22:27:52 +02:00
mjarkk
b89b02eee5
Added h key binding 2019-07-06 21:20:21 +02:00
Quentin McGaw
a01f704d61 Added volume to Dockerfile so bind mounting is optional 2019-07-06 14:56:06 +02:00
Quentin McGaw
5532f1da80 Moved docker-compose.yml out of readme 2019-07-06 14:55:45 +02:00
Jesse Duffield
4dc13ebda5
Update README.md 2019-07-06 12:26:32 +10:00
Jesse Duffield
91e3c0fb0f
Merge pull request #103 from jesseduffield/window-check
bump gocui fork to check window size
2019-07-06 10:35:36 +10:00
Jesse Duffield
a680446596 bump gocui fork to check window size 2019-07-06 10:25:13 +10:00
Jesse Duffield
a5ec531b2b
Merge pull request #98 from mjarkk/fix-76
Add a limit to the c.StatsHistory
2019-07-06 09:26:03 +10:00
mjarkk
58baf82519
Added the sugestion from @jesseduffeild 2019-07-05 16:58:06 +02:00
Jesse Duffield
c04867b36d
Update Config.md 2019-07-05 23:03:22 +10:00
Jesse Duffield
b80b13d308
Update Config.md 2019-07-05 23:02:03 +10:00
Dawid Dziurla
feeb5c9bf6
go mod tidy 2019-07-05 14:53:34 +02:00
Quentin McGaw
92837ca1cb Buffering channel with a size of 1 2019-07-05 14:46:38 +02:00
Jesse Duffield
0d42164c8f
Update README.md 2019-07-05 22:45:06 +10:00
Quentin McGaw
d8b0e1872f Updated readme's Docker section
- No need to build for amd64 architectures, just pull automatically from Docker Hub when invoking `docker run ...`
- Build instructions only for ARM devices as Docker Hub does not build for ARM architectures really.
- Updated Docker image tag from `lazydocker` to `lazyteam/lazydocker`
- Added docker-compose.yml collapsible example
2019-07-05 14:43:41 +02:00
Quentin McGaw
ee6b990792 Applied changes from @dawidd6 to check on terminal space at start 2019-07-05 14:39:27 +02:00
Dawid Dziurla
bb319ddb59
adjust code to newer API 2019-07-05 14:18:44 +02:00
Dawid Dziurla
277681a0dc
use newer Docker SDK 2019-07-05 14:18:19 +02:00
Jesse Duffield
71acacbb0c Update issue templates 2019-07-05 21:59:28 +10:00
Jesse Duffield
a536c09dab set publish: false 2019-07-05 20:56:37 +10:00
Jesse Duffield
35a7c3a58d use devel snapcraft grade 2019-07-05 20:43:27 +10:00
Jesse Duffield
319b026c26 remove snapcraft.login after we've logged in 2019-07-05 20:31:41 +10:00
Jesse Duffield
3087f86b2d
Merge pull request #99 from jesseduffield/ci-snap-2
login to snapcraft when releasing
2019-07-05 20:29:56 +10:00
Jesse Duffield
a2abab02a1 login to snapcraft when releasing 2019-07-05 20:18:34 +10:00
Dawid Dziurla
dc87228ab6
Merge pull request #96 from jesseduffield/ci-snap
CI snapcraft installation
2019-07-05 09:55:03 +02:00
mjarkk
8ef19b781d
Added a litmit to the StatsHistory 2019-07-05 08:10:21 +02:00
Jesse Duffield
ea6eafe2df fix CI 2019-07-05 10:35:25 +10:00
Jesse Duffield
cd95ffce49
Merge pull request #97 from mjarkk/fix-94
Added rounding to timing (fixes #94)
2019-07-05 10:20:06 +10:00
Jesse Duffield
9207dc6faa
Merge pull request #88 from pvande/feature/display-project-directory
Show Project Directory in Status Panel
2019-07-05 10:19:18 +10:00
Quentin McGaw
29ad43c4a4 Fixed infinite loop for checking terminal space as discussed in PR
- All the checks are in the `waitForTerminalSpace` function
- It tries to run the program directly if the width is positive
- It then sleeps 50 milliseconds the first time if the width is zero
- If after the first sleep, the terminal width is still zero, it prints the waiting time to stderr and will keep on doing so with increasing sleep times.
2019-07-05 01:31:28 +02:00
Quentin McGaw
e637497fdb Removed useless installsuffix cgo from Docker Go build 2019-07-05 01:29:33 +02:00
mjarkk
74e8d6eb91
Added rounding to timeing 2019-07-04 21:25:49 +02:00
Dawid Dziurla
787bff1102
Merge pull request #85 from immanuelfodor/master
Map the aarch64 platform to the arm64 binary
2019-07-04 20:32:04 +02:00
immanuelfodor
58ba364348 Map other different platforms to the binaries 2019-07-04 20:21:31 +02:00
immanuelfodor
87f9ef70d3 Merge branch 'master' of https://github.com/jesseduffield/lazydocker 2019-07-04 20:20:20 +02:00
Dawid Dziurla
f84c36c6ad
ci: get squashfs-tools too 2019-07-04 17:48:05 +02:00
Dawid Dziurla
8ecb872c1d
ci: install snapcraft before goreleaser 2019-07-04 17:22:09 +02:00
Dawid Dziurla
bdf53fa911
ci: add snapcraft installation script 2019-07-04 17:21:46 +02:00
Dawid Dziurla
88dca3f40b
README: remove snap installation instructions
temporarily
2019-07-04 17:15:34 +02:00
Immánuel!
ed744bbe80
Map the i386 platform to the x86 binary 2019-07-04 15:58:48 +02:00
Jesse Duffield
36c16804ec
Merge pull request #93 from jesseduffield/snapcraft
use classic confinement for snapcraft
2019-07-04 22:17:25 +10:00
Jesse Duffield
42a3805b0f use classic confinement for snapcraft 2019-07-04 21:57:24 +10:00
Jesse Duffield
b002816e05
Merge pull request #86 from dawidd6/goreleaser
Update Goreleaser
2019-07-04 21:32:46 +10:00
Jesse Duffield
6883c08f62
Merge pull request #90 from oguzcandemircan/turkhis-language-support
Added Turkish language support
2019-07-04 21:05:23 +10:00
Jesse Duffield
2bf13927c4
Merge pull request #92 from dawidd6/polish
Add Polish translation
2019-07-04 21:05:08 +10:00
Dawid Dziurla
16e596efaa
i18n: userLang variable was set needlessly 2019-07-04 11:17:39 +02:00
Dawid Dziurla
08c711a334
i18n: add polish translation 2019-07-04 11:12:21 +02:00
Oğuzcan Demircan
ce6cffa784
"Error return value of mergo.Merge is not checked (from errcheck)" issue solved 2019-07-04 11:53:33 +03:00
Oğuzcan Demircan
563630468c
Create Keybindings_tr.md 2019-07-04 10:13:16 +03:00
Oğuzcan Demircan
f811a3708a
Update turkish.go 2019-07-04 08:48:08 +03:00
Pieter van de Bruggen
b3422c87f4 Pull the project name from docker-compose labels, when available 2019-07-03 16:58:42 -07:00
Jesse Duffield
78a5ff9220
Merge pull request #84 from stdedos/patch-1
bug_report.md: fix `git` command
2019-07-04 09:19:36 +10:00
Pieter van de Bruggen
277882e78b Updating another missed instance of NewAppConfig
Thanks @dawidd6!
2019-07-03 11:27:32 -07:00
Pieter van de Bruggen
f9ba4b875d Updating config tests with new parameter 2019-07-03 11:24:23 -07:00
Pieter van de Bruggen
86dfebdeaf Replace Project panel content with the project directory.
In the future, it may be worth trying to compute this from ComposeFiles.
2019-07-03 10:59:45 -07:00
Pieter van de Bruggen
8654d177d3 Move name to information area 2019-07-03 10:52:43 -07:00
Pieter van de Bruggen
12f3cfab55 Rename StatusPanel to ProjectPanel 2019-07-03 10:48:55 -07:00
Dawid Dziurla
73d9a36cf7
README: snap installation instructions 2019-07-03 19:33:54 +02:00
Dawid Dziurla
27ae69a3d0
goreleaser: switch to strict snapcraft confinement 2019-07-03 19:23:12 +02:00
Dawid Dziurla
8dfd8b6c18
goreleaser: update
- move env from local context to global
- define separate build for snap
- build for armv6 and armv7
- fix some deprecations:
  - archive -> archives
  - brew -> brews
- replace 386 with x86 (for consistency with x86_64)
- add snapcraft (classic confinement, must be consulted with Canonical)
2019-07-03 19:05:00 +02:00
Oguzcan Demircan
3bca81dd9a Added: Turkish language support 2019-07-03 18:17:27 +03:00
immanuelfodor
430e5ec3cc Map the aarch64 platform to the arm64 binary 2019-07-03 17:17:23 +02:00
Stavros Ntentos
200acddec8
bug_report.md: fix git command 2019-07-03 18:14:13 +03:00
Quentin McGaw
7107a4cfa1 Added config bind mount instruction to Docker container 2019-07-02 20:26:15 +02:00
Quentin McGaw
d404716d0a Fixed bad loop for checking terminal size 2019-07-02 20:14:34 +02:00
Quentin McGaw
767488a5e7 Detailed development Docker build in readme with all build args 2019-07-02 20:08:56 +02:00
Quentin McGaw
20f4ad8e49 Resolved screen size issue, final Docker image uses Scratch 2019-07-02 20:08:29 +02:00
Quentin McGaw
d78648cabb Added Docker automated build hook and readme badges for Docker
- Build arguments are injected by the build hook
- Badges added: number of pulls, stars and if the build is automated
2019-07-02 14:06:44 +02:00
Quentin McGaw
18d34d5c01 VERSION and VCS_REF build arguments are unset by default 2019-07-02 13:57:44 +02:00
Quentin McGaw
c89e396121 Lazydocker can be ran directly as entrypoint
Thanks to the script:

```sh
#!/bin/sh

resize > /dev/null
lazydocker $@
```
2019-07-01 15:39:52 +02:00
Quentin McGaw
580936c73c Fixed docker build typo (both URL and PATH) 2019-07-01 15:38:21 +02:00
Quentin McGaw
fd70aaaf05 Git commit reference mechanism changed
- Git commit is passed as a build argument as explained in README.md
- `.git` directory is ignored by Docker for a quicker build and smaller context
- the build arg VCS_REF is used both to tag the image and the Go program
2019-07-01 15:37:55 +02:00
Quentin McGaw
330131b6ca Fixed Docker regarding no size window with gocui
- Runs the container with `/bin/sh` initially
- Invoke `lazydocker` from within the container, or gocui fails as the terminal window is invalid at launch
2019-07-01 11:57:50 +02:00
Quentin McGaw
ceb8cfcd76 Added Docker labels and Go commit, version and buildSource 2019-07-01 11:51:08 +02:00
Quentin McGaw
1542c70b53 Reworked Dockerfile for a more production oriented usage
- Added .dockerignore to speed up build context and avoid rebuilding when unecessary
- Specified Alpine and Go versions as build arguments
- Specified Go target CPU architecture as build arguments to be able to build for ARM devices
- Specified base images as build arguments to be able to build for ARM devices
- Trimmed down size of final image using `-a -installsuffix cgo -ldflags="-s -w"` go build flags and by copying the statically built binary only to the final image
- Added clear build and run instructions for the Docker container
2019-07-01 09:57:48 +02:00
1566 changed files with 302597 additions and 117104 deletions

View file

@ -1,57 +1,81 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.12
environment:
GO111MODULE: "on"
working_directory: /go/src/github.com/jesseduffield/lazydocker
steps:
- checkout
- run:
name: Run gofmt -s
command: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1;
fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "go.sum" }}-v5
- run:
name: Run tests
command: |
./test.sh
- run:
name: Push on codecov result
command: |
bash <(curl -s https://codecov.io/bash)
- run:
name: Compile project on every platform
command: |
go get github.com/mitchellh/gox
gox -mod=vendor -parallel 10 -os "linux freebsd" -osarch "darwin/i386 darwin/amd64"
- save_cache:
key: pkg-cache-{{ checksum "go.sum" }}-v5
paths:
- ~/.cache/go-build
release:
# global environment variables
# '& ' syntax is a YAML thing for creating referenceable documents
env-shared: &env-shared
environment:
GOFLAGS: "-mod=vendor"
# build steps shared across different jobs
build-shared: &build-shared
steps:
- checkout
- run:
name: Run gofmt -s
command: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1;
fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "go.sum" }}-v5
- run:
name: Run tests
command: |
go test -v ./...
- run:
name: Get gox
# -mod=vendor doesn't work with go get while in source tree with go.mod in it
working_directory: "/tmp"
command: |
go get github.com/mitchellh/gox
- run:
name: Compile project on every platform
command: |
gox -parallel 10 -os "linux freebsd" -osarch "darwin/i386 darwin/amd64 windows/i386 windows/amd64"
- save_cache:
key: pkg-cache-{{ checksum "go.sum" }}-v5
paths:
- ~/.cache/go-build
jobs:
# matrix build jobs run on every commit
# building on versioned Go and the latest one
# '<<: *' syntax is a YAML thing for including documents
"golang:1.17":
<<: *env-shared
<<: *build-shared
docker:
- image: circleci/golang:1.12
working_directory: /go/src/github.com/jesseduffield/lazydocker
- image: circleci/golang:1.17
"golang:latest":
<<: *env-shared
<<: *build-shared
docker:
- image: circleci/golang:latest
# release job run only when a commit is tagged
release:
<<: *env-shared
docker:
- image: circleci/golang:1.17
steps:
- checkout
- run:
name: Run gorelease
command: |
curl -sL https://git.io/goreleaser | bash
- run:
name: Update docs
command: |
./.circleci/update_docs.sh
workflows:
version: 2
build:
jobs:
- build
# build matrix
- "golang:1.17"
- "golang:latest"
release:
jobs:
- release:

25
.circleci/install_snapcraft.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
sudo apt-get update
sudo apt-get install -y \
squashfs-tools \
python3-pip \
python3-apt \
python3-debian \
python3-pyelftools \
python3-yaml \
python3-tabulate \
python3-jsonschema \
python3-click \
python3-pymacaroons \
python3-simplejson \
python3-progressbar \
python3-requests-toolbelt \
python3-requests-unixsocket
sudo pip3 install \
petname \
snapcraft
snapcraft --version

24
.circleci/update_docs.sh Executable file
View file

@ -0,0 +1,24 @@
#!/bin/bash
set -ex
# see if we have a new cheatsheet
# if other docs end up being generated automatically we can chuck in the relevant scripts here
go run scripts/generate_cheatsheet.go
# commit and push if we have a change
if [[ -z $(git status -s -- docs/*) ]]; then
echo "no changes to commit in the docs directory"
exit 0
fi
echo "committing updated docs"
git config user.name "lazydocker bot"
git config user.email "jessedduffield@gmail.com"
git checkout master # just making sure we're up to date
git pull
git add docs/*
git commit -m "update docs"
git push -u origin master

15
.claude/settings.json Normal file
View file

@ -0,0 +1,15 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; case \"$f\" in *.go) gofumpt -w \"$f\" ;; esac; } 2>/dev/null || true"
}
]
}
]
}
}

11
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
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

@ -0,0 +1,54 @@
{
"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"
}

12
.dockerignore Normal file
View file

@ -0,0 +1,12 @@
.circleci
.github
docs
test
.gitignore
.goreleaser.yml
*.md
coverage.txt
Dockerfile
LICENSE
test.sh
.git

4
.github/FUNDING.yml vendored
View file

@ -1,4 +1,4 @@
# These are supported funding model platforms
ko_fi: jesseduffield
custom: ['https://donorbox.org/lazydocker']
github: [jesseduffield]
custom: ['https://donorbox.org/lazygit']

View file

@ -26,7 +26,7 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Lazydocker Version [e.g. v0.1.45]
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
- The last commit id if you built project from sources (run : ```git rev-parse HEAD```)
**Additional context**
Add any other context about the problem here.

10
.github/ISSUE_TEMPLATE/discussion.md vendored Normal file
View file

@ -0,0 +1,10 @@
---
name: Discussion
about: For starting a discussion
title: ''
labels: discussion
assignees: ''
---

27
.github/workflows/cd.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: Continuous Delivery
on:
push:
tags:
- "v*"
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Unshallow repo
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.24.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
with:
distribution: goreleaser
version: v1.17.2
args: release --clean
env:
GITHUB_TOKEN: ${{secrets.TOKEN_GITHUB}}

127
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,127 @@
name: Continuous Integration
env:
GO_VERSION: 1.24
on:
push:
branches:
- master
pull_request:
jobs:
ci:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
name: ci - ${{matrix.os}}
runs-on: ${{matrix.os}}
env:
GOFLAGS: -mod=vendor
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
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
run: |
bash ./test.sh
build:
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
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Build linux binary
run: |
GOOS=linux go build
- name: Build windows binary
run: |
GOOS=windows go build
- name: Build darwin binary
run: |
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:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
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
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Lint
uses: golangci/golangci-lint-action@v3.1.0
with:
version: latest
- name: Format code
run: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1
fi
- name: errors
run: golangci-lint run
if: ${{ failure() }}

28
.github/workflows/sponsors.yml vendored Normal file
View file

@ -0,0 +1,28 @@
# see https://github.com/JamesIves/github-sponsors-readme-action
name: Generate Sponsors README
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1.2.2
with:
token: ${{ secrets.TOKEN_GITHUB }}
file: "README.md"
if: ${{ github.repository == 'jesseduffield/lazydocker' }}
- name: Create Pull Request 🚀
uses: peter-evans/create-pull-request@v6
with:
commit-message: "README.md: Update Sponsors"
title: "README.md: Update Sponsors"
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
labels: "ignore-for-release"
delete-branch: true

1
.gitignore vendored
View file

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

27
.golangci.yml Normal file
View file

@ -0,0 +1,27 @@
linters:
enable:
- gofumpt
- thelper
- goimports
- tparallel
- wastedassign
- unparam
- prealloc
- unconvert
- exhaustive
- makezero
- nakedret
# - goconst # TODO: enable and fix issues
fast: false
linters-settings:
exhaustive:
default-signifies-exhaustive: true
nakedret:
# the gods will judge me but I just don't like naked returns at all
max-func-lines: 0
run:
go: '1.21'
timeout: 10m

View file

@ -1,11 +1,15 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
env:
- CGO_ENABLED=0
- GOFLAGS=-mod=vendor
- GO111MODULE=auto
builds:
- env:
- CGO_ENABLED=0
- id: binary
goos:
# - freebsd
# - windows # may reenable later
- windows
- darwin
- linux
goarch:
@ -13,40 +17,116 @@ builds:
- arm
- arm64
- 386
goarm:
- 6
- 7
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
- id: snap
goos:
- linux
goarch:
- amd64
- arm
- arm64
- 386
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=snap
archives:
- builds:
- binary
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: x86
amd64: x86_64
format_overrides:
- goos: windows
format: zip
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
snapshot:
name_template: '{{ .Tag }}-next'
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^bump'
brew:
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazydocker
- "^docs:"
- "^test:"
- "^bump"
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazydocker/'
brews:
- tap:
owner: jesseduffield
name: homebrew-lazydocker
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for docker, written in Go'
# Your app's homepage.
# Default is empty.
homepage: "https://github.com/jesseduffield/lazydocker/"
# Your app's description.
# Default is empty.
description: "A simple terminal UI for docker, written in Go"
#snapcrafts:
# - builds:
# - snap
#
# replacements:
# linux: Linux
# 386: x86
# amd64: x86_64
#
# # Wether to publish the snap to the snapcraft store.
# # Remember you need to `snapcraft login` first.
# # Defaults to false.
# publish: false
#
# # Single-line elevator pitch for your amazing snap.
# # 79 char long at most.
# summary: The lazier way to manage everything docker
#
# # This the description of your snap. You have a paragraph or two to tell the
# # most important story about your snap. Keep it under 100 words though,
# # we live in tweetspace and your description wants to look good in the snap
# # store.
# description: 'A simple terminal UI for docker, written in Go'
#
# # A guardrail to prevent you from releasing a snap to all your users before
# # it is ready.
# # `devel` will let you release only to the `edge` and `beta` channels in the
# # store. `stable` will let you release also to the `candidate` and `stable`
# # channels. More info about channels here:
# # https://snapcraft.io/docs/reference/channels
# # TODO: reset to `stable` when we've been manually reviewed: https://forum.snapcraft.io/t/request-for-classic-confinement-for-lazydocker/12155
# grade: devel
#
# # Snaps can be setup to follow three different confinement policies:
# # `strict`, `devmode` and `classic`. A strict confinement where the snap
# # can only read and write in its own namespace is recommended. Extra
# # permissions for strict snaps can be declared as `plugs` for the app, which
# # are explained later. More info about confinement here:
# # https://snapcraft.io/docs/reference/confinement
# confinement: classic
#
# # Your app's license, based on SPDX license expressions: https://spdx.org/licenses
# # Default is empty.
# license: MIT
#
# # # Each binary built by GoReleaser is an app inside the snap. In this section
# # # you can declare extra details for those binaries. It is optional.
# # apps:
#
# # # The name of the app must be the same name as the binary built or the snapcraft name.
# # lazydocker:
#
# # # If your app requires extra permissions to work outside of its default
# # # confined space, declare them here.
# # # You can read the documentation about the available plugs and the
# # # things they allow:
# # # https://snapcraft.io/docs/reference/interfaces.
# # plugs: []

5
CLAUDE.md Normal file
View file

@ -0,0 +1,5 @@
# CLAUDE.md
## Build & test
- All Go commands need `GOFLAGS=-mod=vendor` (deps are vendored, including the `jesseduffield/gocui` fork and the Docker SDK).
- Unit tests: `GOFLAGS=-mod=vendor go test ./...`

View file

@ -37,6 +37,7 @@ c) if you need to bump a dependency e.g. jesseduffield/gocui, use
```
GOFLAGS= go get -u github.com/jesseduffield/gocui@master
go mod tidy
go mod vendor
```
@ -48,6 +49,7 @@ c) if you need to bump a dependency e.g. jesseduffield/gocui, use
```
go get -u github.com/jesseduffield/gocui@master
go mod tidy
go mod vendor
```

View file

@ -1,15 +1,49 @@
# run with:
# docker build -t lazydocker .
# docker run -it lazydocker:latest /bin/sh -l
ARG BASE_IMAGE_BUILDER=golang
ARG ALPINE_VERSION=3.20
ARG GO_VERSION=1.23
FROM golang:alpine
WORKDIR /go/src/github.com/jesseduffield/lazydocker/
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
ARG GOARCH=amd64
ARG GOARM
ARG VERSION
ARG VCS_REF
WORKDIR /tmp/gobuild
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} GOARM=${GOARM} \
go build -a -mod=vendor \
-ldflags="-s -w \
-X main.commit=${VCS_REF} \
-X main.version=${VERSION} \
-X main.buildSource=Docker"
FROM alpine:latest
RUN apk add -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazydocker/
COPY --from=0 /go/src/github.com/jesseduffield/lazydocker /go/src/github.com/jesseduffield/lazydocker
COPY --from=0 /go/src/github.com/jesseduffield/lazydocker/lazydocker /bin/
RUN echo "alias gg=lazydocker" >> ~/.profile
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS docker-builder
ARG GOARCH=amd64
ARG GOARM
ARG DOCKER_VERSION=v27.0.3
RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev
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
ENV CGO_ENABLED=0 \
GOARCH=${GOARCH} \
GOARM=${GOARM} \
DISABLE_WARN_OUTSIDE_CONTAINER=1
RUN ./scripts/build/binary
RUN rm build/docker && mv build/docker-linux-* build/docker
FROM scratch
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL \
org.opencontainers.image.authors="jessedduffield@gmail.com" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.url="https://github.com/jesseduffield/lazydocker" \
org.opencontainers.image.documentation="https://github.com/jesseduffield/lazydocker" \
org.opencontainers.image.source="https://github.com/jesseduffield/lazydocker" \
org.opencontainers.image.title="lazydocker" \
org.opencontainers.image.description="The lazier way to manage everything docker"
ENTRYPOINT [ "/bin/lazydocker" ]
COPY --from=docker-builder /go/src/github.com/docker/cli/build/docker /bin/docker
COPY --from=builder /tmp/gobuild/lazydocker /bin/lazydocker

236
README.md

File diff suppressed because one or more lines are too long

BIN
assets/subble-job-ad.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
assets/subble.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
assets/tuple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
assets/warp2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

0
config/config.yml Normal file
View file

File diff suppressed because it is too large Load diff

15
docker-compose.yml Normal file
View file

@ -0,0 +1,15 @@
services:
lazydocker:
build:
context: https://github.com/jesseduffield/lazydocker.git
args:
BASE_IMAGE_BUILDER: golang
GOARCH: amd64
GOARM:
image: lazyteam/lazydocker
container_name: lazydocker
stdin_open: true
tty: true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/.config/jesseduffield/lazydocker

View file

@ -1,59 +1,101 @@
# User Config:
## Opening The User Config
The location of the user config will differ depending on your OS. You can open it via lazydocker by opening the application, clicking on the 'project' panel at the top left and pressing 'o' (or pressing 'e' if your files open in vim).
Changes to the user config will only take place after closing and re-opening lazydocker
### Locations:
- OSX: `~/Library/Application Support/jesseduffield/lazydocker/config.yml`
- Linux: `~/.config/lazydocker/config.yml`
- Windows: `C:\Users\<User>\AppData\Roaming\lazydocker\config.yml`
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:
```
```yml
gui:
scrollHeight: 2
language: "auto" # one of 'auto' | 'en' | 'pl' | 'nl' | 'de' | 'tr'
border: "rounded" # one of 'rounded' | 'single' | 'double' | 'hidden'
theme:
activeBorderColor:
- green
- bold
- green
- bold
inactiveBorderColor:
- white
- white
selectedLineBgColor:
- blue
optionsTextColor:
- blue
- blue
returnImmediately: false
wrapMainPanel: false
reporting: undetermined
wrapMainPanel: true
# Side panel width as a ratio of the screen's width
sidePanelWidth: 0.333
# Determines whether we show the bottom line (the one containing keybinding
# info and the status of the app).
showBottomLine: true
# When true, increases vertical space used by focused side panel,
# creating an accordion effect
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:
timestamps: false
since: '60m' # set to '' to show all logs
tail: '' # set to 200 to show last 200 lines of logs
commandTemplates:
dockerCompose: docker-compose
dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates
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 }}'
stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}'
serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}'
viewServiceLogs: '{{ .DockerCompose }} logs --follow {{ .Service.Name }}'
rebuildService: '{{ .DockerCompose }} up -d --build {{ .Service.Name }}'
recreateService: '{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}'
viewContainerLogs: docker logs --timestamps --follow --since=60m {{ .Container.ID
}}
containerLogs: docker logs --timestamps --follow --since=60m {{ .Container.ID }}
allLogs: '{{ .DockerCompose }} logs --tail=300 --follow'
viewAlLogs: '{{ .DockerCompose }} logs'
dockerComposeConfig: '{{ .DockerCompose }} config'
checkDockerComposeConfig: '{{ .DockerCompose }} config --quiet'
serviceTop: '{{ .DockerCompose }} top {{ .Service.Name }}'
customCommands:
containers:
- name: bash
attach: true
command: docker exec -it {{ .Container.ID }} /bin/sh
serviceNames: []
oS:
openCommand: open {{filename}}
openLinkCommand: open {{link}}
update:
method: never
stats:
graphs:
- caption: CPU (%)
statPath: DerivedStats.CPUPercentage
color: blue
- caption: Memory (%)
statPath: DerivedStats.MemoryPercentage
color: green
- caption: CPU (%)
statPath: DerivedStats.CPUPercentage
color: blue
- caption: Memory (%)
statPath: DerivedStats.MemoryPercentage
color: green
```
## To see what all of the config options mean, and what other options you can set, see https://godoc.org/github.com/jesseduffield/lazydocker/pkg/config
## To see what all of the config options mean, and what other options you can set, see [here](https://godoc.org/github.com/jesseduffield/lazydocker/pkg/config)
## Color Attributes:
@ -72,3 +114,32 @@ The available attributes are:
- bold
- reverse # useful for high-contrast
- underline
## Custom Commands
You can add custom commands like so:
```yaml
customCommands:
containers:
- name: bash
attach: true
command: 'docker exec -it {{ .Container.ID }} bash'
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
You can add replacements like so:
```yaml
replacements:
imageNamePrefixes:
'123456789012.dkr.ecr.us-east-1.amazonaws.com': '<prod>'
'923456789999.dkr.ecr.us-east-1.amazonaws.com': '<dev>'
```

View file

@ -1,68 +1,114 @@
# Lazydocker menu
_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._
## Status
# Lazydocker menü
## Projekt
<pre>
<kbd>e</kbd>: bearbeite lazydocker Konfiguration
<kbd>o</kbd>: öffne lazydocker Konfiguration
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes tab
<kbd>m</kbd>: zeige Protokoll
<kbd>enter</kbd>: fokusieren aufs Hauptpanel
</pre>
## Containers
<pre>
<kbd>m</kbd>: zeige Protokolle
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre>
## Container
<pre>
<kbd>d</kbd>: entfernen
<kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause
<kbd>s</kbd>: anhalten
<kbd>r</kbd>: neustarten
<kbd>a</kbd>: anbinden
<kbd>D</kbd>: entferne verlassene Container
<kbd>m</kbd>: zeige Protokolle
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>enter</kbd>: fokusieren aufs Hauptpanel
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre>
## Services
## Dienste
<pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: entferne Container
<kbd>s</kbd>: anhalten
<kbd>p</kbd>: pause
<kbd>r</kbd>: neustarten
<kbd>S</kbd>: start
<kbd>a</kbd>: anbinden
<kbd>m</kbd>: zeige Protokolle
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>U</kbd>: up project
<kbd>D</kbd>: down project
<kbd>R</kbd>: zeige Neustartoptionen
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>enter</kbd>: fokusieren aufs Hauptpanel
<kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>/</kbd>: filter list
</pre>
## Images
<pre>
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>d</kbd>: entferne Image
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>d</kbd>: entferne Image
<kbd>D</kbd>: entferne unbenutzte Images
<kbd>enter</kbd>: fokusieren aufs Hauptpanel
<kbd>/</kbd>: filter list
</pre>
## Volumes
<pre>
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
<kbd>d</kbd>: entferne Volume
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
<kbd>[</kbd>: vorheriges Tab
<kbd>]</kbd>: nächstes Tab
<kbd>d</kbd>: entferne Volumen
<kbd>D</kbd>: entferne unbenutzte Volumen
<kbd>enter</kbd>: fokusieren aufs Hauptpanel
<kbd>/</kbd>: filter list
</pre>
## Main
## 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>
## Haupt
<pre>
<kbd>esc</kbd>: zurück
</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,64 +1,97 @@
_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
## Status
## Project
<pre>
<kbd>e</kbd>: edit lazydocker config
<kbd>o</kbd>: open lazydocker config
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>m</kbd>: view logs
<kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre>
## Containers
<pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove
<kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause
<kbd>s</kbd>: stop
<kbd>r</kbd>: restart
<kbd>a</kbd>: attach
<kbd>D</kbd>: prune exited containers
<kbd>m</kbd>: view logs
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: run predefined custom command
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre>
## Services
<pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: remove containers
<kbd>s</kbd>: stop
<kbd>p</kbd>: pause
<kbd>r</kbd>: restart
<kbd>S</kbd>: start
<kbd>a</kbd>: attach
<kbd>m</kbd>: view logs
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>U</kbd>: up project
<kbd>D</kbd>: down project
<kbd>R</kbd>: view restart options
<kbd>c</kbd>: run predefined custom command
<kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>/</kbd>: filter list
</pre>
## Images
<pre>
<kbd>c</kbd>: run predefined custom command
<kbd>d</kbd>: remove image
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove image
<kbd>D</kbd>: prune unused images
<kbd>enter</kbd>: focus main panel
<kbd>/</kbd>: filter list
</pre>
## Volumes
<pre>
<kbd>c</kbd>: run predefined custom command
<kbd>d</kbd>: remove volume
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus main panel
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove volume
<kbd>D</kbd>: prune unused volumes
<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>
## Main
@ -66,3 +99,16 @@
<pre>
<kbd>esc</kbd>: return
</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

@ -0,0 +1,114 @@
_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

@ -0,0 +1,114 @@
_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,68 +1,114 @@
_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
## Status
## Project
<pre>
<kbd>e</kbd>: verander lazydocker configuratie
<kbd>o</kbd>: open lazydocker configuratie
<kbd>e</kbd>: verander 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>: volgende tab
<kbd>m</kbd>: bekijk logs
<kbd>enter</kbd>: focus hooft panneel
<kbd>/</kbd>: filter list
</pre>
## Containers
<pre>
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>d</kbd>: verwijder
<kbd>e</kbd>: verberg gestopte containers
<kbd>p</kbd>: pause
<kbd>s</kbd>: stop
<kbd>r</kbd>: herstart
<kbd>a</kbd>: vastmaken
<kbd>D</kbd>: verwijder verlaten containers
<kbd>a</kbd>: verbinden
<kbd>m</kbd>: bekijk logs
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>enter</kbd>: focus hooft panneel
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre>
## Services
## Diensten
<pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: verwijder containers
<kbd>s</kbd>: stop
<kbd>p</kbd>: pause
<kbd>r</kbd>: herstart
<kbd>a</kbd>: vermaken
<kbd>S</kbd>: start
<kbd>a</kbd>: verbinden
<kbd>m</kbd>: bekijk logs
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>U</kbd>: up project
<kbd>D</kbd>: down project
<kbd>R</kbd>: bekijk herstart opties
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>enter</kbd>: focus hooft panneel
<kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>/</kbd>: filter list
</pre>
## Images
<pre>
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>d</kbd>: verwijder image
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>d</kbd>: verwijder image
<kbd>D</kbd>: verwijder nietgebruikte images
<kbd>enter</kbd>: focus hooft panneel
<kbd>/</kbd>: filter list
</pre>
## Volumes
<pre>
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
<kbd>d</kbd>: verwijder volume
<kbd>b</kbd>: view bulk commands
<kbd>enter</kbd>: focus hoofdpaneel
<kbd>[</kbd>: vorige tab
<kbd>]</kbd>: volgende tab
<kbd>d</kbd>: verwijder volume
<kbd>D</kbd>: verwijder nietgebruikte images
<kbd>enter</kbd>: focus hooft panneel
<kbd>/</kbd>: filter list
</pre>
## Main
## 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>
## Hoofd
<pre>
<kbd>esc</kbd>: terug
</pre>
## Globaal
<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,68 +1,114 @@
_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
## Status
## Projekt
<pre>
<kbd>e</kbd>: edit lazydocker config
<kbd>o</kbd>: open lazydocker config
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>m</kbd>: view logs
<kbd>enter</kbd>: focus main panel
<kbd>e</kbd>: edytuj 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>: następna zakładka
<kbd>/</kbd>: filter list
</pre>
## Containers
## Kontenery
<pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove
<kbd>s</kbd>: stop
<kbd>r</kbd>: restart
<kbd>a</kbd>: attach
<kbd>D</kbd>: prune exited containers
<kbd>m</kbd>: view logs
<kbd>c</kbd>: run predefined custom command
<kbd>enter</kbd>: focus main panel
<kbd>d</kbd>: usuń
<kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause
<kbd>s</kbd>: zatrzymaj
<kbd>r</kbd>: restartuj
<kbd>a</kbd>: przyczep
<kbd>m</kbd>: pokaż logi
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre>
## Services
## Serwisy
<pre>
<kbd>d</kbd>: remove containers
<kbd>s</kbd>: stop
<kbd>r</kbd>: restart
<kbd>a</kbd>: attach
<kbd>m</kbd>: view logs
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>R</kbd>: view restart options
<kbd>c</kbd>: run predefined custom command
<kbd>enter</kbd>: focus main panel
<kbd>u</kbd>: up service
<kbd>d</kbd>: usuń kontenery
<kbd>s</kbd>: zatrzymaj
<kbd>p</kbd>: pause
<kbd>r</kbd>: restartuj
<kbd>S</kbd>: start
<kbd>a</kbd>: przyczep
<kbd>m</kbd>: pokaż logi
<kbd>U</kbd>: up project
<kbd>D</kbd>: down project
<kbd>R</kbd>: pokaż opcje restartu
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: skup na głównym panelu
<kbd>[</kbd>: poprzednia zakładka
<kbd>]</kbd>: następna zakładka
<kbd>/</kbd>: filter list
</pre>
## Images
## Obrazy
<pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove image
<kbd>D</kbd>: prune unused images
<kbd>enter</kbd>: focus main panel
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>d</kbd>: usuń obraz
<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>
## Volumes
## Wolumeny
<pre>
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: next tab
<kbd>d</kbd>: remove volume
<kbd>D</kbd>: prune unused volumes
<kbd>enter</kbd>: focus main panel
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
<kbd>d</kbd>: usuń wolumen
<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>
## Main
## Sieci
<pre>
<kbd>esc</kbd>: return
<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>
## Główne
<pre>
<kbd>esc</kbd>: powrót
</pre>
## Globalne
<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

@ -0,0 +1,114 @@
_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

@ -0,0 +1,114 @@
_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ü
## Proje
<pre>
<kbd>e</kbd>: lazzydocker ayarlarını düzenle
<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>: sonraki sekme
<kbd>/</kbd>: filter list
</pre>
## Konteynerler
<pre>
<kbd>d</kbd>: kaldır
<kbd>e</kbd>: hide/show stopped containers
<kbd>p</kbd>: pause
<kbd>s</kbd>: durdur
<kbd>r</kbd>: yeniden başlat
<kbd>a</kbd>: bağlan/iliştir
<kbd>m</kbd>: kayıt defterini görüntüle
<kbd>E</kbd>: exec shell
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>b</kbd>: view bulk commands
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre>
## Servisler
<pre>
<kbd>u</kbd>: up service
<kbd>d</kbd>: konteynerleri kaldır
<kbd>s</kbd>: durdur
<kbd>p</kbd>: pause
<kbd>r</kbd>: yeniden başlat
<kbd>S</kbd>: start
<kbd>a</kbd>: bağlan/iliştir
<kbd>m</kbd>: kayıt defterini görüntüle
<kbd>U</kbd>: up project
<kbd>D</kbd>: down project
<kbd>R</kbd>: yeniden başlatma seçeneklerini görüntüle
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>b</kbd>: view bulk commands
<kbd>E</kbd>: exec shell
<kbd>w</kbd>: open in browser (first port is http)
<kbd>enter</kbd>: ana panele odaklan
<kbd>[</kbd>: önceki sekme
<kbd>]</kbd>: sonraki sekme
<kbd>/</kbd>: filter list
</pre>
## Imajlar
<pre>
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>d</kbd>: imajı 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>
## Alanlar
<pre>
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
<kbd>d</kbd>: alanı 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>
## 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>
## Ana
<pre>
<kbd>esc</kbd>: dönüş
</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

@ -0,0 +1,114 @@
_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>

BIN
docs/resources/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -1,62 +0,0 @@
ids = %w[
AnonymousReportingPrompt
AnonymousReportingTitle
attach
cancel
close
Confirm
confirmPruneImages
ConfirmQuit
ContainersTitle
CustomCommand
Donate
EditConfig
Error
ErrorOccurred
execute
ImagesTitle
menu
mustForceToRemoveContainer
navigate
nextContext
NoContainers
NoImages
NotEnoughSpace
NoViewMachingNewLineFocusedSwitchStatement
OpenConfig
pressEnterToReturn
previousContext
pruneImages
PruningStatus
remove
removeImage
removeService
removeWithoutPrune
removeWithVolumes
RemovingStatus
resizingPopupPanel
restart
RestartingStatus
RunningSubprocess
scroll
ServicesTitle
StatusTitle
stop
StopContainer
StoppingStatus
StopService
viewLogs
]
f = File.read('pkg/i18n/english.go')
f.lines.each_with_index do |line, index|
if line[/ID:/]
stripped = line.strip.gsub(/ID: *"/, '",').gsub('",', '')
if ids.include?(stripped)
key = stripped[0].upcase + stripped[1..-1]
value = f.lines[index+1].strip.gsub(/Other: *"/, '').gsub('",', '')
puts "#{key}: \"#{value}\","
end
end
end

99
go.mod
View file

@ -1,38 +1,85 @@
module github.com/jesseduffield/lazydocker
go 1.12
go 1.22
toolchain go1.23.6
require (
github.com/Microsoft/go-winio v0.4.12 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/color v1.7.0
github.com/go-errors/errors v1.0.1
github.com/imdario/mergo v0.3.7
github.com/integrii/flaggy v0.0.0-20190517180110-07ea7eb77404
github.com/docker/cli v27.1.1+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/fatih/color v1.10.0
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.5.0
github.com/imdario/mergo v0.3.16
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
github.com/jesseduffield/gocui v0.3.1-0.20190703115836-08c34c9081b3
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e // indirect
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/mattn/go-runewidth v0.0.2 // indirect
github.com/mattn/go-runewidth v0.0.15
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767
github.com/mgutz/str v1.2.0
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 // indirect
github.com/pmezard/go-difflib v1.0.0
github.com/samber/lo v1.31.0
github.com/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.9.3
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.9.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
)
require (
github.com/containerd/errdefs v1.0.0 // 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/distribution/reference v0.6.0 // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gdamore/encoding v1.0.1 // 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/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.3.0
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.2.2
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
gopkg.in/yaml.v2 v2.2.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.24.0 // 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
gotest.tools/v3 v3.5.1 // indirect
)

282
go.sum
View file

@ -1,109 +1,247 @@
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/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/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/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
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/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
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.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
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.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/integrii/flaggy v0.0.0-20190517180110-07ea7eb77404 h1:oRzqLvY7HzDWD9kGheYP6BoqWa5vtw7ry3XNRbGFH8g=
github.com/integrii/flaggy v0.0.0-20190517180110-07ea7eb77404/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
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/go.mod h1:Z9UKHveKXXgyo8ME7R8yxh/BUTFOK+FgfWKlhy8oOAg=
github.com/jesseduffield/gocui v0.0.0-20190629032923-117e4fb79d29 h1:Qw+aq40UFeFBpIOvzWlGcDvDsRSHjHgfvOpZ+ax3HPw=
github.com/jesseduffield/gocui v0.0.0-20190629032923-117e4fb79d29/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.0 h1:l7wH8MKR2p+ozuZdtdhQiX7szILbv50vkMk1tg2+xow=
github.com/jesseduffield/gocui v0.3.0/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20190703113548-66cf087c5ed6 h1:n5KQ8f6mtW8+2Xm2caq4uX9/Za/70AwBO8HswJ0Ym6s=
github.com/jesseduffield/gocui v0.3.1-0.20190703113548-66cf087c5ed6/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20190703114758-e3a97494ac41 h1:WHt3Pmhgx31HjDrhUMzS2scuLu5PW88s+hc3PAmHRSY=
github.com/jesseduffield/gocui v0.3.1-0.20190703114758-e3a97494ac41/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20190703115836-08c34c9081b3 h1:F9a1FHBFAEa1Jqrd1+vc1UhwVCQH8yywV1qSNadO81U=
github.com/jesseduffield/gocui v0.3.1-0.20190703115836-08c34c9081b3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr6+sfSbdpRWWrwvLYyS56HyIRVfq0Qcl2h28wM=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513 h1:Y1bw5iItrsDCumATc/rklIJ/6K+68ieiWZJedhrNuXo=
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
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/lazycore v0.0.0-20221023210126-718a4caea996 h1:CH1en6GpXSwnXl5Ehc4WX1NpS3uw9qbi7o9A4T2YYmA=
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996/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/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/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.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/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/go.mod h1:ct+byCpkFokm4J0tiuAvB8cf2ttm6GcCe89Yr25nGKg=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762 h1:44Lv0bNi88GweB54TCjB/lEJgp+2Ze5WFpwNu0nh0ag=
github.com/nsf/termbox-go v0.0.0-20190624072549-eeb6cd0a1762/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
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/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
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/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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/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/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
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.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
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/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
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-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
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/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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-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.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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
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 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/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/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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

6
hooks/build Normal file
View file

@ -0,0 +1,6 @@
#!/bin/bash
docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=`git describe --abbrev=0 --tag` \
-t $IMAGE_NAME .

63
main.go
View file

@ -6,27 +6,44 @@ import (
"log"
"os"
"runtime"
"runtime/debug"
"github.com/docker/docker/client"
"github.com/go-errors/errors"
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazydocker/pkg/app"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/yaml"
"github.com/samber/lo"
)
const DEFAULT_VERSION = "unversioned"
var (
commit string
version = "unversioned"
version = DEFAULT_VERSION
date string
buildSource = "unknown"
configFlag = false
debuggingFlag = false
composeFiles []string
projectName string
)
func main() {
updateBuildInfo()
info := fmt.Sprintf(
"%s\nDate: %s\nBuildSource: %s\nCommit: %s\nOS: %s\nArch: %s",
version,
date,
buildSource,
commit,
runtime.GOOS,
runtime.GOARCH,
)
flaggy.SetName("lazydocker")
flaggy.SetDescription("The lazier way to manage everything docker")
@ -35,27 +52,37 @@ func main() {
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
flaggy.SetVersion(fmt.Sprintf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH))
flaggy.String(&projectName, "p", "project", "Specify a docker compose project name")
flaggy.SetVersion(info)
flaggy.Parse()
if configFlag {
var buf bytes.Buffer
yaml.NewEncoder(&buf).Encode(config.GetDefaultConfig())
encoder := yaml.NewEncoder(&buf)
err := encoder.Encode(config.GetDefaultConfig())
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%v\n", buf.String())
os.Exit(0)
}
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles)
projectDir, err := os.Getwd()
if err != nil {
log.Fatal(err.Error())
}
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
if err != nil {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig)
if err == nil {
err = app.Run()
}
app.Close()
if err != nil {
if errMessage, known := app.KnownError(err); known {
@ -72,6 +99,30 @@ func main() {
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
log.Fatalf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace)
}
}
func updateBuildInfo() {
if version == DEFAULT_VERSION {
if buildInfo, ok := debug.ReadBuildInfo(); ok {
revision, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
return setting.Key == "vcs.revision"
})
if ok {
commit = revision.Value
// if lazydocker was built from source we'll show the version as the
// abbreviated commit hash
version = utils.SafeTruncate(revision.Value, 7)
}
// if version hasn't been set we assume that neither has the date
time, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
return setting.Key == "vcs.time"
})
if ok {
date = time.Value
}
}
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/lazydocker/pkg/gui"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/log"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
)
@ -34,7 +35,10 @@ func NewApp(config *config.AppConfig) (*App, error) {
}
var err error
app.Log = log.NewLogger(config, "23432119147a4367abf7c0de2aa99a2d")
app.Tr = i18n.NewTranslationSet(app.Log)
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.UserConfig.Gui.Language)
if err != nil {
return app, err
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
// here is the place to make use of the docker-compose.yml file in the current directory
@ -43,6 +47,7 @@ func NewApp(config *config.AppConfig) (*App, error) {
if err != nil {
return app, err
}
app.closers = append(app.closers, app.DockerCommand)
app.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.ErrorChan)
if err != nil {
return app, err
@ -51,9 +56,11 @@ func NewApp(config *config.AppConfig) (*App, error) {
}
func (app *App) Run() error {
err := app.Gui.RunWithSubprocesses()
return app.Gui.Run()
}
return err
func (app *App) Close() error {
return utils.CloseMany(app.closers)
}
type errorMapping struct {

View file

@ -4,19 +4,23 @@
// The content of this generated file is a keybindings cheatsheet.
//
// To generate cheatsheet in english run:
// LANG=en go run scripts/generate_cheatsheet.go
// LANG=en go run scripts/cheatsheet/main.go generate
package main
package cheatsheet
import (
"fmt"
"log"
"os"
"strings"
"github.com/jesseduffield/lazydocker/pkg/app"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui"
"github.com/jesseduffield/lazydocker/pkg/i18n"
)
const (
generateCheatsheetCmd = "go run scripts/cheatsheet/main.go generate"
)
type bindingSection struct {
@ -24,23 +28,34 @@ type bindingSection struct {
bindings []*gui.Binding
}
func main() {
langs := []string{"pl", "nl", "en"}
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil)
func Generate() {
generateAtDir(GetKeybindingsDir())
}
func generateAtDir(dir string) {
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
if err != nil {
panic(err)
}
for _, lang := range langs {
for lang := range i18n.GetTranslationSets() {
os.Setenv("LC_ALL", lang)
mApp, _ := app.NewApp(mConfig)
file, err := os.Create(getProjectRoot() + "/docs/keybindings/Keybindings_" + lang + ".md")
mApp.Gui.SetupFakeGui()
file, err := os.Create(dir + "/Keybindings_" + lang + ".md")
if err != nil {
panic(err)
}
bindingSections := getBindingSections(mApp)
content := formatSections(mApp, bindingSections)
content = fmt.Sprintf(
"_This file is auto-generated. To update, make the changes in the "+
"pkg/i18n directory and then run `%s` from the project root._\n\n%s",
generateCheatsheetCmd,
content,
)
writeString(file, content)
}
}
@ -76,28 +91,17 @@ func getBindingSections(mApp *app.App) []*bindingSection {
titleMap := map[string]string{
"global": mApp.Tr.GlobalTitle,
"main": mApp.Tr.MainTitle,
"status": mApp.Tr.StatusTitle,
"project": mApp.Tr.ProjectTitle,
"services": mApp.Tr.ServicesTitle,
"containers": mApp.Tr.ContainersTitle,
"images": mApp.Tr.ImagesTitle,
"volumes": mApp.Tr.VolumesTitle,
"networks": mApp.Tr.NetworksTitle,
}
bindingSections = addBinding(titleMap[viewName], bindingSections, binding)
}
// for view, contexts := range mApp.Gui.GetContextMap() {
// for contextName, contextBindings := range contexts {
// translatedView := localisedTitle(mApp, view)
// translatedContextName := localisedTitle(mApp, contextName)
// title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
// for _, binding := range contextBindings {
// bindingSections = addBinding(title, bindingSections, binding)
// }
// }
// }
return bindingSections
}
@ -135,11 +139,3 @@ func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
return content
}
func getProjectRoot() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return strings.Split(dir, "lazydocker")[0] + "lazydocker"
}

View file

@ -0,0 +1,87 @@
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

@ -2,19 +2,17 @@ package commands
import (
"context"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
@ -29,293 +27,22 @@ type Container struct {
OneOff bool
ProjectName string
ID string
Container types.Container
DisplayString string
Container container.Summary
Client *client.Client
OSCommand *OSCommand
Config *config.AppConfig
Log *logrus.Entry
CLIStats ContainerCliStat // for realtime we use the CLI, for long-term we use the client
StatHistory []RecordedStats
Details Details
StatHistory []*RecordedStats
Details container.InspectResponse
MonitoringStats bool
DockerCommand LimitedDockerCommand
Tr *i18n.TranslationSet
}
// Details is a struct containing what we get back from `docker inspect` on a container
type Details struct {
ID string `json:"Id"`
Created time.Time `json:"Created"`
Path string `json:"Path"`
Args []string `json:"Args"`
State struct {
Status string `json:"Status"`
Running bool `json:"Running"`
Paused bool `json:"Paused"`
Restarting bool `json:"Restarting"`
OOMKilled bool `json:"OOMKilled"`
Dead bool `json:"Dead"`
Pid int `json:"Pid"`
ExitCode int `json:"ExitCode"`
Error string `json:"Error"`
StartedAt time.Time `json:"StartedAt"`
FinishedAt time.Time `json:"FinishedAt"`
} `json:"State"`
Image string `json:"Image"`
ResolvConfPath string `json:"ResolvConfPath"`
HostnamePath string `json:"HostnamePath"`
HostsPath string `json:"HostsPath"`
LogPath string `json:"LogPath"`
Name string `json:"Name"`
RestartCount int `json:"RestartCount"`
Driver string `json:"Driver"`
Platform string `json:"Platform"`
MountLabel string `json:"MountLabel"`
ProcessLabel string `json:"ProcessLabel"`
AppArmorProfile string `json:"AppArmorProfile"`
ExecIDs interface{} `json:"ExecIDs"`
HostConfig struct {
Binds []string `json:"Binds"`
ContainerIDFile string `json:"ContainerIDFile"`
LogConfig struct {
Type string `json:"Type"`
Config struct {
} `json:"Config"`
} `json:"LogConfig"`
NetworkMode string `json:"NetworkMode"`
PortBindings struct {
} `json:"PortBindings"`
RestartPolicy struct {
Name string `json:"Name"`
MaximumRetryCount int `json:"MaximumRetryCount"`
} `json:"RestartPolicy"`
AutoRemove bool `json:"AutoRemove"`
VolumeDriver string `json:"VolumeDriver"`
VolumesFrom []interface{} `json:"VolumesFrom"`
CapAdd interface{} `json:"CapAdd"`
CapDrop interface{} `json:"CapDrop"`
DNS interface{} `json:"Dns"`
DNSOptions interface{} `json:"DnsOptions"`
DNSSearch interface{} `json:"DnsSearch"`
ExtraHosts interface{} `json:"ExtraHosts"`
GroupAdd interface{} `json:"GroupAdd"`
IpcMode string `json:"IpcMode"`
Cgroup string `json:"Cgroup"`
Links interface{} `json:"Links"`
OomScoreAdj int `json:"OomScoreAdj"`
PidMode string `json:"PidMode"`
Privileged bool `json:"Privileged"`
PublishAllPorts bool `json:"PublishAllPorts"`
ReadonlyRootfs bool `json:"ReadonlyRootfs"`
SecurityOpt interface{} `json:"SecurityOpt"`
UTSMode string `json:"UTSMode"`
UsernsMode string `json:"UsernsMode"`
ShmSize int `json:"ShmSize"`
Runtime string `json:"Runtime"`
ConsoleSize []int `json:"ConsoleSize"`
Isolation string `json:"Isolation"`
CPUShares int `json:"CpuShares"`
Memory int `json:"Memory"`
NanoCpus int `json:"NanoCpus"`
CgroupParent string `json:"CgroupParent"`
BlkioWeight int `json:"BlkioWeight"`
BlkioWeightDevice interface{} `json:"BlkioWeightDevice"`
BlkioDeviceReadBps interface{} `json:"BlkioDeviceReadBps"`
BlkioDeviceWriteBps interface{} `json:"BlkioDeviceWriteBps"`
BlkioDeviceReadIOps interface{} `json:"BlkioDeviceReadIOps"`
BlkioDeviceWriteIOps interface{} `json:"BlkioDeviceWriteIOps"`
CPUPeriod int `json:"CpuPeriod"`
CPUQuota int `json:"CpuQuota"`
CPURealtimePeriod int `json:"CpuRealtimePeriod"`
CPURealtimeRuntime int `json:"CpuRealtimeRuntime"`
CpusetCpus string `json:"CpusetCpus"`
CpusetMems string `json:"CpusetMems"`
Devices interface{} `json:"Devices"`
DeviceCgroupRules interface{} `json:"DeviceCgroupRules"`
DiskQuota int `json:"DiskQuota"`
KernelMemory int `json:"KernelMemory"`
MemoryReservation int `json:"MemoryReservation"`
MemorySwap int `json:"MemorySwap"`
MemorySwappiness interface{} `json:"MemorySwappiness"`
OomKillDisable bool `json:"OomKillDisable"`
PidsLimit int `json:"PidsLimit"`
Ulimits interface{} `json:"Ulimits"`
CPUCount int `json:"CpuCount"`
CPUPercent int `json:"CpuPercent"`
IOMaximumIOps int `json:"IOMaximumIOps"`
IOMaximumBandwidth int `json:"IOMaximumBandwidth"`
MaskedPaths []string `json:"MaskedPaths"`
ReadonlyPaths []string `json:"ReadonlyPaths"`
} `json:"HostConfig"`
GraphDriver struct {
Data struct {
LowerDir string `json:"LowerDir"`
MergedDir string `json:"MergedDir"`
UpperDir string `json:"UpperDir"`
WorkDir string `json:"WorkDir"`
} `json:"Data"`
Name string `json:"Name"`
} `json:"GraphDriver"`
Mounts []struct {
Type string `json:"Type"`
Name string `json:"Name,omitempty"`
Source string `json:"Source"`
Destination string `json:"Destination"`
Driver string `json:"Driver,omitempty"`
Mode string `json:"Mode"`
RW bool `json:"RW"`
Propagation string `json:"Propagation"`
} `json:"Mounts"`
Config struct {
Hostname string `json:"Hostname"`
Domainname string `json:"Domainname"`
User string `json:"User"`
AttachStdin bool `json:"AttachStdin"`
AttachStdout bool `json:"AttachStdout"`
AttachStderr bool `json:"AttachStderr"`
Tty bool `json:"Tty"`
OpenStdin bool `json:"OpenStdin"`
StdinOnce bool `json:"StdinOnce"`
Env []string `json:"Env"`
Cmd []string `json:"Cmd"`
Image string `json:"Image"`
Volumes struct {
APIBundle struct {
} `json:"/api-bundle"`
App struct {
} `json:"/app"`
} `json:"Volumes"`
WorkingDir string `json:"WorkingDir"`
Entrypoint interface{} `json:"Entrypoint"`
OnBuild interface{} `json:"OnBuild"`
Labels map[string]string `json:"Labels"`
} `json:"Config"`
NetworkSettings struct {
Bridge string `json:"Bridge"`
SandboxID string `json:"SandboxID"`
HairpinMode bool `json:"HairpinMode"`
LinkLocalIPv6Address string `json:"LinkLocalIPv6Address"`
LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen"`
Ports map[string][]struct {
HostIP string `json:"HostIP"`
HostPort string `json:"HostPort"`
} `json:"Ports"`
SandboxKey string `json:"SandboxKey"`
SecondaryIPAddresses interface{} `json:"SecondaryIPAddresses"`
SecondaryIPv6Addresses interface{} `json:"SecondaryIPv6Addresses"`
EndpointID string `json:"EndpointID"`
Gateway string `json:"Gateway"`
GlobalIPv6Address string `json:"GlobalIPv6Address"`
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"`
IPAddress string `json:"IPAddress"`
IPPrefixLen int `json:"IPPrefixLen"`
IPv6Gateway string `json:"IPv6Gateway"`
MacAddress string `json:"MacAddress"`
Networks map[string]struct {
IPAMConfig interface{} `json:"IPAMConfig"`
Links interface{} `json:"Links"`
Aliases []string `json:"Aliases"`
NetworkID string `json:"NetworkID"`
EndpointID string `json:"EndpointID"`
Gateway string `json:"Gateway"`
IPAddress string `json:"IPAddress"`
IPPrefixLen int `json:"IPPrefixLen"`
IPv6Gateway string `json:"IPv6Gateway"`
GlobalIPv6Address string `json:"GlobalIPv6Address"`
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"`
MacAddress string `json:"MacAddress"`
DriverOpts interface{} `json:"DriverOpts"`
} `json:"Networks"`
} `json:"NetworkSettings"`
}
// ContainerCliStat is a stat object returned by the CLI docker stat command
type ContainerCliStat struct {
BlockIO string `json:"BlockIO"`
CPUPerc string `json:"CPUPerc"`
Container string `json:"Container"`
ID string `json:"ID"`
MemPerc string `json:"MemPerc"`
MemUsage string `json:"MemUsage"`
Name string `json:"Name"`
NetIO string `json:"NetIO"`
PIDs string `json:"PIDs"`
}
// GetDisplayStrings returns the dispaly string of Container
func (c *Container) GetDisplayStrings(isFocused bool) []string {
return []string{c.GetDisplayStatus(), c.Name, c.GetDisplayCPUPerc()}
}
// GetDisplayStatus returns the colored status of the container
func (c *Container) GetDisplayStatus() string {
state := c.Container.State
if c.Container.State == "exited" {
state += " (" + strconv.Itoa(c.Details.State.ExitCode) + ")"
}
return utils.ColoredString(state, c.GetColor())
}
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
func (c *Container) GetDisplayCPUPerc() string {
stats := c.CLIStats
if stats.CPUPerc == "" {
return ""
}
percentage, err := strconv.ParseFloat(strings.TrimSuffix(stats.CPUPerc, "%"), 32)
if err != nil {
// probably complaining about not being able to convert '--'
return ""
}
var clr color.Attribute
if percentage > 90 {
clr = color.FgRed
} else if percentage > 50 {
clr = color.FgYellow
} else {
clr = color.FgWhite
}
return utils.ColoredString(stats.CPUPerc, clr)
}
// ProducingLogs tells us whether we should bother checking a container's logs
func (c *Container) ProducingLogs() bool {
return c.Container.State == "running" && !(c.Details.HostConfig.LogConfig.Type == "none")
}
// GetColor Container color
func (c *Container) GetColor() color.Attribute {
switch c.Container.State {
case "exited":
if 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
}
StatsMutex deadlock.Mutex
}
// Remove removes the container
func (c *Container) Remove(options types.ContainerRemoveOptions) error {
func (c *Container) Remove(options container.RemoveOptions) error {
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
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") {
return ComplexError{
@ -330,18 +57,42 @@ func (c *Container) Remove(options types.ContainerRemoveOptions) error {
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
func (c *Container) Stop() error {
return c.Client.ContainerStop(context.Background(), c.ID, nil)
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{})
}
// Pause pauses the container
func (c *Container) Pause() error {
c.Log.Warn(fmt.Sprintf("pausing container %s", c.Name))
return c.Client.ContainerPause(context.Background(), c.ID)
}
// Unpause unpauses the container
func (c *Container) Unpause() error {
c.Log.Warn(fmt.Sprintf("unpausing container %s", c.Name))
return c.Client.ContainerUnpause(context.Background(), c.ID)
}
// Restart restarts the container
func (c *Container) Restart() error {
return c.Client.ContainerRestart(context.Background(), c.ID, nil)
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{})
}
// Attach attaches the container
func (c *Container) Attach() (*exec.Cmd, error) {
if !c.DetailsLoaded() {
return nil, errors.New(c.Tr.WaitingForContainerInfo)
}
// verify that we can in fact attach to this container
if !c.Details.Config.OpenStdin {
return nil, errors.New(c.Tr.UnattachableContainerError)
@ -351,41 +102,25 @@ func (c *Container) Attach() (*exec.Cmd, error) {
return nil, errors.New(c.Tr.CannotAttachStoppedContainerError)
}
cmd := c.OSCommand.PrepareSubProcess("docker", "attach", "--sig-proxy=false", c.ID)
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
// TODO: use SDK
cmd := c.OSCommand.NewCmd("docker", "attach", "--sig-proxy=false", c.ID)
return cmd, nil
}
// Top returns process information
func (c *Container) Top() (types.ContainerProcessList, error) {
return c.Client.ContainerTop(context.Background(), c.ID, []string{})
}
// EraseOldHistory removes any history before the user-specified max duration
func (c *Container) EraseOldHistory() {
if c.Config.UserConfig.Stats.MaxDuration == 0 {
return
func (c *Container) Top(ctx context.Context) (container.TopResponse, error) {
detail, err := c.Inspect()
if err != nil {
return container.TopResponse{}, err
}
for i, stat := range c.StatHistory {
if time.Since(stat.RecordedAt) < c.Config.UserConfig.Stats.MaxDuration {
c.StatHistory = c.StatHistory[i:]
return
}
// check container status
if !detail.State.Running {
return container.TopResponse{}, errors.New("container is not running")
}
}
// ViewLogs attaches to a subprocess viewing the container's logs
func (c *Container) ViewLogs() (*exec.Cmd, error) {
templateString := c.OSCommand.Config.UserConfig.CommandTemplates.ViewContainerLogs
command := utils.ApplyTemplate(
templateString,
c.DockerCommand.NewCommandObject(CommandObject{Container: c}),
)
cmd := c.OSCommand.ExecutableFromString(command)
c.OSCommand.PrepareForChildren(cmd)
return cmd, nil
return c.Client.ContainerTop(ctx, c.ID, []string{})
}
// PruneContainers prunes containers
@ -395,13 +130,13 @@ func (c *DockerCommand) PruneContainers() error {
}
// Inspect returns details about the container
func (c *Container) Inspect() (types.ContainerJSON, error) {
func (c *Container) Inspect() (container.InspectResponse, error) {
return c.Client.ContainerInspect(context.Background(), c.ID)
}
// RenderTop returns details about the container
func (c *Container) RenderTop() (string, error) {
result, err := c.Client.ContainerTop(context.Background(), c.ID, []string{})
func (c *Container) RenderTop(ctx context.Context) (string, error) {
result, err := c.Top(ctx)
if err != nil {
return "", err
}
@ -409,7 +144,9 @@ func (c *Container) RenderTop() (string, error) {
return utils.RenderTable(append([][]string{result.Titles}, result.Processes...))
}
// 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. Details is a struct, not a pointer to a struct, so it starts off with heaps of zero values. One of which is the container Image, which starts as a blank string. Given that every container should have an image, this is a good proxy to use
// DetailsLoaded tells us whether we have yet loaded the details for a container.
// Sometimes it takes some time for a container to have its details loaded
// after it starts.
func (c *Container) DetailsLoaded() bool {
return c.Details.Image != ""
return c.Details.ContainerJSONBase != nil
}

View file

@ -1,19 +1,8 @@
package commands
import (
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"strings"
"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
@ -56,10 +45,9 @@ type ContainerStats struct {
IoTimeRecursive []interface{} `json:"io_time_recursive"`
SectorsRecursive []interface{} `json:"sectors_recursive"`
} `json:"blkio_stats"`
NumProcs int `json:"num_procs"`
StorageStats struct {
} `json:"storage_stats"`
CPUStats struct {
NumProcs int `json:"num_procs"`
StorageStats struct{} `json:"storage_stats"`
CPUStats struct {
CPUUsage struct {
TotalUsage int64 `json:"total_usage"`
PercpuUsage []int64 `json:"percpu_usage"`
@ -150,9 +138,8 @@ type ContainerStats struct {
func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
cpuUsageDelta := s.CPUStats.CPUUsage.TotalUsage - s.PrecpuStats.CPUUsage.TotalUsage
cpuTotalUsageDelta := s.CPUStats.SystemCPUUsage - s.PrecpuStats.SystemCPUUsage
numberOfCores := len(s.CPUStats.CPUUsage.PercpuUsage)
value := float64(cpuUsageDelta*100) * float64(numberOfCores) / float64(cpuTotalUsageDelta)
value := float64(cpuUsageDelta*100) / float64(cpuTotalUsageDelta)
if math.IsNaN(value) {
return 0
}
@ -161,7 +148,6 @@ func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
// CalculateContainerMemoryUsage calculates the memory usage of the container as a percent of total available memory
func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
value := float64(s.MemoryStats.Usage*100) / float64(s.MemoryStats.Limit)
if math.IsNaN(value) {
return 0
@ -169,133 +155,34 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
return value
}
// RenderStats returns a string containing the rendered stats of the container
func (c *Container) RenderStats(viewWidth int) (string, error) {
func (c *Container) appendStats(stats *RecordedStats, maxDuration time.Duration) {
c.StatsMutex.Lock()
defer c.StatsMutex.Unlock()
c.StatHistory = append(c.StatHistory, stats)
c.eraseOldHistory(maxDuration)
}
// eraseOldHistory removes any history before the user-specified max duration
func (c *Container) eraseOldHistory(maxDuration time.Duration) {
if maxDuration == 0 {
return
}
for i, stat := range c.StatHistory {
if time.Since(stat.RecordedAt) < maxDuration {
c.StatHistory = c.StatHistory[i:]
return
}
}
}
func (c *Container) GetLastStats() (*RecordedStats, bool) {
c.StatsMutex.Lock()
defer c.StatsMutex.Unlock()
history := c.StatHistory
if len(history) == 0 {
return "", nil
}
currentStats := history[len(history)-1]
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", currentStats.ClientStats.PidsStats.Current)
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(currentStats.ClientStats.Networks.Eth0.RxBytes))
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(currentStats.ClientStats.Networks.Eth0.TxBytes))
originalJSON, err := json.MarshalIndent(currentStats, "", " ")
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
}
// 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) {
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))),
), 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())
}
return nil, false
}
return history[len(history)-1], true
}

View file

@ -0,0 +1,17 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCalculateContainerCPUPercentage(t *testing.T) {
container := &ContainerStats{}
container.CPUStats.CPUUsage.TotalUsage = 10
container.CPUStats.SystemCPUUsage = 10
container.PrecpuStats.CPUUsage.TotalUsage = 5
container.PrecpuStats.SystemCPUUsage = 2
assert.EqualValues(t, 62.5, container.CalculateContainerCPUPercentage())
}

View file

@ -5,23 +5,35 @@ import (
"context"
"encoding/json"
"fmt"
"io"
ogLog "log"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/acarl005/stripansi"
"github.com/docker/docker/api/types"
cliconfig "github.com/docker/cli/cli/config"
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/imdario/mergo"
"github.com/jesseduffield/lazydocker/pkg/commands/ssh"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
)
// DockerCommand is our main git interface
const (
dockerHostEnvKey = "DOCKER_HOST"
)
// DockerCommand is our main docker interface
type DockerCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
@ -29,17 +41,17 @@ type DockerCommand struct {
Config *config.AppConfig
Client *client.Client
InDockerComposeProject bool
ErrorChan chan error
ContainerMutex sync.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
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
LocalProjectName string
ErrorChan chan error
ContainerMutex deadlock.Mutex
ServiceMutex deadlock.Mutex
Closers []io.Closer
}
var _ io.Closer = &DockerCommand{}
// LimitedDockerCommand is a stripped-down DockerCommand with just the methods the container/service/image might need
type LimitedDockerCommand interface {
NewCommandObject(CommandObject) CommandObject
@ -50,20 +62,72 @@ type CommandObject struct {
DockerCompose string
Service *Service
Container *Container
Image *Image
Volume *Volume
Network *Network
Project *Project
}
// 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 {
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
}
// NewDockerCommand it runs git commands
// 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
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
cli, err := client.NewEnvClient()
dockerHost, err := determineDockerHost()
if err != nil {
return nil, err
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()
if err != nil {
ogLog.Fatal(err)
}
// Retrieve the docker host from the environment which could have been set by
// the `SSHHandler.HandleSSHDockerHost()` and override `dockerHost`.
dockerHostFromEnv := os.Getenv(dockerHostEnvKey)
if dockerHostFromEnv != "" {
dockerHost = dockerHostFromEnv
}
cli, err := newDockerClient(dockerHost)
if err != nil {
ogLog.Fatal(err)
}
dockerCommand := &DockerCommand{
@ -74,14 +138,10 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
Client: cli,
ErrorChan: errorChan,
InDockerComposeProject: true,
Closers: []io.Closer{tunnelCloser},
}
command := utils.ApplyTemplate(
config.UserConfig.CommandTemplates.CheckDockerComposeConfig,
dockerCommand.NewCommandObject(CommandObject{}),
)
log.Warn(command)
dockerCommand.setDockerComposeCommand(config)
err = osCommand.RunCommand(
utils.ApplyTemplate(
@ -94,70 +154,49 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
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
}
// MonitorContainerStats is a function
func (c *DockerCommand) MonitorContainerStats() {
go c.MonitorCLIContainerStats()
go c.MonitorClientContainerStats()
// 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 != ""
}
// MonitorCLIContainerStats monitors a stream of container stats and updates the containers as each new stats object is received
func (c *DockerCommand) MonitorCLIContainerStats() {
command := `docker stats --all --no-trunc --format '{{json .}}'`
cmd := c.OSCommand.RunCustomCommand(command)
r, err := cmd.StdoutPipe()
if err != nil {
c.ErrorChan <- err
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
return
}
cmd.Start()
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
var stats ContainerCliStat
// need to strip ANSI codes because uses escape sequences to clear the screen with each refresh
cleanString := stripansi.Strip(scanner.Text())
if err := json.Unmarshal([]byte(cleanString), &stats); err != nil {
c.ErrorChan <- err
return
}
c.ContainerMutex.Lock()
for _, container := range c.Containers {
if container.ID == stats.ID {
container.CLIStats = stats
}
}
c.ContainerMutex.Unlock()
}
cmd.Wait()
return
}
// MonitorClientContainerStats is a function
func (c *DockerCommand) MonitorClientContainerStats() {
// 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
for range time.Tick(time.Second) {
for _, container := range c.Containers {
if !container.MonitoringStats {
go c.createClientStatMonitor(container)
}
}
// 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) createClientStatMonitor(container *Container) {
func (c *DockerCommand) Close() error {
return utils.CloseMany(c.Closers)
}
func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
container.MonitoringStats = true
stream, err := c.Client.ContainerStats(context.Background(), container.ID, true)
if err != nil {
c.ErrorChan <- err
// not creating error panel because if we've disconnected from docker we'll
// have already created an error panel
c.Log.Error(err)
container.MonitoringStats = false
return
}
@ -167,9 +206,9 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) {
for scanner.Scan() {
data := scanner.Bytes()
var stats ContainerStats
json.Unmarshal(data, &stats)
_ = json.Unmarshal(data, &stats)
recordedStats := RecordedStats{
recordedStats := &RecordedStats{
ClientStats: stats,
DerivedStats: DerivedStats{
CPUPercentage: stats.CalculateContainerCPUPercentage(),
@ -178,74 +217,152 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) {
RecordedAt: time.Now(),
}
c.ContainerMutex.Lock()
container.StatHistory = append(container.StatHistory, recordedStats)
container.EraseOldHistory()
c.ContainerMutex.Unlock()
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
}
container.MonitoringStats = false
return
}
// RefreshContainersAndServices returns a slice of docker containers
func (c *DockerCommand) RefreshContainersAndServices() error {
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
c.ServiceMutex.Lock()
defer c.ServiceMutex.Unlock()
currentServices := c.Services
containers, err := c.GetContainers()
containers, err := c.GetContainers(currentContainers)
if err != nil {
return err
return nil, nil, err
}
var services []*Service
// we only need to get these services once because they won't change in the runtime of the program
if currentServices != nil {
services = currentServices
} else {
services, err = c.GetServices()
// Derive services from container labels (covers all projects)
services := c.GetServicesFromContainers(containers)
var composeServices []*Service
if c.InDockerComposeProject {
composeServices, err = c.GetServices()
if err != nil {
return err
c.Log.Warn("Failed to get compose services: " + err.Error())
}
}
// Determine the local project name before merging services, since
// mergeServices needs it. We match compose service names against container
// labels to handle cases where the project name differs from the directory
// name (e.g. a `name:` directive in the compose file).
if c.LocalProjectName == "" && c.InDockerComposeProject && composeServices != nil {
for _, ctr := range containers {
if ctr.ProjectName == "" || ctr.ServiceName == "" {
continue
}
for _, svc := range composeServices {
if ctr.ServiceName == svc.Name {
c.LocalProjectName = ctr.ProjectName
break
}
}
if c.LocalProjectName != "" {
break
}
}
// Fall back to directory name
if c.LocalProjectName == "" && c.Config.ProjectDir != "" {
c.LocalProjectName = filepath.Base(c.Config.ProjectDir)
}
}
// Merge compose services (which include stopped services) with
// container-derived services from all projects
if composeServices != nil {
services = c.mergeServices(services, composeServices)
}
c.assignContainersToServices(containers, services)
var displayContainers []*Container
if c.Config.UserConfig.Gui.ShowAllContainers {
displayContainers = containers
} else {
displayContainers = c.obtainStandaloneContainers(containers, services)
return containers, services, nil
}
// GetServicesFromContainers derives services from container labels for all projects
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service {
// Use project+service as key to avoid duplicates
type serviceKey struct {
project string
service string
}
seen := make(map[serviceKey]bool)
services := make([]*Service, 0, len(containers))
for _, ctr := range containers {
if ctr.ServiceName == "" || ctr.OneOff {
continue
}
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
if seen[key] {
continue
}
seen[key] = true
services = append(services, &Service{
Name: ctr.ServiceName,
ID: ctr.ProjectName + "-" + ctr.ServiceName,
ProjectName: ctr.ProjectName,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
})
}
// sort services first by whether they have a linked container, and second by alphabetical order
sort.Slice(services, func(i, j int) bool {
if services[i].Container != nil && services[j].Container == nil {
return true
return services
}
// mergeServices merges compose services (which may lack ProjectName) with
// container-derived services. Compose services take priority because they
// include services without running containers.
func (c *DockerCommand) mergeServices(containerServices []*Service, composeServices []*Service) []*Service {
// Set project name on compose services
for _, svc := range composeServices {
if svc.ProjectName == "" {
svc.ProjectName = c.LocalProjectName
}
}
if services[i].Container == nil && services[j].Container != nil {
return false
// 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 services[i].Name < services[j].Name
})
return result
}
c.Containers = containers
c.Services = services
c.DisplayContainers = displayContainers
return nil
// 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) {
L:
for _, service := range services {
for _, container := range containers {
if !container.OneOff && container.ServiceName == service.Name {
service.Container = container
for _, ctr := range containers {
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
service.Container = ctr
continue L
}
}
@ -253,42 +370,24 @@ L:
}
}
// 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
func (c *DockerCommand) GetContainers() ([]*Container, error) {
func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) {
c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock()
existingContainers := c.Containers
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
containers, err := c.Client.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
return nil, err
}
ownContainers := make([]*Container, len(containers))
for i, container := range containers {
for i, ctr := range containers {
var newContainer *Container
// check if we already data stored against the container
// check if we already have data stored against the container
for _, existingContainer := range existingContainers {
if existingContainer.ID == container.ID {
if existingContainer.ID == ctr.ID {
newContainer = existingContainer
break
}
@ -297,31 +396,36 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
// initialise the container if it's completely new
if newContainer == nil {
newContainer = &Container{
ID: container.ID,
ID: ctr.ID,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
Config: c.Config,
DockerCommand: c,
Tr: c.Tr,
}
}
newContainer.Container = container
newContainer.Container = ctr
// if the container is made with a name label we will use that
if name, ok := container.Labels["name"]; ok {
if name, ok := ctr.Labels["name"]; ok {
newContainer.Name = name
} else {
newContainer.Name = strings.TrimLeft(container.Names[0], "/")
if len(ctr.Names) > 0 {
newContainer.Name = strings.TrimLeft(ctr.Names[0], "/")
} else {
newContainer.Name = ctr.ID
}
}
newContainer.ServiceName = container.Labels["com.docker.compose.service"]
newContainer.ProjectName = container.Labels["com.docker.compose.project"]
newContainer.ContainerNumber = container.Labels["com.docker.compose.container"]
newContainer.OneOff = container.Labels["com.docker.compose.oneoff"] == "True"
newContainer.ServiceName = ctr.Labels["com.docker.compose.service"]
newContainer.ProjectName = ctr.Labels["com.docker.compose.project"]
newContainer.ContainerNumber = ctr.Labels["com.docker.compose.container"]
newContainer.OneOff = ctr.Labels["com.docker.compose.oneoff"] == "True"
ownContainers[i] = newContainer
}
c.SetContainerDetails(ownContainers)
return ownContainers, nil
}
@ -332,22 +436,22 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
}
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand))
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand))
if err != nil {
return nil, err
}
// output looks like:
// service1 998d6d286b0499e0ff23d66302e720991a2asdkf9c30d0542034f610daf8a971
// service2 asdld98asdklasd9bccd02438de0994f8e19cbe691feb3755336ec5ca2c55971
// service1
// service2
lines := utils.SplitLines(output)
services := make([]*Service, len(lines))
for i, str := range lines {
arr := strings.Split(str, " ")
services[i] = &Service{
Name: arr[0],
ID: arr[1],
Name: str,
ID: c.LocalProjectName + "-" + str,
ProjectName: c.LocalProjectName,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
@ -357,43 +461,41 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
return services, nil
}
// 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 {
func (c *DockerCommand) RefreshContainerDetails(containers []*Container) error {
c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock()
containers := c.Containers
ids := make([]string, len(containers))
for i, container := range containers {
ids[i] = container.ID
}
cmd := c.OSCommand.RunCustomCommand("docker inspect " + strings.Join(ids, " "))
output, err := cmd.CombinedOutput()
if err != nil {
return err
}
var details []*Details
if err := json.Unmarshal(output, &details); err != nil {
return err
}
for i, container := range containers {
container.Details = *details[i]
}
c.SetContainerDetails(containers)
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
func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
func (c *DockerCommand) ViewAllLogs(project *Project) (*exec.Cmd, error) {
cmd := c.OSCommand.ExecutableFromString(
utils.ApplyTemplate(
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
c.NewCommandObject(CommandObject{}),
c.NewCommandObject(CommandObject{Project: project}),
),
)
@ -404,15 +506,80 @@ func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
// DockerComposeConfig returns the result of 'docker-compose config'
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(
utils.ApplyTemplate(
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
c.NewCommandObject(CommandObject{}),
c.NewCommandObject(CommandObject{Project: project}),
),
)
if err != nil {
output = err.Error()
}
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

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

View file

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

View file

@ -0,0 +1,91 @@
package commands
import (
"os"
"testing"
"github.com/docker/docker/client"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/stretchr/testify/assert"
)
// TestNewDockerClientVersionNegotiation verifies that newDockerClient allows
// API version negotiation even when DOCKER_API_VERSION is set.
//
// This is a regression test for https://github.com/jesseduffield/lazydocker/issues/715
// where users got "client version 1.25 is too old" errors because FromEnv()
// includes WithVersionFromEnv() which sets manualOverride=true, preventing
// API version negotiation.
func TestNewDockerClientVersionNegotiation(t *testing.T) {
// Save original env var and restore after test
originalAPIVersion := os.Getenv("DOCKER_API_VERSION")
defer func() {
if originalAPIVersion == "" {
os.Unsetenv("DOCKER_API_VERSION")
} else {
os.Setenv("DOCKER_API_VERSION", originalAPIVersion)
}
}()
// Set DOCKER_API_VERSION to an old version that would cause
// "client version 1.25 is too old" errors if negotiation is disabled
os.Setenv("DOCKER_API_VERSION", "1.25")
t.Run("FromEnv locks version preventing negotiation", func(t *testing.T) {
// This demonstrates the problematic behavior we're avoiding.
// When using FromEnv with DOCKER_API_VERSION set, the client
// version gets locked to 1.25 and negotiation is disabled.
cli, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
assert.NoError(t, err)
defer cli.Close()
// Version is locked to the env var value
assert.Equal(t, "1.25", cli.ClientVersion())
})
t.Run("newDockerClient allows version negotiation", func(t *testing.T) {
// Test the actual production function.
// Use DefaultDockerHost for cross-platform compatibility
// (unix socket on Linux/macOS, named pipe on Windows).
cli, err := newDockerClient(client.DefaultDockerHost)
assert.NoError(t, err)
defer cli.Close()
// Version is NOT locked to the env var value (1.25).
// Instead, it uses the library's default version and will negotiate
// with the server on first request. This is the key difference that
// fixes the "version too old" error.
assert.NotEqual(t, "1.25", cli.ClientVersion(),
"client version should not be locked to DOCKER_API_VERSION env var")
})
}
// TestIsProjectScoped covers the predicate that drives whether the
// project/services panels appear and whether the containers panel filters by
// project. The "outside compose dir + -p" case is the regression we fixed
// after PR #776 silently disabled it.
func TestIsProjectScoped(t *testing.T) {
cases := []struct {
name string
inDockerComposeProject bool
projectName string
want bool
}{
{"inside compose dir, no -p", true, "", true},
{"inside compose dir, with -p", true, "myproject", true},
{"outside compose dir, no -p", false, "", false},
{"outside compose dir, with -p", false, "myproject", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := &DockerCommand{
InDockerComposeProject: tc.inDockerComposeProject,
Config: &config.AppConfig{ProjectName: tc.projectName},
}
assert.Equal(t, tc.want, c.IsProjectScoped())
})
}
}

View file

@ -1,7 +1,7 @@
package commands
import (
"io/ioutil"
"io"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
@ -31,7 +31,7 @@ func NewDummyAppConfig() *config.AppConfig {
// NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
log.Out = io.Discard
return log.WithField("test", "test")
}
@ -42,10 +42,11 @@ func NewDummyDockerCommand() *DockerCommand {
// NewDummyDockerCommandWithOSCommand creates a new dummy DockerCommand for testing
func NewDummyDockerCommandWithOSCommand(osCommand *OSCommand) *DockerCommand {
newAppConfig := NewDummyAppConfig()
return &DockerCommand{
Log: NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(NewDummyLog()),
Config: NewDummyAppConfig(),
Tr: i18n.NewTranslationSet(NewDummyLog(), newAppConfig.UserConfig.Gui.Language),
Config: newAppConfig,
}
}

View file

@ -4,11 +4,12 @@ import (
"context"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@ -17,21 +18,15 @@ type Image struct {
Name string
Tag string
ID string
Image types.ImageSummary
Image image.Summary
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
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
func (i *Image) Remove(options types.ImageRemoveOptions) error {
func (i *Image) Remove(options image.RemoveOptions) error {
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
return err
}
@ -39,19 +34,13 @@ func (i *Image) Remove(options types.ImageRemoveOptions) error {
return nil
}
// Layer is a layer in an image's history
type Layer struct {
types.ImageHistory
}
// GetDisplayStrings returns the array of strings describing the layer
func (l *Layer) GetDisplayStrings(isFocused bool) []string {
func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []string {
tag := ""
if len(l.Tags) > 0 {
tag = l.Tags[0]
if len(layer.Tags) > 0 {
tag = layer.Tags[0]
}
id := strings.TrimPrefix(l.ID, "sha256:")
id := strings.TrimPrefix(layer.ID, "sha256:")
if len(id) > 10 {
id = id[0:10]
}
@ -61,16 +50,16 @@ func (l *Layer) GetDisplayStrings(isFocused bool) []string {
}
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
createdBy := l.CreatedBy
if strings.Contains(l.CreatedBy, dockerFileCommandPrefix) {
createdBy = strings.Trim(strings.TrimPrefix(l.CreatedBy, dockerFileCommandPrefix), " ")
createdBy := layer.CreatedBy
if strings.Contains(layer.CreatedBy, dockerFileCommandPrefix) {
createdBy = strings.Trim(strings.TrimPrefix(layer.CreatedBy, dockerFileCommandPrefix), " ")
split := strings.Split(createdBy, " ")
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
}
createdBy = strings.Replace(createdBy, "\t", " ", -1)
size := utils.FormatBinaryBytes(int(l.Size))
size := utils.FormatBinaryBytes(int(layer.Size))
sizeColor := color.FgWhite
if size == "0B" {
sizeColor = color.FgBlue
@ -86,34 +75,33 @@ func (l *Layer) GetDisplayStrings(isFocused bool) []string {
// RenderHistory renders the history of the image
func (i *Image) RenderHistory() (string, error) {
history, err := i.Client.ImageHistory(context.Background(), i.ID)
if err != nil {
return "", err
}
layers := make([]*Layer, len(history))
for i, layer := range history {
layers[i] = &Layer{layer}
}
tableBody := lo.Map(history, func(layer image.HistoryResponseItem, _ int) []string {
return getHistoryResponseItemDisplayStrings(layer)
})
return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"}))
headers := [][]string{{"ID", "TAG", "SIZE", "COMMAND"}}
table := append(headers, tableBody...)
return utils.RenderTable(table)
}
// RefreshImages returns a slice of docker images
func (c *DockerCommand) RefreshImages() ([]*Image, error) {
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
images, err := c.Client.ImageList(context.Background(), image.ListOptions{})
if err != nil {
return nil, err
}
ownImages := make([]*Image, len(images))
for i, image := range images {
// func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error)
for i, img := range images {
firstTag := ""
tags := image.RepoTags
tags := img.RepoTags
if len(tags) > 0 {
firstTag = tags[0]
}
@ -124,13 +112,20 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) {
if len(nameParts) > 1 {
tag = nameParts[len(nameParts)-1]
name = strings.Join(nameParts[:len(nameParts)-1], ":")
for prefix, replacement := range c.Config.UserConfig.Replacements.ImageNamePrefixes {
if strings.HasPrefix(name, prefix) {
name = strings.Replace(name, prefix, replacement, 1)
break
}
}
}
ownImages[i] = &Image{
ID: image.ID,
ID: img.ID,
Name: name,
Tag: tag,
Image: image,
Image: img,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,

54
pkg/commands/network.go Normal file
View file

@ -0,0 +1,54 @@
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,55 +1,51 @@
package commands
import (
"context"
"fmt"
"io/ioutil"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/kill"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// Platform stores the os state
type Platform struct {
os string
shell string
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
os string
shell string
shellArg string
openCommand string
openLinkCommand string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config *config.AppConfig
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
Log *logrus.Entry
Platform *Platform
Config *config.AppConfig
command func(string, ...string) *exec.Cmd
getenv func(string) string
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config *config.AppConfig) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
command: exec.Command,
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
Log: log,
Platform: getPlatform(),
Config: config,
command: exec.Command,
getenv: os.Getenv,
}
}
@ -63,7 +59,16 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
cmd := c.ExecutableFromString(command)
before := time.Now()
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
output, err := sanitisedCommandOutput(cmd.Output())
c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before)))
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
}
@ -79,11 +84,41 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
return err
}
// ExecutableFromString takes a string like `git status` 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 {
splitCmd := str.ToArgv(commandStr)
// c.Log.Info(splitCmd)
return c.command(splitCmd[0], splitCmd[1:]...)
return c.NewCmd(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
@ -104,25 +139,16 @@ func (c *OSCommand) FileType(path string) string {
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) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", WrapError(err)
// from stderr if we got an ExitError
exitError, ok := err.(*exec.ExitError)
if ok {
return outputString, errors.New(string(exitError.Stderr))
}
return outputString, errors.New(outputString)
return "", WrapError(err)
}
return outputString, nil
}
@ -154,11 +180,7 @@ func (c *OSCommand) OpenLink(link string) error {
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.getGlobalGitConfig("core.editor")
if editor == "" {
editor = c.getenv("VISUAL")
}
editor := c.getenv("VISUAL")
if editor == "" {
editor = c.getenv("EDITOR")
}
@ -168,25 +190,31 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
return nil, errors.New("No editor defined in $VISUAL or $EDITOR")
}
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...)
return c.NewCmd(editor, filename), nil
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
var quote string
if c.Platform.os == "windows" {
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
`\"`, `\\"`,
).Replace(message)
} else {
quote = `"`
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
}
return escapedQuote + message + escapedQuote
return quote + message + quote
}
// Unquote removes wrapping quotations marks if they are present
@ -197,7 +225,7 @@ func (c *OSCommand) Unquote(message string) string {
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return WrapError(err)
}
@ -212,7 +240,7 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
tmpfile, err := os.CreateTemp("", filename)
if err != nil {
c.Log.Error(err)
return "", WrapError(err)
@ -265,7 +293,7 @@ func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
// GetLazydockerPath returns the path of the currently executed file
func (c *OSCommand) GetLazydockerPath() string {
ex, err := os.Executable() // get the executable path for git to use
ex, err := os.Executable() // get the executable path for docker to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
@ -274,12 +302,11 @@ func (c *OSCommand) GetLazydockerPath() string {
// RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
return c.NewCmd(c.Platform.shell, c.Platform.shellArg, command)
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
cmds := make([]*exec.Cmd, len(commandStrings))
for i, str := range commandStrings {
@ -315,7 +342,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
c.Log.Error(err)
}
if b, err := ioutil.ReadAll(stderr); err == nil {
if b, err := io.ReadAll(stderr); err == nil {
if len(b) > 0 {
finalErrors = append(finalErrors, string(b))
}
@ -339,21 +366,10 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
// Kill kills a process. If the process has Setpgid == true, then we have anticipated that it might spawn its own child processes, so we've given it a process group ID (PGID) equal to its process id (PID) and given its child processes will inherit the PGID, we can kill that group, rather than killing the process itself.
func (c *OSCommand) Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
if cmd.SysProcAttr != nil && cmd.SysProcAttr.Setpgid == true {
// minus sign means we're talking about a PGID as opposed to a PID
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
return cmd.Process.Kill()
return kill.Kill(cmd)
}
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a sideproject, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a subprocess, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
func (c *OSCommand) PrepareForChildren(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
kill.PrepareForChildren(cmd)
}

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package commands
@ -8,12 +9,10 @@ import (
func getPlatform() *Platform {
return &Platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "'",
openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}",
fallbackEscapedQuote: "\"",
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}",
}
}

View file

@ -1,7 +1,7 @@
package commands
import (
"io/ioutil"
"fmt"
"os"
"os/exec"
"testing"
@ -61,11 +61,10 @@ func TestOSCommandRunCommand(t *testing.T) {
// TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
test func(*exec.Cmd, error)
}
scenarios := []scenario{
@ -77,11 +76,8 @@ func TestOSCommandEditFile(t *testing.T) {
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
assert.EqualError(t, err, "No editor defined in $VISUAL or $EDITOR")
},
},
{
@ -93,28 +89,7 @@ func TestOSCommandEditFile(t *testing.T) {
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "nano", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
return exec.Command("exit", "0")
},
func(env string) string {
if env == "VISUAL" {
@ -123,9 +98,6 @@ func TestOSCommandEditFile(t *testing.T) {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
@ -139,7 +111,7 @@ func TestOSCommandEditFile(t *testing.T) {
assert.EqualValues(t, "emacs", name)
return nil
return exec.Command("exit", "0")
},
func(env string) string {
if env == "EDITOR" {
@ -148,9 +120,6 @@ func TestOSCommandEditFile(t *testing.T) {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
@ -164,14 +133,11 @@ func TestOSCommandEditFile(t *testing.T) {
assert.EqualValues(t, "vi", name)
return nil
return exec.Command("exit", "0")
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
@ -181,20 +147,20 @@ func TestOSCommandEditFile(t *testing.T) {
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
OSCmd.getenv = s.getenv
s.test(OSCmd.EditFile(s.filename))
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello `test`")
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
expected := "\"hello \\`test\\`\""
assert.EqualValues(t, expected, actual)
}
@ -207,7 +173,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
expected := `"hello 'test'"`
assert.EqualValues(t, expected, actual)
}
@ -220,7 +186,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
expected := `"hello \"test\""`
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)
}
@ -270,7 +249,7 @@ func TestOSCommandFileType(t *testing.T) {
{
"testDirectory",
func() {
if err := os.Mkdir("testDirectory", 0644); err != nil {
if err := os.Mkdir("testDirectory", 0o644); err != nil {
panic(err)
}
},
@ -310,7 +289,7 @@ func TestOSCommandCreateTempFile(t *testing.T) {
func(path string, err error) {
assert.NoError(t, err)
content, err := ioutil.ReadFile(path)
content, err := os.ReadFile(path)
assert.NoError(t, err)
assert.Equal(t, "content", string(content))
@ -324,3 +303,53 @@ 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

@ -2,10 +2,8 @@ package commands
func getPlatform() *Platform {
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
os: "windows",
shell: "cmd",
shellArg: "/c",
}
}

5
pkg/commands/project.go Normal file
View file

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

View file

@ -1,10 +1,10 @@
package commands
import (
"context"
"os/exec"
"github.com/docker/docker/api/types"
"github.com/fatih/color"
"github.com/docker/docker/api/types/container"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
)
@ -13,43 +13,41 @@ import (
type Service struct {
Name string
ID string
ProjectName string
OSCommand *OSCommand
Log *logrus.Entry
Container *Container
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(), s.Name, cont.GetDisplayCPUPerc()}
}
// Remove removes the service's containers
func (s *Service) Remove(options types.ContainerRemoveOptions) error {
func (s *Service) Remove(options container.RemoveOptions) error {
return s.Container.Remove(options)
}
// Stop stops the service's containers
func (s *Service) Stop() error {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StopService
command := utils.ApplyTemplate(
templateString,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
return s.OSCommand.RunCommand(command)
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService)
}
// Up up's the service
func (s *Service) Up() error {
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.UpService)
}
// Restart restarts the service
func (s *Service) Restart() error {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.RestartService
return s.runCommand(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(
templateString,
templateCmdStr,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
return s.OSCommand.RunCommand(command)
@ -60,11 +58,6 @@ func (s *Service) Attach() (*exec.Cmd, error) {
return s.Container.Attach()
}
// Top returns process information
func (s *Service) Top() (types.ContainerProcessList, error) {
return s.Container.Top()
}
// ViewLogs attaches to a subprocess viewing the service's logs
func (s *Service) ViewLogs() (*exec.Cmd, error) {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs
@ -80,12 +73,12 @@ func (s *Service) ViewLogs() (*exec.Cmd, error) {
}
// RenderTop renders the process list of the service
func (s *Service) RenderTop() (string, error) {
func (s *Service) RenderTop(ctx context.Context) (string, error) {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop
command := utils.ApplyTemplate(
templateString,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
return s.OSCommand.RunCommandWithOutput(command)
return s.OSCommand.RunCommandWithOutputContext(ctx, command)
}

159
pkg/commands/ssh/ssh.go Normal file
View file

@ -0,0 +1,159 @@
package ssh
import (
"context"
"fmt"
"io"
"net"
"net/url"
"os"
"os/exec"
"path"
"time"
)
// we only need these two methods from our OSCommand struct, for killing commands
type CmdKiller interface {
Kill(cmd *exec.Cmd) error
PrepareForChildren(cmd *exec.Cmd)
}
type SSHHandler struct {
oSCommand CmdKiller
dialContext func(ctx context.Context, network, addr string) (io.Closer, error)
startCmd func(*exec.Cmd) error
tempDir func(dir string, pattern string) (name string, err error)
getenv func(key string) string
setenv func(key, value string) error
}
func NewSSHHandler(oSCommand CmdKiller) *SSHHandler {
return &SSHHandler{
oSCommand: oSCommand,
dialContext: func(ctx context.Context, network, addr string) (io.Closer, error) {
return (&net.Dialer{}).DialContext(ctx, network, addr)
},
startCmd: func(cmd *exec.Cmd) error { return cmd.Start() },
tempDir: os.MkdirTemp,
getenv: os.Getenv,
setenv: os.Setenv,
}
}
// HandleSSHDockerHost overrides the DOCKER_HOST environment variable
// to point towards a local unix socket tunneled over SSH to the specified ssh host.
func (self *SSHHandler) HandleSSHDockerHost() (io.Closer, error) {
const key = "DOCKER_HOST"
ctx := context.Background()
u, err := url.Parse(self.getenv(key))
if err != nil {
// if no or an invalid docker host is specified, continue nominally
return noopCloser{}, nil
}
// if the docker host scheme is "ssh", forward the docker socket before creating the client
if u.Scheme == "ssh" {
tunnel, err := self.createDockerHostTunnel(ctx, u.Host)
if err != nil {
return noopCloser{}, fmt.Errorf("tunnel ssh docker host: %w", err)
}
err = self.setenv(key, tunnel.socketPath)
if err != nil {
return noopCloser{}, fmt.Errorf("override DOCKER_HOST to tunneled socket: %w", err)
}
return tunnel, nil
}
return noopCloser{}, nil
}
type noopCloser struct{}
func (noopCloser) Close() error { return nil }
type tunneledDockerHost struct {
socketPath string
cmd *exec.Cmd
oSCommand CmdKiller
}
var _ io.Closer = (*tunneledDockerHost)(nil)
func (t *tunneledDockerHost) Close() error {
return t.oSCommand.Kill(t.cmd)
}
func (self *SSHHandler) createDockerHostTunnel(ctx context.Context, remoteHost string) (*tunneledDockerHost, error) {
socketDir, err := self.tempDir("/tmp", "lazydocker-sshtunnel-")
if err != nil {
return nil, fmt.Errorf("create ssh tunnel tmp file: %w", err)
}
localSocket := path.Join(socketDir, "dockerhost.sock")
cmd, err := self.tunnelSSH(ctx, remoteHost, localSocket)
if err != nil {
return nil, fmt.Errorf("tunnel docker host over ssh: %w", err)
}
// set a reasonable timeout, then wait for the socket to dial successfully
// before attempting to create a new docker client
const socketTunnelTimeout = 8 * time.Second
ctx, cancel := context.WithTimeout(ctx, socketTunnelTimeout)
defer cancel()
err = self.retrySocketDial(ctx, localSocket)
if err != nil {
return nil, fmt.Errorf("ssh tunneled socket never became available: %w", err)
}
// construct the new DOCKER_HOST url with the proper scheme
newDockerHostURL := url.URL{Scheme: "unix", Path: localSocket}
return &tunneledDockerHost{
socketPath: newDockerHostURL.String(),
cmd: cmd,
oSCommand: self.oSCommand,
}, nil
}
// Attempt to dial the socket until it becomes available.
// The retry loop will continue until the parent context is canceled.
func (self *SSHHandler) retrySocketDial(ctx context.Context, socketPath string) error {
t := time.NewTicker(1 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
}
// attempt to dial the socket, exit on success
err := self.tryDial(ctx, socketPath)
if err != nil {
continue
}
return nil
}
}
// Try to dial the specified unix socket, immediately close the connection if successfully created.
func (self *SSHHandler) tryDial(ctx context.Context, socketPath string) error {
conn, err := self.dialContext(ctx, "unix", socketPath)
if err != nil {
return err
}
defer conn.Close()
return nil
}
func (self *SSHHandler) tunnelSSH(ctx context.Context, host, localSocket string) (*exec.Cmd, error) {
cmd := exec.CommandContext(ctx, "ssh", "-L", localSocket+":/var/run/docker.sock", host, "-N")
self.oSCommand.PrepareForChildren(cmd)
err := self.startCmd(cmd)
if err != nil {
return nil, err
}
return cmd, nil
}

View file

@ -0,0 +1,109 @@
package ssh
import (
"context"
"io"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSSHHandlerHandleSSHDockerHost(t *testing.T) {
type scenario struct {
testName string
envVarValue string
expectedDialContextCount int
expectedStartCmdCount int
}
scenarios := []scenario{
{
testName: "No env var set",
envVarValue: "",
expectedDialContextCount: 0,
expectedStartCmdCount: 0,
},
{
testName: "Env var set with https scheme",
envVarValue: "https://myhost.com",
expectedStartCmdCount: 0,
expectedDialContextCount: 0,
},
{
testName: "Env var set with ssh scheme",
envVarValue: "ssh://myhost@192.168.5.178",
expectedStartCmdCount: 1,
expectedDialContextCount: 1,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
getenv := func(key string) string {
if key != "DOCKER_HOST" {
t.Errorf("Expected key to be DOCKER_HOST, got %s", key)
}
return s.envVarValue
}
tempDir := func(dir string, pattern string) (string, error) {
assert.Equal(t, "/tmp", dir)
assert.Equal(t, "lazydocker-sshtunnel-", pattern)
return "/tmp/lazydocker-ssh-tunnel-12345", nil
}
setenv := func(key, value string) error {
assert.Equal(t, "DOCKER_HOST", key)
assert.Equal(t, "unix:///tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock", value)
return nil
}
startCmdCount := 0
startCmd := func(cmd *exec.Cmd) error {
assert.EqualValues(t, []string{"ssh", "-L", "/tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock:/var/run/docker.sock", "192.168.5.178", "-N"}, cmd.Args)
startCmdCount++
return nil
}
dialContextCount := 0
dialContext := func(ctx context.Context, network string, address string) (io.Closer, error) {
assert.Equal(t, "unix", network)
assert.Equal(t, "/tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock", address)
dialContextCount++
return noopCloser{}, nil
}
handler := &SSHHandler{
oSCommand: &fakeCmdKiller{},
dialContext: dialContext,
startCmd: startCmd,
tempDir: tempDir,
getenv: getenv,
setenv: setenv,
}
_, err := handler.HandleSSHDockerHost()
assert.NoError(t, err)
assert.Equal(t, s.expectedDialContextCount, dialContextCount)
assert.Equal(t, s.expectedStartCmdCount, startCmdCount)
})
}
}
type fakeCmdKiller struct{}
func (self *fakeCmdKiller) Kill(cmd *exec.Cmd) error {
return nil
}
func (self *fakeCmdKiller) PrepareForChildren(cmd *exec.Cmd) {}

View file

@ -2,10 +2,9 @@ package commands
import (
"context"
"sort"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
@ -13,37 +12,28 @@ import (
// Volume : A docker Volume
type Volume struct {
Name string
Volume *types.Volume
Volume *volume.Volume
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
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
func (c *DockerCommand) RefreshVolumes() error {
result, err := c.Client.VolumeList(context.Background(), filters.Args{})
func (c *DockerCommand) RefreshVolumes() ([]*Volume, error) {
result, err := c.Client.VolumeList(context.Background(), volume.ListOptions{})
if err != nil {
return err
return nil, err
}
volumes := result.Volumes
ownVolumes := make([]*Volume, len(volumes))
sort.Slice(volumes, func(i, j int) bool {
return volumes[i].Name < volumes[j].Name
})
for i, volume := range volumes {
for i, vol := range volumes {
ownVolumes[i] = &Volume{
Name: volume.Name,
Volume: volume,
Name: vol.Name,
Volume: vol,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
@ -51,9 +41,7 @@ func (c *DockerCommand) RefreshVolumes() error {
}
}
c.Volumes = ownVolumes
return nil
return ownVolumes, nil
}
// PruneVolumes prunes volumes

View file

@ -13,14 +13,13 @@
package config
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
yaml "github.com/jesseduffield/yaml"
"github.com/shibukawa/configdir"
"github.com/OpenPeeDeeP/xdg"
"github.com/jesseduffield/yaml"
)
// UserConfig holds all of the user-configurable options
@ -29,16 +28,13 @@ type UserConfig struct {
// hide things
Gui GuiConfig `yaml:"gui,omitempty"`
// Reporting determines whether events are reported such as errors (and maybe
// application opens but I'm not decided on that yet because it sounds kinda
// creepy but I also would love to know how many people are using this
// program)
Reporting string `yaml:"reporting,omitempty"`
// ConfirmOnQuit when enabled prompts you to confirm you want to quit when you
// hit esc or q when no confirmation panels are open
ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"`
// Logs determines how we render/filter a container's logs
Logs LogsConfig `yaml:"logs,omitempty"`
// CommandTemplates determines what commands actually get called when we run
// certain commands
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
@ -50,22 +46,31 @@ type UserConfig struct {
// those are found in the commands package
CustomCommands CustomCommands `yaml:"customCommands,omitempty"`
// BulkCommands are commands that apply to all items in a panel e.g.
// killing all containers, stopping all services, or pruning all images
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
// OS determines what defaults are set for opening files and links
OS OSConfig `yaml:"oS,omitempty"`
// Update is currently not being used, but like lazygit, it may be used down
// the line to help you update automatically.
Update UpdateConfig `yaml:"update,omitempty"`
// Stats determines how long lazydocker will gather container stats for, and
// what stat info to graph
Stats StatsConfig `yaml:"stats,omitempty"`
// Replacements determines how we render an item's info
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.
type ThemeConfig struct {
ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"`
InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor,omitempty"`
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
}
@ -76,6 +81,9 @@ type GuiConfig struct {
// scrolling the main panel
ScrollHeight int `yaml:"scrollHeight,omitempty"`
// Language determines which language the GUI displayed.
Language string `yaml:"language,omitempty"`
// ScrollPastBottom determines whether you can scroll past the bottom of the
// main view
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
@ -106,6 +114,34 @@ type GuiConfig struct {
// WrapMainPanel determines whether we use word wrap on the main panel
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
// LegacySortContainers determines if containers should be sorted using legacy approach.
// By default, containers are now sorted by status. This setting allows users to
// use legacy behaviour instead.
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
// If 0.333, then the side panels will be 1/3 of the screen's width
SidePanelWidth float64 `yaml:"sidePanelWidth"`
// Determines whether we show the bottom line (the one containing keybinding
// info and the status of the app).
ShowBottomLine bool `yaml:"showBottomLine"`
// When true, increases vertical space used by focused side panel,
// creating an accordion effect
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
@ -116,9 +152,23 @@ type CommandTemplatesConfig struct {
// .Service.Name }}
RestartService string `yaml:"restartService,omitempty"`
// StartService is just like the above but for starting
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
// 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
// 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
@ -150,21 +200,11 @@ type CommandTemplatesConfig struct {
// and ensure they're running before trying to run the service at hand
RecreateService string `yaml:"recreateService,omitempty"`
// ViewContainerLogs is like ViewServiceLogs but for containers
ViewContainerLogs string `yaml:"viewContainerLogs,omitempty"`
// ContainerLogs shows the logs of a container. By default this restricts the
// output to (as of right now) the last hour. This is for the sake of
// performance, and you can feel free to change this
ContainerLogs string `yaml:"containerLogs,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
AllLogs string `yaml:"allLogs,omitempty"`
// ViewAllLogs is to AllLogs what ViewContainerLogs is to ContainerLogs. It's
// just the command we use when you want to see all logs in a subprocess with
// no filtering
// ViewAllLogs is the command we use when you want to see all logs in a subprocess with no filtering
ViewAllLogs string `yaml:"viewAlLogs,omitempty"`
// DockerComposeConfig is the command for viewing the config of your docker
@ -191,12 +231,6 @@ type OSConfig struct {
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
}
// UpdateConfig is currently not being used, but may be used down the line to
// allow for automatic updates
type UpdateConfig struct {
Method string `yaml:"method,omitempty"`
}
// GraphConfig specifies how to make a graph of recorded container stats
type GraphConfig struct {
// Min sets the minimum value that you want to display. If you want to set
@ -258,6 +292,21 @@ type CustomCommands struct {
// Services contains the custom commands for services
Services []CustomCommand `yaml:"services,omitempty"`
// Images contains the custom commands for images
Images []CustomCommand `yaml:"images,omitempty"`
// Volumes contains the custom commands for volumes
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
type Replacements struct {
// ImageNamePrefixes tells us how to replace a prefix in the Docker image name
ImageNamePrefixes map[string]string `yaml:"imageNamePrefixes,omitempty"`
}
// CustomCommand is a template for a command we want to run against a service or
@ -272,6 +321,10 @@ type CustomCommand struct {
// option where the output plays in the main panel.
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
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
// /bin/sh`
@ -282,6 +335,15 @@ type CustomCommand struct {
// field has no effect on customcommands under the 'communications' part of
// the customCommand config.
ServiceNames []string `yaml:"serviceNames"`
// InternalFunction is the name of a function inside lazydocker that we want to run, as opposed to a command-line command. This is only used internally and can't be configured by the user
InternalFunction func() error `yaml:"-"`
}
type LogsConfig struct {
Timestamps bool `yaml:"timestamps,omitempty"`
Since string `yaml:"since,omitempty"`
Tail string `yaml:"tail,omitempty"`
}
// GetDefaultConfig returns the application default configuration NOTE (to
@ -289,25 +351,47 @@ type CustomCommand struct {
// the boolean zero value and this will be ignored when parsing the user's
// config
func GetDefaultConfig() UserConfig {
duration, err := time.ParseDuration("3m")
if err != nil {
panic(err)
}
return UserConfig{
Gui: GuiConfig{
ScrollHeight: 2,
Language: "auto",
ScrollPastBottom: false,
IgnoreMouseEvents: false,
Theme: ThemeConfig{
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"default"},
SelectedLineBgColor: []string{"blue"},
OptionsTextColor: []string{"blue"},
},
ShowAllContainers: false,
ReturnImmediately: false,
WrapMainPanel: false,
ShowAllContainers: false,
ReturnImmediately: false,
WrapMainPanel: true,
LegacySortContainers: false,
SidePanelWidth: 0.3333,
ShowBottomLine: true,
ExpandFocusedSidePanel: false,
ScreenMode: "normal",
ContainerStatusHealthStyle: "long",
},
Reporting: "undetermined",
ConfirmOnQuit: false,
Logs: LogsConfig{
Timestamps: false,
Since: "60m",
Tail: "",
},
CommandTemplates: CommandTemplatesConfig{
DockerCompose: "docker-compose",
DockerCompose: "docker compose",
RestartService: "{{ .DockerCompose }} restart {{ .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 }}",
RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}",
@ -317,25 +401,63 @@ func GetDefaultConfig() UserConfig {
ViewAllLogs: "{{ .DockerCompose }} logs",
DockerComposeConfig: "{{ .DockerCompose }} config",
CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet",
ContainerLogs: "docker logs --timestamps --follow --since=60m {{ .Container.ID }}",
ViewContainerLogs: "docker logs --timestamps --follow --since=60m {{ .Container.ID }}",
ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}",
},
CustomCommands: CustomCommands{
Containers: []CustomCommand{
Containers: []CustomCommand{},
Services: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
BulkCommands: CustomCommands{
Services: []CustomCommand{
{
Name: "bash",
Command: "docker exec -it {{ .Container.ID }} /bin/sh",
Name: "up",
Command: "{{ .DockerCompose }} up -d",
},
{
Name: "up (attached)",
Command: "{{ .DockerCompose }} up",
Attach: true,
},
{
Name: "stop",
Command: "{{ .DockerCompose }} stop",
},
{
Name: "pull",
Command: "{{ .DockerCompose }} pull",
Attach: true,
},
{
Name: "build",
Command: "{{ .DockerCompose }} build --parallel --force-rm",
Attach: true,
},
{
Name: "down",
Command: "{{ .DockerCompose }} down",
},
{
Name: "down with volumes",
Command: "{{ .DockerCompose }} down --volumes",
},
{
Name: "down with images",
Command: "{{ .DockerCompose }} down --rmi all",
},
{
Name: "down with volumes and images",
Command: "{{ .DockerCompose }} down --volumes --rmi all",
},
},
Services: []CustomCommand{},
Containers: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
OS: GetPlatformDefaultConfig(),
Update: UpdateConfig{
Method: "never",
},
Stats: StatsConfig{
MaxDuration: duration,
Graphs: []GraphConfig{
{
Caption: "CPU (%)",
@ -349,23 +471,28 @@ func GetDefaultConfig() UserConfig {
},
},
},
Replacements: Replacements{
ImageNamePrefixes: map[string]string{},
},
}
}
// AppConfig contains the base configuration fields required for lazygit.
// AppConfig contains the base configuration fields required for lazydocker.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
Name string `long:"name" env:"NAME" default:"lazydocker"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *UserConfig
ConfigDir string
ProjectDir string
ProjectName string
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string) (*AppConfig, error) {
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir(name)
if err != nil {
return nil, err
@ -376,7 +503,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
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 {
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
}
@ -390,20 +517,40 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
BuildSource: buildSource,
UserConfig: userConfig,
ConfigDir: configDir,
ProjectDir: projectDir,
ProjectName: projectName,
}
return appConfig, nil
}
func findOrCreateConfigDir(projectName string) (string, error) {
configDirs := configdir.New("jesseduffield", projectName)
folders := configDirs.QueryFolders(configdir.Global)
func configDirForVendor(vendor string, projectName string) string {
envConfigDir := os.Getenv("CONFIG_DIR")
if envConfigDir != "" {
return envConfigDir
}
configDirs := xdg.New(vendor, projectName)
return configDirs.ConfigHome()
}
if err := folders[0].CreateParentDir("foo"); err != nil {
func configDir(projectName string) string {
legacyConfigDirectory := configDirForVendor("jesseduffield", projectName)
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
return legacyConfigDirectory
}
configDirectory := configDirForVendor("", projectName)
return configDirectory
}
func findOrCreateConfigDir(projectName string) (string, error) {
folder := configDir(projectName)
err := os.MkdirAll(folder, 0o755)
if err != nil {
return "", err
}
return folders[0].Path, nil
return folder, nil
}
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
@ -427,7 +574,7 @@ func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
}
}
content, err := ioutil.ReadFile(fileName)
content, err := os.ReadFile(fileName)
if err != nil {
return nil, err
}
@ -453,7 +600,7 @@ func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) erro
return err
}
file, err := os.OpenFile(c.ConfigFilename(), os.O_WRONLY|os.O_CREATE, 0666)
file, err := os.OpenFile(c.ConfigFilename(), os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return err
}

View file

@ -1,19 +1,21 @@
package config
import (
"os"
"testing"
"github.com/jesseduffield/yaml"
)
func TestDockerComposeCommandNoFiles(t *testing.T) {
composeFiles := []string{}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles)
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
actual := conf.UserConfig.CommandTemplates.DockerCompose
expected := "docker-compose"
expected := "docker compose"
if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual)
}
@ -21,14 +23,13 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
func TestDockerComposeCommandSingleFile(t *testing.T) {
composeFiles := []string{"one.yml"}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles)
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
actual := conf.UserConfig.CommandTemplates.DockerCompose
expected := "docker-compose -f one.yml"
expected := "docker compose -f one.yml"
if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual)
}
@ -36,15 +37,62 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles)
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
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 {
t.Fatalf("Expected %s but got %s", expected, actual)
}
}
func TestWritingToConfigFile(t *testing.T) {
// init the AppConfig
emptyComposeFiles := []string{}
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "")
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
testFn := func(t *testing.T, ac *AppConfig, newValue bool) {
t.Helper()
updateFn := func(uc *UserConfig) error {
uc.ConfirmOnQuit = newValue
return nil
}
err = ac.WriteToUserConfig(updateFn)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
file, err := os.OpenFile(ac.ConfigFilename(), os.O_RDONLY, 0o660)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
sampleUC := UserConfig{}
err = yaml.NewDecoder(file).Decode(&sampleUC)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
err = file.Close()
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
if sampleUC.ConfirmOnQuit != newValue {
t.Fatalf("Got %v, Expected %v\n", sampleUC.ConfirmOnQuit, newValue)
}
}
// insert value into an empty file
testFn(t, conf, true)
// modifying an existing file that already has 'ConfirmOnQuit'
testFn(t, conf, false)
}

View file

@ -1,3 +1,4 @@
//go:build !windows && !linux
// +build !windows,!linux
package config

View file

@ -1,6 +1,8 @@
package gui
import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
@ -49,19 +51,29 @@ func (m *statusManager) getStatusString() string {
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go func() {
gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.addWaitingStatus(name)
return nil
})
gui.statusManager.addWaitingStatus(name)
defer gui.g.Update(func(g *gocui.Gui) error {
defer func() {
gui.statusManager.removeStatus(name)
return nil
})
}()
go func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
return
}
if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
gui.Log.Warn(err)
}
}
}()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
return gui.createErrorPanel(err.Error())
})
}
}()

227
pkg/gui/arrangement.go Normal file
View file

@ -0,0 +1,227 @@
package gui
import (
"github.com/jesseduffield/lazycore/pkg/boxlayout"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/mattn/go-runewidth"
"github.com/samber/lo"
)
// In this file we use the boxlayout package, along with knowledge about the app's state,
// to arrange the windows (i.e. panels) on the screen.
const INFO_SECTION_PADDING = " "
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
minimumHeight := 9
minimumWidth := 10
width, height := gui.g.Size()
if width < minimumWidth || height < minimumHeight {
return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
}
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active
infoSectionSize := 0
if showInfoSection {
infoSectionSize = 1
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Window: "main",
Weight: mainSectionWeight,
},
},
},
{
Direction: boxlayout.COLUMN,
Size: infoSectionSize,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
}
func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentStaticWindowName()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 1
sideSectionWeight = 0
} else {
if gui.State.ScreenMode == SCREEN_HALF {
mainSectionWeight = 1
} else if gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 0
}
}
return sideSectionWeight, mainSectionWeight
}
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
result := []*boxlayout.Box{}
if len(appStatus) > 0 {
result = append(result,
&boxlayout.Box{
Window: "appStatus",
Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING),
},
)
}
if gui.State.Filter.active {
return append(result, []*boxlayout.Box{
{
Window: "filterPrefix",
Size: runewidth.StringWidth(gui.filterPrompt()),
},
{
Window: "filter",
Weight: 1,
},
}...)
}
result = append(result,
[]*boxlayout.Box{
{
Window: "options",
Weight: 1,
},
{
Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
},
}...,
)
return result
}
func (gui *Gui) sideViewNames() []string {
visibleSidePanels := lo.Filter(gui.allSidePanels(), func(panel panels.ISideListPanel, _ int) bool {
return !panel.IsHidden()
})
return lo.Map(visibleSidePanels, func(panel panels.ISideListPanel, _ int) string {
return panel.GetView().Name()
})
}
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName()
sideWindowNames := gui.sideViewNames()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: 0,
}
}
}
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
return fullHeightBox(window)
})
} else if height >= 28 {
accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
Window: defaultBox.Window,
Weight: 2,
}
}
return defaultBox
}
// The project panel is compact (Size: 3) when not focused, but expands
// when focused to show the list of projects. This only applies when the
// project panel is actually visible (i.e. we are inside a compose project).
if len(sideWindowNames) > 0 && sideWindowNames[0] == "project" {
projectBox := &boxlayout.Box{
Window: sideWindowNames[0],
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 {
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 {
squashedHeight := 1
if height >= 21 {
squashedHeight = 3
}
squashedSidePanelBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: squashedHeight,
}
}
}
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
return squashedSidePanelBox(window)
})
}
}

View file

@ -8,7 +8,6 @@ package gui
import (
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
@ -16,25 +15,27 @@ import (
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if err := gui.closeConfirmationPrompt(); err != nil {
return err
}
if function != nil {
if err := function(g, v); err != nil {
return err
}
}
return gui.closeConfirmationPrompt(g)
return nil
}
}
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
return nil // if it's already been closed we can just return
func (gui *Gui) closeConfirmationPrompt() error {
if err := gui.returnFocus(); err != nil {
return err
}
if err := gui.returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
gui.g.DeleteViewKeybindings("confirmation")
gui.Views.Confirmation.Visible = false
return nil
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
@ -51,8 +52,8 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
width, height := g.Size()
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
width, height := gui.g.Size()
panelWidth := width / 2
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return width/2 - panelWidth/2,
@ -61,62 +62,56 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
height/2 + panelHeight/2
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, 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()
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false)
err := gui.prepareConfirmationPanel(title, "")
if err != nil {
return err
}
confirmationView.Editable = true
return gui.setKeyBindings(g, handleConfirm, nil)
gui.Views.Confirmation.Editable = true
return gui.setKeyBindings(gui.g, handleConfirm, nil)
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
func (gui *Gui) prepareConfirmationPanel(title, prompt string) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
confirmationView := gui.Views.Confirmation
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
return nil, err
}
confirmationView.HasLoader = hasLoader
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = gocui.ColorWhite
return err
}
confirmationView.Title = title
confirmationView.Visible = true
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(gui.g, currentView, confirmationView)
return gui.switchFocus(confirmationView)
})
return confirmationView, nil
return nil
}
func (gui *Gui) onNewPopupPanel() {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
gui.Views.Menu.Visible = false
gui.Views.Confirmation.Visible = false
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose)
// It is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password.
// The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil.
// nolint:unparam
func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(title, prompt, handleConfirm, handleClose)
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
func (gui *Gui) createPopupPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
if gui.currentViewName() == "confirmation" {
if err := gui.closeConfirmationPrompt(); err != nil {
gui.Log.Error(err.Error())
}
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
err := gui.prepareConfirmationPanel(title, prompt)
if err != nil {
return err
}
confirmationView.Editable = false
gui.Views.Confirmation.Editable = false
if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err
}
@ -144,27 +139,16 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
return nil
}
// createSpecificErrorPanel allows you to create an error popup, specifying the
// view to be focused when the user closes the popup, and a boolean specifying
// whether we will log the error. If the message may include a user password,
// this function is to be used over the more generic createErrorPanel, with
// willLog set to false
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
if willLog {
go func() {
// when reporting is switched on this log call sometimes introduces
// a delay on the error panel popping up. Here I'm adding a second wait
// so that the error is logged while the user is reading the error message
time.Sleep(time.Second)
gui.Log.Error(message)
}()
}
func (gui *Gui) createErrorPanel(message string) error {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.ErrorTitle, coloredMessage, nil, nil)
return gui.createConfirmationPanel(gui.Tr.ErrorTitle, coloredMessage, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
func (gui *Gui) renderConfirmationOptions() error {
optionsMap := map[string]string{
"n/esc": gui.Tr.No,
"y/enter": gui.Tr.Yes,
}
return gui.renderOptionsMap(optionsMap)
}

152
pkg/gui/container_logs.go Normal file
View file

@ -0,0 +1,152 @@
package gui
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped)
},
Duration: time.Millisecond * 200,
// 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)
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{}) {
gui.clearMainView()
defer func() {
notifyStopped <- struct{}{}
}()
mainView := gui.Views.Main
if err := gui.writeContainerLogs(container, ctx, mainView); err != nil {
gui.Log.Error(err)
}
// if we are here because the task has been stopped, we should return
// if we are here then the container must have exited, meaning we should wait until it's back again before
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
result, err := container.Inspect()
if err != nil {
// if we get an error, then the container has probably been removed so we'll get out of here
gui.Log.Error(err)
return
}
if result.State.Running {
return
}
}
}
}
func (gui *Gui) renderLogsToStdout(container *commands.Container) {
stop := make(chan os.Signal, 1)
defer signal.Stop(stop)
ctx, cancel := context.WithCancel(context.Background())
go func() {
signal.Notify(stop, os.Interrupt)
<-stop
cancel()
}()
if err := gui.g.Suspend(); err != nil {
gui.Log.Error(err)
return
}
defer func() {
if err := gui.g.Resume(); err != nil {
gui.Log.Error(err)
}
}()
if err := gui.writeContainerLogs(container, ctx, os.Stdout); err != nil {
gui.Log.Error(err)
return
}
gui.promptToReturn()
}
func (gui *Gui) promptToReturn() {
if !gui.Config.UserConfig.Gui.ReturnImmediately {
fmt.Fprintf(os.Stdout, "\n\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
// wait for enter press
if _, err := fmt.Scanln(); err != nil {
gui.Log.Error(err)
}
}
}
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
Since: gui.Config.UserConfig.Logs.Since,
Tail: gui.Config.UserConfig.Logs.Tail,
Follow: true,
})
if err != nil {
gui.Log.Error(err)
return err
}
defer readCloser.Close()
if !ctr.DetailsLoaded() {
// 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)
if err != nil {
return err
}
} else {
_, err = stdcopy.StdCopy(writer, writer, readCloser)
if err != nil {
return err
}
}
return nil
}

View file

@ -1,103 +1,196 @@
package gui
import (
"encoding/json"
"context"
"fmt"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/fatih/color"
"github.com/go-errors/errors"
"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"
)
// list panel functions
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] {
// Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context.
isStandaloneContainer := func(container *commands.Container) bool {
if container.OneOff || container.ServiceName == "" {
return true
}
func (gui *Gui) getContainerContexts() []string {
return []string{"logs", "stats", "config", "top"}
}
func (gui *Gui) getContainerContextTitles() []string {
return []string{gui.Tr.LogsTitle, gui.Tr.StatsTitle, 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 service.Name == container.ServiceName && service.ProjectName == container.ProjectName
})
}
return gui.DockerCommand.DisplayContainers[selectedLine], nil
return &panels.SideListPanel[*commands.Container]{
ContextState: &panels.ContextState[*commands.Container]{
GetMainTabs: func() []panels.MainTab[*commands.Container] {
return []panels.MainTab[*commands.Container]{
{
Key: "logs",
Title: gui.Tr.LogsTitle,
Render: gui.renderContainerLogsToMain,
},
{
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)
},
}
}
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)
var containerStates = map[string]int{
"running": 1,
"exited": 2,
"created": 3,
}
func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
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]
value := ""
if len(splitEnv) > 1 {
value = splitEnv[1]
}
return []string{
utils.ColoredString(key+":", color.FgGreen),
utils.ColoredString(value, color.FgYellow),
}
})
output, err := utils.RenderTable(envVarsList)
if err != nil {
if err != gui.Errors.ErrNoContainers {
return err
}
return nil
gui.Log.Error(err)
return gui.Tr.CannotDisplayEnvVariables
}
if err := gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v); err != nil {
return err
}
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.renderContainerLogs(container); err != nil {
return err
}
case "config":
if err := gui.renderContainerConfig(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
return output
}
func (gui *Gui) renderContainerConfig(container *commands.Container) error {
mainView := gui.getMainView()
mainView.Autoscroll = false
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
func (gui *Gui) renderContainerConfig(container *commands.Container) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfigStr(container) })
}
func (gui *Gui) containerConfigStr(container *commands.Container) string {
if !container.DetailsLoaded() {
return gui.Tr.WaitingForContainerInfo
}
padding := 10
output := ""
output += utils.WithPadding("ID: ", padding) + container.ID + "\n"
output += utils.WithPadding("Name: ", padding) + container.Name + "\n"
output += utils.WithPadding("Image: ", padding) + container.Details.Config.Image + "\n"
output += utils.WithPadding("Command: ", padding) + strings.Join(append([]string{container.Details.Path}, container.Details.Args...), " ") + "\n"
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, container.Details.Config.Labels)
output += "\n"
@ -107,9 +200,9 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error {
output += "\n"
for _, mount := range container.Details.Mounts {
if mount.Type == "volume" {
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Name)
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name)
} else {
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Source, mount.Destination)
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination)
}
}
} else {
@ -128,354 +221,221 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error {
output += "none\n"
}
data, err := json.MarshalIndent(&container.Details, "", " ")
data, err := utils.MarshalIntoYaml(&container.Details)
if err != nil {
return err
return fmt.Sprintf("Error marshalling container details: %v", err)
}
output += fmt.Sprintf("\nFull details:\n\n%s", string(data))
return gui.T.NewTask(func(stop chan struct{}) {
gui.renderString(gui.g, "main", output)
})
output += fmt.Sprintf("\nFull details:\n\n%s", utils.ColoredYamlString(string(data)))
return output
}
func (gui *Gui) renderContainerStats(container *commands.Container) error {
mainView := gui.getMainView()
mainView.Autoscroll = false
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 {
gui.createErrorPanel(gui.g, err.Error())
}
gui.reRenderString(gui.g, "main", contents)
})
}
func (gui *Gui) renderContainerTop(container *commands.Container) error {
mainView := gui.getMainView()
mainView.Autoscroll = false
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 {
gui.reRenderString(gui.g, "main", err.Error())
}
gui.reRenderString(gui.g, "main", contents)
})
}
func (gui *Gui) renderContainerLogs(container *commands.Container) error {
mainView := gui.getMainView()
mainView.Autoscroll = true
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) {
gui.clearMainView()
command := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.ContainerLogs,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Container: container}),
)
cmd := gui.OSCommand.RunCustomCommand(command)
mainView := gui.getMainView()
cmd.Stdout = mainView
cmd.Stderr = mainView
cmd.Start()
go func() {
<-stop
if err := gui.OSCommand.Kill(cmd); err != nil {
gui.Log.Warn(err)
func (gui *Gui) renderContainerStats(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
contents, err := presentation.RenderStats(gui.Config.UserConfig, container, gui.Views.Main.Width())
if err != nil {
_ = gui.createErrorPanel(err.Error())
}
gui.Log.Info("killed container logs process")
return
}()
cmd.Wait()
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,
})
}
// if we are here because the task has been stopped, we should return
// if we are here then the container must have exited, meaning we should wait until it's back again before
L:
for {
select {
case <-stop:
return
default:
result, err := container.Inspect()
if err != nil {
// if we get an error, then the container has probably been removed so we'll get out of here
gui.Log.Error(err)
notifyStopped <- struct{}{}
return
}
if result.State.Running {
break L
}
time.Sleep(time.Millisecond * 100)
func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
contents, err := container.RenderTop(ctx)
if err != nil {
gui.RenderStringMain(err.Error())
}
}
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 {
containersView := gui.getContainersView()
if containersView == nil {
if gui.Views.Containers == nil {
// if the containersView hasn't been instantiated yet we just return
return nil
}
// keep track of current service selected so that we can reposition our cursor if it moves position in the list
sl := gui.State.Panels.Services.SelectedLine
var selectedService *commands.Service
if len(gui.DockerCommand.Services) > 0 {
selectedService = gui.DockerCommand.Services[sl]
}
originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
if err := gui.DockerCommand.RefreshContainersAndServices(); err != nil {
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
gui.Panels.Containers.List.GetAllItems(),
)
if err != nil {
return err
}
gui.Panels.Services.SetItems(services)
gui.Panels.Containers.SetItems(containers)
// see if our selected service has moved
if selectedService != nil {
for i, service := range gui.DockerCommand.Services {
if isServiceSelected {
for i, service := range gui.Panels.Services.List.GetItems() {
if service.ID == selectedService.ID {
if i == sl {
if i == originalSelectedLineIdx {
break
}
gui.State.Panels.Services.SelectedLine = i
if err := gui.focusPoint(0, i, len(gui.DockerCommand.Services), gui.getServicesView()); err != nil {
return err
}
gui.Panels.Services.SetSelectedLineIdx(i)
gui.Panels.Services.Refocus()
}
}
}
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
return gui.renderContainersAndServices()
}
func (gui *Gui) renderContainersAndServices() error {
if err := gui.Panels.Services.RerenderList(); err != nil {
return err
}
// 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 err := gui.Panels.Containers.RerenderList(); err != nil {
return err
}
if len(gui.DockerCommand.Services)-1 < gui.State.Panels.Services.SelectedLine {
gui.State.Panels.Services.SelectedLine = len(gui.DockerCommand.Services) - 1
}
gui.g.Update(func(g *gocui.Gui) error {
containersView.Clear()
isFocused := gui.g.CurrentView().Name() == "containers"
list, err := utils.RenderList(gui.DockerCommand.DisplayContainers, utils.IsFocused(isFocused))
if err != nil {
return err
}
fmt.Fprint(containersView, list)
if containersView == g.CurrentView() {
if err := gui.handleContainerSelect(g, containersView); err != nil {
return err
}
}
// 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
})
return nil
}
func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
gui.State.ShowExitedContainers = !gui.State.ShowExitedContainers
panelState := gui.State.Panels.Containers
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
}
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
runCommand bool
}
// GetDisplayStrings is a function.
func (r *removeContainerOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
return gui.Panels.Containers.RerenderList()
}
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
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
handleMenuPress := func(configOptions container.RemoveOptions) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := container.Remove(configOptions); err != nil {
if err := ctr.Remove(configOptions); err != nil {
if commands.HasErrorCode(err, commands.MustStopContainer) {
return gui.createConfirmationPanel(gui.g, v, 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 {
configOptions.Force = true
if err := container.Remove(configOptions); err != nil {
return err
}
return gui.refreshContainersAndServices()
return ctr.Remove(configOptions)
})
}, nil)
}
return gui.createErrorPanel(gui.g, err.Error())
return gui.createErrorPanel(err.Error())
}
return gui.refreshContainersAndServices()
return nil
})
}
return gui.createMenu("", options, len(options), handleMenuPress)
menuItems := []*types.MenuItem{
{
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) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
if err != nil {
return nil
}
func (gui *Gui) PauseContainer(container *commands.Container) error {
return gui.WithWaitingStatus(gui.Tr.PausingStatus, func() (err error) {
if container.Details.State.Paused {
err = container.Unpause()
} else {
err = container.Pause()
}
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := container.Stop(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainersAndServices()
})
}, nil)
}
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := container.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.refreshContainersAndServices()
})
}
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
c, err := container.Attach()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.SubProcess = c
return gui.Errors.ErrSubProcess
return gui.PauseContainer(ctr)
}
func (gui *Gui) handlePruneContainers(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := ctr.Stop(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}, nil)
}
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := ctr.Restart(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
c, err := ctr.Attach()
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocessWithMessage(c, gui.Tr.DetachFromContainerShortCut)
}
func (gui *Gui) handlePruneContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneContainers()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.createErrorPanel(err.Error())
}
return nil
})
@ -483,31 +443,126 @@ func (gui *Gui) handlePruneContainers(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
c, err := container.ViewLogs()
gui.renderLogsToStdout(ctr)
return nil
}
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return nil
}
gui.SubProcess = c
return gui.Errors.ErrSubProcess
return gui.containerExecShell(ctr)
}
func (gui *Gui) containerExecShell(container *commands.Container) error {
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Container: container,
})
// TODO: use SDK
resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject)
// attach and return the subprocess error
cmd := gui.OSCommand.ExecutableFromString(resolvedCommand)
return gui.runSubprocess(cmd)
}
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer()
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Container: container,
Container: ctr,
})
customCommands := gui.Config.UserConfig.CustomCommands.Containers
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleStopContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
if err := ctr.Stop(); err != nil {
gui.Log.Error(err)
}
}
return nil
})
}, nil)
}
func (gui *Gui) handleRemoveContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
if err := ctr.Remove(container.RemoveOptions{Force: true}); err != nil {
gui.Log.Error(err)
}
}
return nil
})
}, nil)
}
func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.StopAllContainers,
InternalFunction: gui.handleStopContainers,
},
{
Name: gui.Tr.RemoveAllContainers,
InternalFunction: gui.handleRemoveContainers,
},
{
Name: gui.Tr.PruneContainers,
InternalFunction: gui.handlePruneContainers,
},
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Containers...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}
// Open first port in browser
func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.openContainerInBrowser(ctr)
}
func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {
// skip if no any ports
if len(ctr.Container.Ports) == 0 {
return nil
}
// skip if the first port is not published
port := ctr.Container.Ports[0]
if port.IP == "" {
return nil
}
ip := port.IP
if ip == "0.0.0.0" {
ip = "localhost"
}
link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort)
return gui.OSCommand.OpenLink(link)
}

View file

@ -4,62 +4,56 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"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
}
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 {
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
// GetDisplayStrings is a function.
func (r *customCommandOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.name, utils.ColoredString(r.description, color.FgCyan)}
onPress := func() error {
if command.InternalFunction != nil {
return command.InternalFunction()
}
if command.Shell {
resolvedCommand = gui.OSCommand.NewCommandStringWithShell(resolvedCommand)
}
// if we have a command for attaching, we attach and return the subprocess error
if command.Attach {
return gui.runSubprocess(gui.OSCommand.ExecutableFromString(resolvedCommand))
}
return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.OSCommand.RunCommand(resolvedCommand); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
return &types.MenuItem{
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 {
options := make([]*customCommandOption, len(customCommands)+1)
for i, command := range customCommands {
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
options[i] = &customCommandOption{
customCommand: command,
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,
}
handleMenuPress := func(index int) error {
option := options[index]
if !option.runCommand {
return nil
}
// if we have a command for attaching, we attach and return the subprocess error
if option.customCommand.Attach {
cmd := gui.OSCommand.ExecutableFromString(option.command)
gui.SubProcess = cmd
return gui.Errors.ErrSubProcess
}
return gui.WithWaitingStatus(gui.Tr.RunningCustomCommandStatus, func() error {
if err := gui.OSCommand.RunCommand(option.command); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
})
}
return gui.createMenu("", options, len(options), handleMenuPress)
return gui.createCommandMenu(customCommands, commandObject, gui.Tr.CustomCommandTitle, gui.Tr.RunningCustomCommandStatus)
}
func (gui *Gui) createBulkCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
return gui.createCommandMenu(customCommands, commandObject, gui.Tr.BulkCommandTitle, gui.Tr.RunningBulkCommandStatus)
}

80
pkg/gui/filtering.go Normal file
View file

@ -0,0 +1,80 @@
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)
}

146
pkg/gui/focus.go Normal file
View file

@ -0,0 +1,146 @@
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()
}

45
pkg/gui/gocui.go Normal file
View file

@ -0,0 +1,45 @@
package gui
import (
"github.com/gookit/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
var gocuiColorMap = map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
// GetAttribute gets the gocui color attribute from the string
func GetGocuiAttribute(key string) gocui.Attribute {
if utils.IsValidHexValue(key) {
values := color.HEX(key).Values()
return gocui.NewRGBColor(int32(values[0]), int32(values[1]), int32(values[2]))
}
value, present := gocuiColorMap[key]
if present {
return value
}
return gocui.ColorDefault
}
// GetGocuiStyle bitwise OR's a list of attributes obtained via the given keys
func GetGocuiStyle(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute |= GetGocuiAttribute(key)
}
return attribute
}

View file

@ -1,93 +1,65 @@
package gui
import (
"context"
"os"
"strings"
"sync"
// "io"
// "io/ioutil"
"os/exec"
"time"
"github.com/docker/docker/api/types/events"
"github.com/go-errors/errors"
// "strings"
throttle "github.com/boz/go-throttle"
"github.com/jesseduffield/gocui"
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
"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/types"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/sasha-s/go-deadlock"
"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 {
ErrSubProcess error
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{
ErrSubProcess: errors.New(gui.Tr.RunningSubprocess),
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
type Gui struct {
g *gocui.Gui
Log *logrus.Entry
DockerCommand *commands.DockerCommand
OSCommand *commands.OSCommand
SubProcess *exec.Cmd
State guiState
Config *config.AppConfig
Tr *i18n.TranslationSet
Errors SentinelErrors
statusManager *statusManager
waitForIntro sync.WaitGroup
T *tasks.TaskManager
taskManager *tasks.TaskManager
ErrorChan chan error
CyclableViews []string
Views Views
// 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
// file refreshes
PauseBackgroundThreads bool
Mutexes
Panels Panels
}
type servicePanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
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 containerPanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
}
type statusState 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 Mutexes struct {
SubprocessMutex deadlock.Mutex
ViewStackMutex deadlock.Mutex
}
type mainPanelState struct {
@ -95,38 +67,61 @@ type mainPanelState struct {
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 {
Services *servicePanelState
Containers *containerPanelState
Menu *menuPanelState
Main *mainPanelState
Images *imagePanelState
Volumes *volumePanelState
Status *statusState
Main *mainPanelState
}
type guiState struct {
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
// the names of views in the current focus stack (last item is the current view)
ViewStack []string
Platform commands.Platform
Panels *panelStates
SubProcessOutput string
Stats map[string]commands.ContainerStats
// SessionIndex tells us how many times we've come back from a subprocess.
// We increment it each time we switch to a new subprocess
// Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose
SessionIndex int
// if true, we show containers with an 'exited' status in the containers panel
ShowExitedContainers bool
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
// as in panel, not your terminal's window). Sometimes you want a bit more space
// to see the contents of a panel, and this keeps track of how much maximisation
// you've set
type WindowMaximisation int
const (
SCREEN_NORMAL WindowMaximisation = iota
SCREEN_HALF
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
@ -134,117 +129,66 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
initialState := guiState{
Platform: *oSCommand.Platform,
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{
ObjectKey: "",
},
Status: &statusState{ContextIndex: 0},
},
SessionIndex: 0,
}
ViewStack: []string{},
cyclableViews := []string{"status", "containers", "images", "volumes"}
if dockerCommand.InDockerComposeProject {
cyclableViews = []string{"status", "services", "containers", "images", "volumes"}
ShowExitedContainers: true,
ScreenMode: getScreenMode(config),
}
gui := &Gui{
Log: log,
DockerCommand: dockerCommand,
OSCommand: oSCommand,
// TODO: look into this warning
State: initialState,
Config: config,
Tr: tr,
statusManager: &statusManager{},
T: tasks.NewTaskManager(log),
taskManager: tasks.NewTaskManager(log, tr),
ErrorChan: errorChan,
CyclableViews: cyclableViews,
}
gui.GenerateSentinelErrors()
deadlock.Opts.Disable = !gui.Config.Debug
deadlock.Opts.DeadlockTimeout = 10 * time.Second
return gui, nil
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (gui *Gui) loadNewDirectory() error {
gui.waitForIntro.Done()
if err := gui.refreshSidePanels(gui.g); err != nil {
return err
}
if gui.Config.UserConfig.Reporting == "undetermined" {
if err := gui.promptAnonymousReporting(); err != nil {
return err
}
}
return nil
}
func (gui *Gui) promptAnonymousReporting() error {
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.AnonymousReportingTitle, gui.Tr.AnonymousReportingPrompt, func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
// setting the value here explicitly so that we don't re-request after coming back from a subprocess. The proper solution would be to reload the config but that's tricky because it's loaded at the very top level
gui.Config.UserConfig.Reporting = "on"
return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error {
userConfig.Reporting = "on"
return nil
})
}, func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
gui.Config.UserConfig.Reporting = "off"
return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error {
userConfig.Reporting = "off"
return nil
})
})
}
func (gui *Gui) renderAppStatus() error {
appStatus := gui.statusManager.getStatusString()
if appStatus != "" {
return gui.renderString(gui.g, "appStatus", appStatus)
}
return nil
}
func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{
"PgUp/PgDn": gui.Tr.Scroll,
"← → ↑ ↓": gui.Tr.Navigate,
"esc/q": gui.Tr.Close,
"q": gui.Tr.Quit,
"b": gui.Tr.ViewBulkCommands,
"x": gui.Tr.Menu,
})
}
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
currentSessionIndex := gui.State.SessionIndex
_ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
go func() {
for range time.Tick(interval) {
if gui.State.SessionIndex > currentSessionIndex {
return
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if !gui.PauseBackgroundThreads {
_ = function()
}
_ = function()
}
}()
}
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
// 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()
g, err := gocui.NewGui(gocui.NewGuiOpts{
OutputMode: gocui.OutputTrue,
RuneReplacements: map[rune]string{},
})
if err != nil {
return err
}
@ -257,56 +201,189 @@ func (gui *Gui) Run() error {
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 {
return err
}
if gui.Config.UserConfig.Reporting == "undetermined" {
gui.waitForIntro.Add(2)
} else {
gui.waitForIntro.Add(1)
}
go func() {
gui.waitForIntro.Wait()
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
gui.goEvery(time.Millisecond*100, gui.refreshContainersAndServices)
gui.goEvery(time.Millisecond*100, gui.refreshVolumes)
gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails)
gui.goEvery(time.Millisecond*1000, gui.checkForContextChange)
}()
go gui.DockerCommand.MonitorContainerStats()
throttledRefresh := throttle.ThrottleFunc(time.Millisecond*50, true, gui.refresh)
defer throttledRefresh.Stop()
go func() {
for err := range gui.ErrorChan {
if err == nil {
continue
}
if strings.Contains(err.Error(), "No such container") {
// this happens all the time when e.g. restarting containers so we won't worry about it
gui.Log.Warn(err)
continue
}
gui.createErrorPanel(gui.g, err.Error())
_ = gui.createErrorPanel(err.Error())
}
}()
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 {
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()
if err == gocui.ErrQuit {
return nil
}
return err
}
func (gui *Gui) setPanels() {
gui.Panels = Panels{
Projects: gui.getProjectPanel(),
Services: gui.getServicesPanel(),
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() {
go func() {
// Refresh containers/services first, then projects (which depend on
// container labels to discover projects).
if err := gui.refreshContainersAndServices(); err != nil {
gui.Log.Error(err)
}
if err := gui.refreshProject(); err != nil {
gui.Log.Error(err)
}
}()
go func() {
if err := gui.reloadVolumes(); 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)
}
}()
}
func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
errorCount := 0
onError := func(err error) {
if err != nil {
gui.ErrorChan <- errors.Errorf("Docker event stream returned error: %s\nRetry count: %d", err.Error(), errorCount)
}
errorCount++
time.Sleep(time.Second * 2)
}
outer:
for {
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), events.ListOptions{})
if errorCount > 0 {
select {
case <-ctx.Done():
return
case err := <-errChan:
onError(err)
continue outer
default:
// If we're here then we lost connection to docker and we just got it back.
// The reason we do this refresh explicitly is because successfully
// reconnecting with docker does not mean it's going to send us a new
// event any time soon.
// Assuming the confirmation prompt currently holds the given error
_ = gui.closeConfirmationPrompt()
refresh()
errorCount = 0
}
}
for {
select {
case <-ctx.Done():
return
case message := <-messageChan:
// We could be more granular about what events should trigger which refreshes.
// At the moment it's pretty efficient though, and it might not be worth
// the maintenance burden of mapping specific events to specific refreshes
refresh()
gui.Log.Infof("received event of type: %s", message.Type)
case err := <-errChan:
onError(err)
continue outer
}
}
}
}
// checkForContextChange runs the currently focused panel's 'select' function, simulating the current item having just been selected. This will then trigger a check to see if anything's changed (e.g. a service has a new container) and if so, the appropriate code will run. For example, if you're reading logs from a service and all of a sudden its container changes, this will trigger the 'select' function, which will work out that the context is not different because of the new container, and then it will re-attempt to get the logs, this time for the correct container. This 'context' is stored in the main panel's ObjectKey. I'm using the term 'context' here more broadly than just the different tabs you can view in a panel.
func (gui *Gui) checkForContextChange() error {
return gui.newLineFocused(gui.g.CurrentView())
}
func (gui *Gui) reRenderMain() error {
mainView := gui.getMainView()
mainView := gui.Views.Main
if mainView == nil {
return nil
}
@ -320,13 +397,23 @@ func (gui *Gui) reRenderMain() error {
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.Config.UserConfig.ConfirmOnQuit {
return gui.createConfirmationPanel(g, v, "", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel("", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
}
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 {
if !gui.g.Mouse {
return nil
@ -336,47 +423,33 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
if cx > len(gui.Tr.Donate) {
return nil
}
return gui.OSCommand.OpenLink("https://donorbox.org/lazydocker")
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
}
func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
return err
cmd, err := gui.OSCommand.EditFile(filename)
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocess(cmd)
}
func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.createErrorPanel(err.Error())
}
return nil
}
// runSyncOrAsyncCommand takes the output of a command that may have returned
// either no error, an error, or a subprocess to execute, and if a subprocess
// needs to be set on the gui object, it does so, and then returns the error
// the bool returned tells us whether the calling code should continue
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return false, gui.createErrorPanel(gui.g, err.Error())
}
}
if sub != nil {
gui.SubProcess = sub
return false, gui.Errors.ErrSubProcess
}
return true, nil
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error {
command := gui.trimmedContent(v)
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command))
})
}
func (gui *Gui) shouldRefresh(key string) bool {
func (gui *Gui) ShouldRefresh(key string) bool {
if gui.State.Panels.Main.ObjectKey == key {
return false
}
@ -386,8 +459,56 @@ func (gui *Gui) shouldRefresh(key string) bool {
}
func (gui *Gui) initiallyFocusedViewName() string {
if gui.DockerCommand.InDockerComposeProject {
if gui.DockerCommand.IsProjectScoped() {
return "services"
}
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,257 +5,217 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/fatih/color"
"github.com/go-errors/errors"
"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"
)
// list panel functions
func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
noneLabel := "<none>"
func (gui *Gui) getImageContexts() []string {
return []string{"config"}
return &panels.SideListPanel[*commands.Image]{
ContextState: &panels.ContextState[*commands.Image]{
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) getImageContextTitles() []string {
return []string{gui.Tr.ConfigTitle}
}
func (gui *Gui) getSelectedImage(g *gocui.Gui) (*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(g)
if err != nil {
if err != gui.Errors.ErrNoImages {
return err
}
return gui.renderString(g, "main", gui.Tr.NoImages)
}
if err := gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v); err != nil {
return err
}
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
output := ""
output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
history, err := image.RenderHistory()
if err != nil {
gui.Log.Error(err)
}
output += "\n\n" + history
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) renderImageConfigTask(image *commands.Image) tasks.TaskFunc {
return gui.NewRenderStringTask(RenderStringTaskOpts{
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) refreshImages() error {
ImagesView := gui.getImagesView()
if ImagesView == nil {
// if the ImagesView hasn't been instantiated yet we just return
return nil
func (gui *Gui) imageConfigStr(image *commands.Image) string {
padding := 10
output := ""
output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
history, err := image.RenderHistory()
if err != nil {
gui.Log.Error(err)
}
output += "\n\n" + history
return output
}
func (gui *Gui) reloadImages() error {
if err := gui.refreshStateImages(); err != nil {
return err
}
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().Name() == "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
return gui.Panels.Images.RerenderList()
}
// TODO: leave this to DockerCommand
func (gui *Gui) refreshStateImages() error {
Images, err := gui.DockerCommand.RefreshImages()
images, err := gui.DockerCommand.RefreshImages()
if err != nil {
return err
}
gui.DockerCommand.Images = Images
gui.Panels.Images.SetItems(images)
return nil
}
func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
func (gui *Gui) FilterString(view *gocui.View) string {
if gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() != view {
return ""
}
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)}
return gui.State.Filter.needle
}
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
Image, err := gui.getSelectedImage(g)
type removeImageOption struct {
description string
command string
configOptions image.RemoveOptions
}
img, err := gui.Panels.Images.GetSelectedItem()
if err != nil {
return nil
}
shortSha := Image.ID[7:17]
shortSha := img.ID[7:17]
// TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item
options := []*removeImageOption{
{
description: gui.Tr.Remove,
command: "docker image rm " + shortSha,
configOptions: types.ImageRemoveOptions{PruneChildren: true},
runCommand: true,
configOptions: image.RemoveOptions{PruneChildren: true, Force: false},
},
{
description: gui.Tr.RemoveWithoutPrune,
command: "docker image rm --no-prune " + shortSha,
configOptions: types.ImageRemoveOptions{PruneChildren: false},
runCommand: true,
configOptions: image.RemoveOptions{PruneChildren: false, Force: false},
},
{
description: gui.Tr.Cancel,
runCommand: false,
description: gui.Tr.RemoveWithForce,
command: "docker image rm --force " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: true, Force: true},
},
{
description: gui.Tr.RemoveWithoutPruneWithForce,
command: "docker image rm --no-prune --force " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: false, Force: true},
},
}
handleMenuPress := func(index int) error {
if !options[index].runCommand {
return nil
}
configOptions := options[index].configOptions
if cerr := Image.Remove(configOptions); cerr != nil {
return gui.createErrorPanel(gui.g, cerr.Error())
}
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: []string{
option.description,
color.New(color.FgRed).Sprint(option.command),
},
OnPress: func() error {
if err := img.Remove(option.configOptions); err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.refreshImages()
}
return nil
},
}
})
return gui.createMenu("", options, len(options), handleMenuPress)
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
}
func (gui *Gui) handlePruneImages(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handlePruneImages() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneImages()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.createErrorPanel(err.Error())
}
return gui.refreshImages()
return gui.reloadImages()
})
}, nil)
}
func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
img, err := gui.Panels.Images.GetSelectedItem()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Image: img,
})
customCommands := gui.Config.UserConfig.CustomCommands.Images
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.PruneImages,
InternalFunction: gui.handlePruneImages,
},
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -1,6 +1,8 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
@ -15,11 +17,6 @@ type Binding struct {
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.
func (b *Binding) GetKey() string {
key := 0
@ -53,12 +50,18 @@ func (b *Binding) GetKey() string {
return "PgDn"
}
return string(key)
return fmt.Sprintf("%c", key)
}
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
bindings := []*Binding{
{
ViewName: "",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: wrappedHandler(gui.escape),
},
{
ViewName: "",
Key: 'q',
@ -71,35 +74,29 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.quit,
},
{
ViewName: "",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.quit,
},
{
ViewName: "",
Key: gocui.KeyPgup,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
Handler: wrappedHandler(gui.scrollUpMain),
},
{
ViewName: "",
Key: gocui.KeyPgdn,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Handler: wrappedHandler(gui.scrollDownMain),
},
{
ViewName: "",
Key: gocui.KeyCtrlU,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
Handler: wrappedHandler(gui.scrollUpMain),
},
{
ViewName: "",
Key: gocui.KeyCtrlD,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Handler: wrappedHandler(gui.scrollDownMain),
},
{
ViewName: "",
@ -107,6 +104,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.autoScrollMain,
},
{
ViewName: "",
Key: gocui.KeyHome,
Modifier: gocui.ModNone,
Handler: gui.jumpToTopMain,
},
{
ViewName: "",
Key: 'x',
@ -126,63 +129,55 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleCustomCommand,
},
{
ViewName: "status",
ViewName: "project",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleEditConfig,
Description: gui.Tr.EditConfig,
},
{
ViewName: "status",
ViewName: "project",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleOpenConfig,
Description: gui.Tr.OpenConfig,
},
{
ViewName: "status",
Key: '[',
Modifier: gocui.ModNone,
Handler: gui.handleStatusPrevContext,
Description: gui.Tr.PreviousContext,
},
{
ViewName: "status",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleStatusNextContext,
Description: gui.Tr.NextContext,
},
{
ViewName: "status",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStatusClick,
},
{
ViewName: "status",
ViewName: "project",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleViewAllLogs,
Description: gui.Tr.ViewLogs,
},
{
ViewName: "status",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStatusSelect,
},
{
ViewName: "menu",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
Handler: wrappedHandler(gui.handleMenuClose),
},
{
ViewName: "menu",
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
Handler: wrappedHandler(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",
@ -190,20 +185,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
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",
Key: 'd',
@ -211,6 +192,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainersRemoveMenu,
Description: gui.Tr.Remove,
},
{
ViewName: "containers",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleHideStoppedContainers,
Description: gui.Tr.HideStopped,
},
{
ViewName: "containers",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.handleContainerPause,
Description: gui.Tr.Pause,
},
{
ViewName: "containers",
Key: 's',
@ -232,13 +227,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerAttach,
Description: gui.Tr.Attach,
},
{
ViewName: "containers",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handlePruneContainers,
Description: gui.Tr.PruneContainers,
},
{
ViewName: "containers",
Key: 'm',
@ -246,6 +234,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerViewLogs,
Description: gui.Tr.ViewLogs,
},
{
ViewName: "containers",
Key: 'E',
Modifier: gocui.ModNone,
Handler: gui.handleContainersExecShell,
Description: gui.Tr.ExecShell,
},
{
ViewName: "containers",
Key: 'c',
@ -253,6 +248,27 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainersCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "containers",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handleContainersBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "containers",
Key: 'w',
Modifier: gocui.ModNone,
Handler: gui.handleContainersOpenInBrowserCommand,
Description: gui.Tr.OpenInBrowser,
},
{
ViewName: "services",
Key: 'u',
Modifier: gocui.ModNone,
Handler: gui.handleServiceUp,
Description: gui.Tr.UpService,
},
{
ViewName: "services",
Key: 'd',
@ -267,6 +283,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleServiceStop,
Description: gui.Tr.Stop,
},
{
ViewName: "services",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.handleServicePause,
Description: gui.Tr.Pause,
},
{
ViewName: "services",
Key: 'r',
@ -274,6 +297,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleServiceRestart,
Description: gui.Tr.Restart,
},
{
ViewName: "services",
Key: 'S',
Modifier: gocui.ModNone,
Handler: gui.handleServiceStart,
Description: gui.Tr.Start,
},
{
ViewName: "services",
Key: 'a',
@ -285,22 +315,22 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
ViewName: "services",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleServiceViewLogs,
Handler: gui.handleServiceRenderLogsToMain,
Description: gui.Tr.ViewLogs,
},
{
ViewName: "services",
Key: '[',
Key: 'U',
Modifier: gocui.ModNone,
Handler: gui.handleServicesPrevContext,
Description: gui.Tr.PreviousContext,
Handler: gui.handleProjectUp,
Description: gui.Tr.UpProject,
},
{
ViewName: "services",
Key: ']',
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleServicesNextContext,
Description: gui.Tr.NextContext,
Handler: gui.handleProjectDown,
Description: gui.Tr.DownProject,
},
{
ViewName: "services",
@ -317,18 +347,32 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "images",
Key: '[',
ViewName: "services",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handleImagesPrevContext,
Description: gui.Tr.PreviousContext,
Handler: gui.handleServicesBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "services",
Key: 'E',
Modifier: gocui.ModNone,
Handler: gui.handleServicesExecShell,
Description: gui.Tr.ExecShell,
},
{
ViewName: "services",
Key: 'w',
Modifier: gocui.ModNone,
Handler: gui.handleServicesOpenInBrowserCommand,
Description: gui.Tr.OpenInBrowser,
},
{
ViewName: "images",
Key: ']',
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleImagesNextContext,
Description: gui.Tr.NextContext,
Handler: gui.handleImagesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "images",
@ -339,24 +383,17 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
},
{
ViewName: "images",
Key: 'D',
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePruneImages,
Description: gui.Tr.PruneImages,
Handler: gui.handleImagesBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "volumes",
Key: '[',
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleVolumesPrevContext,
Description: gui.Tr.PreviousContext,
},
{
ViewName: "volumes",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleVolumesNextContext,
Description: gui.Tr.NextContext,
Handler: gui.handleVolumesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "volumes",
@ -367,10 +404,31 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
},
{
ViewName: "volumes",
Key: 'D',
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePruneVolumes,
Description: gui.Tr.PruneVolumes,
Handler: gui.handleVolumesBulkCommand,
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",
@ -403,17 +461,29 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
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: "",
Key: 'J',
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Handler: wrappedHandler(gui.scrollDownMain),
},
{
ViewName: "",
Key: 'K',
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
Handler: wrappedHandler(gui.scrollUpMain),
},
{
ViewName: "",
@ -427,51 +497,94 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.scrollRightMain,
},
{
ViewName: "",
Key: '+',
Handler: wrappedHandler(gui.nextScreenMode),
Description: gui.Tr.LcNextScreenMode,
},
{
ViewName: "",
Key: '_',
Handler: wrappedHandler(gui.prevScreenMode),
Description: gui.Tr.LcPrevScreenMode,
},
}
// TODO: add more views here
for _, viewName := range []string{"status", "services", "containers", "images", "volumes", "menu"} {
for _, panel := range gui.allSidePanels() {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: panel.GetView().Name(), Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: panel.GetView().Name(), Key: gocui.KeyBacktab, Modifier: gocui.ModNone, Handler: gui.previousView},
}...)
}
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 {
setUpDownClickBindings := func(viewName string, onUp func() error, onDown func() error, onClick func() error) {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.onClick},
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)},
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)},
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)},
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: wrappedHandler(onClick)},
}...)
}
for _, viewName := range []string{"status", "services", "containers", "images", "volumes"} {
bindings = append(bindings, &Binding{
ViewName: viewName,
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleEnterMain,
Description: gui.Tr.FocusMain,
})
bindings = append(bindings, []*Binding{
{Handler: gui.handleGoTo(gui.Panels.Projects.View), Key: '1', Description: gui.Tr.FocusProjects},
{Handler: gui.handleGoTo(gui.Panels.Services.View), Key: '2', Description: gui.Tr.FocusServices},
{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,
Modifier: gocui.ModNone,
Handler: gui.handleEnterMain,
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
@ -492,3 +605,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
return nil
}
func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
}
}

View file

@ -1,11 +1,11 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
@ -16,12 +16,8 @@ func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
}
// for now we don't consider losing focus to a popup panel as actually losing focus
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
if err := gui.onFocusLost(previousView, newView); err != nil {
return err
}
if err := gui.onFocus(newView); err != nil {
return err
}
gui.onFocusLost(previousView, newView)
gui.onFocus(newView)
previousView = newView
}
return nil
@ -36,9 +32,9 @@ func (gui *Gui) onFocusChange() error {
return nil
}
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) {
if v == nil {
return nil
return
}
if !gui.isPopupPanel(newView.Name()) {
@ -46,25 +42,19 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
}
// refocusing because in responsive mode (when the window is very short) we want to ensure that after the view size changes we can still see the last selected item
if err := gui.focusPointInView(v); err != nil {
return err
}
gui.focusPointInView(v)
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onFocus(v *gocui.View) error {
func (gui *Gui) onFocus(v *gocui.View) {
if v == nil {
return nil
return
}
if err := gui.focusPointInView(v); err != nil {
return err
}
gui.focusPointInView(v)
gui.Log.Info(v.Name() + " focus gained")
return nil
}
// layout is called for every screen re-render e.g. when the screen is resized
@ -72,217 +62,48 @@ func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
information := gui.Config.Version
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
information = donate + " " + information
}
minimumHeight := 9
minimumWidth := 10
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.NotEnoughSpace
v.Wrap = true
_, _ = g.SetViewOnTop("limit")
}
return nil
}
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviouseView := true
for _, view := range gui.CyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviouseView = false
break
}
}
if usePreviouseView {
currentCyclebleView = gui.State.PreviousView
}
}
usableSpace := height - 4
tallPanels := 3
var vHeights map[string]int
if gui.DockerCommand.InDockerComposeProject {
tallPanels++
vHeights = map[string]int{
"status": 3,
"services": usableSpace/tallPanels + usableSpace%tallPanels,
"containers": usableSpace / tallPanels,
"images": usableSpace / tallPanels,
"volumes": usableSpace / tallPanels,
"options": 1,
}
} else {
vHeights = map[string]int{
"status": 3,
"containers": usableSpace/tallPanels + usableSpace%tallPanels,
"images": usableSpace / tallPanels,
"volumes": usableSpace / tallPanels,
"options": 1,
}
}
if height < 28 {
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights = map[string]int{
"status": defaultHeight,
"containers": defaultHeight,
"images": defaultHeight,
"volumes": defaultHeight,
"options": defaultHeight,
}
if gui.DockerCommand.InDockerComposeProject {
vHeights["services"] = defaultHeight
}
vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1
}
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
leftSideWidth := width / 3
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
_, _ = g.SetViewOnBottom("limit")
g.DeleteView("limit")
viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus)
// we assume that the view has already been created.
setViewFromDimensions := func(viewName string, windowName string) (*gocui.View, error) {
dimensionsObj, ok := viewDimensions[windowName]
v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
v.FgColor = gocui.ColorDefault
// 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.
v.IgnoreCarriageReturns = true
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.StatusTitle
v.FgColor = gocui.ColorDefault
}
var servicesView *gocui.View
aboveContainersView := "status"
if gui.DockerCommand.InDockerComposeProject {
aboveContainersView = "services"
servicesView, err = g.SetViewBeneath("services", "status", vHeights["services"])
view, err := g.View(viewName)
if err != nil {
if err.Error() != "unknown view" {
return err
}
servicesView.Highlight = true
servicesView.Title = gui.Tr.ServicesTitle
servicesView.FgColor = gocui.ColorDefault
return nil, err
}
if !ok {
// view not specified in dimensions object: so create the view and hide it
// making the view take up the whole space in the background in case it needs
// to render content as soon as it appears, because lazyloaded content (via a pty task)
// cares about the size of the view.
_, err := g.SetView(viewName, 0, 0, width, height, 0)
view.Visible = false
return view, err
}
frameOffset := 1
if view.Frame {
frameOffset = 0
}
_, err = g.SetView(
viewName,
dimensionsObj.X0-frameOffset,
dimensionsObj.Y0-frameOffset,
dimensionsObj.X1+frameOffset,
dimensionsObj.Y1+frameOffset,
0,
)
view.Visible = true
return view, err
}
containersView, err := g.SetViewBeneath("containers", aboveContainersView, vHeights["containers"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
containersView.Highlight = true
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
containersView.Title = gui.Tr.ContainersTitle
} else {
containersView.Title = gui.Tr.StandaloneContainersTitle
}
containersView.FgColor = gocui.ColorDefault
}
imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
imagesView.Highlight = true
imagesView.Title = gui.Tr.ImagesTitle
imagesView.FgColor = gocui.ColorDefault
}
volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
volumesView.Highlight = true
volumesView.Title = gui.Tr.VolumesTitle
volumesView.FgColor = gocui.ColorDefault
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Frame = false
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
return err
}
}
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
return err
}
}
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
if err := gui.renderString(g, "information", information); err != nil {
return err
}
// doing this here because it'll only happen once
if err := gui.loadNewDirectory(); err != nil {
return err
}
}
if gui.g.CurrentView() == nil {
v, err := gui.g.View(gui.State.PreviousView)
if err != nil {
viewName := gui.initiallyFocusedViewName()
v, err = gui.g.View(viewName)
if err != nil {
return err
}
}
if err := gui.switchFocus(gui.g, nil, v); err != nil {
for _, viewName := range gui.autoPositionedViewNames() {
_, err := setViewFromDimensions(viewName, viewName)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
}
@ -294,29 +115,21 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return gui.resizeCurrentPopupPanel(g)
}
type listViewState struct {
selectedLine int
lineCount int
}
func (gui *Gui) focusPointInView(view *gocui.View) error {
func (gui *Gui) focusPointInView(view *gocui.View) {
if view == nil {
return nil
return
}
listViews := map[string]listViewState{
"containers": {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.DisplayContainers)},
"images": {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
"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 {
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
return err
for _, panel := range gui.allListPanels() {
if panel.GetView() == view {
panel.Refocus()
return
}
}
return nil
}
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
// arbitrarily giving the view enough size so that we don't get an error, but
// it's expected that the view will be given the correct size before being shown
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
}

View file

@ -6,29 +6,35 @@ import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView()
func (gui *Gui) scrollUpMain() error {
mainView := gui.Views.Main
mainView.Autoscroll = false
ox, oy := mainView.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight)))
return mainView.SetOrigin(ox, newOy)
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView()
func (gui *Gui) scrollDownMain() error {
mainView := gui.Views.Main
mainView.Autoscroll = false
ox, oy := mainView.Origin()
y := oy
reservedLines := 0
if !gui.Config.UserConfig.Gui.ScrollPastBottom {
_, sy := mainView.Size()
y += sy
_, sizeY := mainView.Size()
reservedLines = sizeY
}
// for some reason we can't work out whether we've hit the bottomq
// there is a large discrepancy in the origin's y value and the length of BufferLines
totalLines := mainView.ViewLinesHeight()
if oy+reservedLines >= totalLines {
return nil
}
return mainView.SetOrigin(ox, oy+gui.Config.UserConfig.Gui.ScrollHeight)
}
func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView()
mainView := gui.Views.Main
ox, oy := mainView.Origin()
newOx := int(math.Max(0, float64(ox-gui.Config.UserConfig.Gui.ScrollHeight)))
@ -36,72 +42,72 @@ func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView()
mainView := gui.Views.Main
ox, oy := mainView.Origin()
content := mainView.ViewBufferLines()
var largestNumberOfCharacters int
for _, txt := range content {
if len(txt) > largestNumberOfCharacters {
largestNumberOfCharacters = len(txt)
}
}
sizeX, _ := mainView.Size()
if ox+sizeX >= largestNumberOfCharacters {
return nil
}
return mainView.SetOrigin(ox+gui.Config.UserConfig.Gui.ScrollHeight, oy)
}
func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
gui.getMainView().Autoscroll = true
gui.Views.Main.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
}
func (gui *Gui) onMainTabClick(tabIndex int) error {
gui.Log.Warn(tabIndex)
viewName := gui.currentViewName()
currentSidePanel, ok := gui.currentSidePanel()
mainView := gui.getMainView()
if viewName == "main" && mainView.ParentView != nil {
viewName = mainView.ParentView.Name()
if !ok {
return nil
}
switch viewName {
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())
case "status":
gui.State.Panels.Status.ContextIndex = tabIndex
return gui.handleStatusSelect(gui.g, gui.getStatusView())
}
return nil
currentSidePanel.SetMainTabIndex(tabIndex)
return currentSidePanel.HandleSelect()
}
func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView()
mainView := gui.Views.Main
mainView.ParentView = v
return gui.switchFocus(gui.g, v, mainView)
return gui.switchFocus(mainView)
}
func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error {
v.ParentView = nil
return gui.returnFocus(gui.g, v)
return gui.returnFocus()
}
func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleMainClick() error {
if gui.popupPanelFocused() {
return nil
}
currentView := gui.g.CurrentView()
if currentView != nil && currentView.Name() == "main" {
currentView = nil
} else {
v.ParentView = currentView
if currentView.Name() != "main" {
gui.Views.Main.ParentView = currentView
}
return gui.switchFocus(gui.g, currentView, v)
return gui.switchFocus(gui.Views.Main)
}

View file

@ -1,116 +1,133 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"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/utils"
)
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
type CreateMenuOptions struct {
Title string
Items []*types.MenuItem
HideCancel bool
}
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
return gui.handleMenuSelect(g, v)
func (gui *Gui) getMenuPanel() *panels.SideListPanel[*types.MenuItem] {
return &panels.SideListPanel[*types.MenuItem]{
ListPanel: panels.ListPanel[*types.MenuItem]{
List: panels.NewFilteredList[*types.MenuItem](),
View: gui.Views.Menu,
},
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) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
itemCount := gui.State.MenuItemCount
handleSelect := gui.handleMenuSelect
selectedLine := &gui.State.Panels.Menu.SelectedLine
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
func (gui *Gui) onMenuPress(menuItem *types.MenuItem) error {
if err := gui.handleMenuClose(); err != nil {
return err
}
return gui.State.Panels.Menu.OnPress(g, v)
if menuItem.OnPress != nil {
return menuItem.OnPress()
}
return nil
}
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
func (gui *Gui) handleMenuPress() error {
selectedMenuItem, err := gui.Panels.Menu.GetSelectedItem()
if err != nil {
return nil
}
return gui.handleMenuSelect(g, v)
return gui.onMenuPress(selectedMenuItem)
}
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
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
"esc/q": gui.Tr.Close,
"esc": gui.Tr.Close,
"↑ ↓": gui.Tr.Navigate,
"enter": gui.Tr.Execute,
}
return gui.renderOptionsMap(optionsMap)
}
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 {
func (gui *Gui) handleMenuClose() error {
gui.Views.Menu.Visible = false
// this code is here for when we do add filter ability to the menu panel,
// though it's currently disabled
if gui.State.Filter.panel == gui.Panels.Menu {
if err := gui.clearFilter(); err != nil {
return err
}
// we need to remove the view from the view stack because we're about to
// return focus and don't want to land in the search view when it was searching
// the menu in the first place
gui.removeViewFromStack(gui.Views.Filter)
}
err := g.DeleteView("menu")
if err != nil {
return err
}
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu"
gui.State.MenuItemCount = itemCount
list, err := utils.RenderList(items, utils.IsFocused(isFocused))
if err != nil {
return err
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = gocui.ColorWhite
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
if err := handlePress(selectedLine); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
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", key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
})
return nil
return gui.returnFocus()
}

179
pkg/gui/networks_panel.go Normal file
View file

@ -0,0 +1,179 @@
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,17 +1,14 @@
package gui
import (
"strings"
"github.com/go-errors/errors"
"github.com/samber/lo"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
var bindingsGlobal, bindingsPanel []*Binding
bindings := gui.GetInitialKeybindings()
@ -44,10 +41,6 @@ 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
// panel and global keybindings
bindingsPanel = append(bindingsPanel, &Binding{})
@ -55,25 +48,26 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if v.Name() == "menu" {
if gui.isPopupPanel(v.Name()) {
return nil
}
bindings := gui.getBindings(v)
menuItems := lo.Map(gui.getBindings(v), func(binding *Binding, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: []string{binding.GetKey(), binding.Description},
OnPress: func() error {
if binding.Key == nil {
return nil
}
handleMenuPress := func(index int) error {
if bindings[index].Key == nil {
return nil
return binding.Handler(g, v)
},
}
if index >= len(bindings) {
return errors.New("Index is greater than size of bindings")
}
err := gui.handleMenuClose(g, v)
if err != nil {
return err
}
return bindings[index].Handler(g, v)
}
})
return gui.createMenu(strings.Title(gui.Tr.Menu), bindings, len(bindings), handleMenuPress)
return gui.Menu(CreateMenuOptions{
Title: gui.Tr.MenuTitle,
Items: menuItems,
HideCancel: true,
})
}

7
pkg/gui/panels.go Normal file
View file

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

View file

@ -0,0 +1,69 @@
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

@ -0,0 +1,111 @@
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

@ -0,0 +1,183 @@
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,280 @@
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

@ -0,0 +1,146 @@
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

@ -0,0 +1,202 @@
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

@ -0,0 +1,14 @@
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

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

View file

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

View file

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

View file

@ -0,0 +1,43 @@
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

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

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