mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Compare commits
481 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e7aadc207 | ||
|
|
f5ff116af9 | ||
|
|
3974f6fec1 | ||
|
|
697cd441aa | ||
|
|
9134abeefd | ||
|
|
8106125441 | ||
|
|
b17d474148 | ||
|
|
e3c1c8630a | ||
|
|
ebce4fc631 | ||
|
|
2ebe3848d6 | ||
|
|
b65137addf | ||
|
|
26ce871368 | ||
|
|
577797d9ed | ||
|
|
3c9d134d05 | ||
|
|
f4fc3669ca | ||
|
|
322e10dcb5 | ||
|
|
3580e04b8d | ||
|
|
b38db60ead | ||
|
|
43db69da00 | ||
|
|
78edbf3d2e | ||
|
|
753c0269d7 | ||
|
|
4137d216aa | ||
|
|
826f0293f1 | ||
|
|
c7b58683ae | ||
|
|
5e0aeec310 | ||
|
|
992c138844 | ||
|
|
c8d2fbdfdc | ||
|
|
f023b42c89 | ||
|
|
123af388b0 | ||
|
|
3314ecb66c | ||
|
|
4e2f26b21e | ||
|
|
8a1975ef4a | ||
|
|
4332656606 | ||
|
|
99bf41c873 | ||
|
|
587bbbf4b5 | ||
|
|
46400c4d32 | ||
|
|
8ee57bffaf | ||
|
|
bedde4a037 | ||
|
|
07558afdc8 | ||
|
|
1178381e00 | ||
|
|
c45bdea3a0 | ||
|
|
15677ac3d2 | ||
|
|
b8a3ca1550 | ||
|
|
be05115352 | ||
|
|
e2bc42f4bb | ||
|
|
98721a3a2c | ||
|
|
a8e75ecfe3 | ||
|
|
86cce84955 | ||
|
|
eaecd2c573 | ||
|
|
de40167712 | ||
|
|
df6c8c196e | ||
|
|
4f375bbf36 | ||
|
|
0849b800ff | ||
|
|
c73b3f7cc0 | ||
|
|
77a4de4819 | ||
|
|
2d16389add | ||
|
|
0232610967 | ||
|
|
c84bb887a4 | ||
|
|
ff579d8713 | ||
|
|
463ddfd6ee | ||
|
|
6f85b653d4 | ||
|
|
966ad05338 | ||
|
|
8fe90830b1 | ||
|
|
bb5d70f787 | ||
|
|
3daea1ea4d | ||
|
|
e27d4878a2 | ||
|
|
61cd92bc0c | ||
|
|
8ca3166439 | ||
|
|
6217ba0808 | ||
|
|
b010ea4347 | ||
|
|
ed600c7116 | ||
|
|
015a1086b7 | ||
|
|
f6c044d74f | ||
|
|
06ab7b77b1 | ||
|
|
89020d6ead | ||
|
|
1cf2e0284c | ||
|
|
58d5a18892 | ||
|
|
1e34a9babc | ||
|
|
152b365771 | ||
|
|
55b21dfd30 | ||
|
|
be2d80d015 | ||
|
|
cdf1d9ad11 | ||
|
|
968b09e6c4 | ||
|
|
2729aafe2c | ||
|
|
ce780b8f6d | ||
|
|
7f0f4305a9 | ||
|
|
9440a10053 | ||
|
|
0f1b8c9848 | ||
|
|
630ea7d324 | ||
|
|
8a0fcf99f4 | ||
|
|
623fdb4f33 | ||
|
|
bca15cf6b1 | ||
|
|
a6ade7e78d | ||
|
|
51b3bb5418 | ||
|
|
dbf2bced5e | ||
|
|
67441aa4b7 | ||
|
|
42b7ba18ea | ||
|
|
b210264c05 | ||
|
|
80af149b02 | ||
|
|
8aa725f06f | ||
|
|
559265df4a | ||
|
|
1060e17731 | ||
|
|
8976f10120 | ||
|
|
e860416ee4 | ||
|
|
cce9d40f43 | ||
|
|
6473810fa9 | ||
|
|
f25a5ebff7 | ||
|
|
b2525f4629 | ||
|
|
2dda39dff0 | ||
|
|
39e694f496 | ||
|
|
e69449c824 | ||
|
|
966570ce6c | ||
|
|
d2fa5e9e9c | ||
|
|
649e5e1c90 | ||
|
|
722fb88792 | ||
|
|
d2dd4db582 | ||
|
|
953e56065d | ||
|
|
42a9668e55 | ||
|
|
8f77c0f7c0 | ||
|
|
9e90d2f4f2 | ||
|
|
d68ecbf5f2 | ||
|
|
50582db9b7 | ||
|
|
604ca8f0df | ||
|
|
71e8e43b06 | ||
|
|
c635266fae | ||
|
|
937a6a9c93 | ||
|
|
e8e808ff47 | ||
|
|
454eaee2cf | ||
|
|
f0ee38f7f7 | ||
|
|
7c074d25f7 | ||
|
|
dffe6970aa | ||
|
|
07f26c1e91 | ||
|
|
66f5d176d3 | ||
|
|
ab8d87e6ae | ||
|
|
e5ca391291 | ||
|
|
15bc01b9af | ||
|
|
2a9ed70896 | ||
|
|
fcc0bbe552 | ||
|
|
f93ddeceef | ||
|
|
0066c1c76c | ||
|
|
cd11c3e131 | ||
|
|
62095473c0 | ||
|
|
cdd43478e2 | ||
|
|
afacc9be9e | ||
|
|
3c3c80d570 | ||
|
|
866bcae509 | ||
|
|
966a266c68 | ||
|
|
8a095f74b1 | ||
|
|
ce95ddea2a | ||
|
|
1746bc07c7 | ||
|
|
17fcdb2b20 | ||
|
|
338b655c0a | ||
|
|
6025912336 | ||
|
|
d77b895319 | ||
|
|
8871edc1a0 | ||
|
|
553b9857cd | ||
|
|
d777bfe655 | ||
|
|
821cd81b72 | ||
|
|
b8969fd55f | ||
|
|
83acb91d83 | ||
|
|
b0cef73dc1 | ||
|
|
642abae037 | ||
|
|
6fbf8c3cc5 | ||
|
|
530dbbe570 | ||
|
|
04cf34383d | ||
|
|
3e2a3e608a | ||
|
|
b7fea7a44d | ||
|
|
d1ccf11502 | ||
|
|
b0241c79db | ||
|
|
7f38cf2ab2 | ||
|
|
52040d1d99 | ||
|
|
5ffbeae3e8 | ||
|
|
7324254414 | ||
|
|
d6ad538426 | ||
|
|
02a03ed18d | ||
|
|
a87b698761 | ||
|
|
fc592b5806 | ||
|
|
5d498c796a | ||
|
|
c6d424e4a4 | ||
|
|
95cee378cb | ||
|
|
82516385c0 | ||
|
|
52d2c1ce8c | ||
|
|
b453ce8940 | ||
|
|
0bdd0e0539 | ||
|
|
d964c59df0 | ||
|
|
dfff845469 | ||
|
|
751dbef42e | ||
|
|
aab8bcfbcc | ||
|
|
9b58b8188a | ||
|
|
f7beb6e83f | ||
|
|
e6d4a97545 | ||
|
|
702361559f | ||
|
|
b6b5373999 | ||
|
|
60df208924 | ||
|
|
158ef68372 | ||
|
|
5aea90c08b | ||
|
|
a4b4fee868 | ||
|
|
79a58c8d48 | ||
|
|
b80c8edcd0 | ||
|
|
e46b908006 | ||
|
|
dcce4c67ec | ||
|
|
cb38e48add | ||
|
|
57379252a5 | ||
|
|
be347a7216 | ||
|
|
023370e357 | ||
|
|
50c837fc11 | ||
|
|
8b68bb6480 | ||
|
|
d644d5063f | ||
|
|
ead759108c | ||
|
|
53248a1a78 | ||
|
|
2ecfc5d0dc | ||
|
|
10919b4dfb | ||
|
|
2134025de4 | ||
|
|
478e2edc9d | ||
|
|
150adf7abc | ||
|
|
de07409d84 | ||
|
|
dddcf7c4a5 | ||
|
|
9ec03eef45 | ||
|
|
950936b178 | ||
|
|
129402fd50 | ||
|
|
0a0116b5eb | ||
|
|
008ad719d6 | ||
|
|
0cdac0bca3 | ||
|
|
f948334188 | ||
|
|
2f52bd04e6 | ||
|
|
15990effff | ||
|
|
26b40a9d94 | ||
|
|
195ccf41ce | ||
|
|
33df6ff3b0 | ||
|
|
38959a0e2b | ||
|
|
574e0b6f90 | ||
|
|
dd11b0ee10 | ||
|
|
1cb9506554 | ||
|
|
337e57afbb | ||
|
|
26858c3154 | ||
|
|
ec8dfbac06 | ||
|
|
c56256358a | ||
|
|
119835c64d | ||
|
|
0e0997d41a | ||
|
|
33dafa04d9 | ||
|
|
68ee15d24d | ||
|
|
68f5566fd0 | ||
|
|
080512d58d | ||
|
|
8670bc06e6 | ||
|
|
a614bd9a1d | ||
|
|
46de6b365d | ||
|
|
a9d1694853 | ||
|
|
5988ec5745 | ||
|
|
540c592b04 | ||
|
|
aa8504b8f6 | ||
|
|
fb7b28243b | ||
|
|
c6607686eb | ||
|
|
ae240021c1 | ||
|
|
badc43b225 | ||
|
|
df1a0b7c07 | ||
|
|
270c9de796 | ||
|
|
2db5aa5ce9 | ||
|
|
9c40237fae | ||
|
|
d556ce534a | ||
|
|
63e9d82b4f | ||
|
|
cd791e576f | ||
|
|
e8ebec193f | ||
|
|
79feb369e1 | ||
|
|
7e9bde0f41 | ||
|
|
d64a92227a | ||
|
|
49dd19b01c | ||
|
|
e93cadf1d6 | ||
|
|
3c607534d1 | ||
|
|
339b1068ec | ||
|
|
0b158dfabe | ||
|
|
0da5fef551 | ||
|
|
7581417552 | ||
|
|
ea3c009305 | ||
|
|
54cfd08849 | ||
|
|
923c91121e | ||
|
|
0118786fd9 | ||
|
|
14af9f4547 | ||
|
|
3abff083ad | ||
|
|
a03f550535 | ||
|
|
0ced576d9c | ||
|
|
badc92f192 | ||
|
|
2fa6a8964a | ||
|
|
d0a53fbe60 | ||
|
|
d940f2e7ab | ||
|
|
cc2f8032c7 | ||
|
|
430467711b | ||
|
|
cd744ad699 | ||
|
|
5112ac775d | ||
|
|
da650f4384 | ||
|
|
8319aed87c | ||
|
|
92ce88e5c0 | ||
|
|
76fdbd1239 | ||
|
|
86de6c09bd | ||
|
|
7396dcb0b7 | ||
|
|
2f08e0942a | ||
|
|
b4509fb45e | ||
|
|
62be3d9924 | ||
|
|
d3e84881bb | ||
|
|
100fc69f85 | ||
|
|
cd96b20bb2 | ||
|
|
c4e41b3ee2 | ||
|
|
0b7017150c | ||
|
|
b709f9f6b1 | ||
|
|
a100caa0d9 | ||
|
|
13c0bfc0b4 | ||
|
|
d27ce19f6c | ||
|
|
b5384d6cdb | ||
|
|
011824a9b7 | ||
|
|
75ed6b12e5 | ||
|
|
56b3196ef9 | ||
|
|
1924dd2bdb | ||
|
|
ac1e47766a | ||
|
|
4f19a66d35 | ||
|
|
d8777b7a59 | ||
|
|
05fa32014f | ||
|
|
e8c216a3f9 | ||
|
|
dc2e0ffa54 | ||
|
|
052c2ec1d1 | ||
|
|
e68554cee9 | ||
|
|
14cb013f5d | ||
|
|
4f096c3c2b | ||
|
|
a3d3109103 | ||
|
|
0e13712e91 | ||
|
|
cf87275668 | ||
|
|
087fd00759 | ||
|
|
b438700d2c | ||
|
|
31435c68f5 | ||
|
|
afe824a3d6 | ||
|
|
6bc57b7628 | ||
|
|
212b9d0b03 | ||
|
|
846cadd5fd | ||
|
|
1f5d31c92f | ||
|
|
2fe7505ebc | ||
|
|
5de54dfb0e | ||
|
|
bba2feac94 | ||
|
|
1eec32f1fb | ||
|
|
57ac3c409d | ||
|
|
6af9e54491 | ||
|
|
46fb0f5e5b | ||
|
|
32aaea973f | ||
|
|
35bd0db5f8 | ||
|
|
15105d87ff | ||
|
|
e9d65eda03 | ||
|
|
2718749229 | ||
|
|
9a0bbd1e38 | ||
|
|
ce5e97dd68 | ||
|
|
9fad29449c | ||
|
|
62eeaac437 | ||
|
|
cbacd32a68 | ||
|
|
4d90a0345f | ||
|
|
600e4af8e6 | ||
|
|
dd88876e53 | ||
|
|
c5d90fd09d | ||
|
|
fa2f86fe31 | ||
|
|
663928e81a | ||
|
|
32aa08066c | ||
|
|
af2eb0bb84 | ||
|
|
a78b5af56b | ||
|
|
0d825a49c7 | ||
|
|
1d29a124ee | ||
|
|
4c17f73750 | ||
|
|
23bb5692e4 | ||
|
|
97772a95d0 | ||
|
|
0279534811 | ||
|
|
71251f4f90 | ||
|
|
048c4c9731 | ||
|
|
7105cc67ae | ||
|
|
a0d8803147 | ||
|
|
e29e110476 | ||
|
|
afe3d23cbd | ||
|
|
50681bc9a0 | ||
|
|
e585277c89 | ||
|
|
615ce6536b | ||
|
|
0bd4168dc9 | ||
|
|
892fc090b6 | ||
|
|
7f68d01181 | ||
|
|
db288a1727 | ||
|
|
1681d839fa | ||
|
|
14e6ff1c1f | ||
|
|
a0386618f9 | ||
|
|
b30648c742 | ||
|
|
2712d995cd | ||
|
|
baf1759992 | ||
|
|
ae9bbdaf19 | ||
|
|
4a99c4699b | ||
|
|
9462017eaf | ||
|
|
1166e0ddb7 | ||
|
|
33e207aeac | ||
|
|
a2958c7820 | ||
|
|
7b253141c3 | ||
|
|
503bf0c314 | ||
|
|
839d4904bf | ||
|
|
4391878bab | ||
|
|
52fb944bfb | ||
|
|
83c9fa8e72 | ||
|
|
3c9437f801 | ||
|
|
9db6eeac9d | ||
|
|
8acb257abe | ||
|
|
4e201bb8f4 | ||
|
|
43f83f64ec | ||
|
|
926ab3a40b | ||
|
|
58d1d28652 | ||
|
|
6c27cf3f32 | ||
|
|
c88fba546f | ||
|
|
6fd5337cc2 | ||
|
|
fb6b73bf5f | ||
|
|
3663fe583b | ||
|
|
7ede2f8297 | ||
|
|
2688801cbd | ||
|
|
430600063c | ||
|
|
415b14fb10 | ||
|
|
c3dc8e1cd5 | ||
|
|
2d37030a40 | ||
|
|
1bd850df12 | ||
|
|
ff0a53bfad | ||
|
|
6ef3342e96 | ||
|
|
d4e2fbe6a3 | ||
|
|
941c9fff05 | ||
|
|
272a433f68 | ||
|
|
197ff321d4 | ||
|
|
12e486edd8 | ||
|
|
1dd0875323 | ||
|
|
386334b1f0 | ||
|
|
af22494425 | ||
|
|
e07ea952fc | ||
|
|
53efd63d5e | ||
|
|
10617da560 | ||
|
|
86302e8ccc | ||
|
|
60640d4fe4 | ||
|
|
22ab840439 | ||
|
|
6279b416bc | ||
|
|
fa6460b8ab | ||
|
|
cea67bc570 | ||
|
|
012f8d6e08 | ||
|
|
6adb04a364 | ||
|
|
0c2edc7c22 | ||
|
|
305b9630a0 | ||
|
|
69f32d52d1 | ||
|
|
8309068378 | ||
|
|
9bf4c8bffb | ||
|
|
0597c1e8e0 | ||
|
|
182d810911 | ||
|
|
2e0c1e8054 | ||
|
|
14f27e86c8 | ||
|
|
58d1eef502 | ||
|
|
450d163040 | ||
|
|
724d501115 | ||
|
|
82d95967e5 | ||
|
|
3f976c2ad9 | ||
|
|
36075dc835 | ||
|
|
167feee5b1 | ||
|
|
945ad95bae | ||
|
|
221f2b2490 | ||
|
|
214f96d0ca | ||
|
|
131d14f37d | ||
|
|
168f63c2d5 | ||
|
|
d971621e66 | ||
|
|
bfd3df5354 | ||
|
|
b8804d9af3 | ||
|
|
b389f48267 | ||
|
|
5348d2f821 | ||
|
|
b22b4801af | ||
|
|
5d5ca0d3c4 | ||
|
|
d9a94779f3 | ||
|
|
ab2fed3215 | ||
|
|
191bf9a3dc | ||
|
|
7e4ad3de29 | ||
|
|
4d121cfb0f | ||
|
|
945eb3714d | ||
|
|
d41ba0d551 | ||
|
|
c19c2bab15 | ||
|
|
a283584956 | ||
|
|
362da7abd0 | ||
|
|
0bb696a24e | ||
|
|
74444ae328 | ||
|
|
939305ff5a | ||
|
|
817fb5349b | ||
|
|
ef1b2b57b0 | ||
|
|
dc8a624e07 | ||
|
|
41c46ceb3c | ||
|
|
927ec67043 |
1565 changed files with 274030 additions and 148547 deletions
|
|
@ -1,5 +1,12 @@
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
# global environment variables
|
||||||
|
# '& ' syntax is a YAML thing for creating referenceable documents
|
||||||
|
env-shared: &env-shared
|
||||||
|
environment:
|
||||||
|
GOFLAGS: "-mod=vendor"
|
||||||
|
|
||||||
|
# build steps shared across different jobs
|
||||||
build-shared: &build-shared
|
build-shared: &build-shared
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
|
@ -16,45 +23,43 @@ build-shared: &build-shared
|
||||||
- run:
|
- run:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
command: |
|
command: |
|
||||||
go test -v -mod=vendor ./...
|
go test -v ./...
|
||||||
|
- run:
|
||||||
|
name: Get gox
|
||||||
|
# -mod=vendor doesn't work with go get while in source tree with go.mod in it
|
||||||
|
working_directory: "/tmp"
|
||||||
|
command: |
|
||||||
|
go get github.com/mitchellh/gox
|
||||||
- run:
|
- run:
|
||||||
name: Compile project on every platform
|
name: Compile project on every platform
|
||||||
command: |
|
command: |
|
||||||
go get github.com/mitchellh/gox
|
gox -parallel 10 -os "linux freebsd" -osarch "darwin/i386 darwin/amd64 windows/i386 windows/amd64"
|
||||||
gox -mod=vendor -parallel 10 -os "linux freebsd" -osarch "darwin/i386 darwin/amd64"
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: pkg-cache-{{ checksum "go.sum" }}-v5
|
key: pkg-cache-{{ checksum "go.sum" }}-v5
|
||||||
paths:
|
paths:
|
||||||
- ~/.cache/go-build
|
- ~/.cache/go-build
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
"golang:1.12":
|
# matrix build jobs run on every commit
|
||||||
|
# building on versioned Go and the latest one
|
||||||
|
# '<<: *' syntax is a YAML thing for including documents
|
||||||
|
"golang:1.17":
|
||||||
|
<<: *env-shared
|
||||||
<<: *build-shared
|
<<: *build-shared
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.17
|
||||||
"golang:latest":
|
"golang:latest":
|
||||||
|
<<: *env-shared
|
||||||
<<: *build-shared
|
<<: *build-shared
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:latest
|
- image: circleci/golang:latest
|
||||||
|
# release job run only when a commit is tagged
|
||||||
release:
|
release:
|
||||||
|
<<: *env-shared
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.12
|
- image: circleci/golang:1.17
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run:
|
|
||||||
name: Install snapcraft
|
|
||||||
command: |
|
|
||||||
./.circleci/install_snapcraft.sh
|
|
||||||
- run:
|
|
||||||
name: Login to snapcraft
|
|
||||||
# if you ever lose this, you can recreate it via
|
|
||||||
# `snapcraft export-login snapcraft.login && base64 snapcraft.login` and then chuck that
|
|
||||||
# in circle ci as the SNAPCRAFT_LOGIN_FILE env variable
|
|
||||||
# You'll need your ubuntu one password
|
|
||||||
command: |
|
|
||||||
echo $SNAPCRAFT_LOGIN_FILE | base64 --decode --ignore-garbage > snapcraft.login
|
|
||||||
snapcraft login --with snapcraft.login
|
|
||||||
rm snapcraft.login
|
|
||||||
- run:
|
- run:
|
||||||
name: Run gorelease
|
name: Run gorelease
|
||||||
command: |
|
command: |
|
||||||
|
|
@ -68,7 +73,8 @@ workflows:
|
||||||
version: 2
|
version: 2
|
||||||
build:
|
build:
|
||||||
jobs:
|
jobs:
|
||||||
- "golang:1.12"
|
# build matrix
|
||||||
|
- "golang:1.17"
|
||||||
- "golang:latest"
|
- "golang:latest"
|
||||||
release:
|
release:
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write|MultiEdit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; case \"$f\" in *.go) gofumpt -w \"$f\" ;; esac; } 2>/dev/null || true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.devcontainer/Dockerfile
Normal file
11
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM mcr.microsoft.com/devcontainers/go:bullseye
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN go install mvdan.cc/gofumpt@latest
|
||||||
|
ENV PATH="/root/go/bin:${PATH}"
|
||||||
|
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.0
|
||||||
|
RUN golangci-lint --version
|
||||||
54
.devcontainer/devcontainer.json
Normal file
54
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "Docker in Docker",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "./Dockerfile",
|
||||||
|
"context": "."
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/common-utils:1": {
|
||||||
|
"installZsh": "true",
|
||||||
|
"upgradePackages": "false",
|
||||||
|
"uid": "1000",
|
||||||
|
"gid": "1000",
|
||||||
|
"installOhMyZsh": "true",
|
||||||
|
"nonFreePackages": "true"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/docker-from-docker:1": {
|
||||||
|
"version": "latest",
|
||||||
|
"enableNonRootDocker": "true",
|
||||||
|
"moby": "true"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/go:1": {}
|
||||||
|
},
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
// Configure properties specific to VS Code.
|
||||||
|
"vscode": {
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {
|
||||||
|
"go.toolsManagement.checkForUpdates": "local",
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"go.gopath": "~/go",
|
||||||
|
"[go]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"go.lintTool": "golangci-lint",
|
||||||
|
"gopls": {
|
||||||
|
"formatting.gofumpt": true,
|
||||||
|
"usePlaceholders": false // add parameter placeholders when completing a function
|
||||||
|
},
|
||||||
|
"files.eol": "\n"
|
||||||
|
},
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"golang.Go"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: make this work.
|
||||||
|
// "postStartCommand": "echo \"alias gr=\\\"go run /workspaces/lazydocker/main.go\\\"\" >> ~/.zhsrc"
|
||||||
|
}
|
||||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
ko_fi: jesseduffield
|
github: [jesseduffield]
|
||||||
custom: ['https://donorbox.org/lazydocker']
|
custom: ['https://donorbox.org/lazygit']
|
||||||
|
|
|
||||||
27
.github/workflows/cd.yml
vendored
Normal file
27
.github/workflows/cd.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
name: Continuous Delivery
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Unshallow repo
|
||||||
|
run: git fetch --prune --unshallow
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.24.x
|
||||||
|
- name: Run goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v1
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: v1.17.2
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{secrets.TOKEN_GITHUB}}
|
||||||
127
.github/workflows/ci.yml
vendored
Normal file
127
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: 1.24
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
- windows-latest
|
||||||
|
name: ci - ${{matrix.os}}
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=vendor
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.24.x
|
||||||
|
- name: Cache build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||||
|
restore-keys: |
|
||||||
|
${{runner.os}}-go-
|
||||||
|
- name: Test code
|
||||||
|
run: |
|
||||||
|
bash ./test.sh
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=vendor
|
||||||
|
GOARCH: amd64
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.24.x
|
||||||
|
- name: Cache build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
|
||||||
|
restore-keys: |
|
||||||
|
${{runner.os}}-go-
|
||||||
|
- name: Build linux binary
|
||||||
|
run: |
|
||||||
|
GOOS=linux go build
|
||||||
|
- name: Build windows binary
|
||||||
|
run: |
|
||||||
|
GOOS=windows go build
|
||||||
|
- name: Build darwin binary
|
||||||
|
run: |
|
||||||
|
GOOS=darwin go build
|
||||||
|
check-codebase:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=vendor
|
||||||
|
GOARCH: amd64
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.24.x
|
||||||
|
- name: Cache build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
|
||||||
|
restore-keys: |
|
||||||
|
${{runner.os}}-go-
|
||||||
|
- name: Check Cheatsheet
|
||||||
|
run: |
|
||||||
|
go run scripts/cheatsheet/main.go check
|
||||||
|
- name: Check Vendor Directory
|
||||||
|
# ensure our vendor directory matches up with our go modules
|
||||||
|
run: |
|
||||||
|
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GOFLAGS: -mod=vendor
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 1.24.x
|
||||||
|
- name: Cache build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/go-build
|
||||||
|
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||||
|
restore-keys: |
|
||||||
|
${{runner.os}}-go-
|
||||||
|
- name: Lint
|
||||||
|
uses: golangci/golangci-lint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
- name: Format code
|
||||||
|
run: |
|
||||||
|
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||||
|
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: errors
|
||||||
|
run: golangci-lint run
|
||||||
|
if: ${{ failure() }}
|
||||||
28
.github/workflows/sponsors.yml
vendored
Normal file
28
.github/workflows/sponsors.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# see https://github.com/JamesIves/github-sponsors-readme-action
|
||||||
|
name: Generate Sponsors README
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🛎️
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Generate Sponsors 💖
|
||||||
|
uses: JamesIves/github-sponsors-readme-action@v1.2.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.TOKEN_GITHUB }}
|
||||||
|
file: "README.md"
|
||||||
|
if: ${{ github.repository == 'jesseduffield/lazydocker' }}
|
||||||
|
|
||||||
|
- name: Create Pull Request 🚀
|
||||||
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
with:
|
||||||
|
commit-message: "README.md: Update Sponsors"
|
||||||
|
title: "README.md: Update Sponsors"
|
||||||
|
author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||||
|
labels: "ignore-for-release"
|
||||||
|
delete-branch: true
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@ lazydocker*
|
||||||
TODO.md
|
TODO.md
|
||||||
Lazydocker.code-workspace
|
Lazydocker.code-workspace
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
|
||||||
27
.golangci.yml
Normal file
27
.golangci.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- thelper
|
||||||
|
- goimports
|
||||||
|
- tparallel
|
||||||
|
- wastedassign
|
||||||
|
- unparam
|
||||||
|
- prealloc
|
||||||
|
- unconvert
|
||||||
|
- exhaustive
|
||||||
|
- makezero
|
||||||
|
- nakedret
|
||||||
|
# - goconst # TODO: enable and fix issues
|
||||||
|
fast: false
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
exhaustive:
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
|
||||||
|
nakedret:
|
||||||
|
# the gods will judge me but I just don't like naked returns at all
|
||||||
|
max-func-lines: 0
|
||||||
|
|
||||||
|
run:
|
||||||
|
go: '1.21'
|
||||||
|
timeout: 10m
|
||||||
138
.goreleaser.yml
138
.goreleaser.yml
|
|
@ -9,7 +9,7 @@ builds:
|
||||||
- id: binary
|
- id: binary
|
||||||
goos:
|
goos:
|
||||||
# - freebsd
|
# - freebsd
|
||||||
# - windows # may reenable later
|
- windows
|
||||||
- darwin
|
- darwin
|
||||||
- linux
|
- linux
|
||||||
goarch:
|
goarch:
|
||||||
|
|
@ -30,9 +30,6 @@ builds:
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- 386
|
- 386
|
||||||
goarm:
|
|
||||||
- 6
|
|
||||||
- 7
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=snap
|
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=snap
|
||||||
|
|
||||||
|
|
@ -50,87 +47,86 @@ archives:
|
||||||
format: zip
|
format: zip
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: '{{ .Tag }}-next'
|
name_template: "{{ .Tag }}-next"
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
exclude:
|
exclude:
|
||||||
- '^docs:'
|
- "^docs:"
|
||||||
- '^test:'
|
- "^test:"
|
||||||
- '^bump'
|
- "^bump"
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- github:
|
- tap:
|
||||||
owner: jesseduffield
|
owner: jesseduffield
|
||||||
name: homebrew-lazydocker
|
name: homebrew-lazydocker
|
||||||
|
|
||||||
# Your app's homepage.
|
# Your app's homepage.
|
||||||
# Default is empty.
|
# Default is empty.
|
||||||
homepage: 'https://github.com/jesseduffield/lazydocker/'
|
homepage: "https://github.com/jesseduffield/lazydocker/"
|
||||||
|
|
||||||
# Your app's description.
|
# Your app's description.
|
||||||
# Default is empty.
|
# Default is empty.
|
||||||
description: 'A simple terminal UI for docker, written in Go'
|
description: "A simple terminal UI for docker, written in Go"
|
||||||
|
#snapcrafts:
|
||||||
snapcrafts:
|
# - builds:
|
||||||
- builds:
|
# - snap
|
||||||
- snap
|
#
|
||||||
|
# replacements:
|
||||||
replacements:
|
# linux: Linux
|
||||||
linux: Linux
|
# 386: x86
|
||||||
386: x86
|
# amd64: x86_64
|
||||||
amd64: x86_64
|
#
|
||||||
|
# # Wether to publish the snap to the snapcraft store.
|
||||||
# Wether to publish the snap to the snapcraft store.
|
# # Remember you need to `snapcraft login` first.
|
||||||
# Remember you need to `snapcraft login` first.
|
# # Defaults to false.
|
||||||
# Defaults to false.
|
# publish: false
|
||||||
publish: false
|
#
|
||||||
|
# # Single-line elevator pitch for your amazing snap.
|
||||||
# Single-line elevator pitch for your amazing snap.
|
# # 79 char long at most.
|
||||||
# 79 char long at most.
|
# summary: The lazier way to manage everything docker
|
||||||
summary: The lazier way to manage everything docker
|
#
|
||||||
|
# # This the description of your snap. You have a paragraph or two to tell the
|
||||||
# This the description of your snap. You have a paragraph or two to tell the
|
# # most important story about your snap. Keep it under 100 words though,
|
||||||
# most important story about your snap. Keep it under 100 words though,
|
# # we live in tweetspace and your description wants to look good in the snap
|
||||||
# we live in tweetspace and your description wants to look good in the snap
|
# # store.
|
||||||
# store.
|
# description: 'A simple terminal UI for docker, written in Go'
|
||||||
description: 'A simple terminal UI for docker, written in Go'
|
#
|
||||||
|
# # A guardrail to prevent you from releasing a snap to all your users before
|
||||||
# A guardrail to prevent you from releasing a snap to all your users before
|
# # it is ready.
|
||||||
# it is ready.
|
# # `devel` will let you release only to the `edge` and `beta` channels in the
|
||||||
# `devel` will let you release only to the `edge` and `beta` channels in the
|
# # store. `stable` will let you release also to the `candidate` and `stable`
|
||||||
# store. `stable` will let you release also to the `candidate` and `stable`
|
# # channels. More info about channels here:
|
||||||
# channels. More info about channels here:
|
# # https://snapcraft.io/docs/reference/channels
|
||||||
# https://snapcraft.io/docs/reference/channels
|
# # TODO: reset to `stable` when we've been manually reviewed: https://forum.snapcraft.io/t/request-for-classic-confinement-for-lazydocker/12155
|
||||||
# TODO: reset to `stable` when we've been manually reviewed: https://forum.snapcraft.io/t/request-for-classic-confinement-for-lazydocker/12155
|
# grade: devel
|
||||||
grade: devel
|
#
|
||||||
|
# # Snaps can be setup to follow three different confinement policies:
|
||||||
# Snaps can be setup to follow three different confinement policies:
|
# # `strict`, `devmode` and `classic`. A strict confinement where the snap
|
||||||
# `strict`, `devmode` and `classic`. A strict confinement where the snap
|
# # can only read and write in its own namespace is recommended. Extra
|
||||||
# can only read and write in its own namespace is recommended. Extra
|
# # permissions for strict snaps can be declared as `plugs` for the app, which
|
||||||
# permissions for strict snaps can be declared as `plugs` for the app, which
|
# # are explained later. More info about confinement here:
|
||||||
# are explained later. More info about confinement here:
|
# # https://snapcraft.io/docs/reference/confinement
|
||||||
# https://snapcraft.io/docs/reference/confinement
|
# confinement: classic
|
||||||
confinement: classic
|
#
|
||||||
|
# # Your app's license, based on SPDX license expressions: https://spdx.org/licenses
|
||||||
# Your app's license, based on SPDX license expressions: https://spdx.org/licenses
|
# # Default is empty.
|
||||||
# Default is empty.
|
# license: MIT
|
||||||
license: MIT
|
#
|
||||||
|
# # # Each binary built by GoReleaser is an app inside the snap. In this section
|
||||||
# # Each binary built by GoReleaser is an app inside the snap. In this section
|
# # # you can declare extra details for those binaries. It is optional.
|
||||||
# # you can declare extra details for those binaries. It is optional.
|
# # apps:
|
||||||
# apps:
|
#
|
||||||
|
# # # The name of the app must be the same name as the binary built or the snapcraft name.
|
||||||
# # The name of the app must be the same name as the binary built or the snapcraft name.
|
# # lazydocker:
|
||||||
# lazydocker:
|
#
|
||||||
|
# # # If your app requires extra permissions to work outside of its default
|
||||||
# # If your app requires extra permissions to work outside of its default
|
# # # confined space, declare them here.
|
||||||
# # confined space, declare them here.
|
# # # You can read the documentation about the available plugs and the
|
||||||
# # You can read the documentation about the available plugs and the
|
# # # things they allow:
|
||||||
# # things they allow:
|
# # # https://snapcraft.io/docs/reference/interfaces.
|
||||||
# # https://snapcraft.io/docs/reference/interfaces.
|
# # plugs: []
|
||||||
# plugs: []
|
|
||||||
|
|
|
||||||
5
CLAUDE.md
Normal file
5
CLAUDE.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Build & test
|
||||||
|
- All Go commands need `GOFLAGS=-mod=vendor` (deps are vendored, including the `jesseduffield/gocui` fork and the Docker SDK).
|
||||||
|
- Unit tests: `GOFLAGS=-mod=vendor go test ./...`
|
||||||
|
|
@ -37,6 +37,7 @@ c) if you need to bump a dependency e.g. jesseduffield/gocui, use
|
||||||
|
|
||||||
```
|
```
|
||||||
GOFLAGS= go get -u github.com/jesseduffield/gocui@master
|
GOFLAGS= go get -u github.com/jesseduffield/gocui@master
|
||||||
|
go mod tidy
|
||||||
go mod vendor
|
go mod vendor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -48,6 +49,7 @@ c) if you need to bump a dependency e.g. jesseduffield/gocui, use
|
||||||
|
|
||||||
```
|
```
|
||||||
go get -u github.com/jesseduffield/gocui@master
|
go get -u github.com/jesseduffield/gocui@master
|
||||||
|
go mod tidy
|
||||||
go mod vendor
|
go mod vendor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
ARG BASE_IMAGE_BUILDER=golang
|
ARG BASE_IMAGE_BUILDER=golang
|
||||||
ARG ALPINE_VERSION=3.10
|
ARG ALPINE_VERSION=3.20
|
||||||
ARG GO_VERSION=1.12.6
|
ARG GO_VERSION=1.23
|
||||||
|
|
||||||
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||||
ARG GOARCH=amd64
|
ARG GOARCH=amd64
|
||||||
|
|
@ -19,7 +19,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} GOARM=${GOARM} \
|
||||||
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS docker-builder
|
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS docker-builder
|
||||||
ARG GOARCH=amd64
|
ARG GOARCH=amd64
|
||||||
ARG GOARM
|
ARG GOARM
|
||||||
ARG DOCKER_VERSION=v18.09.7
|
ARG DOCKER_VERSION=v27.0.3
|
||||||
RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev
|
RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev
|
||||||
WORKDIR /go/src/github.com/docker/cli
|
WORKDIR /go/src/github.com/docker/cli
|
||||||
RUN git clone --branch ${DOCKER_VERSION} --single-branch --depth 1 https://github.com/docker/cli.git . > /dev/null 2>&1
|
RUN git clone --branch ${DOCKER_VERSION} --single-branch --depth 1 https://github.com/docker/cli.git . > /dev/null 2>&1
|
||||||
|
|
@ -46,4 +46,4 @@ LABEL \
|
||||||
org.opencontainers.image.description="The lazier way to manage everything docker"
|
org.opencontainers.image.description="The lazier way to manage everything docker"
|
||||||
ENTRYPOINT [ "/bin/lazydocker" ]
|
ENTRYPOINT [ "/bin/lazydocker" ]
|
||||||
COPY --from=docker-builder /go/src/github.com/docker/cli/build/docker /bin/docker
|
COPY --from=docker-builder /go/src/github.com/docker/cli/build/docker /bin/docker
|
||||||
COPY --from=builder /tmp/gobuild/lazydocker /bin/lazydocker
|
COPY --from=builder /tmp/gobuild/lazydocker /bin/lazydocker
|
||||||
|
|
|
||||||
141
README.md
141
README.md
File diff suppressed because one or more lines are too long
BIN
assets/subble-job-ad.jpg
Normal file
BIN
assets/subble-job-ad.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
BIN
assets/subble.webp
Normal file
BIN
assets/subble.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
assets/tuple.png
Normal file
BIN
assets/tuple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
assets/warp2.png
Normal file
BIN
assets/warp2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
0
config/config.yml
Normal file
0
config/config.yml
Normal file
2170
coverage.txt
2170
coverage.txt
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
lazydocker:
|
lazydocker:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
112
docs/Config.md
112
docs/Config.md
|
|
@ -7,61 +7,92 @@ The location of the user config will differ depending on your OS. You can open i
|
||||||
Changes to the user config will only take place after closing and re-opening lazydocker
|
Changes to the user config will only take place after closing and re-opening lazydocker
|
||||||
|
|
||||||
### Locations:
|
### Locations:
|
||||||
|
|
||||||
- OSX: `~/Library/Application Support/jesseduffield/lazydocker/config.yml`
|
- OSX: `~/Library/Application Support/jesseduffield/lazydocker/config.yml`
|
||||||
- Linux: `~/.config/jesseduffield/lazydocker/config.yml`
|
- Linux: `~/.config/lazydocker/config.yml`
|
||||||
- Windows: `C:\\Users\\<User>\\AppData\\Roaming\\jesseduffield\\lazydocker\\config.yml` (I think)
|
- Windows: `C:\Users\<User>\AppData\Roaming\lazydocker\config.yml`
|
||||||
|
|
||||||
|
JSON schema is available for `config.yml` so that IntelliSense in Visual Studio Code
|
||||||
|
(completion and error checking) is automatically enabled when the [YAML Red Hat][yaml]
|
||||||
|
extension is installed. However, note that automatic schema detection only works
|
||||||
|
if your config file is in one of the standard paths mentioned above. If you
|
||||||
|
override the path to the file, you can still make IntelliSense work by adding
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# yaml-language-server: $schema=https://json.schemastore.org/lazydocker.json
|
||||||
|
```
|
||||||
|
|
||||||
|
to the top of your config file or via [Visual Studio Code settings.json config][settings].
|
||||||
|
|
||||||
|
[yaml]: https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml
|
||||||
|
[settings]: https://github.com/redhat-developer/vscode-yaml#associating-a-schema-to-a-glob-pattern-via-yamlschemas
|
||||||
|
|
||||||
## Default:
|
## Default:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
gui:
|
gui:
|
||||||
scrollHeight: 2
|
scrollHeight: 2
|
||||||
|
language: "auto" # one of 'auto' | 'en' | 'pl' | 'nl' | 'de' | 'tr'
|
||||||
|
border: "rounded" # one of 'rounded' | 'single' | 'double' | 'hidden'
|
||||||
theme:
|
theme:
|
||||||
activeBorderColor:
|
activeBorderColor:
|
||||||
- green
|
- green
|
||||||
- bold
|
- bold
|
||||||
inactiveBorderColor:
|
inactiveBorderColor:
|
||||||
- white
|
- white
|
||||||
|
selectedLineBgColor:
|
||||||
|
- blue
|
||||||
optionsTextColor:
|
optionsTextColor:
|
||||||
- blue
|
- blue
|
||||||
returnImmediately: false
|
returnImmediately: false
|
||||||
wrapMainPanel: false
|
wrapMainPanel: true
|
||||||
reporting: undetermined
|
# Side panel width as a ratio of the screen's width
|
||||||
|
sidePanelWidth: 0.333
|
||||||
|
# Determines whether we show the bottom line (the one containing keybinding
|
||||||
|
# info and the status of the app).
|
||||||
|
showBottomLine: true
|
||||||
|
# When true, increases vertical space used by focused side panel,
|
||||||
|
# creating an accordion effect
|
||||||
|
expandFocusedSidePanel: false
|
||||||
|
# Determines which screen mode will be used on startup
|
||||||
|
screenMode: "normal" # one of 'normal' | 'half' | 'fullscreen'
|
||||||
|
# Determines the style of the container status and container health display in the
|
||||||
|
# containers panel. "long": full words (default), "short": one or two characters,
|
||||||
|
# "icon": unicode emoji.
|
||||||
|
containerStatusHealthStyle: "long"
|
||||||
|
logs:
|
||||||
|
timestamps: false
|
||||||
|
since: '60m' # set to '' to show all logs
|
||||||
|
tail: '' # set to 200 to show last 200 lines of logs
|
||||||
commandTemplates:
|
commandTemplates:
|
||||||
dockerCompose: docker-compose
|
dockerCompose: docker compose # Determines the Docker Compose command to run, referred to as .DockerCompose in commandTemplates
|
||||||
restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
|
restartService: '{{ .DockerCompose }} restart {{ .Service.Name }}'
|
||||||
|
up: '{{ .DockerCompose }} up -d'
|
||||||
|
down: '{{ .DockerCompose }} down'
|
||||||
|
downWithVolumes: '{{ .DockerCompose }} down --volumes'
|
||||||
|
upService: '{{ .DockerCompose }} up -d {{ .Service.Name }}'
|
||||||
|
startService: '{{ .DockerCompose }} start {{ .Service.Name }}'
|
||||||
stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}'
|
stopService: '{{ .DockerCompose }} stop {{ .Service.Name }}'
|
||||||
serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}'
|
serviceLogs: '{{ .DockerCompose }} logs --since=60m --follow {{ .Service.Name }}'
|
||||||
viewServiceLogs: '{{ .DockerCompose }} logs --follow {{ .Service.Name }}'
|
viewServiceLogs: '{{ .DockerCompose }} logs --follow {{ .Service.Name }}'
|
||||||
rebuildService: '{{ .DockerCompose }} up -d --build {{ .Service.Name }}'
|
rebuildService: '{{ .DockerCompose }} up -d --build {{ .Service.Name }}'
|
||||||
recreateService: '{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}'
|
recreateService: '{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}'
|
||||||
viewContainerLogs: docker logs --timestamps --follow --since=60m {{ .Container.ID
|
|
||||||
}}
|
|
||||||
containerLogs: docker logs --timestamps --follow --since=60m {{ .Container.ID }}
|
|
||||||
allLogs: '{{ .DockerCompose }} logs --tail=300 --follow'
|
allLogs: '{{ .DockerCompose }} logs --tail=300 --follow'
|
||||||
viewAlLogs: '{{ .DockerCompose }} logs'
|
viewAlLogs: '{{ .DockerCompose }} logs'
|
||||||
dockerComposeConfig: '{{ .DockerCompose }} config'
|
dockerComposeConfig: '{{ .DockerCompose }} config'
|
||||||
checkDockerComposeConfig: '{{ .DockerCompose }} config --quiet'
|
checkDockerComposeConfig: '{{ .DockerCompose }} config --quiet'
|
||||||
serviceTop: '{{ .DockerCompose }} top {{ .Service.Name }}'
|
serviceTop: '{{ .DockerCompose }} top {{ .Service.Name }}'
|
||||||
customCommands:
|
|
||||||
containers:
|
|
||||||
- name: bash
|
|
||||||
attach: true
|
|
||||||
command: docker exec -it {{ .Container.ID }} /bin/sh
|
|
||||||
serviceNames: []
|
|
||||||
oS:
|
oS:
|
||||||
openCommand: open {{filename}}
|
openCommand: open {{filename}}
|
||||||
openLinkCommand: open {{link}}
|
openLinkCommand: open {{link}}
|
||||||
update:
|
|
||||||
dockerRefreshInterval: 100ms
|
|
||||||
stats:
|
stats:
|
||||||
graphs:
|
graphs:
|
||||||
- caption: CPU (%)
|
- caption: CPU (%)
|
||||||
statPath: DerivedStats.CPUPercentage
|
statPath: DerivedStats.CPUPercentage
|
||||||
color: blue
|
color: blue
|
||||||
- caption: Memory (%)
|
- caption: Memory (%)
|
||||||
statPath: DerivedStats.MemoryPercentage
|
statPath: DerivedStats.MemoryPercentage
|
||||||
color: green
|
color: green
|
||||||
```
|
```
|
||||||
|
|
||||||
## To see what all of the config options mean, and what other options you can set, see [here](https://godoc.org/github.com/jesseduffield/lazydocker/pkg/config)
|
## To see what all of the config options mean, and what other options you can set, see [here](https://godoc.org/github.com/jesseduffield/lazydocker/pkg/config)
|
||||||
|
|
@ -83,3 +114,32 @@ The available attributes are:
|
||||||
- bold
|
- bold
|
||||||
- reverse # useful for high-contrast
|
- reverse # useful for high-contrast
|
||||||
- underline
|
- underline
|
||||||
|
|
||||||
|
## Custom Commands
|
||||||
|
|
||||||
|
You can add custom commands like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
customCommands:
|
||||||
|
containers:
|
||||||
|
- name: bash
|
||||||
|
attach: true
|
||||||
|
command: 'docker exec -it {{ .Container.ID }} bash'
|
||||||
|
serviceNames: []
|
||||||
|
```
|
||||||
|
|
||||||
|
You may use the following go templates (such as `{{ .Container.ID }}` above) in your commands:
|
||||||
|
- `{{ .DockerCompose }}`: the docker compose command (default: `docker-compose`)
|
||||||
|
- [`{{ .Container }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Container) and its fields. For example: `{{ .Container.Container.ImageID }}`
|
||||||
|
- [`{{ .Service }}`](https://pkg.go.dev/github.com/jesseduffield/lazydocker@v0.20.0/pkg/commands#Service) and its fields. For example: `{{ .Service.Name }}`
|
||||||
|
|
||||||
|
## Replacements
|
||||||
|
|
||||||
|
You can add replacements like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
replacements:
|
||||||
|
imageNamePrefixes:
|
||||||
|
'123456789012.dkr.ecr.us-east-1.amazonaws.com': '<prod>'
|
||||||
|
'923456789999.dkr.ecr.us-east-1.amazonaws.com': '<dev>'
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,97 @@
|
||||||
# Lazydocker Menü
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
|
# Lazydocker menü
|
||||||
|
|
||||||
## Projekt
|
## Projekt
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>e</kbd>: bearbeite lazydocker Konfiguration
|
<kbd>e</kbd>: bearbeite lazydocker Konfiguration
|
||||||
<kbd>o</kbd>: öffne lazydocker Konfiguration
|
<kbd>o</kbd>: öffne lazydocker Konfiguration
|
||||||
<kbd>[</kbd>: vorheriges Tab
|
|
||||||
<kbd>]</kbd>: nächstes Tab
|
|
||||||
<kbd>m</kbd>: zeige Protokolle
|
<kbd>m</kbd>: zeige Protokolle
|
||||||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
|
<kbd>]</kbd>: nächstes Tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Container
|
## Container
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: vorheriges Tab
|
|
||||||
<kbd>]</kbd>: nächstes Tab
|
|
||||||
<kbd>d</kbd>: entfernen
|
<kbd>d</kbd>: entfernen
|
||||||
<kbd>e</kbd>: Hide/Show stopped containers
|
<kbd>e</kbd>: hide/show stopped containers
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>s</kbd>: anhalten
|
<kbd>s</kbd>: anhalten
|
||||||
<kbd>r</kbd>: neustarten
|
<kbd>r</kbd>: neustarten
|
||||||
<kbd>a</kbd>: anbinden
|
<kbd>a</kbd>: anbinden
|
||||||
<kbd>m</kbd>: zeige Protokolle
|
<kbd>m</kbd>: zeige Protokolle
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
|
<kbd>]</kbd>: nächstes Tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Dienste
|
## Dienste
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>u</kbd>: up service
|
||||||
<kbd>d</kbd>: entferne Container
|
<kbd>d</kbd>: entferne Container
|
||||||
<kbd>s</kbd>: anhalten
|
<kbd>s</kbd>: anhalten
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>r</kbd>: neustarten
|
<kbd>r</kbd>: neustarten
|
||||||
|
<kbd>S</kbd>: start
|
||||||
<kbd>a</kbd>: anbinden
|
<kbd>a</kbd>: anbinden
|
||||||
<kbd>m</kbd>: zeige Protokolle
|
<kbd>m</kbd>: zeige Protokolle
|
||||||
<kbd>[</kbd>: vorheriges Tab
|
<kbd>U</kbd>: up project
|
||||||
<kbd>]</kbd>: nächstes Tab
|
<kbd>D</kbd>: down project
|
||||||
<kbd>R</kbd>: zeige Neustartoptionen
|
<kbd>R</kbd>: zeige Neustartoptionen
|
||||||
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
|
<kbd>]</kbd>: nächstes Tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: vorheriges Tab
|
|
||||||
<kbd>]</kbd>: nächstes Tab
|
|
||||||
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
||||||
<kbd>d</kbd>: entferne Image
|
<kbd>d</kbd>: entferne Image
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
|
||||||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
|
<kbd>]</kbd>: nächstes Tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Volumes
|
## Volumes
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: vorheriges Tab
|
|
||||||
<kbd>]</kbd>: nächstes Tab
|
|
||||||
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
||||||
<kbd>d</kbd>: entferne Volume
|
<kbd>d</kbd>: entferne Volume
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
|
||||||
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
|
<kbd>]</kbd>: nächstes Tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Netzwerk
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: führe vordefinierten benutzerdefinierten Befehl aus
|
||||||
|
<kbd>d</kbd>: entferne Netzwerk
|
||||||
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>enter</kbd>: fokussieren aufs Hauptpanel
|
||||||
|
<kbd>[</kbd>: vorheriges Tab
|
||||||
|
<kbd>]</kbd>: nächstes Tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Haupt
|
## Haupt
|
||||||
|
|
@ -72,3 +99,16 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: zurück
|
<kbd>esc</kbd>: zurück
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Global
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
# Lazydocker menu
|
# Lazydocker menu
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
@ -5,66 +7,91 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>e</kbd>: edit lazydocker config
|
<kbd>e</kbd>: edit lazydocker config
|
||||||
<kbd>o</kbd>: open lazydocker config
|
<kbd>o</kbd>: open lazydocker config
|
||||||
<kbd>[</kbd>: previous tab
|
|
||||||
<kbd>]</kbd>: next tab
|
|
||||||
<kbd>m</kbd>: view logs
|
<kbd>m</kbd>: view logs
|
||||||
<kbd>enter</kbd>: focus main panel
|
<kbd>enter</kbd>: focus main panel
|
||||||
|
<kbd>[</kbd>: previous tab
|
||||||
|
<kbd>]</kbd>: next tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Containers
|
## Containers
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: previous tab
|
|
||||||
<kbd>]</kbd>: next tab
|
|
||||||
<kbd>d</kbd>: remove
|
<kbd>d</kbd>: remove
|
||||||
<kbd>e</kbd>: Hide/Show stopped containers
|
<kbd>e</kbd>: hide/show stopped containers
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>s</kbd>: stop
|
<kbd>s</kbd>: stop
|
||||||
<kbd>r</kbd>: restart
|
<kbd>r</kbd>: restart
|
||||||
<kbd>a</kbd>: attach
|
<kbd>a</kbd>: attach
|
||||||
<kbd>m</kbd>: view logs
|
<kbd>m</kbd>: view logs
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
<kbd>c</kbd>: run predefined custom command
|
<kbd>c</kbd>: run predefined custom command
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: focus main panel
|
<kbd>enter</kbd>: focus main panel
|
||||||
|
<kbd>[</kbd>: previous tab
|
||||||
|
<kbd>]</kbd>: next tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>u</kbd>: up service
|
||||||
<kbd>d</kbd>: remove containers
|
<kbd>d</kbd>: remove containers
|
||||||
<kbd>s</kbd>: stop
|
<kbd>s</kbd>: stop
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>r</kbd>: restart
|
<kbd>r</kbd>: restart
|
||||||
|
<kbd>S</kbd>: start
|
||||||
<kbd>a</kbd>: attach
|
<kbd>a</kbd>: attach
|
||||||
<kbd>m</kbd>: view logs
|
<kbd>m</kbd>: view logs
|
||||||
<kbd>[</kbd>: previous tab
|
<kbd>U</kbd>: up project
|
||||||
<kbd>]</kbd>: next tab
|
<kbd>D</kbd>: down project
|
||||||
<kbd>R</kbd>: view restart options
|
<kbd>R</kbd>: view restart options
|
||||||
<kbd>c</kbd>: run predefined custom command
|
<kbd>c</kbd>: run predefined custom command
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: focus main panel
|
<kbd>enter</kbd>: focus main panel
|
||||||
|
<kbd>[</kbd>: previous tab
|
||||||
|
<kbd>]</kbd>: next tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: previous tab
|
|
||||||
<kbd>]</kbd>: next tab
|
|
||||||
<kbd>c</kbd>: run predefined custom command
|
<kbd>c</kbd>: run predefined custom command
|
||||||
<kbd>d</kbd>: remove image
|
<kbd>d</kbd>: remove image
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: run predefined custom command
|
|
||||||
<kbd>enter</kbd>: focus main panel
|
<kbd>enter</kbd>: focus main panel
|
||||||
|
<kbd>[</kbd>: previous tab
|
||||||
|
<kbd>]</kbd>: next tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Volumes
|
## Volumes
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: previous tab
|
|
||||||
<kbd>]</kbd>: next tab
|
|
||||||
<kbd>c</kbd>: run predefined custom command
|
<kbd>c</kbd>: run predefined custom command
|
||||||
<kbd>d</kbd>: remove volume
|
<kbd>d</kbd>: remove volume
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: run predefined custom command
|
|
||||||
<kbd>enter</kbd>: focus main panel
|
<kbd>enter</kbd>: focus main panel
|
||||||
|
<kbd>[</kbd>: previous tab
|
||||||
|
<kbd>]</kbd>: next tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Networks
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: run predefined custom command
|
||||||
|
<kbd>d</kbd>: remove network
|
||||||
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>enter</kbd>: focus main panel
|
||||||
|
<kbd>[</kbd>: previous tab
|
||||||
|
<kbd>]</kbd>: next tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Main
|
## Main
|
||||||
|
|
@ -72,3 +99,16 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: return
|
<kbd>esc</kbd>: return
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Global
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
|
||||||
114
docs/keybindings/Keybindings_es.md
Normal file
114
docs/keybindings/Keybindings_es.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
|
# Lazydocker menú
|
||||||
|
|
||||||
|
## Proyecto
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>e</kbd>: editar configuración de lazydocker
|
||||||
|
<kbd>o</kbd>: abrir configuración de lazydocker
|
||||||
|
<kbd>m</kbd>: ver logs
|
||||||
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
|
<kbd>[</kbd>: anterior pestaña
|
||||||
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Contenedores
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>d</kbd>: borrar
|
||||||
|
<kbd>e</kbd>: esconder/mostrar contenedores parados
|
||||||
|
<kbd>p</kbd>: pausa
|
||||||
|
<kbd>s</kbd>: parar
|
||||||
|
<kbd>r</kbd>: reiniciar
|
||||||
|
<kbd>a</kbd>: attach
|
||||||
|
<kbd>m</kbd>: ver logs
|
||||||
|
<kbd>E</kbd>: ejecutar shell
|
||||||
|
<kbd>c</kbd>: ejecutar comando personalizado
|
||||||
|
<kbd>b</kbd>: ver comandos masivos
|
||||||
|
<kbd>w</kbd>: abrir en navegador (first port is http)
|
||||||
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
|
<kbd>[</kbd>: anterior pestaña
|
||||||
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Servicios
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>u</kbd>: levantar servicio
|
||||||
|
<kbd>d</kbd>: borrar contenedores
|
||||||
|
<kbd>s</kbd>: parar
|
||||||
|
<kbd>p</kbd>: pausa
|
||||||
|
<kbd>r</kbd>: reiniciar
|
||||||
|
<kbd>S</kbd>: iniciar
|
||||||
|
<kbd>a</kbd>: attach
|
||||||
|
<kbd>m</kbd>: ver logs
|
||||||
|
<kbd>U</kbd>: levantar proyecto
|
||||||
|
<kbd>D</kbd>: dar de baja el proyecto
|
||||||
|
<kbd>R</kbd>: ver opciones de reinicio
|
||||||
|
<kbd>c</kbd>: ejecutar comando personalizado
|
||||||
|
<kbd>b</kbd>: ver comandos masivos
|
||||||
|
<kbd>E</kbd>: ejecutar shell
|
||||||
|
<kbd>w</kbd>: abrir en navegador (first port is http)
|
||||||
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
|
<kbd>[</kbd>: anterior pestaña
|
||||||
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Imágenes
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: ejecutar comando personalizado
|
||||||
|
<kbd>d</kbd>: limpiar imagen
|
||||||
|
<kbd>b</kbd>: ver comandos masivos
|
||||||
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
|
<kbd>[</kbd>: anterior pestaña
|
||||||
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Volúmenes
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: ejecutar comando personalizado
|
||||||
|
<kbd>d</kbd>: limpiar volúmen
|
||||||
|
<kbd>b</kbd>: ver comandos masivos
|
||||||
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
|
<kbd>[</kbd>: anterior pestaña
|
||||||
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Redes
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: ejecutar comando personalizado
|
||||||
|
<kbd>d</kbd>: limpiar red
|
||||||
|
<kbd>b</kbd>: ver comandos masivos
|
||||||
|
<kbd>enter</kbd>: enfocar panel principal
|
||||||
|
<kbd>[</kbd>: anterior pestaña
|
||||||
|
<kbd>]</kbd>: siguiente pestaña
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Inicio
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>esc</kbd>: regresar
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Global
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
114
docs/keybindings/Keybindings_fr.md
Normal file
114
docs/keybindings/Keybindings_fr.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
|
# Lazydocker menu
|
||||||
|
|
||||||
|
## Projet
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>e</kbd>: modifier la configuration lazydocker
|
||||||
|
<kbd>o</kbd>: ouvrir la configuration lazydocker
|
||||||
|
<kbd>m</kbd>: voir les enregistrements
|
||||||
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
|
<kbd>[</kbd>: onglet précédent
|
||||||
|
<kbd>]</kbd>: onglet suivant
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Conteneurs
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>d</kbd>: supprimer
|
||||||
|
<kbd>e</kbd>: cacher/montrer les conteneurs arrêtés
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
|
<kbd>s</kbd>: arrêter
|
||||||
|
<kbd>r</kbd>: redémarrer
|
||||||
|
<kbd>a</kbd>: attacher
|
||||||
|
<kbd>m</kbd>: voir les enregistrements
|
||||||
|
<kbd>E</kbd>: exécuter le shell
|
||||||
|
<kbd>c</kbd>: exécuter une commande prédéfinie
|
||||||
|
<kbd>b</kbd>: voir les commandes groupées
|
||||||
|
<kbd>w</kbd>: ouvrir dans le navigateur (le premier port est http)
|
||||||
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
|
<kbd>[</kbd>: onglet précédent
|
||||||
|
<kbd>]</kbd>: onglet suivant
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>u</kbd>: up service
|
||||||
|
<kbd>d</kbd>: supprimer les conteneurs
|
||||||
|
<kbd>s</kbd>: arrêter
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
|
<kbd>r</kbd>: redémarrer
|
||||||
|
<kbd>S</kbd>: démarrer
|
||||||
|
<kbd>a</kbd>: attacher
|
||||||
|
<kbd>m</kbd>: voir les enregistrements
|
||||||
|
<kbd>U</kbd>: up project
|
||||||
|
<kbd>D</kbd>: down project
|
||||||
|
<kbd>R</kbd>: voir les options de redémarrage
|
||||||
|
<kbd>c</kbd>: exécuter une commande prédéfinie
|
||||||
|
<kbd>b</kbd>: voir les commandes groupées
|
||||||
|
<kbd>E</kbd>: exécuter le shell
|
||||||
|
<kbd>w</kbd>: ouvrir dans le navigateur (le premier port est http)
|
||||||
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
|
<kbd>[</kbd>: onglet précédent
|
||||||
|
<kbd>]</kbd>: onglet suivant
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: exécuter une commande prédéfinie
|
||||||
|
<kbd>d</kbd>: supprimer l'image
|
||||||
|
<kbd>b</kbd>: voir les commandes groupées
|
||||||
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
|
<kbd>[</kbd>: onglet précédent
|
||||||
|
<kbd>]</kbd>: onglet suivant
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: exécuter une commande prédéfinie
|
||||||
|
<kbd>d</kbd>: supprimer le volume
|
||||||
|
<kbd>b</kbd>: voir les commandes groupées
|
||||||
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
|
<kbd>[</kbd>: onglet précédent
|
||||||
|
<kbd>]</kbd>: onglet suivant
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Réseaux
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: exécuter une commande prédéfinie
|
||||||
|
<kbd>d</kbd>: supprimer le réseau
|
||||||
|
<kbd>b</kbd>: voir les commandes groupées
|
||||||
|
<kbd>enter</kbd>: focus panneau principal
|
||||||
|
<kbd>[</kbd>: onglet précédent
|
||||||
|
<kbd>]</kbd>: onglet suivant
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Principal
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>esc</kbd>: retour
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Global
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
# Lazydocker menu
|
# Lazydocker menu
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
@ -5,70 +7,108 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>e</kbd>: verander de lazydocker configuratie
|
<kbd>e</kbd>: verander de lazydocker configuratie
|
||||||
<kbd>o</kbd>: open de lazydocker configuratie
|
<kbd>o</kbd>: open de lazydocker configuratie
|
||||||
|
<kbd>m</kbd>: bekijk logs
|
||||||
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
<kbd>[</kbd>: vorige tab
|
<kbd>[</kbd>: vorige tab
|
||||||
<kbd>]</kbd>: volgende tab
|
<kbd>]</kbd>: volgende tab
|
||||||
<kbd>m</kbd>: bekijk logs
|
<kbd>/</kbd>: filter list
|
||||||
<kbd>enter</kbd>: focus hooft panneel
|
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Containers
|
## Containers
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: vorige tab
|
|
||||||
<kbd>]</kbd>: volgende tab
|
|
||||||
<kbd>d</kbd>: verwijder
|
<kbd>d</kbd>: verwijder
|
||||||
<kbd>e</kbd>: Verberg gestopte containers
|
<kbd>e</kbd>: verberg gestopte containers
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>s</kbd>: stop
|
<kbd>s</kbd>: stop
|
||||||
<kbd>r</kbd>: herstart
|
<kbd>r</kbd>: herstart
|
||||||
<kbd>a</kbd>: verbinden
|
<kbd>a</kbd>: verbinden
|
||||||
<kbd>m</kbd>: bekijk logs
|
<kbd>m</kbd>: bekijk logs
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>enter</kbd>: focus hooft panneel
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
|
<kbd>[</kbd>: vorige tab
|
||||||
|
<kbd>]</kbd>: volgende tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Diensten
|
## Diensten
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>u</kbd>: up service
|
||||||
<kbd>d</kbd>: verwijder containers
|
<kbd>d</kbd>: verwijder containers
|
||||||
<kbd>s</kbd>: stop
|
<kbd>s</kbd>: stop
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>r</kbd>: herstart
|
<kbd>r</kbd>: herstart
|
||||||
|
<kbd>S</kbd>: start
|
||||||
<kbd>a</kbd>: verbinden
|
<kbd>a</kbd>: verbinden
|
||||||
<kbd>m</kbd>: bekijk logs
|
<kbd>m</kbd>: bekijk logs
|
||||||
<kbd>[</kbd>: vorige tab
|
<kbd>U</kbd>: up project
|
||||||
<kbd>]</kbd>: volgende tab
|
<kbd>D</kbd>: down project
|
||||||
<kbd>R</kbd>: bekijk herstart opties
|
<kbd>R</kbd>: bekijk herstart opties
|
||||||
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>enter</kbd>: focus hooft panneel
|
<kbd>E</kbd>: exec shell
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
|
<kbd>[</kbd>: vorige tab
|
||||||
|
<kbd>]</kbd>: volgende tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: vorige tab
|
|
||||||
<kbd>]</kbd>: volgende tab
|
|
||||||
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
||||||
<kbd>d</kbd>: verwijder image
|
<kbd>d</kbd>: verwijder image
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
<kbd>enter</kbd>: focus hooft panneel
|
<kbd>[</kbd>: vorige tab
|
||||||
|
<kbd>]</kbd>: volgende tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Volumes
|
## Volumes
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: vorige tab
|
|
||||||
<kbd>]</kbd>: volgende tab
|
|
||||||
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
||||||
<kbd>d</kbd>: verwijder volume
|
<kbd>d</kbd>: verwijder volume
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
<kbd>enter</kbd>: focus hooft panneel
|
<kbd>[</kbd>: vorige tab
|
||||||
|
<kbd>]</kbd>: volgende tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Hooft
|
## Networks
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: draai een vooraf bedacht aangepaste opdracht
|
||||||
|
<kbd>d</kbd>: verwijder network
|
||||||
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>enter</kbd>: focus hoofdpaneel
|
||||||
|
<kbd>[</kbd>: vorige tab
|
||||||
|
<kbd>]</kbd>: volgende tab
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Hoofd
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: terug
|
<kbd>esc</kbd>: terug
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Globaal
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
# Lazydocker menu
|
# Lazydocker menu
|
||||||
|
|
||||||
## Projekt
|
## Projekt
|
||||||
|
|
@ -5,66 +7,91 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>e</kbd>: edytuj konfigurację
|
<kbd>e</kbd>: edytuj konfigurację
|
||||||
<kbd>o</kbd>: otwórz konfigurację
|
<kbd>o</kbd>: otwórz konfigurację
|
||||||
<kbd>[</kbd>: poprzednia zakładka
|
|
||||||
<kbd>]</kbd>: następna zakładka
|
|
||||||
<kbd>m</kbd>: pokaż logi
|
<kbd>m</kbd>: pokaż logi
|
||||||
<kbd>enter</kbd>: skup na głównym panelu
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
|
<kbd>]</kbd>: następna zakładka
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Kontenery
|
## Kontenery
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: poprzednia zakładka
|
|
||||||
<kbd>]</kbd>: następna zakładka
|
|
||||||
<kbd>d</kbd>: usuń
|
<kbd>d</kbd>: usuń
|
||||||
<kbd>e</kbd>: Hide/Show stopped containers
|
<kbd>e</kbd>: hide/show stopped containers
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>s</kbd>: zatrzymaj
|
<kbd>s</kbd>: zatrzymaj
|
||||||
<kbd>r</kbd>: restartuj
|
<kbd>r</kbd>: restartuj
|
||||||
<kbd>a</kbd>: przyczep
|
<kbd>a</kbd>: przyczep
|
||||||
<kbd>m</kbd>: pokaż logi
|
<kbd>m</kbd>: pokaż logi
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: skup na głównym panelu
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
|
<kbd>]</kbd>: następna zakładka
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Serwisy
|
## Serwisy
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>u</kbd>: up service
|
||||||
<kbd>d</kbd>: usuń kontenery
|
<kbd>d</kbd>: usuń kontenery
|
||||||
<kbd>s</kbd>: zatrzymaj
|
<kbd>s</kbd>: zatrzymaj
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>r</kbd>: restartuj
|
<kbd>r</kbd>: restartuj
|
||||||
|
<kbd>S</kbd>: start
|
||||||
<kbd>a</kbd>: przyczep
|
<kbd>a</kbd>: przyczep
|
||||||
<kbd>m</kbd>: pokaż logi
|
<kbd>m</kbd>: pokaż logi
|
||||||
<kbd>[</kbd>: poprzednia zakładka
|
<kbd>U</kbd>: up project
|
||||||
<kbd>]</kbd>: następna zakładka
|
<kbd>D</kbd>: down project
|
||||||
<kbd>R</kbd>: pokaż opcje restartu
|
<kbd>R</kbd>: pokaż opcje restartu
|
||||||
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: skup na głównym panelu
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
|
<kbd>]</kbd>: następna zakładka
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Obrazy
|
## Obrazy
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: poprzednia zakładka
|
|
||||||
<kbd>]</kbd>: następna zakładka
|
|
||||||
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
||||||
<kbd>d</kbd>: usuń obraz
|
<kbd>d</kbd>: usuń obraz
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
|
||||||
<kbd>enter</kbd>: skup na głównym panelu
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
|
<kbd>]</kbd>: następna zakładka
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Wolumeny
|
## Wolumeny
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: poprzednia zakładka
|
|
||||||
<kbd>]</kbd>: następna zakładka
|
|
||||||
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
||||||
<kbd>d</kbd>: usuń wolumen
|
<kbd>d</kbd>: usuń wolumen
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
|
||||||
<kbd>enter</kbd>: skup na głównym panelu
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
|
<kbd>]</kbd>: następna zakładka
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Sieci
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: wykonaj predefiniowaną własną komende
|
||||||
|
<kbd>d</kbd>: usuń sieci
|
||||||
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>enter</kbd>: skup na głównym panelu
|
||||||
|
<kbd>[</kbd>: poprzednia zakładka
|
||||||
|
<kbd>]</kbd>: następna zakładka
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Główne
|
## Główne
|
||||||
|
|
@ -72,3 +99,16 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: powrót
|
<kbd>esc</kbd>: powrót
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Globalne
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
|
||||||
114
docs/keybindings/Keybindings_pt.md
Normal file
114
docs/keybindings/Keybindings_pt.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
|
# Lazydocker menu
|
||||||
|
|
||||||
|
## Projeto
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>e</kbd>: editar configuração do lazydocker
|
||||||
|
<kbd>o</kbd>: abrir configuração do lazydocker
|
||||||
|
<kbd>m</kbd>: ver logs
|
||||||
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
|
<kbd>[</kbd>: aba anterior
|
||||||
|
<kbd>]</kbd>: próxima aba
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Contêineres
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>d</kbd>: remover
|
||||||
|
<kbd>e</kbd>: ocultar/mostrar contêineres parados
|
||||||
|
<kbd>p</kbd>: pausar
|
||||||
|
<kbd>s</kbd>: parar
|
||||||
|
<kbd>r</kbd>: reiniciar
|
||||||
|
<kbd>a</kbd>: anexar
|
||||||
|
<kbd>m</kbd>: ver logs
|
||||||
|
<kbd>E</kbd>: executar shell
|
||||||
|
<kbd>c</kbd>: executar comando personalizado predefinido
|
||||||
|
<kbd>b</kbd>: ver comandos em massa
|
||||||
|
<kbd>w</kbd>: abrir no navegador (primeira porta é http)
|
||||||
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
|
<kbd>[</kbd>: aba anterior
|
||||||
|
<kbd>]</kbd>: próxima aba
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Serviços
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>u</kbd>: subir serviço
|
||||||
|
<kbd>d</kbd>: remover contêineres
|
||||||
|
<kbd>s</kbd>: parar
|
||||||
|
<kbd>p</kbd>: pausar
|
||||||
|
<kbd>r</kbd>: reiniciar
|
||||||
|
<kbd>S</kbd>: iniciar
|
||||||
|
<kbd>a</kbd>: anexar
|
||||||
|
<kbd>m</kbd>: ver logs
|
||||||
|
<kbd>U</kbd>: subir projeto
|
||||||
|
<kbd>D</kbd>: derrubar projeto
|
||||||
|
<kbd>R</kbd>: ver opções de reinício
|
||||||
|
<kbd>c</kbd>: executar comando personalizado predefinido
|
||||||
|
<kbd>b</kbd>: ver comandos em massa
|
||||||
|
<kbd>E</kbd>: executar shell
|
||||||
|
<kbd>w</kbd>: abrir no navegador (primeira porta é http)
|
||||||
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
|
<kbd>[</kbd>: aba anterior
|
||||||
|
<kbd>]</kbd>: próxima aba
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Imagens
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: executar comando personalizado predefinido
|
||||||
|
<kbd>d</kbd>: remover imagem
|
||||||
|
<kbd>b</kbd>: ver comandos em massa
|
||||||
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
|
<kbd>[</kbd>: aba anterior
|
||||||
|
<kbd>]</kbd>: próxima aba
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: executar comando personalizado predefinido
|
||||||
|
<kbd>d</kbd>: remover volume
|
||||||
|
<kbd>b</kbd>: ver comandos em massa
|
||||||
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
|
<kbd>[</kbd>: aba anterior
|
||||||
|
<kbd>]</kbd>: próxima aba
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Redes
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: executar comando personalizado predefinido
|
||||||
|
<kbd>d</kbd>: remover rede
|
||||||
|
<kbd>b</kbd>: ver comandos em massa
|
||||||
|
<kbd>enter</kbd>: focar no painel principal
|
||||||
|
<kbd>[</kbd>: aba anterior
|
||||||
|
<kbd>]</kbd>: próxima aba
|
||||||
|
<kbd>/</kbd>: filtrar lista
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Principal
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>esc</kbd>: retornar
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Global
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: modo de tela seguinte (normal/meia/tela cheia)
|
||||||
|
<kbd>_</kbd>: modo de tela anterior
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
# Lazydocker menü
|
# Lazydocker menü
|
||||||
|
|
||||||
## Proje
|
## Proje
|
||||||
|
|
@ -5,66 +7,91 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>e</kbd>: lazzydocker ayarlarını düzenle
|
<kbd>e</kbd>: lazzydocker ayarlarını düzenle
|
||||||
<kbd>o</kbd>: lazydocker ayarlarını aç
|
<kbd>o</kbd>: lazydocker ayarlarını aç
|
||||||
<kbd>[</kbd>: önceki sekme
|
|
||||||
<kbd>]</kbd>: sonraki sekme
|
|
||||||
<kbd>m</kbd>: kayıt defterini görüntüle
|
<kbd>m</kbd>: kayıt defterini görüntüle
|
||||||
<kbd>enter</kbd>: ana panele odaklan
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
|
<kbd>[</kbd>: önceki sekme
|
||||||
|
<kbd>]</kbd>: sonraki sekme
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Konteynerler
|
## Konteynerler
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: önceki sekme
|
|
||||||
<kbd>]</kbd>: sonraki sekme
|
|
||||||
<kbd>d</kbd>: kaldır
|
<kbd>d</kbd>: kaldır
|
||||||
<kbd>e</kbd>: Hide/Show stopped containers
|
<kbd>e</kbd>: hide/show stopped containers
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>s</kbd>: durdur
|
<kbd>s</kbd>: durdur
|
||||||
<kbd>r</kbd>: yeniden başlat
|
<kbd>r</kbd>: yeniden başlat
|
||||||
<kbd>a</kbd>: bağlan/iliştir
|
<kbd>a</kbd>: bağlan/iliştir
|
||||||
<kbd>m</kbd>: kayıt defterini görüntüle
|
<kbd>m</kbd>: kayıt defterini görüntüle
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: ana panele odaklan
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
|
<kbd>[</kbd>: önceki sekme
|
||||||
|
<kbd>]</kbd>: sonraki sekme
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Servisler
|
## Servisler
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>u</kbd>: up service
|
||||||
<kbd>d</kbd>: konteynerleri kaldır
|
<kbd>d</kbd>: konteynerleri kaldır
|
||||||
<kbd>s</kbd>: durdur
|
<kbd>s</kbd>: durdur
|
||||||
|
<kbd>p</kbd>: pause
|
||||||
<kbd>r</kbd>: yeniden başlat
|
<kbd>r</kbd>: yeniden başlat
|
||||||
|
<kbd>S</kbd>: start
|
||||||
<kbd>a</kbd>: bağlan/iliştir
|
<kbd>a</kbd>: bağlan/iliştir
|
||||||
<kbd>m</kbd>: kayıt defterini görüntüle
|
<kbd>m</kbd>: kayıt defterini görüntüle
|
||||||
<kbd>[</kbd>: önceki sekme
|
<kbd>U</kbd>: up project
|
||||||
<kbd>]</kbd>: sonraki sekme
|
<kbd>D</kbd>: down project
|
||||||
<kbd>R</kbd>: yeniden başlatma seçeneklerini görüntüle
|
<kbd>R</kbd>: yeniden başlatma seçeneklerini görüntüle
|
||||||
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>E</kbd>: exec shell
|
||||||
|
<kbd>w</kbd>: open in browser (first port is http)
|
||||||
<kbd>enter</kbd>: ana panele odaklan
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
|
<kbd>[</kbd>: önceki sekme
|
||||||
|
<kbd>]</kbd>: sonraki sekme
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Imajlar
|
## Imajlar
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: önceki sekme
|
|
||||||
<kbd>]</kbd>: sonraki sekme
|
|
||||||
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
||||||
<kbd>d</kbd>: imajı kaldır
|
<kbd>d</kbd>: imajı kaldır
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
|
||||||
<kbd>enter</kbd>: ana panele odaklan
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
|
<kbd>[</kbd>: önceki sekme
|
||||||
|
<kbd>]</kbd>: sonraki sekme
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Alanlar
|
## Alanlar
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>[</kbd>: önceki sekme
|
|
||||||
<kbd>]</kbd>: sonraki sekme
|
|
||||||
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
||||||
<kbd>d</kbd>: alanı kaldır
|
<kbd>d</kbd>: alanı kaldır
|
||||||
<kbd>b</kbd>: view bulk commands
|
<kbd>b</kbd>: view bulk commands
|
||||||
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
|
||||||
<kbd>enter</kbd>: ana panele odaklan
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
|
<kbd>[</kbd>: önceki sekme
|
||||||
|
<kbd>]</kbd>: sonraki sekme
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Ağları
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: önceden tanımlanmış özel komutu çalıştır
|
||||||
|
<kbd>d</kbd>: ağı kaldır
|
||||||
|
<kbd>b</kbd>: view bulk commands
|
||||||
|
<kbd>enter</kbd>: ana panele odaklan
|
||||||
|
<kbd>[</kbd>: önceki sekme
|
||||||
|
<kbd>]</kbd>: sonraki sekme
|
||||||
|
<kbd>/</kbd>: filter list
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Ana
|
## Ana
|
||||||
|
|
@ -72,3 +99,16 @@
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: dönüş
|
<kbd>esc</kbd>: dönüş
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Global
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
|
<kbd>_</kbd>: prev screen mode
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
|
|
|
||||||
114
docs/keybindings/Keybindings_zh.md
Normal file
114
docs/keybindings/Keybindings_zh.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||||
|
|
||||||
|
# Lazydocker 菜单
|
||||||
|
|
||||||
|
## 项目
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>e</kbd>: 编辑lazydocker配置
|
||||||
|
<kbd>o</kbd>: 打开lazydocker配置
|
||||||
|
<kbd>m</kbd>: 查看日志
|
||||||
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
|
<kbd>/</kbd>: 过滤列表
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 容器
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>d</kbd>: 移除
|
||||||
|
<kbd>e</kbd>: 隐藏/显示已停止的容器
|
||||||
|
<kbd>p</kbd>: 暂停
|
||||||
|
<kbd>s</kbd>: 停止
|
||||||
|
<kbd>r</kbd>: 重新启动
|
||||||
|
<kbd>a</kbd>: attach
|
||||||
|
<kbd>m</kbd>: 查看日志
|
||||||
|
<kbd>E</kbd>: 执行shell
|
||||||
|
<kbd>c</kbd>: 运行预定义的自定义命令
|
||||||
|
<kbd>b</kbd>: 查看批量命令
|
||||||
|
<kbd>w</kbd>: 在浏览器中打开(第一个端口为http)
|
||||||
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
|
<kbd>/</kbd>: 过滤列表
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 服务
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>u</kbd>: 启动服务
|
||||||
|
<kbd>d</kbd>: 移除容器
|
||||||
|
<kbd>s</kbd>: 停止
|
||||||
|
<kbd>p</kbd>: 暂停
|
||||||
|
<kbd>r</kbd>: 重新启动
|
||||||
|
<kbd>S</kbd>: 启动项目
|
||||||
|
<kbd>a</kbd>: attach
|
||||||
|
<kbd>m</kbd>: 查看日志
|
||||||
|
<kbd>U</kbd>: 创建并启动容器
|
||||||
|
<kbd>D</kbd>: 停止并移除容器
|
||||||
|
<kbd>R</kbd>: 查看重启选项
|
||||||
|
<kbd>c</kbd>: 运行预定义的自定义命令
|
||||||
|
<kbd>b</kbd>: 查看批量命令
|
||||||
|
<kbd>E</kbd>: 执行shell
|
||||||
|
<kbd>w</kbd>: 在浏览器中打开(第一个端口为http)
|
||||||
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
|
<kbd>/</kbd>: 过滤列表
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 镜像
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: 运行预定义的自定义命令
|
||||||
|
<kbd>d</kbd>: 移除镜像
|
||||||
|
<kbd>b</kbd>: 查看批量命令
|
||||||
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
|
<kbd>/</kbd>: 过滤列表
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 卷
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: 运行预定义的自定义命令
|
||||||
|
<kbd>d</kbd>: 移除卷
|
||||||
|
<kbd>b</kbd>: 查看批量命令
|
||||||
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
|
<kbd>/</kbd>: 过滤列表
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 网络
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>c</kbd>: 运行预定义的自定义命令
|
||||||
|
<kbd>d</kbd>: 移除网络
|
||||||
|
<kbd>b</kbd>: 查看批量命令
|
||||||
|
<kbd>enter</kbd>: 聚焦主面板
|
||||||
|
<kbd>[</kbd>: 上一个选项卡
|
||||||
|
<kbd>]</kbd>: 下一个选项卡
|
||||||
|
<kbd>/</kbd>: 过滤列表
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 主要
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>esc</kbd>: 返回
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## 全局
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>+</kbd>: 下一个屏幕模式(正常/半屏/全屏)
|
||||||
|
<kbd>_</kbd>: 上一个屏幕模式
|
||||||
|
<kbd>1</kbd>: focus projects panel
|
||||||
|
<kbd>2</kbd>: focus services panel
|
||||||
|
<kbd>3</kbd>: focus containers panel
|
||||||
|
<kbd>4</kbd>: focus images panel
|
||||||
|
<kbd>5</kbd>: focus volumes panel
|
||||||
|
<kbd>6</kbd>: focus networks panel
|
||||||
|
</pre>
|
||||||
BIN
docs/resources/discord.png
Normal file
BIN
docs/resources/discord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 KiB |
62
getter.rb
62
getter.rb
|
|
@ -1,62 +0,0 @@
|
||||||
ids = %w[
|
|
||||||
AnonymousReportingPrompt
|
|
||||||
AnonymousReportingTitle
|
|
||||||
attach
|
|
||||||
cancel
|
|
||||||
close
|
|
||||||
Confirm
|
|
||||||
confirmPruneImages
|
|
||||||
ConfirmQuit
|
|
||||||
ContainersTitle
|
|
||||||
CustomCommand
|
|
||||||
Donate
|
|
||||||
EditConfig
|
|
||||||
Error
|
|
||||||
ErrorOccurred
|
|
||||||
execute
|
|
||||||
ImagesTitle
|
|
||||||
menu
|
|
||||||
mustForceToRemoveContainer
|
|
||||||
navigate
|
|
||||||
nextContext
|
|
||||||
NoContainers
|
|
||||||
NoImages
|
|
||||||
NotEnoughSpace
|
|
||||||
NoViewMachingNewLineFocusedSwitchStatement
|
|
||||||
OpenConfig
|
|
||||||
pressEnterToReturn
|
|
||||||
previousContext
|
|
||||||
ProjectTitle
|
|
||||||
pruneImages
|
|
||||||
PruningStatus
|
|
||||||
remove
|
|
||||||
removeImage
|
|
||||||
removeService
|
|
||||||
removeWithoutPrune
|
|
||||||
removeWithVolumes
|
|
||||||
RemovingStatus
|
|
||||||
resizingPopupPanel
|
|
||||||
restart
|
|
||||||
RestartingStatus
|
|
||||||
RunningSubprocess
|
|
||||||
scroll
|
|
||||||
ServicesTitle
|
|
||||||
stop
|
|
||||||
StopContainer
|
|
||||||
StoppingStatus
|
|
||||||
StopService
|
|
||||||
viewLogs
|
|
||||||
]
|
|
||||||
|
|
||||||
f = File.read('pkg/i18n/english.go')
|
|
||||||
|
|
||||||
f.lines.each_with_index do |line, index|
|
|
||||||
if line[/ID:/]
|
|
||||||
stripped = line.strip.gsub(/ID: *"/, '",').gsub('",', '')
|
|
||||||
if ids.include?(stripped)
|
|
||||||
key = stripped[0].upcase + stripped[1..-1]
|
|
||||||
value = f.lines[index+1].strip.gsub(/Other: *"/, '').gsub('",', '')
|
|
||||||
puts "#{key}: \"#{value}\","
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
105
go.mod
105
go.mod
|
|
@ -1,46 +1,85 @@
|
||||||
module github.com/jesseduffield/lazydocker
|
module github.com/jesseduffield/lazydocker
|
||||||
|
|
||||||
go 1.12
|
go 1.22
|
||||||
|
|
||||||
|
toolchain go1.23.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.4.12 // indirect
|
|
||||||
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
|
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1
|
||||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
github.com/docker/cli v27.1.1+incompatible
|
||||||
github.com/docker/docker v0.7.3-0.20190307005417-54dddadc7d5d
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/fatih/color v1.10.0
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/go-errors/errors v1.5.1
|
||||||
github.com/fatih/color v1.7.0
|
github.com/gookit/color v1.5.0
|
||||||
github.com/go-errors/errors v1.0.1
|
github.com/imdario/mergo v0.3.16
|
||||||
github.com/gogo/protobuf v1.2.1 // indirect
|
github.com/integrii/flaggy v1.4.0
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
|
||||||
github.com/google/go-cmp v0.3.0 // indirect
|
|
||||||
github.com/gorilla/mux v1.7.3 // indirect
|
|
||||||
github.com/imdario/mergo v0.3.7
|
|
||||||
github.com/integrii/flaggy v0.0.0-20190517180110-07ea7eb77404
|
|
||||||
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
|
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20190803023616-ad0cd60f29f9
|
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513
|
||||||
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7
|
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||||
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e // indirect
|
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996
|
||||||
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
|
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
|
||||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
github.com/mattn/go-runewidth v0.0.15
|
||||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.2 // indirect
|
|
||||||
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767
|
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767
|
||||||
github.com/mgutz/str v1.2.0
|
github.com/mgutz/str v1.2.0
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
github.com/pmezard/go-difflib v1.0.0
|
||||||
|
github.com/samber/lo v1.31.0
|
||||||
|
github.com/sasha-s/go-deadlock v0.3.1
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||||
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
|
github.com/gdamore/tcell/v2 v2.7.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.11.0
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/term v0.5.0 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||||
github.com/onsi/gomega v1.5.0 // indirect
|
github.com/onsi/gomega v1.5.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.3.0
|
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/stretchr/testify v1.2.2
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||||
google.golang.org/grpc v1.22.0 // indirect
|
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||||
|
golang.org/x/crypto v0.24.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
|
golang.org/x/term v0.21.0 // indirect
|
||||||
|
golang.org/x/text v0.16.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gotest.tools/v3 v3.5.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
304
go.sum
304
go.sum
|
|
@ -1,143 +1,247 @@
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
|
|
||||||
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
|
||||||
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c h1:YDsGA6tou+tAxVe0Dre29iSbQ8TrWdWfwOisKArJT5E=
|
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c h1:YDsGA6tou+tAxVe0Dre29iSbQ8TrWdWfwOisKArJT5E=
|
||||||
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
|
github.com/OpenPeeDeeP/xdg v0.2.1-0.20190312153938-4ba9e1eb294c/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 h1:1fx+RA5lk1ZkzPAUP7DEgZnVHYxEcHO77vQO/V8z/2Q=
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1/go.mod h1:z0nyIb42Zs97wyX1V+8MbEFhHeTw1OgFQfR6q57ZuHc=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v0.7.3-0.20190307005417-54dddadc7d5d h1:nIS6IF5oAIkXtSTHeRgFDtNKHpTjvZe1q3RCA0cm1rQ=
|
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
|
||||||
github.com/docker/docker v0.7.3-0.20190307005417-54dddadc7d5d/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||||
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||||
|
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
|
github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54=
|
||||||
|
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/integrii/flaggy v0.0.0-20190517180110-07ea7eb77404 h1:oRzqLvY7HzDWD9kGheYP6BoqWa5vtw7ry3XNRbGFH8g=
|
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
|
||||||
github.com/integrii/flaggy v0.0.0-20190517180110-07ea7eb77404/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
||||||
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee h1:7Zi/OQlGbMz4MT2V1+prN/gv1C64NDyVb/MbJnS0ZfA=
|
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee h1:7Zi/OQlGbMz4MT2V1+prN/gv1C64NDyVb/MbJnS0ZfA=
|
||||||
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee/go.mod h1:Z9UKHveKXXgyo8ME7R8yxh/BUTFOK+FgfWKlhy8oOAg=
|
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee/go.mod h1:Z9UKHveKXXgyo8ME7R8yxh/BUTFOK+FgfWKlhy8oOAg=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20190803023616-ad0cd60f29f9 h1:v2Pf3RaIN+MlFfSDnJw2d9Qksr4Tw08+pjL4LEY+JLM=
|
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513 h1:Y1bw5iItrsDCumATc/rklIJ/6K+68ieiWZJedhrNuXo=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20190803023616-ad0cd60f29f9/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
github.com/jesseduffield/gocui v0.3.1-0.20240418080333-8cd33929c513/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
|
||||||
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
|
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||||
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
|
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||||
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
|
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996 h1:CH1en6GpXSwnXl5Ehc4WX1NpS3uw9qbi7o9A4T2YYmA=
|
||||||
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
|
github.com/jesseduffield/lazycore v0.0.0-20221023210126-718a4caea996/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
|
||||||
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr6+sfSbdpRWWrwvLYyS56HyIRVfq0Qcl2h28wM=
|
|
||||||
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
|
||||||
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg=
|
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg=
|
||||||
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s=
|
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 h1:BrhJNdEFWGuiJk/3/SwsG5Rex3zjFxYsDi2bpd7382Y=
|
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 h1:BrhJNdEFWGuiJk/3/SwsG5Rex3zjFxYsDi2bpd7382Y=
|
||||||
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767/go.mod h1:ct+byCpkFokm4J0tiuAvB8cf2ttm6GcCe89Yr25nGKg=
|
github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767/go.mod h1:ct+byCpkFokm4J0tiuAvB8cf2ttm6GcCe89Yr25nGKg=
|
||||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
|
||||||
|
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
|
||||||
|
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||||
|
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
||||||
|
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||||
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||||
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||||
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||||
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||||
google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
|
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
|
||||||
|
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
|
|
|
||||||
40
main.go
40
main.go
|
|
@ -6,27 +6,35 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/integrii/flaggy"
|
"github.com/integrii/flaggy"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/app"
|
"github.com/jesseduffield/lazydocker/pkg/app"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
"github.com/jesseduffield/yaml"
|
"github.com/jesseduffield/yaml"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DEFAULT_VERSION = "unversioned"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
commit string
|
commit string
|
||||||
version = "unversioned"
|
version = DEFAULT_VERSION
|
||||||
date string
|
date string
|
||||||
buildSource = "unknown"
|
buildSource = "unknown"
|
||||||
|
|
||||||
configFlag = false
|
configFlag = false
|
||||||
debuggingFlag = false
|
debuggingFlag = false
|
||||||
composeFiles []string
|
composeFiles []string
|
||||||
|
projectName string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
updateBuildInfo()
|
||||||
|
|
||||||
info := fmt.Sprintf(
|
info := fmt.Sprintf(
|
||||||
"%s\nDate: %s\nBuildSource: %s\nCommit: %s\nOS: %s\nArch: %s",
|
"%s\nDate: %s\nBuildSource: %s\nCommit: %s\nOS: %s\nArch: %s",
|
||||||
version,
|
version,
|
||||||
|
|
@ -44,6 +52,7 @@ func main() {
|
||||||
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
||||||
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
|
flaggy.Bool(&debuggingFlag, "d", "debug", "a boolean")
|
||||||
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
|
flaggy.StringSlice(&composeFiles, "f", "file", "Specify alternate compose files")
|
||||||
|
flaggy.String(&projectName, "p", "project", "Specify a docker compose project name")
|
||||||
flaggy.SetVersion(info)
|
flaggy.SetVersion(info)
|
||||||
|
|
||||||
flaggy.Parse()
|
flaggy.Parse()
|
||||||
|
|
@ -64,7 +73,7 @@ func main() {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir)
|
appConfig, err := config.NewAppConfig("lazydocker", version, commit, date, buildSource, debuggingFlag, composeFiles, projectDir, projectName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +82,7 @@ func main() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = app.Run()
|
err = app.Run()
|
||||||
}
|
}
|
||||||
|
app.Close()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errMessage, known := app.KnownError(err); known {
|
if errMessage, known := app.KnownError(err); known {
|
||||||
|
|
@ -89,6 +99,30 @@ func main() {
|
||||||
stackTrace := newErr.ErrorStack()
|
stackTrace := newErr.ErrorStack()
|
||||||
app.Log.Error(stackTrace)
|
app.Log.Error(stackTrace)
|
||||||
|
|
||||||
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
|
log.Fatalf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBuildInfo() {
|
||||||
|
if version == DEFAULT_VERSION {
|
||||||
|
if buildInfo, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
revision, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||||
|
return setting.Key == "vcs.revision"
|
||||||
|
})
|
||||||
|
if ok {
|
||||||
|
commit = revision.Value
|
||||||
|
// if lazydocker was built from source we'll show the version as the
|
||||||
|
// abbreviated commit hash
|
||||||
|
version = utils.SafeTruncate(revision.Value, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if version hasn't been set we assume that neither has the date
|
||||||
|
time, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||||
|
return setting.Key == "vcs.time"
|
||||||
|
})
|
||||||
|
if ok {
|
||||||
|
date = time.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/jesseduffield/lazydocker/pkg/gui"
|
"github.com/jesseduffield/lazydocker/pkg/gui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/log"
|
"github.com/jesseduffield/lazydocker/pkg/log"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -34,7 +35,10 @@ func NewApp(config *config.AppConfig) (*App, error) {
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
app.Log = log.NewLogger(config, "23432119147a4367abf7c0de2aa99a2d")
|
app.Log = log.NewLogger(config, "23432119147a4367abf7c0de2aa99a2d")
|
||||||
app.Tr = i18n.NewTranslationSet(app.Log)
|
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.UserConfig.Gui.Language)
|
||||||
|
if err != nil {
|
||||||
|
return app, err
|
||||||
|
}
|
||||||
app.OSCommand = commands.NewOSCommand(app.Log, config)
|
app.OSCommand = commands.NewOSCommand(app.Log, config)
|
||||||
|
|
||||||
// here is the place to make use of the docker-compose.yml file in the current directory
|
// here is the place to make use of the docker-compose.yml file in the current directory
|
||||||
|
|
@ -43,6 +47,7 @@ func NewApp(config *config.AppConfig) (*App, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return app, err
|
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.Gui, err = gui.NewGui(app.Log, app.DockerCommand, app.OSCommand, app.Tr, config, app.ErrorChan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return app, err
|
return app, err
|
||||||
|
|
@ -51,8 +56,11 @@ func NewApp(config *config.AppConfig) (*App, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) Run() error {
|
func (app *App) Run() error {
|
||||||
err := app.Gui.RunWithSubprocesses()
|
return app.Gui.Run()
|
||||||
return err
|
}
|
||||||
|
|
||||||
|
func (app *App) Close() error {
|
||||||
|
return utils.CloseMany(app.closers)
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorMapping struct {
|
type errorMapping struct {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,14 @@
|
||||||
// The content of this generated file is a keybindings cheatsheet.
|
// The content of this generated file is a keybindings cheatsheet.
|
||||||
//
|
//
|
||||||
// To generate cheatsheet in english run:
|
// To generate cheatsheet in english run:
|
||||||
// LANG=en go run scripts/generate_cheatsheet.go
|
// LANG=en go run scripts/cheatsheet/main.go generate
|
||||||
|
|
||||||
package main
|
package cheatsheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/app"
|
"github.com/jesseduffield/lazydocker/pkg/app"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
|
@ -20,13 +19,21 @@ import (
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
generateCheatsheetCmd = "go run scripts/cheatsheet/main.go generate"
|
||||||
|
)
|
||||||
|
|
||||||
type bindingSection struct {
|
type bindingSection struct {
|
||||||
title string
|
title string
|
||||||
bindings []*gui.Binding
|
bindings []*gui.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func Generate() {
|
||||||
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "")
|
generateAtDir(GetKeybindingsDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAtDir(dir string) {
|
||||||
|
mConfig, err := config.NewAppConfig("lazydocker", "", "", "", "", true, nil, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -34,13 +41,21 @@ func main() {
|
||||||
for lang := range i18n.GetTranslationSets() {
|
for lang := range i18n.GetTranslationSets() {
|
||||||
os.Setenv("LC_ALL", lang)
|
os.Setenv("LC_ALL", lang)
|
||||||
mApp, _ := app.NewApp(mConfig)
|
mApp, _ := app.NewApp(mConfig)
|
||||||
file, err := os.Create("./docs/keybindings/Keybindings_" + lang + ".md")
|
mApp.Gui.SetupFakeGui()
|
||||||
|
|
||||||
|
file, err := os.Create(dir + "/Keybindings_" + lang + ".md")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingSections := getBindingSections(mApp)
|
bindingSections := getBindingSections(mApp)
|
||||||
content := formatSections(mApp, bindingSections)
|
content := formatSections(mApp, bindingSections)
|
||||||
|
content = fmt.Sprintf(
|
||||||
|
"_This file is auto-generated. To update, make the changes in the "+
|
||||||
|
"pkg/i18n directory and then run `%s` from the project root._\n\n%s",
|
||||||
|
generateCheatsheetCmd,
|
||||||
|
content,
|
||||||
|
)
|
||||||
writeString(file, content)
|
writeString(file, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,23 +96,12 @@ func getBindingSections(mApp *app.App) []*bindingSection {
|
||||||
"containers": mApp.Tr.ContainersTitle,
|
"containers": mApp.Tr.ContainersTitle,
|
||||||
"images": mApp.Tr.ImagesTitle,
|
"images": mApp.Tr.ImagesTitle,
|
||||||
"volumes": mApp.Tr.VolumesTitle,
|
"volumes": mApp.Tr.VolumesTitle,
|
||||||
|
"networks": mApp.Tr.NetworksTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingSections = addBinding(titleMap[viewName], bindingSections, binding)
|
bindingSections = addBinding(titleMap[viewName], bindingSections, binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
// for view, contexts := range mApp.Gui.GetContextMap() {
|
|
||||||
// for contextName, contextBindings := range contexts {
|
|
||||||
// translatedView := localisedTitle(mApp, view)
|
|
||||||
// translatedContextName := localisedTitle(mApp, contextName)
|
|
||||||
// title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
|
|
||||||
|
|
||||||
// for _, binding := range contextBindings {
|
|
||||||
// bindingSections = addBinding(title, bindingSections, binding)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return bindingSections
|
return bindingSections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,11 +139,3 @@ func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProjectRoot() string {
|
|
||||||
dir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return strings.Split(dir, "lazydocker")[0] + "lazydocker"
|
|
||||||
}
|
|
||||||
87
pkg/cheatsheet/validate.go
Normal file
87
pkg/cheatsheet/validate.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package cheatsheet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||||
|
"github.com/pmezard/go-difflib/difflib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Check() {
|
||||||
|
dir := GetKeybindingsDir()
|
||||||
|
tmpDir := filepath.Join(os.TempDir(), "lazydocker_cheatsheet")
|
||||||
|
|
||||||
|
err := os.RemoveAll(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if err = os.Mkdir(tmpDir, 0o700); err != nil {
|
||||||
|
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
generateAtDir(tmpDir)
|
||||||
|
|
||||||
|
actualContent := obtainContent(dir)
|
||||||
|
expectedContent := obtainContent(tmpDir)
|
||||||
|
|
||||||
|
if expectedContent == "" {
|
||||||
|
log.Fatal("empty expected content")
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualContent != expectedContent {
|
||||||
|
if err := difflib.WriteUnifiedDiff(os.Stdout, difflib.UnifiedDiff{
|
||||||
|
A: difflib.SplitLines(expectedContent),
|
||||||
|
B: difflib.SplitLines(actualContent),
|
||||||
|
FromFile: "Expected",
|
||||||
|
FromDate: "",
|
||||||
|
ToFile: "Actual",
|
||||||
|
ToDate: "",
|
||||||
|
Context: 1,
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(
|
||||||
|
"\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes. "+
|
||||||
|
"If you run the script and no keybindings files are updated as a result, try rebasing onto master"+
|
||||||
|
"and trying again.\n",
|
||||||
|
generateCheatsheetCmd,
|
||||||
|
)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nCheatsheets are up to date")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetKeybindingsDir() string {
|
||||||
|
return utils.GetLazyRootDirectory() + "/docs/keybindings"
|
||||||
|
}
|
||||||
|
|
||||||
|
func obtainContent(dir string) string {
|
||||||
|
re := regexp.MustCompile(`Keybindings_\w+\.md$`)
|
||||||
|
|
||||||
|
content := ""
|
||||||
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if re.MatchString(path) {
|
||||||
|
bytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||||
|
}
|
||||||
|
content += fmt.Sprintf("\n%s\n\n", filepath.Base(path))
|
||||||
|
content += string(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
@ -4,20 +4,15 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/sasha-s/go-deadlock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
)
|
)
|
||||||
|
|
@ -32,295 +27,21 @@ type Container struct {
|
||||||
OneOff bool
|
OneOff bool
|
||||||
ProjectName string
|
ProjectName string
|
||||||
ID string
|
ID string
|
||||||
Container types.Container
|
Container container.Summary
|
||||||
DisplayString string
|
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
OSCommand *OSCommand
|
OSCommand *OSCommand
|
||||||
Config *config.AppConfig
|
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
CLIStats ContainerCliStat // for realtime we use the CLI, for long-term we use the client
|
StatHistory []*RecordedStats
|
||||||
StatHistory []RecordedStats
|
Details container.InspectResponse
|
||||||
Details Details
|
|
||||||
MonitoringStats bool
|
MonitoringStats bool
|
||||||
DockerCommand LimitedDockerCommand
|
DockerCommand LimitedDockerCommand
|
||||||
Tr *i18n.TranslationSet
|
Tr *i18n.TranslationSet
|
||||||
}
|
|
||||||
|
|
||||||
// Details is a struct containing what we get back from `docker inspect` on a container
|
StatsMutex deadlock.Mutex
|
||||||
type Details struct {
|
|
||||||
ID string `json:"Id"`
|
|
||||||
Created time.Time `json:"Created"`
|
|
||||||
Path string `json:"Path"`
|
|
||||||
Args []string `json:"Args"`
|
|
||||||
State struct {
|
|
||||||
Status string `json:"Status"`
|
|
||||||
Running bool `json:"Running"`
|
|
||||||
Paused bool `json:"Paused"`
|
|
||||||
Restarting bool `json:"Restarting"`
|
|
||||||
OOMKilled bool `json:"OOMKilled"`
|
|
||||||
Dead bool `json:"Dead"`
|
|
||||||
Pid int `json:"Pid"`
|
|
||||||
ExitCode int `json:"ExitCode"`
|
|
||||||
Error string `json:"Error"`
|
|
||||||
StartedAt time.Time `json:"StartedAt"`
|
|
||||||
FinishedAt time.Time `json:"FinishedAt"`
|
|
||||||
} `json:"State"`
|
|
||||||
Image string `json:"Image"`
|
|
||||||
ResolvConfPath string `json:"ResolvConfPath"`
|
|
||||||
HostnamePath string `json:"HostnamePath"`
|
|
||||||
HostsPath string `json:"HostsPath"`
|
|
||||||
LogPath string `json:"LogPath"`
|
|
||||||
Name string `json:"Name"`
|
|
||||||
RestartCount int `json:"RestartCount"`
|
|
||||||
Driver string `json:"Driver"`
|
|
||||||
Platform string `json:"Platform"`
|
|
||||||
MountLabel string `json:"MountLabel"`
|
|
||||||
ProcessLabel string `json:"ProcessLabel"`
|
|
||||||
AppArmorProfile string `json:"AppArmorProfile"`
|
|
||||||
ExecIDs interface{} `json:"ExecIDs"`
|
|
||||||
HostConfig struct {
|
|
||||||
Binds []string `json:"Binds"`
|
|
||||||
ContainerIDFile string `json:"ContainerIDFile"`
|
|
||||||
LogConfig struct {
|
|
||||||
Type string `json:"Type"`
|
|
||||||
Config struct {
|
|
||||||
} `json:"Config"`
|
|
||||||
} `json:"LogConfig"`
|
|
||||||
NetworkMode string `json:"NetworkMode"`
|
|
||||||
PortBindings struct {
|
|
||||||
} `json:"PortBindings"`
|
|
||||||
RestartPolicy struct {
|
|
||||||
Name string `json:"Name"`
|
|
||||||
MaximumRetryCount int `json:"MaximumRetryCount"`
|
|
||||||
} `json:"RestartPolicy"`
|
|
||||||
AutoRemove bool `json:"AutoRemove"`
|
|
||||||
VolumeDriver string `json:"VolumeDriver"`
|
|
||||||
VolumesFrom []interface{} `json:"VolumesFrom"`
|
|
||||||
CapAdd interface{} `json:"CapAdd"`
|
|
||||||
CapDrop interface{} `json:"CapDrop"`
|
|
||||||
DNS interface{} `json:"Dns"`
|
|
||||||
DNSOptions interface{} `json:"DnsOptions"`
|
|
||||||
DNSSearch interface{} `json:"DnsSearch"`
|
|
||||||
ExtraHosts interface{} `json:"ExtraHosts"`
|
|
||||||
GroupAdd interface{} `json:"GroupAdd"`
|
|
||||||
IpcMode string `json:"IpcMode"`
|
|
||||||
Cgroup string `json:"Cgroup"`
|
|
||||||
Links interface{} `json:"Links"`
|
|
||||||
OomScoreAdj int `json:"OomScoreAdj"`
|
|
||||||
PidMode string `json:"PidMode"`
|
|
||||||
Privileged bool `json:"Privileged"`
|
|
||||||
PublishAllPorts bool `json:"PublishAllPorts"`
|
|
||||||
ReadonlyRootfs bool `json:"ReadonlyRootfs"`
|
|
||||||
SecurityOpt interface{} `json:"SecurityOpt"`
|
|
||||||
UTSMode string `json:"UTSMode"`
|
|
||||||
UsernsMode string `json:"UsernsMode"`
|
|
||||||
ShmSize int `json:"ShmSize"`
|
|
||||||
Runtime string `json:"Runtime"`
|
|
||||||
ConsoleSize []int `json:"ConsoleSize"`
|
|
||||||
Isolation string `json:"Isolation"`
|
|
||||||
CPUShares int `json:"CpuShares"`
|
|
||||||
Memory int `json:"Memory"`
|
|
||||||
NanoCpus int `json:"NanoCpus"`
|
|
||||||
CgroupParent string `json:"CgroupParent"`
|
|
||||||
BlkioWeight int `json:"BlkioWeight"`
|
|
||||||
BlkioWeightDevice interface{} `json:"BlkioWeightDevice"`
|
|
||||||
BlkioDeviceReadBps interface{} `json:"BlkioDeviceReadBps"`
|
|
||||||
BlkioDeviceWriteBps interface{} `json:"BlkioDeviceWriteBps"`
|
|
||||||
BlkioDeviceReadIOps interface{} `json:"BlkioDeviceReadIOps"`
|
|
||||||
BlkioDeviceWriteIOps interface{} `json:"BlkioDeviceWriteIOps"`
|
|
||||||
CPUPeriod int `json:"CpuPeriod"`
|
|
||||||
CPUQuota int `json:"CpuQuota"`
|
|
||||||
CPURealtimePeriod int `json:"CpuRealtimePeriod"`
|
|
||||||
CPURealtimeRuntime int `json:"CpuRealtimeRuntime"`
|
|
||||||
CpusetCpus string `json:"CpusetCpus"`
|
|
||||||
CpusetMems string `json:"CpusetMems"`
|
|
||||||
Devices interface{} `json:"Devices"`
|
|
||||||
DeviceCgroupRules interface{} `json:"DeviceCgroupRules"`
|
|
||||||
DiskQuota int `json:"DiskQuota"`
|
|
||||||
KernelMemory int `json:"KernelMemory"`
|
|
||||||
MemoryReservation int `json:"MemoryReservation"`
|
|
||||||
MemorySwap int `json:"MemorySwap"`
|
|
||||||
MemorySwappiness interface{} `json:"MemorySwappiness"`
|
|
||||||
OomKillDisable bool `json:"OomKillDisable"`
|
|
||||||
PidsLimit int `json:"PidsLimit"`
|
|
||||||
Ulimits interface{} `json:"Ulimits"`
|
|
||||||
CPUCount int `json:"CpuCount"`
|
|
||||||
CPUPercent int `json:"CpuPercent"`
|
|
||||||
IOMaximumIOps int `json:"IOMaximumIOps"`
|
|
||||||
IOMaximumBandwidth int `json:"IOMaximumBandwidth"`
|
|
||||||
MaskedPaths []string `json:"MaskedPaths"`
|
|
||||||
ReadonlyPaths []string `json:"ReadonlyPaths"`
|
|
||||||
} `json:"HostConfig"`
|
|
||||||
GraphDriver struct {
|
|
||||||
Data struct {
|
|
||||||
LowerDir string `json:"LowerDir"`
|
|
||||||
MergedDir string `json:"MergedDir"`
|
|
||||||
UpperDir string `json:"UpperDir"`
|
|
||||||
WorkDir string `json:"WorkDir"`
|
|
||||||
} `json:"Data"`
|
|
||||||
Name string `json:"Name"`
|
|
||||||
} `json:"GraphDriver"`
|
|
||||||
Mounts []struct {
|
|
||||||
Type string `json:"Type"`
|
|
||||||
Name string `json:"Name,omitempty"`
|
|
||||||
Source string `json:"Source"`
|
|
||||||
Destination string `json:"Destination"`
|
|
||||||
Driver string `json:"Driver,omitempty"`
|
|
||||||
Mode string `json:"Mode"`
|
|
||||||
RW bool `json:"RW"`
|
|
||||||
Propagation string `json:"Propagation"`
|
|
||||||
} `json:"Mounts"`
|
|
||||||
Config struct {
|
|
||||||
Hostname string `json:"Hostname"`
|
|
||||||
Domainname string `json:"Domainname"`
|
|
||||||
User string `json:"User"`
|
|
||||||
AttachStdin bool `json:"AttachStdin"`
|
|
||||||
AttachStdout bool `json:"AttachStdout"`
|
|
||||||
AttachStderr bool `json:"AttachStderr"`
|
|
||||||
Tty bool `json:"Tty"`
|
|
||||||
OpenStdin bool `json:"OpenStdin"`
|
|
||||||
StdinOnce bool `json:"StdinOnce"`
|
|
||||||
Env []string `json:"Env"`
|
|
||||||
Cmd []string `json:"Cmd"`
|
|
||||||
Image string `json:"Image"`
|
|
||||||
Volumes struct {
|
|
||||||
APIBundle struct {
|
|
||||||
} `json:"/api-bundle"`
|
|
||||||
App struct {
|
|
||||||
} `json:"/app"`
|
|
||||||
} `json:"Volumes"`
|
|
||||||
WorkingDir string `json:"WorkingDir"`
|
|
||||||
Entrypoint interface{} `json:"Entrypoint"`
|
|
||||||
OnBuild interface{} `json:"OnBuild"`
|
|
||||||
Labels map[string]string `json:"Labels"`
|
|
||||||
} `json:"Config"`
|
|
||||||
NetworkSettings struct {
|
|
||||||
Bridge string `json:"Bridge"`
|
|
||||||
SandboxID string `json:"SandboxID"`
|
|
||||||
HairpinMode bool `json:"HairpinMode"`
|
|
||||||
LinkLocalIPv6Address string `json:"LinkLocalIPv6Address"`
|
|
||||||
LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen"`
|
|
||||||
Ports map[string][]struct {
|
|
||||||
HostIP string `json:"HostIP"`
|
|
||||||
HostPort string `json:"HostPort"`
|
|
||||||
} `json:"Ports"`
|
|
||||||
SandboxKey string `json:"SandboxKey"`
|
|
||||||
SecondaryIPAddresses interface{} `json:"SecondaryIPAddresses"`
|
|
||||||
SecondaryIPv6Addresses interface{} `json:"SecondaryIPv6Addresses"`
|
|
||||||
EndpointID string `json:"EndpointID"`
|
|
||||||
Gateway string `json:"Gateway"`
|
|
||||||
GlobalIPv6Address string `json:"GlobalIPv6Address"`
|
|
||||||
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"`
|
|
||||||
IPAddress string `json:"IPAddress"`
|
|
||||||
IPPrefixLen int `json:"IPPrefixLen"`
|
|
||||||
IPv6Gateway string `json:"IPv6Gateway"`
|
|
||||||
MacAddress string `json:"MacAddress"`
|
|
||||||
Networks map[string]struct {
|
|
||||||
IPAMConfig interface{} `json:"IPAMConfig"`
|
|
||||||
Links interface{} `json:"Links"`
|
|
||||||
Aliases []string `json:"Aliases"`
|
|
||||||
NetworkID string `json:"NetworkID"`
|
|
||||||
EndpointID string `json:"EndpointID"`
|
|
||||||
Gateway string `json:"Gateway"`
|
|
||||||
IPAddress string `json:"IPAddress"`
|
|
||||||
IPPrefixLen int `json:"IPPrefixLen"`
|
|
||||||
IPv6Gateway string `json:"IPv6Gateway"`
|
|
||||||
GlobalIPv6Address string `json:"GlobalIPv6Address"`
|
|
||||||
GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"`
|
|
||||||
MacAddress string `json:"MacAddress"`
|
|
||||||
DriverOpts interface{} `json:"DriverOpts"`
|
|
||||||
} `json:"Networks"`
|
|
||||||
} `json:"NetworkSettings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerCliStat is a stat object returned by the CLI docker stat command
|
|
||||||
type ContainerCliStat struct {
|
|
||||||
BlockIO string `json:"BlockIO"`
|
|
||||||
CPUPerc string `json:"CPUPerc"`
|
|
||||||
Container string `json:"Container"`
|
|
||||||
ID string `json:"ID"`
|
|
||||||
MemPerc string `json:"MemPerc"`
|
|
||||||
MemUsage string `json:"MemUsage"`
|
|
||||||
Name string `json:"Name"`
|
|
||||||
NetIO string `json:"NetIO"`
|
|
||||||
PIDs string `json:"PIDs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStrings returns the dispaly string of Container
|
|
||||||
func (c *Container) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
image := strings.TrimPrefix(c.Container.Image, "sha256:")
|
|
||||||
|
|
||||||
return []string{c.GetDisplayStatus(), c.Name, c.GetDisplayCPUPerc(), utils.ColoredString(image, color.FgMagenta)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStatus returns the colored status of the container
|
|
||||||
func (c *Container) GetDisplayStatus() string {
|
|
||||||
state := c.Container.State
|
|
||||||
if c.Container.State == "exited" {
|
|
||||||
state += " (" + strconv.Itoa(c.Details.State.ExitCode) + ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.ColoredString(state, c.GetColor())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
|
||||||
func (c *Container) GetDisplayCPUPerc() string {
|
|
||||||
stats := c.CLIStats
|
|
||||||
|
|
||||||
if stats.CPUPerc == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
percentage, err := strconv.ParseFloat(strings.TrimSuffix(stats.CPUPerc, "%"), 32)
|
|
||||||
if err != nil {
|
|
||||||
// probably complaining about not being able to convert '--'
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var clr color.Attribute
|
|
||||||
if percentage > 90 {
|
|
||||||
clr = color.FgRed
|
|
||||||
} else if percentage > 50 {
|
|
||||||
clr = color.FgYellow
|
|
||||||
} else {
|
|
||||||
clr = color.FgWhite
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.ColoredString(stats.CPUPerc, clr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProducingLogs tells us whether we should bother checking a container's logs
|
|
||||||
func (c *Container) ProducingLogs() bool {
|
|
||||||
return c.Container.State == "running" && !(c.Details.HostConfig.LogConfig.Type == "none")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetColor Container color
|
|
||||||
func (c *Container) GetColor() color.Attribute {
|
|
||||||
switch c.Container.State {
|
|
||||||
case "exited":
|
|
||||||
if c.Details.State.ExitCode == 0 {
|
|
||||||
return color.FgYellow
|
|
||||||
}
|
|
||||||
return color.FgRed
|
|
||||||
case "created":
|
|
||||||
return color.FgCyan
|
|
||||||
case "running":
|
|
||||||
return color.FgGreen
|
|
||||||
case "paused":
|
|
||||||
return color.FgYellow
|
|
||||||
case "dead":
|
|
||||||
return color.FgRed
|
|
||||||
case "restarting":
|
|
||||||
return color.FgBlue
|
|
||||||
case "removing":
|
|
||||||
return color.FgMagenta
|
|
||||||
default:
|
|
||||||
return color.FgWhite
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove removes the container
|
// Remove removes the container
|
||||||
func (c *Container) Remove(options types.ContainerRemoveOptions) error {
|
func (c *Container) Remove(options container.RemoveOptions) error {
|
||||||
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
|
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
|
||||||
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
|
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
|
||||||
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
|
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
|
||||||
|
|
@ -336,21 +57,41 @@ func (c *Container) Remove(options types.ContainerRemoveOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start starts the container
|
||||||
|
func (c *Container) Start() error {
|
||||||
|
c.Log.Warn(fmt.Sprintf("starting container %s", c.Name))
|
||||||
|
return c.Client.ContainerStart(context.Background(), c.ID, container.StartOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
// Stop stops the container
|
// Stop stops the container
|
||||||
func (c *Container) Stop() error {
|
func (c *Container) Stop() error {
|
||||||
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
|
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
|
||||||
return c.Client.ContainerStop(context.Background(), c.ID, nil)
|
return c.Client.ContainerStop(context.Background(), c.ID, container.StopOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause pauses the container
|
||||||
|
func (c *Container) Pause() error {
|
||||||
|
c.Log.Warn(fmt.Sprintf("pausing container %s", c.Name))
|
||||||
|
return c.Client.ContainerPause(context.Background(), c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpause unpauses the container
|
||||||
|
func (c *Container) Unpause() error {
|
||||||
|
c.Log.Warn(fmt.Sprintf("unpausing container %s", c.Name))
|
||||||
|
return c.Client.ContainerUnpause(context.Background(), c.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart restarts the container
|
// Restart restarts the container
|
||||||
func (c *Container) Restart() error {
|
func (c *Container) Restart() error {
|
||||||
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
|
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
|
||||||
return c.Client.ContainerRestart(context.Background(), c.ID, nil)
|
return c.Client.ContainerRestart(context.Background(), c.ID, container.StopOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach attaches the container
|
// Attach attaches the container
|
||||||
func (c *Container) Attach() (*exec.Cmd, error) {
|
func (c *Container) Attach() (*exec.Cmd, error) {
|
||||||
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
|
if !c.DetailsLoaded() {
|
||||||
|
return nil, errors.New(c.Tr.WaitingForContainerInfo)
|
||||||
|
}
|
||||||
|
|
||||||
// verify that we can in fact attach to this container
|
// verify that we can in fact attach to this container
|
||||||
if !c.Details.Config.OpenStdin {
|
if !c.Details.Config.OpenStdin {
|
||||||
|
|
@ -361,51 +102,25 @@ func (c *Container) Attach() (*exec.Cmd, error) {
|
||||||
return nil, errors.New(c.Tr.CannotAttachStoppedContainerError)
|
return nil, errors.New(c.Tr.CannotAttachStoppedContainerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := c.OSCommand.PrepareSubProcess("docker", "attach", "--sig-proxy=false", c.ID)
|
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
|
||||||
|
// TODO: use SDK
|
||||||
|
cmd := c.OSCommand.NewCmd("docker", "attach", "--sig-proxy=false", c.ID)
|
||||||
return cmd, nil
|
return cmd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top returns process information
|
// Top returns process information
|
||||||
func (c *Container) Top() (container.ContainerTopOKBody, error) {
|
func (c *Container) Top(ctx context.Context) (container.TopResponse, error) {
|
||||||
detail, err := c.Inspect()
|
detail, err := c.Inspect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return container.ContainerTopOKBody{}, err
|
return container.TopResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check container status
|
// check container status
|
||||||
if !detail.State.Running {
|
if !detail.State.Running {
|
||||||
return container.ContainerTopOKBody{}, errors.New("container is not running")
|
return container.TopResponse{}, errors.New("container is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Client.ContainerTop(context.Background(), c.ID, []string{})
|
return c.Client.ContainerTop(ctx, c.ID, []string{})
|
||||||
}
|
|
||||||
|
|
||||||
// EraseOldHistory removes any history before the user-specified max duration
|
|
||||||
func (c *Container) EraseOldHistory() {
|
|
||||||
if c.Config.UserConfig.Stats.MaxDuration == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, stat := range c.StatHistory {
|
|
||||||
if time.Since(stat.RecordedAt) < c.Config.UserConfig.Stats.MaxDuration {
|
|
||||||
c.StatHistory = c.StatHistory[i:]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewLogs attaches to a subprocess viewing the container's logs
|
|
||||||
func (c *Container) ViewLogs() (*exec.Cmd, error) {
|
|
||||||
templateString := c.OSCommand.Config.UserConfig.CommandTemplates.ViewContainerLogs
|
|
||||||
command := utils.ApplyTemplate(
|
|
||||||
templateString,
|
|
||||||
c.DockerCommand.NewCommandObject(CommandObject{Container: c}),
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd := c.OSCommand.ExecutableFromString(command)
|
|
||||||
c.OSCommand.PrepareForChildren(cmd)
|
|
||||||
|
|
||||||
return cmd, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PruneContainers prunes containers
|
// PruneContainers prunes containers
|
||||||
|
|
@ -415,13 +130,13 @@ func (c *DockerCommand) PruneContainers() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inspect returns details about the container
|
// Inspect returns details about the container
|
||||||
func (c *Container) Inspect() (types.ContainerJSON, error) {
|
func (c *Container) Inspect() (container.InspectResponse, error) {
|
||||||
return c.Client.ContainerInspect(context.Background(), c.ID)
|
return c.Client.ContainerInspect(context.Background(), c.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTop returns details about the container
|
// RenderTop returns details about the container
|
||||||
func (c *Container) RenderTop() (string, error) {
|
func (c *Container) RenderTop(ctx context.Context) (string, error) {
|
||||||
result, err := c.Top()
|
result, err := c.Top(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -429,7 +144,9 @@ func (c *Container) RenderTop() (string, error) {
|
||||||
return utils.RenderTable(append([][]string{result.Titles}, result.Processes...))
|
return utils.RenderTable(append([][]string{result.Titles}, result.Processes...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetailsLoaded tells us whether we have yet loaded the details for a container. Because this is an asynchronous operation, sometimes we have the container before we have its details. Details is a struct, not a pointer to a struct, so it starts off with heaps of zero values. One of which is the container Image, which starts as a blank string. Given that every container should have an image, this is a good proxy to use
|
// DetailsLoaded tells us whether we have yet loaded the details for a container.
|
||||||
|
// Sometimes it takes some time for a container to have its details loaded
|
||||||
|
// after it starts.
|
||||||
func (c *Container) DetailsLoaded() bool {
|
func (c *Container) DetailsLoaded() bool {
|
||||||
return c.Details.Image != ""
|
return c.Details.ContainerJSONBase != nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,8 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/asciigraph"
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
|
||||||
"github.com/mcuadros/go-lookup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct
|
// RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct
|
||||||
|
|
@ -56,10 +45,9 @@ type ContainerStats struct {
|
||||||
IoTimeRecursive []interface{} `json:"io_time_recursive"`
|
IoTimeRecursive []interface{} `json:"io_time_recursive"`
|
||||||
SectorsRecursive []interface{} `json:"sectors_recursive"`
|
SectorsRecursive []interface{} `json:"sectors_recursive"`
|
||||||
} `json:"blkio_stats"`
|
} `json:"blkio_stats"`
|
||||||
NumProcs int `json:"num_procs"`
|
NumProcs int `json:"num_procs"`
|
||||||
StorageStats struct {
|
StorageStats struct{} `json:"storage_stats"`
|
||||||
} `json:"storage_stats"`
|
CPUStats struct {
|
||||||
CPUStats struct {
|
|
||||||
CPUUsage struct {
|
CPUUsage struct {
|
||||||
TotalUsage int64 `json:"total_usage"`
|
TotalUsage int64 `json:"total_usage"`
|
||||||
PercpuUsage []int64 `json:"percpu_usage"`
|
PercpuUsage []int64 `json:"percpu_usage"`
|
||||||
|
|
@ -150,9 +138,8 @@ type ContainerStats struct {
|
||||||
func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
|
func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
|
||||||
cpuUsageDelta := s.CPUStats.CPUUsage.TotalUsage - s.PrecpuStats.CPUUsage.TotalUsage
|
cpuUsageDelta := s.CPUStats.CPUUsage.TotalUsage - s.PrecpuStats.CPUUsage.TotalUsage
|
||||||
cpuTotalUsageDelta := s.CPUStats.SystemCPUUsage - s.PrecpuStats.SystemCPUUsage
|
cpuTotalUsageDelta := s.CPUStats.SystemCPUUsage - s.PrecpuStats.SystemCPUUsage
|
||||||
numberOfCores := len(s.CPUStats.CPUUsage.PercpuUsage)
|
|
||||||
|
|
||||||
value := float64(cpuUsageDelta*100) * float64(numberOfCores) / float64(cpuTotalUsageDelta)
|
value := float64(cpuUsageDelta*100) / float64(cpuTotalUsageDelta)
|
||||||
if math.IsNaN(value) {
|
if math.IsNaN(value) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +148,6 @@ func (s *ContainerStats) CalculateContainerCPUPercentage() float64 {
|
||||||
|
|
||||||
// CalculateContainerMemoryUsage calculates the memory usage of the container as a percent of total available memory
|
// CalculateContainerMemoryUsage calculates the memory usage of the container as a percent of total available memory
|
||||||
func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
||||||
|
|
||||||
value := float64(s.MemoryStats.Usage*100) / float64(s.MemoryStats.Limit)
|
value := float64(s.MemoryStats.Usage*100) / float64(s.MemoryStats.Limit)
|
||||||
if math.IsNaN(value) {
|
if math.IsNaN(value) {
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -169,129 +155,34 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderStats returns a string containing the rendered stats of the container
|
func (c *Container) appendStats(stats *RecordedStats, maxDuration time.Duration) {
|
||||||
func (c *Container) RenderStats(viewWidth int) (string, error) {
|
c.StatsMutex.Lock()
|
||||||
|
defer c.StatsMutex.Unlock()
|
||||||
|
|
||||||
|
c.StatHistory = append(c.StatHistory, stats)
|
||||||
|
c.eraseOldHistory(maxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eraseOldHistory removes any history before the user-specified max duration
|
||||||
|
func (c *Container) eraseOldHistory(maxDuration time.Duration) {
|
||||||
|
if maxDuration == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, stat := range c.StatHistory {
|
||||||
|
if time.Since(stat.RecordedAt) < maxDuration {
|
||||||
|
c.StatHistory = c.StatHistory[i:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) GetLastStats() (*RecordedStats, bool) {
|
||||||
|
c.StatsMutex.Lock()
|
||||||
|
defer c.StatsMutex.Unlock()
|
||||||
history := c.StatHistory
|
history := c.StatHistory
|
||||||
if len(history) == 0 {
|
if len(history) == 0 {
|
||||||
return "", nil
|
return nil, false
|
||||||
}
|
|
||||||
currentStats := history[len(history)-1]
|
|
||||||
|
|
||||||
graphSpecs := c.Config.UserConfig.Stats.Graphs
|
|
||||||
graphs := make([]string, len(graphSpecs))
|
|
||||||
for i, spec := range graphSpecs {
|
|
||||||
graph, err := c.PlotGraph(spec, viewWidth-10)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
|
||||||
}
|
|
||||||
|
|
||||||
pidsCount := fmt.Sprintf("PIDs: %d", currentStats.ClientStats.PidsStats.Current)
|
|
||||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(currentStats.ClientStats.Networks.Eth0.RxBytes))
|
|
||||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(currentStats.ClientStats.Networks.Eth0.TxBytes))
|
|
||||||
|
|
||||||
originalJSON, err := json.MarshalIndent(currentStats, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
|
||||||
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
|
|
||||||
pidsCount,
|
|
||||||
dataReceived,
|
|
||||||
dataSent,
|
|
||||||
string(originalJSON),
|
|
||||||
)
|
|
||||||
|
|
||||||
return contents, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlotGraph returns the plotted graph based on the graph spec and the stat history
|
|
||||||
func (c *Container) PlotGraph(spec config.GraphConfig, width int) (string, error) {
|
|
||||||
data := make([]float64, len(c.StatHistory))
|
|
||||||
|
|
||||||
max := spec.Max
|
|
||||||
min := spec.Min
|
|
||||||
for i, stats := range c.StatHistory {
|
|
||||||
value, err := lookup.LookupString(stats, spec.StatPath)
|
|
||||||
if err != nil {
|
|
||||||
return "Could not find key: " + spec.StatPath, nil
|
|
||||||
}
|
|
||||||
floatValue, err := getFloat(value.Interface())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if spec.MinType == "" {
|
|
||||||
if i == 0 {
|
|
||||||
min = floatValue
|
|
||||||
} else if floatValue < min {
|
|
||||||
min = floatValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if spec.MaxType == "" {
|
|
||||||
if i == 0 {
|
|
||||||
max = floatValue
|
|
||||||
} else if floatValue > max {
|
|
||||||
max = floatValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data[i] = floatValue
|
|
||||||
}
|
|
||||||
|
|
||||||
height := 10
|
|
||||||
if spec.Height > 0 {
|
|
||||||
height = spec.Height
|
|
||||||
}
|
|
||||||
|
|
||||||
return asciigraph.Plot(
|
|
||||||
data,
|
|
||||||
asciigraph.Height(height),
|
|
||||||
asciigraph.Width(width),
|
|
||||||
asciigraph.Min(min),
|
|
||||||
asciigraph.Max(max),
|
|
||||||
asciigraph.Caption(fmt.Sprintf("%s: %0.2f (%v)", spec.Caption, data[len(data)-1], time.Since(c.StatHistory[0].RecordedAt).Round(time.Second))),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
|
|
||||||
func getFloat(unk interface{}) (float64, error) {
|
|
||||||
floatType := reflect.TypeOf(float64(0))
|
|
||||||
stringType := reflect.TypeOf("")
|
|
||||||
|
|
||||||
switch i := unk.(type) {
|
|
||||||
case float64:
|
|
||||||
return i, nil
|
|
||||||
case float32:
|
|
||||||
return float64(i), nil
|
|
||||||
case int64:
|
|
||||||
return float64(i), nil
|
|
||||||
case int32:
|
|
||||||
return float64(i), nil
|
|
||||||
case int:
|
|
||||||
return float64(i), nil
|
|
||||||
case uint64:
|
|
||||||
return float64(i), nil
|
|
||||||
case uint32:
|
|
||||||
return float64(i), nil
|
|
||||||
case uint:
|
|
||||||
return float64(i), nil
|
|
||||||
case string:
|
|
||||||
return strconv.ParseFloat(i, 64)
|
|
||||||
default:
|
|
||||||
v := reflect.ValueOf(unk)
|
|
||||||
v = reflect.Indirect(v)
|
|
||||||
if v.Type().ConvertibleTo(floatType) {
|
|
||||||
fv := v.Convert(floatType)
|
|
||||||
return fv.Float(), nil
|
|
||||||
} else if v.Type().ConvertibleTo(stringType) {
|
|
||||||
sv := v.Convert(stringType)
|
|
||||||
s := sv.String()
|
|
||||||
return strconv.ParseFloat(s, 64)
|
|
||||||
} else {
|
|
||||||
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return history[len(history)-1], true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
pkg/commands/container_stats_test.go
Normal file
17
pkg/commands/container_stats_test.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCalculateContainerCPUPercentage(t *testing.T) {
|
||||||
|
container := &ContainerStats{}
|
||||||
|
container.CPUStats.CPUUsage.TotalUsage = 10
|
||||||
|
container.CPUStats.SystemCPUUsage = 10
|
||||||
|
container.PrecpuStats.CPUUsage.TotalUsage = 5
|
||||||
|
container.PrecpuStats.SystemCPUUsage = 2
|
||||||
|
|
||||||
|
assert.EqualValues(t, 62.5, container.CalculateContainerCPUPercentage())
|
||||||
|
}
|
||||||
|
|
@ -5,24 +5,32 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
ogLog "log"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/acarl005/stripansi"
|
cliconfig "github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/docker/api/types"
|
ddocker "github.com/docker/cli/cli/context/docker"
|
||||||
|
ctxstore "github.com/docker/cli/cli/context/store"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands/ssh"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/sasha-s/go-deadlock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
APIVersion = "1.25"
|
dockerHostEnvKey = "DOCKER_HOST"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DockerCommand is our main docker interface
|
// DockerCommand is our main docker interface
|
||||||
|
|
@ -33,18 +41,17 @@ type DockerCommand struct {
|
||||||
Config *config.AppConfig
|
Config *config.AppConfig
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
InDockerComposeProject bool
|
InDockerComposeProject bool
|
||||||
ShowExited bool
|
// LocalProjectName is the compose project name for the directory where lazydocker was launched.
|
||||||
ErrorChan chan error
|
LocalProjectName string
|
||||||
ContainerMutex sync.Mutex
|
ErrorChan chan error
|
||||||
ServiceMutex sync.Mutex
|
ContainerMutex deadlock.Mutex
|
||||||
Services []*Service
|
ServiceMutex deadlock.Mutex
|
||||||
Containers []*Container
|
|
||||||
// DisplayContainers is the array of containers we will display in the containers panel. If Gui.ShowAllContainers is false, this will only be those containers which aren't based on a service. This reduces clutter and duplication in the UI
|
Closers []io.Closer
|
||||||
DisplayContainers []*Container
|
|
||||||
Images []*Image
|
|
||||||
Volumes []*Volume
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ io.Closer = &DockerCommand{}
|
||||||
|
|
||||||
// LimitedDockerCommand is a stripped-down DockerCommand with just the methods the container/service/image might need
|
// LimitedDockerCommand is a stripped-down DockerCommand with just the methods the container/service/image might need
|
||||||
type LimitedDockerCommand interface {
|
type LimitedDockerCommand interface {
|
||||||
NewCommandObject(CommandObject) CommandObject
|
NewCommandObject(CommandObject) CommandObject
|
||||||
|
|
@ -57,25 +64,70 @@ type CommandObject struct {
|
||||||
Container *Container
|
Container *Container
|
||||||
Image *Image
|
Image *Image
|
||||||
Volume *Volume
|
Volume *Volume
|
||||||
|
Network *Network
|
||||||
|
Project *Project
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
|
// NewCommandObject takes a command object and returns a default command object with the passed command object merged in
|
||||||
func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||||
defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose}
|
defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose}
|
||||||
mergo.Merge(&defaultObj, obj)
|
_ = mergo.Merge(&defaultObj, obj)
|
||||||
|
|
||||||
|
// When operating on a specific project, include -p flag so that
|
||||||
|
// docker compose targets the correct project.
|
||||||
|
if obj.Service != nil && obj.Service.ProjectName != "" {
|
||||||
|
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Service.ProjectName)
|
||||||
|
} else if obj.Project != nil && obj.Project.Name != "" {
|
||||||
|
defaultObj.DockerCompose = fmt.Sprintf("%s -p %s", defaultObj.DockerCompose, obj.Project.Name)
|
||||||
|
}
|
||||||
|
|
||||||
return defaultObj
|
return defaultObj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newDockerClient creates a Docker client with the given host.
|
||||||
|
// We avoid using client.FromEnv because it includes WithVersionFromEnv() which
|
||||||
|
// sets manualOverride=true when DOCKER_API_VERSION is set, preventing API version
|
||||||
|
// negotiation even when WithAPIVersionNegotiation() is specified.
|
||||||
|
// Instead, we explicitly configure only what we need, and rely on proper
|
||||||
|
// API version negotiation to support older Docker daemons.
|
||||||
|
// See https://github.com/jesseduffield/lazydocker/issues/715
|
||||||
|
func newDockerClient(dockerHost string) (*client.Client, error) {
|
||||||
|
return client.NewClientWithOpts(
|
||||||
|
client.WithTLSClientConfigFromEnv(),
|
||||||
|
client.WithAPIVersionNegotiation(),
|
||||||
|
client.WithHost(dockerHost),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// NewDockerCommand it runs docker commands
|
// NewDockerCommand it runs docker commands
|
||||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
||||||
opts := []func(*client.Client) error{
|
dockerHost, err := determineDockerHost()
|
||||||
client.WithVersion(APIVersion),
|
if err != nil {
|
||||||
client.FromEnv,
|
ogLog.Printf("> could not determine host %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := client.NewClientWithOpts(opts...)
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
dockerCommand := &DockerCommand{
|
||||||
|
|
@ -85,16 +137,11 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
|
||||||
Config: config,
|
Config: config,
|
||||||
Client: cli,
|
Client: cli,
|
||||||
ErrorChan: errorChan,
|
ErrorChan: errorChan,
|
||||||
ShowExited: true,
|
|
||||||
InDockerComposeProject: true,
|
InDockerComposeProject: true,
|
||||||
|
Closers: []io.Closer{tunnelCloser},
|
||||||
}
|
}
|
||||||
|
|
||||||
command := utils.ApplyTemplate(
|
dockerCommand.setDockerComposeCommand(config)
|
||||||
config.UserConfig.CommandTemplates.CheckDockerComposeConfig,
|
|
||||||
dockerCommand.NewCommandObject(CommandObject{}),
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Warn(command)
|
|
||||||
|
|
||||||
err = osCommand.RunCommand(
|
err = osCommand.RunCommand(
|
||||||
utils.ApplyTemplate(
|
utils.ApplyTemplate(
|
||||||
|
|
@ -107,68 +154,49 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat
|
||||||
log.Warn(err.Error())
|
log.Warn(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user passes -p outside of a compose directory, treat it as the
|
||||||
|
// local project so the project/services panels still appear and filtering
|
||||||
|
// is applied. Inside a compose dir, LocalProjectName is derived from
|
||||||
|
// container labels later in RefreshContainersAndServices.
|
||||||
|
if !dockerCommand.InDockerComposeProject && config.ProjectName != "" {
|
||||||
|
dockerCommand.LocalProjectName = config.ProjectName
|
||||||
|
}
|
||||||
|
|
||||||
return dockerCommand, nil
|
return dockerCommand, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonitorContainerStats is a function
|
// IsProjectScoped reports whether lazydocker should be scoped to a single
|
||||||
func (c *DockerCommand) MonitorContainerStats() {
|
// compose project — either because we're inside a compose directory or
|
||||||
go c.MonitorCLIContainerStats()
|
// because the user passed -p. When false, the project/services panels are
|
||||||
go c.MonitorClientContainerStats()
|
// hidden and all containers are shown in a flat list.
|
||||||
|
func (c *DockerCommand) IsProjectScoped() bool {
|
||||||
|
return c.InDockerComposeProject || c.Config.ProjectName != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonitorCLIContainerStats monitors a stream of container stats and updates the containers as each new stats object is received
|
func (c *DockerCommand) setDockerComposeCommand(config *config.AppConfig) {
|
||||||
func (c *DockerCommand) MonitorCLIContainerStats() {
|
if config.UserConfig.CommandTemplates.DockerCompose != "docker compose" {
|
||||||
command := `docker stats --all --no-trunc --format '{{json .}}'`
|
|
||||||
cmd := c.OSCommand.RunCustomCommand(command)
|
|
||||||
|
|
||||||
r, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
c.ErrorChan <- err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Start()
|
// 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")
|
||||||
scanner := bufio.NewScanner(r)
|
if err != nil {
|
||||||
scanner.Split(bufio.ScanLines)
|
config.UserConfig.CommandTemplates.DockerCompose = "docker-compose"
|
||||||
for scanner.Scan() {
|
|
||||||
var stats ContainerCliStat
|
|
||||||
// need to strip ANSI codes because uses escape sequences to clear the screen with each refresh
|
|
||||||
cleanString := stripansi.Strip(scanner.Text())
|
|
||||||
if err := json.Unmarshal([]byte(cleanString), &stats); err != nil {
|
|
||||||
c.ErrorChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.ContainerMutex.Lock()
|
|
||||||
for _, container := range c.Containers {
|
|
||||||
if container.ID == stats.ID {
|
|
||||||
container.CLIStats = stats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.ContainerMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MonitorClientContainerStats is a function
|
|
||||||
func (c *DockerCommand) MonitorClientContainerStats() {
|
|
||||||
// periodically loop through running containers and see if we need to create a monitor goroutine for any
|
|
||||||
// every second we check if we need to spawn a new goroutine
|
|
||||||
for range time.Tick(time.Second) {
|
|
||||||
for _, container := range c.Containers {
|
|
||||||
if !container.MonitoringStats {
|
|
||||||
go c.createClientStatMonitor(container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DockerCommand) createClientStatMonitor(container *Container) {
|
func (c *DockerCommand) Close() error {
|
||||||
|
return utils.CloseMany(c.Closers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DockerCommand) CreateClientStatMonitor(container *Container) {
|
||||||
container.MonitoringStats = true
|
container.MonitoringStats = true
|
||||||
stream, err := c.Client.ContainerStats(context.Background(), container.ID, true)
|
stream, err := c.Client.ContainerStats(context.Background(), container.ID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ErrorChan <- err
|
// not creating error panel because if we've disconnected from docker we'll
|
||||||
|
// have already created an error panel
|
||||||
|
c.Log.Error(err)
|
||||||
|
container.MonitoringStats = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,9 +206,9 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
data := scanner.Bytes()
|
data := scanner.Bytes()
|
||||||
var stats ContainerStats
|
var stats ContainerStats
|
||||||
json.Unmarshal(data, &stats)
|
_ = json.Unmarshal(data, &stats)
|
||||||
|
|
||||||
recordedStats := RecordedStats{
|
recordedStats := &RecordedStats{
|
||||||
ClientStats: stats,
|
ClientStats: stats,
|
||||||
DerivedStats: DerivedStats{
|
DerivedStats: DerivedStats{
|
||||||
CPUPercentage: stats.CalculateContainerCPUPercentage(),
|
CPUPercentage: stats.CalculateContainerCPUPercentage(),
|
||||||
|
|
@ -189,71 +217,152 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) {
|
||||||
RecordedAt: time.Now(),
|
RecordedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ContainerMutex.Lock()
|
container.appendStats(recordedStats, c.Config.UserConfig.Stats.MaxDuration)
|
||||||
container.StatHistory = append(container.StatHistory, recordedStats)
|
|
||||||
container.EraseOldHistory()
|
|
||||||
c.ContainerMutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
container.MonitoringStats = false
|
container.MonitoringStats = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshContainersAndServices returns a slice of docker containers
|
func (c *DockerCommand) RefreshContainersAndServices(currentContainers []*Container) ([]*Container, []*Service, error) {
|
||||||
func (c *DockerCommand) RefreshContainersAndServices() error {
|
|
||||||
c.ServiceMutex.Lock()
|
c.ServiceMutex.Lock()
|
||||||
defer c.ServiceMutex.Unlock()
|
defer c.ServiceMutex.Unlock()
|
||||||
|
|
||||||
currentServices := c.Services
|
containers, err := c.GetContainers(currentContainers)
|
||||||
|
|
||||||
containers, err := c.GetContainers()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var services []*Service
|
// Derive services from container labels (covers all projects)
|
||||||
// we only need to get these services once because they won't change in the runtime of the program
|
services := c.GetServicesFromContainers(containers)
|
||||||
if currentServices != nil {
|
|
||||||
services = currentServices
|
var composeServices []*Service
|
||||||
} else {
|
if c.InDockerComposeProject {
|
||||||
services, err = c.GetServices()
|
composeServices, err = c.GetServices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
c.Log.Warn("Failed to get compose services: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the local project name before merging services, since
|
||||||
|
// mergeServices needs it. We match compose service names against container
|
||||||
|
// labels to handle cases where the project name differs from the directory
|
||||||
|
// name (e.g. a `name:` directive in the compose file).
|
||||||
|
if c.LocalProjectName == "" && c.InDockerComposeProject && composeServices != nil {
|
||||||
|
for _, ctr := range containers {
|
||||||
|
if ctr.ProjectName == "" || ctr.ServiceName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, svc := range composeServices {
|
||||||
|
if ctr.ServiceName == svc.Name {
|
||||||
|
c.LocalProjectName = ctr.ProjectName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.LocalProjectName != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to directory name
|
||||||
|
if c.LocalProjectName == "" && c.Config.ProjectDir != "" {
|
||||||
|
c.LocalProjectName = filepath.Base(c.Config.ProjectDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge compose services (which include stopped services) with
|
||||||
|
// container-derived services from all projects
|
||||||
|
if composeServices != nil {
|
||||||
|
services = c.mergeServices(services, composeServices)
|
||||||
|
}
|
||||||
|
|
||||||
c.assignContainersToServices(containers, services)
|
c.assignContainersToServices(containers, services)
|
||||||
|
|
||||||
var displayContainers = containers
|
return containers, services, nil
|
||||||
if !c.Config.UserConfig.Gui.ShowAllContainers {
|
}
|
||||||
displayContainers = c.obtainStandaloneContainers(containers, services)
|
|
||||||
|
// GetServicesFromContainers derives services from container labels for all projects
|
||||||
|
func (c *DockerCommand) GetServicesFromContainers(containers []*Container) []*Service {
|
||||||
|
// Use project+service as key to avoid duplicates
|
||||||
|
type serviceKey struct {
|
||||||
|
project string
|
||||||
|
service string
|
||||||
|
}
|
||||||
|
seen := make(map[serviceKey]bool)
|
||||||
|
services := make([]*Service, 0, len(containers))
|
||||||
|
|
||||||
|
for _, ctr := range containers {
|
||||||
|
if ctr.ServiceName == "" || ctr.OneOff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := serviceKey{project: ctr.ProjectName, service: ctr.ServiceName}
|
||||||
|
if seen[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
services = append(services, &Service{
|
||||||
|
Name: ctr.ServiceName,
|
||||||
|
ID: ctr.ProjectName + "-" + ctr.ServiceName,
|
||||||
|
ProjectName: ctr.ProjectName,
|
||||||
|
OSCommand: c.OSCommand,
|
||||||
|
Log: c.Log,
|
||||||
|
DockerCommand: c,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort services first by whether they have a linked container, and second by alphabetical order
|
return services
|
||||||
sort.Slice(services, func(i, j int) bool {
|
}
|
||||||
if services[i].Container != nil && services[j].Container == nil {
|
|
||||||
return true
|
// mergeServices merges compose services (which may lack ProjectName) with
|
||||||
|
// container-derived services. Compose services take priority because they
|
||||||
|
// include services without running containers.
|
||||||
|
func (c *DockerCommand) mergeServices(containerServices []*Service, composeServices []*Service) []*Service {
|
||||||
|
// Set project name on compose services
|
||||||
|
for _, svc := range composeServices {
|
||||||
|
if svc.ProjectName == "" {
|
||||||
|
svc.ProjectName = c.LocalProjectName
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if services[i].Container == nil && services[j].Container != nil {
|
// Build a set of compose service names for the local project
|
||||||
return false
|
composeServiceNames := make(map[string]bool)
|
||||||
|
for _, svc := range composeServices {
|
||||||
|
composeServiceNames[svc.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with compose services, then add container-derived services
|
||||||
|
// that aren't already covered by compose (i.e. from other projects)
|
||||||
|
result := make([]*Service, 0, len(composeServices)+len(containerServices))
|
||||||
|
result = append(result, composeServices...)
|
||||||
|
|
||||||
|
for _, svc := range containerServices {
|
||||||
|
if svc.ProjectName == c.LocalProjectName && composeServiceNames[svc.Name] {
|
||||||
|
continue // already covered by compose service
|
||||||
}
|
}
|
||||||
|
result = append(result, svc)
|
||||||
|
}
|
||||||
|
|
||||||
return services[i].Name < services[j].Name
|
return result
|
||||||
})
|
}
|
||||||
|
|
||||||
c.Containers = containers
|
// GetProjectNames returns all unique project names from containers
|
||||||
c.Services = services
|
func (c *DockerCommand) GetProjectNames(containers []*Container) []string {
|
||||||
c.DisplayContainers = c.filterOutExited(displayContainers)
|
seen := make(map[string]bool)
|
||||||
|
var names []string
|
||||||
return nil
|
for _, ctr := range containers {
|
||||||
|
if ctr.ProjectName != "" && !seen[ctr.ProjectName] {
|
||||||
|
seen[ctr.ProjectName] = true
|
||||||
|
names = append(names, ctr.ProjectName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) {
|
||||||
L:
|
L:
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
for _, container := range containers {
|
for _, ctr := range containers {
|
||||||
if !container.OneOff && container.ServiceName == service.Name {
|
if !ctr.OneOff && ctr.ServiceName == service.Name && ctr.ProjectName == service.ProjectName {
|
||||||
service.Container = container
|
service.Container = ctr
|
||||||
continue L
|
continue L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -261,56 +370,24 @@ L:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterOutExited filters out the exited containers if c.ShowExited is false
|
|
||||||
func (c *DockerCommand) filterOutExited(containers []*Container) []*Container {
|
|
||||||
if c.ShowExited {
|
|
||||||
return containers
|
|
||||||
}
|
|
||||||
toReturn := []*Container{}
|
|
||||||
for _, container := range containers {
|
|
||||||
if container.Container.State != "exited" {
|
|
||||||
toReturn = append(toReturn, container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return toReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
// obtainStandaloneContainers returns standalone containers. Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context
|
|
||||||
func (c *DockerCommand) obtainStandaloneContainers(containers []*Container, services []*Service) []*Container {
|
|
||||||
standaloneContainers := []*Container{}
|
|
||||||
L:
|
|
||||||
for _, container := range containers {
|
|
||||||
for _, service := range services {
|
|
||||||
if !container.OneOff && container.ServiceName != "" && container.ServiceName == service.Name {
|
|
||||||
continue L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
standaloneContainers = append(standaloneContainers, container)
|
|
||||||
}
|
|
||||||
|
|
||||||
return standaloneContainers
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContainers gets the docker containers
|
// GetContainers gets the docker containers
|
||||||
func (c *DockerCommand) GetContainers() ([]*Container, error) {
|
func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) {
|
||||||
c.ContainerMutex.Lock()
|
c.ContainerMutex.Lock()
|
||||||
defer c.ContainerMutex.Unlock()
|
defer c.ContainerMutex.Unlock()
|
||||||
|
|
||||||
existingContainers := c.Containers
|
containers, err := c.Client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||||
|
|
||||||
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ownContainers := make([]*Container, len(containers))
|
ownContainers := make([]*Container, len(containers))
|
||||||
|
|
||||||
for i, container := range containers {
|
for i, ctr := range containers {
|
||||||
var newContainer *Container
|
var newContainer *Container
|
||||||
|
|
||||||
// check if we already data stored against the container
|
// check if we already have data stored against the container
|
||||||
for _, existingContainer := range existingContainers {
|
for _, existingContainer := range existingContainers {
|
||||||
if existingContainer.ID == container.ID {
|
if existingContainer.ID == ctr.ID {
|
||||||
newContainer = existingContainer
|
newContainer = existingContainer
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -319,31 +396,36 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
|
||||||
// initialise the container if it's completely new
|
// initialise the container if it's completely new
|
||||||
if newContainer == nil {
|
if newContainer == nil {
|
||||||
newContainer = &Container{
|
newContainer = &Container{
|
||||||
ID: container.ID,
|
ID: ctr.ID,
|
||||||
Client: c.Client,
|
Client: c.Client,
|
||||||
OSCommand: c.OSCommand,
|
OSCommand: c.OSCommand,
|
||||||
Log: c.Log,
|
Log: c.Log,
|
||||||
Config: c.Config,
|
|
||||||
DockerCommand: c,
|
DockerCommand: c,
|
||||||
Tr: c.Tr,
|
Tr: c.Tr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newContainer.Container = container
|
newContainer.Container = ctr
|
||||||
// if the container is made with a name label we will use that
|
// if the container is made with a name label we will use that
|
||||||
if name, ok := container.Labels["name"]; ok {
|
if name, ok := ctr.Labels["name"]; ok {
|
||||||
newContainer.Name = name
|
newContainer.Name = name
|
||||||
} else {
|
} else {
|
||||||
newContainer.Name = strings.TrimLeft(container.Names[0], "/")
|
if len(ctr.Names) > 0 {
|
||||||
|
newContainer.Name = strings.TrimLeft(ctr.Names[0], "/")
|
||||||
|
} else {
|
||||||
|
newContainer.Name = ctr.ID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
newContainer.ServiceName = container.Labels["com.docker.compose.service"]
|
newContainer.ServiceName = ctr.Labels["com.docker.compose.service"]
|
||||||
newContainer.ProjectName = container.Labels["com.docker.compose.project"]
|
newContainer.ProjectName = ctr.Labels["com.docker.compose.project"]
|
||||||
newContainer.ContainerNumber = container.Labels["com.docker.compose.container"]
|
newContainer.ContainerNumber = ctr.Labels["com.docker.compose.container"]
|
||||||
newContainer.OneOff = container.Labels["com.docker.compose.oneoff"] == "True"
|
newContainer.OneOff = ctr.Labels["com.docker.compose.oneoff"] == "True"
|
||||||
|
|
||||||
ownContainers[i] = newContainer
|
ownContainers[i] = newContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.SetContainerDetails(ownContainers)
|
||||||
|
|
||||||
return ownContainers, nil
|
return ownContainers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,22 +436,22 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
|
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
|
||||||
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand))
|
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// output looks like:
|
// output looks like:
|
||||||
// service1 998d6d286b0499e0ff23d66302e720991a2asdkf9c30d0542034f610daf8a971
|
// service1
|
||||||
// service2 asdld98asdklasd9bccd02438de0994f8e19cbe691feb3755336ec5ca2c55971
|
// service2
|
||||||
|
|
||||||
lines := utils.SplitLines(output)
|
lines := utils.SplitLines(output)
|
||||||
services := make([]*Service, len(lines))
|
services := make([]*Service, len(lines))
|
||||||
for i, str := range lines {
|
for i, str := range lines {
|
||||||
arr := strings.Split(str, " ")
|
|
||||||
services[i] = &Service{
|
services[i] = &Service{
|
||||||
Name: arr[0],
|
Name: str,
|
||||||
ID: arr[1],
|
ID: c.LocalProjectName + "-" + str,
|
||||||
|
ProjectName: c.LocalProjectName,
|
||||||
OSCommand: c.OSCommand,
|
OSCommand: c.OSCommand,
|
||||||
Log: c.Log,
|
Log: c.Log,
|
||||||
DockerCommand: c,
|
DockerCommand: c,
|
||||||
|
|
@ -379,43 +461,41 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
|
||||||
return services, nil
|
return services, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateContainerDetails attaches the details returned from docker inspect to each of the containers
|
func (c *DockerCommand) RefreshContainerDetails(containers []*Container) error {
|
||||||
// this contains a bit more info than what you get from the go-docker client
|
|
||||||
func (c *DockerCommand) UpdateContainerDetails() error {
|
|
||||||
c.ContainerMutex.Lock()
|
c.ContainerMutex.Lock()
|
||||||
defer c.ContainerMutex.Unlock()
|
defer c.ContainerMutex.Unlock()
|
||||||
|
|
||||||
containers := c.Containers
|
c.SetContainerDetails(containers)
|
||||||
|
|
||||||
ids := make([]string, len(containers))
|
|
||||||
for i, container := range containers {
|
|
||||||
ids[i] = container.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := c.OSCommand.RunCustomCommand("docker inspect " + strings.Join(ids, " "))
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var details []*Details
|
|
||||||
if err := json.Unmarshal(output, &details); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, container := range containers {
|
|
||||||
container.Details = *details[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attaches the details returned from docker inspect to each of the containers
|
||||||
|
// this contains a bit more info than what you get from the go-docker client
|
||||||
|
func (c *DockerCommand) SetContainerDetails(containers []*Container) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for _, ctr := range containers {
|
||||||
|
ctr := ctr
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
details, err := c.Client.ContainerInspect(context.Background(), ctr.ID)
|
||||||
|
if err != nil {
|
||||||
|
c.Log.Error(err)
|
||||||
|
} else {
|
||||||
|
ctr.Details = details
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
// ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose
|
||||||
func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
|
func (c *DockerCommand) ViewAllLogs(project *Project) (*exec.Cmd, error) {
|
||||||
cmd := c.OSCommand.ExecutableFromString(
|
cmd := c.OSCommand.ExecutableFromString(
|
||||||
utils.ApplyTemplate(
|
utils.ApplyTemplate(
|
||||||
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
|
c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs,
|
||||||
c.NewCommandObject(CommandObject{}),
|
c.NewCommandObject(CommandObject{Project: project}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -426,15 +506,80 @@ func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) {
|
||||||
|
|
||||||
// DockerComposeConfig returns the result of 'docker-compose config'
|
// DockerComposeConfig returns the result of 'docker-compose config'
|
||||||
func (c *DockerCommand) DockerComposeConfig() string {
|
func (c *DockerCommand) DockerComposeConfig() string {
|
||||||
|
return c.DockerComposeConfigForProject(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DockerComposeConfigForProject returns the result of 'docker-compose config' for a specific project
|
||||||
|
func (c *DockerCommand) DockerComposeConfigForProject(project *Project) string {
|
||||||
output, err := c.OSCommand.RunCommandWithOutput(
|
output, err := c.OSCommand.RunCommandWithOutput(
|
||||||
utils.ApplyTemplate(
|
utils.ApplyTemplate(
|
||||||
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
|
c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig,
|
||||||
c.NewCommandObject(CommandObject{}),
|
c.NewCommandObject(CommandObject{Project: project}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
output = err.Error()
|
output = err.Error()
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determineDockerHost tries to the determine the docker host that we should connect to
|
||||||
|
// in the following order of decreasing precedence:
|
||||||
|
// - value of "DOCKER_HOST" environment variable
|
||||||
|
// - host retrieved from the current context (specified via DOCKER_CONTEXT)
|
||||||
|
// - "default docker host" for the host operating system, otherwise
|
||||||
|
func determineDockerHost() (string, error) {
|
||||||
|
// If the docker host is explicitly set via the "DOCKER_HOST" environment variable,
|
||||||
|
// then its a no-brainer :shrug:
|
||||||
|
if os.Getenv("DOCKER_HOST") != "" {
|
||||||
|
return os.Getenv("DOCKER_HOST"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContext := os.Getenv("DOCKER_CONTEXT")
|
||||||
|
if currentContext == "" {
|
||||||
|
cf, err := cliconfig.Load(cliconfig.Dir())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
currentContext = cf.CurrentContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// On some systems (windows) `default` is stored in the docker config as the currentContext.
|
||||||
|
if currentContext == "" || currentContext == "default" {
|
||||||
|
// If a docker context is neither specified via the "DOCKER_CONTEXT" environment variable nor via the
|
||||||
|
// $HOME/.docker/config file, then we fall back to connecting to the "default docker host" meant for
|
||||||
|
// the host operating system.
|
||||||
|
return defaultDockerHost, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
storeConfig := ctxstore.NewConfig(
|
||||||
|
func() interface{} { return &ddocker.EndpointMeta{} },
|
||||||
|
ctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),
|
||||||
|
)
|
||||||
|
|
||||||
|
st := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)
|
||||||
|
md, err := st.GetMetadata(currentContext)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]
|
||||||
|
if !ok {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dockerEPMeta.Host != "" {
|
||||||
|
return dockerEPMeta.Host, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').
|
||||||
|
// For example:
|
||||||
|
// ```sh
|
||||||
|
// docker context create foo --docker "host="
|
||||||
|
// ```
|
||||||
|
// In such scenario, we mimic the `docker` cli and try to connect to the "default docker host".
|
||||||
|
return defaultDockerHost, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
7
pkg/commands/docker_host_unix.go
Normal file
7
pkg/commands/docker_host_unix.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDockerHost = "unix:///var/run/docker.sock"
|
||||||
|
)
|
||||||
5
pkg/commands/docker_host_windows.go
Normal file
5
pkg/commands/docker_host_windows.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDockerHost = "npipe:////./pipe/docker_engine"
|
||||||
|
)
|
||||||
91
pkg/commands/docker_test.go
Normal file
91
pkg/commands/docker_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNewDockerClientVersionNegotiation verifies that newDockerClient allows
|
||||||
|
// API version negotiation even when DOCKER_API_VERSION is set.
|
||||||
|
//
|
||||||
|
// This is a regression test for https://github.com/jesseduffield/lazydocker/issues/715
|
||||||
|
// where users got "client version 1.25 is too old" errors because FromEnv()
|
||||||
|
// includes WithVersionFromEnv() which sets manualOverride=true, preventing
|
||||||
|
// API version negotiation.
|
||||||
|
func TestNewDockerClientVersionNegotiation(t *testing.T) {
|
||||||
|
// Save original env var and restore after test
|
||||||
|
originalAPIVersion := os.Getenv("DOCKER_API_VERSION")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIVersion == "" {
|
||||||
|
os.Unsetenv("DOCKER_API_VERSION")
|
||||||
|
} else {
|
||||||
|
os.Setenv("DOCKER_API_VERSION", originalAPIVersion)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set DOCKER_API_VERSION to an old version that would cause
|
||||||
|
// "client version 1.25 is too old" errors if negotiation is disabled
|
||||||
|
os.Setenv("DOCKER_API_VERSION", "1.25")
|
||||||
|
|
||||||
|
t.Run("FromEnv locks version preventing negotiation", func(t *testing.T) {
|
||||||
|
// This demonstrates the problematic behavior we're avoiding.
|
||||||
|
// When using FromEnv with DOCKER_API_VERSION set, the client
|
||||||
|
// version gets locked to 1.25 and negotiation is disabled.
|
||||||
|
cli, err := client.NewClientWithOpts(
|
||||||
|
client.FromEnv,
|
||||||
|
client.WithAPIVersionNegotiation(),
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
// Version is locked to the env var value
|
||||||
|
assert.Equal(t, "1.25", cli.ClientVersion())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("newDockerClient allows version negotiation", func(t *testing.T) {
|
||||||
|
// Test the actual production function.
|
||||||
|
// Use DefaultDockerHost for cross-platform compatibility
|
||||||
|
// (unix socket on Linux/macOS, named pipe on Windows).
|
||||||
|
cli, err := newDockerClient(client.DefaultDockerHost)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer cli.Close()
|
||||||
|
|
||||||
|
// Version is NOT locked to the env var value (1.25).
|
||||||
|
// Instead, it uses the library's default version and will negotiate
|
||||||
|
// with the server on first request. This is the key difference that
|
||||||
|
// fixes the "version too old" error.
|
||||||
|
assert.NotEqual(t, "1.25", cli.ClientVersion(),
|
||||||
|
"client version should not be locked to DOCKER_API_VERSION env var")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsProjectScoped covers the predicate that drives whether the
|
||||||
|
// project/services panels appear and whether the containers panel filters by
|
||||||
|
// project. The "outside compose dir + -p" case is the regression we fixed
|
||||||
|
// after PR #776 silently disabled it.
|
||||||
|
func TestIsProjectScoped(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
inDockerComposeProject bool
|
||||||
|
projectName string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"inside compose dir, no -p", true, "", true},
|
||||||
|
{"inside compose dir, with -p", true, "myproject", true},
|
||||||
|
{"outside compose dir, no -p", false, "", false},
|
||||||
|
{"outside compose dir, with -p", false, "myproject", true},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c := &DockerCommand{
|
||||||
|
InDockerComposeProject: tc.inDockerComposeProject,
|
||||||
|
Config: &config.AppConfig{ProjectName: tc.projectName},
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.want, c.IsProjectScoped())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
|
|
@ -31,7 +31,7 @@ func NewDummyAppConfig() *config.AppConfig {
|
||||||
// NewDummyLog creates a new dummy Log for testing
|
// NewDummyLog creates a new dummy Log for testing
|
||||||
func NewDummyLog() *logrus.Entry {
|
func NewDummyLog() *logrus.Entry {
|
||||||
log := logrus.New()
|
log := logrus.New()
|
||||||
log.Out = ioutil.Discard
|
log.Out = io.Discard
|
||||||
return log.WithField("test", "test")
|
return log.WithField("test", "test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,10 +42,11 @@ func NewDummyDockerCommand() *DockerCommand {
|
||||||
|
|
||||||
// NewDummyDockerCommandWithOSCommand creates a new dummy DockerCommand for testing
|
// NewDummyDockerCommandWithOSCommand creates a new dummy DockerCommand for testing
|
||||||
func NewDummyDockerCommandWithOSCommand(osCommand *OSCommand) *DockerCommand {
|
func NewDummyDockerCommandWithOSCommand(osCommand *OSCommand) *DockerCommand {
|
||||||
|
newAppConfig := NewDummyAppConfig()
|
||||||
return &DockerCommand{
|
return &DockerCommand{
|
||||||
Log: NewDummyLog(),
|
Log: NewDummyLog(),
|
||||||
OSCommand: osCommand,
|
OSCommand: osCommand,
|
||||||
Tr: i18n.NewTranslationSet(NewDummyLog()),
|
Tr: i18n.NewTranslationSet(NewDummyLog(), newAppConfig.UserConfig.Gui.Language),
|
||||||
Config: NewDummyAppConfig(),
|
Config: newAppConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/docker/docker/api/types/image"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -18,21 +18,15 @@ type Image struct {
|
||||||
Name string
|
Name string
|
||||||
Tag string
|
Tag string
|
||||||
ID string
|
ID string
|
||||||
Image types.ImageSummary
|
Image image.Summary
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
OSCommand *OSCommand
|
OSCommand *OSCommand
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
DockerCommand LimitedDockerCommand
|
DockerCommand LimitedDockerCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayStrings returns the display string of Image
|
|
||||||
func (i *Image) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
|
|
||||||
return []string{i.Name, i.Tag, utils.FormatDecimalBytes(int(i.Image.Size))}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the image
|
// Remove removes the image
|
||||||
func (i *Image) Remove(options types.ImageRemoveOptions) error {
|
func (i *Image) Remove(options image.RemoveOptions) error {
|
||||||
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
|
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -40,19 +34,13 @@ func (i *Image) Remove(options types.ImageRemoveOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer is a layer in an image's history
|
func getHistoryResponseItemDisplayStrings(layer image.HistoryResponseItem) []string {
|
||||||
type Layer struct {
|
|
||||||
image.HistoryResponseItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStrings returns the array of strings describing the layer
|
|
||||||
func (l *Layer) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
tag := ""
|
tag := ""
|
||||||
if len(l.Tags) > 0 {
|
if len(layer.Tags) > 0 {
|
||||||
tag = l.Tags[0]
|
tag = layer.Tags[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
id := strings.TrimPrefix(l.ID, "sha256:")
|
id := strings.TrimPrefix(layer.ID, "sha256:")
|
||||||
if len(id) > 10 {
|
if len(id) > 10 {
|
||||||
id = id[0:10]
|
id = id[0:10]
|
||||||
}
|
}
|
||||||
|
|
@ -62,16 +50,16 @@ func (l *Layer) GetDisplayStrings(isFocused bool) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
|
dockerFileCommandPrefix := "/bin/sh -c #(nop) "
|
||||||
createdBy := l.CreatedBy
|
createdBy := layer.CreatedBy
|
||||||
if strings.Contains(l.CreatedBy, dockerFileCommandPrefix) {
|
if strings.Contains(layer.CreatedBy, dockerFileCommandPrefix) {
|
||||||
createdBy = strings.Trim(strings.TrimPrefix(l.CreatedBy, dockerFileCommandPrefix), " ")
|
createdBy = strings.Trim(strings.TrimPrefix(layer.CreatedBy, dockerFileCommandPrefix), " ")
|
||||||
split := strings.Split(createdBy, " ")
|
split := strings.Split(createdBy, " ")
|
||||||
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
|
createdBy = utils.ColoredString(split[0], color.FgYellow) + " " + strings.Join(split[1:], " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
createdBy = strings.Replace(createdBy, "\t", " ", -1)
|
createdBy = strings.Replace(createdBy, "\t", " ", -1)
|
||||||
|
|
||||||
size := utils.FormatBinaryBytes(int(l.Size))
|
size := utils.FormatBinaryBytes(int(layer.Size))
|
||||||
sizeColor := color.FgWhite
|
sizeColor := color.FgWhite
|
||||||
if size == "0B" {
|
if size == "0B" {
|
||||||
sizeColor = color.FgBlue
|
sizeColor = color.FgBlue
|
||||||
|
|
@ -87,34 +75,33 @@ func (l *Layer) GetDisplayStrings(isFocused bool) []string {
|
||||||
|
|
||||||
// RenderHistory renders the history of the image
|
// RenderHistory renders the history of the image
|
||||||
func (i *Image) RenderHistory() (string, error) {
|
func (i *Image) RenderHistory() (string, error) {
|
||||||
|
|
||||||
history, err := i.Client.ImageHistory(context.Background(), i.ID)
|
history, err := i.Client.ImageHistory(context.Background(), i.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
layers := make([]*Layer, len(history))
|
tableBody := lo.Map(history, func(layer image.HistoryResponseItem, _ int) []string {
|
||||||
for i, layer := range history {
|
return getHistoryResponseItemDisplayStrings(layer)
|
||||||
layers[i] = &Layer{layer}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"}))
|
headers := [][]string{{"ID", "TAG", "SIZE", "COMMAND"}}
|
||||||
|
table := append(headers, tableBody...)
|
||||||
|
|
||||||
|
return utils.RenderTable(table)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshImages returns a slice of docker images
|
// RefreshImages returns a slice of docker images
|
||||||
func (c *DockerCommand) RefreshImages() ([]*Image, error) {
|
func (c *DockerCommand) RefreshImages() ([]*Image, error) {
|
||||||
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
|
images, err := c.Client.ImageList(context.Background(), image.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ownImages := make([]*Image, len(images))
|
ownImages := make([]*Image, len(images))
|
||||||
|
|
||||||
for i, image := range images {
|
for i, img := range images {
|
||||||
// func (cli *Client) ImageHistory(ctx context.Context, imageID string) ([]image.HistoryResponseItem, error)
|
|
||||||
|
|
||||||
firstTag := ""
|
firstTag := ""
|
||||||
tags := image.RepoTags
|
tags := img.RepoTags
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
firstTag = tags[0]
|
firstTag = tags[0]
|
||||||
}
|
}
|
||||||
|
|
@ -125,13 +112,20 @@ func (c *DockerCommand) RefreshImages() ([]*Image, error) {
|
||||||
if len(nameParts) > 1 {
|
if len(nameParts) > 1 {
|
||||||
tag = nameParts[len(nameParts)-1]
|
tag = nameParts[len(nameParts)-1]
|
||||||
name = strings.Join(nameParts[:len(nameParts)-1], ":")
|
name = strings.Join(nameParts[:len(nameParts)-1], ":")
|
||||||
|
|
||||||
|
for prefix, replacement := range c.Config.UserConfig.Replacements.ImageNamePrefixes {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
name = strings.Replace(name, prefix, replacement, 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ownImages[i] = &Image{
|
ownImages[i] = &Image{
|
||||||
ID: image.ID,
|
ID: img.ID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
Image: image,
|
Image: img,
|
||||||
Client: c.Client,
|
Client: c.Client,
|
||||||
OSCommand: c.OSCommand,
|
OSCommand: c.OSCommand,
|
||||||
Log: c.Log,
|
Log: c.Log,
|
||||||
|
|
|
||||||
54
pkg/commands/network.go
Normal file
54
pkg/commands/network.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Network : A docker Network
|
||||||
|
type Network struct {
|
||||||
|
Name string
|
||||||
|
Network network.Inspect
|
||||||
|
Client *client.Client
|
||||||
|
OSCommand *OSCommand
|
||||||
|
Log *logrus.Entry
|
||||||
|
DockerCommand LimitedDockerCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshNetworks gets the networks and stores them
|
||||||
|
func (c *DockerCommand) RefreshNetworks() ([]*Network, error) {
|
||||||
|
networks, err := c.Client.NetworkList(context.Background(), network.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ownNetworks := make([]*Network, len(networks))
|
||||||
|
|
||||||
|
for i, nw := range networks {
|
||||||
|
ownNetworks[i] = &Network{
|
||||||
|
Name: nw.Name,
|
||||||
|
Network: nw,
|
||||||
|
Client: c.Client,
|
||||||
|
OSCommand: c.OSCommand,
|
||||||
|
Log: c.Log,
|
||||||
|
DockerCommand: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ownNetworks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PruneNetworks prunes networks
|
||||||
|
func (c *DockerCommand) PruneNetworks() error {
|
||||||
|
_, err := c.Client.NetworksPrune(context.Background(), filters.Args{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the network
|
||||||
|
func (v *Network) Remove() error {
|
||||||
|
return v.Client.NetworkRemove(context.Background(), v.Name)
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/kill"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
"github.com/mgutz/str"
|
"github.com/mgutz/str"
|
||||||
|
|
@ -21,13 +22,11 @@ import (
|
||||||
|
|
||||||
// Platform stores the os state
|
// Platform stores the os state
|
||||||
type Platform struct {
|
type Platform struct {
|
||||||
os string
|
os string
|
||||||
shell string
|
shell string
|
||||||
shellArg string
|
shellArg string
|
||||||
escapedQuote string
|
openCommand string
|
||||||
openCommand string
|
openLinkCommand string
|
||||||
openLinkCommand string
|
|
||||||
fallbackEscapedQuote string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OSCommand holds all the os commands
|
// OSCommand holds all the os commands
|
||||||
|
|
@ -60,7 +59,16 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||||
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
||||||
cmd := c.ExecutableFromString(command)
|
cmd := c.ExecutableFromString(command)
|
||||||
before := time.Now()
|
before := time.Now()
|
||||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
output, err := sanitisedCommandOutput(cmd.Output())
|
||||||
|
c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before)))
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||||
|
func (c *OSCommand) RunCommandWithOutputContext(ctx context.Context, command string) (string, error) {
|
||||||
|
cmd := c.ExecutableFromStringContext(ctx, command)
|
||||||
|
before := time.Now()
|
||||||
|
output, err := sanitisedCommandOutput(cmd.Output())
|
||||||
c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before)))
|
c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before)))
|
||||||
return output, err
|
return output, err
|
||||||
}
|
}
|
||||||
|
|
@ -79,8 +87,38 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
|
||||||
// ExecutableFromString takes a string like `docker ps -a` and returns an executable command for it
|
// ExecutableFromString takes a string like `docker ps -a` and returns an executable command for it
|
||||||
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
||||||
splitCmd := str.ToArgv(commandStr)
|
splitCmd := str.ToArgv(commandStr)
|
||||||
// c.Log.Info(splitCmd)
|
return c.NewCmd(splitCmd[0], splitCmd[1:]...)
|
||||||
return c.command(splitCmd[0], splitCmd[1:]...)
|
}
|
||||||
|
|
||||||
|
// Same as ExecutableFromString but cancellable via a context
|
||||||
|
func (c *OSCommand) ExecutableFromStringContext(ctx context.Context, commandStr string) *exec.Cmd {
|
||||||
|
splitCmd := str.ToArgv(commandStr)
|
||||||
|
return exec.CommandContext(ctx, splitCmd[0], splitCmd[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OSCommand) NewCmd(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||||
|
cmd := c.command(cmdName, commandArgs...)
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OSCommand) NewCommandStringWithShell(commandStr string) string {
|
||||||
|
var quotedCommand string
|
||||||
|
// Windows does not seem to like quotes around the command
|
||||||
|
if c.Platform.os == "windows" {
|
||||||
|
quotedCommand = strings.NewReplacer(
|
||||||
|
"^", "^^",
|
||||||
|
"&", "^&",
|
||||||
|
"|", "^|",
|
||||||
|
"<", "^<",
|
||||||
|
">", "^>",
|
||||||
|
"%", "^%",
|
||||||
|
).Replace(commandStr)
|
||||||
|
} else {
|
||||||
|
quotedCommand = c.Quote(commandStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s %s", c.Platform.shell, c.Platform.shellArg, quotedCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunCommand runs a command and just returns the error
|
// RunCommand runs a command and just returns the error
|
||||||
|
|
@ -101,25 +139,16 @@ func (c *OSCommand) FileType(path string) string {
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunDirectCommand wrapper around direct commands
|
|
||||||
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
|
||||||
c.Log.WithField("command", command).Info("RunDirectCommand")
|
|
||||||
|
|
||||||
return sanitisedCommandOutput(
|
|
||||||
c.command(c.Platform.shell, c.Platform.shellArg, command).
|
|
||||||
CombinedOutput(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||||
outputString := string(output)
|
outputString := string(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||||
// from the combined output
|
// from stderr if we got an ExitError
|
||||||
if outputString == "" {
|
exitError, ok := err.(*exec.ExitError)
|
||||||
return "", WrapError(err)
|
if ok {
|
||||||
|
return outputString, errors.New(string(exitError.Stderr))
|
||||||
}
|
}
|
||||||
return outputString, errors.New(outputString)
|
return "", WrapError(err)
|
||||||
}
|
}
|
||||||
return outputString, nil
|
return outputString, nil
|
||||||
}
|
}
|
||||||
|
|
@ -164,22 +193,28 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||||
return nil, errors.New("No editor defined in $VISUAL or $EDITOR")
|
return nil, errors.New("No editor defined in $VISUAL or $EDITOR")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.PrepareSubProcess(editor, filename), nil
|
return c.NewCmd(editor, filename), nil
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
|
||||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
|
||||||
return c.command(cmdName, commandArgs...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quote wraps a message in platform-specific quotation marks
|
// Quote wraps a message in platform-specific quotation marks
|
||||||
func (c *OSCommand) Quote(message string) string {
|
func (c *OSCommand) Quote(message string) string {
|
||||||
message = strings.Replace(message, "`", "\\`", -1)
|
var quote string
|
||||||
escapedQuote := c.Platform.escapedQuote
|
if c.Platform.os == "windows" {
|
||||||
if strings.Contains(message, c.Platform.escapedQuote) {
|
quote = `\"`
|
||||||
escapedQuote = c.Platform.fallbackEscapedQuote
|
message = strings.NewReplacer(
|
||||||
|
`"`, `"'"'"`,
|
||||||
|
`\"`, `\\"`,
|
||||||
|
).Replace(message)
|
||||||
|
} else {
|
||||||
|
quote = `"`
|
||||||
|
message = strings.NewReplacer(
|
||||||
|
`\`, `\\`,
|
||||||
|
`"`, `\"`,
|
||||||
|
`$`, `\$`,
|
||||||
|
"`", "\\`",
|
||||||
|
).Replace(message)
|
||||||
}
|
}
|
||||||
return escapedQuote + message + escapedQuote
|
return quote + message + quote
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unquote removes wrapping quotations marks if they are present
|
// Unquote removes wrapping quotations marks if they are present
|
||||||
|
|
@ -190,7 +225,7 @@ func (c *OSCommand) Unquote(message string) string {
|
||||||
|
|
||||||
// AppendLineToFile adds a new line in file
|
// AppendLineToFile adds a new line in file
|
||||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return WrapError(err)
|
return WrapError(err)
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +240,7 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||||
|
|
||||||
// CreateTempFile writes a string to a new temp file and returns the file's name
|
// CreateTempFile writes a string to a new temp file and returns the file's name
|
||||||
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||||
tmpfile, err := ioutil.TempFile("", filename)
|
tmpfile, err := os.CreateTemp("", filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log.Error(err)
|
c.Log.Error(err)
|
||||||
return "", WrapError(err)
|
return "", WrapError(err)
|
||||||
|
|
@ -267,12 +302,11 @@ func (c *OSCommand) GetLazydockerPath() string {
|
||||||
|
|
||||||
// RunCustomCommand returns the pointer to a custom command
|
// RunCustomCommand returns the pointer to a custom command
|
||||||
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
|
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
|
||||||
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
|
return c.NewCmd(c.Platform.shell, c.Platform.shellArg, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||||
|
|
||||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||||
|
|
||||||
for i, str := range commandStrings {
|
for i, str := range commandStrings {
|
||||||
|
|
@ -308,7 +342,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||||
c.Log.Error(err)
|
c.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b, err := ioutil.ReadAll(stderr); err == nil {
|
if b, err := io.ReadAll(stderr); err == nil {
|
||||||
if len(b) > 0 {
|
if len(b) > 0 {
|
||||||
finalErrors = append(finalErrors, string(b))
|
finalErrors = append(finalErrors, string(b))
|
||||||
}
|
}
|
||||||
|
|
@ -332,21 +366,10 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||||
|
|
||||||
// Kill kills a process. If the process has Setpgid == true, then we have anticipated that it might spawn its own child processes, so we've given it a process group ID (PGID) equal to its process id (PID) and given its child processes will inherit the PGID, we can kill that group, rather than killing the process itself.
|
// Kill kills a process. If the process has Setpgid == true, then we have anticipated that it might spawn its own child processes, so we've given it a process group ID (PGID) equal to its process id (PID) and given its child processes will inherit the PGID, we can kill that group, rather than killing the process itself.
|
||||||
func (c *OSCommand) Kill(cmd *exec.Cmd) error {
|
func (c *OSCommand) Kill(cmd *exec.Cmd) error {
|
||||||
if cmd.Process == nil {
|
return kill.Kill(cmd)
|
||||||
// somebody got to it before we were able to, poor bastard
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cmd.SysProcAttr != nil && cmd.SysProcAttr.Setpgid {
|
|
||||||
// minus sign means we're talking about a PGID as opposed to a PID
|
|
||||||
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd.Process.Kill()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a sideproject, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
|
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a subprocess, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
|
||||||
func (c *OSCommand) PrepareForChildren(cmd *exec.Cmd) {
|
func (c *OSCommand) PrepareForChildren(cmd *exec.Cmd) {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
kill.PrepareForChildren(cmd)
|
||||||
Setpgid: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package commands
|
package commands
|
||||||
|
|
@ -8,12 +9,10 @@ import (
|
||||||
|
|
||||||
func getPlatform() *Platform {
|
func getPlatform() *Platform {
|
||||||
return &Platform{
|
return &Platform{
|
||||||
os: runtime.GOOS,
|
os: runtime.GOOS,
|
||||||
shell: "bash",
|
shell: "bash",
|
||||||
shellArg: "-c",
|
shellArg: "-c",
|
||||||
escapedQuote: "'",
|
openCommand: "open {{filename}}",
|
||||||
openCommand: "open {{filename}}",
|
openLinkCommand: "open {{link}}",
|
||||||
openLinkCommand: "open {{link}}",
|
|
||||||
fallbackEscapedQuote: "\"",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -89,7 +89,7 @@ func TestOSCommandEditFile(t *testing.T) {
|
||||||
|
|
||||||
assert.EqualValues(t, "nano", name)
|
assert.EqualValues(t, "nano", name)
|
||||||
|
|
||||||
return nil
|
return exec.Command("exit", "0")
|
||||||
},
|
},
|
||||||
func(env string) string {
|
func(env string) string {
|
||||||
if env == "VISUAL" {
|
if env == "VISUAL" {
|
||||||
|
|
@ -111,7 +111,7 @@ func TestOSCommandEditFile(t *testing.T) {
|
||||||
|
|
||||||
assert.EqualValues(t, "emacs", name)
|
assert.EqualValues(t, "emacs", name)
|
||||||
|
|
||||||
return nil
|
return exec.Command("exit", "0")
|
||||||
},
|
},
|
||||||
func(env string) string {
|
func(env string) string {
|
||||||
if env == "EDITOR" {
|
if env == "EDITOR" {
|
||||||
|
|
@ -133,7 +133,7 @@ func TestOSCommandEditFile(t *testing.T) {
|
||||||
|
|
||||||
assert.EqualValues(t, "vi", name)
|
assert.EqualValues(t, "vi", name)
|
||||||
|
|
||||||
return nil
|
return exec.Command("exit", "0")
|
||||||
},
|
},
|
||||||
func(env string) string {
|
func(env string) string {
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -153,13 +153,14 @@ func TestOSCommandEditFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOSCommandQuote is a function.
|
|
||||||
func TestOSCommandQuote(t *testing.T) {
|
func TestOSCommandQuote(t *testing.T) {
|
||||||
osCommand := NewDummyOSCommand()
|
osCommand := NewDummyOSCommand()
|
||||||
|
|
||||||
|
osCommand.Platform.os = "linux"
|
||||||
|
|
||||||
actual := osCommand.Quote("hello `test`")
|
actual := osCommand.Quote("hello `test`")
|
||||||
|
|
||||||
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
|
expected := "\"hello \\`test\\`\""
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +173,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||||
|
|
||||||
actual := osCommand.Quote("hello 'test'")
|
actual := osCommand.Quote("hello 'test'")
|
||||||
|
|
||||||
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
|
expected := `"hello 'test'"`
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +186,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||||
|
|
||||||
actual := osCommand.Quote(`hello "test"`)
|
actual := osCommand.Quote(`hello "test"`)
|
||||||
|
|
||||||
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
|
expected := `"hello \"test\""`
|
||||||
|
|
||||||
|
assert.EqualValues(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOSCommandQuoteWindows tests the quote function for Windows
|
||||||
|
func TestOSCommandQuoteWindows(t *testing.T) {
|
||||||
|
osCommand := NewDummyOSCommand()
|
||||||
|
|
||||||
|
osCommand.Platform.os = "windows"
|
||||||
|
|
||||||
|
actual := osCommand.Quote(`hello "test" 'test2'`)
|
||||||
|
|
||||||
|
expected := `\"hello "'"'"test"'"'" 'test2'\"`
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +249,7 @@ func TestOSCommandFileType(t *testing.T) {
|
||||||
{
|
{
|
||||||
"testDirectory",
|
"testDirectory",
|
||||||
func() {
|
func() {
|
||||||
if err := os.Mkdir("testDirectory", 0644); err != nil {
|
if err := os.Mkdir("testDirectory", 0o644); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -275,7 +289,7 @@ func TestOSCommandCreateTempFile(t *testing.T) {
|
||||||
func(path string, err error) {
|
func(path string, err error) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
content, err := ioutil.ReadFile(path)
|
content, err := os.ReadFile(path)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "content", string(content))
|
assert.Equal(t, "content", string(content))
|
||||||
|
|
@ -289,3 +303,53 @@ func TestOSCommandCreateTempFile(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOSCommandExecutableFromStringWithShellLinux(t *testing.T) {
|
||||||
|
osCommand := NewDummyOSCommand()
|
||||||
|
|
||||||
|
osCommand.Platform.os = "linux"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
commandStr string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"success",
|
||||||
|
"pwd",
|
||||||
|
fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "\"pwd\""),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := osCommand.NewCommandStringWithShell(tt.commandStr)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOSCommandNewCommandStringWithShellWindows(t *testing.T) {
|
||||||
|
osCommand := NewDummyOSCommand()
|
||||||
|
|
||||||
|
osCommand.Platform.os = "windows"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
commandStr string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"success",
|
||||||
|
"pwd",
|
||||||
|
fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "pwd"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := osCommand.NewCommandStringWithShell(tt.commandStr)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ package commands
|
||||||
|
|
||||||
func getPlatform() *Platform {
|
func getPlatform() *Platform {
|
||||||
return &Platform{
|
return &Platform{
|
||||||
os: "windows",
|
os: "windows",
|
||||||
shell: "cmd",
|
shell: "cmd",
|
||||||
shellArg: "/c",
|
shellArg: "/c",
|
||||||
escapedQuote: `\"`,
|
|
||||||
fallbackEscapedQuote: "\\'",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
pkg/commands/project.go
Normal file
5
pkg/commands/project.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/docker/api/types/container"
|
"context"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
@ -14,43 +13,41 @@ import (
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string
|
Name string
|
||||||
ID string
|
ID string
|
||||||
|
ProjectName string
|
||||||
OSCommand *OSCommand
|
OSCommand *OSCommand
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
Container *Container
|
Container *Container
|
||||||
DockerCommand LimitedDockerCommand
|
DockerCommand LimitedDockerCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayStrings returns the dispaly string of Container
|
|
||||||
func (s *Service) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
|
|
||||||
if s.Container == nil {
|
|
||||||
return []string{utils.ColoredString("none", color.FgBlue), s.Name, ""}
|
|
||||||
}
|
|
||||||
|
|
||||||
cont := s.Container
|
|
||||||
return []string{cont.GetDisplayStatus(), s.Name, cont.GetDisplayCPUPerc()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the service's containers
|
// Remove removes the service's containers
|
||||||
func (s *Service) Remove(options types.ContainerRemoveOptions) error {
|
func (s *Service) Remove(options container.RemoveOptions) error {
|
||||||
return s.Container.Remove(options)
|
return s.Container.Remove(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the service's containers
|
// Stop stops the service's containers
|
||||||
func (s *Service) Stop() error {
|
func (s *Service) Stop() error {
|
||||||
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StopService
|
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StopService)
|
||||||
command := utils.ApplyTemplate(
|
}
|
||||||
templateString,
|
|
||||||
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
// Up up's the service
|
||||||
)
|
func (s *Service) Up() error {
|
||||||
return s.OSCommand.RunCommand(command)
|
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.UpService)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart restarts the service
|
// Restart restarts the service
|
||||||
func (s *Service) Restart() error {
|
func (s *Service) Restart() error {
|
||||||
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.RestartService
|
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.RestartService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the service
|
||||||
|
func (s *Service) Start() error {
|
||||||
|
return s.runCommand(s.OSCommand.Config.UserConfig.CommandTemplates.StartService)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) runCommand(templateCmdStr string) error {
|
||||||
command := utils.ApplyTemplate(
|
command := utils.ApplyTemplate(
|
||||||
templateString,
|
templateCmdStr,
|
||||||
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
||||||
)
|
)
|
||||||
return s.OSCommand.RunCommand(command)
|
return s.OSCommand.RunCommand(command)
|
||||||
|
|
@ -61,11 +58,6 @@ func (s *Service) Attach() (*exec.Cmd, error) {
|
||||||
return s.Container.Attach()
|
return s.Container.Attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top returns process information
|
|
||||||
func (s *Service) Top() (container.ContainerTopOKBody, error) {
|
|
||||||
return s.Container.Top()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewLogs attaches to a subprocess viewing the service's logs
|
// ViewLogs attaches to a subprocess viewing the service's logs
|
||||||
func (s *Service) ViewLogs() (*exec.Cmd, error) {
|
func (s *Service) ViewLogs() (*exec.Cmd, error) {
|
||||||
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs
|
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs
|
||||||
|
|
@ -81,12 +73,12 @@ func (s *Service) ViewLogs() (*exec.Cmd, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTop renders the process list of the service
|
// RenderTop renders the process list of the service
|
||||||
func (s *Service) RenderTop() (string, error) {
|
func (s *Service) RenderTop(ctx context.Context) (string, error) {
|
||||||
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop
|
templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop
|
||||||
command := utils.ApplyTemplate(
|
command := utils.ApplyTemplate(
|
||||||
templateString,
|
templateString,
|
||||||
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
s.DockerCommand.NewCommandObject(CommandObject{Service: s}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return s.OSCommand.RunCommandWithOutput(command)
|
return s.OSCommand.RunCommandWithOutputContext(ctx, command)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
pkg/commands/ssh/ssh.go
Normal file
159
pkg/commands/ssh/ssh.go
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// we only need these two methods from our OSCommand struct, for killing commands
|
||||||
|
type CmdKiller interface {
|
||||||
|
Kill(cmd *exec.Cmd) error
|
||||||
|
PrepareForChildren(cmd *exec.Cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SSHHandler struct {
|
||||||
|
oSCommand CmdKiller
|
||||||
|
|
||||||
|
dialContext func(ctx context.Context, network, addr string) (io.Closer, error)
|
||||||
|
startCmd func(*exec.Cmd) error
|
||||||
|
tempDir func(dir string, pattern string) (name string, err error)
|
||||||
|
getenv func(key string) string
|
||||||
|
setenv func(key, value string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSSHHandler(oSCommand CmdKiller) *SSHHandler {
|
||||||
|
return &SSHHandler{
|
||||||
|
oSCommand: oSCommand,
|
||||||
|
|
||||||
|
dialContext: func(ctx context.Context, network, addr string) (io.Closer, error) {
|
||||||
|
return (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||||
|
},
|
||||||
|
startCmd: func(cmd *exec.Cmd) error { return cmd.Start() },
|
||||||
|
tempDir: os.MkdirTemp,
|
||||||
|
getenv: os.Getenv,
|
||||||
|
setenv: os.Setenv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSSHDockerHost overrides the DOCKER_HOST environment variable
|
||||||
|
// to point towards a local unix socket tunneled over SSH to the specified ssh host.
|
||||||
|
func (self *SSHHandler) HandleSSHDockerHost() (io.Closer, error) {
|
||||||
|
const key = "DOCKER_HOST"
|
||||||
|
ctx := context.Background()
|
||||||
|
u, err := url.Parse(self.getenv(key))
|
||||||
|
if err != nil {
|
||||||
|
// if no or an invalid docker host is specified, continue nominally
|
||||||
|
return noopCloser{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the docker host scheme is "ssh", forward the docker socket before creating the client
|
||||||
|
if u.Scheme == "ssh" {
|
||||||
|
tunnel, err := self.createDockerHostTunnel(ctx, u.Host)
|
||||||
|
if err != nil {
|
||||||
|
return noopCloser{}, fmt.Errorf("tunnel ssh docker host: %w", err)
|
||||||
|
}
|
||||||
|
err = self.setenv(key, tunnel.socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return noopCloser{}, fmt.Errorf("override DOCKER_HOST to tunneled socket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tunnel, nil
|
||||||
|
}
|
||||||
|
return noopCloser{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopCloser struct{}
|
||||||
|
|
||||||
|
func (noopCloser) Close() error { return nil }
|
||||||
|
|
||||||
|
type tunneledDockerHost struct {
|
||||||
|
socketPath string
|
||||||
|
cmd *exec.Cmd
|
||||||
|
oSCommand CmdKiller
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.Closer = (*tunneledDockerHost)(nil)
|
||||||
|
|
||||||
|
func (t *tunneledDockerHost) Close() error {
|
||||||
|
return t.oSCommand.Kill(t.cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SSHHandler) createDockerHostTunnel(ctx context.Context, remoteHost string) (*tunneledDockerHost, error) {
|
||||||
|
socketDir, err := self.tempDir("/tmp", "lazydocker-sshtunnel-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create ssh tunnel tmp file: %w", err)
|
||||||
|
}
|
||||||
|
localSocket := path.Join(socketDir, "dockerhost.sock")
|
||||||
|
|
||||||
|
cmd, err := self.tunnelSSH(ctx, remoteHost, localSocket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tunnel docker host over ssh: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set a reasonable timeout, then wait for the socket to dial successfully
|
||||||
|
// before attempting to create a new docker client
|
||||||
|
const socketTunnelTimeout = 8 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, socketTunnelTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = self.retrySocketDial(ctx, localSocket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssh tunneled socket never became available: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct the new DOCKER_HOST url with the proper scheme
|
||||||
|
newDockerHostURL := url.URL{Scheme: "unix", Path: localSocket}
|
||||||
|
return &tunneledDockerHost{
|
||||||
|
socketPath: newDockerHostURL.String(),
|
||||||
|
cmd: cmd,
|
||||||
|
oSCommand: self.oSCommand,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to dial the socket until it becomes available.
|
||||||
|
// The retry loop will continue until the parent context is canceled.
|
||||||
|
func (self *SSHHandler) retrySocketDial(ctx context.Context, socketPath string) error {
|
||||||
|
t := time.NewTicker(1 * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-t.C:
|
||||||
|
}
|
||||||
|
// attempt to dial the socket, exit on success
|
||||||
|
err := self.tryDial(ctx, socketPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to dial the specified unix socket, immediately close the connection if successfully created.
|
||||||
|
func (self *SSHHandler) tryDial(ctx context.Context, socketPath string) error {
|
||||||
|
conn, err := self.dialContext(ctx, "unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SSHHandler) tunnelSSH(ctx context.Context, host, localSocket string) (*exec.Cmd, error) {
|
||||||
|
cmd := exec.CommandContext(ctx, "ssh", "-L", localSocket+":/var/run/docker.sock", host, "-N")
|
||||||
|
self.oSCommand.PrepareForChildren(cmd)
|
||||||
|
err := self.startCmd(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
109
pkg/commands/ssh/ssh_test.go
Normal file
109
pkg/commands/ssh/ssh_test.go
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSSHHandlerHandleSSHDockerHost(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
envVarValue string
|
||||||
|
expectedDialContextCount int
|
||||||
|
expectedStartCmdCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
testName: "No env var set",
|
||||||
|
envVarValue: "",
|
||||||
|
expectedDialContextCount: 0,
|
||||||
|
expectedStartCmdCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Env var set with https scheme",
|
||||||
|
envVarValue: "https://myhost.com",
|
||||||
|
expectedStartCmdCount: 0,
|
||||||
|
expectedDialContextCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Env var set with ssh scheme",
|
||||||
|
envVarValue: "ssh://myhost@192.168.5.178",
|
||||||
|
expectedStartCmdCount: 1,
|
||||||
|
expectedDialContextCount: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
s := s
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
getenv := func(key string) string {
|
||||||
|
if key != "DOCKER_HOST" {
|
||||||
|
t.Errorf("Expected key to be DOCKER_HOST, got %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.envVarValue
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := func(dir string, pattern string) (string, error) {
|
||||||
|
assert.Equal(t, "/tmp", dir)
|
||||||
|
assert.Equal(t, "lazydocker-sshtunnel-", pattern)
|
||||||
|
|
||||||
|
return "/tmp/lazydocker-ssh-tunnel-12345", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setenv := func(key, value string) error {
|
||||||
|
assert.Equal(t, "DOCKER_HOST", key)
|
||||||
|
assert.Equal(t, "unix:///tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock", value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
startCmdCount := 0
|
||||||
|
startCmd := func(cmd *exec.Cmd) error {
|
||||||
|
assert.EqualValues(t, []string{"ssh", "-L", "/tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock:/var/run/docker.sock", "192.168.5.178", "-N"}, cmd.Args)
|
||||||
|
|
||||||
|
startCmdCount++
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dialContextCount := 0
|
||||||
|
dialContext := func(ctx context.Context, network string, address string) (io.Closer, error) {
|
||||||
|
assert.Equal(t, "unix", network)
|
||||||
|
assert.Equal(t, "/tmp/lazydocker-ssh-tunnel-12345/dockerhost.sock", address)
|
||||||
|
|
||||||
|
dialContextCount++
|
||||||
|
|
||||||
|
return noopCloser{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := &SSHHandler{
|
||||||
|
oSCommand: &fakeCmdKiller{},
|
||||||
|
|
||||||
|
dialContext: dialContext,
|
||||||
|
startCmd: startCmd,
|
||||||
|
tempDir: tempDir,
|
||||||
|
getenv: getenv,
|
||||||
|
setenv: setenv,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.HandleSSHDockerHost()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, s.expectedDialContextCount, dialContextCount)
|
||||||
|
assert.Equal(t, s.expectedStartCmdCount, startCmdCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeCmdKiller struct{}
|
||||||
|
|
||||||
|
func (self *fakeCmdKiller) Kill(cmd *exec.Cmd) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *fakeCmdKiller) PrepareForChildren(cmd *exec.Cmd) {}
|
||||||
|
|
@ -2,10 +2,9 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
@ -13,37 +12,28 @@ import (
|
||||||
// Volume : A docker Volume
|
// Volume : A docker Volume
|
||||||
type Volume struct {
|
type Volume struct {
|
||||||
Name string
|
Name string
|
||||||
Volume *types.Volume
|
Volume *volume.Volume
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
OSCommand *OSCommand
|
OSCommand *OSCommand
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
DockerCommand LimitedDockerCommand
|
DockerCommand LimitedDockerCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayStrings returns the dispaly string of Container
|
|
||||||
func (v *Volume) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
return []string{v.Volume.Driver, v.Name}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshVolumes gets the volumes and stores them
|
// RefreshVolumes gets the volumes and stores them
|
||||||
func (c *DockerCommand) RefreshVolumes() error {
|
func (c *DockerCommand) RefreshVolumes() ([]*Volume, error) {
|
||||||
result, err := c.Client.VolumeList(context.Background(), filters.Args{})
|
result, err := c.Client.VolumeList(context.Background(), volume.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
volumes := result.Volumes
|
volumes := result.Volumes
|
||||||
|
|
||||||
ownVolumes := make([]*Volume, len(volumes))
|
ownVolumes := make([]*Volume, len(volumes))
|
||||||
|
|
||||||
sort.Slice(volumes, func(i, j int) bool {
|
for i, vol := range volumes {
|
||||||
return volumes[i].Name < volumes[j].Name
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, volume := range volumes {
|
|
||||||
ownVolumes[i] = &Volume{
|
ownVolumes[i] = &Volume{
|
||||||
Name: volume.Name,
|
Name: vol.Name,
|
||||||
Volume: volume,
|
Volume: vol,
|
||||||
Client: c.Client,
|
Client: c.Client,
|
||||||
OSCommand: c.OSCommand,
|
OSCommand: c.OSCommand,
|
||||||
Log: c.Log,
|
Log: c.Log,
|
||||||
|
|
@ -51,9 +41,7 @@ func (c *DockerCommand) RefreshVolumes() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Volumes = ownVolumes
|
return ownVolumes, nil
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PruneVolumes prunes volumes
|
// PruneVolumes prunes volumes
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,13 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/OpenPeeDeeP/xdg"
|
"github.com/OpenPeeDeeP/xdg"
|
||||||
yaml "github.com/jesseduffield/yaml"
|
"github.com/jesseduffield/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserConfig holds all of the user-configurable options
|
// UserConfig holds all of the user-configurable options
|
||||||
|
|
@ -29,16 +28,13 @@ type UserConfig struct {
|
||||||
// hide things
|
// hide things
|
||||||
Gui GuiConfig `yaml:"gui,omitempty"`
|
Gui GuiConfig `yaml:"gui,omitempty"`
|
||||||
|
|
||||||
// Reporting determines whether events are reported such as errors (and maybe
|
|
||||||
// application opens but I'm not decided on that yet because it sounds kinda
|
|
||||||
// creepy but I also would love to know how many people are using this
|
|
||||||
// program)
|
|
||||||
Reporting string `yaml:"reporting,omitempty"`
|
|
||||||
|
|
||||||
// ConfirmOnQuit when enabled prompts you to confirm you want to quit when you
|
// ConfirmOnQuit when enabled prompts you to confirm you want to quit when you
|
||||||
// hit esc or q when no confirmation panels are open
|
// hit esc or q when no confirmation panels are open
|
||||||
ConfirmOnQuit bool `yaml:"confirmOnQuit,omitempty"`
|
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
|
// CommandTemplates determines what commands actually get called when we run
|
||||||
// certain commands
|
// certain commands
|
||||||
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
|
CommandTemplates CommandTemplatesConfig `yaml:"commandTemplates,omitempty"`
|
||||||
|
|
@ -57,18 +53,24 @@ type UserConfig struct {
|
||||||
// OS determines what defaults are set for opening files and links
|
// OS determines what defaults are set for opening files and links
|
||||||
OS OSConfig `yaml:"oS,omitempty"`
|
OS OSConfig `yaml:"oS,omitempty"`
|
||||||
|
|
||||||
// UpdateConfig determines what the default settings are for updating the ui
|
|
||||||
Update UpdateConfig `yaml:"update,omitempty"`
|
|
||||||
|
|
||||||
// Stats determines how long lazydocker will gather container stats for, and
|
// Stats determines how long lazydocker will gather container stats for, and
|
||||||
// what stat info to graph
|
// what stat info to graph
|
||||||
Stats StatsConfig `yaml:"stats,omitempty"`
|
Stats StatsConfig `yaml:"stats,omitempty"`
|
||||||
|
|
||||||
|
// Replacements determines how we render an item's info
|
||||||
|
Replacements Replacements `yaml:"replacements,omitempty"`
|
||||||
|
|
||||||
|
// For demo purposes: any list item with one of these strings as a substring
|
||||||
|
// will be filtered out and not displayed.
|
||||||
|
// Not documented because it's subject to change
|
||||||
|
Ignore []string `yaml:"ignore,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThemeConfig is for setting the colors of panels and some text.
|
// ThemeConfig is for setting the colors of panels and some text.
|
||||||
type ThemeConfig struct {
|
type ThemeConfig struct {
|
||||||
ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"`
|
ActiveBorderColor []string `yaml:"activeBorderColor,omitempty"`
|
||||||
InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"`
|
InactiveBorderColor []string `yaml:"inactiveBorderColor,omitempty"`
|
||||||
|
SelectedLineBgColor []string `yaml:"selectedLineBgColor,omitempty"`
|
||||||
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
|
OptionsTextColor []string `yaml:"optionsTextColor,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,6 +81,9 @@ type GuiConfig struct {
|
||||||
// scrolling the main panel
|
// scrolling the main panel
|
||||||
ScrollHeight int `yaml:"scrollHeight,omitempty"`
|
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
|
// ScrollPastBottom determines whether you can scroll past the bottom of the
|
||||||
// main view
|
// main view
|
||||||
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
|
ScrollPastBottom bool `yaml:"scrollPastBottom,omitempty"`
|
||||||
|
|
@ -109,6 +114,34 @@ type GuiConfig struct {
|
||||||
|
|
||||||
// WrapMainPanel determines whether we use word wrap on the main panel
|
// WrapMainPanel determines whether we use word wrap on the main panel
|
||||||
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
|
WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"`
|
||||||
|
|
||||||
|
// LegacySortContainers determines if containers should be sorted using legacy approach.
|
||||||
|
// By default, containers are now sorted by status. This setting allows users to
|
||||||
|
// use legacy behaviour instead.
|
||||||
|
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"`
|
||||||
|
|
||||||
|
// If 0.333, then the side panels will be 1/3 of the screen's width
|
||||||
|
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||||
|
|
||||||
|
// Determines whether we show the bottom line (the one containing keybinding
|
||||||
|
// info and the status of the app).
|
||||||
|
ShowBottomLine bool `yaml:"showBottomLine"`
|
||||||
|
|
||||||
|
// When true, increases vertical space used by focused side panel,
|
||||||
|
// creating an accordion effect
|
||||||
|
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||||
|
|
||||||
|
// ScreenMode allow user to specify which screen mode will be used on startup
|
||||||
|
ScreenMode string `yaml:"screenMode,omitempty"`
|
||||||
|
|
||||||
|
// Determines the style of the container status and container health display in the
|
||||||
|
// containers panel. "long": full words (default), "short": one or two characters,
|
||||||
|
// "icon": unicode emoji.
|
||||||
|
ContainerStatusHealthStyle string `yaml:"containerStatusHealthStyle"`
|
||||||
|
|
||||||
|
// Window border style.
|
||||||
|
// One of 'rounded' (default) | 'single' | 'double' | 'hidden'
|
||||||
|
Border string `yaml:"border"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandTemplatesConfig determines what commands actually get called when we
|
// CommandTemplatesConfig determines what commands actually get called when we
|
||||||
|
|
@ -119,9 +152,23 @@ type CommandTemplatesConfig struct {
|
||||||
// .Service.Name }}
|
// .Service.Name }}
|
||||||
RestartService string `yaml:"restartService,omitempty"`
|
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
|
// DockerCompose is for your docker-compose command. You may want to combine a
|
||||||
// few different docker-compose.yml files together, in which case you can set
|
// few different docker-compose.yml files together, in which case you can set
|
||||||
// this to "docker-compose -f foo/docker-compose.yml -f
|
// this to "docker compose -f foo/docker-compose.yml -f
|
||||||
// bah/docker-compose.yml". The reason that the other docker-compose command
|
// bah/docker-compose.yml". The reason that the other docker-compose command
|
||||||
// templates all start with {{ .DockerCompose }} is so that they can make use
|
// templates all start with {{ .DockerCompose }} is so that they can make use
|
||||||
// of whatever you've set in this value rather than you having to copy and
|
// of whatever you've set in this value rather than you having to copy and
|
||||||
|
|
@ -153,21 +200,11 @@ type CommandTemplatesConfig struct {
|
||||||
// and ensure they're running before trying to run the service at hand
|
// and ensure they're running before trying to run the service at hand
|
||||||
RecreateService string `yaml:"recreateService,omitempty"`
|
RecreateService string `yaml:"recreateService,omitempty"`
|
||||||
|
|
||||||
// ViewContainerLogs is like ViewServiceLogs but for containers
|
// AllLogs is for showing what you get from doing `docker compose logs`. It
|
||||||
ViewContainerLogs string `yaml:"viewContainerLogs,omitempty"`
|
|
||||||
|
|
||||||
// ContainerLogs shows the logs of a container. By default this restricts the
|
|
||||||
// output to (as of right now) the last hour. This is for the sake of
|
|
||||||
// performance, and you can feel free to change this
|
|
||||||
ContainerLogs string `yaml:"containerLogs,omitempty"`
|
|
||||||
|
|
||||||
// AllLogs is for showing what you get from doing `docker-compose logs`. It
|
|
||||||
// combines all the logs together
|
// combines all the logs together
|
||||||
AllLogs string `yaml:"allLogs,omitempty"`
|
AllLogs string `yaml:"allLogs,omitempty"`
|
||||||
|
|
||||||
// ViewAllLogs is to AllLogs what ViewContainerLogs is to ContainerLogs. It's
|
// ViewAllLogs is the command we use when you want to see all logs in a subprocess with no filtering
|
||||||
// just the command we use when you want to see all logs in a subprocess with
|
|
||||||
// no filtering
|
|
||||||
ViewAllLogs string `yaml:"viewAlLogs,omitempty"`
|
ViewAllLogs string `yaml:"viewAlLogs,omitempty"`
|
||||||
|
|
||||||
// DockerComposeConfig is the command for viewing the config of your docker
|
// DockerComposeConfig is the command for viewing the config of your docker
|
||||||
|
|
@ -194,14 +231,6 @@ type OSConfig struct {
|
||||||
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
|
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateConfig determines what the default settings are for updating the ui
|
|
||||||
type UpdateConfig struct {
|
|
||||||
// RefreshProjectTime determines the time betweens updates of all continues docker commands like docker ps, docker images, etc.
|
|
||||||
// It expects a valid duration like: 100ms, 2s, 200ns
|
|
||||||
// for docs see: https://golang.org/pkg/time/#ParseDuration
|
|
||||||
DockerRefreshInterval time.Duration `yaml:"dockerRefreshInterval,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GraphConfig specifies how to make a graph of recorded container stats
|
// GraphConfig specifies how to make a graph of recorded container stats
|
||||||
type GraphConfig struct {
|
type GraphConfig struct {
|
||||||
// Min sets the minimum value that you want to display. If you want to set
|
// Min sets the minimum value that you want to display. If you want to set
|
||||||
|
|
@ -269,6 +298,15 @@ type CustomCommands struct {
|
||||||
|
|
||||||
// Volumes contains the custom commands for volumes
|
// Volumes contains the custom commands for volumes
|
||||||
Volumes []CustomCommand `yaml:"volumes,omitempty"`
|
Volumes []CustomCommand `yaml:"volumes,omitempty"`
|
||||||
|
|
||||||
|
// Networks contains the custom commands for networks
|
||||||
|
Networks []CustomCommand `yaml:"networks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replacements contains the stuff relating to rendering a container's info
|
||||||
|
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
|
// CustomCommand is a template for a command we want to run against a service or
|
||||||
|
|
@ -283,6 +321,10 @@ type CustomCommand struct {
|
||||||
// option where the output plays in the main panel.
|
// option where the output plays in the main panel.
|
||||||
Attach bool `yaml:"attach"`
|
Attach bool `yaml:"attach"`
|
||||||
|
|
||||||
|
// Shell indicates whether to invoke the Command on a shell or not.
|
||||||
|
// Example of a bash invoked command: `/bin/bash -c "{Command}".
|
||||||
|
Shell bool `yaml:"shell"`
|
||||||
|
|
||||||
// Command is the command we want to run. We can use the go templates here as
|
// Command is the command we want to run. We can use the go templates here as
|
||||||
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
|
// well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }}
|
||||||
// /bin/sh`
|
// /bin/sh`
|
||||||
|
|
@ -298,6 +340,12 @@ type CustomCommand struct {
|
||||||
InternalFunction func() error `yaml:"-"`
|
InternalFunction func() error `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogsConfig struct {
|
||||||
|
Timestamps bool `yaml:"timestamps,omitempty"`
|
||||||
|
Since string `yaml:"since,omitempty"`
|
||||||
|
Tail string `yaml:"tail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetDefaultConfig returns the application default configuration NOTE (to
|
// GetDefaultConfig returns the application default configuration NOTE (to
|
||||||
// contributors, not users): do not default a boolean to true, because false is
|
// 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
|
// the boolean zero value and this will be ignored when parsing the user's
|
||||||
|
|
@ -311,22 +359,39 @@ func GetDefaultConfig() UserConfig {
|
||||||
return UserConfig{
|
return UserConfig{
|
||||||
Gui: GuiConfig{
|
Gui: GuiConfig{
|
||||||
ScrollHeight: 2,
|
ScrollHeight: 2,
|
||||||
|
Language: "auto",
|
||||||
ScrollPastBottom: false,
|
ScrollPastBottom: false,
|
||||||
IgnoreMouseEvents: false,
|
IgnoreMouseEvents: false,
|
||||||
Theme: ThemeConfig{
|
Theme: ThemeConfig{
|
||||||
ActiveBorderColor: []string{"green", "bold"},
|
ActiveBorderColor: []string{"green", "bold"},
|
||||||
InactiveBorderColor: []string{"default"},
|
InactiveBorderColor: []string{"default"},
|
||||||
|
SelectedLineBgColor: []string{"blue"},
|
||||||
OptionsTextColor: []string{"blue"},
|
OptionsTextColor: []string{"blue"},
|
||||||
},
|
},
|
||||||
ShowAllContainers: false,
|
ShowAllContainers: false,
|
||||||
ReturnImmediately: false,
|
ReturnImmediately: false,
|
||||||
WrapMainPanel: false,
|
WrapMainPanel: true,
|
||||||
|
LegacySortContainers: false,
|
||||||
|
SidePanelWidth: 0.3333,
|
||||||
|
ShowBottomLine: true,
|
||||||
|
ExpandFocusedSidePanel: false,
|
||||||
|
ScreenMode: "normal",
|
||||||
|
ContainerStatusHealthStyle: "long",
|
||||||
},
|
},
|
||||||
Reporting: "undetermined",
|
|
||||||
ConfirmOnQuit: false,
|
ConfirmOnQuit: false,
|
||||||
|
Logs: LogsConfig{
|
||||||
|
Timestamps: false,
|
||||||
|
Since: "60m",
|
||||||
|
Tail: "",
|
||||||
|
},
|
||||||
CommandTemplates: CommandTemplatesConfig{
|
CommandTemplates: CommandTemplatesConfig{
|
||||||
DockerCompose: "docker-compose",
|
DockerCompose: "docker compose",
|
||||||
RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}",
|
RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}",
|
||||||
|
StartService: "{{ .DockerCompose }} start {{ .Service.Name }}",
|
||||||
|
Up: "{{ .DockerCompose }} up -d",
|
||||||
|
Down: "{{ .DockerCompose }} down",
|
||||||
|
DownWithVolumes: "{{ .DockerCompose }} down --volumes",
|
||||||
|
UpService: "{{ .DockerCompose }} up -d {{ .Service.Name }}",
|
||||||
RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
|
RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}",
|
||||||
RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
|
RecreateService: "{{ .DockerCompose }} up -d --force-recreate {{ .Service.Name }}",
|
||||||
StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}",
|
StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}",
|
||||||
|
|
@ -336,21 +401,13 @@ func GetDefaultConfig() UserConfig {
|
||||||
ViewAllLogs: "{{ .DockerCompose }} logs",
|
ViewAllLogs: "{{ .DockerCompose }} logs",
|
||||||
DockerComposeConfig: "{{ .DockerCompose }} config",
|
DockerComposeConfig: "{{ .DockerCompose }} config",
|
||||||
CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet",
|
CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet",
|
||||||
ContainerLogs: "docker logs --timestamps --follow --since=60m {{ .Container.ID }}",
|
|
||||||
ViewContainerLogs: "docker logs --timestamps --follow --since=60m {{ .Container.ID }}",
|
|
||||||
ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}",
|
ServiceTop: "{{ .DockerCompose }} top {{ .Service.Name }}",
|
||||||
},
|
},
|
||||||
CustomCommands: CustomCommands{
|
CustomCommands: CustomCommands{
|
||||||
Containers: []CustomCommand{
|
Containers: []CustomCommand{},
|
||||||
{
|
Services: []CustomCommand{},
|
||||||
Name: "bash",
|
Images: []CustomCommand{},
|
||||||
Command: "docker exec -it {{ .Container.ID }} /bin/sh",
|
Volumes: []CustomCommand{},
|
||||||
Attach: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Services: []CustomCommand{},
|
|
||||||
Images: []CustomCommand{},
|
|
||||||
Volumes: []CustomCommand{},
|
|
||||||
},
|
},
|
||||||
BulkCommands: CustomCommands{
|
BulkCommands: CustomCommands{
|
||||||
Services: []CustomCommand{
|
Services: []CustomCommand{
|
||||||
|
|
@ -399,9 +456,6 @@ func GetDefaultConfig() UserConfig {
|
||||||
Volumes: []CustomCommand{},
|
Volumes: []CustomCommand{},
|
||||||
},
|
},
|
||||||
OS: GetPlatformDefaultConfig(),
|
OS: GetPlatformDefaultConfig(),
|
||||||
Update: UpdateConfig{
|
|
||||||
DockerRefreshInterval: time.Millisecond * 100,
|
|
||||||
},
|
|
||||||
Stats: StatsConfig{
|
Stats: StatsConfig{
|
||||||
MaxDuration: duration,
|
MaxDuration: duration,
|
||||||
Graphs: []GraphConfig{
|
Graphs: []GraphConfig{
|
||||||
|
|
@ -417,6 +471,9 @@ func GetDefaultConfig() UserConfig {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Replacements: Replacements{
|
||||||
|
ImageNamePrefixes: map[string]string{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,10 +488,11 @@ type AppConfig struct {
|
||||||
UserConfig *UserConfig
|
UserConfig *UserConfig
|
||||||
ConfigDir string
|
ConfigDir string
|
||||||
ProjectDir string
|
ProjectDir string
|
||||||
|
ProjectName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAppConfig makes a new app config
|
// NewAppConfig makes a new app config
|
||||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string) (*AppConfig, error) {
|
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool, composeFiles []string, projectDir string, projectName string) (*AppConfig, error) {
|
||||||
configDir, err := findOrCreateConfigDir(name)
|
configDir, err := findOrCreateConfigDir(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -445,7 +503,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass compose files as individual -f flags to docker-compose
|
// Pass compose files as individual -f flags to docker compose
|
||||||
if len(composeFiles) > 0 {
|
if len(composeFiles) > 0 {
|
||||||
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
|
userConfig.CommandTemplates.DockerCompose += " -f " + strings.Join(composeFiles, " -f ")
|
||||||
}
|
}
|
||||||
|
|
@ -460,16 +518,34 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||||
UserConfig: userConfig,
|
UserConfig: userConfig,
|
||||||
ConfigDir: configDir,
|
ConfigDir: configDir,
|
||||||
ProjectDir: projectDir,
|
ProjectDir: projectDir,
|
||||||
|
ProjectName: projectName,
|
||||||
}
|
}
|
||||||
|
|
||||||
return appConfig, nil
|
return appConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findOrCreateConfigDir(projectName string) (string, error) {
|
func configDirForVendor(vendor string, projectName string) string {
|
||||||
configDirs := xdg.New("jesseduffield", projectName)
|
envConfigDir := os.Getenv("CONFIG_DIR")
|
||||||
folder := configDirs.ConfigHome()
|
if envConfigDir != "" {
|
||||||
|
return envConfigDir
|
||||||
|
}
|
||||||
|
configDirs := xdg.New(vendor, projectName)
|
||||||
|
return configDirs.ConfigHome()
|
||||||
|
}
|
||||||
|
|
||||||
err := os.MkdirAll(folder, 0755)
|
func configDir(projectName string) string {
|
||||||
|
legacyConfigDirectory := configDirForVendor("jesseduffield", projectName)
|
||||||
|
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
||||||
|
return legacyConfigDirectory
|
||||||
|
}
|
||||||
|
configDirectory := configDirForVendor("", projectName)
|
||||||
|
return configDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOrCreateConfigDir(projectName string) (string, error) {
|
||||||
|
folder := configDir(projectName)
|
||||||
|
|
||||||
|
err := os.MkdirAll(folder, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -498,7 +574,7 @@ func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := ioutil.ReadFile(fileName)
|
content, err := os.ReadFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -524,7 +600,7 @@ func (c *AppConfig) WriteToUserConfig(updateConfig func(*UserConfig) error) erro
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.OpenFile(c.ConfigFilename(), os.O_WRONLY|os.O_CREATE, 0666)
|
file, err := os.OpenFile(c.ConfigFilename(), os.O_WRONLY|os.O_CREATE, 0o666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@ import (
|
||||||
|
|
||||||
func TestDockerComposeCommandNoFiles(t *testing.T) {
|
func TestDockerComposeCommandNoFiles(t *testing.T) {
|
||||||
composeFiles := []string{}
|
composeFiles := []string{}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
||||||
expected := "docker-compose"
|
expected := "docker compose"
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Fatalf("Expected %s but got %s", expected, actual)
|
t.Fatalf("Expected %s but got %s", expected, actual)
|
||||||
}
|
}
|
||||||
|
|
@ -24,14 +23,13 @@ func TestDockerComposeCommandNoFiles(t *testing.T) {
|
||||||
|
|
||||||
func TestDockerComposeCommandSingleFile(t *testing.T) {
|
func TestDockerComposeCommandSingleFile(t *testing.T) {
|
||||||
composeFiles := []string{"one.yml"}
|
composeFiles := []string{"one.yml"}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
||||||
expected := "docker-compose -f one.yml"
|
expected := "docker compose -f one.yml"
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Fatalf("Expected %s but got %s", expected, actual)
|
t.Fatalf("Expected %s but got %s", expected, actual)
|
||||||
}
|
}
|
||||||
|
|
@ -39,30 +37,30 @@ func TestDockerComposeCommandSingleFile(t *testing.T) {
|
||||||
|
|
||||||
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
func TestDockerComposeCommandMultipleFiles(t *testing.T) {
|
||||||
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
|
composeFiles := []string{"one.yml", "two.yml", "three.yml"}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, composeFiles, "projectDir", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
actual := conf.UserConfig.CommandTemplates.DockerCompose
|
||||||
expected := "docker-compose -f one.yml -f two.yml -f three.yml"
|
expected := "docker compose -f one.yml -f two.yml -f three.yml"
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
t.Fatalf("Expected %s but got %s", expected, actual)
|
t.Fatalf("Expected %s but got %s", expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWritingToConfigFile(t *testing.T) {
|
func TestWritingToConfigFile(t *testing.T) {
|
||||||
//init the AppConfig
|
// init the AppConfig
|
||||||
emptyComposeFiles := []string{}
|
emptyComposeFiles := []string{}
|
||||||
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir")
|
conf, err := NewAppConfig("name", "version", "commit", "date", "buildSource", false, emptyComposeFiles, "projectDir", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testFn := func(ac *AppConfig, newReportingValue string, t *testing.T) {
|
testFn := func(t *testing.T, ac *AppConfig, newValue bool) {
|
||||||
|
t.Helper()
|
||||||
updateFn := func(uc *UserConfig) error {
|
updateFn := func(uc *UserConfig) error {
|
||||||
uc.Reporting = newReportingValue
|
uc.ConfirmOnQuit = newValue
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +69,7 @@ func TestWritingToConfigFile(t *testing.T) {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.OpenFile(ac.ConfigFilename(), os.O_RDONLY, 0660)
|
file, err := os.OpenFile(ac.ConfigFilename(), os.O_RDONLY, 0o660)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -87,14 +85,14 @@ func TestWritingToConfigFile(t *testing.T) {
|
||||||
t.Fatalf("Unexpected error: %s", err)
|
t.Fatalf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleUC.Reporting != newReportingValue {
|
if sampleUC.ConfirmOnQuit != newValue {
|
||||||
t.Fatalf("Got %s, Expected %s\n", sampleUC.Reporting, newReportingValue)
|
t.Fatalf("Got %v, Expected %v\n", sampleUC.ConfirmOnQuit, newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert value into an empty file
|
// insert value into an empty file
|
||||||
testFn(conf, "on", t)
|
testFn(t, conf, true)
|
||||||
|
|
||||||
// modifying an existing file that already has 'Reporting'
|
// modifying an existing file that already has 'ConfirmOnQuit'
|
||||||
testFn(conf, "off", t)
|
testFn(t, conf, false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows && !linux
|
||||||
// +build !windows,!linux
|
// +build !windows,!linux
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -49,19 +51,29 @@ func (m *statusManager) getStatusString() string {
|
||||||
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
||||||
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
|
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
|
||||||
go func() {
|
go func() {
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
gui.statusManager.addWaitingStatus(name)
|
||||||
gui.statusManager.addWaitingStatus(name)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
defer gui.g.Update(func(g *gocui.Gui) error {
|
defer func() {
|
||||||
gui.statusManager.removeStatus(name)
|
gui.statusManager.removeStatus(name)
|
||||||
return nil
|
}()
|
||||||
})
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 50)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
appStatus := gui.statusManager.getStatusString()
|
||||||
|
if appStatus == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
|
||||||
|
gui.Log.Warn(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := f(); err != nil {
|
if err := f(); err != nil {
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
227
pkg/gui/arrangement.go
Normal file
227
pkg/gui/arrangement.go
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazycore/pkg/boxlayout"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// In this file we use the boxlayout package, along with knowledge about the app's state,
|
||||||
|
// to arrange the windows (i.e. panels) on the screen.
|
||||||
|
|
||||||
|
const INFO_SECTION_PADDING = " "
|
||||||
|
|
||||||
|
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||||
|
minimumHeight := 9
|
||||||
|
minimumWidth := 10
|
||||||
|
width, height := gui.g.Size()
|
||||||
|
if width < minimumWidth || height < minimumHeight {
|
||||||
|
return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
|
||||||
|
|
||||||
|
sidePanelsDirection := boxlayout.COLUMN
|
||||||
|
portraitMode := width <= 84 && height > 45
|
||||||
|
if portraitMode {
|
||||||
|
sidePanelsDirection = boxlayout.ROW
|
||||||
|
}
|
||||||
|
|
||||||
|
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine || gui.State.Filter.active
|
||||||
|
infoSectionSize := 0
|
||||||
|
if showInfoSection {
|
||||||
|
infoSectionSize = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
root := &boxlayout.Box{
|
||||||
|
Direction: boxlayout.ROW,
|
||||||
|
Children: []*boxlayout.Box{
|
||||||
|
{
|
||||||
|
Direction: sidePanelsDirection,
|
||||||
|
Weight: 1,
|
||||||
|
Children: []*boxlayout.Box{
|
||||||
|
{
|
||||||
|
Direction: boxlayout.ROW,
|
||||||
|
Weight: sideSectionWeight,
|
||||||
|
ConditionalChildren: gui.sidePanelChildren,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Window: "main",
|
||||||
|
Weight: mainSectionWeight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Direction: boxlayout.COLUMN,
|
||||||
|
Size: infoSectionSize,
|
||||||
|
Children: gui.infoSectionChildren(informationStr, appStatus),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||||
|
currentWindow := gui.currentStaticWindowName()
|
||||||
|
|
||||||
|
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||||
|
sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth
|
||||||
|
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||||
|
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||||
|
sideSectionWeight := 1
|
||||||
|
|
||||||
|
if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL {
|
||||||
|
mainSectionWeight = 1
|
||||||
|
sideSectionWeight = 0
|
||||||
|
} else {
|
||||||
|
if gui.State.ScreenMode == SCREEN_HALF {
|
||||||
|
mainSectionWeight = 1
|
||||||
|
} else if gui.State.ScreenMode == SCREEN_FULL {
|
||||||
|
mainSectionWeight = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sideSectionWeight, mainSectionWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
|
||||||
|
result := []*boxlayout.Box{}
|
||||||
|
|
||||||
|
if len(appStatus) > 0 {
|
||||||
|
result = append(result,
|
||||||
|
&boxlayout.Box{
|
||||||
|
Window: "appStatus",
|
||||||
|
Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gui.State.Filter.active {
|
||||||
|
return append(result, []*boxlayout.Box{
|
||||||
|
{
|
||||||
|
Window: "filterPrefix",
|
||||||
|
Size: runewidth.StringWidth(gui.filterPrompt()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Window: "filter",
|
||||||
|
Weight: 1,
|
||||||
|
},
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result,
|
||||||
|
[]*boxlayout.Box{
|
||||||
|
{
|
||||||
|
Window: "options",
|
||||||
|
Weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Window: "information",
|
||||||
|
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||||
|
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
|
||||||
|
},
|
||||||
|
}...,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) sideViewNames() []string {
|
||||||
|
visibleSidePanels := lo.Filter(gui.allSidePanels(), func(panel panels.ISideListPanel, _ int) bool {
|
||||||
|
return !panel.IsHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
return lo.Map(visibleSidePanels, func(panel panels.ISideListPanel, _ int) string {
|
||||||
|
return panel.GetView().Name()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||||
|
currentWindow := gui.currentSideWindowName()
|
||||||
|
sideWindowNames := gui.sideViewNames()
|
||||||
|
|
||||||
|
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
|
||||||
|
fullHeightBox := func(window string) *boxlayout.Box {
|
||||||
|
if window == currentWindow {
|
||||||
|
return &boxlayout.Box{
|
||||||
|
Window: window,
|
||||||
|
Weight: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &boxlayout.Box{
|
||||||
|
Window: window,
|
||||||
|
Size: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||||
|
return fullHeightBox(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
} else if height >= 28 {
|
||||||
|
accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel
|
||||||
|
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||||
|
if accordionMode && defaultBox.Window == currentWindow {
|
||||||
|
return &boxlayout.Box{
|
||||||
|
Window: defaultBox.Window,
|
||||||
|
Weight: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultBox
|
||||||
|
}
|
||||||
|
|
||||||
|
// The project panel is compact (Size: 3) when not focused, but expands
|
||||||
|
// when focused to show the list of projects. This only applies when the
|
||||||
|
// project panel is actually visible (i.e. we are inside a compose project).
|
||||||
|
if len(sideWindowNames) > 0 && sideWindowNames[0] == "project" {
|
||||||
|
projectBox := &boxlayout.Box{
|
||||||
|
Window: sideWindowNames[0],
|
||||||
|
Size: 3,
|
||||||
|
}
|
||||||
|
if currentWindow == sideWindowNames[0] {
|
||||||
|
projectBox = &boxlayout.Box{
|
||||||
|
Window: sideWindowNames[0],
|
||||||
|
Weight: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append([]*boxlayout.Box{
|
||||||
|
projectBox,
|
||||||
|
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
||||||
|
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||||
|
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
squashedHeight := 1
|
||||||
|
if height >= 21 {
|
||||||
|
squashedHeight = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
squashedSidePanelBox := func(window string) *boxlayout.Box {
|
||||||
|
if window == currentWindow {
|
||||||
|
return &boxlayout.Box{
|
||||||
|
Window: window,
|
||||||
|
Weight: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &boxlayout.Box{
|
||||||
|
Window: window,
|
||||||
|
Size: squashedHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||||
|
return squashedSidePanelBox(window)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,6 @@ package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
|
@ -16,25 +15,27 @@ import (
|
||||||
|
|
||||||
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
||||||
return func(g *gocui.Gui, v *gocui.View) error {
|
return func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.closeConfirmationPrompt(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if function != nil {
|
if function != nil {
|
||||||
if err := function(g, v); err != nil {
|
if err := function(g, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return gui.closeConfirmationPrompt(g)
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
|
func (gui *Gui) closeConfirmationPrompt() error {
|
||||||
view, err := g.View("confirmation")
|
if err := gui.returnFocus(); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil // if it's already been closed we can just return
|
|
||||||
}
|
}
|
||||||
if err := gui.returnFocus(g, view); err != nil {
|
gui.g.DeleteViewKeybindings("confirmation")
|
||||||
panic(err)
|
gui.Views.Confirmation.Visible = false
|
||||||
}
|
return nil
|
||||||
g.DeleteKeybindings("confirmation")
|
|
||||||
return g.DeleteView("confirmation")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||||
|
|
@ -51,8 +52,8 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||||
return lineCount
|
return lineCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
|
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
|
||||||
width, height := g.Size()
|
width, height := gui.g.Size()
|
||||||
panelWidth := width / 2
|
panelWidth := width / 2
|
||||||
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
|
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
|
||||||
return width/2 - panelWidth/2,
|
return width/2 - panelWidth/2,
|
||||||
|
|
@ -61,62 +62,56 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
|
||||||
height/2 + panelHeight/2
|
height/2 + panelHeight/2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||||
gui.onNewPopupPanel()
|
gui.onNewPopupPanel()
|
||||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false)
|
err := gui.prepareConfirmationPanel(title, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
confirmationView.Editable = true
|
gui.Views.Confirmation.Editable = true
|
||||||
return gui.setKeyBindings(g, handleConfirm, nil)
|
return gui.setKeyBindings(gui.g, handleConfirm, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
|
func (gui *Gui) prepareConfirmationPanel(title, prompt string) error {
|
||||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
||||||
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
confirmationView := gui.Views.Confirmation
|
||||||
|
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() != "unknown view" {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
confirmationView.HasLoader = hasLoader
|
|
||||||
confirmationView.Title = title
|
|
||||||
confirmationView.Wrap = true
|
|
||||||
confirmationView.FgColor = gocui.ColorWhite
|
|
||||||
}
|
}
|
||||||
|
confirmationView.Title = title
|
||||||
|
confirmationView.Visible = true
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
return gui.switchFocus(gui.g, currentView, confirmationView, false)
|
return gui.switchFocus(confirmationView)
|
||||||
})
|
})
|
||||||
return confirmationView, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) onNewPopupPanel() {
|
func (gui *Gui) onNewPopupPanel() {
|
||||||
viewNames := []string{"commitMessage",
|
gui.Views.Menu.Visible = false
|
||||||
"credentials",
|
gui.Views.Confirmation.Visible = false
|
||||||
"menu"}
|
|
||||||
for _, viewName := range viewNames {
|
|
||||||
_, _ = gui.g.SetViewOnBottom(viewName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
|
// It is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password.
|
||||||
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
// The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil.
|
||||||
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose)
|
// nolint:unparam
|
||||||
|
func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||||
|
return gui.createPopupPanel(title, prompt, handleConfirm, handleClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) createPopupPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||||
gui.onNewPopupPanel()
|
gui.onNewPopupPanel()
|
||||||
g.Update(func(g *gocui.Gui) error {
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
// delete the existing confirmation panel if it exists
|
if gui.currentViewName() == "confirmation" {
|
||||||
if view, _ := g.View("confirmation"); view != nil {
|
if err := gui.closeConfirmationPrompt(); err != nil {
|
||||||
if err := gui.closeConfirmationPrompt(g); err != nil {
|
|
||||||
gui.Log.Error(err.Error())
|
gui.Log.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
|
err := gui.prepareConfirmationPanel(title, prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
confirmationView.Editable = false
|
gui.Views.Confirmation.Editable = false
|
||||||
if err := gui.renderString(g, "confirmation", prompt); err != nil {
|
if err := gui.renderString(g, "confirmation", prompt); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -144,29 +139,10 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createSpecificErrorPanel allows you to create an error popup, specifying the
|
func (gui *Gui) createErrorPanel(message string) error {
|
||||||
// view to be focused when the user closes the popup, and a boolean specifying
|
|
||||||
// whether we will log the error. If the message may include a user password,
|
|
||||||
// this function is to be used over the more generic createErrorPanel, with
|
|
||||||
// willLog set to false
|
|
||||||
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
|
|
||||||
if willLog {
|
|
||||||
go func() {
|
|
||||||
// when reporting is switched on this log call sometimes introduces
|
|
||||||
// a delay on the error panel popping up. Here I'm adding a second wait
|
|
||||||
// so that the error is logged while the user is reading the error message
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
gui.Log.Error(message)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||||
return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.ErrorTitle, coloredMessage, nil, nil)
|
return gui.createConfirmationPanel(gui.Tr.ErrorTitle, coloredMessage, nil, nil)
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
|
||||||
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderConfirmationOptions() error {
|
func (gui *Gui) renderConfirmationOptions() error {
|
||||||
|
|
|
||||||
152
pkg/gui/container_logs.go
Normal file
152
pkg/gui/container_logs.go
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
|
||||||
|
return gui.NewTickerTask(TickerTaskOpts{
|
||||||
|
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||||
|
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped)
|
||||||
|
},
|
||||||
|
Duration: time.Millisecond * 200,
|
||||||
|
// TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed)
|
||||||
|
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||||
|
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||||
|
Autoscroll: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx context.Context, notifyStopped chan struct{}) {
|
||||||
|
gui.clearMainView()
|
||||||
|
defer func() {
|
||||||
|
notifyStopped <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
mainView := gui.Views.Main
|
||||||
|
|
||||||
|
if err := gui.writeContainerLogs(container, ctx, mainView); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we are here because the task has been stopped, we should return
|
||||||
|
// if we are here then the container must have exited, meaning we should wait until it's back again before
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 100)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
result, err := container.Inspect()
|
||||||
|
if err != nil {
|
||||||
|
// if we get an error, then the container has probably been removed so we'll get out of here
|
||||||
|
gui.Log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result.State.Running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderLogsToStdout(container *commands.Container) {
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
defer signal.Stop(stop)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
signal.Notify(stop, os.Interrupt)
|
||||||
|
<-stop
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := gui.g.Suspend(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := gui.g.Resume(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := gui.writeContainerLogs(container, ctx, os.Stdout); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.promptToReturn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) promptToReturn() {
|
||||||
|
if !gui.Config.UserConfig.Gui.ReturnImmediately {
|
||||||
|
fmt.Fprintf(os.Stdout, "\n\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
|
||||||
|
|
||||||
|
// wait for enter press
|
||||||
|
if _, err := fmt.Scanln(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
|
||||||
|
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
|
||||||
|
Since: gui.Config.UserConfig.Logs.Since,
|
||||||
|
Tail: gui.Config.UserConfig.Logs.Tail,
|
||||||
|
Follow: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer readCloser.Close()
|
||||||
|
|
||||||
|
if !ctr.DetailsLoaded() {
|
||||||
|
// loop until the details load or context is cancelled, using timer
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 100)
|
||||||
|
defer ticker.Stop()
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
if ctr.DetailsLoaded() {
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctr.Details.Config.Tty {
|
||||||
|
_, err = io.Copy(writer, readCloser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = stdcopy.StdCopy(writer, writer, readCloser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,104 +1,196 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-errors/errors"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// list panel functions
|
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] {
|
||||||
|
// Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context.
|
||||||
|
isStandaloneContainer := func(container *commands.Container) bool {
|
||||||
|
if container.OneOff || container.ServiceName == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) getContainerContexts() []string {
|
return !lo.SomeBy(gui.Panels.Services.List.GetAllItems(), func(service *commands.Service) bool {
|
||||||
return []string{"logs", "stats", "config", "top"}
|
return service.Name == container.ServiceName && service.ProjectName == container.ProjectName
|
||||||
}
|
})
|
||||||
|
|
||||||
func (gui *Gui) getContainerContextTitles() []string {
|
|
||||||
return []string{gui.Tr.LogsTitle, gui.Tr.StatsTitle, gui.Tr.ConfigTitle, gui.Tr.TopTitle}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) getSelectedContainer() (*commands.Container, error) {
|
|
||||||
selectedLine := gui.State.Panels.Containers.SelectedLine
|
|
||||||
if selectedLine == -1 {
|
|
||||||
return &commands.Container{}, gui.Errors.ErrNoContainers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.DockerCommand.DisplayContainers[selectedLine], nil
|
return &panels.SideListPanel[*commands.Container]{
|
||||||
|
ContextState: &panels.ContextState[*commands.Container]{
|
||||||
|
GetMainTabs: func() []panels.MainTab[*commands.Container] {
|
||||||
|
return []panels.MainTab[*commands.Container]{
|
||||||
|
{
|
||||||
|
Key: "logs",
|
||||||
|
Title: gui.Tr.LogsTitle,
|
||||||
|
Render: gui.renderContainerLogsToMain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "stats",
|
||||||
|
Title: gui.Tr.StatsTitle,
|
||||||
|
Render: gui.renderContainerStats,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "env",
|
||||||
|
Title: gui.Tr.EnvTitle,
|
||||||
|
Render: gui.renderContainerEnv,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "config",
|
||||||
|
Title: gui.Tr.ConfigTitle,
|
||||||
|
Render: gui.renderContainerConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "top",
|
||||||
|
Title: gui.Tr.TopTitle,
|
||||||
|
Render: gui.renderContainerTop,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GetItemContextCacheKey: func(container *commands.Container) string {
|
||||||
|
// Including the container state in the cache key so that if the container
|
||||||
|
// restarts we re-read the logs. In the past we've had some glitchiness
|
||||||
|
// where a container restarts but the new logs don't get read.
|
||||||
|
// Note that this might be jarring if we have a lot of logs and the container
|
||||||
|
// restarts a lot, so let's keep an eye on it.
|
||||||
|
return "containers-" + container.ID + "-" + container.Container.State
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ListPanel: panels.ListPanel[*commands.Container]{
|
||||||
|
List: panels.NewFilteredList[*commands.Container](),
|
||||||
|
View: gui.Views.Containers,
|
||||||
|
},
|
||||||
|
NoItemsMessage: gui.Tr.NoContainers,
|
||||||
|
Gui: gui.intoInterface(),
|
||||||
|
// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created)
|
||||||
|
// and sorted by name if c.SortContainersByState is false
|
||||||
|
Sort: func(a *commands.Container, b *commands.Container) bool {
|
||||||
|
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
|
||||||
|
},
|
||||||
|
Filter: func(container *commands.Container) bool {
|
||||||
|
if !gui.State.ShowExitedContainers && container.Container.State == "exited" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// When project-scoped, apply project and standalone filtering.
|
||||||
|
// Otherwise all containers are shown in a flat list regardless
|
||||||
|
// of which compose project they belong to.
|
||||||
|
if gui.DockerCommand.IsProjectScoped() {
|
||||||
|
// This check must be inside the IsProjectScoped guard: when
|
||||||
|
// not project-scoped, services are still derived from container
|
||||||
|
// labels, so compose-managed containers from other projects
|
||||||
|
// would be incorrectly hidden.
|
||||||
|
//
|
||||||
|
// Note that this is O(N*M) time complexity where N is the number of services
|
||||||
|
// and M is the number of containers. We expect N to be small but M may be large,
|
||||||
|
// so we will need to keep an eye on this.
|
||||||
|
if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by selected project. Containers with no project (truly
|
||||||
|
// standalone, not from any compose project) are always shown.
|
||||||
|
selectedProject := gui.getSelectedProjectName()
|
||||||
|
if selectedProject == "" {
|
||||||
|
selectedProject = gui.DockerCommand.LocalProjectName
|
||||||
|
}
|
||||||
|
if selectedProject != "" && container.ProjectName != "" && container.ProjectName != selectedProject {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
GetTableCells: func(container *commands.Container) []string {
|
||||||
|
return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainersClick(g *gocui.Gui, v *gocui.View) error {
|
var containerStates = map[string]int{
|
||||||
itemCount := len(gui.DockerCommand.DisplayContainers)
|
"running": 1,
|
||||||
handleSelect := gui.handleContainerSelect
|
"exited": 2,
|
||||||
selectedLine := &gui.State.Panels.Containers.SelectedLine
|
"created": 3,
|
||||||
|
|
||||||
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
|
func sortContainers(a *commands.Container, b *commands.Container, legacySort bool) bool {
|
||||||
container, err := gui.getSelectedContainer()
|
if legacySort {
|
||||||
|
return a.Name < b.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
stateLeft := containerStates[a.Container.State]
|
||||||
|
stateRight := containerStates[b.Container.State]
|
||||||
|
if stateLeft == stateRight {
|
||||||
|
return a.Name < b.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerStates[a.Container.State] < containerStates[b.Container.State]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc {
|
||||||
|
return gui.NewSimpleRenderStringTask(func() string { return gui.containerEnv(container) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) containerEnv(container *commands.Container) string {
|
||||||
|
if !container.DetailsLoaded() {
|
||||||
|
return gui.Tr.WaitingForContainerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(container.Details.Config.Env) == 0 {
|
||||||
|
return gui.Tr.NothingToDisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
envVarsList := lo.Map(container.Details.Config.Env, func(envVar string, _ int) []string {
|
||||||
|
splitEnv := strings.SplitN(envVar, "=", 2)
|
||||||
|
key := splitEnv[0]
|
||||||
|
value := ""
|
||||||
|
if len(splitEnv) > 1 {
|
||||||
|
value = splitEnv[1]
|
||||||
|
}
|
||||||
|
return []string{
|
||||||
|
utils.ColoredString(key+":", color.FgGreen),
|
||||||
|
utils.ColoredString(value, color.FgYellow),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
output, err := utils.RenderTable(envVarsList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != gui.Errors.ErrNoContainers {
|
gui.Log.Error(err)
|
||||||
return err
|
return gui.Tr.CannotDisplayEnvVariables
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v); err != nil {
|
return output
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex]
|
|
||||||
if !gui.shouldRefresh(key) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Tabs = gui.getContainerContextTitles()
|
|
||||||
mainView.TabIndex = gui.State.Panels.Containers.ContextIndex
|
|
||||||
|
|
||||||
gui.clearMainView()
|
|
||||||
|
|
||||||
switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] {
|
|
||||||
case "logs":
|
|
||||||
if err := gui.renderContainerLogs(container); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "config":
|
|
||||||
if err := gui.renderContainerConfig(container); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "stats":
|
|
||||||
if err := gui.renderContainerStats(container); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "top":
|
|
||||||
if err := gui.renderContainerTop(container); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("Unknown context for containers panel")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderContainerConfig(container *commands.Container) error {
|
func (gui *Gui) renderContainerConfig(container *commands.Container) tasks.TaskFunc {
|
||||||
mainView := gui.getMainView()
|
return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfigStr(container) })
|
||||||
mainView.Autoscroll = false
|
}
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
|
||||||
|
func (gui *Gui) containerConfigStr(container *commands.Container) string {
|
||||||
|
if !container.DetailsLoaded() {
|
||||||
|
return gui.Tr.WaitingForContainerInfo
|
||||||
|
}
|
||||||
|
|
||||||
padding := 10
|
padding := 10
|
||||||
output := ""
|
output := ""
|
||||||
output += utils.WithPadding("ID: ", padding) + container.ID + "\n"
|
output += utils.WithPadding("ID: ", padding) + container.ID + "\n"
|
||||||
output += utils.WithPadding("Name: ", padding) + container.Name + "\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("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 += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, container.Details.Config.Labels)
|
||||||
output += "\n"
|
output += "\n"
|
||||||
|
|
@ -108,9 +200,9 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error {
|
||||||
output += "\n"
|
output += "\n"
|
||||||
for _, mount := range container.Details.Mounts {
|
for _, mount := range container.Details.Mounts {
|
||||||
if mount.Type == "volume" {
|
if mount.Type == "volume" {
|
||||||
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Name)
|
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name)
|
||||||
} else {
|
} else {
|
||||||
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Source, mount.Destination)
|
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -129,360 +221,221 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error {
|
||||||
output += "none\n"
|
output += "none\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(&container.Details, "", " ")
|
data, err := utils.MarshalIntoYaml(&container.Details)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Sprintf("Error marshalling container details: %v", err)
|
||||||
}
|
}
|
||||||
output += fmt.Sprintf("\nFull details:\n\n%s", string(data))
|
|
||||||
|
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
output += fmt.Sprintf("\nFull details:\n\n%s", utils.ColoredYamlString(string(data)))
|
||||||
gui.renderString(gui.g, "main", output)
|
|
||||||
})
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderContainerStats(container *commands.Container) error {
|
func (gui *Gui) renderContainerStats(container *commands.Container) tasks.TaskFunc {
|
||||||
mainView := gui.getMainView()
|
return gui.NewTickerTask(TickerTaskOpts{
|
||||||
mainView.Autoscroll = false
|
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
contents, err := presentation.RenderStats(gui.Config.UserConfig, container, gui.Views.Main.Width())
|
||||||
|
if err != nil {
|
||||||
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
|
_ = gui.createErrorPanel(err.Error())
|
||||||
width, _ := mainView.Size()
|
|
||||||
|
|
||||||
contents, err := container.RenderStats(width)
|
|
||||||
if err != nil {
|
|
||||||
gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.reRenderString(gui.g, "main", contents)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderContainerTop(container *commands.Container) error {
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Autoscroll = false
|
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
|
||||||
|
|
||||||
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
|
|
||||||
contents, err := container.RenderTop()
|
|
||||||
if err != nil {
|
|
||||||
gui.reRenderString(gui.g, "main", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.reRenderString(gui.g, "main", contents)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderContainerLogs(container *commands.Container) error {
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Autoscroll = true
|
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
|
||||||
|
|
||||||
return gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) {
|
|
||||||
gui.clearMainView()
|
|
||||||
|
|
||||||
command := utils.ApplyTemplate(
|
|
||||||
gui.Config.UserConfig.CommandTemplates.ContainerLogs,
|
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Container: container}),
|
|
||||||
)
|
|
||||||
cmd := gui.OSCommand.RunCustomCommand(command)
|
|
||||||
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
cmd.Stdout = mainView
|
|
||||||
cmd.Stderr = mainView
|
|
||||||
|
|
||||||
cmd.Start()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-stop
|
|
||||||
if err := gui.OSCommand.Kill(cmd); err != nil {
|
|
||||||
gui.Log.Warn(err)
|
|
||||||
}
|
}
|
||||||
gui.Log.Info("killed container logs process")
|
|
||||||
return
|
|
||||||
}()
|
|
||||||
|
|
||||||
cmd.Wait()
|
gui.reRenderStringMain(contents)
|
||||||
|
},
|
||||||
|
Duration: time.Second,
|
||||||
|
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||||
|
Wrap: false, // wrapping looks bad here so we're overriding the config value
|
||||||
|
Autoscroll: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// if we are here because the task has been stopped, we should return
|
func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc {
|
||||||
// if we are here then the container must have exited, meaning we should wait until it's back again before
|
return gui.NewTickerTask(TickerTaskOpts{
|
||||||
L:
|
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||||
for {
|
contents, err := container.RenderTop(ctx)
|
||||||
select {
|
if err != nil {
|
||||||
case <-stop:
|
gui.RenderStringMain(err.Error())
|
||||||
return
|
|
||||||
default:
|
|
||||||
result, err := container.Inspect()
|
|
||||||
if err != nil {
|
|
||||||
// if we get an error, then the container has probably been removed so we'll get out of here
|
|
||||||
gui.Log.Error(err)
|
|
||||||
notifyStopped <- struct{}{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if result.State.Running {
|
|
||||||
break L
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
gui.reRenderStringMain(contents)
|
||||||
|
},
|
||||||
|
Duration: time.Second,
|
||||||
|
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||||
|
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||||
|
Autoscroll: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshContainersAndServices() error {
|
func (gui *Gui) refreshContainersAndServices() error {
|
||||||
containersView := gui.getContainersView()
|
if gui.Views.Containers == nil {
|
||||||
if containersView == nil {
|
|
||||||
// if the containersView hasn't been instantiated yet we just return
|
// if the containersView hasn't been instantiated yet we just return
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep track of current service selected so that we can reposition our cursor if it moves position in the list
|
// keep track of current service selected so that we can reposition our cursor if it moves position in the list
|
||||||
sl := gui.State.Panels.Services.SelectedLine
|
originalSelectedLineIdx := gui.Panels.Services.SelectedIdx
|
||||||
var selectedService *commands.Service
|
selectedService, isServiceSelected := gui.Panels.Services.List.TryGet(originalSelectedLineIdx)
|
||||||
if len(gui.DockerCommand.Services) > 0 {
|
|
||||||
selectedService = gui.DockerCommand.Services[sl]
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gui.DockerCommand.RefreshContainersAndServices(); err != nil {
|
containers, services, err := gui.DockerCommand.RefreshContainersAndServices(
|
||||||
|
gui.Panels.Containers.List.GetAllItems(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gui.Panels.Services.SetItems(services)
|
||||||
|
gui.Panels.Containers.SetItems(containers)
|
||||||
|
|
||||||
// see if our selected service has moved
|
// see if our selected service has moved
|
||||||
if selectedService != nil {
|
if isServiceSelected {
|
||||||
for i, service := range gui.DockerCommand.Services {
|
for i, service := range gui.Panels.Services.List.GetItems() {
|
||||||
if service.ID == selectedService.ID {
|
if service.ID == selectedService.ID {
|
||||||
if i == sl {
|
if i == originalSelectedLineIdx {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
gui.State.Panels.Services.SelectedLine = i
|
gui.Panels.Services.SetSelectedLineIdx(i)
|
||||||
if err := gui.focusPoint(0, i, len(gui.DockerCommand.Services), gui.getServicesView()); err != nil {
|
gui.Panels.Services.Refocus()
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(gui.DockerCommand.DisplayContainers) > 0 && gui.State.Panels.Containers.SelectedLine == -1 {
|
return gui.renderContainersAndServices()
|
||||||
gui.State.Panels.Containers.SelectedLine = 0
|
}
|
||||||
}
|
|
||||||
if len(gui.DockerCommand.DisplayContainers)-1 < gui.State.Panels.Containers.SelectedLine {
|
func (gui *Gui) renderContainersAndServices() error {
|
||||||
gui.State.Panels.Containers.SelectedLine = len(gui.DockerCommand.DisplayContainers) - 1
|
if err := gui.Panels.Services.RerenderList(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// doing the exact same thing for services
|
if err := gui.Panels.Containers.RerenderList(); err != nil {
|
||||||
if len(gui.DockerCommand.Services) > 0 && gui.State.Panels.Services.SelectedLine == -1 {
|
return err
|
||||||
gui.State.Panels.Services.SelectedLine = 0
|
|
||||||
}
|
}
|
||||||
if len(gui.DockerCommand.Services)-1 < gui.State.Panels.Services.SelectedLine {
|
|
||||||
gui.State.Panels.Services.SelectedLine = len(gui.DockerCommand.Services) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
|
||||||
containersView.Clear()
|
|
||||||
isFocused := gui.g.CurrentView().Name() == "containers"
|
|
||||||
|
|
||||||
list, err := utils.RenderList(gui.DockerCommand.DisplayContainers, utils.IsFocused(isFocused))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprint(containersView, list)
|
|
||||||
|
|
||||||
if containersView == g.CurrentView() {
|
|
||||||
if err := gui.handleContainerSelect(g, containersView); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doing the exact same thing for services
|
|
||||||
if !gui.DockerCommand.InDockerComposeProject {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
servicesView := gui.getServicesView()
|
|
||||||
servicesView.Clear()
|
|
||||||
isFocused = gui.g.CurrentView().Name() == "services"
|
|
||||||
list, err = utils.RenderList(gui.DockerCommand.Services, utils.IsFocused(isFocused))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprint(servicesView, list)
|
|
||||||
|
|
||||||
if servicesView == g.CurrentView() {
|
|
||||||
return gui.handleServiceSelect(g, servicesView)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
panelState := gui.State.Panels.Containers
|
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.DisplayContainers), false)
|
|
||||||
|
|
||||||
return gui.handleContainerSelect(gui.g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleContainersPrevLine(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
panelState := gui.State.Panels.Containers
|
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.DisplayContainers), true)
|
|
||||||
|
|
||||||
return gui.handleContainerSelect(gui.g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleContainersNextContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getContainerContexts()
|
|
||||||
if gui.State.Panels.Containers.ContextIndex >= len(contexts)-1 {
|
|
||||||
gui.State.Panels.Containers.ContextIndex = 0
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Containers.ContextIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleContainerSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleContainersPrevContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getContainerContexts()
|
|
||||||
if gui.State.Panels.Containers.ContextIndex <= 0 {
|
|
||||||
gui.State.Panels.Containers.ContextIndex = len(contexts) - 1
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Containers.ContextIndex--
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleContainerSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type removeContainerOption struct {
|
|
||||||
description string
|
|
||||||
command string
|
|
||||||
configOptions types.ContainerRemoveOptions
|
|
||||||
runCommand bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStrings is a function.
|
|
||||||
func (r *removeContainerOption) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
|
||||||
gui.DockerCommand.ShowExited = !gui.DockerCommand.ShowExited
|
gui.State.ShowExitedContainers = !gui.State.ShowExitedContainers
|
||||||
return nil
|
|
||||||
|
return gui.Panels.Containers.RerenderList()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
container, err := gui.getSelectedContainer()
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
options := []*removeContainerOption{
|
handleMenuPress := func(configOptions container.RemoveOptions) error {
|
||||||
{
|
|
||||||
description: gui.Tr.Remove,
|
|
||||||
command: "docker rm " + container.ID[1:10],
|
|
||||||
configOptions: types.ContainerRemoveOptions{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: gui.Tr.RemoveWithVolumes,
|
|
||||||
command: "docker rm --volumes " + container.ID[1:10],
|
|
||||||
configOptions: types.ContainerRemoveOptions{RemoveVolumes: true},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: gui.Tr.Cancel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMenuPress := func(index int) error {
|
|
||||||
if options[index].command == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
configOptions := options[index].configOptions
|
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||||
if err := container.Remove(configOptions); err != nil {
|
if err := ctr.Remove(configOptions); err != nil {
|
||||||
if commands.HasErrorCode(err, commands.MustStopContainer) {
|
if commands.HasErrorCode(err, commands.MustStopContainer) {
|
||||||
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||||
configOptions.Force = true
|
configOptions.Force = true
|
||||||
if err := container.Remove(configOptions); err != nil {
|
return ctr.Remove(configOptions)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return gui.refreshContainersAndServices()
|
|
||||||
})
|
})
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
return gui.refreshContainersAndServices()
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.createMenu("", options, len(options), handleMenuPress)
|
menuItems := []*types.MenuItem{
|
||||||
|
{
|
||||||
|
LabelColumns: []string{gui.Tr.Remove, "docker rm " + ctr.ID[1:10]},
|
||||||
|
OnPress: func() error { return handleMenuPress(container.RemoveOptions{}) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LabelColumns: []string{gui.Tr.RemoveWithVolumes, "docker rm --volumes " + ctr.ID[1:10]},
|
||||||
|
OnPress: func() error { return handleMenuPress(container.RemoveOptions{RemoveVolumes: true}) },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.Menu(CreateMenuOptions{
|
||||||
|
Title: "",
|
||||||
|
Items: menuItems,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) PauseContainer(container *commands.Container) error {
|
||||||
container, err := gui.getSelectedContainer()
|
return gui.WithWaitingStatus(gui.Tr.PausingStatus, func() (err error) {
|
||||||
if err != nil {
|
if container.Details.State.Paused {
|
||||||
return nil
|
err = container.Unpause()
|
||||||
}
|
} else {
|
||||||
|
err = container.Pause()
|
||||||
|
}
|
||||||
|
|
||||||
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
|
if err != nil {
|
||||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
return gui.createErrorPanel(err.Error())
|
||||||
if err := container.Stop(); err != nil {
|
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.refreshContainersAndServices()
|
|
||||||
})
|
|
||||||
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
container, err := gui.getSelectedContainer()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
|
||||||
if err := container.Restart(); err != nil {
|
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.refreshContainersAndServices()
|
return gui.refreshContainersAndServices()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
|
||||||
container, err := gui.getSelectedContainer()
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := container.Attach()
|
return gui.PauseContainer(ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SubProcess = c
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.Errors.ErrSubProcess
|
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||||
|
if err := ctr.Stop(); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||||
|
if err := ctr.Restart(); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := ctr.Attach()
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.runSubprocessWithMessage(c, gui.Tr.DetachFromContainerShortCut)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handlePruneContainers() error {
|
func (gui *Gui) handlePruneContainers() error {
|
||||||
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) 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 {
|
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||||
err := gui.DockerCommand.PruneContainers()
|
err := gui.DockerCommand.PruneContainers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
@ -490,28 +443,45 @@ func (gui *Gui) handlePruneContainers() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
|
||||||
container, err := gui.getSelectedContainer()
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := container.ViewLogs()
|
gui.renderLogsToStdout(ctr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SubProcess = c
|
return gui.containerExecShell(ctr)
|
||||||
return gui.Errors.ErrSubProcess
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) containerExecShell(container *commands.Container) error {
|
||||||
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||||
|
Container: container,
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: use SDK
|
||||||
|
resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject)
|
||||||
|
// attach and return the subprocess error
|
||||||
|
cmd := gui.OSCommand.ExecutableFromString(resolvedCommand)
|
||||||
|
return gui.runSubprocess(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
container, err := gui.getSelectedContainer()
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||||
Container: container,
|
Container: ctr,
|
||||||
})
|
})
|
||||||
|
|
||||||
customCommands := gui.Config.UserConfig.CustomCommands.Containers
|
customCommands := gui.Config.UserConfig.CustomCommands.Containers
|
||||||
|
|
@ -520,11 +490,12 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleStopContainers() error {
|
func (gui *Gui) handleStopContainers() error {
|
||||||
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||||
|
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
|
||||||
for _, container := range gui.DockerCommand.Containers {
|
if err := ctr.Stop(); err != nil {
|
||||||
_ = container.Stop()
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -533,11 +504,12 @@ func (gui *Gui) handleStopContainers() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleRemoveContainers() error {
|
func (gui *Gui) handleRemoveContainers() error {
|
||||||
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||||
|
for _, ctr := range gui.Panels.Containers.List.GetAllItems() {
|
||||||
for _, container := range gui.DockerCommand.Containers {
|
if err := ctr.Remove(container.RemoveOptions{Force: true}); err != nil {
|
||||||
_ = container.Remove(types.ContainerRemoveOptions{Force: true})
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -566,3 +538,31 @@ func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
|
||||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open first port in browser
|
||||||
|
func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
ctr, err := gui.Panels.Containers.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.openContainerInBrowser(ctr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {
|
||||||
|
// skip if no any ports
|
||||||
|
if len(ctr.Container.Ports) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// skip if the first port is not published
|
||||||
|
port := ctr.Container.Ports[0]
|
||||||
|
if port.IP == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ip := port.IP
|
||||||
|
if ip == "0.0.0.0" {
|
||||||
|
ip = "localhost"
|
||||||
|
}
|
||||||
|
link := fmt.Sprintf("http://%s:%d/", ip, port.PublicPort)
|
||||||
|
return gui.OSCommand.OpenLink(link)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,68 +4,50 @@ import (
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type customCommandOption struct {
|
|
||||||
customCommand config.CustomCommand
|
|
||||||
description string
|
|
||||||
command string
|
|
||||||
name string
|
|
||||||
runCommand bool
|
|
||||||
attach bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStrings is a function.
|
|
||||||
func (r *customCommandOption) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
return []string{r.name, utils.ColoredString(r.description, color.FgCyan)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error {
|
func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error {
|
||||||
options := make([]*customCommandOption, len(customCommands)+1)
|
menuItems := lo.Map(customCommands, func(command config.CustomCommand, _ int) *types.MenuItem {
|
||||||
for i, command := range customCommands {
|
|
||||||
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
|
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
|
||||||
|
|
||||||
options[i] = &customCommandOption{
|
onPress := func() error {
|
||||||
customCommand: command,
|
if command.InternalFunction != nil {
|
||||||
description: utils.WithShortSha(resolvedCommand),
|
return command.InternalFunction()
|
||||||
command: resolvedCommand,
|
|
||||||
runCommand: true,
|
|
||||||
attach: command.Attach,
|
|
||||||
name: command.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
options[len(options)-1] = &customCommandOption{
|
|
||||||
name: gui.Tr.Cancel,
|
|
||||||
runCommand: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMenuPress := func(index int) error {
|
|
||||||
option := options[index]
|
|
||||||
if !option.runCommand {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if option.customCommand.InternalFunction != nil {
|
|
||||||
return option.customCommand.InternalFunction()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have a command for attaching, we attach and return the subprocess error
|
|
||||||
if option.customCommand.Attach {
|
|
||||||
cmd := gui.OSCommand.ExecutableFromString(option.command)
|
|
||||||
gui.SubProcess = cmd
|
|
||||||
return gui.Errors.ErrSubProcess
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
|
||||||
if err := gui.OSCommand.RunCommand(option.command); err != nil {
|
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.createMenu(title, options, len(options), handleMenuPress)
|
if command.Shell {
|
||||||
|
resolvedCommand = gui.OSCommand.NewCommandStringWithShell(resolvedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a command for attaching, we attach and return the subprocess error
|
||||||
|
if command.Attach {
|
||||||
|
return gui.runSubprocess(gui.OSCommand.ExecutableFromString(resolvedCommand))
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||||
|
if err := gui.OSCommand.RunCommand(resolvedCommand); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.MenuItem{
|
||||||
|
LabelColumns: []string{
|
||||||
|
command.Name,
|
||||||
|
utils.ColoredString(utils.WithShortSha(resolvedCommand), color.FgCyan),
|
||||||
|
},
|
||||||
|
OnPress: onPress,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return gui.Menu(CreateMenuOptions{
|
||||||
|
Title: title,
|
||||||
|
Items: menuItems,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
|
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
|
||||||
|
|
|
||||||
80
pkg/gui/filtering.go
Normal file
80
pkg/gui/filtering.go
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) handleOpenFilter() error {
|
||||||
|
panel, ok := gui.currentListPanel()
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if panel.IsFilterDisabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.State.Filter.active = true
|
||||||
|
gui.State.Filter.panel = panel
|
||||||
|
|
||||||
|
return gui.switchFocus(gui.Views.Filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) onNewFilterNeedle(value string) error {
|
||||||
|
gui.State.Filter.needle = value
|
||||||
|
gui.ResetOrigin(gui.State.Filter.panel.GetView())
|
||||||
|
return gui.State.Filter.panel.RerenderList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||||
|
return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||||
|
matched := f(v, key, ch, mod)
|
||||||
|
if matched {
|
||||||
|
if err := gui.onNewFilterNeedle(v.TextArea.GetContent()); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) escapeFilterPrompt() error {
|
||||||
|
if err := gui.clearFilter(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.returnFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) clearFilter() error {
|
||||||
|
gui.State.Filter.needle = ""
|
||||||
|
gui.State.Filter.active = false
|
||||||
|
panel := gui.State.Filter.panel
|
||||||
|
gui.State.Filter.panel = nil
|
||||||
|
gui.Views.Filter.ClearTextArea()
|
||||||
|
|
||||||
|
if panel == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.ResetOrigin(panel.GetView())
|
||||||
|
|
||||||
|
return panel.RerenderList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns to the list view with the filter still applied
|
||||||
|
func (gui *Gui) commitFilter() error {
|
||||||
|
if gui.State.Filter.needle == "" {
|
||||||
|
if err := gui.clearFilter(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.returnFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) filterPrompt() string {
|
||||||
|
return fmt.Sprintf("%s: ", gui.Tr.FilterPrompt)
|
||||||
|
}
|
||||||
146
pkg/gui/focus.go
Normal file
146
pkg/gui/focus.go
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) newLineFocused(v *gocui.View) error {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentListPanel, ok := gui.currentListPanel()
|
||||||
|
if ok {
|
||||||
|
return currentListPanel.HandleSelect()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Name() {
|
||||||
|
case "confirmation":
|
||||||
|
return nil
|
||||||
|
case "main":
|
||||||
|
v.Highlight = false
|
||||||
|
return nil
|
||||||
|
case "filter":
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
panic(gui.Tr.NoViewMachingNewLineFocusedSwitchStatement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move some of this logic into our onFocusLost and onFocus hooks
|
||||||
|
func (gui *Gui) switchFocus(newView *gocui.View) error {
|
||||||
|
gui.Mutexes.ViewStackMutex.Lock()
|
||||||
|
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||||
|
|
||||||
|
return gui.switchFocusAux(newView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) switchFocusAux(newView *gocui.View) error {
|
||||||
|
gui.pushView(newView.Name())
|
||||||
|
gui.Log.Info("setting highlight to true for view " + newView.Name())
|
||||||
|
gui.Log.Info("new focused view is " + newView.Name())
|
||||||
|
if _, err := gui.g.SetCurrentView(newView.Name()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.g.Cursor = newView.Editable
|
||||||
|
|
||||||
|
if err := gui.renderPanelOptions(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newViewStack := gui.State.ViewStack
|
||||||
|
|
||||||
|
if gui.State.Filter.panel != nil && !lo.Contains(newViewStack, gui.State.Filter.panel.GetView().Name()) {
|
||||||
|
if err := gui.clearFilter(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add 'onFocusLost' hook
|
||||||
|
if !lo.Contains(newViewStack, "menu") {
|
||||||
|
gui.Views.Menu.Visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.newLineFocused(newView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) returnFocus() error {
|
||||||
|
gui.Mutexes.ViewStackMutex.Lock()
|
||||||
|
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||||
|
|
||||||
|
if len(gui.State.ViewStack) <= 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2]
|
||||||
|
previousView, err := gui.g.View(previousViewName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.switchFocusAux(previousView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) removeViewFromStack(view *gocui.View) {
|
||||||
|
gui.Mutexes.ViewStackMutex.Lock()
|
||||||
|
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||||
|
|
||||||
|
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||||
|
return viewName != view.Name()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not to be called directly. Use `switchFocus` instead
|
||||||
|
func (gui *Gui) pushView(name string) {
|
||||||
|
// No matter what view we're pushing, we first remove all popup panels from the stack
|
||||||
|
// (unless it's the search view because we may be searching the menu panel)
|
||||||
|
if name != "filter" {
|
||||||
|
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||||
|
return !gui.isPopupPanel(viewName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're pushing a side panel, we remove all other panels
|
||||||
|
if lo.Contains(gui.sideViewNames(), name) {
|
||||||
|
gui.State.ViewStack = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're pushing a panel that's already in the stack, we remove it
|
||||||
|
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||||
|
return viewName != name
|
||||||
|
})
|
||||||
|
|
||||||
|
gui.State.ViewStack = append(gui.State.ViewStack, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// excludes popups
|
||||||
|
func (gui *Gui) currentStaticViewName() string {
|
||||||
|
gui.Mutexes.ViewStackMutex.Lock()
|
||||||
|
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||||
|
|
||||||
|
for i := len(gui.State.ViewStack) - 1; i >= 0; i-- {
|
||||||
|
if !lo.Contains(gui.popupViewNames(), gui.State.ViewStack[i]) {
|
||||||
|
return gui.State.ViewStack[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.initiallyFocusedViewName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) currentSideViewName() string {
|
||||||
|
gui.Mutexes.ViewStackMutex.Lock()
|
||||||
|
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||||
|
|
||||||
|
// we expect that there is a side window somewhere in the view stack, so we will search from top to bottom
|
||||||
|
for idx := range gui.State.ViewStack {
|
||||||
|
reversedIdx := len(gui.State.ViewStack) - 1 - idx
|
||||||
|
viewName := gui.State.ViewStack[reversedIdx]
|
||||||
|
if lo.Contains(gui.sideViewNames(), viewName) {
|
||||||
|
return viewName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.initiallyFocusedViewName()
|
||||||
|
}
|
||||||
45
pkg/gui/gocui.go
Normal file
45
pkg/gui/gocui.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gookit/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gocuiColorMap = map[string]gocui.Attribute{
|
||||||
|
"default": gocui.ColorDefault,
|
||||||
|
"black": gocui.ColorBlack,
|
||||||
|
"red": gocui.ColorRed,
|
||||||
|
"green": gocui.ColorGreen,
|
||||||
|
"yellow": gocui.ColorYellow,
|
||||||
|
"blue": gocui.ColorBlue,
|
||||||
|
"magenta": gocui.ColorMagenta,
|
||||||
|
"cyan": gocui.ColorCyan,
|
||||||
|
"white": gocui.ColorWhite,
|
||||||
|
"bold": gocui.AttrBold,
|
||||||
|
"reverse": gocui.AttrReverse,
|
||||||
|
"underline": gocui.AttrUnderline,
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAttribute gets the gocui color attribute from the string
|
||||||
|
func GetGocuiAttribute(key string) gocui.Attribute {
|
||||||
|
if utils.IsValidHexValue(key) {
|
||||||
|
values := color.HEX(key).Values()
|
||||||
|
return gocui.NewRGBColor(int32(values[0]), int32(values[1]), int32(values[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
value, present := gocuiColorMap[key]
|
||||||
|
if present {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return gocui.ColorDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGocuiStyle bitwise OR's a list of attributes obtained via the given keys
|
||||||
|
func GetGocuiStyle(keys []string) gocui.Attribute {
|
||||||
|
var attribute gocui.Attribute
|
||||||
|
for _, key := range keys {
|
||||||
|
attribute |= GetGocuiAttribute(key)
|
||||||
|
}
|
||||||
|
return attribute
|
||||||
|
}
|
||||||
518
pkg/gui/gui.go
518
pkg/gui/gui.go
|
|
@ -1,94 +1,65 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang-collections/collections/stack"
|
"context"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
// "io"
|
|
||||||
// "io/ioutil"
|
|
||||||
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/events"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
|
|
||||||
// "strings"
|
throttle "github.com/boz/go-throttle"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
|
"github.com/sasha-s/go-deadlock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OverlappingEdges determines if panel edges overlap
|
|
||||||
var OverlappingEdges = false
|
|
||||||
|
|
||||||
// SentinelErrors are the errors that have special meaning and need to be checked
|
|
||||||
// by calling functions. The less of these, the better
|
|
||||||
type SentinelErrors struct {
|
|
||||||
ErrSubProcess error
|
|
||||||
ErrNoContainers error
|
|
||||||
ErrNoImages error
|
|
||||||
ErrNoVolumes error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
|
|
||||||
// because we can't do package-scoped errors with localization, and also because
|
|
||||||
// it seems like package-scoped variables are bad in general
|
|
||||||
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
|
|
||||||
// In the future it would be good to implement some of the recommendations of
|
|
||||||
// that article. For now, if we don't need an error to be a sentinel, we will just
|
|
||||||
// define it inline. This has implications for error messages that pop up everywhere
|
|
||||||
// in that we'll be duplicating the default values. We may need to look at
|
|
||||||
// having a default localisation bundle defined, and just using keys-only when
|
|
||||||
// localising things in the code.
|
|
||||||
func (gui *Gui) GenerateSentinelErrors() {
|
|
||||||
gui.Errors = SentinelErrors{
|
|
||||||
ErrSubProcess: errors.New(gui.Tr.RunningSubprocess),
|
|
||||||
ErrNoContainers: errors.New(gui.Tr.NoContainers),
|
|
||||||
ErrNoImages: errors.New(gui.Tr.NoImages),
|
|
||||||
ErrNoVolumes: errors.New(gui.Tr.NoVolumes),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gui wraps the gocui Gui object which handles rendering and events
|
// Gui wraps the gocui Gui object which handles rendering and events
|
||||||
type Gui struct {
|
type Gui struct {
|
||||||
g *gocui.Gui
|
g *gocui.Gui
|
||||||
Log *logrus.Entry
|
Log *logrus.Entry
|
||||||
DockerCommand *commands.DockerCommand
|
DockerCommand *commands.DockerCommand
|
||||||
OSCommand *commands.OSCommand
|
OSCommand *commands.OSCommand
|
||||||
SubProcess *exec.Cmd
|
|
||||||
State guiState
|
State guiState
|
||||||
Config *config.AppConfig
|
Config *config.AppConfig
|
||||||
Tr *i18n.TranslationSet
|
Tr *i18n.TranslationSet
|
||||||
Errors SentinelErrors
|
|
||||||
statusManager *statusManager
|
statusManager *statusManager
|
||||||
waitForIntro sync.WaitGroup
|
taskManager *tasks.TaskManager
|
||||||
T *tasks.TaskManager
|
|
||||||
ErrorChan chan error
|
ErrorChan chan error
|
||||||
CyclableViews []string
|
Views Views
|
||||||
|
|
||||||
|
// if we've suspended the gui (e.g. because we've switched to a subprocess)
|
||||||
|
// we typically want to pause some things that are running like background
|
||||||
|
// file refreshes
|
||||||
|
PauseBackgroundThreads bool
|
||||||
|
|
||||||
|
Mutexes
|
||||||
|
|
||||||
|
Panels Panels
|
||||||
}
|
}
|
||||||
|
|
||||||
type servicePanelState struct {
|
type Panels struct {
|
||||||
SelectedLine int
|
Projects *panels.SideListPanel[*commands.Project]
|
||||||
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
Services *panels.SideListPanel[*commands.Service]
|
||||||
|
Containers *panels.SideListPanel[*commands.Container]
|
||||||
|
Images *panels.SideListPanel[*commands.Image]
|
||||||
|
Volumes *panels.SideListPanel[*commands.Volume]
|
||||||
|
Networks *panels.SideListPanel[*commands.Network]
|
||||||
|
Menu *panels.SideListPanel[*types.MenuItem]
|
||||||
}
|
}
|
||||||
|
|
||||||
type containerPanelState struct {
|
type Mutexes struct {
|
||||||
SelectedLine int
|
SubprocessMutex deadlock.Mutex
|
||||||
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
ViewStackMutex deadlock.Mutex
|
||||||
}
|
|
||||||
|
|
||||||
type projectState struct {
|
|
||||||
ContextIndex int // for specifying if you are looking at credits/logs
|
|
||||||
}
|
|
||||||
|
|
||||||
type menuPanelState struct {
|
|
||||||
SelectedLine int
|
|
||||||
OnPress func(*gocui.Gui, *gocui.View) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mainPanelState struct {
|
type mainPanelState struct {
|
||||||
|
|
@ -96,38 +67,61 @@ type mainPanelState struct {
|
||||||
ObjectKey string
|
ObjectKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type imagePanelState struct {
|
|
||||||
SelectedLine int
|
|
||||||
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
|
|
||||||
}
|
|
||||||
|
|
||||||
type volumePanelState struct {
|
|
||||||
SelectedLine int
|
|
||||||
ContextIndex int
|
|
||||||
}
|
|
||||||
|
|
||||||
type panelStates struct {
|
type panelStates struct {
|
||||||
Services *servicePanelState
|
Main *mainPanelState
|
||||||
Containers *containerPanelState
|
|
||||||
Menu *menuPanelState
|
|
||||||
Main *mainPanelState
|
|
||||||
Images *imagePanelState
|
|
||||||
Volumes *volumePanelState
|
|
||||||
Project *projectState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type guiState struct {
|
type guiState struct {
|
||||||
MenuItemCount int // can't store the actual list because it's of interface{} type
|
// the names of views in the current focus stack (last item is the current view)
|
||||||
PreviousViews *stack.Stack
|
ViewStack []string
|
||||||
Platform commands.Platform
|
Platform commands.Platform
|
||||||
Panels *panelStates
|
Panels *panelStates
|
||||||
SubProcessOutput string
|
SubProcessOutput string
|
||||||
Stats map[string]commands.ContainerStats
|
Stats map[string]commands.ContainerStats
|
||||||
|
|
||||||
// SessionIndex tells us how many times we've come back from a subprocess.
|
// if true, we show containers with an 'exited' status in the containers panel
|
||||||
// We increment it each time we switch to a new subprocess
|
ShowExitedContainers bool
|
||||||
// Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose
|
|
||||||
SessionIndex int
|
ScreenMode WindowMaximisation
|
||||||
|
|
||||||
|
// Maintains the state of manual filtering i.e. typing in a substring
|
||||||
|
// to filter on in the current panel.
|
||||||
|
Filter filterState
|
||||||
|
}
|
||||||
|
|
||||||
|
type filterState struct {
|
||||||
|
// If true then we're either currently inside the filter view
|
||||||
|
// or we've committed the filter and we're back in the list view
|
||||||
|
active bool
|
||||||
|
// The panel that we're filtering.
|
||||||
|
panel panels.ISideListPanel
|
||||||
|
// The string that we're filtering on
|
||||||
|
needle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// screen sizing determines how much space your selected window takes up (window
|
||||||
|
// as in panel, not your terminal's window). Sometimes you want a bit more space
|
||||||
|
// to see the contents of a panel, and this keeps track of how much maximisation
|
||||||
|
// you've set
|
||||||
|
type WindowMaximisation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SCREEN_NORMAL WindowMaximisation = iota
|
||||||
|
SCREEN_HALF
|
||||||
|
SCREEN_FULL
|
||||||
|
)
|
||||||
|
|
||||||
|
func getScreenMode(config *config.AppConfig) WindowMaximisation {
|
||||||
|
switch config.UserConfig.Gui.ScreenMode {
|
||||||
|
case "normal":
|
||||||
|
return SCREEN_NORMAL
|
||||||
|
case "half":
|
||||||
|
return SCREEN_HALF
|
||||||
|
case "fullscreen":
|
||||||
|
return SCREEN_FULL
|
||||||
|
default:
|
||||||
|
return SCREEN_NORMAL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGui builds a new gui handler
|
// NewGui builds a new gui handler
|
||||||
|
|
@ -135,112 +129,53 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
|
||||||
initialState := guiState{
|
initialState := guiState{
|
||||||
Platform: *oSCommand.Platform,
|
Platform: *oSCommand.Platform,
|
||||||
Panels: &panelStates{
|
Panels: &panelStates{
|
||||||
Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0},
|
|
||||||
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
|
|
||||||
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
|
|
||||||
Volumes: &volumePanelState{SelectedLine: -1, ContextIndex: 0},
|
|
||||||
Menu: &menuPanelState{SelectedLine: 0},
|
|
||||||
Main: &mainPanelState{
|
Main: &mainPanelState{
|
||||||
ObjectKey: "",
|
ObjectKey: "",
|
||||||
},
|
},
|
||||||
Project: &projectState{ContextIndex: 0},
|
|
||||||
},
|
},
|
||||||
SessionIndex: 0,
|
ViewStack: []string{},
|
||||||
PreviousViews: stack.New(),
|
|
||||||
}
|
|
||||||
|
|
||||||
cyclableViews := []string{"project", "containers", "images", "volumes"}
|
ShowExitedContainers: true,
|
||||||
if dockerCommand.InDockerComposeProject {
|
ScreenMode: getScreenMode(config),
|
||||||
cyclableViews = []string{"project", "services", "containers", "images", "volumes"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gui := &Gui{
|
gui := &Gui{
|
||||||
Log: log,
|
Log: log,
|
||||||
DockerCommand: dockerCommand,
|
DockerCommand: dockerCommand,
|
||||||
OSCommand: oSCommand,
|
OSCommand: oSCommand,
|
||||||
// TODO: look into this warning
|
|
||||||
State: initialState,
|
State: initialState,
|
||||||
Config: config,
|
Config: config,
|
||||||
Tr: tr,
|
Tr: tr,
|
||||||
statusManager: &statusManager{},
|
statusManager: &statusManager{},
|
||||||
T: tasks.NewTaskManager(log, tr),
|
taskManager: tasks.NewTaskManager(log, tr),
|
||||||
ErrorChan: errorChan,
|
ErrorChan: errorChan,
|
||||||
CyclableViews: cyclableViews,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.GenerateSentinelErrors()
|
deadlock.Opts.Disable = !gui.Config.Debug
|
||||||
|
deadlock.Opts.DeadlockTimeout = 10 * time.Second
|
||||||
|
|
||||||
return gui, nil
|
return gui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func max(a, b int) int {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) loadNewDirectory() error {
|
|
||||||
gui.waitForIntro.Done()
|
|
||||||
|
|
||||||
if err := gui.refreshSidePanels(gui.g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if gui.Config.UserConfig.Reporting == "undetermined" {
|
|
||||||
if err := gui.promptAnonymousReporting(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) promptAnonymousReporting() error {
|
|
||||||
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.AnonymousReportingTitle, gui.Tr.AnonymousReportingPrompt, func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
gui.waitForIntro.Done()
|
|
||||||
// setting the value here explicitly so that we don't re-request after coming back from a subprocess. The proper solution would be to reload the config but that's tricky because it's loaded at the very top level
|
|
||||||
gui.Config.UserConfig.Reporting = "on"
|
|
||||||
return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error {
|
|
||||||
userConfig.Reporting = "on"
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
gui.waitForIntro.Done()
|
|
||||||
gui.Config.UserConfig.Reporting = "off"
|
|
||||||
return gui.Config.WriteToUserConfig(func(userConfig *config.UserConfig) error {
|
|
||||||
userConfig.Reporting = "off"
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderAppStatus() error {
|
|
||||||
appStatus := gui.statusManager.getStatusString()
|
|
||||||
if appStatus != "" {
|
|
||||||
return gui.renderString(gui.g, "appStatus", appStatus)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderGlobalOptions() error {
|
func (gui *Gui) renderGlobalOptions() error {
|
||||||
return gui.renderOptionsMap(map[string]string{
|
return gui.renderOptionsMap(map[string]string{
|
||||||
"PgUp/PgDn": gui.Tr.Scroll,
|
"PgUp/PgDn": gui.Tr.Scroll,
|
||||||
"← → ↑ ↓": gui.Tr.Navigate,
|
"← → ↑ ↓": gui.Tr.Navigate,
|
||||||
"esc/q": gui.Tr.Close,
|
"q": gui.Tr.Quit,
|
||||||
"b": gui.Tr.ViewBulkCommands,
|
"b": gui.Tr.ViewBulkCommands,
|
||||||
"x": gui.Tr.Menu,
|
"x": gui.Tr.Menu,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
||||||
currentSessionIndex := gui.State.SessionIndex
|
|
||||||
_ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
|
_ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
|
||||||
go func() {
|
go func() {
|
||||||
for range time.Tick(interval) {
|
ticker := time.NewTicker(interval)
|
||||||
if gui.State.SessionIndex > currentSessionIndex {
|
defer ticker.Stop()
|
||||||
return
|
for range ticker.C {
|
||||||
|
if !gui.PauseBackgroundThreads {
|
||||||
|
_ = function()
|
||||||
}
|
}
|
||||||
_ = function()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
@ -248,9 +183,12 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
||||||
// Run setup the gui with keybindings and start the mainloop
|
// Run setup the gui with keybindings and start the mainloop
|
||||||
func (gui *Gui) Run() error {
|
func (gui *Gui) Run() error {
|
||||||
// closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker
|
// closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker
|
||||||
defer gui.T.Close()
|
defer gui.taskManager.Close()
|
||||||
|
|
||||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||||
|
OutputMode: gocui.OutputTrue,
|
||||||
|
RuneReplacements: map[rune]string{},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -263,29 +201,18 @@ func (gui *Gui) Run() error {
|
||||||
|
|
||||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||||
|
|
||||||
|
// if the deadlock package wants to report a deadlock, we first need to
|
||||||
|
// close the gui so that we can actually read what it prints.
|
||||||
|
deadlock.Opts.LogBuf = lcUtils.NewOnceWriter(os.Stderr, func() {
|
||||||
|
gui.g.Close()
|
||||||
|
})
|
||||||
|
|
||||||
if err := gui.SetColorScheme(); err != nil {
|
if err := gui.SetColorScheme(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if gui.Config.UserConfig.Reporting == "undetermined" {
|
throttledRefresh := throttle.ThrottleFunc(time.Millisecond*50, true, gui.refresh)
|
||||||
gui.waitForIntro.Add(2)
|
defer throttledRefresh.Stop()
|
||||||
} else {
|
|
||||||
gui.waitForIntro.Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerRefreshInterval := gui.Config.UserConfig.Update.DockerRefreshInterval
|
|
||||||
go func() {
|
|
||||||
gui.waitForIntro.Wait()
|
|
||||||
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
|
|
||||||
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
|
|
||||||
gui.goEvery(dockerRefreshInterval, gui.refreshProject)
|
|
||||||
gui.goEvery(dockerRefreshInterval, gui.refreshContainersAndServices)
|
|
||||||
gui.goEvery(dockerRefreshInterval, gui.refreshVolumes)
|
|
||||||
gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails)
|
|
||||||
gui.goEvery(time.Millisecond*1000, gui.checkForContextChange)
|
|
||||||
}()
|
|
||||||
|
|
||||||
go gui.DockerCommand.MonitorContainerStats()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for err := range gui.ErrorChan {
|
for err := range gui.ErrorChan {
|
||||||
|
|
@ -297,27 +224,166 @@ func (gui *Gui) Run() error {
|
||||||
gui.Log.Warn(err)
|
gui.Log.Warn(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gui.createErrorPanel(gui.g, err.Error())
|
_ = gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
|
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
|
||||||
|
|
||||||
|
if err := gui.createAllViews(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gui.setInitialViewContent(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: see if we can avoid the circular dependency
|
||||||
|
gui.setPanels()
|
||||||
|
|
||||||
if err = gui.keybindings(g); err != nil {
|
if err = gui.keybindings(g); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gui.g.CurrentView() == nil {
|
||||||
|
viewName := gui.initiallyFocusedViewName()
|
||||||
|
view, err := gui.g.View(viewName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gui.switchFocus(view); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, finish := context.WithCancel(context.Background())
|
||||||
|
defer finish()
|
||||||
|
|
||||||
|
go gui.listenForEvents(ctx, throttledRefresh.Trigger)
|
||||||
|
go gui.monitorContainerStats(ctx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
throttledRefresh.Trigger()
|
||||||
|
|
||||||
|
gui.goEvery(time.Millisecond*30, gui.reRenderMain)
|
||||||
|
gui.goEvery(time.Millisecond*1000, gui.updateContainerDetails)
|
||||||
|
gui.goEvery(time.Millisecond*1000, gui.checkForContextChange)
|
||||||
|
// we need to regularly re-render these because their stats will be changed in the background
|
||||||
|
gui.goEvery(time.Millisecond*1000, gui.renderContainersAndServices)
|
||||||
|
}()
|
||||||
|
|
||||||
err = g.MainLoop()
|
err = g.MainLoop()
|
||||||
|
if err == gocui.ErrQuit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) setPanels() {
|
||||||
|
gui.Panels = Panels{
|
||||||
|
Projects: gui.getProjectPanel(),
|
||||||
|
Services: gui.getServicesPanel(),
|
||||||
|
Containers: gui.getContainersPanel(),
|
||||||
|
Images: gui.getImagesPanel(),
|
||||||
|
Volumes: gui.getVolumesPanel(),
|
||||||
|
Networks: gui.getNetworksPanel(),
|
||||||
|
Menu: gui.getMenuPanel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) updateContainerDetails() error {
|
||||||
|
return gui.DockerCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refresh() {
|
||||||
|
go func() {
|
||||||
|
// Refresh containers/services first, then projects (which depend on
|
||||||
|
// container labels to discover projects).
|
||||||
|
if err := gui.refreshContainersAndServices(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
if err := gui.refreshProject(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
if err := gui.reloadVolumes(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
if err := gui.reloadNetworks(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
if err := gui.reloadImages(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
|
||||||
|
errorCount := 0
|
||||||
|
|
||||||
|
onError := func(err error) {
|
||||||
|
if err != nil {
|
||||||
|
gui.ErrorChan <- errors.Errorf("Docker event stream returned error: %s\nRetry count: %d", err.Error(), errorCount)
|
||||||
|
}
|
||||||
|
errorCount++
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), events.ListOptions{})
|
||||||
|
|
||||||
|
if errorCount > 0 {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case err := <-errChan:
|
||||||
|
onError(err)
|
||||||
|
continue outer
|
||||||
|
default:
|
||||||
|
// If we're here then we lost connection to docker and we just got it back.
|
||||||
|
// The reason we do this refresh explicitly is because successfully
|
||||||
|
// reconnecting with docker does not mean it's going to send us a new
|
||||||
|
// event any time soon.
|
||||||
|
|
||||||
|
// Assuming the confirmation prompt currently holds the given error
|
||||||
|
_ = gui.closeConfirmationPrompt()
|
||||||
|
refresh()
|
||||||
|
errorCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case message := <-messageChan:
|
||||||
|
// We could be more granular about what events should trigger which refreshes.
|
||||||
|
// At the moment it's pretty efficient though, and it might not be worth
|
||||||
|
// the maintenance burden of mapping specific events to specific refreshes
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
gui.Log.Infof("received event of type: %s", message.Type)
|
||||||
|
case err := <-errChan:
|
||||||
|
onError(err)
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// checkForContextChange runs the currently focused panel's 'select' function, simulating the current item having just been selected. This will then trigger a check to see if anything's changed (e.g. a service has a new container) and if so, the appropriate code will run. For example, if you're reading logs from a service and all of a sudden its container changes, this will trigger the 'select' function, which will work out that the context is not different because of the new container, and then it will re-attempt to get the logs, this time for the correct container. This 'context' is stored in the main panel's ObjectKey. I'm using the term 'context' here more broadly than just the different tabs you can view in a panel.
|
// 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 {
|
func (gui *Gui) checkForContextChange() error {
|
||||||
return gui.newLineFocused(gui.g.CurrentView())
|
return gui.newLineFocused(gui.g.CurrentView())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) reRenderMain() error {
|
func (gui *Gui) reRenderMain() error {
|
||||||
mainView := gui.getMainView()
|
mainView := gui.Views.Main
|
||||||
if mainView == nil {
|
if mainView == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -331,13 +397,23 @@ func (gui *Gui) reRenderMain() error {
|
||||||
|
|
||||||
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
if gui.Config.UserConfig.ConfirmOnQuit {
|
if gui.Config.UserConfig.ConfirmOnQuit {
|
||||||
return gui.createConfirmationPanel(g, v, "", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel("", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gocui.ErrQuit
|
return gocui.ErrQuit
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
return gocui.ErrQuit
|
return gocui.ErrQuit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this handler is executed when we press escape when there is only one view
|
||||||
|
// on the stack.
|
||||||
|
func (gui *Gui) escape() error {
|
||||||
|
if gui.State.Filter.active {
|
||||||
|
return gui.clearFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
|
||||||
if !gui.g.Mouse {
|
if !gui.g.Mouse {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -347,47 +423,33 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
|
||||||
if cx > len(gui.Tr.Donate) {
|
if cx > len(gui.Tr.Donate) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return gui.OSCommand.OpenLink("https://donorbox.org/lazydocker")
|
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) editFile(filename string) error {
|
func (gui *Gui) editFile(filename string) error {
|
||||||
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
|
cmd, err := gui.OSCommand.EditFile(filename)
|
||||||
return err
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.runSubprocess(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) openFile(filename string) error {
|
func (gui *Gui) openFile(filename string) error {
|
||||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
|
||||||
// either no error, an error, or a subprocess to execute, and if a subprocess
|
|
||||||
// needs to be set on the gui object, it does so, and then returns the error
|
|
||||||
// the bool returned tells us whether the calling code should continue
|
|
||||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
|
||||||
if err != nil {
|
|
||||||
if err != gui.Errors.ErrSubProcess {
|
|
||||||
return false, gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sub != nil {
|
|
||||||
gui.SubProcess = sub
|
|
||||||
return false, gui.Errors.ErrSubProcess
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createPromptPanel(gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
command := gui.trimmedContent(v)
|
command := gui.trimmedContent(v)
|
||||||
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
|
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command))
|
||||||
return gui.Errors.ErrSubProcess
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) shouldRefresh(key string) bool {
|
func (gui *Gui) ShouldRefresh(key string) bool {
|
||||||
if gui.State.Panels.Main.ObjectKey == key {
|
if gui.State.Panels.Main.ObjectKey == key {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -397,8 +459,56 @@ func (gui *Gui) shouldRefresh(key string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) initiallyFocusedViewName() string {
|
func (gui *Gui) initiallyFocusedViewName() string {
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
if gui.DockerCommand.IsProjectScoped() {
|
||||||
return "services"
|
return "services"
|
||||||
}
|
}
|
||||||
return "containers"
|
return "containers"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) IgnoreStrings() []string {
|
||||||
|
return gui.Config.UserConfig.Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) Update(f func() error) {
|
||||||
|
gui.g.Update(func(*gocui.Gui) error { return f() })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) monitorContainerStats(ctx context.Context) {
|
||||||
|
// periodically loop through running containers and see if we need to create a monitor goroutine for any
|
||||||
|
// every second we check if we need to spawn a new goroutine
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
for _, container := range gui.Panels.Containers.List.GetAllItems() {
|
||||||
|
if !container.MonitoringStats {
|
||||||
|
go gui.DockerCommand.CreateClientStatMonitor(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is used by our cheatsheet code to generate keybindings. We need some views
|
||||||
|
// and panels to exist for us to know what keybindings there are, so we invoke
|
||||||
|
// gocui in headless mode and create them.
|
||||||
|
func (gui *Gui) SetupFakeGui() {
|
||||||
|
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||||
|
OutputMode: gocui.OutputTrue,
|
||||||
|
RuneReplacements: map[rune]string{},
|
||||||
|
Headless: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gui.g = g
|
||||||
|
defer g.Close()
|
||||||
|
if err := gui.createAllViews(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.setPanels()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,270 +5,200 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-errors/errors"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// list panel functions
|
func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
|
||||||
|
noneLabel := "<none>"
|
||||||
|
|
||||||
func (gui *Gui) getImageContexts() []string {
|
return &panels.SideListPanel[*commands.Image]{
|
||||||
return []string{"config"}
|
ContextState: &panels.ContextState[*commands.Image]{
|
||||||
|
GetMainTabs: func() []panels.MainTab[*commands.Image] {
|
||||||
|
return []panels.MainTab[*commands.Image]{
|
||||||
|
{
|
||||||
|
Key: "config",
|
||||||
|
Title: gui.Tr.ConfigTitle,
|
||||||
|
Render: gui.renderImageConfigTask,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GetItemContextCacheKey: func(image *commands.Image) string {
|
||||||
|
return "images-" + image.ID
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ListPanel: panels.ListPanel[*commands.Image]{
|
||||||
|
List: panels.NewFilteredList[*commands.Image](),
|
||||||
|
View: gui.Views.Images,
|
||||||
|
},
|
||||||
|
NoItemsMessage: gui.Tr.NoImages,
|
||||||
|
Gui: gui.intoInterface(),
|
||||||
|
Sort: func(a *commands.Image, b *commands.Image) bool {
|
||||||
|
if a.Name == noneLabel && b.Name != noneLabel {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Name != noneLabel && b.Name == noneLabel {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Name != b.Name {
|
||||||
|
return a.Name < b.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Tag != b.Tag {
|
||||||
|
return a.Tag < b.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.ID < b.ID
|
||||||
|
},
|
||||||
|
GetTableCells: presentation.GetImageDisplayStrings,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getImageContextTitles() []string {
|
func (gui *Gui) renderImageConfigTask(image *commands.Image) tasks.TaskFunc {
|
||||||
return []string{gui.Tr.ConfigTitle}
|
return gui.NewRenderStringTask(RenderStringTaskOpts{
|
||||||
}
|
GetStrContent: func() string { return gui.imageConfigStr(image) },
|
||||||
|
Autoscroll: false,
|
||||||
func (gui *Gui) getSelectedImage() (*commands.Image, error) {
|
Wrap: false, // don't care what your config is this page is ugly without wrapping
|
||||||
selectedLine := gui.State.Panels.Images.SelectedLine
|
|
||||||
if selectedLine == -1 {
|
|
||||||
return &commands.Image{}, gui.Errors.ErrNoImages
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.DockerCommand.Images[selectedLine], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleImagesClick(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
itemCount := len(gui.DockerCommand.Images)
|
|
||||||
handleSelect := gui.handleImageSelect
|
|
||||||
selectedLine := &gui.State.Panels.Images.SelectedLine
|
|
||||||
|
|
||||||
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
Image, err := gui.getSelectedImage()
|
|
||||||
if err != nil {
|
|
||||||
if err != gui.Errors.ErrNoImages {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return gui.renderString(g, "main", gui.Tr.NoImages)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
|
|
||||||
if !gui.shouldRefresh(key) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Tabs = gui.getImageContextTitles()
|
|
||||||
mainView.TabIndex = gui.State.Panels.Images.ContextIndex
|
|
||||||
|
|
||||||
switch gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] {
|
|
||||||
case "config":
|
|
||||||
if err := gui.renderImageConfig(mainView, Image); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("Unknown context for Images panel")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) error {
|
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
|
||||||
padding := 10
|
|
||||||
output := ""
|
|
||||||
output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
|
|
||||||
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
|
|
||||||
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
|
|
||||||
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
|
|
||||||
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
|
|
||||||
|
|
||||||
history, err := image.RenderHistory()
|
|
||||||
if err != nil {
|
|
||||||
gui.Log.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output += "\n\n" + history
|
|
||||||
|
|
||||||
mainView.Autoscroll = false
|
|
||||||
mainView.Wrap = false // don't care what your config is this page is ugly without wrapping
|
|
||||||
|
|
||||||
gui.renderString(gui.g, "main", output)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshImages() error {
|
func (gui *Gui) imageConfigStr(image *commands.Image) string {
|
||||||
ImagesView := gui.getImagesView()
|
padding := 10
|
||||||
if ImagesView == nil {
|
output := ""
|
||||||
// if the ImagesView hasn't been instantiated yet we just return
|
output += utils.WithPadding("Name: ", padding) + image.Name + "\n"
|
||||||
return nil
|
output += utils.WithPadding("ID: ", padding) + image.Image.ID + "\n"
|
||||||
|
output += utils.WithPadding("Tags: ", padding) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
|
||||||
|
output += utils.WithPadding("Size: ", padding) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
|
||||||
|
output += utils.WithPadding("Created: ", padding) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
|
||||||
|
|
||||||
|
history, err := image.RenderHistory()
|
||||||
|
if err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output += "\n\n" + history
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) reloadImages() error {
|
||||||
if err := gui.refreshStateImages(); err != nil {
|
if err := gui.refreshStateImages(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(gui.DockerCommand.Images) > 0 && gui.State.Panels.Images.SelectedLine == -1 {
|
return gui.Panels.Images.RerenderList()
|
||||||
gui.State.Panels.Images.SelectedLine = 0
|
|
||||||
}
|
|
||||||
if len(gui.DockerCommand.Images)-1 < gui.State.Panels.Images.SelectedLine {
|
|
||||||
gui.State.Panels.Images.SelectedLine = len(gui.DockerCommand.Images) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
|
||||||
|
|
||||||
ImagesView.Clear()
|
|
||||||
isFocused := gui.g.CurrentView().Name() == "Images"
|
|
||||||
list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Fprint(ImagesView, list)
|
|
||||||
|
|
||||||
if ImagesView == g.CurrentView() {
|
|
||||||
return gui.handleImageSelect(g, ImagesView)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: leave this to DockerCommand
|
|
||||||
func (gui *Gui) refreshStateImages() error {
|
func (gui *Gui) refreshStateImages() error {
|
||||||
Images, err := gui.DockerCommand.RefreshImages()
|
images, err := gui.DockerCommand.RefreshImages()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.DockerCommand.Images = Images
|
gui.Panels.Images.SetItems(images)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) FilterString(view *gocui.View) string {
|
||||||
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
|
if gui.State.Filter.panel != nil && gui.State.Filter.panel.GetView() != view {
|
||||||
return nil
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
panelState := gui.State.Panels.Images
|
return gui.State.Filter.needle
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), false)
|
|
||||||
|
|
||||||
return gui.handleImageSelect(gui.g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
panelState := gui.State.Panels.Images
|
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Images), true)
|
|
||||||
|
|
||||||
return gui.handleImageSelect(gui.g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleImagesNextContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getImageContexts()
|
|
||||||
if gui.State.Panels.Images.ContextIndex >= len(contexts)-1 {
|
|
||||||
gui.State.Panels.Images.ContextIndex = 0
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Images.ContextIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleImageSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleImagesPrevContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getImageContexts()
|
|
||||||
if gui.State.Panels.Images.ContextIndex <= 0 {
|
|
||||||
gui.State.Panels.Images.ContextIndex = len(contexts) - 1
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Images.ContextIndex--
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleImageSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type removeImageOption struct {
|
|
||||||
description string
|
|
||||||
command string
|
|
||||||
configOptions types.ImageRemoveOptions
|
|
||||||
runCommand bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDisplayStrings is a function.
|
|
||||||
func (r *removeImageOption) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
Image, err := gui.getSelectedImage()
|
type removeImageOption struct {
|
||||||
|
description string
|
||||||
|
command string
|
||||||
|
configOptions image.RemoveOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := gui.Panels.Images.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
shortSha := Image.ID[7:17]
|
shortSha := img.ID[7:17]
|
||||||
|
|
||||||
|
// TODO: have a way of toggling in a menu instead of showing each permutation as a separate menu item
|
||||||
options := []*removeImageOption{
|
options := []*removeImageOption{
|
||||||
{
|
{
|
||||||
description: gui.Tr.Remove,
|
description: gui.Tr.Remove,
|
||||||
command: "docker image rm " + shortSha,
|
command: "docker image rm " + shortSha,
|
||||||
configOptions: types.ImageRemoveOptions{PruneChildren: true},
|
configOptions: image.RemoveOptions{PruneChildren: true, Force: false},
|
||||||
runCommand: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: gui.Tr.RemoveWithoutPrune,
|
description: gui.Tr.RemoveWithoutPrune,
|
||||||
command: "docker image rm --no-prune " + shortSha,
|
command: "docker image rm --no-prune " + shortSha,
|
||||||
configOptions: types.ImageRemoveOptions{PruneChildren: false},
|
configOptions: image.RemoveOptions{PruneChildren: false, Force: false},
|
||||||
runCommand: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: gui.Tr.Cancel,
|
description: gui.Tr.RemoveWithForce,
|
||||||
runCommand: false,
|
command: "docker image rm --force " + shortSha,
|
||||||
|
configOptions: image.RemoveOptions{PruneChildren: true, Force: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: gui.Tr.RemoveWithoutPruneWithForce,
|
||||||
|
command: "docker image rm --no-prune --force " + shortSha,
|
||||||
|
configOptions: image.RemoveOptions{PruneChildren: false, Force: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMenuPress := func(index int) error {
|
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *types.MenuItem {
|
||||||
if !options[index].runCommand {
|
return &types.MenuItem{
|
||||||
return nil
|
LabelColumns: []string{
|
||||||
}
|
option.description,
|
||||||
configOptions := options[index].configOptions
|
color.New(color.FgRed).Sprint(option.command),
|
||||||
if cerr := Image.Remove(configOptions); cerr != nil {
|
},
|
||||||
return gui.createErrorPanel(gui.g, cerr.Error())
|
OnPress: func() error {
|
||||||
}
|
if err := img.Remove(option.configOptions); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return gui.refreshImages()
|
return nil
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return gui.createMenu("", options, len(options), handleMenuPress)
|
return gui.Menu(CreateMenuOptions{
|
||||||
|
Title: "",
|
||||||
|
Items: menuItems,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handlePruneImages() error {
|
func (gui *Gui) handlePruneImages() error {
|
||||||
return gui.createConfirmationPanel(gui.g, gui.getImagesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||||
err := gui.DockerCommand.PruneImages()
|
err := gui.DockerCommand.PruneImages()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
return gui.refreshImages()
|
return gui.reloadImages()
|
||||||
})
|
})
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
image, err := gui.getSelectedImage()
|
img, err := gui.Panels.Images.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||||
Image: image,
|
Image: img,
|
||||||
})
|
})
|
||||||
|
|
||||||
customCommands := gui.Config.UserConfig.CustomCommands.Images
|
customCommands := gui.Config.UserConfig.CustomCommands.Images
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,11 +17,6 @@ type Binding struct {
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayStrings returns the display string of a file
|
|
||||||
func (b *Binding) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
return []string{b.GetKey(), b.Description}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetKey is a function.
|
// GetKey is a function.
|
||||||
func (b *Binding) GetKey() string {
|
func (b *Binding) GetKey() string {
|
||||||
key := 0
|
key := 0
|
||||||
|
|
@ -53,12 +50,18 @@ func (b *Binding) GetKey() string {
|
||||||
return "PgDn"
|
return "PgDn"
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(key)
|
return fmt.Sprintf("%c", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInitialKeybindings is a function.
|
// GetInitialKeybindings is a function.
|
||||||
func (gui *Gui) GetInitialKeybindings() []*Binding {
|
func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
bindings := []*Binding{
|
bindings := []*Binding{
|
||||||
|
{
|
||||||
|
ViewName: "",
|
||||||
|
Key: gocui.KeyEsc,
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.escape),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: 'q',
|
Key: 'q',
|
||||||
|
|
@ -71,35 +74,29 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.quit,
|
Handler: gui.quit,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ViewName: "",
|
|
||||||
Key: gocui.KeyEsc,
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.quit,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyPgup,
|
Key: gocui.KeyPgup,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollUpMain,
|
Handler: wrappedHandler(gui.scrollUpMain),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyPgdn,
|
Key: gocui.KeyPgdn,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollDownMain,
|
Handler: wrappedHandler(gui.scrollDownMain),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyCtrlU,
|
Key: gocui.KeyCtrlU,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollUpMain,
|
Handler: wrappedHandler(gui.scrollUpMain),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: gocui.KeyCtrlD,
|
Key: gocui.KeyCtrlD,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollDownMain,
|
Handler: wrappedHandler(gui.scrollDownMain),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
|
|
@ -107,6 +104,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.autoScrollMain,
|
Handler: gui.autoScrollMain,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "",
|
||||||
|
Key: gocui.KeyHome,
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.jumpToTopMain,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: 'x',
|
Key: 'x',
|
||||||
|
|
@ -139,26 +142,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleOpenConfig,
|
Handler: gui.handleOpenConfig,
|
||||||
Description: gui.Tr.OpenConfig,
|
Description: gui.Tr.OpenConfig,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ViewName: "project",
|
|
||||||
Key: '[',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleProjectPrevContext,
|
|
||||||
Description: gui.Tr.PreviousContext,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ViewName: "project",
|
|
||||||
Key: ']',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleProjectNextContext,
|
|
||||||
Description: gui.Tr.NextContext,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ViewName: "project",
|
|
||||||
Key: gocui.MouseLeft,
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleProjectClick,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: "project",
|
ViewName: "project",
|
||||||
Key: 'm',
|
Key: 'm',
|
||||||
|
|
@ -166,23 +149,35 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleViewAllLogs,
|
Handler: gui.handleViewAllLogs,
|
||||||
Description: gui.Tr.ViewLogs,
|
Description: gui.Tr.ViewLogs,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ViewName: "project",
|
|
||||||
Key: gocui.MouseLeft,
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleProjectSelect,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: "menu",
|
ViewName: "menu",
|
||||||
Key: gocui.KeyEsc,
|
Key: gocui.KeyEsc,
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleMenuClose,
|
Handler: wrappedHandler(gui.handleMenuClose),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "menu",
|
ViewName: "menu",
|
||||||
Key: 'q',
|
Key: 'q',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleMenuClose,
|
Handler: wrappedHandler(gui.handleMenuClose),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "menu",
|
||||||
|
Key: ' ',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.handleMenuPress),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "menu",
|
||||||
|
Key: gocui.KeyEnter,
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.handleMenuPress),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "menu",
|
||||||
|
Key: 'y',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.handleMenuPress),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "information",
|
ViewName: "information",
|
||||||
|
|
@ -190,20 +185,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleDonate,
|
Handler: gui.handleDonate,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ViewName: "containers",
|
|
||||||
Key: '[',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleContainersPrevContext,
|
|
||||||
Description: gui.Tr.PreviousContext,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ViewName: "containers",
|
|
||||||
Key: ']',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleContainersNextContext,
|
|
||||||
Description: gui.Tr.NextContext,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: "containers",
|
ViewName: "containers",
|
||||||
Key: 'd',
|
Key: 'd',
|
||||||
|
|
@ -218,6 +199,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleHideStoppedContainers,
|
Handler: gui.handleHideStoppedContainers,
|
||||||
Description: gui.Tr.HideStopped,
|
Description: gui.Tr.HideStopped,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "containers",
|
||||||
|
Key: 'p',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleContainerPause,
|
||||||
|
Description: gui.Tr.Pause,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "containers",
|
ViewName: "containers",
|
||||||
Key: 's',
|
Key: 's',
|
||||||
|
|
@ -246,6 +234,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleContainerViewLogs,
|
Handler: gui.handleContainerViewLogs,
|
||||||
Description: gui.Tr.ViewLogs,
|
Description: gui.Tr.ViewLogs,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "containers",
|
||||||
|
Key: 'E',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleContainersExecShell,
|
||||||
|
Description: gui.Tr.ExecShell,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "containers",
|
ViewName: "containers",
|
||||||
Key: 'c',
|
Key: 'c',
|
||||||
|
|
@ -260,6 +255,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleContainersBulkCommand,
|
Handler: gui.handleContainersBulkCommand,
|
||||||
Description: gui.Tr.ViewBulkCommands,
|
Description: gui.Tr.ViewBulkCommands,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "containers",
|
||||||
|
Key: 'w',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleContainersOpenInBrowserCommand,
|
||||||
|
Description: gui.Tr.OpenInBrowser,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "services",
|
||||||
|
Key: 'u',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleServiceUp,
|
||||||
|
Description: gui.Tr.UpService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
Key: 'd',
|
Key: 'd',
|
||||||
|
|
@ -274,6 +283,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleServiceStop,
|
Handler: gui.handleServiceStop,
|
||||||
Description: gui.Tr.Stop,
|
Description: gui.Tr.Stop,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "services",
|
||||||
|
Key: 'p',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleServicePause,
|
||||||
|
Description: gui.Tr.Pause,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
Key: 'r',
|
Key: 'r',
|
||||||
|
|
@ -281,6 +297,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleServiceRestart,
|
Handler: gui.handleServiceRestart,
|
||||||
Description: gui.Tr.Restart,
|
Description: gui.Tr.Restart,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "services",
|
||||||
|
Key: 'S',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleServiceStart,
|
||||||
|
Description: gui.Tr.Start,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
Key: 'a',
|
Key: 'a',
|
||||||
|
|
@ -292,22 +315,22 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
Key: 'm',
|
Key: 'm',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleServiceViewLogs,
|
Handler: gui.handleServiceRenderLogsToMain,
|
||||||
Description: gui.Tr.ViewLogs,
|
Description: gui.Tr.ViewLogs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
Key: '[',
|
Key: 'U',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleServicesPrevContext,
|
Handler: gui.handleProjectUp,
|
||||||
Description: gui.Tr.PreviousContext,
|
Description: gui.Tr.UpProject,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
Key: ']',
|
Key: 'D',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleServicesNextContext,
|
Handler: gui.handleProjectDown,
|
||||||
Description: gui.Tr.NextContext,
|
Description: gui.Tr.DownProject,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "services",
|
ViewName: "services",
|
||||||
|
|
@ -331,18 +354,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Description: gui.Tr.ViewBulkCommands,
|
Description: gui.Tr.ViewBulkCommands,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "images",
|
ViewName: "services",
|
||||||
Key: '[',
|
Key: 'E',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleImagesPrevContext,
|
Handler: gui.handleServicesExecShell,
|
||||||
Description: gui.Tr.PreviousContext,
|
Description: gui.Tr.ExecShell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "images",
|
ViewName: "services",
|
||||||
Key: ']',
|
Key: 'w',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.handleImagesNextContext,
|
Handler: gui.handleServicesOpenInBrowserCommand,
|
||||||
Description: gui.Tr.NextContext,
|
Description: gui.Tr.OpenInBrowser,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "images",
|
ViewName: "images",
|
||||||
|
|
@ -365,27 +388,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleImagesBulkCommand,
|
Handler: gui.handleImagesBulkCommand,
|
||||||
Description: gui.Tr.ViewBulkCommands,
|
Description: gui.Tr.ViewBulkCommands,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ViewName: "images",
|
|
||||||
Key: 'c',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleImagesCustomCommand,
|
|
||||||
Description: gui.Tr.RunCustomCommand,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ViewName: "volumes",
|
|
||||||
Key: '[',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleVolumesPrevContext,
|
|
||||||
Description: gui.Tr.PreviousContext,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ViewName: "volumes",
|
|
||||||
Key: ']',
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: gui.handleVolumesNextContext,
|
|
||||||
Description: gui.Tr.NextContext,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: "volumes",
|
ViewName: "volumes",
|
||||||
Key: 'c',
|
Key: 'c',
|
||||||
|
|
@ -407,6 +409,27 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Handler: gui.handleVolumesBulkCommand,
|
Handler: gui.handleVolumesBulkCommand,
|
||||||
Description: gui.Tr.ViewBulkCommands,
|
Description: gui.Tr.ViewBulkCommands,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "networks",
|
||||||
|
Key: 'c',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleNetworksCustomCommand,
|
||||||
|
Description: gui.Tr.RunCustomCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "networks",
|
||||||
|
Key: 'd',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleNetworksRemoveMenu,
|
||||||
|
Description: gui.Tr.RemoveNetwork,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "networks",
|
||||||
|
Key: 'b',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleNetworksBulkCommand,
|
||||||
|
Description: gui.Tr.ViewBulkCommands,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "main",
|
ViewName: "main",
|
||||||
Key: gocui.KeyEsc,
|
Key: gocui.KeyEsc,
|
||||||
|
|
@ -438,17 +461,29 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollRightMain,
|
Handler: gui.scrollRightMain,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "filter",
|
||||||
|
Key: gocui.KeyEnter,
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.commitFilter),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "filter",
|
||||||
|
Key: gocui.KeyEsc,
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.escapeFilterPrompt),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: 'J',
|
Key: 'J',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollDownMain,
|
Handler: wrappedHandler(gui.scrollDownMain),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
Key: 'K',
|
Key: 'K',
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollUpMain,
|
Handler: wrappedHandler(gui.scrollUpMain),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ViewName: "",
|
ViewName: "",
|
||||||
|
|
@ -462,51 +497,94 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.scrollRightMain,
|
Handler: gui.scrollRightMain,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ViewName: "",
|
||||||
|
Key: '+',
|
||||||
|
Handler: wrappedHandler(gui.nextScreenMode),
|
||||||
|
Description: gui.Tr.LcNextScreenMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ViewName: "",
|
||||||
|
Key: '_',
|
||||||
|
Handler: wrappedHandler(gui.prevScreenMode),
|
||||||
|
Description: gui.Tr.LcPrevScreenMode,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add more views here
|
for _, panel := range gui.allSidePanels() {
|
||||||
for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "menu"} {
|
|
||||||
bindings = append(bindings, []*Binding{
|
bindings = append(bindings, []*Binding{
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
|
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
|
{ViewName: panel.GetView().Name(), Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||||
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
|
{ViewName: panel.GetView().Name(), Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||||
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
|
{ViewName: panel.GetView().Name(), Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||||
|
{ViewName: panel.GetView().Name(), Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||||
|
{ViewName: panel.GetView().Name(), Key: gocui.KeyBacktab, Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||||
}...)
|
}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
panelMap := map[string]struct {
|
setUpDownClickBindings := func(viewName string, onUp func() error, onDown func() error, onClick func() error) {
|
||||||
onKeyUpPress func(*gocui.Gui, *gocui.View) error
|
|
||||||
onKeyDownPress func(*gocui.Gui, *gocui.View) error
|
|
||||||
onClick func(*gocui.Gui, *gocui.View) error
|
|
||||||
}{
|
|
||||||
"menu": {onKeyUpPress: gui.handleMenuPrevLine, onKeyDownPress: gui.handleMenuNextLine, onClick: gui.handleMenuClick},
|
|
||||||
"services": {onKeyUpPress: gui.handleServicesPrevLine, onKeyDownPress: gui.handleServicesNextLine, onClick: gui.handleServicesClick},
|
|
||||||
"containers": {onKeyUpPress: gui.handleContainersPrevLine, onKeyDownPress: gui.handleContainersNextLine, onClick: gui.handleContainersClick},
|
|
||||||
"images": {onKeyUpPress: gui.handleImagesPrevLine, onKeyDownPress: gui.handleImagesNextLine, onClick: gui.handleImagesClick},
|
|
||||||
"volumes": {onKeyUpPress: gui.handleVolumesPrevLine, onKeyDownPress: gui.handleVolumesNextLine, onClick: gui.handleVolumesClick},
|
|
||||||
"main": {onKeyUpPress: gui.scrollUpMain, onKeyDownPress: gui.scrollDownMain, onClick: gui.handleMainClick},
|
|
||||||
}
|
|
||||||
|
|
||||||
for viewName, functions := range panelMap {
|
|
||||||
bindings = append(bindings, []*Binding{
|
bindings = append(bindings, []*Binding{
|
||||||
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
|
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
|
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)},
|
||||||
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: functions.onKeyUpPress},
|
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: wrappedHandler(onUp)},
|
||||||
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
|
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)},
|
||||||
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
|
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)},
|
||||||
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.onKeyDownPress},
|
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: wrappedHandler(onDown)},
|
||||||
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.onClick},
|
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: wrappedHandler(onClick)},
|
||||||
}...)
|
}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, viewName := range []string{"project", "services", "containers", "images", "volumes"} {
|
bindings = append(bindings, []*Binding{
|
||||||
bindings = append(bindings, &Binding{
|
{Handler: gui.handleGoTo(gui.Panels.Projects.View), Key: '1', Description: gui.Tr.FocusProjects},
|
||||||
ViewName: viewName,
|
{Handler: gui.handleGoTo(gui.Panels.Services.View), Key: '2', Description: gui.Tr.FocusServices},
|
||||||
Key: gocui.KeyEnter,
|
{Handler: gui.handleGoTo(gui.Panels.Containers.View), Key: '3', Description: gui.Tr.FocusContainers},
|
||||||
Modifier: gocui.ModNone,
|
{Handler: gui.handleGoTo(gui.Panels.Images.View), Key: '4', Description: gui.Tr.FocusImages},
|
||||||
Handler: gui.handleEnterMain,
|
{Handler: gui.handleGoTo(gui.Panels.Volumes.View), Key: '5', Description: gui.Tr.FocusVolumes},
|
||||||
Description: gui.Tr.FocusMain,
|
{Handler: gui.handleGoTo(gui.Panels.Networks.View), Key: '6', Description: gui.Tr.FocusNetworks},
|
||||||
})
|
}...)
|
||||||
|
|
||||||
|
for _, panel := range gui.allListPanels() {
|
||||||
|
setUpDownClickBindings(panel.GetView().Name(), panel.HandlePrevLine, panel.HandleNextLine, panel.HandleClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpDownClickBindings("main", gui.scrollUpMain, gui.scrollDownMain, gui.handleMainClick)
|
||||||
|
|
||||||
|
for _, panel := range gui.allSidePanels() {
|
||||||
|
bindings = append(bindings,
|
||||||
|
&Binding{
|
||||||
|
ViewName: panel.GetView().Name(),
|
||||||
|
Key: gocui.KeyEnter,
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: gui.handleEnterMain,
|
||||||
|
Description: gui.Tr.FocusMain,
|
||||||
|
},
|
||||||
|
&Binding{
|
||||||
|
ViewName: panel.GetView().Name(),
|
||||||
|
Key: '[',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(panel.HandlePrevMainTab),
|
||||||
|
Description: gui.Tr.PreviousContext,
|
||||||
|
},
|
||||||
|
&Binding{
|
||||||
|
ViewName: panel.GetView().Name(),
|
||||||
|
Key: ']',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(panel.HandleNextMainTab),
|
||||||
|
Description: gui.Tr.NextContext,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, panel := range gui.allListPanels() {
|
||||||
|
if !panel.IsFilterDisabled() {
|
||||||
|
bindings = append(bindings, &Binding{
|
||||||
|
ViewName: panel.GetView().Name(),
|
||||||
|
Key: '/',
|
||||||
|
Modifier: gocui.ModNone,
|
||||||
|
Handler: wrappedHandler(gui.handleOpenFilter),
|
||||||
|
Description: gui.Tr.LcFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
|
|
@ -527,3 +605,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error {
|
||||||
|
return func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
|
||||||
|
|
||||||
// getFocusLayout returns a manager function for when view gain and lose focus
|
// getFocusLayout returns a manager function for when view gain and lose focus
|
||||||
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||||
var previousView *gocui.View
|
var previousView *gocui.View
|
||||||
|
|
@ -16,12 +16,8 @@ func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||||
}
|
}
|
||||||
// for now we don't consider losing focus to a popup panel as actually losing focus
|
// for now we don't consider losing focus to a popup panel as actually losing focus
|
||||||
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
||||||
if err := gui.onFocusLost(previousView, newView); err != nil {
|
gui.onFocusLost(previousView, newView)
|
||||||
return err
|
gui.onFocus(newView)
|
||||||
}
|
|
||||||
if err := gui.onFocus(newView); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
previousView = newView
|
previousView = newView
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -36,9 +32,9 @@ func (gui *Gui) onFocusChange() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
|
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gui.isPopupPanel(newView.Name()) {
|
if !gui.isPopupPanel(newView.Name()) {
|
||||||
|
|
@ -46,25 +42,19 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// refocusing because in responsive mode (when the window is very short) we want to ensure that after the view size changes we can still see the last selected item
|
// refocusing because in responsive mode (when the window is very short) we want to ensure that after the view size changes we can still see the last selected item
|
||||||
if err := gui.focusPointInView(v); err != nil {
|
gui.focusPointInView(v)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.Log.Info(v.Name() + " focus lost")
|
gui.Log.Info(v.Name() + " focus lost")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) onFocus(v *gocui.View) error {
|
func (gui *Gui) onFocus(v *gocui.View) {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gui.focusPointInView(v); err != nil {
|
gui.focusPointInView(v)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.Log.Info(v.Name() + " focus gained")
|
gui.Log.Info(v.Name() + " focus gained")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// layout is called for every screen re-render e.g. when the screen is resized
|
// layout is called for every screen re-render e.g. when the screen is resized
|
||||||
|
|
@ -72,217 +62,48 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||||
g.Highlight = true
|
g.Highlight = true
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
|
|
||||||
information := "lazydocker " + gui.Config.Version
|
|
||||||
if gui.g.Mouse {
|
|
||||||
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
|
|
||||||
information = donate + " " + information
|
|
||||||
}
|
|
||||||
|
|
||||||
minimumHeight := 9
|
|
||||||
minimumWidth := 10
|
|
||||||
if height < minimumHeight || width < minimumWidth {
|
|
||||||
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Title = gui.Tr.NotEnoughSpace
|
|
||||||
v.Wrap = true
|
|
||||||
_, _ = g.SetViewOnTop("limit")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
currView := gui.g.CurrentView()
|
|
||||||
currentCyclebleView := gui.peekPreviousView()
|
|
||||||
if currView != nil {
|
|
||||||
viewName := currView.Name()
|
|
||||||
usePreviouseView := true
|
|
||||||
for _, view := range gui.CyclableViews {
|
|
||||||
if view == viewName {
|
|
||||||
currentCyclebleView = viewName
|
|
||||||
usePreviouseView = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if usePreviouseView {
|
|
||||||
currentCyclebleView = gui.peekPreviousView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usableSpace := height - 4
|
|
||||||
|
|
||||||
tallPanels := 3
|
|
||||||
var vHeights map[string]int
|
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
|
||||||
tallPanels++
|
|
||||||
vHeights = map[string]int{
|
|
||||||
"project": 3,
|
|
||||||
"services": usableSpace/tallPanels + usableSpace%tallPanels,
|
|
||||||
"containers": usableSpace / tallPanels,
|
|
||||||
"images": usableSpace / tallPanels,
|
|
||||||
"volumes": usableSpace / tallPanels,
|
|
||||||
"options": 1,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vHeights = map[string]int{
|
|
||||||
"project": 3,
|
|
||||||
"containers": usableSpace/tallPanels + usableSpace%tallPanels,
|
|
||||||
"images": usableSpace / tallPanels,
|
|
||||||
"volumes": usableSpace / tallPanels,
|
|
||||||
"options": 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if height < 28 {
|
|
||||||
defaultHeight := 3
|
|
||||||
if height < 21 {
|
|
||||||
defaultHeight = 1
|
|
||||||
}
|
|
||||||
vHeights = map[string]int{
|
|
||||||
"project": defaultHeight,
|
|
||||||
"containers": defaultHeight,
|
|
||||||
"images": defaultHeight,
|
|
||||||
"volumes": defaultHeight,
|
|
||||||
"options": defaultHeight,
|
|
||||||
}
|
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
|
||||||
vHeights["services"] = defaultHeight
|
|
||||||
}
|
|
||||||
vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
|
|
||||||
leftSideWidth := width / 3
|
|
||||||
|
|
||||||
appStatus := gui.statusManager.getStatusString()
|
appStatus := gui.statusManager.getStatusString()
|
||||||
appStatusOptionsBoundary := 0
|
|
||||||
if appStatus != "" {
|
|
||||||
appStatusOptionsBoundary = len(appStatus) + 2
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = g.SetViewOnBottom("limit")
|
viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus)
|
||||||
g.DeleteView("limit")
|
// we assume that the view has already been created.
|
||||||
|
setViewFromDimensions := func(viewName string, windowName string) (*gocui.View, error) {
|
||||||
|
dimensionsObj, ok := viewDimensions[windowName]
|
||||||
|
|
||||||
v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT)
|
view, err := g.View(viewName)
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
|
||||||
v.FgColor = gocui.ColorDefault
|
|
||||||
|
|
||||||
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
|
|
||||||
v.IgnoreCarriageReturns = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := g.SetView("project", 0, 0, leftSideWidth, vHeights["project"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Title = gui.Tr.ProjectTitle
|
|
||||||
v.FgColor = gocui.ColorDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
var servicesView *gocui.View
|
|
||||||
aboveContainersView := "project"
|
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
|
||||||
aboveContainersView = "services"
|
|
||||||
servicesView, err = g.SetViewBeneath("services", "project", vHeights["services"])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() != "unknown view" {
|
return nil, err
|
||||||
return err
|
|
||||||
}
|
|
||||||
servicesView.Highlight = true
|
|
||||||
servicesView.Title = gui.Tr.ServicesTitle
|
|
||||||
servicesView.FgColor = gocui.ColorDefault
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
// view not specified in dimensions object: so create the view and hide it
|
||||||
|
// making the view take up the whole space in the background in case it needs
|
||||||
|
// to render content as soon as it appears, because lazyloaded content (via a pty task)
|
||||||
|
// cares about the size of the view.
|
||||||
|
_, err := g.SetView(viewName, 0, 0, width, height, 0)
|
||||||
|
view.Visible = false
|
||||||
|
return view, err
|
||||||
|
}
|
||||||
|
|
||||||
|
frameOffset := 1
|
||||||
|
if view.Frame {
|
||||||
|
frameOffset = 0
|
||||||
|
}
|
||||||
|
_, err = g.SetView(
|
||||||
|
viewName,
|
||||||
|
dimensionsObj.X0-frameOffset,
|
||||||
|
dimensionsObj.Y0-frameOffset,
|
||||||
|
dimensionsObj.X1+frameOffset,
|
||||||
|
dimensionsObj.Y1+frameOffset,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
view.Visible = true
|
||||||
|
|
||||||
|
return view, err
|
||||||
}
|
}
|
||||||
|
|
||||||
containersView, err := g.SetViewBeneath("containers", aboveContainersView, vHeights["containers"])
|
for _, viewName := range gui.autoPositionedViewNames() {
|
||||||
if err != nil {
|
_, err := setViewFromDimensions(viewName, viewName)
|
||||||
if err.Error() != "unknown view" {
|
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||||
return err
|
|
||||||
}
|
|
||||||
containersView.Highlight = true
|
|
||||||
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
|
|
||||||
containersView.Title = gui.Tr.ContainersTitle
|
|
||||||
} else {
|
|
||||||
containersView.Title = gui.Tr.StandaloneContainersTitle
|
|
||||||
}
|
|
||||||
containersView.FgColor = gocui.ColorDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"])
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
imagesView.Highlight = true
|
|
||||||
imagesView.Title = gui.Tr.ImagesTitle
|
|
||||||
imagesView.FgColor = gocui.ColorDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"])
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
volumesView.Highlight = true
|
|
||||||
volumesView.Title = gui.Tr.VolumesTitle
|
|
||||||
volumesView.FgColor = gocui.ColorDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Frame = false
|
|
||||||
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
appStatusView.BgColor = gocui.ColorDefault
|
|
||||||
appStatusView.FgColor = gocui.ColorCyan
|
|
||||||
appStatusView.Frame = false
|
|
||||||
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
|
|
||||||
if err.Error() != "unknown view" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.BgColor = gocui.ColorDefault
|
|
||||||
v.FgColor = gocui.ColorGreen
|
|
||||||
v.Frame = false
|
|
||||||
if err := gui.renderString(g, "information", information); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// doing this here because it'll only happen once
|
|
||||||
if err := gui.loadNewDirectory(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if gui.g.CurrentView() == nil {
|
|
||||||
v, err := gui.g.View(gui.peekPreviousView())
|
|
||||||
if err != nil {
|
|
||||||
viewName := gui.initiallyFocusedViewName()
|
|
||||||
v, err = gui.g.View(viewName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gui.switchFocus(gui.g, nil, v, false); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,29 +115,21 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||||
return gui.resizeCurrentPopupPanel(g)
|
return gui.resizeCurrentPopupPanel(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
type listViewState struct {
|
func (gui *Gui) focusPointInView(view *gocui.View) {
|
||||||
selectedLine int
|
|
||||||
lineCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) focusPointInView(view *gocui.View) error {
|
|
||||||
if view == nil {
|
if view == nil {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
listViews := map[string]listViewState{
|
for _, panel := range gui.allListPanels() {
|
||||||
"containers": {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.DisplayContainers)},
|
if panel.GetView() == view {
|
||||||
"images": {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.DockerCommand.Images)},
|
panel.Refocus()
|
||||||
"volumes": {selectedLine: gui.State.Panels.Volumes.SelectedLine, lineCount: len(gui.DockerCommand.Volumes)},
|
return
|
||||||
"services": {selectedLine: gui.State.Panels.Services.SelectedLine, lineCount: len(gui.DockerCommand.Services)},
|
|
||||||
"menu": {selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount},
|
|
||||||
}
|
|
||||||
|
|
||||||
if state, ok := listViews[view.Name()]; ok {
|
|
||||||
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
|
||||||
|
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
|
||||||
|
// arbitrarily giving the view enough size so that we don't get an error, but
|
||||||
|
// it's expected that the view will be given the correct size before being shown
|
||||||
|
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,16 @@ import (
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) scrollUpMain() error {
|
||||||
mainView := gui.getMainView()
|
mainView := gui.Views.Main
|
||||||
mainView.Autoscroll = false
|
mainView.Autoscroll = false
|
||||||
ox, oy := mainView.Origin()
|
ox, oy := mainView.Origin()
|
||||||
newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight)))
|
newOy := int(math.Max(0, float64(oy-gui.Config.UserConfig.Gui.ScrollHeight)))
|
||||||
return mainView.SetOrigin(ox, newOy)
|
return mainView.SetOrigin(ox, newOy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) scrollDownMain() error {
|
||||||
mainView := gui.getMainView()
|
mainView := gui.Views.Main
|
||||||
mainView.Autoscroll = false
|
mainView.Autoscroll = false
|
||||||
ox, oy := mainView.Origin()
|
ox, oy := mainView.Origin()
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
mainView := gui.getMainView()
|
mainView := gui.Views.Main
|
||||||
ox, oy := mainView.Origin()
|
ox, oy := mainView.Origin()
|
||||||
newOx := int(math.Max(0, float64(ox-gui.Config.UserConfig.Gui.ScrollHeight)))
|
newOx := int(math.Max(0, float64(ox-gui.Config.UserConfig.Gui.ScrollHeight)))
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ func (gui *Gui) scrollLeftMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
mainView := gui.getMainView()
|
mainView := gui.Views.Main
|
||||||
ox, oy := mainView.Origin()
|
ox, oy := mainView.Origin()
|
||||||
|
|
||||||
content := mainView.ViewBufferLines()
|
content := mainView.ViewBufferLines()
|
||||||
|
|
@ -62,65 +62,52 @@ func (gui *Gui) scrollRightMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) autoScrollMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
gui.getMainView().Autoscroll = true
|
gui.Views.Main.Autoscroll = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) jumpToTopMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.Views.Main.Autoscroll = false
|
||||||
|
_ = gui.Views.Main.SetOrigin(0, 0)
|
||||||
|
_ = gui.Views.Main.SetCursor(0, 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) onMainTabClick(tabIndex int) error {
|
func (gui *Gui) onMainTabClick(tabIndex int) error {
|
||||||
gui.Log.Warn(tabIndex)
|
gui.Log.Warn(tabIndex)
|
||||||
|
|
||||||
viewName := gui.currentViewName()
|
currentSidePanel, ok := gui.currentSidePanel()
|
||||||
|
|
||||||
mainView := gui.getMainView()
|
if !ok {
|
||||||
if viewName == "main" && mainView.ParentView != nil {
|
return nil
|
||||||
viewName = mainView.ParentView.Name()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch viewName {
|
currentSidePanel.SetMainTabIndex(tabIndex)
|
||||||
case "project":
|
return currentSidePanel.HandleSelect()
|
||||||
gui.State.Panels.Project.ContextIndex = tabIndex
|
|
||||||
return gui.handleProjectSelect(gui.g, gui.getProjectView())
|
|
||||||
case "services":
|
|
||||||
gui.State.Panels.Services.ContextIndex = tabIndex
|
|
||||||
return gui.handleServiceSelect(gui.g, gui.getServicesView())
|
|
||||||
case "containers":
|
|
||||||
gui.State.Panels.Containers.ContextIndex = tabIndex
|
|
||||||
return gui.handleContainerSelect(gui.g, gui.getContainersView())
|
|
||||||
case "images":
|
|
||||||
gui.State.Panels.Images.ContextIndex = tabIndex
|
|
||||||
return gui.handleImageSelect(gui.g, gui.getImagesView())
|
|
||||||
case "volumes":
|
|
||||||
gui.State.Panels.Volumes.ContextIndex = tabIndex
|
|
||||||
return gui.handleVolumeSelect(gui.g, gui.getVolumesView())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
mainView := gui.getMainView()
|
mainView := gui.Views.Main
|
||||||
mainView.ParentView = v
|
mainView.ParentView = v
|
||||||
|
|
||||||
return gui.switchFocus(gui.g, v, mainView, false)
|
return gui.switchFocus(mainView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
v.ParentView = nil
|
v.ParentView = nil
|
||||||
return gui.returnFocus(gui.g, v)
|
return gui.returnFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleMainClick() error {
|
||||||
if gui.popupPanelFocused() {
|
if gui.popupPanelFocused() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
currentView := gui.g.CurrentView()
|
currentView := gui.g.CurrentView()
|
||||||
|
|
||||||
if currentView != nil && currentView.Name() == "main" {
|
if currentView.Name() != "main" {
|
||||||
currentView = nil
|
gui.Views.Main.ParentView = currentView
|
||||||
} else {
|
|
||||||
v.ParentView = currentView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.switchFocus(gui.g, currentView, v, false)
|
return gui.switchFocus(gui.Views.Main)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,133 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// list panel functions
|
type CreateMenuOptions struct {
|
||||||
|
Title string
|
||||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
Items []*types.MenuItem
|
||||||
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
|
HideCancel bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) getMenuPanel() *panels.SideListPanel[*types.MenuItem] {
|
||||||
panelState := gui.State.Panels.Menu
|
return &panels.SideListPanel[*types.MenuItem]{
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
|
ListPanel: panels.ListPanel[*types.MenuItem]{
|
||||||
|
List: panels.NewFilteredList[*types.MenuItem](),
|
||||||
return gui.handleMenuSelect(g, v)
|
View: gui.Views.Menu,
|
||||||
|
},
|
||||||
|
NoItemsMessage: "",
|
||||||
|
Gui: gui.intoInterface(),
|
||||||
|
OnClick: gui.onMenuPress,
|
||||||
|
Sort: nil,
|
||||||
|
GetTableCells: presentation.GetMenuItemDisplayStrings,
|
||||||
|
OnRerender: func() error {
|
||||||
|
return gui.resizePopupPanel(gui.Views.Menu)
|
||||||
|
},
|
||||||
|
// so that we can avoid some UI trickiness, the menu will not have filtering
|
||||||
|
// abillity yet. To support it, we would need to have filter state against
|
||||||
|
// each panel (e.g. for when you filter the images panel, then bring up
|
||||||
|
// the options menu, then try to filter that too.
|
||||||
|
DisableFilter: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) onMenuPress(menuItem *types.MenuItem) error {
|
||||||
itemCount := gui.State.MenuItemCount
|
if err := gui.handleMenuClose(); err != nil {
|
||||||
handleSelect := gui.handleMenuSelect
|
|
||||||
selectedLine := &gui.State.Panels.Menu.SelectedLine
|
|
||||||
|
|
||||||
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.State.Panels.Menu.OnPress(g, v)
|
if menuItem.OnPress != nil {
|
||||||
|
return menuItem.OnPress()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleMenuPress() error {
|
||||||
panelState := gui.State.Panels.Menu
|
selectedMenuItem, err := gui.Panels.Menu.GetSelectedItem()
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return gui.handleMenuSelect(g, v)
|
return gui.onMenuPress(selectedMenuItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) Menu(opts CreateMenuOptions) error {
|
||||||
|
if !opts.HideCancel {
|
||||||
|
// this is mutative but I'm okay with that for now
|
||||||
|
opts.Items = append(opts.Items, &types.MenuItem{
|
||||||
|
LabelColumns: []string{gui.Tr.Cancel},
|
||||||
|
OnPress: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
maxColumnSize := 1
|
||||||
|
|
||||||
|
for _, item := range opts.Items {
|
||||||
|
if item.LabelColumns == nil {
|
||||||
|
item.LabelColumns = []string{item.Label}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.OpensMenu {
|
||||||
|
item.LabelColumns[0] = utils.OpensMenuStyle(item.LabelColumns[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range opts.Items {
|
||||||
|
if len(item.LabelColumns) < maxColumnSize {
|
||||||
|
// we require that each item has the same number of columns so we're padding out with blank strings
|
||||||
|
// if this item has too few
|
||||||
|
item.LabelColumns = append(item.LabelColumns, make([]string, maxColumnSize-len(item.LabelColumns))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.Panels.Menu.SetItems(opts.Items)
|
||||||
|
gui.Panels.Menu.SetSelectedLineIdx(0)
|
||||||
|
|
||||||
|
if err := gui.Panels.Menu.RerenderList(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.Views.Menu.Title = opts.Title
|
||||||
|
gui.Views.Menu.Visible = true
|
||||||
|
|
||||||
|
return gui.switchFocus(gui.Views.Menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// specific functions
|
// specific functions
|
||||||
|
|
||||||
func (gui *Gui) renderMenuOptions() error {
|
func (gui *Gui) renderMenuOptions() error {
|
||||||
optionsMap := map[string]string{
|
optionsMap := map[string]string{
|
||||||
"esc/q": gui.Tr.Close,
|
"esc": gui.Tr.Close,
|
||||||
"↑ ↓": gui.Tr.Navigate,
|
"↑ ↓": gui.Tr.Navigate,
|
||||||
"enter": gui.Tr.Execute,
|
"enter": gui.Tr.Execute,
|
||||||
}
|
}
|
||||||
return gui.renderOptionsMap(optionsMap)
|
return gui.renderOptionsMap(optionsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleMenuClose() error {
|
||||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
gui.Views.Menu.Visible = false
|
||||||
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
|
|
||||||
|
// this code is here for when we do add filter ability to the menu panel,
|
||||||
|
// though it's currently disabled
|
||||||
|
if gui.State.Filter.panel == gui.Panels.Menu {
|
||||||
|
if err := gui.clearFilter(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we need to remove the view from the view stack because we're about to
|
||||||
|
// return focus and don't want to land in the search view when it was searching
|
||||||
|
// the menu in the first place
|
||||||
|
gui.removeViewFromStack(gui.Views.Filter)
|
||||||
}
|
}
|
||||||
err := g.DeleteView("menu")
|
|
||||||
if err != nil {
|
return gui.returnFocus()
|
||||||
return err
|
|
||||||
}
|
|
||||||
return gui.returnFocus(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
|
|
||||||
isFocused := gui.g.CurrentView().Name() == "menu"
|
|
||||||
gui.State.MenuItemCount = itemCount
|
|
||||||
list, err := utils.RenderList(items, utils.IsFocused(isFocused))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
|
|
||||||
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
|
||||||
menuView.Title = title
|
|
||||||
menuView.FgColor = gocui.ColorWhite
|
|
||||||
menuView.Clear()
|
|
||||||
fmt.Fprint(menuView, list)
|
|
||||||
gui.State.Panels.Menu.SelectedLine = 0
|
|
||||||
|
|
||||||
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
selectedLine := gui.State.Panels.Menu.SelectedLine
|
|
||||||
if err := handlePress(selectedLine); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := gui.g.View("menu"); err == nil {
|
|
||||||
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.returnFocus(gui.g, menuView)
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.State.Panels.Menu.OnPress = wrappedHandlePress
|
|
||||||
|
|
||||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
|
||||||
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
|
|
||||||
|
|
||||||
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
|
||||||
if _, err := gui.g.View("menu"); err == nil {
|
|
||||||
if _, err := g.SetViewOnTop("menu"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentView := gui.g.CurrentView()
|
|
||||||
return gui.switchFocus(gui.g, currentView, menuView, false)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
179
pkg/gui/networks_panel.go
Normal file
179
pkg/gui/networks_panel.go
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) getNetworksPanel() *panels.SideListPanel[*commands.Network] {
|
||||||
|
return &panels.SideListPanel[*commands.Network]{
|
||||||
|
ContextState: &panels.ContextState[*commands.Network]{
|
||||||
|
GetMainTabs: func() []panels.MainTab[*commands.Network] {
|
||||||
|
return []panels.MainTab[*commands.Network]{
|
||||||
|
{
|
||||||
|
Key: "config",
|
||||||
|
Title: gui.Tr.ConfigTitle,
|
||||||
|
Render: gui.renderNetworkConfig,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GetItemContextCacheKey: func(network *commands.Network) string {
|
||||||
|
return "networks-" + network.Name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ListPanel: panels.ListPanel[*commands.Network]{
|
||||||
|
List: panels.NewFilteredList[*commands.Network](),
|
||||||
|
View: gui.Views.Networks,
|
||||||
|
},
|
||||||
|
NoItemsMessage: gui.Tr.NoNetworks,
|
||||||
|
Gui: gui.intoInterface(),
|
||||||
|
// we're sorting these networks based on whether they have labels defined,
|
||||||
|
// because those are the ones you typically care about.
|
||||||
|
// Within that, we also sort them alphabetically
|
||||||
|
Sort: func(a *commands.Network, b *commands.Network) bool {
|
||||||
|
return a.Name < b.Name
|
||||||
|
},
|
||||||
|
GetTableCells: presentation.GetNetworkDisplayStrings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderNetworkConfig(network *commands.Network) tasks.TaskFunc {
|
||||||
|
return gui.NewSimpleRenderStringTask(func() string { return gui.networkConfigStr(network) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) networkConfigStr(network *commands.Network) string {
|
||||||
|
padding := 15
|
||||||
|
output := ""
|
||||||
|
output += utils.WithPadding("ID: ", padding) + network.Network.ID + "\n"
|
||||||
|
output += utils.WithPadding("Name: ", padding) + network.Name + "\n"
|
||||||
|
output += utils.WithPadding("Driver: ", padding) + network.Network.Driver + "\n"
|
||||||
|
output += utils.WithPadding("Scope: ", padding) + network.Network.Scope + "\n"
|
||||||
|
output += utils.WithPadding("EnabledIPV6: ", padding) + strconv.FormatBool(network.Network.EnableIPv6) + "\n"
|
||||||
|
output += utils.WithPadding("Internal: ", padding) + strconv.FormatBool(network.Network.Internal) + "\n"
|
||||||
|
output += utils.WithPadding("Attachable: ", padding) + strconv.FormatBool(network.Network.Attachable) + "\n"
|
||||||
|
output += utils.WithPadding("Ingress: ", padding) + strconv.FormatBool(network.Network.Ingress) + "\n"
|
||||||
|
|
||||||
|
output += utils.WithPadding("Containers: ", padding)
|
||||||
|
if len(network.Network.Containers) > 0 {
|
||||||
|
output += "\n"
|
||||||
|
for _, v := range network.Network.Containers {
|
||||||
|
output += utils.FormatMapItem(padding, v.Name, v.EndpointID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output += "none\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
output += "\n"
|
||||||
|
output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, network.Network.Labels) + "\n"
|
||||||
|
output += utils.WithPadding("Options: ", padding) + utils.FormatMap(padding, network.Network.Options)
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) reloadNetworks() error {
|
||||||
|
if err := gui.refreshStateNetworks(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.Panels.Networks.RerenderList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refreshStateNetworks() error {
|
||||||
|
networks, err := gui.DockerCommand.RefreshNetworks()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.Panels.Networks.SetItems(networks)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNetworksRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
network, err := gui.Panels.Networks.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type removeNetworkOption struct {
|
||||||
|
description string
|
||||||
|
command string
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []*removeNetworkOption{
|
||||||
|
{
|
||||||
|
description: gui.Tr.Remove,
|
||||||
|
command: utils.WithShortSha("docker network rm " + network.Name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems := lo.Map(options, func(option *removeNetworkOption, _ int) *types.MenuItem {
|
||||||
|
return &types.MenuItem{
|
||||||
|
LabelColumns: []string{option.description, color.New(color.FgRed).Sprint(option.command)},
|
||||||
|
OnPress: func() error {
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||||
|
if err := network.Remove(); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return gui.Menu(CreateMenuOptions{
|
||||||
|
Title: "",
|
||||||
|
Items: menuItems,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handlePruneNetworks() error {
|
||||||
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneNetworks, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
|
||||||
|
err := gui.DockerCommand.PruneNetworks()
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNetworksCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
network, err := gui.Panels.Networks.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
|
||||||
|
Network: network,
|
||||||
|
})
|
||||||
|
|
||||||
|
customCommands := gui.Config.UserConfig.CustomCommands.Networks
|
||||||
|
|
||||||
|
return gui.createCustomCommandMenu(customCommands, commandObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNetworksBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
baseBulkCommands := []config.CustomCommand{
|
||||||
|
{
|
||||||
|
Name: gui.Tr.PruneNetworks,
|
||||||
|
InternalFunction: gui.handlePruneNetworks,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Networks...)
|
||||||
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
||||||
|
|
||||||
|
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||||
var (
|
var bindingsGlobal, bindingsPanel []*Binding
|
||||||
bindingsGlobal, bindingsPanel []*Binding
|
|
||||||
)
|
|
||||||
|
|
||||||
bindings := gui.GetInitialKeybindings()
|
bindings := gui.GetInitialKeybindings()
|
||||||
|
|
||||||
|
|
@ -44,10 +41,6 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// } else if v.ParentView != nil && binding.ViewName == v.ParentView.Name() {
|
|
||||||
// // only add this if we don't have our own matching binding
|
|
||||||
// bindingsPanel = append(bindingsPanel, binding)
|
|
||||||
|
|
||||||
// append dummy element to have a separator between
|
// append dummy element to have a separator between
|
||||||
// panel and global keybindings
|
// panel and global keybindings
|
||||||
bindingsPanel = append(bindingsPanel, &Binding{})
|
bindingsPanel = append(bindingsPanel, &Binding{})
|
||||||
|
|
@ -55,25 +48,26 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
if v.Name() == "menu" || v.Name() == "confirmation" {
|
if gui.isPopupPanel(v.Name()) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bindings := gui.getBindings(v)
|
menuItems := lo.Map(gui.getBindings(v), func(binding *Binding, _ int) *types.MenuItem {
|
||||||
|
return &types.MenuItem{
|
||||||
|
LabelColumns: []string{binding.GetKey(), binding.Description},
|
||||||
|
OnPress: func() error {
|
||||||
|
if binding.Key == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
handleMenuPress := func(index int) error {
|
return binding.Handler(g, v)
|
||||||
if bindings[index].Key == nil {
|
},
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if index >= len(bindings) {
|
})
|
||||||
return errors.New("Index is greater than size of bindings")
|
|
||||||
}
|
|
||||||
err := gui.handleMenuClose(g, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return bindings[index].Handler(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.createMenu(strings.Title(gui.Tr.Menu), bindings, len(bindings), handleMenuPress)
|
return gui.Menu(CreateMenuOptions{
|
||||||
|
Title: gui.Tr.MenuTitle,
|
||||||
|
Items: menuItems,
|
||||||
|
HideCancel: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
pkg/gui/panels.go
Normal file
7
pkg/gui/panels.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
|
||||||
|
func (gui *Gui) intoInterface() panels.IGui {
|
||||||
|
return gui
|
||||||
|
}
|
||||||
69
pkg/gui/panels/context_state.go
Normal file
69
pkg/gui/panels/context_state.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
package panels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A 'context' generally corresponds to an item and the tab in the main panel that we're
|
||||||
|
// displaying. So if we switch to a new item, or change the tab in the panel panel
|
||||||
|
// for the current item, we end up with a new context. When we have a new context,
|
||||||
|
// we render new content to the main panel.
|
||||||
|
type ContextState[T any] struct {
|
||||||
|
// index of the currently selected tab in the main view.
|
||||||
|
mainTabIdx int
|
||||||
|
// this function returns the tabs that we can display for an item (the tabs
|
||||||
|
// are shown on the main view)
|
||||||
|
GetMainTabs func() []MainTab[T]
|
||||||
|
// This tells us whether we need to re-render to the main panel for a given item.
|
||||||
|
// This should include the item's ID and if you want to invalidate the cache for
|
||||||
|
// some other reason, you can add that to the key as well (e.g. the container's state).
|
||||||
|
GetItemContextCacheKey func(item T) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MainTab[T any] struct {
|
||||||
|
// key used as part of the context cache key
|
||||||
|
Key string
|
||||||
|
// title of the tab, rendered in the main view
|
||||||
|
Title string
|
||||||
|
// function to render the content of the tab
|
||||||
|
Render func(item T) tasks.TaskFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ContextState[T]) GetMainTabTitles() []string {
|
||||||
|
return lo.Map(self.GetMainTabs(), func(tab MainTab[T], _ int) string {
|
||||||
|
return tab.Title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ContextState[T]) GetCurrentContextKey(item T) string {
|
||||||
|
return self.GetItemContextCacheKey(item) + "-" + self.GetCurrentMainTab().Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ContextState[T]) GetCurrentMainTab() MainTab[T] {
|
||||||
|
return self.GetMainTabs()[self.mainTabIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ContextState[T]) HandleNextMainTab() {
|
||||||
|
tabs := self.GetMainTabs()
|
||||||
|
|
||||||
|
if len(tabs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mainTabIdx = (self.mainTabIdx + 1) % len(tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ContextState[T]) HandlePrevMainTab() {
|
||||||
|
tabs := self.GetMainTabs()
|
||||||
|
|
||||||
|
if len(tabs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mainTabIdx = (self.mainTabIdx - 1 + len(tabs)) % len(tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ContextState[T]) SetMainTabIndex(index int) {
|
||||||
|
self.mainTabIdx = index
|
||||||
|
}
|
||||||
111
pkg/gui/panels/filtered_list.go
Normal file
111
pkg/gui/panels/filtered_list.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package panels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilteredList[T comparable] struct {
|
||||||
|
allItems []T
|
||||||
|
// indices of items in the allItems slice that are included in the filtered list
|
||||||
|
indices []int
|
||||||
|
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilteredList[T comparable]() *FilteredList[T] {
|
||||||
|
return &FilteredList[T]{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) SetItems(items []T) {
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer self.mutex.Unlock()
|
||||||
|
|
||||||
|
self.allItems = items
|
||||||
|
self.indices = make([]int, len(items))
|
||||||
|
for i := range self.indices {
|
||||||
|
self.indices[i] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) Filter(filter func(T, int) bool) {
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer self.mutex.Unlock()
|
||||||
|
|
||||||
|
self.indices = self.indices[:0]
|
||||||
|
for i, item := range self.allItems {
|
||||||
|
if filter(item, i) {
|
||||||
|
self.indices = append(self.indices, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) Sort(less func(T, T) bool) {
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer self.mutex.Unlock()
|
||||||
|
|
||||||
|
if less == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(self.indices, func(i, j int) bool {
|
||||||
|
return less(self.allItems[self.indices[i]], self.allItems[self.indices[j]])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) Get(index int) T {
|
||||||
|
self.mutex.RLock()
|
||||||
|
defer self.mutex.RUnlock()
|
||||||
|
|
||||||
|
return self.allItems[self.indices[index]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) TryGet(index int) (T, bool) {
|
||||||
|
self.mutex.RLock()
|
||||||
|
defer self.mutex.RUnlock()
|
||||||
|
|
||||||
|
if index < 0 || index >= len(self.indices) {
|
||||||
|
var zero T
|
||||||
|
return zero, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.allItems[self.indices[index]], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the length of the filtered list
|
||||||
|
func (self *FilteredList[T]) Len() int {
|
||||||
|
self.mutex.RLock()
|
||||||
|
defer self.mutex.RUnlock()
|
||||||
|
|
||||||
|
return len(self.indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) GetIndex(item T) int {
|
||||||
|
self.mutex.RLock()
|
||||||
|
defer self.mutex.RUnlock()
|
||||||
|
|
||||||
|
for i, index := range self.indices {
|
||||||
|
if self.allItems[index] == item {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) GetItems() []T {
|
||||||
|
self.mutex.RLock()
|
||||||
|
defer self.mutex.RUnlock()
|
||||||
|
|
||||||
|
result := make([]T, len(self.indices))
|
||||||
|
for i, index := range self.indices {
|
||||||
|
result[i] = self.allItems[index]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FilteredList[T]) GetAllItems() []T {
|
||||||
|
self.mutex.RLock()
|
||||||
|
defer self.mutex.RUnlock()
|
||||||
|
|
||||||
|
return self.allItems
|
||||||
|
}
|
||||||
183
pkg/gui/panels/filtered_list_test.go
Normal file
183
pkg/gui/panels/filtered_list_test.go
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
package panels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFilteredListGet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
args int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: 1,
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: 2,
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||||
|
args: 0,
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := tt.f.Get(tt.args); got != tt.want {
|
||||||
|
t.Errorf("FilteredList.Get() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilteredListLen(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := tt.f.Len(); got != tt.want {
|
||||||
|
t.Errorf("FilteredList.Len() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilteredListFilter(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
args func(int, int) bool
|
||||||
|
want *FilteredList[int]
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: func(i int, _ int) bool { return i%2 == 0 },
|
||||||
|
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: func(i int, _ int) bool { return i%2 == 1 },
|
||||||
|
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 2}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt.f.Filter(tt.args)
|
||||||
|
assert.EqualValues(t, tt.f.indices, tt.want.indices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilteredListSort(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
args func(int, int) bool
|
||||||
|
want *FilteredList[int]
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: func(i int, j int) bool { return i < j },
|
||||||
|
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: func(i int, j int) bool { return i > j },
|
||||||
|
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{2, 1, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt.f.Sort(tt.args)
|
||||||
|
assert.EqualValues(t, tt.f.indices, tt.want.indices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilteredListGetIndex(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
args int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: 1,
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: 2,
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||||
|
args: 0,
|
||||||
|
want: -1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := tt.f.GetIndex(tt.args); got != tt.want {
|
||||||
|
t.Errorf("FilteredList.GetIndex() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilteredListGetItems(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
want []int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
want: []int{1, 2, 3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||||
|
want: []int{2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := tt.f.GetItems()
|
||||||
|
assert.EqualValues(t, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilteredListSetItems(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f *FilteredList[int]
|
||||||
|
args []int
|
||||||
|
want *FilteredList[int]
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||||
|
args: []int{4, 5, 6},
|
||||||
|
want: &FilteredList[int]{allItems: []int{4, 5, 6}, indices: []int{0, 1, 2}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||||
|
args: []int{4},
|
||||||
|
want: &FilteredList[int]{allItems: []int{4}, indices: []int{0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt.f.SetItems(tt.args)
|
||||||
|
assert.EqualValues(t, tt.f.indices, tt.want.indices)
|
||||||
|
assert.EqualValues(t, tt.f.allItems, tt.want.allItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
pkg/gui/panels/list_panel.go
Normal file
42
pkg/gui/panels/list_panel.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package panels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListPanel[T comparable] struct {
|
||||||
|
SelectedIdx int
|
||||||
|
List *FilteredList[T]
|
||||||
|
View *gocui.View
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListPanel[T]) SetSelectedLineIdx(value int) {
|
||||||
|
clampedValue := 0
|
||||||
|
if self.List.Len() > 0 {
|
||||||
|
clampedValue = lcUtils.Clamp(value, 0, self.List.Len()-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.SelectedIdx = clampedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListPanel[T]) clampSelectedLineIdx() {
|
||||||
|
clamped := lcUtils.Clamp(self.SelectedIdx, 0, self.List.Len()-1)
|
||||||
|
|
||||||
|
if clamped != self.SelectedIdx {
|
||||||
|
self.SelectedIdx = clamped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves the cursor up or down by the given amount (up for negative values)
|
||||||
|
func (self *ListPanel[T]) moveSelectedLine(delta int) {
|
||||||
|
self.SetSelectedLineIdx(self.SelectedIdx + delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListPanel[T]) SelectNextLine() {
|
||||||
|
self.moveSelectedLine(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *ListPanel[T]) SelectPrevLine() {
|
||||||
|
self.moveSelectedLine(-1)
|
||||||
|
}
|
||||||
280
pkg/gui/panels/side_list_panel.go
Normal file
280
pkg/gui/panels/side_list_panel.go
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
package panels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-errors/errors"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ISideListPanel interface {
|
||||||
|
SetMainTabIndex(int)
|
||||||
|
HandleSelect() error
|
||||||
|
GetView() *gocui.View
|
||||||
|
Refocus()
|
||||||
|
RerenderList() error
|
||||||
|
IsFilterDisabled() bool
|
||||||
|
IsHidden() bool
|
||||||
|
HandleNextLine() error
|
||||||
|
HandlePrevLine() error
|
||||||
|
HandleClick() error
|
||||||
|
HandlePrevMainTab() error
|
||||||
|
HandleNextMainTab() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// list panel at the side of the screen that renders content to the main panel
|
||||||
|
type SideListPanel[T comparable] struct {
|
||||||
|
ContextState *ContextState[T]
|
||||||
|
|
||||||
|
ListPanel[T]
|
||||||
|
|
||||||
|
// message to render in the main view if there are no items in the panel
|
||||||
|
// and it has focus. Leave empty if you don't want to render anything
|
||||||
|
NoItemsMessage string
|
||||||
|
|
||||||
|
// a representation of the gui
|
||||||
|
Gui IGui
|
||||||
|
|
||||||
|
// this Filter is applied on top of additional default filters
|
||||||
|
Filter func(T) bool
|
||||||
|
Sort func(a, b T) bool
|
||||||
|
|
||||||
|
// a callback to invoke when the item is clicked
|
||||||
|
OnClick func(T) error
|
||||||
|
|
||||||
|
// a callback to invoke when a new item is selected (e.g. keyboard navigation)
|
||||||
|
OnSelect func(T) error
|
||||||
|
|
||||||
|
// returns the cells that we render to the view in a table format. The cells will
|
||||||
|
// be rendered with padding.
|
||||||
|
GetTableCells func(T) []string
|
||||||
|
|
||||||
|
// function to be called after re-rendering list. Can be nil
|
||||||
|
OnRerender func() error
|
||||||
|
|
||||||
|
// set this to true if you don't want to allow manual filtering via '/'
|
||||||
|
DisableFilter bool
|
||||||
|
|
||||||
|
// This can be nil if you want to always show the panel
|
||||||
|
Hide func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ISideListPanel = &SideListPanel[int]{}
|
||||||
|
|
||||||
|
type IGui interface {
|
||||||
|
HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error
|
||||||
|
NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc
|
||||||
|
FocusY(selectedLine int, itemCount int, view *gocui.View)
|
||||||
|
ShouldRefresh(contextKey string) bool
|
||||||
|
GetMainView() *gocui.View
|
||||||
|
IsCurrentView(*gocui.View) bool
|
||||||
|
FilterString(view *gocui.View) string
|
||||||
|
IgnoreStrings() []string
|
||||||
|
Update(func() error)
|
||||||
|
|
||||||
|
QueueTask(f func(ctx context.Context)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) HandleClick() error {
|
||||||
|
itemCount := self.List.Len()
|
||||||
|
handleSelect := self.HandleSelect
|
||||||
|
selectedLine := &self.SelectedIdx
|
||||||
|
|
||||||
|
if err := self.Gui.HandleClick(self.View, itemCount, selectedLine, handleSelect); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.OnClick != nil {
|
||||||
|
selectedItem, err := self.GetSelectedItem()
|
||||||
|
if err == nil {
|
||||||
|
return self.OnClick(selectedItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) GetView() *gocui.View {
|
||||||
|
return self.View
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) HandleSelect() error {
|
||||||
|
item, err := self.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() != self.NoItemsMessage {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.NoItemsMessage != "" {
|
||||||
|
self.Gui.NewSimpleRenderStringTask(func() string { return self.NoItemsMessage })
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.Refocus()
|
||||||
|
|
||||||
|
if self.OnSelect != nil {
|
||||||
|
if err := self.OnSelect(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.renderContext(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) renderContext(item T) error {
|
||||||
|
if self.ContextState == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := self.ContextState.GetCurrentContextKey(item)
|
||||||
|
if !self.Gui.ShouldRefresh(key) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mainView := self.Gui.GetMainView()
|
||||||
|
mainView.Tabs = self.ContextState.GetMainTabTitles()
|
||||||
|
mainView.TabIndex = self.ContextState.mainTabIdx
|
||||||
|
|
||||||
|
task := self.ContextState.GetCurrentMainTab().Render(item)
|
||||||
|
|
||||||
|
return self.Gui.QueueTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) GetSelectedItem() (T, error) {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
item, ok := self.List.TryGet(self.SelectedIdx)
|
||||||
|
if !ok {
|
||||||
|
// could probably have a better error here
|
||||||
|
return zero, errors.New(self.NoItemsMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) HandleNextLine() error {
|
||||||
|
self.SelectNextLine()
|
||||||
|
|
||||||
|
return self.HandleSelect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) HandlePrevLine() error {
|
||||||
|
self.SelectPrevLine()
|
||||||
|
|
||||||
|
return self.HandleSelect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) HandleNextMainTab() error {
|
||||||
|
if self.ContextState == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ContextState.HandleNextMainTab()
|
||||||
|
|
||||||
|
return self.HandleSelect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) HandlePrevMainTab() error {
|
||||||
|
if self.ContextState == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ContextState.HandlePrevMainTab()
|
||||||
|
|
||||||
|
return self.HandleSelect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) Refocus() {
|
||||||
|
self.Gui.FocusY(self.SelectedIdx, self.List.Len(), self.View)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) SetItems(items []T) {
|
||||||
|
self.List.SetItems(items)
|
||||||
|
self.FilterAndSort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) FilterAndSort() {
|
||||||
|
filterString := self.Gui.FilterString(self.View)
|
||||||
|
|
||||||
|
self.List.Filter(func(item T, index int) bool {
|
||||||
|
if self.Filter != nil && !self.Filter(item) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if lo.SomeBy(self.Gui.IgnoreStrings(), func(ignore string) bool {
|
||||||
|
return lo.SomeBy(self.GetTableCells(item), func(searchString string) bool {
|
||||||
|
return strings.Contains(searchString, ignore)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filterString != "" {
|
||||||
|
return lo.SomeBy(self.GetTableCells(item), func(searchString string) bool {
|
||||||
|
return strings.Contains(searchString, filterString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
self.List.Sort(self.Sort)
|
||||||
|
|
||||||
|
self.clampSelectedLineIdx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) RerenderList() error {
|
||||||
|
self.FilterAndSort()
|
||||||
|
|
||||||
|
self.Gui.Update(func() error {
|
||||||
|
self.View.Clear()
|
||||||
|
table := lo.Map(self.List.GetItems(), func(item T, index int) []string {
|
||||||
|
return self.GetTableCells(item)
|
||||||
|
})
|
||||||
|
renderedTable, err := utils.RenderTable(table)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprint(self.View, renderedTable)
|
||||||
|
|
||||||
|
if self.OnRerender != nil {
|
||||||
|
if err := self.OnRerender(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.Gui.IsCurrentView(self.View) {
|
||||||
|
return self.HandleSelect()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) SetMainTabIndex(index int) {
|
||||||
|
if self.ContextState == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ContextState.SetMainTabIndex(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) IsFilterDisabled() bool {
|
||||||
|
return self.DisableFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *SideListPanel[T]) IsHidden() bool {
|
||||||
|
if self.Hide == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.Hide()
|
||||||
|
}
|
||||||
146
pkg/gui/presentation/container_stats.go
Normal file
146
pkg/gui/presentation/container_stats.go
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/asciigraph"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/mcuadros/go-lookup"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderStats(userConfig *config.UserConfig, container *commands.Container, viewWidth int) (string, error) {
|
||||||
|
stats, ok := container.GetLastStats()
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
graphSpecs := userConfig.Stats.Graphs
|
||||||
|
graphs := make([]string, len(graphSpecs))
|
||||||
|
for i, spec := range graphSpecs {
|
||||||
|
graph, err := plotGraph(container, spec, viewWidth-10)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
||||||
|
}
|
||||||
|
|
||||||
|
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
|
||||||
|
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
|
||||||
|
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
|
||||||
|
|
||||||
|
originalStats, err := utils.MarshalIntoYaml(stats)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
||||||
|
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
|
||||||
|
pidsCount,
|
||||||
|
dataReceived,
|
||||||
|
dataSent,
|
||||||
|
utils.ColoredYamlString(string(originalStats)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// plotGraph returns the plotted graph based on the graph spec and the stat history
|
||||||
|
func plotGraph(container *commands.Container, spec config.GraphConfig, width int) (string, error) {
|
||||||
|
container.StatsMutex.Lock()
|
||||||
|
defer container.StatsMutex.Unlock()
|
||||||
|
|
||||||
|
data := make([]float64, len(container.StatHistory))
|
||||||
|
|
||||||
|
for i, stats := range container.StatHistory {
|
||||||
|
value, err := lookup.LookupString(stats, spec.StatPath)
|
||||||
|
if err != nil {
|
||||||
|
return "Could not find key: " + spec.StatPath, nil
|
||||||
|
}
|
||||||
|
floatValue, err := getFloat(value.Interface())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
data[i] = floatValue
|
||||||
|
}
|
||||||
|
|
||||||
|
max := spec.Max
|
||||||
|
if spec.MaxType == "" {
|
||||||
|
max = lo.Max(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
min := spec.Min
|
||||||
|
if spec.MinType == "" {
|
||||||
|
min = lo.Min(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
height := 10
|
||||||
|
if spec.Height > 0 {
|
||||||
|
height = spec.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
caption := fmt.Sprintf(
|
||||||
|
"%s: %0.2f (%v)",
|
||||||
|
spec.Caption,
|
||||||
|
data[len(data)-1],
|
||||||
|
time.Since(container.StatHistory[0].RecordedAt).Round(time.Second),
|
||||||
|
)
|
||||||
|
|
||||||
|
return asciigraph.Plot(
|
||||||
|
data,
|
||||||
|
asciigraph.Height(height),
|
||||||
|
asciigraph.Width(width),
|
||||||
|
asciigraph.Min(min),
|
||||||
|
asciigraph.Max(max),
|
||||||
|
asciigraph.Caption(caption),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
|
||||||
|
func getFloat(unk interface{}) (float64, error) {
|
||||||
|
floatType := reflect.TypeOf(float64(0))
|
||||||
|
stringType := reflect.TypeOf("")
|
||||||
|
|
||||||
|
switch i := unk.(type) {
|
||||||
|
case float64:
|
||||||
|
return i, nil
|
||||||
|
case float32:
|
||||||
|
return float64(i), nil
|
||||||
|
case int64:
|
||||||
|
return float64(i), nil
|
||||||
|
case int32:
|
||||||
|
return float64(i), nil
|
||||||
|
case int:
|
||||||
|
return float64(i), nil
|
||||||
|
case uint64:
|
||||||
|
return float64(i), nil
|
||||||
|
case uint32:
|
||||||
|
return float64(i), nil
|
||||||
|
case uint:
|
||||||
|
return float64(i), nil
|
||||||
|
case string:
|
||||||
|
return strconv.ParseFloat(i, 64)
|
||||||
|
default:
|
||||||
|
v := reflect.ValueOf(unk)
|
||||||
|
v = reflect.Indirect(v)
|
||||||
|
if v.Type().ConvertibleTo(floatType) {
|
||||||
|
fv := v.Convert(floatType)
|
||||||
|
return fv.Float(), nil
|
||||||
|
} else if v.Type().ConvertibleTo(stringType) {
|
||||||
|
sv := v.Convert(stringType)
|
||||||
|
s := sv.String()
|
||||||
|
return strconv.ParseFloat(s, 64)
|
||||||
|
} else {
|
||||||
|
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
pkg/gui/presentation/containers.go
Normal file
202
pkg/gui/presentation/containers.go
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container) []string {
|
||||||
|
return []string{
|
||||||
|
getContainerDisplayStatus(guiConfig, container),
|
||||||
|
getContainerDisplaySubstatus(guiConfig, container),
|
||||||
|
container.Name,
|
||||||
|
getDisplayCPUPerc(container),
|
||||||
|
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||||
|
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayContainerImage(container *commands.Container) string {
|
||||||
|
return strings.TrimPrefix(container.Container.Image, "sha256:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayPorts(c *commands.Container) string {
|
||||||
|
portStrings := lo.Map(c.Container.Ports, func(port container.Port, _ int) string {
|
||||||
|
if port.PublicPort == 0 {
|
||||||
|
return fmt.Sprintf("%d/%s", port.PrivatePort, port.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// docker ps will show '0.0.0.0:80->80/tcp' but we'll show
|
||||||
|
// '80->80/tcp' instead to save space (unless the IP is something other than
|
||||||
|
// 0.0.0.0)
|
||||||
|
ipString := ""
|
||||||
|
if port.IP != "0.0.0.0" {
|
||||||
|
ipString = port.IP + ":"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type)
|
||||||
|
})
|
||||||
|
|
||||||
|
// sorting because the order of the ports is not deterministic
|
||||||
|
// and we don't want to have them constantly swapping
|
||||||
|
sort.Strings(portStrings)
|
||||||
|
|
||||||
|
return strings.Join(portStrings, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContainerDisplayStatus returns the colored status of the container
|
||||||
|
func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
|
||||||
|
shortStatusMap := map[string]string{
|
||||||
|
"paused": "P",
|
||||||
|
"exited": "X",
|
||||||
|
"created": "C",
|
||||||
|
"removing": "RM",
|
||||||
|
"restarting": "RS",
|
||||||
|
"running": "R",
|
||||||
|
"dead": "D",
|
||||||
|
}
|
||||||
|
|
||||||
|
iconStatusMap := map[string]rune{
|
||||||
|
"paused": '◫',
|
||||||
|
"exited": '⨯',
|
||||||
|
"created": '+',
|
||||||
|
"removing": '−',
|
||||||
|
"restarting": '⟳',
|
||||||
|
"running": '▶',
|
||||||
|
"dead": '!',
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerState string
|
||||||
|
switch guiConfig.ContainerStatusHealthStyle {
|
||||||
|
case "short":
|
||||||
|
containerState = shortStatusMap[c.Container.State]
|
||||||
|
case "icon":
|
||||||
|
containerState = string(iconStatusMap[c.Container.State])
|
||||||
|
case "long":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
containerState = c.Container.State
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColoredString(containerState, getContainerColor(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check)
|
||||||
|
func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Container) string {
|
||||||
|
if !c.DetailsLoaded() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Container.State {
|
||||||
|
case "exited":
|
||||||
|
return utils.ColoredString(
|
||||||
|
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c),
|
||||||
|
)
|
||||||
|
case "running":
|
||||||
|
return getHealthStatus(guiConfig, c)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string {
|
||||||
|
if !c.DetailsLoaded() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
healthStatusColorMap := map[string]color.Attribute{
|
||||||
|
"healthy": color.FgGreen,
|
||||||
|
"unhealthy": color.FgRed,
|
||||||
|
"starting": color.FgYellow,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Details.State.Health == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
shortHealthStatusMap := map[string]string{
|
||||||
|
"healthy": "H",
|
||||||
|
"unhealthy": "U",
|
||||||
|
"starting": "S",
|
||||||
|
}
|
||||||
|
|
||||||
|
iconHealthStatusMap := map[string]rune{
|
||||||
|
"healthy": '✔',
|
||||||
|
"unhealthy": '?',
|
||||||
|
"starting": '…',
|
||||||
|
}
|
||||||
|
|
||||||
|
var healthStatus string
|
||||||
|
switch guiConfig.ContainerStatusHealthStyle {
|
||||||
|
case "short":
|
||||||
|
healthStatus = shortHealthStatusMap[c.Details.State.Health.Status]
|
||||||
|
case "icon":
|
||||||
|
healthStatus = string(iconHealthStatusMap[c.Details.State.Health.Status])
|
||||||
|
case "long":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
healthStatus = c.Details.State.Health.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
if healthStatusColor, ok := healthStatusColorMap[c.Details.State.Health.Status]; ok {
|
||||||
|
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
||||||
|
func getDisplayCPUPerc(c *commands.Container) string {
|
||||||
|
stats, ok := c.GetLastStats()
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
percentage := stats.DerivedStats.CPUPercentage
|
||||||
|
formattedPercentage := fmt.Sprintf("%.2f%%", stats.DerivedStats.CPUPercentage)
|
||||||
|
|
||||||
|
var clr color.Attribute
|
||||||
|
if percentage > 90 {
|
||||||
|
clr = color.FgRed
|
||||||
|
} else if percentage > 50 {
|
||||||
|
clr = color.FgYellow
|
||||||
|
} else {
|
||||||
|
clr = color.FgWhite
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.ColoredString(formattedPercentage, clr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContainerColor Container color
|
||||||
|
func getContainerColor(c *commands.Container) color.Attribute {
|
||||||
|
switch c.Container.State {
|
||||||
|
case "exited":
|
||||||
|
// This means the colour may be briefly yellow and then switch to red upon starting
|
||||||
|
// Not sure what a better alternative is.
|
||||||
|
if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 {
|
||||||
|
return color.FgYellow
|
||||||
|
}
|
||||||
|
return color.FgRed
|
||||||
|
case "created":
|
||||||
|
return color.FgCyan
|
||||||
|
case "running":
|
||||||
|
return color.FgGreen
|
||||||
|
case "paused":
|
||||||
|
return color.FgYellow
|
||||||
|
case "dead":
|
||||||
|
return color.FgRed
|
||||||
|
case "restarting":
|
||||||
|
return color.FgBlue
|
||||||
|
case "removing":
|
||||||
|
return color.FgMagenta
|
||||||
|
default:
|
||||||
|
return color.FgWhite
|
||||||
|
}
|
||||||
|
}
|
||||||
14
pkg/gui/presentation/images.go
Normal file
14
pkg/gui/presentation/images.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetImageDisplayStrings(image *commands.Image) []string {
|
||||||
|
return []string{
|
||||||
|
image.Name,
|
||||||
|
image.Tag,
|
||||||
|
utils.FormatDecimalBytes(int(image.Image.Size)),
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pkg/gui/presentation/menu_items.go
Normal file
7
pkg/gui/presentation/menu_items.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
|
|
||||||
|
func GetMenuItemDisplayStrings(menuItem *types.MenuItem) []string {
|
||||||
|
return menuItem.LabelColumns
|
||||||
|
}
|
||||||
7
pkg/gui/presentation/networks.go
Normal file
7
pkg/gui/presentation/networks.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
|
||||||
|
func GetNetworkDisplayStrings(network *commands.Network) []string {
|
||||||
|
return []string{network.Network.Driver, network.Name}
|
||||||
|
}
|
||||||
7
pkg/gui/presentation/projects.go
Normal file
7
pkg/gui/presentation/projects.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
|
||||||
|
func GetProjectDisplayStrings(project *commands.Project) []string {
|
||||||
|
return []string{project.Name}
|
||||||
|
}
|
||||||
43
pkg/gui/presentation/services.go
Normal file
43
pkg/gui/presentation/services.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetServiceDisplayStrings(guiConfig *config.GuiConfig, service *commands.Service) []string {
|
||||||
|
if service.Container == nil {
|
||||||
|
var containerState string
|
||||||
|
switch guiConfig.ContainerStatusHealthStyle {
|
||||||
|
case "short":
|
||||||
|
containerState = "n"
|
||||||
|
case "icon":
|
||||||
|
containerState = "."
|
||||||
|
case "long":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
containerState = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{
|
||||||
|
utils.ColoredString(containerState, color.FgBlue),
|
||||||
|
"",
|
||||||
|
service.Name,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container := service.Container
|
||||||
|
return []string{
|
||||||
|
getContainerDisplayStatus(guiConfig, container),
|
||||||
|
getContainerDisplaySubstatus(guiConfig, container),
|
||||||
|
service.Name,
|
||||||
|
getDisplayCPUPerc(container),
|
||||||
|
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||||
|
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
|
||||||
|
}
|
||||||
|
}
|
||||||
7
pkg/gui/presentation/volumes.go
Normal file
7
pkg/gui/presentation/volumes.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package presentation
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
|
||||||
|
func GetVolumeDisplayStrings(volume *commands.Volume) []string {
|
||||||
|
return []string{volume.Volume.Driver, volume.Name}
|
||||||
|
}
|
||||||
|
|
@ -2,167 +2,207 @@ package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"context"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-errors/errors"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
"github.com/jesseduffield/yaml"
|
"github.com/jesseduffield/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gui *Gui) getProjectContexts() []string {
|
func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
return &panels.SideListPanel[*commands.Project]{
|
||||||
return []string{"logs", "config", "credits"}
|
ContextState: &panels.ContextState[*commands.Project]{
|
||||||
}
|
GetMainTabs: func() []panels.MainTab[*commands.Project] {
|
||||||
return []string{"credits"}
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
func (gui *Gui) getProjectContextTitles() []string {
|
ListPanel: panels.ListPanel[*commands.Project]{
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
List: panels.NewFilteredList[*commands.Project](),
|
||||||
return []string{gui.Tr.LogsTitle, gui.Tr.DockerComposeConfigTitle, gui.Tr.CreditsTitle}
|
View: gui.Views.Project,
|
||||||
|
},
|
||||||
|
NoItemsMessage: "",
|
||||||
|
Gui: gui.intoInterface(),
|
||||||
|
|
||||||
|
Sort: func(a *commands.Project, b *commands.Project) bool {
|
||||||
|
return a.Name < b.Name
|
||||||
|
},
|
||||||
|
GetTableCells: presentation.GetProjectDisplayStrings,
|
||||||
|
OnSelect: func(project *commands.Project) error {
|
||||||
|
// When a different project is selected, re-filter services and
|
||||||
|
// containers to show only those belonging to the selected project.
|
||||||
|
return gui.renderContainersAndServices()
|
||||||
|
},
|
||||||
|
Hide: func() bool {
|
||||||
|
return !gui.DockerCommand.IsProjectScoped()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return []string{gui.Tr.CreditsTitle}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshProject() error {
|
func (gui *Gui) refreshProject() error {
|
||||||
v := gui.getProjectView()
|
projects := gui.getDiscoveredProjects()
|
||||||
|
|
||||||
projectName := path.Base(gui.Config.ProjectDir)
|
// Preserve the current selection across refreshes. On the first refresh,
|
||||||
if gui.DockerCommand.InDockerComposeProject {
|
// select the project specified via -p flag, or fall back to the local project.
|
||||||
for _, service := range gui.DockerCommand.Services {
|
selectedName := gui.getSelectedProjectName()
|
||||||
if service.Container != nil {
|
if selectedName == "" {
|
||||||
projectName = service.Container.Details.Config.Labels["com.docker.compose.project"]
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.g.Update(func(*gocui.Gui) error {
|
return gui.Panels.Projects.RerenderList()
|
||||||
v.Clear()
|
|
||||||
fmt.Fprint(v, projectName)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleProjectClick(g *gocui.Gui, v *gocui.View) error {
|
// getDiscoveredProjects returns all docker compose projects by examining container labels.
|
||||||
if gui.popupPanelFocused() {
|
// The local project (from docker-compose.yml in the current directory, or from -p) is
|
||||||
return nil
|
// included even when it has no running containers, so the user always sees the project
|
||||||
}
|
// they explicitly scoped to.
|
||||||
|
func (gui *Gui) getDiscoveredProjects() []*commands.Project {
|
||||||
|
containers := gui.Panels.Containers.List.GetAllItems()
|
||||||
|
projectNames := gui.DockerCommand.GetProjectNames(containers)
|
||||||
|
|
||||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
// If we're scoped to a project but it has no running containers, still
|
||||||
return err
|
// 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
|
||||||
|
|
||||||
return gui.handleProjectSelect(g, v)
|
if gui.DockerCommand.IsProjectScoped() && localName != "" {
|
||||||
}
|
found := false
|
||||||
|
for _, name := range projectNames {
|
||||||
func (gui *Gui) handleProjectSelect(g *gocui.Gui, v *gocui.View) error {
|
if name == localName {
|
||||||
if gui.popupPanelFocused() {
|
found = true
|
||||||
return nil
|
break
|
||||||
}
|
|
||||||
|
|
||||||
key := gui.getProjectContexts()[gui.State.Panels.Project.ContextIndex]
|
|
||||||
if !gui.shouldRefresh(key) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.clearMainView()
|
|
||||||
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Tabs = gui.getProjectContextTitles()
|
|
||||||
mainView.TabIndex = gui.State.Panels.Project.ContextIndex
|
|
||||||
|
|
||||||
switch gui.getProjectContexts()[gui.State.Panels.Project.ContextIndex] {
|
|
||||||
case "credits":
|
|
||||||
if err := gui.renderCredits(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "logs":
|
|
||||||
if err := gui.renderAllLogs(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "config":
|
|
||||||
if err := gui.renderDockerComposeConfig(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("Unknown context for status panel")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderCredits() error {
|
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Autoscroll = false
|
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
|
||||||
|
|
||||||
var configBuf bytes.Buffer
|
|
||||||
yaml.NewEncoder(&configBuf, yaml.IncludeOmitted).Encode(gui.Config.UserConfig)
|
|
||||||
|
|
||||||
dashboardString := 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://donorbox.org/lazydocker", color.FgMagenta), // caffeine ain't free
|
|
||||||
"Here's your lazydocker config when merged in with the defaults (you can open your config by pressing 'o'):",
|
|
||||||
configBuf.String(),
|
|
||||||
}, "\n\n")
|
|
||||||
|
|
||||||
gui.renderString(gui.g, "main", dashboardString)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderAllLogs() error {
|
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
mainView.Autoscroll = true
|
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
|
||||||
|
|
||||||
gui.clearMainView()
|
|
||||||
|
|
||||||
cmd := gui.OSCommand.RunCustomCommand(
|
|
||||||
utils.ApplyTemplate(
|
|
||||||
gui.Config.UserConfig.CommandTemplates.AllLogs,
|
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd.Stdout = mainView
|
|
||||||
cmd.Stderr = mainView
|
|
||||||
|
|
||||||
gui.OSCommand.PrepareForChildren(cmd)
|
|
||||||
cmd.Start()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-stop
|
|
||||||
if err := gui.OSCommand.Kill(cmd); err != nil {
|
|
||||||
gui.Log.Error(err)
|
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
if !found {
|
||||||
|
projectNames = append([]string{localName}, projectNames...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Wait()
|
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() error {
|
func (gui *Gui) renderDockerComposeConfig(project *commands.Project) tasks.TaskFunc {
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
if !gui.DockerCommand.InDockerComposeProject {
|
||||||
mainView := gui.getMainView()
|
return gui.NewSimpleRenderStringTask(func() string {
|
||||||
mainView.Autoscroll = false
|
return "Compose config is only available when launched from a docker-compose project directory"
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
})
|
||||||
|
}
|
||||||
config := gui.DockerCommand.DockerComposeConfig()
|
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||||
gui.renderString(gui.g, "main", config)
|
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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,39 +227,13 @@ func lazydockerTitle() string {
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleProjectNextContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getProjectContexts()
|
|
||||||
if gui.State.Panels.Project.ContextIndex >= len(contexts)-1 {
|
|
||||||
gui.State.Panels.Project.ContextIndex = 0
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Project.ContextIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleProjectSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleProjectPrevContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getProjectContexts()
|
|
||||||
if gui.State.Panels.Project.ContextIndex <= 0 {
|
|
||||||
gui.State.Panels.Project.ContextIndex = len(contexts) - 1
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Project.ContextIndex--
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleProjectSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
|
// handleViewAllLogs switches to a subprocess viewing all the logs from docker-compose
|
||||||
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
|
||||||
c, err := gui.DockerCommand.ViewAllLogs()
|
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||||
|
c, err := gui.DockerCommand.ViewAllLogs(project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SubProcess = c
|
return gui.runSubprocess(c)
|
||||||
return gui.Errors.ErrSubProcess
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,200 +1,177 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/go-errors/errors"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// list panel functions
|
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
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) getServiceContexts() []string {
|
if a.Container == nil && b.Container != nil {
|
||||||
return []string{"logs", "stats", "container-config", "top"}
|
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 {
|
||||||
|
return !gui.DockerCommand.IsProjectScoped()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getServiceContextTitles() []string {
|
func (gui *Gui) renderServiceContainerConfig(service *commands.Service) tasks.TaskFunc {
|
||||||
return []string{gui.Tr.LogsTitle, gui.Tr.StatsTitle, gui.Tr.ContainerConfigTitle, gui.Tr.TopTitle}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) getSelectedService() (*commands.Service, error) {
|
|
||||||
selectedLine := gui.State.Panels.Services.SelectedLine
|
|
||||||
if selectedLine == -1 {
|
|
||||||
return &commands.Service{}, errors.New("no service selected")
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.DockerCommand.Services[selectedLine], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServicesClick(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
itemCount := len(gui.DockerCommand.Services)
|
|
||||||
handleSelect := gui.handleServiceSelect
|
|
||||||
selectedLine := &gui.State.Panels.Services.SelectedLine
|
|
||||||
|
|
||||||
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
service, err := gui.getSelectedService()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
containerID := ""
|
|
||||||
if service.Container != nil {
|
|
||||||
containerID = service.Container.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := "services-" + service.ID + "-" + containerID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex]
|
|
||||||
if !gui.shouldRefresh(key) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mainView := gui.getMainView()
|
|
||||||
|
|
||||||
mainView.Tabs = gui.getServiceContextTitles()
|
|
||||||
mainView.TabIndex = gui.State.Panels.Services.ContextIndex
|
|
||||||
|
|
||||||
switch gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] {
|
|
||||||
case "logs":
|
|
||||||
if err := gui.renderServiceLogs(service); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "stats":
|
|
||||||
if err := gui.renderServiceStats(service); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "container-config":
|
|
||||||
if service.Container == nil {
|
|
||||||
return gui.renderString(gui.g, "main", gui.Tr.NoContainer)
|
|
||||||
}
|
|
||||||
if err := gui.renderContainerConfig(service.Container); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "top":
|
|
||||||
if err := gui.renderServiceTop(service); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("Unknown context for services panel")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) renderServiceStats(service *commands.Service) error {
|
|
||||||
if service.Container == nil {
|
if service.Container == nil {
|
||||||
return 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)
|
return gui.renderContainerStats(service.Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderServiceTop(service *commands.Service) error {
|
func (gui *Gui) renderServiceTop(service *commands.Service) tasks.TaskFunc {
|
||||||
mainView := gui.getMainView()
|
return gui.NewTickerTask(TickerTaskOpts{
|
||||||
mainView.Autoscroll = false
|
Func: func(ctx context.Context, notifyStopped chan struct{}) {
|
||||||
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
contents, err := service.RenderTop(ctx)
|
||||||
|
if err != nil {
|
||||||
|
gui.RenderStringMain(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
|
gui.reRenderStringMain(contents)
|
||||||
contents, err := service.RenderTop()
|
},
|
||||||
if err != nil {
|
Duration: time.Second,
|
||||||
gui.reRenderString(gui.g, "main", err.Error())
|
Before: func(ctx context.Context) { gui.clearMainView() },
|
||||||
}
|
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||||
|
Autoscroll: false,
|
||||||
gui.reRenderString(gui.g, "main", contents)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) renderServiceLogs(service *commands.Service) error {
|
func (gui *Gui) renderServiceLogs(service *commands.Service) tasks.TaskFunc {
|
||||||
service, err := gui.getSelectedService()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if service.Container == nil {
|
if service.Container == nil {
|
||||||
return gui.T.NewTask(func(stop chan struct{}) {
|
return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainerForService })
|
||||||
gui.clearMainView()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.renderContainerLogs(service.Container)
|
return gui.renderContainerLogsToMain(service.Container)
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
panelState := gui.State.Panels.Services
|
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), false)
|
|
||||||
|
|
||||||
return gui.handleServiceSelect(gui.g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServicesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
panelState := gui.State.Panels.Services
|
|
||||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), true)
|
|
||||||
|
|
||||||
return gui.handleServiceSelect(gui.g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServicesNextContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getServiceContexts()
|
|
||||||
if gui.State.Panels.Services.ContextIndex >= len(contexts)-1 {
|
|
||||||
gui.State.Panels.Services.ContextIndex = 0
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Services.ContextIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleServiceSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleServicesPrevContext(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
contexts := gui.getServiceContexts()
|
|
||||||
if gui.State.Panels.Services.ContextIndex <= 0 {
|
|
||||||
gui.State.Panels.Services.ContextIndex = len(contexts) - 1
|
|
||||||
} else {
|
|
||||||
gui.State.Panels.Services.ContextIndex--
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.handleServiceSelect(gui.g, v)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type commandOption struct {
|
type commandOption struct {
|
||||||
description string
|
description string
|
||||||
command string
|
command string
|
||||||
f func() error
|
onPress func() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisplayStrings is a function.
|
func (r *commandOption) getDisplayStrings() []string {
|
||||||
func (r *commandOption) GetDisplayStrings(isFocused bool) []string {
|
|
||||||
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
|
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isServiceFromLocalProject returns true if the given service belongs to the
|
||||||
|
// local compose project (the one whose compose file is in the current directory).
|
||||||
|
// Compose commands like up/stop/restart only work for local project services.
|
||||||
|
func (gui *Gui) isServiceFromLocalProject(service *commands.Service) bool {
|
||||||
|
return service.ProjectName == gui.DockerCommand.LocalProjectName
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose
|
if !gui.isServiceFromLocalProject(service) {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeCommand := gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}).DockerCompose
|
||||||
|
|
||||||
options := []*commandOption{
|
options := []*commandOption{
|
||||||
{
|
{
|
||||||
|
|
@ -205,88 +182,242 @@ func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
description: gui.Tr.RemoveWithVolumes,
|
description: gui.Tr.RemoveWithVolumes,
|
||||||
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
|
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
description: gui.Tr.Cancel,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.createServiceCommandMenu(options, gui.Tr.RemovingStatus)
|
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 {
|
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
|
||||||
if err := service.Stop(); err != nil {
|
if !gui.isServiceFromLocalProject(service) {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
if service.Container == nil {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
return service.Container.Stop()
|
||||||
}
|
}
|
||||||
|
if err := service.Stop(); err != nil {
|
||||||
return gui.refreshContainersAndServices()
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleServiceUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !gui.isServiceFromLocalProject(service) {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.UppingServiceStatus, func() error {
|
||||||
|
if err := service.Up(); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||||
if err := service.Restart(); err != nil {
|
if !gui.isServiceFromLocalProject(service) {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return gui.refreshContainersAndServices()
|
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 {
|
func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.Container == nil {
|
if service.Container == nil {
|
||||||
return gui.createErrorPanel(gui.g, gui.Tr.NoContainers)
|
return gui.createErrorPanel(gui.Tr.NoContainers)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := service.Attach()
|
c, err := service.Attach()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SubProcess = c
|
return gui.runSubprocess(c)
|
||||||
return gui.Errors.ErrSubProcess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServiceViewLogs(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := service.ViewLogs()
|
c, err := service.ViewLogs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SubProcess = c
|
return gui.runSubprocess(c)
|
||||||
return gui.Errors.ErrSubProcess
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleProjectUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||||
|
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmUpProject, func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
cmdStr := utils.ApplyTemplate(
|
||||||
|
gui.Config.UserConfig.CommandTemplates.Up,
|
||||||
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.UppingProjectStatus, func() error {
|
||||||
|
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||||
|
if project != nil && project.Name != gui.DockerCommand.LocalProjectName {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
downCommand := utils.ApplyTemplate(
|
||||||
|
gui.Config.UserConfig.CommandTemplates.Down,
|
||||||
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
|
)
|
||||||
|
|
||||||
|
downWithVolumesCommand := utils.ApplyTemplate(
|
||||||
|
gui.Config.UserConfig.CommandTemplates.DownWithVolumes,
|
||||||
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project}),
|
||||||
|
)
|
||||||
|
|
||||||
|
options := []*commandOption{
|
||||||
|
{
|
||||||
|
description: gui.Tr.Down,
|
||||||
|
command: downCommand,
|
||||||
|
onPress: func() error {
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
|
||||||
|
if err := gui.OSCommand.RunCommand(downCommand); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: gui.Tr.DownWithVolumes,
|
||||||
|
command: downWithVolumesCommand,
|
||||||
|
onPress: func() error {
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.DowningStatus, func() error {
|
||||||
|
if err := gui.OSCommand.RunCommand(downWithVolumesCommand); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||||
|
return &types.MenuItem{
|
||||||
|
LabelColumns: option.getDisplayStrings(),
|
||||||
|
OnPress: option.onPress,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return gui.Menu(CreateMenuOptions{
|
||||||
|
Title: "",
|
||||||
|
Items: menuItems,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !gui.isServiceFromLocalProject(service) {
|
||||||
|
return gui.createErrorPanel(gui.Tr.CannotManageNonLocalService)
|
||||||
|
}
|
||||||
|
|
||||||
rebuildCommand := utils.ApplyTemplate(
|
rebuildCommand := utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||||
|
|
@ -304,24 +435,25 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
gui.Config.UserConfig.CommandTemplates.RestartService,
|
gui.Config.UserConfig.CommandTemplates.RestartService,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||||
),
|
),
|
||||||
f: func() error {
|
onPress: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||||
if err := service.Restart(); err != nil {
|
if err := service.Restart(); err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
description: gui.Tr.Recreate,
|
description: gui.Tr.Recreate,
|
||||||
command: utils.ApplyTemplate(
|
command: utils.ApplyTemplate(
|
||||||
gui.Config.UserConfig.CommandTemplates.RecreateService,
|
gui.Config.UserConfig.CommandTemplates.RecreateService,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||||
),
|
),
|
||||||
f: func() error {
|
onPress: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
|
||||||
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
|
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
@ -333,41 +465,27 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
||||||
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
gui.Config.UserConfig.CommandTemplates.RebuildService,
|
||||||
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
|
||||||
),
|
),
|
||||||
f: func() error {
|
onPress: func() error {
|
||||||
gui.SubProcess = gui.OSCommand.RunCustomCommand(rebuildCommand)
|
return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
|
||||||
return gui.Errors.ErrSubProcess
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
description: gui.Tr.Cancel,
|
|
||||||
f: func() error { return nil },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMenuPress := func(index int) error { return options[index].f() }
|
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||||
|
return &types.MenuItem{
|
||||||
return gui.createMenu("", options, len(options), handleMenuPress)
|
LabelColumns: option.getDisplayStrings(),
|
||||||
}
|
OnPress: option.onPress,
|
||||||
|
|
||||||
func (gui *Gui) createServiceCommandMenu(options []*commandOption, status string) error {
|
|
||||||
handleMenuPress := func(index int) error {
|
|
||||||
if options[index].command == "" {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return gui.WithWaitingStatus(status, func() error {
|
})
|
||||||
if err := gui.OSCommand.RunCommand(options[index].command); err != nil {
|
|
||||||
return gui.createErrorPanel(gui.g, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return gui.Menu(CreateMenuOptions{
|
||||||
})
|
Title: "",
|
||||||
}
|
Items: menuItems,
|
||||||
|
})
|
||||||
return gui.createMenu("", options, len(options), handleMenuPress)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
service, err := gui.getSelectedService()
|
service, err := gui.Panels.Services.GetSelectedItem()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -404,8 +522,37 @@ L:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
project, _ := gui.Panels.Projects.GetSelectedItem()
|
||||||
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
|
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
|
||||||
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
|
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{Project: project})
|
||||||
|
|
||||||
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
return gui.createBulkCommandMenu(bulkCommands, commandObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
148
pkg/gui/sort_container_test.go
Normal file
148
pkg/gui/sort_container_test.go
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sampleContainers() []*commands.Container {
|
||||||
|
return []*commands.Container{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Name: "1",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "exited",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Name: "2",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "3",
|
||||||
|
Name: "3",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "4",
|
||||||
|
Name: "4",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "created",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedPerStatusContainers() []*commands.Container {
|
||||||
|
return []*commands.Container{
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Name: "2",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "3",
|
||||||
|
Name: "3",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Name: "1",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "exited",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "4",
|
||||||
|
Name: "4",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "created",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedLegacySortedContainers() []*commands.Container {
|
||||||
|
return []*commands.Container{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Name: "1",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "exited",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "2",
|
||||||
|
Name: "2",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "3",
|
||||||
|
Name: "3",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "running",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "4",
|
||||||
|
Name: "4",
|
||||||
|
Container: container.Summary{
|
||||||
|
State: "created",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqualContainers(t *testing.T, left *commands.Container, right *commands.Container) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, left.Container.State, right.Container.State)
|
||||||
|
assert.Equal(t, left.Container.ID, right.Container.ID)
|
||||||
|
assert.Equal(t, left.Name, right.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSortContainers(t *testing.T) {
|
||||||
|
actual := sampleContainers()
|
||||||
|
|
||||||
|
expected := expectedPerStatusContainers()
|
||||||
|
|
||||||
|
sort.Slice(actual, func(i, j int) bool {
|
||||||
|
return sortContainers(actual[i], actual[j], false)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, len(actual), len(expected))
|
||||||
|
|
||||||
|
for i := 0; i < len(actual); i++ {
|
||||||
|
assertEqualContainers(t, expected[i], actual[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLegacySortedContainers(t *testing.T) {
|
||||||
|
actual := sampleContainers()
|
||||||
|
|
||||||
|
expected := expectedLegacySortedContainers()
|
||||||
|
|
||||||
|
sort.Slice(actual, func(i, j int) bool {
|
||||||
|
return sortContainers(actual[i], actual[j], true)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, len(actual), len(expected))
|
||||||
|
|
||||||
|
for i := 0; i < len(actual); i++ {
|
||||||
|
assertEqualContainers(t, expected[i], actual[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,86 +2,71 @@ package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
|
func (gui *Gui) runSubprocess(cmd *exec.Cmd) error {
|
||||||
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
|
return gui.runSubprocessWithMessage(cmd, "")
|
||||||
// otherwise it handles the error, possibly by quitting the application
|
}
|
||||||
func (gui *Gui) RunWithSubprocesses() error {
|
|
||||||
for {
|
|
||||||
if err := gui.Run(); err != nil {
|
|
||||||
if err == gocui.ErrQuit {
|
|
||||||
break
|
|
||||||
} else if err == gui.Errors.ErrSubProcess {
|
|
||||||
// preparing the state for when we return
|
|
||||||
gui.pushPreviousView(gui.currentViewName())
|
|
||||||
// giving goEvery goroutines time to finish
|
|
||||||
gui.State.SessionIndex++
|
|
||||||
|
|
||||||
if err := gui.runCommand(); err != nil {
|
func (gui *Gui) runSubprocessWithMessage(cmd *exec.Cmd, msg string) error {
|
||||||
return err
|
gui.Mutexes.SubprocessMutex.Lock()
|
||||||
}
|
defer gui.Mutexes.SubprocessMutex.Unlock()
|
||||||
|
|
||||||
// pop here so we don't stack up view names
|
if err := gui.g.Suspend(); err != nil {
|
||||||
gui.popPreviousView()
|
return gui.createErrorPanel(err.Error())
|
||||||
// ensuring we render e.g. the logs of the currently selected item upon return
|
|
||||||
gui.State.Panels.Main.ObjectKey = ""
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gui.PauseBackgroundThreads = true
|
||||||
|
|
||||||
|
gui.runCommand(cmd, msg)
|
||||||
|
|
||||||
|
if err := gui.g.Resume(); err != nil {
|
||||||
|
return gui.createErrorPanel(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.PauseBackgroundThreads = false
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) runCommand() error {
|
func (gui *Gui) runCommand(cmd *exec.Cmd, msg string) {
|
||||||
gui.SubProcess.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
gui.SubProcess.Stderr = os.Stdout
|
cmd.Stderr = os.Stdout
|
||||||
gui.SubProcess.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
|
defer signal.Stop(stop)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(stop, os.Interrupt)
|
||||||
<-c
|
<-stop
|
||||||
signal.Stop(c)
|
|
||||||
|
|
||||||
if err := gui.OSCommand.Kill(gui.SubProcess); err != nil {
|
if err := gui.OSCommand.Kill(cmd); err != nil {
|
||||||
gui.Log.Error(err)
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue))
|
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(cmd.Args, " "), color.FgBlue))
|
||||||
|
if msg != "" {
|
||||||
if err := gui.SubProcess.Run(); err != nil {
|
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString(msg, color.FgGreen))
|
||||||
|
}
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
// not handling the error explicitly because usually we're going to see it
|
// not handling the error explicitly because usually we're going to see it
|
||||||
// in the output anyway
|
// in the output anyway
|
||||||
gui.Log.Error(err)
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SubProcess.Stdin = nil
|
cmd.Stdin = nil
|
||||||
gui.SubProcess.Stdout = ioutil.Discard
|
cmd.Stdout = io.Discard
|
||||||
gui.SubProcess.Stderr = ioutil.Discard
|
cmd.Stderr = io.Discard
|
||||||
gui.SubProcess = nil
|
|
||||||
|
|
||||||
signal.Stop(c)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
101
pkg/gui/tasks_adapter.go
Normal file
101
pkg/gui/tasks_adapter.go
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) QueueTask(f func(ctx context.Context)) error {
|
||||||
|
return gui.taskManager.NewTask(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderStringTaskOpts struct {
|
||||||
|
Autoscroll bool
|
||||||
|
Wrap bool
|
||||||
|
GetStrContent func() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskOpts struct {
|
||||||
|
Autoscroll bool
|
||||||
|
Wrap bool
|
||||||
|
Func func(ctx context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TickerTaskOpts struct {
|
||||||
|
Duration time.Duration
|
||||||
|
Before func(ctx context.Context)
|
||||||
|
Func func(ctx context.Context, notifyStopped chan struct{})
|
||||||
|
Autoscroll bool
|
||||||
|
Wrap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) NewRenderStringTask(opts RenderStringTaskOpts) tasks.TaskFunc {
|
||||||
|
taskOpts := TaskOpts{
|
||||||
|
Autoscroll: opts.Autoscroll,
|
||||||
|
Wrap: opts.Wrap,
|
||||||
|
Func: func(ctx context.Context) {
|
||||||
|
gui.RenderStringMain(opts.GetStrContent())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.NewTask(taskOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumes it's cheap to obtain the content (otherwise we would pass a function that returns the content)
|
||||||
|
func (gui *Gui) NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc {
|
||||||
|
return gui.NewRenderStringTask(RenderStringTaskOpts{
|
||||||
|
GetStrContent: getContent,
|
||||||
|
Autoscroll: false,
|
||||||
|
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) NewTask(opts TaskOpts) tasks.TaskFunc {
|
||||||
|
return func(ctx context.Context) {
|
||||||
|
mainView := gui.Views.Main
|
||||||
|
mainView.Autoscroll = opts.Autoscroll
|
||||||
|
mainView.Wrap = opts.Wrap
|
||||||
|
|
||||||
|
opts.Func(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second
|
||||||
|
// the before function gets called after the lock is obtained, but before the ticker starts.
|
||||||
|
// if you handle a message on the stop channel in f() you need to send a message on the notifyStopped channel because returning is not sufficient. Here, unlike in a regular task, simply returning means we're now going to wait till the next tick to run again.
|
||||||
|
func (gui *Gui) NewTickerTask(opts TickerTaskOpts) tasks.TaskFunc {
|
||||||
|
notifyStopped := make(chan struct{}, 10)
|
||||||
|
|
||||||
|
task := func(ctx context.Context) {
|
||||||
|
if opts.Before != nil {
|
||||||
|
opts.Before(ctx)
|
||||||
|
}
|
||||||
|
tickChan := time.NewTicker(opts.Duration)
|
||||||
|
defer tickChan.Stop()
|
||||||
|
// calling f first so that we're not waiting for the first tick
|
||||||
|
opts.Func(ctx, notifyStopped)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-notifyStopped:
|
||||||
|
gui.Log.Info("exiting ticker task due to notifyStopped channel")
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
gui.Log.Info("exiting ticker task due to stopped channel")
|
||||||
|
return
|
||||||
|
case <-tickChan.C:
|
||||||
|
gui.Log.Info("running ticker task again")
|
||||||
|
opts.Func(ctx, notifyStopped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taskOpts := TaskOpts{
|
||||||
|
Autoscroll: opts.Autoscroll,
|
||||||
|
Wrap: opts.Wrap,
|
||||||
|
Func: task,
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.NewTask(taskOpts)
|
||||||
|
}
|
||||||
|
|
@ -2,26 +2,18 @@ package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetColor bitwise OR's a list of attributes obtained via the given keys
|
|
||||||
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
|
|
||||||
var attribute gocui.Attribute
|
|
||||||
for _, key := range keys {
|
|
||||||
attribute |= utils.GetGocuiAttribute(key)
|
|
||||||
}
|
|
||||||
return attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOptionsPanelTextColor gets the color of the options panel text
|
// GetOptionsPanelTextColor gets the color of the options panel text
|
||||||
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
|
func (gui *Gui) GetOptionsPanelTextColor() gocui.Attribute {
|
||||||
return gui.GetColor(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil
|
return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetColorScheme sets the color scheme for the app based on the user config
|
// SetColorScheme sets the color scheme for the app based on the user config
|
||||||
func (gui *Gui) SetColorScheme() error {
|
func (gui *Gui) SetColorScheme() error {
|
||||||
gui.g.FgColor = gui.GetColor(gui.Config.UserConfig.Gui.Theme.InactiveBorderColor)
|
gui.g.FgColor = GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.InactiveBorderColor)
|
||||||
gui.g.SelFgColor = gui.GetColor(gui.Config.UserConfig.Gui.Theme.ActiveBorderColor)
|
gui.g.SelFgColor = GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.ActiveBorderColor)
|
||||||
|
gui.g.FrameColor = gui.g.FgColor
|
||||||
|
gui.g.SelFrameColor = gui.g.SelFgColor
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue