Port lazydocker to lazyapple for Apple Container runtime

Rewrite of lazydocker to work with Apple's native container CLI instead of Docker.

- Replace Docker SDK with Apple Container CLI wrapper
- Remove docker-compose/services/projects support
- 4 panels: Containers, Images, Volumes, Networks
- Remove all Docker Go dependencies
- Rename binary to lazyapple
- Update README with install and usage instructions
This commit is contained in:
g-battaglia 2026-03-18 15:22:06 +01:00
parent 2ebe3848d6
commit 065a57e445
727 changed files with 6269 additions and 89734 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
lazydocker*
lazyapple
lazycontainer
TODO.md
Lazydocker.code-workspace
.vscode

387
README.md

File diff suppressed because one or more lines are too long

48
go.mod
View file

@ -1,19 +1,14 @@
module github.com/jesseduffield/lazydocker
module github.com/jesseduffield/lazycontainer
go 1.22
toolchain go1.23.6
go 1.24.0
require (
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/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.20240418080333-8cd33929c513
@ -33,53 +28,26 @@ require (
)
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
dario.cat/mergo v1.0.2
github.com/google/go-cmp v0.7.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.39.1 // 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 // 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/sys v0.40.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
golang.org/x/text v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
)

165
go.sum
View file

@ -1,43 +1,19 @@
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=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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/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/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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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=
@ -46,32 +22,30 @@ github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7
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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
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/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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.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=
@ -100,31 +74,21 @@ github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 h1:BrhJNdEFWGui
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/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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
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 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/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -144,6 +108,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
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/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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=
@ -151,47 +116,48 @@ 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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/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=
@ -199,8 +165,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
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/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
@ -212,36 +178,33 @@ 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/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
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.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/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=

BIN
lazycontainer Executable file

Binary file not shown.

27
main.go
View file

@ -8,12 +8,11 @@ import (
"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/lazycontainer/pkg/app"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/jesseduffield/yaml"
"github.com/samber/lo"
)
@ -28,8 +27,6 @@ var (
configFlag = false
debuggingFlag = false
composeFiles []string
projectName string
)
func main() {
@ -45,14 +42,12 @@ func main() {
runtime.GOARCH,
)
flaggy.SetName("lazydocker")
flaggy.SetDescription("The lazier way to manage everything docker")
flaggy.DefaultParser.AdditionalHelpPrepend = "https://github.com/jesseduffield/lazydocker"
flaggy.SetName("lazyapple")
flaggy.SetDescription("The lazier way to manage Apple Containers")
flaggy.DefaultParser.AdditionalHelpPrepend = "https://github.com/g-battaglia/lazy-apple-container"
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.String(&projectName, "p", "project", "Specify a docker compose project name")
flaggy.SetVersion(info)
flaggy.Parse()
@ -73,7 +68,7 @@ func main() {
log.Fatal(err.Error())
}
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
appConfig, err := config.NewAppConfig("lazyapple", version, commit, date, buildSource, debuggingFlag, nil, projectDir, "")
if err != nil {
log.Fatal(err.Error())
}
@ -90,11 +85,6 @@ func main() {
os.Exit(0)
}
if client.IsErrConnectionFailed(err) {
log.Println(app.Tr.ConnectionFailed)
os.Exit(0)
}
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
@ -111,12 +101,9 @@ func updateBuildInfo() {
})
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"
})

View file

@ -4,29 +4,27 @@ import (
"io"
"strings"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/gui"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/log"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/sirupsen/logrus"
)
// App struct
type App struct {
closers []io.Closer
Config *config.AppConfig
Log *logrus.Entry
OSCommand *commands.OSCommand
DockerCommand *commands.DockerCommand
ContainerCmd *commands.ContainerCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
ErrorChan chan error
}
// NewApp bootstrap a new application
func NewApp(config *config.AppConfig) (*App, error) {
app := &App{
closers: []io.Closer{},
@ -41,14 +39,12 @@ func NewApp(config *config.AppConfig) (*App, error) {
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
// here is the place to make use of the docker-compose.yml file in the current directory
app.DockerCommand, err = commands.NewDockerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
app.ContainerCmd, err = commands.NewContainerCommand(app.Log, app.OSCommand, app.Tr, app.Config, app.ErrorChan)
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)
app.closers = append(app.closers, app.ContainerCmd)
app.Gui, err = gui.NewGui(app.Log, app.ContainerCmd, app.OSCommand, app.Tr, config, app.ErrorChan)
if err != nil {
return app, err
}
@ -68,14 +64,17 @@ type errorMapping struct {
newError string
}
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
mappings := []errorMapping{
{
originalError: "Got permission denied while trying to connect to the Docker daemon socket",
newError: app.Tr.CannotAccessDockerSocketError,
originalError: "container: command not found",
newError: "Apple Container is not installed. Please install it and try again.",
},
{
originalError: "container system is not running",
newError: "Apple Container system is not running. Run 'container system start' first.",
},
}

View file

@ -13,10 +13,10 @@ import (
"log"
"os"
"github.com/jesseduffield/lazydocker/pkg/app"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/gui"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/app"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/gui"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
)
const (
@ -33,7 +33,7 @@ func Generate() {
}
func generateAtDir(dir string) {
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
mConfig, err := config.NewAppConfig("lazyapple", "", "", "", "", true, nil, "", "")
if err != nil {
panic(err)
}

171
pkg/commands/apple_types.go Normal file
View file

@ -0,0 +1,171 @@
package commands
type AppleContainer struct {
Configuration struct {
ID string `json:"id"`
Name string `json:"-"`
Image struct {
Reference string `json:"reference"`
Descriptor struct {
Digest string `json:"digest"`
Size int64 `json:"size"`
MediaType string `json:"mediaType"`
} `json:"descriptor"`
} `json:"image"`
Labels map[string]string `json:"labels"`
Resources struct {
CPUS int `json:"cpus"`
MemoryInBytes int64 `json:"memoryInBytes"`
} `json:"resources"`
Platform struct {
Architecture string `json:"architecture"`
OS string `json:"os"`
} `json:"platform"`
InitProcess struct {
Environment []string `json:"environment"`
Executable string `json:"executable"`
Arguments []string `json:"arguments"`
WorkingDir string `json:"workingDirectory"`
Terminal bool `json:"terminal"`
User struct {
ID struct {
UID int `json:"uid"`
GID int `json:"gid"`
} `json:"id"`
} `json:"user"`
} `json:"initProcess"`
Mounts []Mount `json:"mounts"`
PublishedPorts []struct {
HostIP string `json:"hostIP"`
HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"`
Protocol string `json:"protocol"`
} `json:"publishedPorts"`
Networks []struct {
Network string `json:"network"`
Options struct {
Hostname string `json:"hostname"`
} `json:"options"`
} `json:"networks"`
RuntimeHandler string `json:"runtimeHandler"`
Rosetta bool `json:"rosetta"`
UseInit bool `json:"useInit"`
Virtualization bool `json:"virtualization"`
ReadOnly bool `json:"readOnly"`
SSH bool `json:"ssh"`
} `json:"configuration"`
Networks []struct {
Network string `json:"network"`
IPv4Address string `json:"ipv4Address"`
IPv6Address string `json:"ipv6Address"`
IPv4Gateway string `json:"ipv4Gateway"`
MACAddress string `json:"macAddress"`
Hostname string `json:"hostname"`
} `json:"networks"`
Status string `json:"status"`
StartedDate float64 `json:"startedDate"`
}
type Mount struct {
Type string `json:"type"`
Source string `json:"source"`
Target string `json:"target"`
ReadOnly bool `json:"readonly"`
}
type AppleContainerStats struct {
ID string `json:"id"`
CPUUsageUsec int64 `json:"cpuUsageUsec"`
MemoryUsageBytes int64 `json:"memoryUsageBytes"`
MemoryLimitBytes int64 `json:"memoryLimitBytes"`
NetworkRxBytes int64 `json:"networkRxBytes"`
NetworkTxBytes int64 `json:"networkTxBytes"`
BlockReadBytes int64 `json:"blockReadBytes"`
BlockWriteBytes int64 `json:"blockWriteBytes"`
NumProcesses int `json:"numProcesses"`
}
type AppleImage struct {
Reference string `json:"reference"`
Descriptor struct {
Digest string `json:"digest"`
Size int64 `json:"size"`
MediaType string `json:"mediaType"`
} `json:"descriptor"`
FullSize string `json:"fullSize"`
}
type AppleVolume struct {
Name string `json:"name"`
SizeInBytes int64 `json:"sizeInBytes"`
}
type AppleNetwork struct {
ID string `json:"id"`
Config struct {
Labels map[string]string `json:"labels"`
CreationDate float64 `json:"creationDate"`
Mode string `json:"mode"`
PluginInfo struct {
Variant string `json:"variant"`
Plugin string `json:"plugin"`
} `json:"pluginInfo"`
} `json:"config"`
State string `json:"state"`
Status struct {
IPv4Subnet string `json:"ipv4Subnet"`
IPv6Subnet string `json:"ipv6Subnet"`
IPv4Gateway string `json:"ipv4Gateway"`
} `json:"status"`
}
type SystemDF struct {
Containers struct {
Active int `json:"active"`
Reclaimable int `json:"reclaimable"`
SizeInBytes int64 `json:"sizeInBytes"`
Total int `json:"total"`
} `json:"containers"`
Images struct {
Active int `json:"active"`
Reclaimable int `json:"reclaimable"`
SizeInBytes int64 `json:"sizeInBytes"`
Total int `json:"total"`
} `json:"images"`
Volumes struct {
Active int `json:"active"`
Reclaimable int `json:"reclaimable"`
SizeInBytes int64 `json:"sizeInBytes"`
Total int `json:"total"`
} `json:"volumes"`
}
type SystemStatus struct {
Running bool
}
type CreateContainerOptions struct {
Name string
Image string
Command []string
Env []string
WorkingDir string
CPUS int
Memory string
Volumes []string
Networks []string
Ports []string
Labels map[string]string
Detach bool
Interactive bool
TTY bool
}
type ExecOptions struct {
Command []string
Env []string
WorkingDir string
Interactive bool
TTY bool
User string
}

View file

@ -6,45 +6,32 @@ import (
"os/exec"
"strings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
// Container : A docker Container
type Container struct {
Name string
ServiceName string
ContainerNumber string // might make this an int in the future if need be
// OneOff tells us if the container is just a job container or is actually bound to the service
OneOff bool
ProjectName string
ID string
Container container.Summary
Client *client.Client
AppleContainer AppleContainer
Client *ContainerClient
OSCommand *OSCommand
Log *logrus.Entry
StatHistory []*RecordedStats
Details container.InspectResponse
MonitoringStats bool
DockerCommand LimitedDockerCommand
Tr *i18n.TranslationSet
StatsMutex deadlock.Mutex
}
// Remove removes the container
func (c *Container) Remove(options container.RemoveOptions) error {
func (c *Container) Remove(force bool) 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") {
if err := c.Client.RemoveContainer(c.ID, force); err != nil {
if strings.Contains(err.Error(), "Stop the container before attempting removal") {
return ComplexError{
Code: MustStopContainer,
Message: err.Error(),
@ -53,100 +40,146 @@ func (c *Container) Remove(options container.RemoveOptions) error {
}
return err
}
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{})
return c.Client.StartContainer(c.ID)
}
// Stop stops the container
func (c *Container) Stop() error {
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{})
return c.Client.StopContainer(c.ID)
}
// 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)
return errors.New("pause is not supported by Apple Container")
}
// 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)
return errors.New("unpause is not supported by Apple Container")
}
// Restart restarts the container
func (c *Container) Restart() error {
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{})
return c.Client.RestartContainer(c.ID)
}
func (c *Container) Kill(signal string) error {
c.Log.Warn(fmt.Sprintf("killing container %s with signal %s", c.Name, signal))
return c.Client.KillContainer(c.ID, signal)
}
// 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)
}
if c.Container.State == "exited" {
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
if c.AppleContainer.Status != "running" {
return nil, errors.New(c.Tr.CannotAttachStoppedContainerError)
}
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
return c.Client.ExecContainer(c.ID, ExecOptions{
Command: []string{c.getShell()},
Interactive: true,
TTY: true,
}), nil
}
// Top returns process information
func (c *Container) Top(ctx context.Context) (container.TopResponse, error) {
detail, err := c.Inspect()
func (c *Container) getShell() string {
for _, env := range c.AppleContainer.Configuration.InitProcess.Environment {
if strings.HasPrefix(env, "PATH=") {
path := strings.TrimPrefix(env, "PATH=")
for _, dir := range strings.Split(path, ":") {
for _, shell := range []string{"/bin/sh", "/bin/bash", "/usr/bin/bash"} {
if dir == "/bin" || dir == "/usr/bin" {
return shell
}
}
}
}
}
return "/bin/sh"
}
func (c *Container) Top(ctx context.Context) ([][]string, []string, error) {
cmd := c.Client.ExecContainer(c.ID, ExecOptions{
Command: []string{"ps", "aux"},
})
output, err := cmd.Output()
if err != nil {
return container.TopResponse{}, err
return nil, nil, err
}
// check container status
if !detail.State.Running {
return container.TopResponse{}, errors.New("container is not running")
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) == 0 {
return nil, nil, nil
}
return c.Client.ContainerTop(ctx, c.ID, []string{})
titles := strings.Fields(lines[0])
var processes [][]string
for _, line := range lines[1:] {
if strings.TrimSpace(line) != "" {
processes = append(processes, strings.Fields(line))
}
}
return processes, titles, nil
}
// PruneContainers prunes containers
func (c *DockerCommand) PruneContainers() error {
_, err := c.Client.ContainersPrune(context.Background(), filters.Args{})
return err
func (c *Container) Inspect() (*AppleContainer, error) {
return c.Client.InspectContainer(c.ID)
}
// Inspect returns details about the container
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(ctx context.Context) (string, error) {
result, err := c.Top(ctx)
processes, titles, err := c.Top(ctx)
if err != nil {
return "", err
}
return utils.RenderTable(append([][]string{result.Titles}, result.Processes...))
return utils.RenderTable(append([][]string{titles}, processes...))
}
// 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.ContainerJSONBase != nil
return c.AppleContainer.Configuration.ID != ""
}
func (c *Container) GetStatus() string {
return c.AppleContainer.Status
}
func (c *Container) GetImage() string {
return c.AppleContainer.Configuration.Image.Reference
}
func (c *Container) GetIP() string {
for _, net := range c.AppleContainer.Networks {
if net.IPv4Address != "" {
return strings.Split(net.IPv4Address, "/")[0]
}
}
return ""
}
func (c *Container) GetPorts() []string {
var ports []string
for _, p := range c.AppleContainer.Configuration.PublishedPorts {
ports = append(ports, fmt.Sprintf("%s:%d->%d/%s", p.HostIP, p.HostPort, p.ContainerPort, p.Protocol))
}
return ports
}
func (c *Container) GetEnv() []string {
return c.AppleContainer.Configuration.InitProcess.Environment
}
func (c *Container) GetLabels() map[string]string {
return c.AppleContainer.Configuration.Labels
}
func (c *Container) GetCPUs() int {
return c.AppleContainer.Configuration.Resources.CPUS
}
func (c *Container) GetMemory() int64 {
return c.AppleContainer.Configuration.Resources.MemoryInBytes
}

View file

@ -0,0 +1,632 @@
package commands
import (
"encoding/json"
"fmt"
"io"
"os/exec"
"strings"
"time"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
)
type ContainerClient struct {
Log *logrus.Entry
OSCommand *OSCommand
Tr *i18n.TranslationSet
Config *config.AppConfig
ErrorChan chan error
ContainerMutex deadlock.Mutex
ImageMutex deadlock.Mutex
VolumeMutex deadlock.Mutex
NetworkMutex deadlock.Mutex
}
func NewContainerClient(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*ContainerClient, error) {
return &ContainerClient{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: config,
ErrorChan: errorChan,
}, nil
}
func (c *ContainerClient) runContainerCommand(args ...string) ([]byte, error) {
cmd := exec.Command("container", args...)
output, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("%s: %s", err.Error(), string(exitErr.Stderr))
}
return nil, err
}
return output, nil
}
func (c *ContainerClient) runContainerCommandWithOutput(args ...string) (string, error) {
output, err := c.runContainerCommand(args...)
return string(output), err
}
// Containers
func (c *ContainerClient) ListContainers(all bool) ([]AppleContainer, error) {
args := []string{"list", "--format", "json"}
if all {
args = []string{"list", "--all", "--format", "json"}
}
output, err := c.runContainerCommand(args...)
if err != nil {
return nil, err
}
if len(output) == 0 {
return []AppleContainer{}, nil
}
var containers []AppleContainer
if err := json.Unmarshal(output, &containers); err != nil {
return nil, fmt.Errorf("failed to parse container list: %w", err)
}
return containers, nil
}
func (c *ContainerClient) InspectContainer(id string) (*AppleContainer, error) {
output, err := c.runContainerCommand("inspect", id)
if err != nil {
return nil, err
}
var containers []AppleContainer
if err := json.Unmarshal(output, &containers); err != nil {
return nil, fmt.Errorf("failed to parse container inspect: %w", err)
}
if len(containers) == 0 {
return nil, fmt.Errorf("container %s not found", id)
}
return &containers[0], nil
}
func (c *ContainerClient) CreateContainer(opts CreateContainerOptions) error {
args := []string{"create"}
if opts.Name != "" {
args = append(args, "--name", opts.Name)
}
for _, env := range opts.Env {
args = append(args, "-e", env)
}
if opts.WorkingDir != "" {
args = append(args, "-w", opts.WorkingDir)
}
if opts.CPUS > 0 {
args = append(args, "-c", fmt.Sprintf("%d", opts.CPUS))
}
if opts.Memory != "" {
args = append(args, "-m", opts.Memory)
}
for _, vol := range opts.Volumes {
args = append(args, "-v", vol)
}
for _, net := range opts.Networks {
args = append(args, "--network", net)
}
for _, port := range opts.Ports {
args = append(args, "-p", port)
}
for k, v := range opts.Labels {
args = append(args, "-l", fmt.Sprintf("%s=%s", k, v))
}
if opts.Interactive {
args = append(args, "-i")
}
if opts.TTY {
args = append(args, "-t")
}
args = append(args, opts.Image)
if len(opts.Command) > 0 {
args = append(args, opts.Command...)
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) RunContainer(opts CreateContainerOptions) (string, error) {
args := []string{"run"}
if opts.Name != "" {
args = append(args, "--name", opts.Name)
}
if opts.Detach {
args = append(args, "-d")
}
for _, env := range opts.Env {
args = append(args, "-e", env)
}
if opts.WorkingDir != "" {
args = append(args, "-w", opts.WorkingDir)
}
if opts.CPUS > 0 {
args = append(args, "-c", fmt.Sprintf("%d", opts.CPUS))
}
if opts.Memory != "" {
args = append(args, "-m", opts.Memory)
}
for _, vol := range opts.Volumes {
args = append(args, "-v", vol)
}
for _, net := range opts.Networks {
args = append(args, "--network", net)
}
for _, port := range opts.Ports {
args = append(args, "-p", port)
}
for k, v := range opts.Labels {
args = append(args, "-l", fmt.Sprintf("%s=%s", k, v))
}
if opts.Interactive {
args = append(args, "-i")
}
if opts.TTY {
args = append(args, "-t")
}
args = append(args, opts.Image)
if len(opts.Command) > 0 {
args = append(args, opts.Command...)
}
output, err := c.runContainerCommand(args...)
return string(output), err
}
func (c *ContainerClient) StartContainer(id string) error {
_, err := c.runContainerCommand("start", id)
return err
}
func (c *ContainerClient) StopContainer(id string) error {
_, err := c.runContainerCommand("stop", id)
return err
}
func (c *ContainerClient) RestartContainer(id string) error {
_, err := c.runContainerCommand("stop", id)
if err != nil {
return err
}
return c.StartContainer(id)
}
func (c *ContainerClient) KillContainer(id string, signal string) error {
args := []string{"kill", id}
if signal != "" {
args = []string{"kill", "--signal", signal, id}
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) RemoveContainer(id string, force bool) error {
args := []string{"rm", id}
if force {
args = []string{"rm", "--force", id}
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) PruneContainers() error {
_, err := c.runContainerCommand("prune")
return err
}
func (c *ContainerClient) ExecContainer(id string, opts ExecOptions) *exec.Cmd {
args := []string{"exec"}
for _, env := range opts.Env {
args = append(args, "-e", env)
}
if opts.WorkingDir != "" {
args = append(args, "-w", opts.WorkingDir)
}
if opts.Interactive {
args = append(args, "-i")
}
if opts.TTY {
args = append(args, "-t")
}
if opts.User != "" {
args = append(args, "-u", opts.User)
}
args = append(args, id)
args = append(args, opts.Command...)
return exec.Command("container", args...)
}
func (c *ContainerClient) LogsContainer(id string, follow bool, tail int) *exec.Cmd {
args := []string{"logs"}
if follow {
args = append(args, "--follow")
}
if tail > 0 {
args = append(args, "-n", fmt.Sprintf("%d", tail))
}
args = append(args, id)
return exec.Command("container", args...)
}
func (c *ContainerClient) StatsContainer(id string, noStream bool) ([]AppleContainerStats, error) {
args := []string{"stats", "--format", "json"}
if noStream {
args = append(args, "--no-stream")
}
if id != "" {
args = append(args, id)
}
output, err := c.runContainerCommand(args...)
if err != nil {
return nil, err
}
if len(output) == 0 {
return []AppleContainerStats{}, nil
}
var stats []AppleContainerStats
if err := json.Unmarshal(output, &stats); err != nil {
return nil, fmt.Errorf("failed to parse container stats: %w", err)
}
return stats, nil
}
// Images
func (c *ContainerClient) ListImages() ([]AppleImage, error) {
c.ImageMutex.Lock()
defer c.ImageMutex.Unlock()
output, err := c.runContainerCommand("image", "list", "--format", "json")
if err != nil {
return nil, err
}
if len(output) == 0 {
return []AppleImage{}, nil
}
var images []AppleImage
if err := json.Unmarshal(output, &images); err != nil {
return nil, fmt.Errorf("failed to parse image list: %w", err)
}
return images, nil
}
func (c *ContainerClient) PullImage(ref string) error {
_, err := c.runContainerCommand("image", "pull", ref)
return err
}
func (c *ContainerClient) RemoveImage(id string, force bool) error {
args := []string{"image", "rm", id}
if force {
args = []string{"image", "rm", "--force", id}
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) PruneImages(all bool) error {
args := []string{"image", "prune"}
if all {
args = []string{"image", "prune", "-a"}
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) BuildImage(contextDir string, dockerfile string, tag string) error {
args := []string{"build"}
if dockerfile != "" {
args = append(args, "-f", dockerfile)
}
if tag != "" {
args = append(args, "-t", tag)
}
if contextDir != "" {
args = append(args, contextDir)
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) SaveImage(ids []string, output string) error {
args := []string{"image", "save"}
args = append(args, ids...)
if output != "" {
args = append(args, "--output", output)
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) LoadImage(input string) error {
args := []string{"image", "load"}
if input != "" {
args = append(args, "--input", input)
}
_, err := c.runContainerCommand(args...)
return err
}
// Volumes
func (c *ContainerClient) ListVolumes() ([]AppleVolume, error) {
c.VolumeMutex.Lock()
defer c.VolumeMutex.Unlock()
output, err := c.runContainerCommand("volume", "list", "--format", "json")
if err != nil {
return nil, err
}
if len(output) == 0 {
return []AppleVolume{}, nil
}
var volumes []AppleVolume
if err := json.Unmarshal(output, &volumes); err != nil {
return nil, fmt.Errorf("failed to parse volume list: %w", err)
}
return volumes, nil
}
func (c *ContainerClient) CreateVolume(name string) error {
args := []string{"volume", "create"}
if name != "" {
args = append(args, name)
}
_, err := c.runContainerCommand(args...)
return err
}
func (c *ContainerClient) RemoveVolume(name string) error {
_, err := c.runContainerCommand("volume", "rm", name)
return err
}
func (c *ContainerClient) PruneVolumes() error {
_, err := c.runContainerCommand("volume", "prune")
return err
}
// Networks
func (c *ContainerClient) ListNetworks() ([]AppleNetwork, error) {
c.NetworkMutex.Lock()
defer c.NetworkMutex.Unlock()
output, err := c.runContainerCommand("network", "list", "--format", "json")
if err != nil {
return nil, err
}
if len(output) == 0 {
return []AppleNetwork{}, nil
}
var networks []AppleNetwork
if err := json.Unmarshal(output, &networks); err != nil {
return nil, fmt.Errorf("failed to parse network list: %w", err)
}
return networks, nil
}
func (c *ContainerClient) CreateNetwork(name string) error {
_, err := c.runContainerCommand("network", "create", name)
return err
}
func (c *ContainerClient) RemoveNetwork(name string) error {
_, err := c.runContainerCommand("network", "rm", name)
return err
}
func (c *ContainerClient) PruneNetworks() error {
_, err := c.runContainerCommand("network", "prune")
return err
}
// System
func (c *ContainerClient) SystemDF() (*SystemDF, error) {
output, err := c.runContainerCommand("system", "df", "--format", "json")
if err != nil {
return nil, err
}
var df SystemDF
if err := json.Unmarshal(output, &df); err != nil {
return nil, fmt.Errorf("failed to parse system df: %w", err)
}
return &df, nil
}
func (c *ContainerClient) SystemStatus() (*SystemStatus, error) {
output, err := c.runContainerCommand("system", "status")
if err != nil {
return &SystemStatus{Running: false}, nil
}
status := &SystemStatus{
Running: strings.Contains(string(output), "running"),
}
return status, nil
}
func (c *ContainerClient) SystemStart() error {
_, err := c.runContainerCommand("system", "start")
return err
}
func (c *ContainerClient) SystemStop() error {
_, err := c.runContainerCommand("system", "stop")
return err
}
func (c *ContainerClient) SystemVersion() (string, error) {
return c.runContainerCommandWithOutput("--version")
}
// Monitor stats for a container
func (c *ContainerClient) CreateStatsMonitor(container *Container, callback func(stats AppleContainerStats)) {
container.MonitoringStats = true
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
if !container.MonitoringStats {
return
}
stats, err := c.StatsContainer(container.ID, true)
if err != nil {
c.Log.Error(err)
continue
}
if len(stats) > 0 {
callback(stats[0])
}
}
}
var _ io.Closer = &ContainerClient{}
func (c *ContainerClient) Close() error {
return nil
}
// CommandObject is what we pass to our template resolvers
type CommandObject struct {
Container *Container
Image *Image
Volume *Volume
Network *Network
}
func (c *ContainerClient) NewCommandObject(obj CommandObject) CommandObject {
return obj
}
func (c *ContainerClient) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, error) {
return c.RefreshContainers(currentContainers), nil
}
func (c *ContainerClient) RefreshContainers(currentContainers []*Container) []*Container {
c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock()
appleContainers, err := c.ListContainers(true)
if err != nil {
c.Log.Error(err)
return currentContainers
}
containers := make([]*Container, len(appleContainers))
for i, ac := range appleContainers {
var existingContainer *Container
for _, cc := range currentContainers {
if cc.ID == ac.Configuration.ID {
existingContainer = cc
break
}
}
if existingContainer != nil {
existingContainer.AppleContainer = ac
existingContainer.Name = ac.Configuration.ID
if ac.Configuration.ID != "" {
existingContainer.Name = ac.Configuration.ID
}
containers[i] = existingContainer
} else {
container := &Container{
ID: ac.Configuration.ID,
Name: ac.Configuration.ID,
AppleContainer: ac,
Client: c,
OSCommand: c.OSCommand,
Log: c.Log,
Tr: c.Tr,
}
containers[i] = container
}
}
return containers
}
func (c *ContainerClient) GetProjectNames(containers []*Container) []string {
return []string{}
}

View file

@ -0,0 +1,167 @@
package commands
import (
"io"
ogLog "log"
"time"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
)
type ContainerCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
Tr *i18n.TranslationSet
Config *config.AppConfig
Client *ContainerClient
ErrorChan chan error
ContainerMutex deadlock.Mutex
ImageMutex deadlock.Mutex
VolumeMutex deadlock.Mutex
NetworkMutex deadlock.Mutex
Closers []io.Closer
}
var _ io.Closer = &ContainerCommand{}
type LimitedContainerCommand interface {
NewCommandObject(CommandObject) CommandObject
}
func NewContainerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*ContainerCommand, error) {
client, err := NewContainerClient(log, osCommand, tr, config, errorChan)
if err != nil {
ogLog.Fatal(err)
}
containerCommand := &ContainerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: config,
Client: client,
ErrorChan: errorChan,
Closers: []io.Closer{client},
}
return containerCommand, nil
}
func (c *ContainerCommand) Close() error {
return utils.CloseMany(c.Closers)
}
func (c *ContainerCommand) NewCommandObject(obj CommandObject) CommandObject {
return obj
}
func (c *ContainerCommand) CreateClientStatMonitor(container *Container) {
container.MonitoringStats = true
var prevStats *ContainerStats
lastTime := time.Now()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
if !container.MonitoringStats {
return
}
now := time.Now()
timeDelta := now.Sub(lastTime)
stats, err := c.Client.StatsContainer(container.ID, true)
if err != nil {
c.Log.Error(err)
continue
}
if len(stats) > 0 {
containerStats := ConvertAppleStatsToContainerStats(stats[0], prevStats, timeDelta)
recordedStats := &RecordedStats{
ClientStats: *containerStats,
DerivedStats: DerivedStats{
CPUPercentage: containerStats.CalculateContainerCPUPercentage(),
MemoryPercentage: containerStats.CalculateContainerMemoryUsage(),
},
RecordedAt: now,
}
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
prevStats = containerStats
lastTime = now
}
}
container.MonitoringStats = false
}
func (c *ContainerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, error) {
containers := c.RefreshContainers(currentContainers)
return containers, nil
}
func (c *ContainerCommand) RefreshContainers(currentContainers []*Container) []*Container {
return c.Client.RefreshContainers(currentContainers)
}
func (c *ContainerCommand) RefreshImages() ([]*Image, error) {
return c.Client.RefreshImages()
}
func (c *ContainerCommand) RefreshVolumes() ([]*Volume, error) {
return c.Client.RefreshVolumes()
}
func (c *ContainerCommand) RefreshNetworks() ([]*Network, error) {
return c.Client.RefreshNetworks()
}
func (c *ContainerCommand) PruneContainers() error {
return c.Client.PruneContainers()
}
func (c *ContainerCommand) PruneImages() error {
return c.Client.PruneImages(false)
}
func (c *ContainerCommand) PruneVolumes() error {
return c.Client.PruneVolumes()
}
func (c *ContainerCommand) PruneNetworks() error {
return c.Client.PruneNetworks()
}
func (c *ContainerCommand) SystemDF() (*SystemDF, error) {
return c.Client.SystemDF()
}
func (c *ContainerCommand) SystemStatus() (*SystemStatus, error) {
return c.Client.SystemStatus()
}
func (c *ContainerCommand) SystemStart() error {
return c.Client.SystemStart()
}
func (c *ContainerCommand) SystemStop() error {
return c.Client.SystemStop()
}
func (c *ContainerCommand) BuildImage(contextDir string, dockerfile string, tag string) error {
return c.Client.BuildImage(contextDir, dockerfile, tag)
}
func (c *ContainerCommand) GetProjectNames(containers []*Container) []string {
return []string{}
}

View file

@ -5,151 +5,53 @@ import (
"time"
)
// 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
type RecordedStats struct {
ClientStats ContainerStats
DerivedStats DerivedStats
RecordedAt time.Time
}
// DerivedStats contains some useful stats that we've calculated based on the raw container stats that we got back from docker
type DerivedStats struct {
CPUPercentage float64
MemoryPercentage float64
}
// ContainerStats autogenerated at https://mholt.github.io/json-to-go/
type ContainerStats struct {
Read time.Time `json:"read"`
Preread time.Time `json:"preread"`
PidsStats struct {
Current int `json:"current"`
} `json:"pids_stats"`
BlkioStats struct {
IoServiceBytesRecursive []struct {
Major int `json:"major"`
Minor int `json:"minor"`
Op string `json:"op"`
Value int `json:"value"`
} `json:"io_service_bytes_recursive"`
IoServicedRecursive []struct {
Major int `json:"major"`
Minor int `json:"minor"`
Op string `json:"op"`
Value int `json:"value"`
} `json:"io_serviced_recursive"`
IoQueueRecursive []interface{} `json:"io_queue_recursive"`
IoServiceTimeRecursive []interface{} `json:"io_service_time_recursive"`
IoWaitTimeRecursive []interface{} `json:"io_wait_time_recursive"`
IoMergedRecursive []interface{} `json:"io_merged_recursive"`
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 {
CPUUsage struct {
TotalUsage int64 `json:"total_usage"`
PercpuUsage []int64 `json:"percpu_usage"`
UsageInKernelmode int64 `json:"usage_in_kernelmode"`
UsageInUsermode int64 `json:"usage_in_usermode"`
} `json:"cpu_usage"`
SystemCPUUsage int64 `json:"system_cpu_usage"`
OnlineCpus int `json:"online_cpus"`
ThrottlingData struct {
Periods int `json:"periods"`
ThrottledPeriods int `json:"throttled_periods"`
ThrottledTime int `json:"throttled_time"`
} `json:"throttling_data"`
} `json:"cpu_stats"`
PrecpuStats struct {
CPUUsage struct {
TotalUsage int64 `json:"total_usage"`
PercpuUsage []int64 `json:"percpu_usage"`
UsageInKernelmode int64 `json:"usage_in_kernelmode"`
UsageInUsermode int64 `json:"usage_in_usermode"`
} `json:"cpu_usage"`
SystemCPUUsage int64 `json:"system_cpu_usage"`
OnlineCpus int `json:"online_cpus"`
ThrottlingData struct {
Periods int `json:"periods"`
ThrottledPeriods int `json:"throttled_periods"`
ThrottledTime int `json:"throttled_time"`
} `json:"throttling_data"`
} `json:"precpu_stats"`
MemoryStats struct {
Usage int `json:"usage"`
MaxUsage int `json:"max_usage"`
Stats struct {
ActiveAnon int `json:"active_anon"`
ActiveFile int `json:"active_file"`
Cache int `json:"cache"`
Dirty int `json:"dirty"`
HierarchicalMemoryLimit int64 `json:"hierarchical_memory_limit"`
HierarchicalMemswLimit int64 `json:"hierarchical_memsw_limit"`
InactiveAnon int `json:"inactive_anon"`
InactiveFile int `json:"inactive_file"`
MappedFile int `json:"mapped_file"`
Pgfault int `json:"pgfault"`
Pgmajfault int `json:"pgmajfault"`
Pgpgin int `json:"pgpgin"`
Pgpgout int `json:"pgpgout"`
Rss int `json:"rss"`
RssHuge int `json:"rss_huge"`
TotalActiveAnon int `json:"total_active_anon"`
TotalActiveFile int `json:"total_active_file"`
TotalCache int `json:"total_cache"`
TotalDirty int `json:"total_dirty"`
TotalInactiveAnon int `json:"total_inactive_anon"`
TotalInactiveFile int `json:"total_inactive_file"`
TotalMappedFile int `json:"total_mapped_file"`
TotalPgfault int `json:"total_pgfault"`
TotalPgmajfault int `json:"total_pgmajfault"`
TotalPgpgin int `json:"total_pgpgin"`
TotalPgpgout int `json:"total_pgpgout"`
TotalRss int `json:"total_rss"`
TotalRssHuge int `json:"total_rss_huge"`
TotalUnevictable int `json:"total_unevictable"`
TotalWriteback int `json:"total_writeback"`
Unevictable int `json:"unevictable"`
Writeback int `json:"writeback"`
} `json:"stats"`
Limit int64 `json:"limit"`
} `json:"memory_stats"`
Name string `json:"name"`
ID string `json:"id"`
Networks struct {
Eth0 struct {
RxBytes int `json:"rx_bytes"`
RxPackets int `json:"rx_packets"`
RxErrors int `json:"rx_errors"`
RxDropped int `json:"rx_dropped"`
TxBytes int `json:"tx_bytes"`
TxPackets int `json:"tx_packets"`
TxErrors int `json:"tx_errors"`
TxDropped int `json:"tx_dropped"`
} `json:"eth0"`
} `json:"networks"`
ID string `json:"id"`
CPUUsageUsec int64 `json:"cpuUsageUsec"`
MemoryUsageBytes int64 `json:"memoryUsageBytes"`
MemoryLimitBytes int64 `json:"memoryLimitBytes"`
NetworkRxBytes int64 `json:"networkRxBytes"`
NetworkTxBytes int64 `json:"networkTxBytes"`
BlockReadBytes int64 `json:"blockReadBytes"`
BlockWriteBytes int64 `json:"blockWriteBytes"`
NumProcesses int `json:"numProcesses"`
PrevCPUUsageUsec int64 `json:"-"`
TimeDeltaUsec int64 `json:"-"`
}
// CalculateContainerCPUPercentage calculates the cpu usage of the container as a percent of total CPU usage
// to calculate CPU usage, we take the increase in CPU time from the container since the last poll, divide that by the total increase in CPU time since the last poll, times by the number of cores, and times by 100 to get a percentage
// I'm not entirely sure why we need to multiply by the number of cores, but the numbers work
func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
cpuUsageDelta := s.CPUStats.CPUUsage.TotalUsage - s.PrecpuStats.CPUUsage.TotalUsage
cpuTotalUsageDelta := s.CPUStats.SystemCPUUsage - s.PrecpuStats.SystemCPUUsage
value := float64(cpuUsageDelta*100) / float64(cpuTotalUsageDelta)
if math.IsNaN(value) {
if s.TimeDeltaUsec == 0 {
return 0
}
cpuDelta := s.CPUUsageUsec - s.PrevCPUUsageUsec
if cpuDelta < 0 {
cpuDelta = s.CPUUsageUsec
}
value := float64(cpuDelta*1000000) / float64(s.TimeDeltaUsec)
if math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return value
}
// 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) {
if s.MemoryLimitBytes == 0 {
return 0
}
value := float64(s.MemoryUsageBytes*100) / float64(s.MemoryLimitBytes)
if math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return value
@ -163,7 +65,6 @@ func (c *Container) appendStats(stats *RecordedStats, maxDuration time.Duration)
c.eraseOldHistory(maxDuration)
}
// eraseOldHistory removes any history before the user-specified max duration
func (c *Container) eraseOldHistory(maxDuration time.Duration) {
if maxDuration == 0 {
return
@ -186,3 +87,24 @@ func (c *Container) GetLastStats() (*RecordedStats, bool) {
}
return history[len(history)-1], true
}
func ConvertAppleStatsToContainerStats(appleStats AppleContainerStats, prevStats *ContainerStats, timeDelta time.Duration) *ContainerStats {
stats := &ContainerStats{
ID: appleStats.ID,
CPUUsageUsec: appleStats.CPUUsageUsec,
MemoryUsageBytes: appleStats.MemoryUsageBytes,
MemoryLimitBytes: appleStats.MemoryLimitBytes,
NetworkRxBytes: appleStats.NetworkRxBytes,
NetworkTxBytes: appleStats.NetworkTxBytes,
BlockReadBytes: appleStats.BlockReadBytes,
BlockWriteBytes: appleStats.BlockWriteBytes,
NumProcesses: appleStats.NumProcesses,
}
if prevStats != nil {
stats.PrevCPUUsageUsec = prevStats.CPUUsageUsec
stats.TimeDeltaUsec = int64(timeDelta.Microseconds())
}
return stats
}

View file

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

@ -1,569 +0,0 @@
package commands
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
ogLog "log"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"time"
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"
)
const (
dockerHostEnvKey = "DOCKER_HOST"
)
// DockerCommand is our main docker interface
type DockerCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
Tr *i18n.TranslationSet
Config *config.AppConfig
Client *client.Client
InDockerComposeProject bool
// 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
}
// CommandObject is what we pass to our template resolvers when we are running a custom command. We do not guarantee that all fields will be populated: just the ones that make sense for the current context
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)
// 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
}
// 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) {
dockerHost, err := determineDockerHost()
if err != nil {
ogLog.Printf("> could not determine host %v", err)
}
// NOTE: Inject the determined docker host to the environment. This allows the
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
// over SSH to the specified ssh host.
if strings.HasPrefix(dockerHost, "ssh://") {
os.Setenv(dockerHostEnvKey, dockerHost)
}
tunnelCloser, err := ssh.NewSSHHandler(osCommand).HandleSSHDockerHost()
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{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: config,
Client: cli,
ErrorChan: errorChan,
InDockerComposeProject: true,
Closers: []io.Closer{tunnelCloser},
}
dockerCommand.setDockerComposeCommand(config)
err = osCommand.RunCommand(
utils.ApplyTemplate(
config.UserConfig.CommandTemplates.CheckDockerComposeConfig,
dockerCommand.NewCommandObject(CommandObject{}),
),
)
if err != nil {
dockerCommand.InDockerComposeProject = false
log.Warn(err.Error())
}
return dockerCommand, nil
}
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
return
}
// it's possible that a user is still using docker-compose, so we'll check if 'docker comopose' is available, and if not, we'll fall back to 'docker-compose'
err := c.OSCommand.RunCommand("docker compose version")
if err != nil {
config.UserConfig.CommandTemplates.DockerCompose = "docker-compose"
}
}
func (c *DockerCommand) Close() error {
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 {
// 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
}
defer stream.Body.Close()
scanner := bufio.NewScanner(stream.Body)
for scanner.Scan() {
data := scanner.Bytes()
var stats ContainerStats
_ = json.Unmarshal(data, &stats)
recordedStats := &RecordedStats{
ClientStats: stats,
DerivedStats: DerivedStats{
CPUPercentage: stats.CalculateContainerCPUPercentage(),
MemoryPercentage: stats.CalculateContainerMemoryUsage(),
},
RecordedAt: time.Now(),
}
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
}
container.MonitoringStats = false
}
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
c.ServiceMutex.Lock()
defer c.ServiceMutex.Unlock()
containers, err := c.GetContainers(currentContainers)
if err != nil {
return nil, nil, err
}
// 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 {
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)
return containers, services, nil
}
// GetServicesFromContainers derives services from container labels for all projects
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service {
// Use project+service as key to avoid duplicates
type serviceKey struct {
project string
service string
}
seen := make(map[serviceKey]bool)
services := make([]*Service, 0, len(containers))
for _, ctr := range containers {
if ctr.ServiceName == "" || ctr.OneOff {
continue
}
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
if seen[key] {
continue
}
seen[key] = true
services = append(services, &Service{
Name: ctr.ServiceName,
ID: ctr.ProjectName + "-" + ctr.ServiceName,
ProjectName: ctr.ProjectName,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
})
}
return services
}
// mergeServices merges compose services (which may lack ProjectName) with
// container-derived services. Compose services take priority because they
// include services without running containers.
func (c *DockerCommand) mergeServices(containerServices []*Service, composeServices []*Service) []*Service {
// Set project name on compose services
for _, svc := range composeServices {
if svc.ProjectName == "" {
svc.ProjectName = c.LocalProjectName
}
}
// Build a set of compose service names for the local project
composeServiceNames := make(map[string]bool)
for _, svc := range composeServices {
composeServiceNames[svc.Name] = true
}
// Start with compose services, then add container-derived services
// that aren't already covered by compose (i.e. from other projects)
result := make([]*Service, 0, len(composeServices)+len(containerServices))
result = append(result, composeServices...)
for _, svc := range containerServices {
if svc.ProjectName == c.LocalProjectName && composeServiceNames[svc.Name] {
continue // already covered by compose service
}
result = append(result, svc)
}
return result
}
// GetProjectNames returns all unique project names from containers
func (c *DockerCommand) GetProjectNames(containers []*Container) []string {
seen := make(map[string]bool)
var names []string
for _, ctr := range containers {
if ctr.ProjectName != "" && !seen[ctr.ProjectName] {
seen[ctr.ProjectName] = true
names = append(names, ctr.ProjectName)
}
}
sort.Strings(names)
return names
}
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
L:
for _, service := range services {
for _, ctr := range containers {
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
service.Container = ctr
continue L
}
}
service.Container = nil
}
}
// GetContainers gets the docker containers
func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) {
c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock()
containers, err := c.Client.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
return nil, err
}
ownContainers := make([]*Container, len(containers))
for i, ctr := range containers {
var newContainer *Container
// check if we already have data stored against the container
for _, existingContainer := range existingContainers {
if existingContainer.ID == ctr.ID {
newContainer = existingContainer
break
}
}
// initialise the container if it's completely new
if newContainer == nil {
newContainer = &Container{
ID: ctr.ID,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
Tr: c.Tr,
}
}
newContainer.Container = ctr
// if the container is made with a name label we will use that
if name, ok := ctr.Labels["name"]; ok {
newContainer.Name = name
} else {
if len(ctr.Names) > 0 {
newContainer.Name = strings.TrimLeft(ctr.Names[0], "/")
} else {
newContainer.Name = ctr.ID
}
}
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
}
// GetServices gets services
func (c *DockerCommand) GetServices() ([]*Service, error) {
if !c.InDockerComposeProject {
return nil, nil
}
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand))
if err != nil {
return nil, err
}
// output looks like:
// service1
// service2
lines := utils.SplitLines(output)
services := make([]*Service, len(lines))
for i, str := range lines {
services[i] = &Service{
Name: str,
ID: c.LocalProjectName + "-" + str,
ProjectName: c.LocalProjectName,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
}
}
return services, nil
}
func (c *DockerCommand) RefreshContainerDetails(containers []*Container) error {
c.ContainerMutex.Lock()
defer c.ContainerMutex.Unlock()
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(project *Project) (*exec.Cmd, error) {
cmd := c.OSCommand.ExecutableFromString(
utils.ApplyTemplate(
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
c.NewCommandObject(CommandObject{Project: project}),
),
)
c.OSCommand.PrepareForChildren(cmd)
return cmd, nil
}
// 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{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

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

View file

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

View file

@ -1,63 +0,0 @@
package commands
import (
"os"
"testing"
"github.com/docker/docker/client"
"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")
})
}

View file

@ -1,52 +0,0 @@
package commands
import (
"io"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/sirupsen/logrus"
)
// This file exports dummy constructors for use by tests in other packages
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
}
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *config.AppConfig {
appConfig := &config.AppConfig{
Name: "lazydocker",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
}
return appConfig
}
// NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = io.Discard
return log.WithField("test", "test")
}
// NewDummyDockerCommand creates a new dummy DockerCommand for testing
func NewDummyDockerCommand() *DockerCommand {
return NewDummyDockerCommandWithOSCommand(NewDummyOSCommand())
}
// 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(), newAppConfig.UserConfig.Gui.Language),
Config: newAppConfig,
}
}

View file

@ -1,46 +1,42 @@
package commands
import (
"context"
"strings"
"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/jesseduffield/lazycontainer/pkg/utils"
"github.com/sirupsen/logrus"
)
// Image : A docker Image
type Image struct {
Name string
Tag string
ID string
Image image.Summary
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
DockerCommand LimitedDockerCommand
Name string
Tag string
ID string
AppleImage AppleImage
Client *ContainerClient
OSCommand *OSCommand
Log *logrus.Entry
ContainerCmd LimitedContainerCommand
}
// Remove removes the image
func (i *Image) Remove(options image.RemoveOptions) error {
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
return err
}
return nil
func (i *Image) Remove(force bool) error {
return i.Client.RemoveImage(i.ID, force)
}
func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []string {
tag := ""
if len(layer.Tags) > 0 {
tag = layer.Tags[0]
func (i *Image) Pull() error {
return i.Client.PullImage(i.Name + ":" + i.Tag)
}
func getImageDisplayStrings(img *Image, replacements map[string]string) []string {
name := img.Name
for prefix, replacement := range replacements {
if strings.HasPrefix(name, prefix) {
name = strings.Replace(name, prefix, replacement, 1)
break
}
}
id := strings.TrimPrefix(layer.ID, "sha256:")
id := strings.TrimPrefix(img.AppleImage.Descriptor.Digest, "sha256:")
if len(id) > 10 {
id = id[0:10]
}
@ -49,95 +45,56 @@ func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []str
idColor = color.FgBlue
}
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
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(layer.Size))
sizeColor := color.FgWhite
if size == "0B" {
sizeColor = color.FgBlue
size := img.AppleImage.FullSize
if size == "" {
size = utils.FormatBinaryBytes(int(img.AppleImage.Descriptor.Size))
}
return []string{
utils.ColoredString(id, idColor),
utils.ColoredString(tag, color.FgGreen),
utils.ColoredString(size, sizeColor),
createdBy,
utils.ColoredString(name, color.FgGreen),
utils.ColoredString(img.Tag, color.FgCyan),
utils.ColoredString(size, color.FgYellow),
}
}
// 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
}
tableBody := lo.Map(history, func(layer image.HistoryResponseItem, _ int) []string {
return getHistoryResponseItemDisplayStrings(layer)
})
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(), image.ListOptions{})
func (c *ContainerClient) RefreshImages() ([]*Image, error) {
appleImages, err := c.ListImages()
if err != nil {
return nil, err
}
ownImages := make([]*Image, len(images))
ownImages := make([]*Image, len(appleImages))
for i, img := range images {
firstTag := ""
tags := img.RepoTags
if len(tags) > 0 {
firstTag = tags[0]
}
nameParts := strings.Split(firstTag, ":")
tag := ""
name := "none"
for i, img := range appleImages {
nameParts := strings.Split(img.Reference, ":")
name := ""
tag := "latest"
if len(nameParts) > 1 {
tag = nameParts[len(nameParts)-1]
name = strings.Join(nameParts[:len(nameParts)-1], ":")
} else if len(nameParts) == 1 {
name = nameParts[0]
}
for prefix, replacement := range c.Config.UserConfig.Replacements.ImageNamePrefixes {
if strings.HasPrefix(name, prefix) {
name = strings.Replace(name, prefix, replacement, 1)
break
}
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: img.ID,
Name: name,
Tag: tag,
Image: img,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
ID: img.Descriptor.Digest,
Name: name,
Tag: tag,
AppleImage: img,
Client: c,
OSCommand: c.OSCommand,
Log: c.Log,
ContainerCmd: c,
}
}
return ownImages, nil
}
// PruneImages prunes images
func (c *DockerCommand) PruneImages() error {
_, err := c.Client.ImagesPrune(context.Background(), filters.Args{})
return err
}

View file

@ -1,54 +1,39 @@
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
AppleNetwork AppleNetwork
Client *ContainerClient
OSCommand *OSCommand
Log *logrus.Entry
DockerCommand LimitedDockerCommand
ContainerCmd LimitedContainerCommand
}
// RefreshNetworks gets the networks and stores them
func (c *DockerCommand) RefreshNetworks() ([]*Network, error) {
networks, err := c.Client.NetworkList(context.Background(), network.ListOptions{})
func (n *Network) Remove() error {
return n.Client.RemoveNetwork(n.Name)
}
func (c *ContainerClient) RefreshNetworks() ([]*Network, error) {
appleNetworks, err := c.ListNetworks()
if err != nil {
return nil, err
}
ownNetworks := make([]*Network, len(networks))
ownNetworks := make([]*Network, len(appleNetworks))
for i, nw := range networks {
for i, net := range appleNetworks {
ownNetworks[i] = &Network{
Name: nw.Name,
Network: nw,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
Name: net.ID,
AppleNetwork: net,
Client: c,
OSCommand: c.OSCommand,
Log: c.Log,
}
}
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

@ -14,8 +14,8 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/kill"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
)

View file

@ -1,355 +0,0 @@
package commands
import (
"fmt"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
)
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
}
scenarios := []scenario{
{
"echo -n '123'",
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123", output)
},
},
{
"rmdir unexisting-folder",
func(output string, err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
type scenario struct {
command string
test func(error)
}
scenarios := []scenario{
{
"rmdir unexisting-folder",
func(err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommand(s.command))
}
}
// TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(env string) string {
return ""
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL or $EDITOR")
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return exec.Command("exit", "0")
},
func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
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, "emacs", name)
return exec.Command("exit", "0")
},
func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
return exec.Command("exit", "0")
},
func(env string) string {
return ""
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.getenv = s.getenv
s.test(OSCmd.EditFile(s.filename))
}
}
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello `test`")
expected := "\"hello \\`test\\`\""
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello 'test'")
expected := `"hello 'test'"`
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote(`hello "test"`)
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)
}
// TestOSCommandUnquote is a function.
func TestOSCommandUnquote(t *testing.T) {
osCommand := NewDummyOSCommand()
actual := osCommand.Unquote(`hello "test"`)
expected := "hello test"
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
setup func()
test func(string)
}
scenarios := []scenario{
{
"testFile",
func() {
if _, err := os.Create("testFile"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"file with spaces",
func() {
if _, err := os.Create("file with spaces"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"testDirectory",
func() {
if err := os.Mkdir("testDirectory", 0o644); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "directory", output)
},
},
{
"nonExistant",
func() {},
func(output string) {
assert.EqualValues(t, "other", output)
},
},
}
for _, s := range scenarios {
s.setup()
s.test(NewDummyOSCommand().FileType(s.path))
_ = os.RemoveAll(s.path)
}
}
func TestOSCommandCreateTempFile(t *testing.T) {
type scenario struct {
testName string
filename string
content string
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"filename",
"content",
func(path string, err error) {
assert.NoError(t, err)
content, err := os.ReadFile(path)
assert.NoError(t, err)
assert.Equal(t, "content", string(content))
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
})
}
}
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

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

View file

@ -1,84 +0,0 @@
package commands
import (
"context"
"os/exec"
"github.com/docker/docker/api/types/container"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
)
// Service : A docker Service
type Service struct {
Name string
ID string
ProjectName string
OSCommand *OSCommand
Log *logrus.Entry
Container *Container
DockerCommand LimitedDockerCommand
}
// Remove removes the service's containers
func (s *Service) Remove(options container.RemoveOptions) error {
return s.Container.Remove(options)
}
// Stop stops the service's containers
func (s *Service) Stop() error {
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 {
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(
templateCmdStr,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
return s.OSCommand.RunCommand(command)
}
// Attach attaches to the service
func (s *Service) Attach() (*exec.Cmd, error) {
return s.Container.Attach()
}
// ViewLogs attaches to a subprocess viewing the service's logs
func (s *Service) ViewLogs() (*exec.Cmd, error) {
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs
command := utils.ApplyTemplate(
templateString,
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
)
cmd := s.OSCommand.ExecutableFromString(command)
s.OSCommand.PrepareForChildren(cmd)
return cmd, nil
}
// RenderTop renders the process list of the service
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.RunCommandWithOutputContext(ctx, command)
}

View file

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

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

@ -1,56 +1,39 @@
package commands
import (
"context"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// Volume : A docker Volume
type Volume struct {
Name string
Volume *volume.Volume
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
DockerCommand LimitedDockerCommand
Name string
AppleVolume AppleVolume
Client *ContainerClient
OSCommand *OSCommand
Log *logrus.Entry
ContainerCmd LimitedContainerCommand
}
// RefreshVolumes gets the volumes and stores them
func (c *DockerCommand) RefreshVolumes() ([]*Volume, error) {
result, err := c.Client.VolumeList(context.Background(), volume.ListOptions{})
func (v *Volume) Remove() error {
return v.Client.RemoveVolume(v.Name)
}
func (c *ContainerClient) RefreshVolumes() ([]*Volume, error) {
appleVolumes, err := c.ListVolumes()
if err != nil {
return nil, err
}
volumes := result.Volumes
ownVolumes := make([]*Volume, len(appleVolumes))
ownVolumes := make([]*Volume, len(volumes))
for i, vol := range volumes {
for i, vol := range appleVolumes {
ownVolumes[i] = &Volume{
Name: vol.Name,
Volume: vol,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
DockerCommand: c,
Name: vol.Name,
AppleVolume: vol,
Client: c,
OSCommand: c.OSCommand,
Log: c.Log,
}
}
return ownVolumes, nil
}
// PruneVolumes prunes volumes
func (c *DockerCommand) PruneVolumes() error {
_, err := c.Client.VolumesPrune(context.Background(), filters.Args{})
return err
}
// Remove removes the volume
func (v *Volume) Remove(force bool) error {
return v.Client.VolumeRemove(context.Background(), v.Name, force)
}

View file

@ -1,72 +1,26 @@
// Package config handles all the user-configuration. The fields here are
// all in PascalCase but in your actual config.yml they'll be in camelCase.
// You can view the default config with `lazydocker --config`.
// You can open your config file by going to the status panel (using left-arrow)
// and pressing 'o'.
// You can directly edit the file (e.g. in vim) by pressing 'e' instead.
// To see the final config after your user-specific options have been merged
// with the defaults, go to the 'about' tab in the status panel.
// Because of the way we merge your user config with the defaults you may need
// to be careful: if for example you set a `commandTemplates:` yaml key but then
// give it no child values, it will scrap all of the defaults and the app will
// probably crash.
package config
import (
"os"
"path/filepath"
"strings"
"time"
"github.com/OpenPeeDeeP/xdg"
"github.com/jesseduffield/yaml"
)
// UserConfig holds all of the user-configurable options
type UserConfig struct {
// Gui is for configuring visual things like colors and whether we show or
// hide things
Gui GuiConfig `yaml:"gui,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"`
// CustomCommands determines what shows up in your custom commands menu when
// you press 'c'. You can use go templates to access three items on the
// struct: the DockerCompose command (defaulted to 'docker-compose'), the
// Service if present, and the Container if present. The struct types for
// those are found in the commands package
Gui GuiConfig `yaml:"gui,omitempty"`
ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"`
Logs LogsConfig `yaml:"logs,omitempty"`
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"`
// 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"`
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
OS OSConfig `yaml:"oS,omitempty"`
Stats StatsConfig `yaml:"stats,omitempty"`
Replacements Replacements `yaml:"replacements,omitempty"`
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"`
@ -74,269 +28,56 @@ type ThemeConfig struct {
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
}
// GuiConfig is for configuring visual things like colors and whether we show or
// hide things
type GuiConfig struct {
// ScrollHeight determines how many characters you scroll at a time when
// 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"`
// IgnoreMouseEvents is for when you do not want to use your mouse to interact
// with anything
IgnoreMouseEvents bool `yaml:"mouseEvents,omitempty"`
// Theme determines what colors and color attributes your panel borders have.
// I always set inactiveBorderColor to black because in my terminal it's more
// of a grey, but that doesn't work in your average terminal. I highly
// recommended finding a combination that works for you
Theme ThemeConfig `yaml:"theme,omitempty"`
// ShowAllContainers determines whether the Containers panel contains all the
// containers returned by `docker ps -a`, or just those containers that aren't
// directly linked to a service. It is probably desirable to enable this if
// you have multiple containers per service, but otherwise it can cause a lot
// of clutter
ShowAllContainers bool `yaml:"showAllContainers,omitempty"`
// ReturnImmediately determines whether you get the 'press enter to return to
// lazydocker' message after a subprocess has completed. You would set this to
// true if you often want to see the output of subprocesses before returning
// to lazydocker. I would default this to false but then people who want it
// set to true won't even know the config option exists.
ReturnImmediately bool `yaml:"returnImmediately,omitempty"`
// 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"`
ScrollHeight int `yaml:"scrollHeight,omitempty"`
Language string `yaml:"language,omitempty"`
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
IgnoreMouseEvents bool `yaml:"mouseEvents,omitempty"`
Theme ThemeConfig `yaml:"theme,omitempty"`
ShowAllContainers bool `yaml:"showAllContainers,omitempty"`
ReturnImmediately bool `yaml:"returnImmediately,omitempty"`
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ShowBottomLine bool `yaml:"showBottomLine"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
ScreenMode string `yaml:"screenMode,omitempty"`
ContainerStatusHealthStyle string `yaml:"containerStatusHealthStyle"`
Border string `yaml:"border"`
}
// CommandTemplatesConfig determines what commands actually get called when we
// run certain commands
type CommandTemplatesConfig struct {
// RestartService is for restarting a service. docker-compose restart {{
// .Service.Name }} works but I prefer docker-compose up --force-recreate {{
// .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
// 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
// paste it to all the other commands
DockerCompose string `yaml:"dockerCompose,omitempty"`
// StopService is the command for stopping a service
StopService string `yaml:"stopService,omitempty"`
// ServiceLogs get the logs for a service. This is actually not currently
// used; we just get the logs of the corresponding container. But we should
// probably support explicitly returning the logs of the service when you've
// selected the service, given that a service may have multiple containers.
ServiceLogs string `yaml:"serviceLogs,omitempty"`
// ViewServiceLogs is for when you want to view the logs of a service as a
// subprocess. This defaults to having no filter, unlike the in-app logs
// commands which will usually filter down to the last hour for the sake of
// performance.
ViewServiceLogs string `yaml:"viewServiceLogs,omitempty"`
// RebuildService is the command for rebuilding a service. Defaults to
// something along the lines of `{{ .DockerCompose }} up --build {{
// .Service.Name }}`
RebuildService string `yaml:"rebuildService,omitempty"`
// RecreateService is for force-recreating a service. I prefer this to
// restarting a service because it will also restart any dependent services
// and ensure they're running before trying to run the service at hand
RecreateService string `yaml:"recreateService,omitempty"`
// 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 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
// compose. It basically prints out the yaml from your docker-compose.yml
// file(s)
DockerComposeConfig string `yaml:"dockerComposeConfig,omitempty"`
// CheckDockerComposeConfig is what we use to check whether we are in a
// docker-compose context. If the command returns an error then we clearly
// aren't in a docker-compose config and we then just hide the services panel
// and only show containers
CheckDockerComposeConfig string `yaml:"checkDockerComposeConfig,omitempty"`
// ServiceTop is the command for viewing the processes under a given service
ServiceTop string `yaml:"serviceTop,omitempty"`
}
// OSConfig contains config on the level of the os
type OSConfig struct {
// OpenCommand is the command for opening a file
OpenCommand string `yaml:"openCommand,omitempty"`
// OpenCommand is the command for opening a link
OpenLinkCommand string `yaml:"openLinkCommand,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
// this, you should also set MinType to "static". The reason for this is that
// if Min == 0, it's not clear if it has not been set (given that the
// zero-value of an int is 0) or if it's intentionally been set to 0.
Min float64 `yaml:"min,omitempty"`
// Max sets the maximum value that you want to display. If you want to set
// this, you should also set MaxType to "static". The reason for this is that
// if Max == 0, it's not clear if it has not been set (given that the
// zero-value of an int is 0) or if it's intentionally been set to 0.
Max float64 `yaml:"max,omitempty"`
// Height sets the height of the graph in ascii characters
Height int `yaml:"height,omitempty"`
// Caption sets the caption of the graph. If you want to show CPU Percentage
// you could set this to "CPU (%)"
Caption string `yaml:"caption,omitempty"`
// This is the path to the stat that you want to display. It is based on the
// RecordedStats struct in container_stats.go, so feel free to look there to
// see all the options available. Alternatively if you go into lazydocker and
// go to the stats tab, you'll see that same struct in JSON format, so you can
// just PascalCase the path and you'll have a valid path. E.g.
// ClientStats.blkio_stats -> "ClientStats.BlkioStats"
StatPath string `yaml:"statPath,omitempty"`
// This determines the color of the graph. This can be any color attribute,
// e.g. 'blue', 'green'
Color string `yaml:"color,omitempty"`
// MinType and MaxType are each one of "", "static". blank means the min/max
// of the data set will be used. "static" means the min/max specified will be
// used
MinType string `yaml:"minType,omitempty"`
// MaxType is just like MinType but for the max value
MaxType string `yaml:"maxType,omitempty"`
Min float64 `yaml:"min,omitempty"`
Max float64 `yaml:"max,omitempty"`
Height int `yaml:"height,omitempty"`
Caption string `yaml:"caption,omitempty"`
StatPath string `yaml:"statPath,omitempty"`
Color string `yaml:"color,omitempty"`
MinType string `yaml:"minType,omitempty"`
MaxType string `yaml:"maxType,omitempty"`
}
// StatsConfig contains the stuff relating to stats and graphs
type StatsConfig struct {
// Graphs contains the configuration for the stats graphs we want to show in
// the app
Graphs []GraphConfig
// MaxDuration tells us how long to collect stats for. Currently this defaults
// to "5m" i.e. 5 minutes.
Graphs []GraphConfig `yaml:"graphs"`
MaxDuration time.Duration `yaml:"maxDuration,omitempty"`
}
// CustomCommands contains the custom commands that you might want to use on any
// given service or container
type CustomCommands struct {
// Containers contains the custom commands for containers
Containers []CustomCommand `yaml:"containers,omitempty"`
// 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"`
Images []CustomCommand `yaml:"images,omitempty"`
Volumes []CustomCommand `yaml:"volumes,omitempty"`
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
// container
type CustomCommand struct {
// Name is the name of the command, purely for visual display
Name string `yaml:"name"`
// Attach tells us whether to switch to a subprocess to interact with the
// called program, or just read its output. If Attach is set to false, the
// command will run in the background. I'm open to the idea of having a third
// 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`
Command string `yaml:"command"`
// ServiceNames is used to restrict this command to just one or more services.
// An example might be 'rails migrate' for your rails api service(s). This
// 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
Name string `yaml:"name"`
Attach bool `yaml:"attach"`
Shell bool `yaml:"shell"`
Command string `yaml:"command"`
InternalFunction func() error `yaml:"-"`
}
@ -346,10 +87,11 @@ type LogsConfig struct {
Tail string `yaml:"tail,omitempty"`
}
// GetDefaultConfig returns the application default configuration NOTE (to
// contributors, not users): do not default a boolean to true, because false is
// the boolean zero value and this will be ignored when parsing the user's
// config
type OSConfig struct {
OpenCommand string `yaml:"openCommand,omitempty"`
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
}
func GetDefaultConfig() UserConfig {
duration, err := time.ParseDuration("3m")
if err != nil {
@ -368,11 +110,11 @@ func GetDefaultConfig() UserConfig {
SelectedLineBgColor: []string{"blue"},
OptionsTextColor: []string{"blue"},
},
ShowAllContainers: false,
ShowAllContainers: true,
ReturnImmediately: false,
WrapMainPanel: true,
LegacySortContainers: false,
SidePanelWidth: 0.3333,
SidePanelWidth: 0.25,
ShowBottomLine: true,
ExpandFocusedSidePanel: false,
ScreenMode: "normal",
@ -384,76 +126,41 @@ func GetDefaultConfig() UserConfig {
Since: "60m",
Tail: "",
},
CommandTemplates: CommandTemplatesConfig{
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 }}",
ServiceLogs: "{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}",
ViewServiceLogs: "{{ .DockerCompose }} logs --follow {{ .Service.Name }}",
AllLogs: "{{ .DockerCompose }} logs --tail=300 --follow",
ViewAllLogs: "{{ .DockerCompose }} logs",
DockerComposeConfig: "{{ .DockerCompose }} config",
CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet",
ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}",
},
CustomCommands: CustomCommands{
Containers: []CustomCommand{},
Services: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
Networks: []CustomCommand{},
},
BulkCommands: CustomCommands{
Services: []CustomCommand{
Containers: []CustomCommand{
{
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",
Name: "prune stopped",
Command: "container prune",
},
},
Images: []CustomCommand{
{
Name: "prune dangling",
Command: "container image prune",
},
{
Name: "prune all",
Command: "container image prune -a",
},
},
Volumes: []CustomCommand{
{
Name: "prune",
Command: "container volume prune",
},
},
Networks: []CustomCommand{
{
Name: "prune",
Command: "container network prune",
},
},
Containers: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
OS: GetPlatformDefaultConfig(),
Stats: StatsConfig{
@ -477,13 +184,12 @@ func GetDefaultConfig() UserConfig {
}
}
// 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:"lazydocker"`
Name string `long:"name" env:"NAME" default:"lazyapple"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *UserConfig
ConfigDir string
@ -491,7 +197,6 @@ type AppConfig struct {
ProjectName string
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir(name)
if err != nil {
@ -503,11 +208,6 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
return nil, err
}
// Pass compose files as individual -f flags to docker compose
if len(composeFiles) > 0 {
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
}
appConfig := &AppConfig{
Name: name,
Version: version,
@ -586,10 +286,6 @@ func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
return base, nil
}
// WriteToUserConfig allows you to set a value on the user config to be saved
// note that if you set a zero-value, it may be ignored e.g. a false or 0 or
// empty string this is because we are using the omitempty yaml directive so
// that we don't write a heap of zero values to the user's config.yml
func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) error {
userConfig, err := loadUserConfig(c.ConfigDir, &UserConfig{})
if err != nil {
@ -608,7 +304,6 @@ func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) erro
return yaml.NewEncoder(file).Encode(userConfig)
}
// ConfigFilename returns the filename of the current config file
func (c *AppConfig) ConfigFilename() string {
return filepath.Join(c.ConfigDir, "config.yml")
}

View file

@ -7,50 +7,7 @@ import (
"github.com/jesseduffield/yaml"
)
func TestDockerComposeCommandNoFiles(t *testing.T) {
composeFiles := []string{}
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"
if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual)
}
}
func TestDockerComposeCommandSingleFile(t *testing.T) {
composeFiles := []string{"one.yml"}
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"
if actual != expected {
t.Fatalf("Expected %s but got %s", expected, actual)
}
}
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
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"
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 {
@ -90,9 +47,6 @@ func TestWritingToConfigFile(t *testing.T) {
}
}
// insert value into an empty file
testFn(t, conf, true)
// modifying an existing file that already has 'ConfirmOnQuit'
testFn(t, conf, false)
}

View file

@ -4,7 +4,7 @@ import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
type appStatus struct {

View file

@ -2,15 +2,12 @@ package gui
import (
"github.com/jesseduffield/lazycore/pkg/boxlayout"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/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 {
@ -67,9 +64,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
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
@ -120,7 +115,6 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
},
{
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)),
},
}...,
@ -143,6 +137,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName()
sideWindowNames := gui.sideViewNames()
if len(sideWindowNames) == 0 {
return []*boxlayout.Box{}
}
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow {
@ -162,7 +160,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
return fullHeightBox(window)
})
} else if height >= 28 {
} else if height >= 20 {
accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
@ -175,27 +173,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
return defaultBox
}
// The project panel is compact (Size: 3) when not focused, but expands
// when focused to show the list of projects.
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 lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
})...)
})
} else {
squashedHeight := 1
if height >= 21 {
if height >= 12 {
squashedHeight = 3
}

View file

@ -1,19 +1,19 @@
package gui
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"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"
"github.com/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
@ -22,7 +22,6 @@ func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.T
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,
@ -41,8 +40,6 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx
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 {
@ -52,11 +49,10 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx
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 {
if result.Status == "running" {
return
}
}
@ -97,7 +93,6 @@ 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)
}
@ -105,48 +100,71 @@ func (gui *Gui) promptToReturn() {
}
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,
})
tail := 100
if gui.Config.UserConfig.Logs.Tail != "" {
fmt.Sscanf(gui.Config.UserConfig.Logs.Tail, "%d", &tail)
}
cmd := gui.ContainerCmd.Client.LogsContainer(ctr.ID, true, tail)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
gui.Log.Error(err)
return err
}
stderrPipe, err := cmd.StderrPipe()
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 {
if err := cmd.Start(); err != nil {
gui.Log.Error(err)
return err
}
done := make(chan struct{})
go func() {
scanner := bufio.NewScanner(stdoutPipe)
for scanner.Scan() {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if ctr.DetailsLoaded() {
break outer
}
return
default:
fmt.Fprintln(writer, scanner.Text())
}
}
}
}()
if ctr.Details.Config.Tty {
_, err = io.Copy(writer, readCloser)
if err != nil {
return err
go func() {
scanner := bufio.NewScanner(stderrPipe)
for scanner.Scan() {
select {
case <-ctx.Done():
return
default:
fmt.Fprintln(writer, scanner.Text())
}
}
} else {
_, err = stdcopy.StdCopy(writer, writer, readCloser)
if err != nil {
return err
}
}
}()
return nil
go func() {
cmd.Wait()
close(done)
}()
select {
case <-ctx.Done():
killCmd(cmd)
return nil
case <-done:
return nil
}
}
func killCmd(cmd *exec.Cmd) {
if cmd.Process != nil {
cmd.Process.Kill()
}
}

View file

@ -6,31 +6,16 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types/container"
"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"
"github.com/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
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
}
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool {
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName
})
}
return &panels.SideListPanel[*commands.Container]{
ContextState: &panels.ContextState[*commands.Container]{
GetMainTabs: func() []panels.MainTab[*commands.Container] {
@ -63,12 +48,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
}
},
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
return "containers-" + container.ID + "-" + container.GetStatus()
},
},
ListPanel: panels.ListPanel[*commands.Container]{
@ -77,33 +57,13 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
},
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 {
// 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) {
if !gui.State.ShowExitedContainers && container.GetStatus() != "running" {
return false
}
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
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 {
@ -114,6 +74,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
var containerStates = map[string]int{
"running": 1,
"stopped": 2,
"exited": 2,
"created": 3,
}
@ -123,13 +84,13 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo
return a.Name < b.Name
}
stateLeft := containerStates[a.Container.State]
stateRight := containerStates[b.Container.State]
stateLeft := containerStates[a.GetStatus()]
stateRight := containerStates[b.GetStatus()]
if stateLeft == stateRight {
return a.Name < b.Name
}
return containerStates[a.Container.State] < containerStates[b.Container.State]
return stateLeft < stateRight
}
func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc {
@ -137,105 +98,113 @@ func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc
}
func (gui *Gui) containerEnv(container *commands.Container) string {
if !container.DetailsLoaded() {
return gui.Tr.WaitingForContainerInfo
}
if len(container.Details.Config.Env) == 0 {
env := container.GetEnv()
if len(env) == 0 {
return gui.Tr.NothingToDisplay
}
envVarsList := lo.Map(container.Details.Config.Env, func(envVar string, _ int) []string {
var lines []string
for _, envVar := range env {
splitEnv := strings.SplitN(envVar, "=", 2)
key := splitEnv[0]
value := ""
if len(splitEnv) > 1 {
value = splitEnv[1]
}
return []string{
utils.ColoredString(key+":", color.FgGreen),
lines = append(lines, fmt.Sprintf("%s=%s",
utils.ColoredString(key, color.FgGreen),
utils.ColoredString(value, color.FgYellow),
}
})
output, err := utils.RenderTable(envVarsList)
if err != nil {
gui.Log.Error(err)
return gui.Tr.CannotDisplayEnvVariables
))
}
return output
return strings.Join(lines, "\n")
}
func (gui *Gui) renderContainerConfig(container *commands.Container) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfigStr(container) })
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfig(container) })
}
func (gui *Gui) containerConfigStr(container *commands.Container) string {
if !container.DetailsLoaded() {
return gui.Tr.WaitingForContainerInfo
func (gui *Gui) containerConfig(container *commands.Container) string {
var lines []string
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("ID", color.FgCyan),
container.ID,
))
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("Name", color.FgCyan),
container.Name,
))
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("Image", color.FgCyan),
container.GetImage(),
))
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("Status", color.FgCyan),
container.GetStatus(),
))
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("IP", color.FgCyan),
container.GetIP(),
))
lines = append(lines, fmt.Sprintf("%s: %d",
utils.ColoredString("CPUs", color.FgCyan),
container.GetCPUs(),
))
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("Memory", color.FgCyan),
utils.FormatBinaryBytes(int(container.GetMemory())),
))
ports := container.GetPorts()
if len(ports) > 0 {
lines = append(lines, fmt.Sprintf("%s: %s",
utils.ColoredString("Ports", color.FgCyan),
strings.Join(ports, ", "),
))
}
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"
output += utils.WithPadding("Mounts: ", padding)
if len(container.Details.Mounts) > 0 {
output += "\n"
for _, mount := range container.Details.Mounts {
if mount.Type == "volume" {
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name)
} else {
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination)
}
}
} else {
output += "none\n"
}
output += utils.WithPadding("Ports: ", padding)
if len(container.Details.NetworkSettings.Ports) > 0 {
output += "\n"
for k, v := range container.Details.NetworkSettings.Ports {
for _, host := range v {
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(host.HostPort+":", color.FgYellow), k)
}
}
} else {
output += "none\n"
}
data, err := utils.MarshalIntoYaml(&container.Details)
if err != nil {
return fmt.Sprintf("Error marshalling container details: %v", err)
}
output += fmt.Sprintf("\nFull details:\n\n%s", utils.ColoredYamlString(string(data)))
return output
return strings.Join(lines, "\n")
}
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())
}
return gui.NewSimpleRenderStringTask(func() string { return gui.containerStats(container) })
}
gui.reRenderStringMain(contents)
},
Duration: time.Second,
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: false, // wrapping looks bad here so we're overriding the config value
Autoscroll: false,
})
func (gui *Gui) containerStats(container *commands.Container) string {
stats, ok := container.GetLastStats()
if !ok {
return gui.Tr.NoStats
}
var lines []string
lines = append(lines, fmt.Sprintf("%s: %.1f%%",
utils.ColoredString("CPU", color.FgCyan),
stats.DerivedStats.CPUPercentage,
))
lines = append(lines, fmt.Sprintf("%s: %.1f%% (%s / %s)",
utils.ColoredString("Memory",
color.FgCyan),
stats.DerivedStats.MemoryPercentage,
utils.FormatBinaryBytes(int(stats.ClientStats.MemoryUsageBytes)),
utils.FormatBinaryBytes(int(stats.ClientStats.MemoryLimitBytes)),
))
lines = append(lines, fmt.Sprintf("%s: %s / %s",
utils.ColoredString("Network I/O", color.FgCyan),
utils.FormatBinaryBytes(int(stats.ClientStats.NetworkRxBytes)),
utils.FormatBinaryBytes(int(stats.ClientStats.NetworkTxBytes)),
))
lines = append(lines, fmt.Sprintf("%s: %s / %s",
utils.ColoredString("Block I/O", color.FgCyan),
utils.FormatBinaryBytes(int(stats.ClientStats.BlockReadBytes)),
utils.FormatBinaryBytes(int(stats.ClientStats.BlockWriteBytes)),
))
lines = append(lines, fmt.Sprintf("%s: %d",
utils.ColoredString("Processes", color.FgCyan),
stats.ClientStats.NumProcesses,
))
return strings.Join(lines, "\n")
}
func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc {
@ -255,176 +224,94 @@ func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc
})
}
func (gui *Gui) refreshContainersAndServices() error {
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
originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
gui.Panels.Containers.List.GetAllItems(),
)
if err != nil {
return err
}
gui.Panels.Services.SetItems(services)
func (gui *Gui) refreshContainers() error {
containers := gui.ContainerCmd.RefreshContainers(gui.Panels.Containers.List.GetAllItems())
gui.Panels.Containers.SetItems(containers)
// see if our selected service has moved
if isServiceSelected {
for i, service := range gui.Panels.Services.List.GetItems() {
if service.ID == selectedService.ID {
if i == originalSelectedLineIdx {
break
}
gui.Panels.Services.SetSelectedLineIdx(i)
gui.Panels.Services.Refocus()
}
}
}
return gui.renderContainersAndServices()
}
func (gui *Gui) renderContainersAndServices() error {
if err := gui.Panels.Services.RerenderList(); err != nil {
return err
}
if err := gui.Panels.Containers.RerenderList(); err != nil {
return err
}
return nil
return gui.Panels.Containers.RerenderList()
}
func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
gui.State.ShowExitedContainers = !gui.State.ShowExitedContainers
return gui.Panels.Containers.RerenderList()
}
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
handleMenuPress := func(configOptions container.RemoveOptions) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := ctr.Remove(configOptions); err != nil {
if commands.HasErrorCode(err, commands.MustStopContainer) {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
configOptions.Force = true
return ctr.Remove(configOptions)
})
}, nil)
}
return gui.createErrorPanel(err.Error())
}
return nil
})
}
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) 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()
}
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.refreshContainersAndServices()
})
}
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.PauseContainer(ctr)
return gui.refreshContainers()
}
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
container, 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 {
if err := container.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()
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := ctr.Restart(); err != nil {
if err := container.Restart(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
c, err := ctr.Attach()
c, err := container.Attach()
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocessWithMessage(c, gui.Tr.DetachFromContainerShortCut)
return gui.runSubprocess(c)
}
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 {
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
cmd := gui.ContainerCmd.Client.LogsContainer(container.ID, true, 0)
return gui.runSubprocess(cmd)
}
func (gui *Gui) handleContainerKill(g *gocui.Gui, v *gocui.View) error {
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.KillingStatus, func() error {
if err := container.Kill(""); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.RemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := container.Remove(true); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
@ -432,46 +319,33 @@ func (gui *Gui) handlePruneContainers() error {
}, nil)
}
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
gui.renderLogsToStdout(ctr)
return nil
}
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem()
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.containerExecShell(ctr)
}
if container.GetStatus() != "running" {
return gui.createErrorPanel(gui.Tr.ContainerNotRunning)
}
func (gui *Gui) containerExecShell(container *commands.Container) error {
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Container: container,
cmd := gui.ContainerCmd.Client.ExecContainer(container.ID, commands.ExecOptions{
Command: []string{"/bin/sh"},
Interactive: true,
TTY: true,
})
// 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 {
ctr, err := gui.Panels.Containers.GetSelectedItem()
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Container: ctr,
commandObject := gui.ContainerCmd.NewCommandObject(commands.CommandObject{
Container: container,
})
customCommands := gui.Config.UserConfig.CustomCommands.Containers
@ -479,80 +353,41 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
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{})
bulkCommands := gui.Config.UserConfig.BulkCommands.Containers
commandObject := gui.ContainerCmd.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()
container, err := gui.Panels.Containers.GetSelectedItem()
if err != nil {
return nil
}
return gui.openContainerInBrowser(ctr)
ip := container.GetIP()
if ip == "" {
return gui.createErrorPanel(gui.Tr.NoIp)
}
ports := container.GetPorts()
if len(ports) == 0 {
return gui.createErrorPanel(gui.Tr.NoPorts)
}
for _, port := range ports {
parts := strings.Split(port, ":")
if len(parts) >= 2 {
hostPort := strings.Split(parts[0], "->")[0]
url := fmt.Sprintf("http://%s:%s", ip, hostPort)
return gui.OSCommand.OpenLink(url)
}
}
return nil
}
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)
func (gui *Gui) PauseContainer(container *commands.Container) error {
return gui.createErrorPanel("Pause is not supported by Apple Container")
}

View file

@ -2,10 +2,10 @@ package gui
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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/samber/lo"
)

View file

@ -3,7 +3,7 @@ package gui
import (
"github.com/gookit/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
var gocuiColorMap = map[string]gocui.Attribute{

View file

@ -6,40 +6,32 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types/events"
"github.com/go-errors/errors"
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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
)
// 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
State guiState
Config *config.AppConfig
Tr *i18n.TranslationSet
statusManager *statusManager
taskManager *tasks.TaskManager
ErrorChan chan error
Views Views
g *gocui.Gui
Log *logrus.Entry
ContainerCmd *commands.ContainerCommand
OSCommand *commands.OSCommand
State guiState
Config *config.AppConfig
Tr *i18n.TranslationSet
statusManager *statusManager
taskManager *tasks.TaskManager
ErrorChan chan error
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
@ -48,8 +40,6 @@ type Gui struct {
}
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]
@ -63,7 +53,6 @@ type Mutexes struct {
}
type mainPanelState struct {
// ObjectKey tells us what context we are in. For example, if we are looking at the logs of a particular service in the services panel this key might be 'services-<service id>-logs'. The key is made so that if something changes which might require us to re-run the logs command or run a different command, the key will be different, and we'll then know to do whatever is required. Object key probably isn't the best name for this but Context is already used to refer to tabs. Maybe I should just call them tabs.
ObjectKey string
}
@ -72,37 +61,25 @@ type panelStates struct {
}
type guiState struct {
// 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
// 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
panel panels.ISideListPanel
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 (
@ -124,8 +101,7 @@ func getScreenMode(config *config.AppConfig) WindowMaximisation {
}
}
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
func NewGui(log *logrus.Entry, containerCmd *commands.ContainerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
initialState := guiState{
Platform: *oSCommand.Platform,
Panels: &panelStates{
@ -141,7 +117,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
gui := &Gui{
Log: log,
DockerCommand: dockerCommand,
ContainerCmd: containerCmd,
OSCommand: oSCommand,
State: initialState,
Config: config,
@ -168,7 +144,7 @@ func (gui *Gui) renderGlobalOptions() error {
}
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
_ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
_ = function()
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
@ -180,9 +156,7 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) {
}()
}
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
// closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker
defer gui.taskManager.Close()
g, err := gocui.NewGui(gocui.NewGuiOpts{
@ -194,15 +168,12 @@ func (gui *Gui) Run() error {
}
defer g.Close()
// forgive the double-negative, this is because of my yaml `omitempty` woes
if !gui.Config.UserConfig.Gui.IgnoreMouseEvents {
g.Mouse = true
}
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
gui.g = g
// 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()
})
@ -220,7 +191,6 @@ func (gui *Gui) Run() error {
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
}
@ -237,7 +207,6 @@ func (gui *Gui) Run() error {
return err
}
// TODO: see if we can avoid the circular dependency
gui.setPanels()
if err = gui.keybindings(g); err != nil {
@ -259,17 +228,14 @@ func (gui *Gui) Run() error {
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.renderContainers)
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()
@ -281,8 +247,6 @@ func (gui *Gui) Run() error {
func (gui *Gui) setPanels() {
gui.Panels = Panels{
Projects: gui.getProjectPanel(),
Services: gui.getServicesPanel(),
Containers: gui.getContainersPanel(),
Images: gui.getImagesPanel(),
Volumes: gui.getVolumesPanel(),
@ -291,18 +255,13 @@ func (gui *Gui) setPanels() {
}
}
func (gui *Gui) updateContainerDetails() error {
return gui.DockerCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems())
func (gui *Gui) renderContainers() error {
return gui.Panels.Containers.RerenderList()
}
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 {
if err := gui.refreshContainers(); err != nil {
gui.Log.Error(err)
}
}()
@ -323,61 +282,6 @@ func (gui *Gui) refresh() {
}()
}
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())
}
@ -404,8 +308,6 @@ func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
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()
@ -459,9 +361,6 @@ func (gui *Gui) ShouldRefresh(key string) bool {
}
func (gui *Gui) initiallyFocusedViewName() string {
if gui.DockerCommand.InDockerComposeProject {
return "services"
}
return "containers"
}
@ -474,8 +373,6 @@ func (gui *Gui) Update(f func() error) {
}
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 {
@ -485,16 +382,13 @@ func (gui *Gui) monitorContainerStats(ctx context.Context) {
case <-ticker.C:
for _, container := range gui.Panels.Containers.List.GetAllItems() {
if !container.MonitoringStats {
go gui.DockerCommand.CreateClientStatMonitor(container)
go gui.ContainerCmd.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,

View file

@ -1,21 +1,14 @@
package gui
import (
"fmt"
"strings"
"time"
"github.com/docker/docker/api/types/image"
"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"
"github.com/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
@ -42,23 +35,19 @@ func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
},
NoItemsMessage: gui.Tr.NoImages,
Gui: gui.intoInterface(),
Sort: func(a *commands.Image, b *commands.Image) bool {
Sort: func(a, 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,
@ -69,7 +58,7 @@ 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
Wrap: false,
})
}
@ -77,18 +66,11 @@ 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
output += utils.WithPadding("Tag: ", padding) + image.Tag + "\n"
output += utils.WithPadding("ID: ", padding) + image.ID + "\n"
output += utils.WithPadding("Reference: ", padding) + image.AppleImage.Reference + "\n"
output += utils.WithPadding("Digest: ", padding) + image.AppleImage.Descriptor.Digest + "\n"
output += utils.WithPadding("Size: ", padding) + image.AppleImage.FullSize + "\n"
return output
}
@ -96,82 +78,51 @@ func (gui *Gui) reloadImages() error {
if err := gui.refreshStateImages(); err != nil {
return err
}
return gui.Panels.Images.RerenderList()
}
func (gui *Gui) refreshStateImages() error {
images, err := gui.DockerCommand.RefreshImages()
images, err := gui.ContainerCmd.RefreshImages()
if err != nil {
return err
}
gui.Panels.Images.SetItems(images)
return nil
}
func (gui *Gui) FilterString(view *gocui.View) string {
if gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() != view {
return ""
}
return gui.State.Filter.needle
}
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
type removeImageOption struct {
description string
command string
configOptions image.RemoveOptions
}
img, err := gui.Panels.Images.GetSelectedItem()
if err != nil {
return nil
}
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: image.RemoveOptions{PruneChildren: true, Force: false},
},
{
description: gui.Tr.RemoveWithoutPrune,
command: "docker image rm --no-prune " + shortSha,
configOptions: image.RemoveOptions{PruneChildren: false, Force: 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},
},
shortSha := img.ID
if len(img.ID) > 12 {
shortSha = img.ID[:12]
}
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: []string{
option.description,
color.New(color.FgRed).Sprint(option.command),
},
options := []struct {
description string
command string
force bool
}{
{description: gui.Tr.Remove, command: "container image rm " + shortSha, force: false},
{description: gui.Tr.RemoveWithForce, command: "container image rm --force " + shortSha, force: true},
}
menuItems := make([]*types.MenuItem, len(options))
for i, opt := range options {
opt := opt
menuItems[i] = &types.MenuItem{
LabelColumns: []string{opt.description, color.New(color.FgRed).Sprint(opt.command)},
OnPress: func() error {
if err := img.Remove(option.configOptions); err != nil {
if err := img.Remove(opt.force); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
},
}
})
}
return gui.Menu(CreateMenuOptions{
Title: "",
@ -179,10 +130,11 @@ func (gui *Gui) handleImagesRemoveMenu(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 {
func (gui *Gui) handlePruneImages(all bool) error {
title := gui.Tr.ConfirmPruneImages
return gui.createConfirmationPanel(gui.Tr.Confirm, title, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneImages()
err := gui.ContainerCmd.Client.PruneImages(all)
if err != nil {
return gui.createErrorPanel(err.Error())
}
@ -197,25 +149,13 @@ func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Image: img,
})
commandObject := gui.ContainerCmd.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{})
bulkCommands := gui.Config.UserConfig.BulkCommands.Images
commandObject := gui.ContainerCmd.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -6,18 +6,14 @@ import (
"github.com/jesseduffield/gocui"
)
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
// is only handled if the given view has focus, or handled globally if the view
// is ""
type Binding struct {
ViewName string
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Key interface{}
Modifier gocui.Modifier
Description string
}
// GetKey is a function.
func (b *Binding) GetKey() string {
key := 0
@ -28,7 +24,6 @@ func (b *Binding) GetKey() string {
key = int(b.Key.(gocui.Key))
}
// special keys
switch key {
case 27:
return "esc"
@ -53,7 +48,6 @@ func (b *Binding) GetKey() string {
return fmt.Sprintf("%c", key)
}
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
bindings := []*Binding{
{
@ -128,27 +122,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleCustomCommand,
},
{
ViewName: "project",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleEditConfig,
Description: gui.Tr.EditConfig,
},
{
ViewName: "project",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleOpenConfig,
Description: gui.Tr.OpenConfig,
},
{
ViewName: "project",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleViewAllLogs,
Description: gui.Tr.ViewLogs,
},
{
ViewName: "menu",
Key: gocui.KeyEsc,
@ -199,13 +172,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
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',
@ -263,109 +229,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.OpenInBrowser,
},
{
ViewName: "services",
Key: 'u',
ViewName: "containers",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleServiceUp,
Description: gui.Tr.UpService,
},
{
ViewName: "services",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleServiceRemoveMenu,
Description: gui.Tr.RemoveService,
},
{
ViewName: "services",
Key: 's',
Modifier: gocui.ModNone,
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',
Modifier: gocui.ModNone,
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',
Modifier: gocui.ModNone,
Handler: gui.handleServiceAttach,
Description: gui.Tr.Attach,
},
{
ViewName: "services",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleServiceRenderLogsToMain,
Description: gui.Tr.ViewLogs,
},
{
ViewName: "services",
Key: 'U',
Modifier: gocui.ModNone,
Handler: gui.handleProjectUp,
Description: gui.Tr.UpProject,
},
{
ViewName: "services",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleProjectDown,
Description: gui.Tr.DownProject,
},
{
ViewName: "services",
Key: 'R',
Modifier: gocui.ModNone,
Handler: gui.handleServiceRestartMenu,
Description: gui.Tr.ViewRestartOptions,
},
{
ViewName: "services",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleServicesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "services",
Key: 'b',
Modifier: gocui.ModNone,
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,
Handler: gui.handleContainerKill,
Description: gui.Tr.Kill,
},
{
ViewName: "images",
@ -535,12 +403,10 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}
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},
{Handler: gui.handleGoTo(gui.Panels.Containers.View), Key: '1', Description: gui.Tr.FocusContainers},
{Handler: gui.handleGoTo(gui.Panels.Images.View), Key: '2', Description: gui.Tr.FocusImages},
{Handler: gui.handleGoTo(gui.Panels.Volumes.View), Key: '3', Description: gui.Tr.FocusVolumes},
{Handler: gui.handleGoTo(gui.Panels.Networks.View), Key: '4', Description: gui.Tr.FocusNetworks},
}...)
for _, panel := range gui.allListPanels() {

View file

@ -1,10 +1,10 @@
package gui
import (
"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"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
type CreateMenuOptions struct {

View file

@ -1,17 +1,15 @@
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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/samber/lo"
)
@ -37,9 +35,6 @@ func (gui *Gui) getNetworksPanel() *panels.SideListPanel[*commands.Network] {
},
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
},
@ -54,28 +49,14 @@ func (gui *Gui) renderNetworkConfig(network *commands.Network) tasks.TaskFunc {
func (gui *Gui) networkConfigStr(network *commands.Network) string {
padding := 15
output := ""
output += utils.WithPadding("ID: ", padding) + network.Network.ID + "\n"
output += utils.WithPadding("ID: ", padding) + network.AppleNetwork.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 += utils.WithPadding("Mode: ", padding) + network.AppleNetwork.Config.Mode + "\n"
output += utils.WithPadding("State: ", padding) + network.AppleNetwork.State + "\n"
output += utils.WithPadding("IPv4 Subnet: ", padding) + network.AppleNetwork.Status.IPv4Subnet + "\n"
output += utils.WithPadding("IPv4 Gateway: ", padding) + network.AppleNetwork.Status.IPv4Gateway + "\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)
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, network.AppleNetwork.Config.Labels)
return output
}
@ -89,7 +70,7 @@ func (gui *Gui) reloadNetworks() error {
}
func (gui *Gui) refreshStateNetworks() error {
networks, err := gui.DockerCommand.RefreshNetworks()
networks, err := gui.ContainerCmd.RefreshNetworks()
if err != nil {
return err
}
@ -113,7 +94,7 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
options := []*removeNetworkOption{
{
description: gui.Tr.Remove,
command: utils.WithShortSha("docker network rm " + network.Name),
command: utils.WithShortSha("container network rm " + network.Name),
},
}
@ -140,7 +121,7 @@ func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
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()
err := gui.ContainerCmd.PruneNetworks()
if err != nil {
return gui.createErrorPanel(err.Error())
}
@ -155,7 +136,7 @@ func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{
Network: network,
})
@ -173,7 +154,7 @@ func (gui *Gui) handleNetworksBulkCommand(g *gocui.Gui, v *gocui.View) error {
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Networks...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -4,7 +4,7 @@ import (
"github.com/samber/lo"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {

View file

@ -1,7 +1,17 @@
package gui
import "github.com/jesseduffield/lazydocker/pkg/gui/panels"
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
)
func (gui *Gui) intoInterface() panels.IGui {
return gui
}
func (gui *Gui) FilterString(view *gocui.View) string {
if gui.State.Filter.active && gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() == view {
return gui.State.Filter.needle
}
return ""
}

View file

@ -1,7 +1,7 @@
package panels
import (
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/samber/lo"
)

View file

@ -7,8 +7,8 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/samber/lo"
)

View file

@ -10,9 +10,9 @@ import (
"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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/mcuadros/go-lookup"
"github.com/samber/lo"
)
@ -33,9 +33,9 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v
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))
pidsCount := fmt.Sprintf("Processes: %d", stats.ClientStats.NumProcesses)
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(int(stats.ClientStats.NetworkRxBytes)))
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(int(stats.ClientStats.NetworkTxBytes)))
originalStats, err := utils.MarshalIntoYaml(stats)
if err != nil {
@ -53,7 +53,6 @@ func RenderStats(userConfig *config.UserConfig, container *commands.Container, v
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()
@ -105,7 +104,6 @@ func plotGraph(container *commands.Container, spec config.GraphConfig, width int
), 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("")

View file

@ -3,14 +3,12 @@ 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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/samber/lo"
)
@ -26,34 +24,36 @@ func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands
}
func displayContainerImage(container *commands.Container) string {
return strings.TrimPrefix(container.Container.Image, "sha256:")
return strings.TrimPrefix(container.GetImage(), "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)
ports := c.AppleContainer.Configuration.PublishedPorts
portStrings := lo.Map(ports, func(port struct {
HostIP string `json:"hostIP"`
HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"`
Protocol string `json:"protocol"`
}, _ int) string {
if port.HostPort == 0 {
return fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)
}
// 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 + ":"
if port.HostIP != "0.0.0.0" && port.HostIP != "" {
ipString = port.HostIP + ":"
}
return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type)
return fmt.Sprintf("%s%d->%d/%s", ipString, port.HostPort, port.ContainerPort, port.Protocol)
})
// 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 {
status := c.GetStatus()
shortStatusMap := map[string]string{
"paused": "P",
"exited": "X",
@ -62,6 +62,7 @@ func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Containe
"restarting": "RS",
"running": "R",
"dead": "D",
"stopped": "S",
}
iconStatusMap := map[string]rune{
@ -72,33 +73,34 @@ func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Containe
"restarting": '⟳',
"running": '▶',
"dead": '!',
"stopped": '■',
}
var containerState string
switch guiConfig.ContainerStatusHealthStyle {
case "short":
containerState = shortStatusMap[c.Container.State]
containerState = shortStatusMap[status]
case "icon":
containerState = string(iconStatusMap[c.Container.State])
containerState = string(iconStatusMap[status])
case "long":
fallthrough
default:
containerState = c.Container.State
containerState = status
}
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":
status := c.GetStatus()
switch status {
case "exited", "stopped":
return utils.ColoredString(
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c),
fmt.Sprintf("(%d)", 0), getContainerColor(c),
)
case "running":
return getHealthStatus(guiConfig, c)
@ -108,51 +110,9 @@ func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Conta
}
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 {
@ -174,16 +134,11 @@ func getDisplayCPUPerc(c *commands.Container) string {
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
status := c.GetStatus()
switch status {
case "exited", "stopped":
return color.FgYellow
case "created":
return color.FgCyan
case "running":

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,230 +0,0 @@
package gui
import (
"bytes"
"context"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/yaml"
)
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
return &panels.SideListPanel[*commands.Project]{
ContextState: &panels.ContextState[*commands.Project]{
GetMainTabs: func() []panels.MainTab[*commands.Project] {
return []panels.MainTab[*commands.Project]{
{
Key: "logs",
Title: gui.Tr.LogsTitle,
Render: gui.renderAllLogs,
},
{
Key: "config",
Title: gui.Tr.DockerComposeConfigTitle,
Render: gui.renderDockerComposeConfig,
},
{
Key: "credits",
Title: gui.Tr.CreditsTitle,
Render: gui.renderCredits,
},
}
},
GetItemContextCacheKey: func(project *commands.Project) string {
return "projects-" + project.Name
},
},
ListPanel: panels.ListPanel[*commands.Project]{
List: panels.NewFilteredList[*commands.Project](),
View: gui.Views.Project,
},
NoItemsMessage: "",
Gui: gui.intoInterface(),
Sort: func(a *commands.Project, b *commands.Project) bool {
return a.Name < b.Name
},
GetTableCells: presentation.GetProjectDisplayStrings,
OnSelect: func(project *commands.Project) error {
// When a different project is selected, re-filter services and
// containers to show only those belonging to the selected project.
return gui.renderContainersAndServices()
},
}
}
func (gui *Gui) refreshProject() error {
projects := gui.getDiscoveredProjects()
// Preserve the current selection across refreshes. On the first refresh,
// select the project specified via -p flag, or fall back to the local project.
selectedName := gui.getSelectedProjectName()
if selectedName == "" {
if gui.Config.ProjectName != "" {
selectedName = gui.Config.ProjectName
} else {
selectedName = gui.DockerCommand.LocalProjectName
}
}
gui.Panels.Projects.SetItems(projects)
if selectedName != "" {
for i, p := range gui.Panels.Projects.List.GetItems() {
if p.Name == selectedName {
gui.Panels.Projects.SetSelectedLineIdx(i)
gui.Panels.Projects.Refocus()
break
}
}
}
return gui.Panels.Projects.RerenderList()
}
// getDiscoveredProjects returns all docker compose projects by examining container labels.
// The local project (from docker-compose.yml in the current directory) is included if
// it has running containers or if InDockerComposeProject is true.
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
containers := gui.Panels.Containers.List.GetAllItems()
projectNames := gui.DockerCommand.GetProjectNames(containers)
// If we're in a docker compose project but it has no running containers,
// still include it. We don't fall back to the directory name here to avoid
// briefly flashing the wrong project name on startup.
localName := gui.DockerCommand.LocalProjectName
if gui.DockerCommand.InDockerComposeProject && localName != "" {
found := false
for _, name := range projectNames {
if name == localName {
found = true
break
}
}
if !found {
projectNames = append([]string{localName}, projectNames...)
}
}
projects := make([]*commands.Project, len(projectNames))
for i, name := range projectNames {
projects[i] = &commands.Project{Name: name}
}
return projects
}
// getSelectedProjectName returns the name of the currently selected project,
// or empty string if none is selected.
func (gui *Gui) getSelectedProjectName() string {
project, err := gui.Panels.Projects.GetSelectedItem()
if err != nil {
return ""
}
return project.Name
}
func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.creditsStr() })
}
func (gui *Gui) creditsStr() string {
var configBuf bytes.Buffer
_ = yaml.NewEncoder(&configBuf, yaml.IncludeOmitted).Encode(gui.Config.UserConfig)
return strings.Join(
[]string{
lazydockerTitle(),
"Copyright (c) 2019 Jesse Duffield",
"Keybindings: https://github.com/jesseduffield/lazydocker/blob/master/docs/keybindings",
"Config Options: https://github.com/jesseduffield/lazydocker/blob/master/docs/Config.md",
"Raise an Issue: https://github.com/jesseduffield/lazydocker/issues",
utils.ColoredString("Buy Jesse a coffee: https://github.com/sponsors/jesseduffield", color.FgMagenta), // caffeine ain't free
"Here's your lazydocker config when merged in with the defaults (you can open your config by pressing 'o'):",
utils.ColoredYamlString(configBuf.String()),
}, "\n\n")
}
func (gui *Gui) renderAllLogs(project *commands.Project) tasks.TaskFunc {
return gui.NewTask(TaskOpts{
Autoscroll: true,
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Func: func(ctx context.Context) {
gui.clearMainView()
cmd := gui.OSCommand.RunCustomCommand(
utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.AllLogs,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
),
)
cmd.Stdout = gui.Views.Main
cmd.Stderr = gui.Views.Main
gui.OSCommand.PrepareForChildren(cmd)
_ = cmd.Start()
go func() {
<-ctx.Done()
if err := gui.OSCommand.Kill(cmd); err != nil {
gui.Log.Error(err)
}
}()
_ = cmd.Wait()
},
})
}
func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc {
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
return gui.NewSimpleRenderStringTask(func() string {
return "Compose config is not available for non-local projects"
})
}
return gui.NewSimpleRenderStringTask(func() string {
return utils.ColoredYamlString(gui.DockerCommand.DockerComposeConfigForProject(project))
})
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
return gui.openFile(gui.Config.ConfigFilename())
}
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
return gui.editFile(gui.Config.ConfigFilename())
}
func lazydockerTitle() string {
return `
_ _ _
| | | | | |
| | __ _ _____ _ __| | ___ ___| | _____ _ __
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` |/ _ \ / __| |/ / _ \ '__|
| | (_| |/ /| |_| | (_| | (_) | (__| < __/ |
|_|\__,_/___|\__, |\__,_|\___/ \___|_|\_\___|_|
__/ |
|___/
`
}
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
project, _ := gui.Panels.Projects.GetSelectedItem()
c, err := gui.DockerCommand.ViewAllLogs(project)
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocess(c)
}

View file

@ -1,559 +0,0 @@
package gui
import (
"context"
"fmt"
"time"
"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) getServicesPanel() *panels.SideListPanel[*commands.Service] {
return &panels.SideListPanel[*commands.Service]{
ContextState: &panels.ContextState[*commands.Service]{
GetMainTabs: func() []panels.MainTab[*commands.Service] {
return []panels.MainTab[*commands.Service]{
{
Key: "logs",
Title: gui.Tr.LogsTitle,
Render: gui.renderServiceLogs,
},
{
Key: "stats",
Title: gui.Tr.StatsTitle,
Render: gui.renderServiceStats,
},
{
Key: "container-env",
Title: gui.Tr.ContainerEnvTitle,
Render: gui.renderServiceContainerEnv,
},
{
Key: "container-config",
Title: gui.Tr.ContainerConfigTitle,
Render: gui.renderServiceContainerConfig,
},
{
Key: "top",
Title: gui.Tr.TopTitle,
Render: gui.renderServiceTop,
},
}
},
GetItemContextCacheKey: func(service *commands.Service) string {
if service.Container == nil {
return "services-" + service.ID
}
return "services-" + service.ID + "-" + service.Container.ID + "-" + service.Container.Container.State
},
},
ListPanel: panels.ListPanel[*commands.Service]{
List: panels.NewFilteredList[*commands.Service](),
View: gui.Views.Services,
},
NoItemsMessage: gui.Tr.NoServices,
Gui: gui.intoInterface(),
// sort services first by whether they have a linked container, and second by alphabetical order
Sort: func(a *commands.Service, b *commands.Service) bool {
if a.Container != nil && b.Container == nil {
return true
}
if a.Container == nil && b.Container != nil {
return false
}
return a.Name < b.Name
},
Filter: func(service *commands.Service) bool {
selectedProject := gui.getSelectedProjectName()
if selectedProject == "" {
// Before any project is selected (e.g. startup), default to
// the local project so we don't briefly flash all services.
selectedProject = gui.DockerCommand.LocalProjectName
}
if selectedProject == "" {
return true
}
return service.ProjectName == selectedProject
},
GetTableCells: func(service *commands.Service) []string {
return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service)
},
Hide: func() bool {
// Show services panel if there are any compose projects (local or discovered)
return !gui.DockerCommand.InDockerComposeProject && len(gui.Panels.Services.List.GetAllItems()) == 0
},
}
}
func (gui *Gui) renderServiceContainerConfig(service *commands.Service) tasks.TaskFunc {
if service.Container == nil {
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer })
}
return gui.renderContainerConfig(service.Container)
}
func (gui *Gui) renderServiceContainerEnv(service *commands.Service) tasks.TaskFunc {
if service.Container == nil {
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer })
}
return gui.renderContainerEnv(service.Container)
}
func (gui *Gui) renderServiceStats(service *commands.Service) tasks.TaskFunc {
if service.Container == nil {
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer })
}
return gui.renderContainerStats(service.Container)
}
func (gui *Gui) renderServiceTop(service *commands.Service) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
contents, err := service.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) renderServiceLogs(service *commands.Service) tasks.TaskFunc {
if service.Container == nil {
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainerForService })
}
return gui.renderContainerLogsToMain(service.Container)
}
type commandOption struct {
description string
command string
onPress func() error
}
func (r *commandOption) getDisplayStrings() []string {
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
}
// isServiceFromLocalProject returns true if the given service belongs to the
// local compose project (the one whose compose file is in the current directory).
// Compose commands like up/stop/restart only work for local project services.
func (gui *Gui) isServiceFromLocalProject(service *commands.Service) bool {
return service.ProjectName == gui.DockerCommand.LocalProjectName
}
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if !gui.isServiceFromLocalProject(service) {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
composeCommand := gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}).DockerCompose
options := []*commandOption{
{
description: gui.Tr.Remove,
command: fmt.Sprintf("%s rm --stop --force %s", composeCommand, service.Name),
},
{
description: gui.Tr.RemoveWithVolumes,
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
},
}
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: option.getDisplayStrings(),
OnPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := gui.OSCommand.RunCommand(option.command); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
}
})
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
}
func (gui *Gui) handleServicePause(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if service.Container == nil {
return nil
}
return gui.PauseContainer(service.Container)
}
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if !gui.isServiceFromLocalProject(service) {
if service.Container == nil {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
return service.Container.Stop()
}
if err := service.Stop(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}, nil)
}
func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if !gui.isServiceFromLocalProject(service) {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
if err := service.Up(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if !gui.isServiceFromLocalProject(service) {
if service.Container == nil {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
return service.Container.Restart()
}
if err := service.Restart(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if !gui.isServiceFromLocalProject(service) {
if service.Container == nil {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
return service.Container.Start()
})
}
return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
if err := service.Start(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}
func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if service.Container == nil {
return gui.createErrorPanel(gui.Tr.NoContainers)
}
c, err := service.Attach()
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocess(c)
}
func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
c, err := service.ViewLogs()
if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocess(c)
}
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
project, _ := gui.Panels.Projects.GetSelectedItem()
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
cmdStr := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Up,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
)
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
}, nil)
}
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
project, _ := gui.Panels.Projects.GetSelectedItem()
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
downCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.Down,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
)
downWithVolumesCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
)
options := []*commandOption{
{
description: gui.Tr.Down,
command: downCommand,
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
if err := gui.OSCommand.RunCommand(downCommand); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
},
{
description: gui.Tr.DownWithVolumes,
command: downWithVolumesCommand,
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
},
}
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: option.getDisplayStrings(),
OnPress: option.onPress,
}
})
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
}
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
if !gui.isServiceFromLocalProject(service) {
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
}
rebuildCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
)
recreateCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
)
options := []*commandOption{
{
description: gui.Tr.Restart,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RestartService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := service.Restart(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
},
{
description: gui.Tr.Recreate,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
})
},
},
{
description: gui.Tr.Rebuild,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
onPress: func() error {
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
},
},
}
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
return &types.MenuItem{
LabelColumns: option.getDisplayStrings(),
OnPress: option.onPress,
}
})
return gui.Menu(CreateMenuOptions{
Title: "",
Items: menuItems,
})
}
func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Service: service,
Container: service.Container,
})
var customCommands []config.CustomCommand
customServiceCommands := gui.Config.UserConfig.CustomCommands.Services
// we only include service commands if they have no serviceNames defined or if our service happens to be one of the serviceNames defined
L:
for _, cmd := range customServiceCommands {
if len(cmd.ServiceNames) == 0 {
customCommands = append(customCommands, cmd)
continue L
}
for _, serviceName := range cmd.ServiceNames {
if serviceName == service.Name {
// appending these to the top given they're more likely to be selected
customCommands = append([]config.CustomCommand{cmd}, customCommands...)
continue L
}
}
}
if service.Container != nil {
customCommands = append(customCommands, gui.Config.UserConfig.CustomCommands.Containers...)
}
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
project, _ := gui.Panels.Projects.GetSelectedItem()
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}
func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
container := service.Container
if container == nil {
return gui.createErrorPanel(gui.Tr.NoContainers)
}
return gui.containerExecShell(container)
}
func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
service, err := gui.Panels.Services.GetSelectedItem()
if err != nil {
return nil
}
container := service.Container
if container == nil {
return gui.createErrorPanel(gui.Tr.NoContainers)
}
return gui.openContainerInBrowser(container)
}

View file

@ -4,40 +4,31 @@ import (
"sort"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/commands"
"github.com/stretchr/testify/assert"
)
func sampleContainers() []*commands.Container {
return []*commands.Container{
{
ID: "1",
Name: "1",
Container: container.Summary{
State: "exited",
},
ID: "1",
Name: "1",
AppleContainer: commands.AppleContainer{Status: "exited"},
},
{
ID: "2",
Name: "2",
Container: container.Summary{
State: "running",
},
ID: "2",
Name: "2",
AppleContainer: commands.AppleContainer{Status: "running"},
},
{
ID: "3",
Name: "3",
Container: container.Summary{
State: "running",
},
ID: "3",
Name: "3",
AppleContainer: commands.AppleContainer{Status: "running"},
},
{
ID: "4",
Name: "4",
Container: container.Summary{
State: "created",
},
ID: "4",
Name: "4",
AppleContainer: commands.AppleContainer{Status: "created"},
},
}
}
@ -45,32 +36,24 @@ func sampleContainers() []*commands.Container {
func expectedPerStatusContainers() []*commands.Container {
return []*commands.Container{
{
ID: "2",
Name: "2",
Container: container.Summary{
State: "running",
},
ID: "2",
Name: "2",
AppleContainer: commands.AppleContainer{Status: "running"},
},
{
ID: "3",
Name: "3",
Container: container.Summary{
State: "running",
},
ID: "3",
Name: "3",
AppleContainer: commands.AppleContainer{Status: "running"},
},
{
ID: "1",
Name: "1",
Container: container.Summary{
State: "exited",
},
ID: "1",
Name: "1",
AppleContainer: commands.AppleContainer{Status: "exited"},
},
{
ID: "4",
Name: "4",
Container: container.Summary{
State: "created",
},
ID: "4",
Name: "4",
AppleContainer: commands.AppleContainer{Status: "created"},
},
}
}
@ -78,40 +61,32 @@ func expectedPerStatusContainers() []*commands.Container {
func expectedLegacySortedContainers() []*commands.Container {
return []*commands.Container{
{
ID: "1",
Name: "1",
Container: container.Summary{
State: "exited",
},
ID: "1",
Name: "1",
AppleContainer: commands.AppleContainer{Status: "exited"},
},
{
ID: "2",
Name: "2",
Container: container.Summary{
State: "running",
},
ID: "2",
Name: "2",
AppleContainer: commands.AppleContainer{Status: "running"},
},
{
ID: "3",
Name: "3",
Container: container.Summary{
State: "running",
},
ID: "3",
Name: "3",
AppleContainer: commands.AppleContainer{Status: "running"},
},
{
ID: "4",
Name: "4",
Container: container.Summary{
State: "created",
},
ID: "4",
Name: "4",
AppleContainer: commands.AppleContainer{Status: "created"},
},
}
}
func assertEqualContainers(t *testing.T, left *commands.Container, right *commands.Container) {
t.Helper()
assert.Equal(t, left.Container.State, right.Container.State)
assert.Equal(t, left.Container.ID, right.Container.ID)
assert.Equal(t, left.GetStatus(), right.GetStatus())
assert.Equal(t, left.ID, right.ID)
assert.Equal(t, left.Name, right.Name)
}

View file

@ -9,7 +9,7 @@ import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/utils"
)
func (gui *Gui) runSubprocess(cmd *exec.Cmd) error {

View file

@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
)
func (gui *Gui) QueueTask(f func(ctx context.Context)) error {

View file

@ -6,8 +6,8 @@ import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/samber/lo"
"github.com/spkg/bom"
)
@ -364,8 +364,6 @@ func (gui *Gui) currentListPanel() (panels.ISideListPanel, bool) {
func (gui *Gui) allSidePanels() []panels.ISideListPanel {
return []panels.ISideListPanel{
gui.Panels.Projects,
gui.Panels.Services,
gui.Panels.Containers,
gui.Panels.Images,
gui.Panels.Volumes,

View file

@ -8,9 +8,6 @@ import (
"github.com/samber/lo"
)
// See https://github.com/xtermjs/xterm.js/issues/4238
// VSCode is soon to fix this in an upcoming update.
// Once that's done, we can scrap the HIDE_UNDERSCORES variable
var (
underscoreEnvChecked bool
hideUnderscores bool
@ -26,48 +23,33 @@ func hideUnderScores() bool {
}
type Views struct {
// side panels
Project *gocui.View
Services *gocui.View
Containers *gocui.View
Images *gocui.View
Volumes *gocui.View
Networks *gocui.View
// main panel
Main *gocui.View
// bottom line
Options *gocui.View
Information *gocui.View
AppStatus *gocui.View
// text that prompts you to enter text in the Filter view
FilterPrefix *gocui.View
// appears next to the SearchPrefix view, it's where you type in the search string
Filter *gocui.View
// popups
Confirmation *gocui.View
Menu *gocui.View
// will cover everything when it appears
Limit *gocui.View
}
type viewNameMapping struct {
viewPtr **gocui.View
name string
// if true, we handle the position/size of the view in arrangement.go. Otherwise
// we handle it manually.
autoPosition bool
}
func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
return []viewNameMapping{
// first layer. Ordering within this layer does not matter because there are
// no overlapping views
{viewPtr: &gui.Views.Project, name: "project", autoPosition: true},
{viewPtr: &gui.Views.Services, name: "services", autoPosition: true},
{viewPtr: &gui.Views.Containers, name: "containers", autoPosition: true},
{viewPtr: &gui.Views.Images, name: "images", autoPosition: true},
{viewPtr: &gui.Views.Volumes, name: "volumes", autoPosition: true},
@ -75,18 +57,15 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
{viewPtr: &gui.Views.Main, name: "main", autoPosition: true},
// bottom line
{viewPtr: &gui.Views.Options, name: "options", autoPosition: true},
{viewPtr: &gui.Views.AppStatus, name: "appStatus", autoPosition: true},
{viewPtr: &gui.Views.Information, name: "information", autoPosition: true},
{viewPtr: &gui.Views.Filter, name: "filter", autoPosition: true},
{viewPtr: &gui.Views.FilterPrefix, name: "filterPrefix", autoPosition: true},
// popups.
{viewPtr: &gui.Views.Menu, name: "menu", autoPosition: false},
{viewPtr: &gui.Views.Confirmation, name: "confirmation", autoPosition: false},
// this guy will cover everything else when it appears
{viewPtr: &gui.Views.Limit, name: "limit", autoPosition: true},
}
}
@ -115,41 +94,26 @@ func (gui *Gui) createAllViews() error {
selectedLineBgColor := GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.SelectedLineBgColor)
gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Project.Title = gui.Tr.ProjectTitle
gui.Views.Project.TitlePrefix = "[1]"
gui.Views.Project.Highlight = true
gui.Views.Project.SelBgColor = selectedLineBgColor
gui.Views.Services.Highlight = true
gui.Views.Services.Title = gui.Tr.ServicesTitle
gui.Views.Services.TitlePrefix = "[2]"
gui.Views.Services.SelBgColor = selectedLineBgColor
gui.Views.Containers.Highlight = true
gui.Views.Containers.SelBgColor = selectedLineBgColor
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
gui.Views.Containers.Title = gui.Tr.ContainersTitle
} else {
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
}
gui.Views.Containers.TitlePrefix = "[3]"
gui.Views.Containers.Title = gui.Tr.ContainersTitle
gui.Views.Containers.TitlePrefix = "[1]"
gui.Views.Images.Highlight = true
gui.Views.Images.Title = gui.Tr.ImagesTitle
gui.Views.Images.SelBgColor = selectedLineBgColor
gui.Views.Images.TitlePrefix = "[4]"
gui.Views.Images.TitlePrefix = "[2]"
gui.Views.Volumes.Highlight = true
gui.Views.Volumes.Title = gui.Tr.VolumesTitle
gui.Views.Volumes.TitlePrefix = "[5]"
gui.Views.Volumes.TitlePrefix = "[3]"
gui.Views.Volumes.SelBgColor = selectedLineBgColor
gui.Views.Networks.Highlight = true
gui.Views.Networks.Title = gui.Tr.NetworksTitle
gui.Views.Networks.TitlePrefix = "[6]"
gui.Views.Networks.TitlePrefix = "[4]"
gui.Views.Networks.SelBgColor = selectedLineBgColor
gui.Views.Options.Frame = false
@ -212,7 +176,6 @@ func (gui *Gui) popupViewNames() []string {
return []string{"confirmation", "menu"}
}
// these views have their position and size determined by arrangement.go
func (gui *Gui) autoPositionedViewNames() []string {
views := lo.Filter(gui.orderedViewNameMappings(), func(viewNameMapping viewNameMapping, _ int) bool {
return viewNameMapping.autoPosition

View file

@ -1,17 +1,15 @@
package gui
import (
"fmt"
"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/jesseduffield/lazycontainer/pkg/commands"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/gui/panels"
"github.com/jesseduffield/lazycontainer/pkg/gui/presentation"
"github.com/jesseduffield/lazycontainer/pkg/gui/types"
"github.com/jesseduffield/lazycontainer/pkg/tasks"
"github.com/jesseduffield/lazycontainer/pkg/utils"
"github.com/samber/lo"
)
@ -37,16 +35,7 @@ func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] {
},
NoItemsMessage: gui.Tr.NoVolumes,
Gui: gui.intoInterface(),
// we're sorting these volumes based on whether they have labels defined,
// because those are the ones you typically care about.
// Within that, we also sort them alphabetically
Sort: func(a *commands.Volume, b *commands.Volume) bool {
if len(a.Volume.Labels) == 0 && len(b.Volume.Labels) > 0 {
return false
}
if len(a.Volume.Labels) > 0 && len(b.Volume.Labels) == 0 {
return true
}
return a.Name < b.Name
},
GetTableCells: presentation.GetVolumeDisplayStrings,
@ -61,26 +50,7 @@ func (gui *Gui) volumeConfigStr(volume *commands.Volume) string {
padding := 15
output := ""
output += utils.WithPadding("Name: ", padding) + volume.Name + "\n"
output += utils.WithPadding("Driver: ", padding) + volume.Volume.Driver + "\n"
output += utils.WithPadding("Scope: ", padding) + volume.Volume.Scope + "\n"
output += utils.WithPadding("Mountpoint: ", padding) + volume.Volume.Mountpoint + "\n"
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, volume.Volume.Labels) + "\n"
output += utils.WithPadding("Options: ", padding) + utils.FormatMap(padding, volume.Volume.Options) + "\n"
output += utils.WithPadding("Status: ", padding)
if volume.Volume.Status != nil {
output += "\n"
for k, v := range volume.Volume.Status {
output += utils.FormatMapItem(padding, k, v)
}
} else {
output += "n/a"
}
if volume.Volume.UsageData != nil {
output += utils.WithPadding("RefCount: ", padding) + fmt.Sprintf("%d", volume.Volume.UsageData.RefCount) + "\n"
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.Volume.UsageData.Size)) + "\n"
}
output += utils.WithPadding("Size: ", padding) + utils.FormatBinaryBytes(int(volume.AppleVolume.SizeInBytes)) + "\n"
return output
}
@ -94,7 +64,7 @@ func (gui *Gui) reloadVolumes() error {
}
func (gui *Gui) refreshStateVolumes() error {
volumes, err := gui.DockerCommand.RefreshVolumes()
volumes, err := gui.ContainerCmd.RefreshVolumes()
if err != nil {
return err
}
@ -113,19 +83,12 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
type removeVolumeOption struct {
description string
command string
force bool
}
options := []*removeVolumeOption{
{
description: gui.Tr.Remove,
command: utils.WithShortSha("docker volume rm " + volume.Name),
force: false,
},
{
description: gui.Tr.ForceRemove,
command: utils.WithShortSha("docker volume rm --force " + volume.Name),
force: true,
command: utils.WithShortSha("container volume rm " + volume.Name),
},
}
@ -134,7 +97,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
LabelColumns: []string{option.description, color.New(color.FgRed).Sprint(option.command)},
OnPress: func() error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := volume.Remove(option.force); err != nil {
if err := volume.Remove(); err != nil {
return gui.createErrorPanel(err.Error())
}
return nil
@ -152,7 +115,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handlePruneVolumes() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneVolumes()
err := gui.ContainerCmd.PruneVolumes()
if err != nil {
return gui.createErrorPanel(err.Error())
}
@ -167,7 +130,7 @@ func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{
Volume: volume,
})
@ -185,7 +148,7 @@ func (gui *Gui) handleVolumesBulkCommand(g *gocui.Gui, v *gocui.View) error {
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Volumes...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
commandObject := gui.ContainerCmd.Client.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -16,12 +16,12 @@ func chineseSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "没有匹配 newLineFocused switch 语句的视图",
ErrorOccurred: "发生错误!请在 https://github.com/jesseduffield/lazydocker/issues 上创建一个问题",
ErrorOccurred: "发生错误!请在 https://github.com/g-battaglia/lazy-apple-container/issues 上创建一个问题",
ConnectionFailed: "无法连接到 Docker 客户端。您可能需要重新启动 Docker 客户端",
UnattachableContainerError: "容器不支持 attaching。您必须使用“-it”标志运行服务或者在docker-compose.yml文件中使用`stdin_open: truetty: true`",
WaitingForContainerInfo: "在 Docker 给我们更多关于容器的信息之前,无法继续。请几分钟后重试。",
CannotAttachStoppedContainerError: "您不能 attach 到已停止的容器,您需要先启动它(您可以用 'r' 键来执行此操作)(是的,我懒得为您自动执行此操作)(很酷的是,我可以通过错误消息与您进行一对一的通讯)",
CannotAccessDockerSocketError: "无法访问 Docker 套接字unix:///var/run/docker.sock\n请以 root 用户身份运行 lazydocker 或阅读https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "无法访问 Docker 套接字unix:///var/run/docker.sock\n请以 root 用户身份运行 lazyapple 或阅读https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "等待三秒钟以停止子进程。可能有一个孤儿进程在您的系统上继续运行。",
Donate: "捐赠",
@ -37,8 +37,8 @@ func chineseSet() TranslationSet {
Menu: "菜单",
MenuTitle: "菜单",
Scroll: "滚动",
OpenConfig: "打开lazydocker配置",
EditConfig: "编辑lazydocker配置",
OpenConfig: "打开lazyapple配置",
EditConfig: "编辑lazyapple配置",
Cancel: "取消",
Remove: "移除",
HideStopped: "隐藏/显示已停止的容器",
@ -124,7 +124,7 @@ func chineseSet() TranslationSet {
ConfirmPruneNetworks: "您确定要删除所有未使用的网络吗?",
StopService: "您确定要停止此服务的容器吗?",
StopContainer: "您确定要停止此容器吗?",
PressEnterToReturn: "按 enter 返回 lazydocker(您可以在配置文件中设置 `gui.returnImmediately: true` 来禁用此提示)",
PressEnterToReturn: "按 enter 返回 lazyapple(您可以在配置文件中设置 `gui.returnImmediately: true` 来禁用此提示)",
No: "否",
Yes: "是",

View file

@ -10,11 +10,11 @@ func dutchSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "connectie naar de docker client mislukt. Het zou kunnen dat je de docker client moet herstarten",
UnattachableContainerError: "Container heeft geen ondersteuning voor vastmaken. Je zou de service met het '-it' argument kunnen draaien of stop dit in je `stdin_open: true, tty: true` docker-compose.yml",
CannotAttachStoppedContainerError: "Je kan niet een vastgemaakte container stoppen, je moet het eerst starten (dit kan je doen met de 'r' toets) (ja ik ben te leu om dat voor je te doen automatisch)",
CannotAccessDockerSocketError: "Kan de docker socket niet bereiken: unix:///var/run/docker.sock\nDraai lazydocker als root of lees https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "Kan de docker socket niet bereiken: unix:///var/run/docker.sock\nDraai lazyapple als root of lees https://docs.docker.com/install/linux/linux-postinstall/",
Donate: "Doneer",
Confirm: "Bevestigen",
@ -27,8 +27,8 @@ func dutchSet() TranslationSet {
Menu: "menu",
MenuTitle: "Menu",
Scroll: "scroll",
OpenConfig: "open de lazydocker configuratie",
EditConfig: "verander de lazydocker configuratie",
OpenConfig: "open de lazyapple configuratie",
EditConfig: "verander de lazyapple configuratie",
Cancel: "annuleren",
Remove: "verwijder",
HideStopped: "verberg gestopte containers",
@ -92,7 +92,7 @@ func dutchSet() TranslationSet {
ConfirmPruneNetworks: "Weet je zeker dat je alle niet gebruikte networks wil vernietigen?",
StopService: "Weet je zeker dat je deze service zijn containers wil stoppen?",
StopContainer: "Weet je zeker dat je deze container wil stoppen?",
PressEnterToReturn: "Druk op enter om terug te gaan naar lazydocker (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)",
PressEnterToReturn: "Druk op enter om terug te gaan naar lazyapple (Deze popup kan uit gezet worden door in de config dit neer te zetten `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Als u wilt loskoppelen van de container, drukt u standaard op ctrl-p en vervolgens op ctrl-q",
}
}

View file

@ -136,6 +136,14 @@ type TranslationSet struct {
FocusImages string
FocusVolumes string
FocusNetworks string
Kill string
KillingStatus string
RemoveContainer string
ContainerNotRunning string
NoIp string
NoPorts string
NoStats string
}
func englishSet() TranslationSet {
@ -154,12 +162,12 @@ func englishSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "connection to docker client failed. You may need to restart the docker client",
UnattachableContainerError: "Container does not support attaching. You must either run the service with the '-it' flag or use `stdin_open: true, tty: true` in the docker-compose.yml file",
WaitingForContainerInfo: "Cannot proceed until docker gives us more information about the container. Please retry in a few moments.",
CannotAttachStoppedContainerError: "You cannot attach to a stopped container, you need to start it first (which you can actually do with the 'r' key) (yes I'm too lazy to do this automatically for you) (pretty cool that I get to communicate one-on-one with you in the form of an error message though)",
CannotAccessDockerSocketError: "Can't access docker socket at: unix:///var/run/docker.sock\nRun lazydocker as root or read https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "Can't access docker socket at: unix:///var/run/docker.sock\nRun lazyapple as root or read https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "Waited three seconds for child process to stop. There may be an orphan process that continues to run on your system.",
Donate: "Donate",
@ -175,8 +183,8 @@ func englishSet() TranslationSet {
Menu: "menu",
MenuTitle: "Menu",
Scroll: "scroll",
OpenConfig: "open lazydocker config",
EditConfig: "edit lazydocker config",
OpenConfig: "open lazyapple config",
EditConfig: "edit lazyapple config",
Cancel: "cancel",
Remove: "remove",
HideStopped: "hide/show stopped containers",
@ -242,7 +250,7 @@ func englishSet() TranslationSet {
NothingToDisplay: "Nothing to display",
NoContainerForService: "No logs to show; service is not associated with a container",
CannotDisplayEnvVariables: "Something went wrong while displaying environment variables",
CannotManageNonLocalService: "This service belongs to a different compose project. Run lazydocker from that project's directory to manage it.",
CannotManageNonLocalService: "This service belongs to a different compose project. Run lazyapple from that project's directory to manage it.",
NoContainers: "No containers",
NoContainer: "No container",
@ -263,7 +271,7 @@ func englishSet() TranslationSet {
ConfirmPruneNetworks: "Are you sure you want to prune all unused networks?",
StopService: "Are you sure you want to stop this service's containers?",
StopContainer: "Are you sure you want to stop this container?",
PressEnterToReturn: "Press enter to return to lazydocker (this prompt can be disabled in your config by setting `gui.returnImmediately: true`)",
PressEnterToReturn: "Press enter to return to lazyapple (this prompt can be disabled in your config by setting `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "By default, to detach from the container press ctrl-p then ctrl-q",
No: "no",
@ -279,5 +287,13 @@ func englishSet() TranslationSet {
FocusImages: "focus images panel",
FocusVolumes: "focus volumes panel",
FocusNetworks: "focus networks panel",
Kill: "kill",
KillingStatus: "killing",
RemoveContainer: "remove container",
ContainerNotRunning: "container is not running",
NoIp: "n/a",
NoPorts: "n/a",
NoStats: "No stats available",
}
}

View file

@ -13,13 +13,13 @@ func frenchSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "Aucune vue correspondant au switch newLineFocused",
ErrorOccurred: "Une erreur s'est produite ! Veuillez créer un rapport d'erreur sur https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "Une erreur s'est produite ! Veuillez créer un rapport d'erreur sur https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "Erreur lors de la connexion au client Docker. Essayez de redémarrer votre client Docker",
UnattachableContainerError: "Le conteneur ne peut pas être attaché. Vous devez exécuter le service avec le drapeau 'it' ou bien utiliser `stdin_open: true, tty: true` dans votre fichier docker-compose.yml",
WaitingForContainerInfo: "Le processus ne peut pas continuer avant que Docker ne fournisse plus d'informations. Veuillez réessayer dans quelques instants.",
CannotAttachStoppedContainerError: "Vous ne pouvez pas vous attacher à un conteneur arrêté, vous devez le démarrer en amont (ce que vous pouvez faire avec la touche 'r') (oui, je suis trop paresseux pour le faire automatiquement pour vous) (plutôt cool que je puisse communiquer en tête-à-tête avec vous au travers d'un message d'erreur, cependant)",
CannotAccessDockerSocketError: "Impossible d'accéder au socket Docker à : unix:///var/run/docker.sock\nLancez lazydocker en tant que root ou alors lisez https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "Impossible d'accéder au socket Docker à : unix:///var/run/docker.sock\nLancez lazyapple en tant que root ou alors lisez https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "Trois secondes se sont écoulées depuis la demande d'arrêt des processus enfants. Il se peut qu'un processus orphelin continue à tourner sur votre système.",
Donate: "Donner",
@ -33,8 +33,8 @@ func frenchSet() TranslationSet {
Menu: "menu",
MenuTitle: "Menu",
Scroll: "faire défiler",
OpenConfig: "ouvrir la configuration lazydocker",
EditConfig: "modifier la configuration lazydocker",
OpenConfig: "ouvrir la configuration lazyapple",
EditConfig: "modifier la configuration lazyapple",
Cancel: "annuler",
Remove: "supprimer",
HideStopped: "cacher/montrer les conteneurs arrêtés",
@ -111,7 +111,7 @@ func frenchSet() TranslationSet {
ConfirmPruneNetworks: "Êtes-vous certain de vouloir détruire tous les réseaux non utilisés ?",
StopService: "Êtes-vous certain de vouloir arrêter le conteneur de ce service ?",
StopContainer: "Êtes-vous certain de vouloir arrêter ce conteneur ?",
PressEnterToReturn: "Appuyez sur Entrée pour revenir à lazydocker (ce message peut être désactivé dans vos configurations en appliquant `gui.returnImmediately: true`)",
PressEnterToReturn: "Appuyez sur Entrée pour revenir à lazyapple (ce message peut être désactivé dans vos configurations en appliquant `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Par défaut, pour se détacher du conteneur appuyez sur CTRL-P puis CTRL-Q",
No: "non",

View file

@ -10,11 +10,11 @@ func germanSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "Verbindung zum Docker Client fehlgeschlagen. Du musst ggf. den Docker Client neustarten.",
UnattachableContainerError: "Der Container bietet keine Unterstützung für das Anbinden. Du musst den Dienst entweder mit der '-it' Flagge benutzen oder `stdin_open: true, tty: true` in der docker-compose.yml Datei setzen.",
CannotAttachStoppedContainerError: "Du kannst keinen angehaltenen Container anbinden. Du musst ihn erst starten (was du tun kannst, indem du 'r' drückst), (ja, ich bin zu faul um das zu automatisieren) (aber ist schon cool, dass ich so eine Konversation durch eine Fehlermeldung mit dir führen kann)",
CannotAccessDockerSocketError: "Kann nicht auf den Socket zugreifen: unix:///var/run/docker.sock\nFühre lazydocker als root aus oder lese https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "Kann nicht auf den Socket zugreifen: unix:///var/run/docker.sock\nFühre lazyapple als root aus oder lese https://docs.docker.com/install/linux/linux-postinstall/",
Donate: "Spenden",
Confirm: "Bestätigen",
@ -27,8 +27,8 @@ func germanSet() TranslationSet {
Menu: "menü",
MenuTitle: "Menü",
Scroll: "scrollen",
OpenConfig: "öffne lazydocker Konfiguration",
EditConfig: "bearbeite lazydocker Konfiguration",
OpenConfig: "öffne lazyapple Konfiguration",
EditConfig: "bearbeite lazyapple Konfiguration",
Cancel: "abbrechen",
Remove: "entfernen",
ForceRemove: "Entfernen erzwingen",
@ -91,7 +91,7 @@ func germanSet() TranslationSet {
ConfirmPruneNetworks: "Bist du dir sicher, dass du alle unbenutzen Netzwerk entfernen möchtest?",
StopService: "Bist du dir sicher, dass du den Dienst dieses Containers anhalten möchtest?",
StopContainer: "Bist du dir sicher, dass du den Container anhalten möchtest?",
PressEnterToReturn: "Drücke Eingabe um zu lazydocker zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)",
PressEnterToReturn: "Drücke Eingabe um zu lazyapple zurückzukehren. (Diese Nachfrage kann in Deiner Konfiguration deaktiviert werden, indem du folgenden Wert setzt: `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Um sich vom Container zu trennen, drücken Sie standardmäßig ctrl-p und dann ctrl-q",
}
}

View file

@ -3,7 +3,7 @@ package i18n
import (
"strings"
"github.com/imdario/mergo"
"dario.cat/mergo"
"github.com/cloudfoundry/jibber_jabber"
"github.com/go-errors/errors"

View file

@ -10,7 +10,7 @@ func polishSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "Żaden widok nie odpowiada instrukcji przełączenia newLineFocused",
ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "Błąd połączenia z Dockerem. Być może należy go zrestartować.",
UnattachableContainerError: "Kontener nie obsługuje przyczepiania (attach). Musisz albo użyć flag '-it', albo `stdin_open: true, tty: true` w pliku docker-compose.yml.",
CannotAttachStoppedContainerError: "Nie można przyczepić się do zatrzymanego kontenera, należy go najpierw uruchomić (co można wykonać wciskając przycisk 'r')",
@ -91,7 +91,7 @@ func polishSet() TranslationSet {
ConfirmPruneNetworks: "Na pewno wyczyścić wszystkie nieużywane sieci?",
StopService: "Na pewno zatrzymać kontenery tego serwisu?",
StopContainer: "Na pewno zatrzymać ten kontener?",
PressEnterToReturn: "Wciśnij enter aby powrócić do lazydockera (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)",
PressEnterToReturn: "Wciśnij enter aby powrócić do lazyapplea (ten komunikat może być wyłączony w konfiguracji poprzez ustawienie `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Domyślnie, aby odłączyć się od kontenera, naciśnij ctrl-p, a następnie ctrl-q",
}
}

View file

@ -16,12 +16,12 @@ func portugueseSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "Um erro ocorreu! Por favor, crie uma issue em https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "Um erro ocorreu! Por favor, crie uma issue em https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "Falha na conexão com o cliente Docker. Você pode precisar reiniciar o seu cliente Docker",
UnattachableContainerError: "O contêiner não suporta anexação. Você deve executar o serviço com a flag '-it' ou usar `stdin_open: true, tty: true` no arquivo docker-compose.yml",
WaitingForContainerInfo: "Não é possível prosseguir até que o Docker forneça mais informações sobre o contêiner. Por favor, tente novamente em alguns momentos.",
CannotAttachStoppedContainerError: "Você não pode anexar a um contêiner parado, você precisa iniciá-lo primeiro (o que você pode fazer com a tecla 'r') (sim, sou preguiçoso demais para fazer isso automaticamente para você) (aliás, bem legal que eu posso me comunicar diretamente com você na forma de uma mensagem de erro)",
CannotAccessDockerSocketError: "Não é possível acessar o sôquete docker em: unix:///var/run/docker.sock\nExecute o lazydocker como root ou leia https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "Não é possível acessar o sôquete docker em: unix:///var/run/docker.sock\nExecute o lazyapple como root ou leia https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "Três segundos foram esperarados para que os processos filhos parassem. Pode haver um processo órfão que continua em execução em seu sistema.",
Donate: "Doar",
@ -37,8 +37,8 @@ func portugueseSet() TranslationSet {
Menu: "menu",
MenuTitle: "Menu",
Scroll: "rolar",
OpenConfig: "abrir configuração do lazydocker",
EditConfig: "editar configuração do lazydocker",
OpenConfig: "abrir configuração do lazyapple",
EditConfig: "editar configuração do lazyapple",
Cancel: "cancelar",
Remove: "remover",
HideStopped: "ocultar/mostrar contêineres parados",
@ -124,7 +124,7 @@ func portugueseSet() TranslationSet {
ConfirmPruneNetworks: "Tem certeza que deseja destruir todas as redes não utilizadas?",
StopService: "Tem certeza que deseja parar os contêineres deste serviço?",
StopContainer: "Tem certeza que deseja parar este contêiner?",
PressEnterToReturn: "Pressione enter para retornar ao lazydocker (este prompt pode ser desativado em sua configuração definindo `gui.returnImmediately: true`)",
PressEnterToReturn: "Pressione enter para retornar ao lazyapple (este prompt pode ser desativado em sua configuração definindo `gui.returnImmediately: true`)",
DetachFromContainerShortCut: "Por padrão, para desanexar do contêiner, pressione ctrl-p e depois ctrl-q",
No: "não",

View file

@ -14,11 +14,11 @@ func spanishSet() TranslationSet {
RunningCustomCommandStatus: "ejecutando comando personalizado",
RunningBulkCommandStatus: "ejecutando comando masivo",
ErrorOccurred: "¡Hubo un error! Por favor crea un issue en https://github.com/jesseduffield/lazydocker/issues",
ErrorOccurred: "¡Hubo un error! Por favor crea un issue en https://github.com/g-battaglia/lazy-apple-container/issues",
ConnectionFailed: "Falló la conexión con el docker client. Quizá necesitas reiniciar tu docker client",
UnattachableContainerError: "Container does not support attaching. You must either run the service with the '-it' flag or use `stdin_open: true, tty: true` in the docker-compose.yml file",
WaitingForContainerInfo: "No podemos proceder hasta que docker nos de más información sobre el contenedor. Inténtalo otra vez en unos segundos.",
CannotAccessDockerSocketError: "No es posible acceder al docker socket en: unix:///var/run/docker.sock\nEjecuta lazydocker como root o lee https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "No es posible acceder al docker socket en: unix:///var/run/docker.sock\nEjecuta lazyapple como root o lee https://docs.docker.com/install/linux/linux-postinstall/",
CannotKillChildError: "Esperamos tres segundos a que el proceso hijo se detenga. Debe de haber un proceso huérfano que continua activo en tu sistema.",
Donate: "Donar",
@ -33,8 +33,8 @@ func spanishSet() TranslationSet {
Quit: "salir",
Menu: "menú",
MenuTitle: "Menú",
OpenConfig: "abrir configuración de lazydocker",
EditConfig: "editar configuración de lazydocker",
OpenConfig: "abrir configuración de lazyapple",
EditConfig: "editar configuración de lazyapple",
Cancel: "cancelar",
Remove: "borrar",
HideStopped: "esconder/mostrar contenedores parados",
@ -119,7 +119,7 @@ func spanishSet() TranslationSet {
ConfirmPruneNetworks: "¿Realmente quieres limpiar todas las redes sin usar?",
StopService: "¿Realmente quieres detener los contenedores de este servicio?",
StopContainer: "¿Realmente quieres detener este contenedor?",
PressEnterToReturn: "Presionar [enter] para volver a lazydocker (este mensaje puede ser desactivado en tu configuración poniendo `gui.returnImmediately: true`)",
PressEnterToReturn: "Presionar [enter] para volver a lazyapple (este mensaje puede ser desactivado en tu configuración poniendo `gui.returnImmediately: true`)",
No: "no",
Yes: "sí",

View file

@ -10,11 +10,11 @@ func turkishSet() TranslationSet {
NoViewMachingNewLineFocusedSwitchStatement: "NewLineFocused anahtar deyimi ile eşleşen görünüm yok",
ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/jesseduffield/lazydocker/issues adresinden bir hataya ilişkin konu oluşturun",
ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/g-battaglia/lazy-apple-container/issues adresinden bir hataya ilişkin konu oluşturun",
ConnectionFailed: "Docker bağlantısı başarısız oldu. Docker' ı yeniden başlatmanız gerekebilir",
UnattachableContainerError: "Konteyner attaching modunda çalışmayı desteklemiyor. Hizmeti '-it' opsiyonu ile çalıştırmanız veya docker-compose.yml dosyasında `stdin_open: true, tty: true` kullanmanız gerekir.",
CannotAttachStoppedContainerError: "Durdurulan konteynera bağlanamazsınız, ilk önce başlatmanız gerekir (aslında başlatmayı r tuşu ile yapabilirsiniz) (evet, senin için bunu otomatik olarak yapabilirim fakat çok tembelim) (hata mesajı ile seninle birebir iletişim kurmam çok daha güzel)",
CannotAccessDockerSocketError: "Docker' a şu adresten erişilemiyor : unix:///var/run/docker.sock\n lazydocker' ı root(kök kullanıcı) olarak çalıştır veya şu adresteki adımları takip et : https://docs.docker.com/install/linux/linux-postinstall/",
CannotAccessDockerSocketError: "Docker' a şu adresten erişilemiyor : unix:///var/run/docker.sock\n lazyapple' ı root(kök kullanıcı) olarak çalıştır veya şu adresteki adımları takip et : https://docs.docker.com/install/linux/linux-postinstall/",
Donate: "Bağış",
Confirm: "Onayla",
@ -27,7 +27,7 @@ func turkishSet() TranslationSet {
Menu: "menü",
MenuTitle: "Menü",
Scroll: "kaydır",
OpenConfig: "lazydocker ayarlarını aç",
OpenConfig: "lazyapple ayarlarını aç",
EditConfig: "lazzydocker ayarlarını düzenle",
Cancel: "iptal",
Remove: "kaldır",
@ -91,7 +91,7 @@ func turkishSet() TranslationSet {
ConfirmPruneNetworks: "Kullanılmayan tüm ağları temizlemek istediğinizden emin misiniz?",
StopService: "Bu servisin konteynerlerini durdurmak istediğinize emin misiniz?",
StopContainer: "Bu konteyneri durdurmak istediğinize emin misiniz?",
PressEnterToReturn: "lazydocker' a geri dönmek için enter tuşuna basın ( Bu uyarı, `gui.return Immediately: true` ayarıyla devre dışı bırakılabilir)",
PressEnterToReturn: "lazyapple' a geri dönmek için enter tuşuna basın ( Bu uyarı, `gui.return Immediately: true` ayarıyla devre dışı bırakılabilir)",
DetachFromContainerShortCut: "Varsayılan olarak, kaptan ayırmak için ctrl-p ve ardından ctrl-q tuşlarına basın",
}
}

View file

@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazycontainer/pkg/config"
"github.com/sirupsen/logrus"
)

View file

@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
)

View file

@ -5,7 +5,7 @@ import (
"log"
"os"
"github.com/jesseduffield/lazydocker/pkg/cheatsheet"
"github.com/jesseduffield/lazycontainer/pkg/cheatsheet"
)
func main() {

View file

@ -4,7 +4,7 @@ import (
"fmt"
"reflect"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazycontainer/pkg/i18n"
)
func main() {

View file

@ -9,4 +9,4 @@ name = "go"
enabled = true
[analyzers.meta]
import_path = "github.com/imdario/mergo"
import_path = "dario.cat/mergo"

View file

@ -13,6 +13,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Golang/Intellij
.idea
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

7
vendor/dario.cat/mergo/FUNDING.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930"
}
}
}

View file

@ -44,77 +44,59 @@ Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the
## Status
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, Microsoft, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
Mergo is stable and frozen, ready for production. Check a short list of the projects using at large scale it [here](https://github.com/imdario/mergo#mergo-in-the-wild).
### Important note
No new features are accepted. They will be considered for a future v2 that improves the implementation and fixes bugs for corner cases.
### Important notes
#### 1.0.0
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`. No more v1 versions will be released.
If the vanity URL is causing issues in your project due to a dependency pulling Mergo - it isn't a direct dependency in your project - it is recommended to use [replace](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive) to pin the version to the last one with the old import URL:
```
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
```
#### 0.3.9
Please keep in mind that a problematic PR broke [0.3.9](//github.com/imdario/mergo/releases/tag/0.3.9). I reverted it in [0.3.10](//github.com/imdario/mergo/releases/tag/0.3.10), and I consider it stable but not bug-free. Also, this version adds support for go modules.
Keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2), Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). I added an optional/variadic argument so that it won't break the existing code.
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with ```go get -u dario.cat/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
### Donations
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
<a href='https://github.com/sponsors/imdario' target='_blank'><img alt="Become my sponsor" src="https://img.shields.io/github/sponsors/imdario?style=for-the-badge" /></a>
### Mergo in the wild
- [moby/moby](https://github.com/moby/moby)
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
- [vmware/dispatch](https://github.com/vmware/dispatch)
- [Shopify/themekit](https://github.com/Shopify/themekit)
- [imdario/zas](https://github.com/imdario/zas)
- [matcornic/hermes](https://github.com/matcornic/hermes)
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
- [kataras/iris](https://github.com/kataras/iris)
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
- [go-task/task](https://github.com/go-task/task)
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
- [ory/hydra](https://github.com/ory/hydra)
- [sisatech/vcli](https://github.com/sisatech/vcli)
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
- [projectcalico/felix](https://github.com/projectcalico/felix)
- [resin-os/balena](https://github.com/resin-os/balena)
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
- [Telefonica/govice](https://github.com/Telefonica/govice)
- [supergiant/supergiant](supergiant/supergiant)
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
- [divshot/gitling](https://github.com/divshot/gitling)
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
- [elwinar/rambler](https://github.com/elwinar/rambler)
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
- [thoas/picfit](https://github.com/thoas/picfit)
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
- [containerssh/containerssh](https://github.com/containerssh/containerssh)
- [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
- [tjpnz/structbot](https://github.com/tjpnz/structbot)
Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/dependents) [of](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.16/dependents) [projects](https://deps.dev/go/github.com%2Fimdario%2Fmergo/v0.3.12), including:
* [containerd/containerd](https://github.com/containerd/containerd)
* [datadog/datadog-agent](https://github.com/datadog/datadog-agent)
* [docker/cli/](https://github.com/docker/cli/)
* [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
* [go-micro/go-micro](https://github.com/go-micro/go-micro)
* [grafana/loki](https://github.com/grafana/loki)
* [masterminds/sprig](github.com/Masterminds/sprig)
* [moby/moby](https://github.com/moby/moby)
* [slackhq/nebula](https://github.com/slackhq/nebula)
* [volcano-sh/volcano](https://github.com/volcano-sh/volcano)
## Install
go get github.com/imdario/mergo
go get dario.cat/mergo
// use in your .go code
import (
"github.com/imdario/mergo"
"dario.cat/mergo"
)
## Usage
@ -135,6 +117,39 @@ if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
}
```
If you need to override pointers, so the source pointer's value is assigned to the destination's pointer, you must use `WithoutDereference`:
```go
package main
import (
"fmt"
"dario.cat/mergo"
)
type Foo struct {
A *string
B int64
}
func main() {
first := "first"
second := "second"
src := Foo{
A: &first,
B: 2,
}
dest := Foo{
A: &second,
B: 1,
}
mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference)
}
```
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
```go
@ -152,7 +167,7 @@ package main
import (
"fmt"
"github.com/imdario/mergo"
"dario.cat/mergo"
)
type Foo struct {
@ -175,10 +190,6 @@ func main() {
}
```
Note: if test are failing due missing package, please execute:
go get gopkg.in/yaml.v3
### Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
@ -188,9 +199,9 @@ package main
import (
"fmt"
"github.com/imdario/mergo"
"reflect"
"time"
"dario.cat/mergo"
"reflect"
"time"
)
type timeTransformer struct {

View file

@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
| 0.3.x | :white_check_mark: |
| < 0.3 | :x: |
| 1.x.x | :white_check_mark: |
| < 1.0 | :x: |
## Security contact information

View file

@ -8,30 +8,36 @@ A helper to merge structs and maps in Golang. Useful for configuration default v
Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection).
Status
# Status
It is ready for production use. It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc.
Important note
# Important notes
1.0.0
In 1.0.0 Mergo moves to a vanity URL `dario.cat/mergo`.
0.3.9
Please keep in mind that a problematic PR broke 0.3.9. We reverted it in 0.3.10. We consider 0.3.10 as stable but not bug-free. . Also, this version adds suppot for go modules.
Keep in mind that in 0.3.2, Mergo changed Merge() and Map() signatures to support transformers. We added an optional/variadic argument so that it won't break the existing code.
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u github.com/imdario/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u dario.cat/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
Install
# Install
Do your usual installation procedure:
go get github.com/imdario/mergo
go get dario.cat/mergo
// use in your .go code
import (
"github.com/imdario/mergo"
)
// use in your .go code
import (
"dario.cat/mergo"
)
Usage
# Usage
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as they are zero values too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
@ -59,7 +65,7 @@ Here is a nice example:
import (
"fmt"
"github.com/imdario/mergo"
"dario.cat/mergo"
)
type Foo struct {
@ -81,7 +87,7 @@ Here is a nice example:
// {two 2}
}
Transformers
# Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, time.Time is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero time.Time?
@ -89,9 +95,9 @@ Transformers allow to merge specific types differently than in the default behav
import (
"fmt"
"github.com/imdario/mergo"
"reflect"
"time"
"dario.cat/mergo"
"reflect"
"time"
)
type timeTransformer struct {
@ -127,17 +133,16 @@ Transformers allow to merge specific types differently than in the default behav
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
}
Contact me
# Contact me
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): https://twitter.com/im_dario
About
# About
Written by Dario Castañé: https://da.rio.hn
License
# License
BSD 3-Clause license, as Go language.
*/
package mergo

View file

@ -58,7 +58,7 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, conf
}
fieldName := field.Name
fieldName = changeInitialCase(fieldName, unicode.ToLower)
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v), !config.ShouldNotDereference) || overwrite) {
if _, ok := dstMap[fieldName]; !ok || (!isEmptyValue(reflect.ValueOf(src.Field(i).Interface()), !config.ShouldNotDereference) && overwrite) || config.overwriteWithEmptyValue {
dstMap[fieldName] = src.Field(i).Interface()
}
}

View file

@ -269,7 +269,7 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, co
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
} else {
} else if src.Elem().Kind() != reflect.Struct {
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
dst.Set(src)
}

View file

@ -1 +0,0 @@
* text=auto eol=lf

View file

@ -1,10 +0,0 @@
.vscode/
*.exe
# testing
testdata
# go workspaces
go.work
go.work.sum

View file

@ -1,147 +0,0 @@
linters:
enable:
# style
- containedctx # struct contains a context
- dupl # duplicate code
- errname # erorrs are named correctly
- nolintlint # "//nolint" directives are properly explained
- revive # golint replacement
- unconvert # unnecessary conversions
- wastedassign
# bugs, performance, unused, etc ...
- contextcheck # function uses a non-inherited context
- errorlint # errors not wrapped for 1.13
- exhaustive # check exhaustiveness of enum switch statements
- gofmt # files are gofmt'ed
- gosec # security
- nilerr # returns nil even with non-nil error
- thelper # test helpers without t.Helper()
- unparam # unused function params
issues:
exclude-dirs:
- pkg/etw/sample
exclude-rules:
# err is very often shadowed in nested scopes
- linters:
- govet
text: '^shadow: declaration of "err" shadows declaration'
# ignore long lines for skip autogen directives
- linters:
- revive
text: "^line-length-limit: "
source: "^//(go:generate|sys) "
#TODO: remove after upgrading to go1.18
# ignore comment spacing for nolint and sys directives
- linters:
- revive
text: "^comment-spacings: no space between comment delimiter and comment text"
source: "//(cspell:|nolint:|sys |todo)"
# not on go 1.18 yet, so no any
- linters:
- revive
text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'"
# allow unjustified ignores of error checks in defer statements
- linters:
- nolintlint
text: "^directive `//nolint:errcheck` should provide explanation"
source: '^\s*defer '
# allow unjustified ignores of error lints for io.EOF
- linters:
- nolintlint
text: "^directive `//nolint:errorlint` should provide explanation"
source: '[=|!]= io.EOF'
linters-settings:
exhaustive:
default-signifies-exhaustive: true
govet:
enable-all: true
disable:
# struct order is often for Win32 compat
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
- fieldalignment
nolintlint:
require-explanation: true
require-specific: true
revive:
# revive is more configurable than static check, so likely the preferred alternative to static-check
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
enable-all-rules:
true
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
rules:
# rules with required arguments
- name: argument-limit
disabled: true
- name: banned-characters
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: file-header
disabled: true
- name: function-length
disabled: true
- name: function-result-limit
disabled: true
- name: max-public-structs
disabled: true
# geneally annoying rules
- name: add-constant # complains about any and all strings and integers
disabled: true
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
disabled: true
- name: flag-parameter # excessive, and a common idiom we use
disabled: true
- name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead
disabled: true
# general config
- name: line-length-limit
arguments:
- 140
- name: var-naming
arguments:
- []
- - CID
- CRI
- CTRD
- DACL
- DLL
- DOS
- ETW
- FSCTL
- GCS
- GMSA
- HCS
- HV
- IO
- LCOW
- LDAP
- LPAC
- LTSC
- MMIO
- NT
- OCI
- PMEM
- PWSH
- RX
- SACl
- SID
- SMB
- TX
- VHD
- VHDX
- VMID
- VPCI
- WCOW
- WIM

View file

@ -1 +0,0 @@
* @microsoft/containerplat

View file

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,89 +0,0 @@
# go-winio [![Build Status](https://github.com/microsoft/go-winio/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/go-winio/actions/workflows/ci.yml)
This repository contains utilities for efficiently performing Win32 IO operations in
Go. Currently, this is focused on accessing named pipes and other file handles, and
for using named pipes as a net transport.
This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go
to reuse the thread to schedule another goroutine. This limits support to Windows Vista and
newer operating systems. This is similar to the implementation of network sockets in Go's net
package.
Please see the LICENSE file for licensing information.
## Contributing
This project welcomes contributions and suggestions.
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that
you have the right to, and actually do, grant us the rights to use your contribution.
For details, visit [Microsoft CLA](https://cla.microsoft.com).
When you submit a pull request, a CLA-bot will automatically determine whether you need to
provide a CLA and decorate the PR appropriately (e.g., label, comment).
Simply follow the instructions provided by the bot.
You will only need to do this once across all repos using our CLA.
Additionally, the pull request pipeline requires the following steps to be performed before
mergining.
### Code Sign-Off
We require that contributors sign their commits using [`git commit --signoff`][git-commit-s]
to certify they either authored the work themselves or otherwise have permission to use it in this project.
A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s].
Please see [the developer certificate](https://developercertificate.org) for more info,
as well as to make sure that you can attest to the rules listed.
Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off.
### Linting
Code must pass a linting stage, which uses [`golangci-lint`][lint].
The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run
automatically with VSCode by adding the following to your workspace or folder settings:
```json
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
```
Additional editor [integrations options are also available][lint-ide].
Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root:
```shell
# use . or specify a path to only lint a package
# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0"
> golangci-lint run ./...
```
### Go Generate
The pipeline checks that auto-generated code, via `go generate`, are up to date.
This can be done for the entire repo:
```shell
> go generate ./...
```
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Special Thanks
Thanks to [natefinch][natefinch] for the inspiration for this library.
See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation.
[lint]: https://golangci-lint.run/
[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration
[lint-install]: https://golangci-lint.run/usage/install/#local-installation
[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s
[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff
[natefinch]: https://github.com/natefinch

View file

@ -1,41 +0,0 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

View file

@ -1,287 +0,0 @@
//go:build windows
// +build windows
package winio
import (
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"runtime"
"unicode/utf16"
"github.com/Microsoft/go-winio/internal/fs"
"golang.org/x/sys/windows"
)
//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
const (
BackupData = uint32(iota + 1)
BackupEaData
BackupSecurity
BackupAlternateData
BackupLink
BackupPropertyData
BackupObjectId //revive:disable-line:var-naming ID, not Id
BackupReparseData
BackupSparseBlock
BackupTxfsData
)
const (
StreamSparseAttributes = uint32(8)
)
//nolint:revive // var-naming: ALL_CAPS
const (
WRITE_DAC = windows.WRITE_DAC
WRITE_OWNER = windows.WRITE_OWNER
ACCESS_SYSTEM_SECURITY = windows.ACCESS_SYSTEM_SECURITY
)
// BackupHeader represents a backup stream of a file.
type BackupHeader struct {
//revive:disable-next-line:var-naming ID, not Id
Id uint32 // The backup stream ID
Attributes uint32 // Stream attributes
Size int64 // The size of the stream in bytes
Name string // The name of the stream (for BackupAlternateData only).
Offset int64 // The offset of the stream in the file (for BackupSparseBlock only).
}
type win32StreamID struct {
StreamID uint32
Attributes uint32
Size uint64
NameSize uint32
}
// BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series
// of BackupHeader values.
type BackupStreamReader struct {
r io.Reader
bytesLeft int64
}
// NewBackupStreamReader produces a BackupStreamReader from any io.Reader.
func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
return &BackupStreamReader{r, 0}
}
// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if
// it was not completely read.
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
if r.bytesLeft > 0 { //nolint:nestif // todo: flatten this
if s, ok := r.r.(io.Seeker); ok {
// Make sure Seek on io.SeekCurrent sometimes succeeds
// before trying the actual seek.
if _, err := s.Seek(0, io.SeekCurrent); err == nil {
if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil {
return nil, err
}
r.bytesLeft = 0
}
}
if _, err := io.Copy(io.Discard, r); err != nil {
return nil, err
}
}
var wsi win32StreamID
if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil {
return nil, err
}
hdr := &BackupHeader{
Id: wsi.StreamID,
Attributes: wsi.Attributes,
Size: int64(wsi.Size),
}
if wsi.NameSize != 0 {
name := make([]uint16, int(wsi.NameSize/2))
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
return nil, err
}
hdr.Name = windows.UTF16ToString(name)
}
if wsi.StreamID == BackupSparseBlock {
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
return nil, err
}
hdr.Size -= 8
}
r.bytesLeft = hdr.Size
return hdr, nil
}
// Read reads from the current backup stream.
func (r *BackupStreamReader) Read(b []byte) (int, error) {
if r.bytesLeft == 0 {
return 0, io.EOF
}
if int64(len(b)) > r.bytesLeft {
b = b[:r.bytesLeft]
}
n, err := r.r.Read(b)
r.bytesLeft -= int64(n)
if err == io.EOF {
err = io.ErrUnexpectedEOF
} else if r.bytesLeft == 0 && err == nil {
err = io.EOF
}
return n, err
}
// BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API.
type BackupStreamWriter struct {
w io.Writer
bytesLeft int64
}
// NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer.
func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter {
return &BackupStreamWriter{w, 0}
}
// WriteHeader writes the next backup stream header and prepares for calls to Write().
func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error {
if w.bytesLeft != 0 {
return fmt.Errorf("missing %d bytes", w.bytesLeft)
}
name := utf16.Encode([]rune(hdr.Name))
wsi := win32StreamID{
StreamID: hdr.Id,
Attributes: hdr.Attributes,
Size: uint64(hdr.Size),
NameSize: uint32(len(name) * 2),
}
if hdr.Id == BackupSparseBlock {
// Include space for the int64 block offset
wsi.Size += 8
}
if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil {
return err
}
if len(name) != 0 {
if err := binary.Write(w.w, binary.LittleEndian, name); err != nil {
return err
}
}
if hdr.Id == BackupSparseBlock {
if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil {
return err
}
}
w.bytesLeft = hdr.Size
return nil
}
// Write writes to the current backup stream.
func (w *BackupStreamWriter) Write(b []byte) (int, error) {
if w.bytesLeft < int64(len(b)) {
return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft)
}
n, err := w.w.Write(b)
w.bytesLeft -= int64(n)
return n, err
}
// BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API.
type BackupFileReader struct {
f *os.File
includeSecurity bool
ctx uintptr
}
// NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true,
// Read will attempt to read the security descriptor of the file.
func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
r := &BackupFileReader{f, includeSecurity, 0}
return r
}
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
func (r *BackupFileReader) Read(b []byte) (int, error) {
var bytesRead uint32
err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
if err != nil {
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
}
runtime.KeepAlive(r.f)
if bytesRead == 0 {
return 0, io.EOF
}
return int(bytesRead), nil
}
// Close frees Win32 resources associated with the BackupFileReader. It does not close
// the underlying file.
func (r *BackupFileReader) Close() error {
if r.ctx != 0 {
_ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
runtime.KeepAlive(r.f)
r.ctx = 0
}
return nil
}
// BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API.
type BackupFileWriter struct {
f *os.File
includeSecurity bool
ctx uintptr
}
// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true,
// Write() will attempt to restore the security descriptor from the stream.
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
w := &BackupFileWriter{f, includeSecurity, 0}
return w
}
// Write restores a portion of the file using the provided backup stream.
func (w *BackupFileWriter) Write(b []byte) (int, error) {
var bytesWritten uint32
err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
if err != nil {
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
}
runtime.KeepAlive(w.f)
if int(bytesWritten) != len(b) {
return int(bytesWritten), errors.New("not all bytes could be written")
}
return len(b), nil
}
// Close frees Win32 resources associated with the BackupFileWriter. It does not
// close the underlying file.
func (w *BackupFileWriter) Close() error {
if w.ctx != 0 {
_ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
runtime.KeepAlive(w.f)
w.ctx = 0
}
return nil
}
// OpenForBackup opens a file or directory, potentially skipping access checks if the backup
// or restore privileges have been acquired.
//
// If the file opened was a directory, it cannot be used with Readdir().
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
h, err := fs.CreateFile(path,
fs.AccessMask(access),
fs.FileShareMode(share),
nil,
fs.FileCreationDisposition(createmode),
fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT,
0,
)
if err != nil {
err = &os.PathError{Op: "open", Path: path, Err: err}
return nil, err
}
return os.NewFile(uintptr(h), path), nil
}

View file

@ -1,22 +0,0 @@
// This package provides utilities for efficiently performing Win32 IO operations in Go.
// Currently, this package is provides support for genreal IO and management of
// - named pipes
// - files
// - [Hyper-V sockets]
//
// This code is similar to Go's [net] package, and uses IO completion ports to avoid
// blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines.
//
// This limits support to Windows Vista and newer operating systems.
//
// Additionally, this package provides support for:
// - creating and managing GUIDs
// - writing to [ETW]
// - opening and manageing VHDs
// - parsing [Windows Image files]
// - auto-generating Win32 API code
//
// [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service
// [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-
// [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images
package winio

View file

@ -1,137 +0,0 @@
package winio
import (
"bytes"
"encoding/binary"
"errors"
)
type fileFullEaInformation struct {
NextEntryOffset uint32
Flags uint8
NameLength uint8
ValueLength uint16
}
var (
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
errEaNameTooLarge = errors.New("extended attribute name too large")
errEaValueTooLarge = errors.New("extended attribute value too large")
)
// ExtendedAttribute represents a single Windows EA.
type ExtendedAttribute struct {
Name string
Value []byte
Flags uint8
}
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
var info fileFullEaInformation
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
if err != nil {
err = errInvalidEaBuffer
return ea, nb, err
}
nameOffset := fileFullEaInformationSize
nameLen := int(info.NameLength)
valueOffset := nameOffset + int(info.NameLength) + 1
valueLen := int(info.ValueLength)
nextOffset := int(info.NextEntryOffset)
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
err = errInvalidEaBuffer
return ea, nb, err
}
ea.Name = string(b[nameOffset : nameOffset+nameLen])
ea.Value = b[valueOffset : valueOffset+valueLen]
ea.Flags = info.Flags
if info.NextEntryOffset != 0 {
nb = b[info.NextEntryOffset:]
}
return ea, nb, err
}
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
for len(b) != 0 {
ea, nb, err := parseEa(b)
if err != nil {
return nil, err
}
eas = append(eas, ea)
b = nb
}
return eas, err
}
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
if int(uint8(len(ea.Name))) != len(ea.Name) {
return errEaNameTooLarge
}
if int(uint16(len(ea.Value))) != len(ea.Value) {
return errEaValueTooLarge
}
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
withPadding := (entrySize + 3) &^ 3
nextOffset := uint32(0)
if !last {
nextOffset = withPadding
}
info := fileFullEaInformation{
NextEntryOffset: nextOffset,
Flags: ea.Flags,
NameLength: uint8(len(ea.Name)),
ValueLength: uint16(len(ea.Value)),
}
err := binary.Write(buf, binary.LittleEndian, &info)
if err != nil {
return err
}
_, err = buf.Write([]byte(ea.Name))
if err != nil {
return err
}
err = buf.WriteByte(0)
if err != nil {
return err
}
_, err = buf.Write(ea.Value)
if err != nil {
return err
}
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
if err != nil {
return err
}
return nil
}
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
// buffer for use with BackupWrite, ZwSetEaFile, etc.
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
var buf bytes.Buffer
for i := range eas {
last := false
if i == len(eas)-1 {
last = true
}
err := writeEa(&buf, &eas[i], last)
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

View file

@ -1,320 +0,0 @@
//go:build windows
// +build windows
package winio
import (
"errors"
"io"
"runtime"
"sync"
"sync/atomic"
"syscall"
"time"
"golang.org/x/sys/windows"
)
//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
var (
ErrFileClosed = errors.New("file has already been closed")
ErrTimeout = &timeoutError{}
)
type timeoutError struct{}
func (*timeoutError) Error() string { return "i/o timeout" }
func (*timeoutError) Timeout() bool { return true }
func (*timeoutError) Temporary() bool { return true }
type timeoutChan chan struct{}
var ioInitOnce sync.Once
var ioCompletionPort windows.Handle
// ioResult contains the result of an asynchronous IO operation.
type ioResult struct {
bytes uint32
err error
}
// ioOperation represents an outstanding asynchronous Win32 IO.
type ioOperation struct {
o windows.Overlapped
ch chan ioResult
}
func initIO() {
h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
if err != nil {
panic(err)
}
ioCompletionPort = h
go ioCompletionProcessor(h)
}
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
// It takes ownership of this handle and will close it if it is garbage collected.
type win32File struct {
handle windows.Handle
wg sync.WaitGroup
wgLock sync.RWMutex
closing atomic.Bool
socket bool
readDeadline deadlineHandler
writeDeadline deadlineHandler
}
type deadlineHandler struct {
setLock sync.Mutex
channel timeoutChan
channelLock sync.RWMutex
timer *time.Timer
timedout atomic.Bool
}
// makeWin32File makes a new win32File from an existing file handle.
func makeWin32File(h windows.Handle) (*win32File, error) {
f := &win32File{handle: h}
ioInitOnce.Do(initIO)
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
if err != nil {
return nil, err
}
err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE)
if err != nil {
return nil, err
}
f.readDeadline.channel = make(timeoutChan)
f.writeDeadline.channel = make(timeoutChan)
return f, nil
}
// Deprecated: use NewOpenFile instead.
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
return NewOpenFile(windows.Handle(h))
}
func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
// If we return the result of makeWin32File directly, it can result in an
// interface-wrapped nil, rather than a nil interface value.
f, err := makeWin32File(h)
if err != nil {
return nil, err
}
return f, nil
}
// closeHandle closes the resources associated with a Win32 handle.
func (f *win32File) closeHandle() {
f.wgLock.Lock()
// Atomically set that we are closing, releasing the resources only once.
if !f.closing.Swap(true) {
f.wgLock.Unlock()
// cancel all IO and wait for it to complete
_ = cancelIoEx(f.handle, nil)
f.wg.Wait()
// at this point, no new IO can start
windows.Close(f.handle)
f.handle = 0
} else {
f.wgLock.Unlock()
}
}
// Close closes a win32File.
func (f *win32File) Close() error {
f.closeHandle()
return nil
}
// IsClosed checks if the file has been closed.
func (f *win32File) IsClosed() bool {
return f.closing.Load()
}
// prepareIO prepares for a new IO operation.
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
func (f *win32File) prepareIO() (*ioOperation, error) {
f.wgLock.RLock()
if f.closing.Load() {
f.wgLock.RUnlock()
return nil, ErrFileClosed
}
f.wg.Add(1)
f.wgLock.RUnlock()
c := &ioOperation{}
c.ch = make(chan ioResult)
return c, nil
}
// ioCompletionProcessor processes completed async IOs forever.
func ioCompletionProcessor(h windows.Handle) {
for {
var bytes uint32
var key uintptr
var op *ioOperation
err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
if op == nil {
panic(err)
}
op.ch <- ioResult{bytes, err}
}
}
// todo: helsaawy - create an asyncIO version that takes a context
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
// the operation has actually completed.
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
return int(bytes), err
}
if f.closing.Load() {
_ = cancelIoEx(f.handle, &c.o)
}
var timeout timeoutChan
if d != nil {
d.channelLock.Lock()
timeout = d.channel
d.channelLock.Unlock()
}
var r ioResult
select {
case r = <-c.ch:
err = r.err
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
if f.closing.Load() {
err = ErrFileClosed
}
} else if err != nil && f.socket {
// err is from Win32. Query the overlapped structure to get the winsock error.
var bytes, flags uint32
err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags)
}
case <-timeout:
_ = cancelIoEx(f.handle, &c.o)
r = <-c.ch
err = r.err
if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
err = ErrTimeout
}
}
// runtime.KeepAlive is needed, as c is passed via native
// code to ioCompletionProcessor, c must remain alive
// until the channel read is complete.
// todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive?
runtime.KeepAlive(c)
return int(r.bytes), err
}
// Read reads from a file handle.
func (f *win32File) Read(b []byte) (int, error) {
c, err := f.prepareIO()
if err != nil {
return 0, err
}
defer f.wg.Done()
if f.readDeadline.timedout.Load() {
return 0, ErrTimeout
}
var bytes uint32
err = windows.ReadFile(f.handle, b, &bytes, &c.o)
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
runtime.KeepAlive(b)
// Handle EOF conditions.
if err == nil && n == 0 && len(b) != 0 {
return 0, io.EOF
} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
return 0, io.EOF
}
return n, err
}
// Write writes to a file handle.
func (f *win32File) Write(b []byte) (int, error) {
c, err := f.prepareIO()
if err != nil {
return 0, err
}
defer f.wg.Done()
if f.writeDeadline.timedout.Load() {
return 0, ErrTimeout
}
var bytes uint32
err = windows.WriteFile(f.handle, b, &bytes, &c.o)
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
runtime.KeepAlive(b)
return n, err
}
func (f *win32File) SetReadDeadline(deadline time.Time) error {
return f.readDeadline.set(deadline)
}
func (f *win32File) SetWriteDeadline(deadline time.Time) error {
return f.writeDeadline.set(deadline)
}
func (f *win32File) Flush() error {
return windows.FlushFileBuffers(f.handle)
}
func (f *win32File) Fd() uintptr {
return uintptr(f.handle)
}
func (d *deadlineHandler) set(deadline time.Time) error {
d.setLock.Lock()
defer d.setLock.Unlock()
if d.timer != nil {
if !d.timer.Stop() {
<-d.channel
}
d.timer = nil
}
d.timedout.Store(false)
select {
case <-d.channel:
d.channelLock.Lock()
d.channel = make(chan struct{})
d.channelLock.Unlock()
default:
}
if deadline.IsZero() {
return nil
}
timeoutIO := func() {
d.timedout.Store(true)
close(d.channel)
}
now := time.Now()
duration := deadline.Sub(now)
if deadline.After(now) {
// Deadline is in the future, set a timer to wait
d.timer = time.AfterFunc(duration, timeoutIO)
} else {
// Deadline is in the past. Cancel all pending IO now.
timeoutIO()
}
return nil
}

View file

@ -1,106 +0,0 @@
//go:build windows
// +build windows
package winio
import (
"os"
"runtime"
"unsafe"
"golang.org/x/sys/windows"
)
// FileBasicInfo contains file access time and file attributes information.
type FileBasicInfo struct {
CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime
FileAttributes uint32
_ uint32 // padding
}
// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing
// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64
// alignment is necessary to pass this as FILE_BASIC_INFO.
type alignedFileBasicInfo struct {
CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64
FileAttributes uint32
_ uint32 // padding
}
// GetFileBasicInfo retrieves times and attributes for a file.
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
bi := &alignedFileBasicInfo{}
if err := windows.GetFileInformationByHandleEx(
windows.Handle(f.Fd()),
windows.FileBasicInfo,
(*byte)(unsafe.Pointer(bi)),
uint32(unsafe.Sizeof(*bi)),
); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
// Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the
// public API of this module. The data may be unnecessarily aligned.
return (*FileBasicInfo)(unsafe.Pointer(bi)), nil
}
// SetFileBasicInfo sets times and attributes for a file.
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
// Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is
// suitable to pass to GetFileInformationByHandleEx.
biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi))
if err := windows.SetFileInformationByHandle(
windows.Handle(f.Fd()),
windows.FileBasicInfo,
(*byte)(unsafe.Pointer(&biAligned)),
uint32(unsafe.Sizeof(biAligned)),
); err != nil {
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
return nil
}
// FileStandardInfo contains extended information for the file.
// FILE_STANDARD_INFO in WinBase.h
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
type FileStandardInfo struct {
AllocationSize, EndOfFile int64
NumberOfLinks uint32
DeletePending, Directory bool
}
// GetFileStandardInfo retrieves ended information for the file.
func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) {
si := &FileStandardInfo{}
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()),
windows.FileStandardInfo,
(*byte)(unsafe.Pointer(si)),
uint32(unsafe.Sizeof(*si))); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
return si, nil
}
// FileIDInfo contains the volume serial number and file ID for a file. This pair should be
// unique on a system.
type FileIDInfo struct {
VolumeSerialNumber uint64
FileID [16]byte
}
// GetFileID retrieves the unique (volume, file ID) pair for a file.
func GetFileID(f *os.File) (*FileIDInfo, error) {
fileID := &FileIDInfo{}
if err := windows.GetFileInformationByHandleEx(
windows.Handle(f.Fd()),
windows.FileIdInfo,
(*byte)(unsafe.Pointer(fileID)),
uint32(unsafe.Sizeof(*fileID)),
); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
runtime.KeepAlive(f)
return fileID, nil
}

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