Compare commits

...

146 Commits
v0.7 ... v0.7.4

Author SHA1 Message Date
Bradley Cicenas
426dd2c985 update dockerclient -> v1.6.6 2020-10-26 19:30:33 +00:00
bradley
0fb627a529 Merge pull request #214 from whalehub/static-binaries
Makefile: Add CGO_ENABLED=0 to generate statically linked binaries
2020-10-26 15:11:02 -04:00
Aaron
c8f74a47a1 Makefile: Add CGO_ENABLED=0 to generate statically linked binaries
Signed-off-by: Aaron <admin@datahoarder.dev>
2020-10-26 16:14:57 +01:00
bradley
5b2d180f60 Merge pull request #210 from bcicen/fix-config-encoding
Fix config encoding
2020-10-26 09:04:07 -04:00
Bradley Cicenas
2fdbb91f87 ensure existing config file is removed prior to writing 2020-10-26 13:04:00 +00:00
Bradley Cicenas
c0703db094 add health check to mock connector 2020-10-26 12:02:31 +00:00
Bradley Cicenas
53ec5c911a return static error on unimplemented manager actions 2020-10-26 11:38:17 +00:00
Bradley Cicenas
c5038e2edd aggregate io r/w across all reported stats volumes 2020-10-25 17:42:20 +00:00
Bradley Cicenas
e1a52a314d v0.7.4, update docs 2020-10-25 16:55:44 +00:00
Bradley Cicenas
bbecbc66b9 fix byte format for compact view (short) 2020-10-25 16:02:31 +00:00
Bradley Cicenas
192d3eaa7a Merge branch '0.7.4-dev' into master 2020-10-25 11:41:49 -04:00
Bradley Cicenas
d34de844e0 add column config key to help menu 2020-10-25 10:31:51 -04:00
Bradley Cicenas
a8e235beca commit missing compact column, row files 2020-10-25 10:31:51 -04:00
Bradley Cicenas
ed194e8c04 update to go 1.13 2020-10-25 10:31:51 -04:00
Bradley Cicenas
09566a4043 add optional tooltip to menu widget 2020-10-25 10:31:51 -04:00
Bradley Cicenas
f11a705b8b add padding to column menu 2020-10-25 10:31:51 -04:00
Bradley Cicenas
6fe6e7c316 truncate id in compact widget 2020-10-25 10:31:51 -04:00
Bradley Cicenas
9aa104fbc6 handle single kv pair given to NewMeta 2020-10-25 10:31:51 -04:00
Bradley Cicenas
7c6b5c54dc init column config menu 2020-10-25 10:31:51 -04:00
Bradley Cicenas
ffb96f4e90 handle empty entries in column config 2020-10-25 10:31:51 -04:00
Bradley Cicenas
fc9bd9e5ca refactor column config 2020-10-25 10:31:51 -04:00
Bradley Cicenas
c7a8bfa26f integrate widget order, toggling into global config and compact grid 2020-10-25 10:31:51 -04:00
Bradley Cicenas
6b79e5a370 remove unused static col width 2020-10-25 10:31:51 -04:00
Bradley Cicenas
54fc5ac5c6 Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2020-10-25 10:31:51 -04:00
Bradley Cicenas
eb8237cbb1 continuing compact widget refactor 2020-10-25 10:31:51 -04:00
Bradley Cicenas
60875b179c initial refactor of all column widgets to standard interface 2020-10-25 10:31:19 -04:00
Marcos Diez
15c5c31726 shows total memory usage 2020-10-25 10:23:31 -04:00
Bradley Cicenas
ea5968edce update sig 2020-10-25 10:23:30 -04:00
Bradley Cicenas
c0db41ebcb Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2020-10-25 10:23:30 -04:00
Bradley Cicenas
dc14c79edf add run-dev to makefile 2020-10-25 10:23:30 -04:00
Bradley Cicenas
0ca5235ae5 continuing compact widget refactor 2020-10-25 10:23:30 -04:00
Bradley Cicenas
8427b0c81d initial refactor of all column widgets to standard interface 2020-10-25 10:23:30 -04:00
Marcos Diez
9bcf2c2c7a shows total memory usage 2020-10-25 10:23:30 -04:00
Bradley Cicenas
03a0da3230 update go version, deps for runc v1.0.0-rc92 2020-10-25 14:22:47 +00:00
Bradley Cicenas
4d7d69d4cf refactor byte unit formatting, add unit-specific display precision 2020-10-25 14:08:06 +00:00
bradley
fae9deb1d9 Merge pull request #203 from kerolloz/patch-1
enhance README.md
2020-08-28 07:29:46 -04:00
Kerollos Magdy
8027b990f8 enhance README.md 2020-07-30 16:18:24 +02:00
Bradley Cicenas
c446fb0e11 Merge branch 'master' of github.com:bcicen/ctop into 1.8-dev 2020-01-03 13:50:46 -05:00
Bradley Cicenas
4741b276e4 v0.7.3 2020-01-03 18:37:30 +00:00
Bradley Cicenas
d60b16aad1 add column config key to help menu 2020-01-03 13:21:57 +00:00
Bradley Cicenas
f704898212 commit missing compact column, row files 2020-01-03 13:20:31 +00:00
Bradley Cicenas
1523cc80ca update to go 1.13 2020-01-03 13:18:01 +00:00
Bradley Cicenas
b16561dccb Merge branch 'refactor-widgets' 2020-01-03 08:04:51 -05:00
Bradley Cicenas
bf3b89a010 add optional tooltip to menu widget 2020-01-03 12:53:25 +00:00
Bradley Cicenas
5585a22962 add padding to column menu 2020-01-03 12:25:54 +00:00
Bradley Cicenas
ca5d40b7cc truncate id in compact widget 2020-01-03 12:07:54 +00:00
Bradley Cicenas
50d1c29d57 handle single kv pair given to NewMeta 2020-01-03 12:07:21 +00:00
Bradley Cicenas
22a5607012 init column config menu 2020-01-02 23:02:53 +00:00
Bradley Cicenas
6e60fc905e handle empty entries in column config 2020-01-02 19:29:20 +00:00
Bradley Cicenas
118b89240d refactor column config 2020-01-02 19:28:51 +00:00
Bradley Cicenas
ee25f80a9c integrate widget order, toggling into global config and compact grid 2020-01-02 14:00:55 +00:00
Bradley Cicenas
416eb5c363 update sig 2019-12-30 08:39:03 -05:00
Bradley Cicenas
746da760fb Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2019-12-30 08:39:03 -05:00
Bradley Cicenas
cc6f706c4b add run-dev to makefile 2019-12-30 08:39:03 -05:00
Bradley Cicenas
1ca40bb7e1 continuing compact widget refactor 2019-12-30 08:39:03 -05:00
Bradley Cicenas
918ccdbe39 initial refactor of all column widgets to standard interface 2019-12-30 08:39:03 -05:00
Marcos Diez
8fcd14e097 shows total memory usage 2019-12-30 08:39:03 -05:00
bradley
9dd12103fc Merge pull request #186 from alexislefebvre/patch-1
Format options and keybindings as code
2019-12-30 08:31:49 -05:00
Alexis Lefebvre
c38942c7ed Format options and keybindings as code 2019-11-21 17:34:53 +01:00
Bradley Cicenas
d34b9c2bf6 remove unused static col width 2019-11-11 22:04:25 +00:00
Bradley Cicenas
a60861437f Merge branch 'refactor-widgets' 2019-11-06 07:33:42 -05:00
Bradley Cicenas
4b391e900c use raw log stream in docker log collector 2019-11-06 07:32:29 -05:00
Bradley Cicenas
4460162380 go 1.13 2019-11-06 12:32:03 +00:00
Bradley Cicenas
d56cc9475a update sig 2019-11-06 12:31:57 +00:00
bradley
4584cf34f5 Merge pull request #178 from rvolosatovs/feature/go-sum
Add go.sum
2019-10-03 07:35:26 -04:00
Roman Volosatovs
1ce07448ce go.sum: Add 2019-09-10 20:22:25 +02:00
Bradley Cicenas
c8e896e371 Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2019-07-03 11:27:17 +00:00
Bradley Cicenas
db2c832bd7 add run-dev to makefile 2019-07-05 19:09:30 -04:00
Bradley Cicenas
7fdcd7bbf1 continuing compact widget refactor 2019-07-05 19:09:30 -04:00
Bradley Cicenas
923edb967e initial refactor of all column widgets to standard interface 2019-07-05 19:08:16 -04:00
Marcos Diez
1271ce96e8 shows total memory usage 2019-07-05 19:07:13 -04:00
Bradley Cicenas
d8c7dd4c5c move image to go 1.12 2019-06-29 15:15:13 -04:00
Bradley Cicenas
b7d81485f9 update dockerclient, runc deps 2019-06-29 15:15:13 -04:00
bradley
8946c4b03b Merge pull request #169 from captn3m0/patch-1
Switches to a read-only socket mount
2019-06-29 14:32:08 -04:00
Bradley Cicenas
331f50f03e update status doc image 2019-06-22 18:44:07 +00:00
Bradley Cicenas
4c4f041b40 improve health check visibility 2019-06-22 18:42:48 +00:00
Bradley Cicenas
c8ac331652 fix timer 2019-05-22 17:39:37 +00:00
Bradley Cicenas
0a5a4c9062 add multi-line scrolling support, timestamps to error view 2019-05-22 17:38:01 +00:00
Bradley Cicenas
98fcfe8b6f refactor connectors for retry logic, add error view 2019-05-22 16:58:55 +00:00
bradley
42f095cd85 Merge pull request #170 from fr05t1k/exec
Enable cursor
2019-05-13 12:24:41 -04:00
Stanislav Pavlovichev
73986d2732 Enable cursor 2019-05-13 15:47:22 +03:00
Bradley Cicenas
c1d4615cc0 Merge branch 'fr05t1k-exec' 2019-05-12 16:26:51 -04:00
Bradley Cicenas
d187e8c623 drop potentially empty initial frames during exec attach 2019-05-12 20:23:54 +00:00
Bradley Cicenas
b8c38d09ef add exec shortcut key to container menu 2019-05-12 20:23:29 +00:00
Bradley Cicenas
d7384db373 Merge branch 'exec' of https://github.com/fr05t1k/ctop into fr05t1k-exec 2019-05-12 15:31:33 -04:00
Nemo
1b441db189 Switches to a read-only socket mount 2019-04-17 00:43:15 +05:30
bradley
0479d42e31 Merge pull request #168 from CodeLingoBot/rewrite
Fix function comments based on best practices from Effective Go
2019-03-07 09:41:02 +05:30
CodeLingo Bot
b401e7b17e Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-03-07 02:33:29 +00:00
Bradley Cicenas
9592de82a0 add keyboard shortcuts to container menu 2019-01-24 14:06:40 +00:00
bradley
29fa8cf3e7 Update status.md 2019-01-24 08:02:52 -05:00
bradley
c49939f965 Update status.md 2019-01-24 08:02:13 -05:00
bradley
2f7bc2a172 Update status.md 2019-01-24 08:00:51 -05:00
Bradley Cicenas
7b4d4db049 add status indicator doc 2019-01-24 12:50:59 +00:00
Bradley Cicenas
70bd2ae3a3 v0.7.2 2019-01-24 11:50:49 +00:00
Bradley Cicenas
665e8fdd06 move to go module 2018-12-01 17:50:47 +00:00
Stanislav Pavlovichev
101ddad692 Fixed a problem with rendering 2018-10-28 13:43:24 +02:00
Stanislav Pavlovichev
ca35ef2aab Unnecessary loop stopping 2018-10-28 12:07:43 +02:00
Stanislav Pavlovichev
d59c91a461 Do not allow to close /dev/stdin 2018-10-26 17:08:33 +03:00
bradley
a39b7a3a3e Merge pull request #152 from barthr/master
Refactoring improvements based on linting issues
2018-10-26 09:00:02 -05:00
bartfokker
77f5e6b735 remove ignore of variable (unneeded when only index is needed) 2018-10-25 22:25:36 +02:00
bartfokker
3c83b7576b refactor string on multiple places to constant 2018-10-25 22:23:44 +02:00
bartfokker
8a0bd3cf8a remove unneeded cast 2018-10-25 22:22:28 +02:00
bartfokker
78caad2dbd depend on io.WriteCloser instead of net.Conn 2018-10-25 22:22:04 +02:00
bartfokker
8d8f1e72eb rename ConfigFile to File because config.ConfigFile stutters. Instead it's config.File 2018-10-25 22:21:08 +02:00
bartfokker
93556a1754 replace += with ++ 2018-10-25 22:17:53 +02:00
bartfokker
4d247f5272 replace unkeyed fiels with keyed fields when instantiating log struct 2018-10-25 22:17:05 +02:00
bartfokker
db3d7e8927 change strings.Index for strings.Contains 2018-10-25 22:14:00 +02:00
bartfokker
efef345665 remove unneeded fmt.Sprintf 2018-10-25 22:13:04 +02:00
bartfokker
f158fa742f simplify append operation by omitting loop 2018-10-25 22:12:46 +02:00
bartfokker
4d48245d7d improve boolean logic 2018-10-25 22:12:17 +02:00
bartfokker
6bee1b7f31 remove unneeded select for simple channel receive 2018-10-25 22:11:17 +02:00
bartfokker
7118e45f3a add vendor directory to gitignore 2018-10-25 22:04:40 +02:00
Stanislav Pavlovichev
a26fc9169c Ability to change Shell 2018-10-25 21:58:33 +03:00
Stanislav Pavlovichev
967a87a65f Exec using API 2018-10-13 08:33:53 +03:00
Stanislav Pavlovichev
e68f7ba96a fix: handlers used to work after "exec sh" command
feature: hot key for "exec sh"
2018-10-12 10:03:27 +03:00
bradley
3405d19be8 Merge pull request #147 from serg-bloim/env-var
Display environment variables on single view page
2018-10-10 21:45:08 +08:00
Stanislav Pavlovichev
f27de1c29e Exec Sh Feature 2018-10-07 16:46:32 +03:00
Serhii Bilonozhko
9a185b2388 env-var 2018-10-05 17:35:22 -04:00
Bradley Cicenas
caf6fc63c1 add config toggle for full-row cursor 2018-09-17 01:33:52 +00:00
Bradley Cicenas
cf352f7c8a implement full-row cursor highlighting 2018-09-17 01:24:06 +00:00
bradley
ac5bed210f Merge pull request #143 from jphautin/add_networks_ips
add IP of networks in single view mode
2018-09-15 10:43:22 +09:00
Jean-Philippe
a72d43526f add IP of networks in single view mode 2018-09-06 21:01:16 +02:00
bradley
9eb2457aa4 Merge pull request #132 from xiechengsheng/fix-120
add support for alternative navigation
2018-06-30 08:31:44 +02:00
Bradley Cicenas
b83402b886 add TERM env var to Dockerfile 2018-06-28 11:19:07 +00:00
xiechengsheng
078564bd38 add support for alternative navigation
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
2018-06-28 16:40:26 +08:00
bradley
a2c08d312e Merge pull request #131 from xiechengsheng/add-pause-unpause
Feature: add more commands in container manager menu
2018-06-28 10:25:45 +02:00
xiechengsheng
f7a3d38d6b add more commands in container manager menu
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
2018-06-22 15:41:16 +08:00
Bradley Cicenas
a3b8585697 add requirement check to install script 2018-06-13 09:20:05 +00:00
bradley
2e526e9b86 Merge pull request #130 from felipeconti/master
Installing on /usr/local/bin as root
2018-06-13 11:04:34 +02:00
Felipe B. Conti
541fe70b78 Remove unnecessary treatment 2018-06-10 14:12:07 -03:00
Felipe Conti
c786b697bf Add function command_exists 2018-06-10 14:04:34 -03:00
Felipe Conti
aa6c00b083 Treatment to use root 2018-06-10 14:02:42 -03:00
Bradley Cicenas
17855e3d8e add container name to log view title 2018-05-10 09:53:59 +00:00
Bradley Cicenas
842809bef5 enable termbox alt input 2018-05-10 09:44:57 +00:00
Bradley Cicenas
4e567ee007 update README 2018-03-09 05:37:36 +00:00
Bradley Cicenas
56700e120b add go-winio dep 2018-03-09 05:28:11 +00:00
Bradley Cicenas
5261444265 v0.7.1 2018-03-09 05:19:34 +00:00
Bradley Cicenas
b3aa291182 update termbox-go dependency 2018-03-09 05:15:55 +00:00
Bradley Cicenas
051b474bf0 dep updates 2018-02-23 13:57:59 +09:30
Bradley Cicenas
fac6632459 build image from go 1.10 2018-02-22 14:58:21 +09:30
Bradley Cicenas
1c7cf98e58 handle window resize in help dialog 2018-02-02 15:21:33 +00:00
Bradley Cicenas
44a54e070d combine conditionals 2018-02-01 16:55:23 +00:00
Bradley Cicenas
10b9a6c013 include windows build 2018-01-31 17:05:54 +00:00
Bradley Cicenas
a3b67e4607 add available connectors to help dialog 2018-01-30 07:54:13 -03:00
Bradley Cicenas
ac1ce18143 refactor enabled connectors 2018-01-29 12:47:10 +00:00
bradley
01a305d326 Update README.md
escape brackets
2018-01-11 15:58:37 -03:00
66 changed files with 2204 additions and 835 deletions

View File

@@ -7,7 +7,7 @@ jobs:
steps:
- checkout
- setup_remote_docker:
version: 17.05.0-ce
version: 19.03.13
- run: make image
- deploy:
command: |

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
ctop
.idea
/vendor/

View File

@@ -1,16 +1,17 @@
FROM quay.io/vektorcloud/go:1.9
FROM quay.io/vektorcloud/go:1.13
RUN apk add --no-cache make
COPY Gopkg.* /go/src/github.com/bcicen/ctop/
WORKDIR /go/src/github.com/bcicen/ctop/
RUN dep ensure -vendor-only
WORKDIR /app
COPY go.mod .
RUN go mod download
COPY . /go/src/github.com/bcicen/ctop
COPY . .
RUN make build && \
mkdir -p /go/bin && \
mv -v ctop /go/bin/
FROM scratch
ENV TERM=linux
COPY --from=0 /go/bin/ctop /ctop
ENTRYPOINT ["/ctop"]

159
Gopkg.lock generated
View File

@@ -1,159 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/Azure/go-ansiterm"
packages = [".","winterm"]
revision = "fa152c58bc15761d0200cb75fe958b89a9d4888e"
[[projects]]
name = "github.com/Microsoft/go-winio"
packages = ["."]
revision = "fff283ad5116362ca252298cfc9b95828956d85d"
version = "v0.3.8"
[[projects]]
branch = "master"
name = "github.com/Nvveen/Gotty"
packages = ["."]
revision = "cd527374f1e5bff4938207604a14f2e38a9cf512"
[[projects]]
name = "github.com/Sirupsen/logrus"
packages = ["."]
revision = "26709e2714106fb8ad40b773b711ebce25b78914"
[[projects]]
name = "github.com/c9s/goprocinfo"
packages = ["linux"]
revision = "b34328d6e0cd139894ea7347d2624ccf31fa3c58"
[[projects]]
name = "github.com/coreos/go-systemd"
packages = ["dbus","util"]
revision = "b4a58d95188dd092ae20072bac14cece0e67c388"
version = "v4"
[[projects]]
name = "github.com/docker/docker"
packages = ["api/types","api/types/blkiodev","api/types/container","api/types/filters","api/types/mount","api/types/network","api/types/registry","api/types/strslice","api/types/swarm","api/types/versions","opts","pkg/archive","pkg/fileutils","pkg/homedir","pkg/idtools","pkg/ioutils","pkg/jsonlog","pkg/jsonmessage","pkg/longpath","pkg/mount","pkg/pools","pkg/promise","pkg/stdcopy","pkg/symlink","pkg/system","pkg/term","pkg/term/windows"]
revision = "90d35abf7b3535c1c319c872900fbd76374e521c"
version = "v17.05.0-ce-rc3"
[[projects]]
name = "github.com/docker/go-connections"
packages = ["nat"]
revision = "a2afab9802043837035592f1c24827fb70766de9"
[[projects]]
branch = "master"
name = "github.com/docker/go-units"
packages = ["."]
revision = "0dadbb0345b35ec7ef35e228dabb8de89a65bf52"
[[projects]]
name = "github.com/fsouza/go-dockerclient"
packages = ["."]
revision = "318513eb1ab27495afbc67f671ba1080513d8aa0"
[[projects]]
branch = "barchart-numfmt"
name = "github.com/gizak/termui"
packages = ["."]
revision = "ea10e6ccee219e572ffad0ac1909f1a17f6db7d6"
source = "https://github.com/bcicen/termui"
[[projects]]
name = "github.com/godbus/dbus"
packages = ["."]
revision = "c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f"
version = "v3"
[[projects]]
branch = "master"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "0a4f71a498b7c4812f64969510bcb4eca251e33a"
[[projects]]
branch = "master"
name = "github.com/hashicorp/go-cleanhttp"
packages = ["."]
revision = "3573b8b52aa7b37b9358d966a898feb387f62437"
[[projects]]
branch = "master"
name = "github.com/jgautheron/codename-generator"
packages = ["."]
revision = "16d037c7cc3c9b552fe4af9828b7338d752dbaf9"
[[projects]]
name = "github.com/maruel/panicparse"
packages = ["stack"]
revision = "25bcac0d793cf4109483505a0d66e066a3a90a80"
[[projects]]
name = "github.com/mattn/go-runewidth"
packages = ["."]
revision = "14207d285c6c197daabb5c9793d63e7af9ab2d50"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-wordwrap"
packages = ["."]
revision = "ad45545899c7b13c020ea92b2072220eefad42b8"
[[projects]]
name = "github.com/nsf/termbox-go"
packages = ["."]
revision = "91bae1bb5fa9ee504905ecbe7043fa30e92feaa3"
[[projects]]
branch = "master"
name = "github.com/nu7hatch/gouuid"
packages = ["."]
revision = "179d4d0c4d8d407a32af483c2354df1d2c91e6c3"
[[projects]]
name = "github.com/op/go-logging"
packages = ["."]
revision = "b2cb9fa56473e98db8caba80237377e83fe44db5"
version = "v1"
[[projects]]
name = "github.com/opencontainers/runc"
packages = ["libcontainer","libcontainer/apparmor","libcontainer/cgroups","libcontainer/cgroups/fs","libcontainer/cgroups/systemd","libcontainer/configs","libcontainer/configs/validate","libcontainer/criurpc","libcontainer/keys","libcontainer/label","libcontainer/seccomp","libcontainer/selinux","libcontainer/stacktrace","libcontainer/system","libcontainer/user","libcontainer/utils"]
revision = "baf6536d6259209c3edfa2b22237af82942d3dfa"
version = "v0.1.1"
[[projects]]
name = "github.com/seccomp/libseccomp-golang"
packages = ["."]
revision = "1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1"
[[projects]]
name = "github.com/syndtr/gocapability"
packages = ["capability"]
revision = "2c00daeb6c3b45114c80ac44119e7b8801fdd852"
[[projects]]
name = "github.com/vishvananda/netlink"
packages = [".","nl"]
revision = "1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270"
[[projects]]
name = "golang.org/x/net"
packages = ["context","context/ctxhttp"]
revision = "a6577fac2d73be281a500b310739095313165611"
[[projects]]
name = "golang.org/x/sys"
packages = ["unix","windows"]
revision = "99f16d856c9836c42d24e7ab64ea72916925fa97"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "cf4dacc32111b22d72ac23189b826c8316ec265e55bf987338c7a00633af788e"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,47 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/fsouza/go-dockerclient"
revision = "318513eb1ab27495afbc67f671ba1080513d8aa0"
[[constraint]]
branch = "barchart-numfmt"
name = "github.com/gizak/termui"
source = "https://github.com/bcicen/termui"
[[constraint]]
branch = "master"
name = "github.com/jgautheron/codename-generator"
[[constraint]]
branch = "master"
name = "github.com/nu7hatch/gouuid"
[[constraint]]
name = "github.com/op/go-logging"
version = "1.0.0"
[[constraint]]
name = "github.com/opencontainers/runc"
version = "0.1.1"

View File

@@ -1,27 +1,29 @@
NAME=ctop
VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD)
EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
clean:
rm -rf _build/ release/
build:
dep ensure
go mod download
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-dev:
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
build-all:
mkdir -p _build
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64
cd _build; sha256sum * > sha256sums.txt
run-dev:
rm -f ctop.sock ctop
go build -ldflags $(LD_FLAGS) -o ctop
CTOP_DEBUG=1 ./ctop
image:
docker build -t ctop -f Dockerfile .

View File

@@ -9,7 +9,7 @@ Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
as well as an [single container view][single_view] for inspecting a specific container.
as well as a [single container view][single_view] for inspecting a specific container.
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
@@ -20,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
#### Linux
```bash
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7/ctop-0.7-linux-amd64 -O /usr/local/bin/ctop
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.4/ctop-0.7.4-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop
```
@@ -31,7 +31,7 @@ brew install ctop
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7/ctop-0.7-darwin-amd64
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.4/ctop-0.7.4-darwin-amd64
sudo chmod +x /usr/local/bin/ctop
```
@@ -40,7 +40,7 @@ sudo chmod +x /usr/local/bin/ctop
```bash
docker run --rm -ti \
--name=ctop \
-v /var/run/docker.sock:/var/run/docker.sock \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop:latest
```
@@ -56,36 +56,41 @@ Build steps can be found [here][build].
### Config file
While running, use `S` to save the current filters, sort field, and other options to a default config path. These settings will be loaded and applied the next time `ctop` is started.
While running, use `S` to save the current filters, sort field, and other options to a default config path (`~/.config/ctop/config` on XDG systems, else `~/.ctop`).
Config file values will be loaded and applied the next time `ctop` is started.
### Options
Option | Description
--- | ---
-a | show active containers only
-f <string> | set an initial filter string
-h | display help dialog
-i | invert default colors
-r | reverse container sort order
-s | select initial container sort field
-scale-cpu | show cpu as % of system total
-v | output version information and exit
`-a` | show active containers only
`-f <string>` | set an initial filter string
`-h` | display help dialog
`-i` | invert default colors
`-r` | reverse container sort order
`-s` | select initial container sort field
`-scale-cpu` | show cpu as % of system total
`-v` | output version information and exit
`-shell` | exec shell to use (default: sh)
### Keybindings
Key | Action
--- | ---
<enter> | Open container menu
a | Toggle display of all (running and non-running) containers
f | Filter displayed containers (`esc` to clear when open)
H | Toggle ctop header
h | Open help dialog
s | Select container sort field
r | Reverse container sort order
o | Open single view
l | View container logs (`t` to toggle timestamp when open)
S | Save current configuration to file
q | Quit ctop
| Key | Action |
| :----------------------: | ---------------------------------------------------------- |
| <kbd>&lt;ENTER&gt;</kbd> | Open container menu |
| <kbd>a</kbd> | Toggle display of all (running and non-running) containers |
| <kbd>f</kbd> | Filter displayed containers (`esc` to clear when open) |
| <kbd>H</kbd> | Toggle ctop header |
| <kbd>h</kbd> | Open help dialog |
| <kbd>s</kbd> | Select container sort field |
| <kbd>r</kbd> | Reverse container sort order |
| <kbd>o</kbd> | Open single view |
| <kbd>l</kbd> | View container logs (`t` to toggle timestamp when open) |
| <kbd>e</kbd> | Exec Shell |
| <kbd>c</kbd> | Configure columns |
| <kbd>S</kbd> | Save current configuration to file |
| <kbd>q</kbd> | Quit ctop |
[build]: _docs/build.md
[connectors]: _docs/connectors.md

View File

@@ -1 +1 @@
0.7
0.7.4

View File

@@ -1,10 +1,8 @@
# Build
To build `ctop` from source, ensure you have [dep](https://github.com/golang/dep) installed and run:
To build `ctop` from source, simply clone the repo and run:
```bash
go get github.com/bcicen/ctop && \
cd $GOPATH/src/github.com/bcicen/ctop && \
make build
```
@@ -16,5 +14,8 @@ make image
Now you can run your local image:
```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
docker run --rm -ti \
--name ctop \
-v /var/run/docker.sock:/var/run/docker.sock \
ctop:latest
```

View File

@@ -1,4 +1,4 @@
# connectors
# Connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.

BIN
_docs/img/status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

30
_docs/status.md Normal file
View File

@@ -0,0 +1,30 @@
# Status Indicator
The `ctop` grid view provides a compact status indicator to convey container state
<img width="200px" src="img/status.png" alt="ctop"/>
### Status
<span align="center">
Appearance | Description
--- | ---
red | container is stopped
green | container is running
▮▮ | container is paused
</span>
### Health
If the container is configured with a health check, a `+` will appear next to the indicator
<span align="center">
Appearance | Description
--- | ---
red | health check in failed state
yellow | health check in starting state
green | health check in OK state
</span>

View File

@@ -52,7 +52,7 @@ var ColorMap = map[string]ui.Attribute{
func InvertColorMap() {
re := regexp.MustCompile(".*.fg")
for k, _ := range ColorMap {
for k := range ColorMap {
if re.FindAllString(k, 1) != nil {
ColorMap[k] = ui.ColorBlack
}

145
config/columns.go Normal file
View File

@@ -0,0 +1,145 @@
package config
import (
"strings"
)
// defaults
var defaultColumns = []Column{
Column{
Name: "status",
Label: "Status Indicator",
Enabled: true,
},
Column{
Name: "name",
Label: "Container Name",
Enabled: true,
},
Column{
Name: "id",
Label: "Container ID",
Enabled: true,
},
Column{
Name: "cpu",
Label: "CPU Usage",
Enabled: true,
},
Column{
Name: "mem",
Label: "Memory Usage",
Enabled: true,
},
Column{
Name: "net",
Label: "Network RX/TX",
Enabled: true,
},
Column{
Name: "io",
Label: "Disk IO Read/Write",
Enabled: true,
},
Column{
Name: "pids",
Label: "Container PID Count",
Enabled: true,
},
}
type Column struct {
Name string
Label string
Enabled bool
}
// ColumnsString returns an ordered and comma-delimited string of currently enabled Columns
func ColumnsString() string { return strings.Join(EnabledColumns(), ",") }
// EnabledColumns returns an ordered array of enabled column names
func EnabledColumns() (a []string) {
lock.RLock()
defer lock.RUnlock()
for _, col := range GlobalColumns {
if col.Enabled {
a = append(a, col.Name)
}
}
return a
}
// ColumnToggle toggles the enabled status of a given column name
func ColumnToggle(name string) {
col := GlobalColumns[colIndex(name)]
col.Enabled = !col.Enabled
log.Noticef("config change [column-%s]: %t -> %t", col.Name, !col.Enabled, col.Enabled)
}
// ColumnLeft moves the column with given name up one position, if possible
func ColumnLeft(name string) {
idx := colIndex(name)
if idx > 0 {
swapCols(idx, idx-1)
}
}
// ColumnRight moves the column with given name up one position, if possible
func ColumnRight(name string) {
idx := colIndex(name)
if idx < len(GlobalColumns)-1 {
swapCols(idx, idx+1)
}
}
// Set Column order and enabled status from one or more provided Column names
func SetColumns(names []string) {
var (
n int
curColStr = ColumnsString()
newColumns = make([]*Column, len(GlobalColumns))
)
lock.Lock()
// add enabled columns by name
for _, name := range names {
newColumns[n] = popColumn(name)
newColumns[n].Enabled = true
n++
}
// extend with omitted columns as disabled
for _, col := range GlobalColumns {
newColumns[n] = col
newColumns[n].Enabled = false
n++
}
GlobalColumns = newColumns
lock.Unlock()
log.Noticef("config change [columns]: %s -> %s", curColStr, ColumnsString())
}
func swapCols(i, j int) { GlobalColumns[i], GlobalColumns[j] = GlobalColumns[j], GlobalColumns[i] }
func popColumn(name string) *Column {
idx := colIndex(name)
if idx < 0 {
panic("no such column name: " + name)
}
col := GlobalColumns[idx]
GlobalColumns = append(GlobalColumns[:idx], GlobalColumns[idx+1:]...)
return col
}
// return index of column with given name, if any
func colIndex(name string) int {
for n, c := range GlobalColumns {
if c.Name == name {
return n
}
}
return -1
}

View File

@@ -13,27 +13,36 @@ var (
xdgRe = regexp.MustCompile("^XDG_*")
)
type ConfigFile struct {
type File struct {
Options map[string]string `toml:"options"`
Toggles map[string]bool `toml:"toggles"`
}
func exportConfig() ConfigFile {
c := ConfigFile{
func exportConfig() File {
// update columns param from working config
Update("columns", ColumnsString())
lock.RLock()
defer lock.RUnlock()
c := File{
Options: make(map[string]string),
Toggles: make(map[string]bool),
}
for _, p := range GlobalParams {
c.Options[p.Key] = p.Val
}
for _, sw := range GlobalSwitches {
c.Toggles[sw.Key] = sw.Val
}
return c
}
//
func Read() error {
var config ConfigFile
var config File
path, err := getConfigPath()
if err != nil {
@@ -43,13 +52,26 @@ func Read() error {
if _, err := toml.DecodeFile(path, &config); err != nil {
return err
}
for k, v := range config.Options {
Update(k, v)
}
for k, v := range config.Toggles {
UpdateSwitch(k, v)
}
// set working column config, if provided
colStr := GetVal("columns")
if len(colStr) > 0 {
var colNames []string
for _, s := range strings.Split(colStr, ",") {
s = strings.TrimSpace(s)
if s != "" {
colNames = append(colNames, strings.TrimSpace(s))
}
}
SetColumns(colNames)
}
return nil
}
@@ -68,6 +90,13 @@ func Write() (path string, err error) {
}
}
// remove prior to writing new file
if err := os.Remove(path); err != nil {
if !os.IsNotExist(err) {
return path, err
}
}
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return path, fmt.Errorf("failed to open config for writing: %s", err)

View File

@@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"sync"
"github.com/bcicen/ctop/logging"
)
@@ -10,17 +11,24 @@ import (
var (
GlobalParams []*Param
GlobalSwitches []*Switch
GlobalColumns []*Column
lock sync.RWMutex
log = logging.Init()
)
func Init() {
for _, p := range params {
for _, p := range defaultParams {
GlobalParams = append(GlobalParams, p)
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
log.Infof("loaded default config param [%s]: %s", quote(p.Key), quote(p.Val))
}
for _, s := range switches {
for _, s := range defaultSwitches {
GlobalSwitches = append(GlobalSwitches, s)
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)
log.Infof("loaded default config switch [%s]: %t", quote(s.Key), s.Val)
}
for _, c := range defaultColumns {
x := c
GlobalColumns = append(GlobalColumns, &x)
log.Infof("loaded default widget config [%s]: %t", quote(x.Name), x.Enabled)
}
}

View File

@@ -1,7 +1,7 @@
package config
// defaults
var params = []*Param{
var defaultParams = []*Param{
&Param{
Key: "filterStr",
Val: "",
@@ -12,6 +12,16 @@ var params = []*Param{
Val: "state",
Label: "Container Sort Field",
},
&Param{
Key: "shell",
Val: "sh",
Label: "Shell",
},
&Param{
Key: "columns",
Val: "status,name,id,cpu,mem,net,io,pids",
Label: "Enabled Columns",
},
}
type Param struct {
@@ -22,6 +32,9 @@ type Param struct {
// Get Param by key
func Get(k string) *Param {
lock.RLock()
defer lock.RUnlock()
for _, p := range GlobalParams {
if p.Key == k {
return p
@@ -30,7 +43,7 @@ func Get(k string) *Param {
return &Param{} // default
}
// Get Param value by key
// GetVal gets Param value by key
func GetVal(k string) string {
return Get(k).Val
}
@@ -38,7 +51,10 @@ func GetVal(k string) string {
// Set param value
func Update(k, v string) {
p := Get(k)
log.Noticef("config change: %s: %s -> %s", k, quote(p.Val), quote(v))
log.Noticef("config change [%s]: %s -> %s", k, quote(p.Val), quote(v))
lock.Lock()
defer lock.Unlock()
p.Val = v
// log.Errorf("ignoring update for non-existant parameter: %s", k)
}

View File

@@ -1,21 +1,26 @@
package config
// defaults
var switches = []*Switch{
var defaultSwitches = []*Switch{
&Switch{
Key: "sortReversed",
Val: false,
Label: "Reverse Sort Order",
Label: "Reverse sort order",
},
&Switch{
Key: "allContainers",
Val: true,
Label: "Show All Containers",
Label: "Show all containers",
},
&Switch{
Key: "fullRowCursor",
Val: true,
Label: "Highlight entire cursor row (vs. name only)",
},
&Switch{
Key: "enableHeader",
Val: true,
Label: "Enable Status Header",
Label: "Enable status header",
},
&Switch{
Key: "scaleCpu",
@@ -30,8 +35,11 @@ type Switch struct {
Label string
}
// Return Switch by key
// GetSwitch returns Switch by key
func GetSwitch(k string) *Switch {
lock.RLock()
defer lock.RUnlock()
for _, sw := range GlobalSwitches {
if sw.Key == k {
return sw
@@ -40,15 +48,19 @@ func GetSwitch(k string) *Switch {
return &Switch{} // default
}
// Return Switch value by key
// GetSwitchVal returns Switch value by key
func GetSwitchVal(k string) bool {
return GetSwitch(k).Val
}
func UpdateSwitch(k string, val bool) {
sw := GetSwitch(k)
lock.Lock()
defer lock.Unlock()
if sw.Val != val {
log.Noticef("config change: %s: %t -> %t", k, sw.Val, val)
log.Noticef("config change [%s]: %t -> %t", k, sw.Val, val)
sw.Val = val
}
}
@@ -56,8 +68,11 @@ func UpdateSwitch(k string, val bool) {
// Toggle a boolean switch
func Toggle(k string) {
sw := GetSwitch(k)
newVal := sw.Val != true
log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
sw.Val = newVal
lock.Lock()
defer lock.Unlock()
sw.Val = !sw.Val
log.Noticef("config change [%s]: %t -> %t", k, !sw.Val, sw.Val)
//log.Errorf("ignoring toggle for non-existant switch: %s", k)
}

View File

@@ -114,10 +114,10 @@ func (c *Docker) ReadIO(stats *api.Stats) {
var read, write int64
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
if blk.Op == "Read" {
read = int64(blk.Value)
read += int64(blk.Value)
}
if blk.Op == "Write" {
write = int64(blk.Value)
write += int64(blk.Value)
}
}
c.IOBytesRead, c.IOBytesWrite = read, write

View File

@@ -34,12 +34,13 @@ func (l *DockerLogs) Stream() chan models.Log {
Context: ctx,
Container: l.id,
OutputStream: w,
ErrorStream: w,
Stdout: true,
Stderr: true,
Tail: "10",
Follow: true,
Timestamps: true,
//ErrorStream: w,
Stdout: true,
Stderr: true,
Tail: "20",
Follow: true,
Timestamps: true,
RawTerminal: true,
}
// read io pipe into channel
@@ -48,7 +49,7 @@ func (l *DockerLogs) Stream() chan models.Log {
for scanner.Scan() {
parts := strings.Split(scanner.Text(), " ")
ts := l.parseTime(parts[0])
logCh <- models.Log{ts, strings.Join(parts[1:], " ")}
logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
}
}()
@@ -62,10 +63,8 @@ func (l *DockerLogs) Stream() chan models.Log {
}()
go func() {
select {
case <-l.done:
cancel()
}
<-l.done
cancel()
}()
log.Infof("log reader started for container: %s", l.id)
@@ -76,9 +75,25 @@ func (l *DockerLogs) Stop() { l.done <- true }
func (l *DockerLogs) parseTime(s string) time.Time {
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
if err == nil {
return ts
}
return ts
ts, err2 := time.Parse("2006-01-02T15:04:05.000000000Z", l.stripPfx(s))
if err2 == nil {
return ts
}
log.Errorf("failed to parse container log: %s", err)
log.Errorf("failed to parse container log2: %s", err2)
return time.Now()
}
// attempt to strip message header prefix from a given raw docker log string
func (l *DockerLogs) stripPfx(s string) string {
b := []byte(s)
if len(b) > 8 {
return string(b[8:])
}
return s
}

View File

@@ -20,7 +20,7 @@ func (l *MockLogs) Stream() chan models.Log {
case <-l.done:
break
default:
logCh <- models.Log{time.Now(), mockLog}
logCh <- models.Log{Timestamp: time.Now(), Message: mockLog}
time.Sleep(250 * time.Millisecond)
}
}

View File

@@ -1,16 +1,17 @@
// +build !darwin
// +build linux
package collector
import (
linuxproc "github.com/c9s/goprocinfo/linux"
"github.com/opencontainers/runc/libcontainer/system"
)
var sysMemTotal = getSysMemTotal()
var clockTicksPerSecond = uint64(system.GetClockTicks())
const nanoSecondsPerSecond = 1e9
const (
clockTicksPerSecond uint64 = 100
nanoSecondsPerSecond = 1e9
)
func getSysMemTotal() int64 {
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")

View File

@@ -1,14 +1,16 @@
// +build !darwin
// +build linux
package collector
import (
"time"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/types"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
)
// Runc collector
@@ -112,7 +114,7 @@ func (c *Runc) ReadMem(stats *cgroups.Stats) {
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {
var rx, tx int64
for _, network := range interfaces {
rx += int64(network.RxBytes)

View File

@@ -11,31 +11,51 @@ import (
api "github.com/fsouza/go-dockerclient"
)
func init() { enabled["docker"] = NewDocker }
type Docker struct {
client *api.Client
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
closed chan struct{}
lock sync.RWMutex
}
func NewDocker() Connector {
func NewDocker() (Connector, error) {
// init docker client
client, err := api.NewClientFromEnv()
if err != nil {
panic(err)
return nil, err
}
cm := &Docker{
client: client,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
closed: make(chan struct{}),
lock: sync.RWMutex{},
}
// query info as pre-flight healthcheck
info, err := client.Info()
if err != nil {
return nil, err
}
log.Debugf("docker-connector ID: %s", info.ID)
log.Debugf("docker-connector Driver: %s", info.Driver)
log.Debugf("docker-connector Images: %d", info.Images)
log.Debugf("docker-connector Name: %s", info.Name)
log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
go cm.Loop()
cm.refreshAll()
go cm.watchEvents()
return cm
return cm, nil
}
// Docker implements Connector
func (cm *Docker) Wait() struct{} { return <-cm.closed }
// Docker events watcher
func (cm *Docker) watchEvents() {
log.Info("docker event listener starting")
@@ -58,6 +78,8 @@ func (cm *Docker) watchEvents() {
cm.delByID(e.ID)
}
}
log.Info("docker event listener exited")
close(cm.closed)
}
func portsFormat(ports map[api.Port][]api.PortBinding) string {
@@ -78,6 +100,17 @@ func portsFormat(ports map[api.Port][]api.PortBinding) string {
return strings.Join(append(exposed, published...), "\n")
}
func ipsFormat(networks map[string]api.ContainerNetwork) string {
var ips []string
for k, v := range networks {
s := fmt.Sprintf("%s:%s", k, v.IPAddress)
ips = append(ips, s)
}
return strings.Join(ips, "\n")
}
func (cm *Docker) refresh(c *container.Container) {
insp := cm.inspect(c.Id)
// remove container if no longer exists
@@ -87,17 +120,21 @@ func (cm *Docker) refresh(c *container.Container) {
}
c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image)
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", insp.State.Health.Status)
for _, env := range insp.Config.Env {
c.SetMeta("[ENV-VAR]", env)
}
c.SetState(insp.State.Status)
}
func (cm *Docker) inspect(id string) *api.Container {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, ok := err.(*api.NoSuchContainer); ok == false {
log.Errorf(err.Error())
if _, ok := err.(*api.NoSuchContainer); !ok {
log.Errorf("%s (%T)", err.Error(), err)
}
}
return c
@@ -108,7 +145,8 @@ func (cm *Docker) refreshAll() {
opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
panic(err)
log.Errorf("%s (%T)", err.Error(), err)
return
}
for _, i := range allContainers {
@@ -120,13 +158,18 @@ func (cm *Docker) refreshAll() {
}
func (cm *Docker) Loop() {
for id := range cm.needsRefresh {
c := cm.MustGet(id)
cm.refresh(c)
for {
select {
case id := <-cm.needsRefresh:
c := cm.MustGet(id)
cm.refresh(c)
case <-cm.closed:
return
}
}
}
// Get a single container, creating one anew if not existing
// MustGet gets a single container, creating one anew if not existing
func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
// append container struct for new containers
@@ -144,7 +187,7 @@ func (cm *Docker) MustGet(id string) *container.Container {
return c
}
// Get a single container, by ID
// Docker implements Connector
func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
@@ -160,7 +203,7 @@ func (cm *Docker) delByID(id string) {
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
// Docker implements Connector
func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {

View File

@@ -1,7 +0,0 @@
// +build !linux
package connector
var enabled = map[string]func() Connector{
"docker": NewDocker,
}

View File

@@ -1,8 +0,0 @@
// +build !darwin
package connector
var enabled = map[string]func() Connector{
"docker": NewDocker,
"runc": NewRunc,
}

View File

@@ -2,25 +2,103 @@ package connector
import (
"fmt"
"sort"
"sync"
"time"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
var (
log = logging.Init()
enabled = make(map[string]ConnectorFn)
)
func ByName(s string) (Connector, error) {
if _, ok := enabled[s]; !ok {
msg := fmt.Sprintf("invalid connector type \"%s\"\nconnector must be one of:", s)
for k, _ := range enabled {
msg += fmt.Sprintf("\n %s", k)
}
return nil, fmt.Errorf(msg)
}
return enabled[s](), nil
}
type ConnectorFn func() (Connector, error)
type Connector interface {
// All returns a pre-sorted container.Containers of all discovered containers
All() container.Containers
// Get returns a single container.Container by ID
Get(string) (*container.Container, bool)
// Wait blocks until the underlying connection is lost
Wait() struct{}
}
// ConnectorSuper provides initial connection and retry on failure for
// an undlerying Connector type
type ConnectorSuper struct {
conn Connector
connFn ConnectorFn
err error
lock sync.RWMutex
}
func NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {
cs := &ConnectorSuper{
connFn: connFn,
err: fmt.Errorf("connecting..."),
}
go cs.loop()
return cs
}
// Get returns the underlying Connector, or nil and an error
// if the Connector is not yet initialized or is disconnected.
func (cs *ConnectorSuper) Get() (Connector, error) {
cs.lock.RLock()
defer cs.lock.RUnlock()
if cs.err != nil {
return nil, cs.err
}
return cs.conn, nil
}
func (cs *ConnectorSuper) setError(err error) {
cs.lock.Lock()
defer cs.lock.Unlock()
cs.err = err
}
func (cs *ConnectorSuper) loop() {
const interval = 3
for {
log.Infof("initializing connector")
conn, err := cs.connFn()
if err != nil {
cs.setError(err)
log.Errorf("failed to initialize connector: %s (%T)", err, err)
log.Errorf("retrying in %ds", interval)
time.Sleep(interval * time.Second)
} else {
cs.conn = conn
cs.setError(nil)
log.Infof("successfully initialized connector")
// wait until connection closed
cs.conn.Wait()
cs.setError(fmt.Errorf("attempting to reconnect..."))
log.Infof("connector closed")
}
}
}
// Enabled returns names for all enabled connectors on the current platform
func Enabled() (a []string) {
for k, _ := range enabled {
a = append(a, k)
}
sort.Strings(a)
return a
}
// ByName returns a ConnectorSuper for a given name, or error if the connector
// does not exists on the current platform
func ByName(s string) (*ConnectorSuper, error) {
if cfn, ok := enabled[s]; ok {
return NewConnectorSuper(cfn), nil
}
return nil, fmt.Errorf("invalid connector type \"%s\"", s)
}

View File

@@ -3,6 +3,9 @@ package manager
import (
"fmt"
api "github.com/fsouza/go-dockerclient"
"github.com/pkg/errors"
"io"
"os"
)
type Docker struct {
@@ -17,6 +20,88 @@ func NewDocker(client *api.Client, id string) *Docker {
}
}
// Do not allow to close reader (i.e. /dev/stdin which docker client tries to close after command execution)
type noClosableReader struct {
io.Reader
}
func (w *noClosableReader) Read(p []byte) (n int, err error) {
return w.Reader.Read(p)
}
const (
STDIN = 0
STDOUT = 1
STDERR = 2
)
var wrongFrameFormat = errors.New("Wrong frame format")
// A frame has a Header and a Payload
// Header: [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
// STREAM_TYPE can be:
// 0: stdin (is written on stdout)
// 1: stdout
// 2: stderr
// SIZE1, SIZE2, SIZE3, SIZE4 are the four bytes of the uint32 size encoded as big endian.
// But we don't use size, because we don't need to find the end of frame.
type frameWriter struct {
stdout io.Writer
stderr io.Writer
stdin io.Writer
}
func (w *frameWriter) Write(p []byte) (n int, err error) {
// drop initial empty frames
if len(p) == 0 {
return 0, nil
}
if len(p) > 8 {
var targetWriter io.Writer
switch p[0] {
case STDIN:
targetWriter = w.stdin
break
case STDOUT:
targetWriter = w.stdout
break
case STDERR:
targetWriter = w.stderr
break
default:
return 0, wrongFrameFormat
}
n, err := targetWriter.Write(p[8:])
return n + 8, err
}
return 0, wrongFrameFormat
}
func (dc *Docker) Exec(cmd []string) error {
execCmd, err := dc.client.CreateExec(api.CreateExecOptions{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
Container: dc.id,
Tty: true,
})
if err != nil {
return err
}
return dc.client.StartExec(execCmd.ID, api.StartExecOptions{
InputStream: &noClosableReader{os.Stdin},
OutputStream: &frameWriter{os.Stdout, os.Stderr, os.Stdin},
ErrorStream: os.Stderr,
RawTerminal: true,
})
}
func (dc *Docker) Start() error {
c, err := dc.client.InspectContainer(dc.id)
if err != nil {
@@ -42,3 +127,24 @@ func (dc *Docker) Remove() error {
}
return nil
}
func (dc *Docker) Pause() error {
if err := dc.client.PauseContainer(dc.id); err != nil {
return fmt.Errorf("cannot pause container: %v", err)
}
return nil
}
func (dc *Docker) Unpause() error {
if err := dc.client.UnpauseContainer(dc.id); err != nil {
return fmt.Errorf("cannot unpause container: %v", err)
}
return nil
}
func (dc *Docker) Restart() error {
if err := dc.client.RestartContainer(dc.id, 3); err != nil {
return fmt.Errorf("cannot restart container: %v", err)
}
return nil
}

View File

@@ -1,7 +1,15 @@
package manager
import "errors"
var ActionNotImplErr = errors.New("action not implemented")
type Manager interface {
Start() error
Stop() error
Remove() error
Pause() error
Unpause() error
Restart() error
Exec(cmd []string) error
}

View File

@@ -7,13 +7,29 @@ func NewMock() *Mock {
}
func (m *Mock) Start() error {
return nil
return ActionNotImplErr
}
func (m *Mock) Stop() error {
return nil
return ActionNotImplErr
}
func (m *Mock) Remove() error {
return nil
return ActionNotImplErr
}
func (m *Mock) Pause() error {
return ActionNotImplErr
}
func (m *Mock) Unpause() error {
return ActionNotImplErr
}
func (m *Mock) Restart() error {
return ActionNotImplErr
}
func (m *Mock) Exec(cmd []string) error {
return ActionNotImplErr
}

View File

@@ -7,13 +7,29 @@ func NewRunc() *Runc {
}
func (rc *Runc) Start() error {
return nil
return ActionNotImplErr
}
func (rc *Runc) Stop() error {
return nil
return ActionNotImplErr
}
func (rc *Runc) Remove() error {
return nil
return ActionNotImplErr
}
func (rc *Runc) Pause() error {
return ActionNotImplErr
}
func (rc *Runc) Unpause() error {
return ActionNotImplErr
}
func (rc *Runc) Restart() error {
return ActionNotImplErr
}
func (rc *Runc) Exec(cmd []string) error {
return ActionNotImplErr
}

View File

@@ -14,15 +14,17 @@ import (
"github.com/nu7hatch/gouuid"
)
func init() { enabled["mock"] = NewMock }
type Mock struct {
containers container.Containers
}
func NewMock() *Mock {
func NewMock() (Connector, error) {
cs := &Mock{}
go cs.Init()
go cs.Loop()
return cs
return cs, nil
}
// Create Mock containers
@@ -30,21 +32,46 @@ func (cs *Mock) Init() {
rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < 4; i++ {
cs.makeContainer(3)
cs.makeContainer(3, true)
}
for i := 0; i < 16; i++ {
cs.makeContainer(1)
cs.makeContainer(1, false)
}
}
func (cs *Mock) makeContainer(aggression int64) {
func (cs *Mock) Wait() struct{} {
ch := make(chan struct{})
go func() {
time.Sleep(30 * time.Second)
close(ch)
}()
return <-ch
}
var healthStates = []string{"starting", "healthy", "unhealthy"}
func (cs *Mock) makeContainer(aggression int64, health bool) {
collector := collector.NewMock(aggression)
manager := manager.NewMock()
c := container.New(makeID(), collector, manager)
c.SetMeta("name", makeName())
c.SetState(makeState())
if health {
var i int
c.SetMeta("health", healthStates[i])
go func() {
for {
i++
if i >= len(healthStates) {
i = 0
}
c.SetMeta("health", healthStates[i])
time.Sleep(12 * time.Second)
}
}()
}
cs.containers = append(cs.containers, c)
}
@@ -71,7 +98,7 @@ func (cs *Mock) Get(id string) (*container.Container, bool) {
return nil, false
}
// Return array of all containers, sorted by field
// All returns array of all containers, sorted by field
func (cs *Mock) All() container.Containers {
cs.containers.Sort()
cs.containers.Filter()

View File

@@ -1,4 +1,4 @@
// +build !darwin
// +build linux
package connector
@@ -17,6 +17,8 @@ import (
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
)
func init() { enabled["runc"] = NewRunc }
type RuncOpts struct {
root string // runc root path
systemdCgroups bool // use systemd cgroups
@@ -52,35 +54,44 @@ type Runc struct {
factory libcontainer.Factory
containers map[string]*container.Container
libContainers map[string]libcontainer.Container
closed chan struct{}
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewRunc() Connector {
func NewRunc() (Connector, error) {
opts, err := NewRuncOpts()
runcFailOnErr(err)
if err != nil {
return nil, err
}
factory, err := getFactory(opts)
runcFailOnErr(err)
if err != nil {
return nil, err
}
cm := &Runc{
opts: opts,
factory: factory,
containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container),
needsRefresh: make(chan string, 60),
closed: make(chan struct{}),
lock: sync.RWMutex{},
}
go func() {
for {
cm.refreshAll()
time.Sleep(5 * time.Second)
select {
case <-cm.closed:
return
case <-time.After(5 * time.Second):
cm.refreshAll()
}
}
}()
go cm.Loop()
return cm
return cm, nil
}
func (cm *Runc) GetLibc(id string) libcontainer.Container {
@@ -139,7 +150,11 @@ func (cm *Runc) refresh(id string) {
// Read runc root, creating any new containers
func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root)
runcFailOnErr(err)
if err != nil {
log.Errorf("%s (%T)", err.Error(), err)
close(cm.closed)
return
}
for _, i := range list {
if i.IsDir() {
@@ -166,7 +181,7 @@ func (cm *Runc) Loop() {
}
}
// Get a single ctop container in the map matching libc container, creating one anew if not existing
// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
if !ok {
@@ -197,14 +212,6 @@ func (cm *Runc) MustGet(id string) *container.Container {
return c
}
// Get a single container, by ID
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Remove containers by ID
func (cm *Runc) delByID(id string) {
cm.lock.Lock()
@@ -214,7 +221,18 @@ func (cm *Runc) delByID(id string) {
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
// Runc implements Connector
func (cm *Runc) Wait() struct{} { return <-cm.closed }
// Runc implements Connector
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Runc implements Connector
func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
@@ -229,7 +247,7 @@ func (cm *Runc) All() (containers container.Containers) {
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
cgroupManager := libcontainer.Cgroupfs
if opts.systemdCgroups {
if systemd.UseSystemd() {
if systemd.IsRunningSystemd() {
cgroupManager = libcontainer.SystemdCgroups
} else {
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
@@ -237,9 +255,3 @@ func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
}
return libcontainer.New(opts.root, cgroupManager)
}
func runcFailOnErr(err error) {
if err != nil {
panic(fmt.Errorf("fatal runc error: %s", err))
}
}

View File

@@ -13,12 +13,16 @@ var (
log = logging.Init()
)
const (
running = "running"
)
// Metrics and metadata representing a container
type Container struct {
models.Metrics
Id string
Meta map[string]string
Widgets *compact.Compact
Meta models.Meta
Widgets *compact.CompactRow
Display bool // display this container in compact view
updater cwidgets.WidgetUpdater
collector collector.Collector
@@ -26,11 +30,11 @@ type Container struct {
}
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
widgets := compact.NewCompact(id)
widgets := compact.NewCompactRow()
return &Container{
Metrics: models.NewMetrics(),
Id: id,
Meta: make(map[string]string),
Meta: models.NewMeta("id", id),
Widgets: widgets,
updater: widgets,
collector: collector,
@@ -38,39 +42,40 @@ func New(id string, collector collector.Collector, manager manager.Manager) *Con
}
}
func (c *Container) RecreateWidgets() {
c.SetUpdater(cwidgets.NullWidgetUpdater{})
c.Widgets = compact.NewCompactRow()
c.SetUpdater(c.Widgets)
}
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
c.updater = u
for k, v := range c.Meta {
c.updater.SetMeta(k, v)
}
c.updater.SetMeta(c.Meta)
}
func (c *Container) SetMeta(k, v string) {
c.Meta[k] = v
c.updater.SetMeta(k, v)
c.updater.SetMeta(c.Meta)
}
func (c *Container) GetMeta(k string) string {
if v, ok := c.Meta[k]; ok {
return v
}
return ""
return c.Meta.Get(k)
}
func (c *Container) SetState(s string) {
c.SetMeta("state", s)
// start collector, if needed
if s == "running" && !c.collector.Running() {
if s == running && !c.collector.Running() {
c.collector.Start()
c.Read(c.collector.Stream())
}
// stop collector, if needed
if s != "running" && c.collector.Running() {
if s != running && c.collector.Running() {
c.collector.Stop()
}
}
// Return container log collector
// Logs returns container log collector
func (c *Container) Logs() collector.LogCollector {
return c.collector.Logs()
}
@@ -90,18 +95,18 @@ func (c *Container) Read(stream chan models.Metrics) {
}
func (c *Container) Start() {
if c.Meta["state"] != "running" {
if c.Meta["state"] != running {
if err := c.manager.Start(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState("running")
c.SetState(running)
}
}
func (c *Container) Stop() {
if c.Meta["state"] == "running" {
if c.Meta["state"] == running {
if err := c.manager.Stop(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
@@ -117,3 +122,39 @@ func (c *Container) Remove() {
log.StatusErr(err)
}
}
func (c *Container) Pause() {
if c.Meta["state"] == running {
if err := c.manager.Pause(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState("paused")
}
}
func (c *Container) Unpause() {
if c.Meta["state"] == "paused" {
if err := c.manager.Unpause(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState(running)
}
}
func (c *Container) Restart() {
if c.Meta["state"] == running {
if err := c.manager.Restart(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
}
}
func (c *Container) Exec(cmd []string) error {
return c.manager.Exec(cmd)
}

View File

@@ -11,7 +11,7 @@ import (
type GridCursor struct {
selectedID string // id of currently selected container
filtered container.Containers
cSource connector.Connector
cSuper *connector.ConnectorSuper
isScrolling bool // toggled when actively scrolling
}
@@ -25,14 +25,20 @@ func (gc *GridCursor) Selected() *container.Container {
return nil
}
// Refresh containers from source
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
// Refresh containers from source, returning whether the quantity of
// containers has changed and any error
func (gc *GridCursor) RefreshContainers() (bool, error) {
oldLen := gc.Len()
// Containers filtered by display bool
gc.filtered = container.Containers{}
cSource, err := gc.cSuper.Get()
if err != nil {
return true, err
}
// filter Containers by display bool
var cursorVisible bool
for _, c := range gc.cSource.All() {
for _, c := range cSource.All() {
if c.Display {
if c.Id == gc.selectedID {
cursorVisible = true
@@ -41,31 +47,30 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
}
}
if oldLen != gc.Len() {
lenChanged = true
if !cursorVisible || gc.selectedID == "" {
gc.Reset()
}
if !cursorVisible {
gc.Reset()
}
if gc.selectedID == "" {
gc.Reset()
}
return lenChanged
return oldLen != gc.Len(), nil
}
// Set an initial cursor position, if possible
func (gc *GridCursor) Reset() {
for _, c := range gc.cSource.All() {
c.Widgets.Name.UnHighlight()
cSource, err := gc.cSuper.Get()
if err != nil {
return
}
for _, c := range cSource.All() {
c.Widgets.UnHighlight()
}
if gc.Len() > 0 {
gc.selectedID = gc.filtered[0].Id
gc.filtered[0].Widgets.Name.Highlight()
gc.filtered[0].Widgets.Highlight()
}
}
// Return current cursor index
// Idx returns current cursor index
func (gc *GridCursor) Idx() int {
for n, c := range gc.filtered {
if c.Id == gc.selectedID {
@@ -109,9 +114,9 @@ func (gc *GridCursor) Up() {
active := gc.filtered[idx]
next := gc.filtered[idx-1]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
@@ -128,9 +133,9 @@ func (gc *GridCursor) Down() {
active := gc.filtered[idx]
next := gc.filtered[idx+1]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
gc.ScrollPage()
ui.Render(cGrid)
@@ -151,9 +156,9 @@ func (gc *GridCursor) PgUp() {
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)
@@ -174,9 +179,9 @@ func (gc *GridCursor) PgDown() {
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.Name.UnHighlight()
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Name.Highlight()
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)

View File

@@ -0,0 +1,49 @@
package compact
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
var (
allCols = map[string]NewCompactColFn{
"status": NewStatus,
"name": NewNameCol,
"id": NewCIDCol,
"cpu": NewCPUCol,
"mem": NewMemCol,
"net": NewNetCol,
"io": NewIOCol,
"pids": NewPIDCol,
}
)
type NewCompactColFn func() CompactCol
func newRowWidgets() []CompactCol {
enabled := config.EnabledColumns()
cols := make([]CompactCol, len(enabled))
for n, name := range enabled {
wFn, ok := allCols[name]
if !ok {
panic("no such widget name: %s" + name)
}
cols[n] = wFn()
}
return cols
}
type CompactCol interface {
ui.GridBufferer
Reset()
Header() string // header text to display for column
FixedWidth() int // fixed width size. if == 0, width is automatically calculated
Highlight()
UnHighlight()
SetMeta(models.Meta)
SetMetrics(models.Metrics)
}

View File

@@ -1,21 +1,60 @@
package compact
import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
type GaugeCol struct {
*ui.Gauge
type CPUCol struct {
*GaugeCol
}
func NewGaugeCol() *GaugeCol {
g := ui.NewGauge()
func NewCPUCol() CompactCol {
return &CPUCol{NewGaugeCol("CPU")}
}
func (w *CPUCol) SetMetrics(m models.Metrics) {
val := m.CPUUtil
w.BarColor = colorScale(val)
w.Label = fmt.Sprintf("%d%%", val)
if val > 100 {
val = 100
}
w.Percent = val
}
type MemCol struct {
*GaugeCol
}
func NewMemCol() CompactCol {
return &MemCol{NewGaugeCol("MEM")}
}
func (w *MemCol) SetMetrics(m models.Metrics) {
w.BarColor = ui.ThemeAttr("gauge.bar.bg")
w.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.MemUsage), cwidgets.ByteFormat64Short(m.MemLimit))
w.Percent = m.MemPercent
}
type GaugeCol struct {
*ui.Gauge
header string
fWidth int
}
func NewGaugeCol(header string) *GaugeCol {
g := &GaugeCol{ui.NewGauge(), header, 0}
g.Height = 1
g.Border = false
g.Percent = 0
g.PaddingBottom = 0
g.Label = "-"
return &GaugeCol{g}
g.Reset()
return g
}
func (w *GaugeCol) Reset() {
@@ -23,6 +62,35 @@ func (w *GaugeCol) Reset() {
w.Percent = 0
}
func (w *GaugeCol) Buffer() ui.Buffer {
// if bar would not otherwise be visible, set a minimum
// percentage value and low-contrast color for structure
if w.Percent < 5 {
w.Percent = 5
w.BarColor = ui.ColorBlack
}
return w.Gauge.Buffer()
}
// GaugeCol implements CompactCol
func (w *GaugeCol) SetMeta(models.Meta) {}
func (w *GaugeCol) SetMetrics(models.Metrics) {}
func (w *GaugeCol) Header() string { return w.header }
func (w *GaugeCol) FixedWidth() int { return w.fWidth }
// GaugeCol implements CompactCol
func (w *GaugeCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg")
w.PercentColor = ui.ThemeAttr("par.text.hi")
}
// GaugeCol implements CompactCol
func (w *GaugeCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg")
w.PercentColor = ui.ThemeAttr("par.text.bg")
}
func colorScale(n int) ui.Attribute {
if n > 70 {
return ui.ThemeAttr("status.danger")

View File

@@ -4,11 +4,11 @@ import (
ui "github.com/gizak/termui"
)
var header *CompactHeader
type CompactGrid struct {
ui.GridBufferer
Rows []ui.GridBufferer
header *CompactHeader
cols []CompactCol // reference columns
Rows []RowBufferer
X, Y int
Width int
Height int
@@ -16,37 +16,64 @@ type CompactGrid struct {
}
func NewCompactGrid() *CompactGrid {
header = NewCompactHeader() // init column header
return &CompactGrid{}
cg := &CompactGrid{header: NewCompactHeader()}
cg.rebuildHeader()
return cg
}
func (cg *CompactGrid) Align() {
y := cg.Y
if cg.Offset >= len(cg.Rows) {
cg.Offset = 0
}
if cg.Offset < 0 {
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
cg.Offset = 0
}
// update row ypos, width recursively
colWidths := cg.calcWidths()
for _, r := range cg.pageRows() {
r.SetY(y)
y += r.GetHeight()
r.SetWidth(cg.Width)
r.SetWidths(cg.Width, colWidths)
}
}
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
func (cg *CompactGrid) Clear() {
cg.Rows = []RowBufferer{}
cg.rebuildHeader()
}
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height }
func (cg *CompactGrid) SetX(x int) { cg.X = x }
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y }
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - cg.header.Height - cg.Y }
func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) {
rows = append(rows, header)
// calculate and return per-column width
func (cg *CompactGrid) calcWidths() []int {
var autoCols int
width := cg.Width
colWidths := make([]int, len(cg.cols))
for n, w := range cg.cols {
colWidths[n] = w.FixedWidth()
width -= w.FixedWidth()
if w.FixedWidth() == 0 {
autoCols++
}
}
spacing := colSpacing * len(cg.cols)
autoWidth := (width - spacing) / autoCols
for n, val := range colWidths {
if val == 0 {
colWidths[n] = autoWidth
}
}
return colWidths
}
func (cg *CompactGrid) pageRows() (rows []RowBufferer) {
rows = append(rows, cg.header)
rows = append(rows, cg.Rows[cg.Offset:]...)
return rows
}
@@ -59,8 +86,14 @@ func (cg *CompactGrid) Buffer() ui.Buffer {
return buf
}
func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
for _, r := range rows {
cg.Rows = append(cg.Rows, r)
func (cg *CompactGrid) AddRows(rows ...RowBufferer) {
cg.Rows = append(cg.Rows, rows...)
}
func (cg *CompactGrid) rebuildHeader() {
cg.cols = newRowWidgets()
cg.header.clearFieldPars()
for _, col := range cg.cols {
cg.header.addFieldPar(col.Header())
}
}

View File

@@ -8,63 +8,59 @@ type CompactHeader struct {
X, Y int
Width int
Height int
cols []CompactCol
widths []int
pars []*ui.Par
}
func NewCompactHeader() *CompactHeader {
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
ch := &CompactHeader{}
ch.Height = 2
for _, f := range fields {
ch.addFieldPar(f)
return &CompactHeader{
X: rowPadding,
Height: 2,
}
return ch
}
func (ch *CompactHeader) GetHeight() int {
return ch.Height
func (row *CompactHeader) GetHeight() int {
return row.Height
}
func (ch *CompactHeader) SetWidth(w int) {
x := ch.X
autoWidth := calcWidth(w)
for n, col := range ch.pars {
// set column to static width
if colWidths[n] != 0 {
col.SetX(x)
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)
col.SetWidth(autoWidth)
x += autoWidth + colSpacing
func (row *CompactHeader) SetWidths(totalWidth int, widths []int) {
x := row.X
for n, w := range row.pars {
w.SetX(x)
w.SetWidth(widths[n])
x += widths[n] + colSpacing
}
ch.Width = w
row.Width = totalWidth
}
func (ch *CompactHeader) SetX(x int) {
ch.X = x
func (row *CompactHeader) SetX(x int) {
row.X = x
}
func (ch *CompactHeader) SetY(y int) {
for _, p := range ch.pars {
func (row *CompactHeader) SetY(y int) {
for _, p := range row.pars {
p.SetY(y)
}
ch.Y = y
row.Y = y
}
func (ch *CompactHeader) Buffer() ui.Buffer {
func (row *CompactHeader) Buffer() ui.Buffer {
buf := ui.NewBuffer()
for _, p := range ch.pars {
for _, p := range row.pars {
buf.Merge(p.Buffer())
}
return buf
}
func (ch *CompactHeader) addFieldPar(s string) {
p := ui.NewPar(s)
p.Height = ch.Height
p.Border = false
ch.pars = append(ch.pars, p)
func (row *CompactHeader) clearFieldPars() {
row.pars = []*ui.Par{}
}
func (row *CompactHeader) addFieldPar(s string) {
p := ui.NewPar(s)
p.Height = row.Height
p.Border = false
row.pars = append(row.pars, p)
}

View File

@@ -1,144 +0,0 @@
package compact
import (
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
var log = logging.Init()
type Compact struct {
Status *Status
Name *TextCol
Cid *TextCol
Cpu *GaugeCol
Mem *GaugeCol
Net *TextCol
IO *TextCol
Pids *TextCol
X, Y int
Width int
Height int
}
func NewCompact(id string) *Compact {
// truncate container id
if len(id) > 12 {
id = id[:12]
}
row := &Compact{
Status: NewStatus(),
Name: NewTextCol("-"),
Cid: NewTextCol(id),
Cpu: NewGaugeCol(),
Mem: NewGaugeCol(),
Net: NewTextCol("-"),
IO: NewTextCol("-"),
Pids: NewTextCol("-"),
X: 1,
Height: 1,
}
return row
}
//func (row *Compact) ToggleExpand() {
//if row.Height == 1 {
//row.Height = 4
//} else {
//row.Height = 1
//}
//}
func (row *Compact) SetMeta(k, v string) {
switch k {
case "name":
row.Name.Set(v)
case "state":
row.Status.Set(v)
case "health":
row.Status.SetHealth(v)
}
}
func (row *Compact) SetMetrics(m models.Metrics) {
row.SetCPU(m.CPUUtil)
row.SetNet(m.NetRx, m.NetTx)
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
row.SetPids(m.Pids)
}
// Set gauges, counters to default unread values
func (row *Compact) Reset() {
row.Cpu.Reset()
row.Mem.Reset()
row.Net.Reset()
row.IO.Reset()
row.Pids.Reset()
}
func (row *Compact) GetHeight() int {
return row.Height
}
func (row *Compact) SetX(x int) {
row.X = x
}
func (row *Compact) SetY(y int) {
if y == row.Y {
return
}
for _, col := range row.all() {
col.SetY(y)
}
row.Y = y
}
func (row *Compact) SetWidth(width int) {
if width == row.Width {
return
}
x := row.X
autoWidth := calcWidth(width)
for n, col := range row.all() {
if colWidths[n] != 0 {
col.SetX(x)
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)
col.SetWidth(autoWidth)
x += autoWidth + colSpacing
}
row.Width = width
}
func (row *Compact) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(row.Status.Buffer())
buf.Merge(row.Name.Buffer())
buf.Merge(row.Cid.Buffer())
buf.Merge(row.Cpu.Buffer())
buf.Merge(row.Mem.Buffer())
buf.Merge(row.Net.Buffer())
buf.Merge(row.IO.Buffer())
buf.Merge(row.Pids.Buffer())
return buf
}
func (row *Compact) all() []ui.GridBufferer {
return []ui.GridBufferer{
row.Status,
row.Name,
row.Cid,
row.Cpu,
row.Mem,
row.Net,
row.IO,
row.Pids,
}
}

129
cwidgets/compact/row.go Normal file
View File

@@ -0,0 +1,129 @@
package compact
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
const rowPadding = 1
var log = logging.Init()
type RowBufferer interface {
SetY(int)
SetWidths(int, []int)
GetHeight() int
Buffer() ui.Buffer
}
type CompactRow struct {
Bg *RowBg
Cols []CompactCol
X, Y int
Height int
widths []int // column widths
}
func NewCompactRow() *CompactRow {
row := &CompactRow{
Bg: NewRowBg(),
Cols: newRowWidgets(),
X: rowPadding,
Height: 1,
}
return row
}
func (row *CompactRow) SetMeta(m models.Meta) {
for _, w := range row.Cols {
w.SetMeta(m)
}
}
func (row *CompactRow) SetMetrics(m models.Metrics) {
for _, w := range row.Cols {
w.SetMetrics(m)
}
}
// Set gauges, counters, etc. to default unread values
func (row *CompactRow) Reset() {
for _, w := range row.Cols {
w.Reset()
}
}
func (row *CompactRow) GetHeight() int { return row.Height }
//func (row *CompactRow) SetX(x int) { row.X = x }
func (row *CompactRow) SetY(y int) {
if y == row.Y {
return
}
row.Bg.Y = y
for _, w := range row.Cols {
w.SetY(y)
}
row.Y = y
}
func (row *CompactRow) SetWidths(totalWidth int, widths []int) {
x := row.X
row.Bg.SetX(x)
row.Bg.SetWidth(totalWidth)
for n, w := range row.Cols {
w.SetX(x)
w.SetWidth(widths[n])
x += widths[n] + colSpacing
}
}
func (row *CompactRow) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(row.Bg.Buffer())
for _, w := range row.Cols {
buf.Merge(w.Buffer())
}
return buf
}
func (row *CompactRow) Highlight() {
row.Cols[1].Highlight()
if config.GetSwitchVal("fullRowCursor") {
for _, w := range row.Cols {
w.Highlight()
}
}
}
func (row *CompactRow) UnHighlight() {
row.Cols[1].UnHighlight()
if config.GetSwitchVal("fullRowCursor") {
for _, w := range row.Cols {
w.UnHighlight()
}
}
}
type RowBg struct {
*ui.Par
}
func NewRowBg() *RowBg {
bg := ui.NewPar("")
bg.Height = 1
bg.Border = false
bg.Bg = ui.ThemeAttr("par.text.bg")
return &RowBg{bg}
}
func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") }
func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }

View File

@@ -1,48 +0,0 @@
package compact
import (
"fmt"
"strconv"
"github.com/bcicen/ctop/cwidgets"
ui "github.com/gizak/termui"
)
func (row *Compact) SetNet(rx int64, tx int64) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(rx), cwidgets.ByteFormat(tx))
row.Net.Set(label)
}
func (row *Compact) SetIO(read int64, write int64) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(read), cwidgets.ByteFormat(write))
row.IO.Set(label)
}
func (row *Compact) SetPids(val int) {
label := fmt.Sprintf("%s", strconv.Itoa(val))
row.Pids.Set(label)
}
func (row *Compact) SetCPU(val int) {
row.Cpu.BarColor = colorScale(val)
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
if val < 5 {
val = 5
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
}
if val > 100 {
val = 100
}
row.Cpu.Percent = val
}
func (row *Compact) SetMem(val int64, limit int64, percent int) {
row.Mem.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
if percent < 5 {
percent = 5
row.Mem.BarColor = ui.ColorBlack
} else {
row.Mem.BarColor = ui.ThemeAttr("gauge.bar.bg")
}
row.Mem.Percent = percent
}

View File

@@ -1,12 +1,14 @@
package compact
import (
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
const (
mark = string('\u25C9')
healthMark = string('\u207A')
mark = "◉"
healthMark = "✚"
vBar = string('\u25AE') + string('\u25AE')
)
@@ -17,29 +19,46 @@ type Status struct {
health []ui.Cell
}
func NewStatus() *Status {
s := &Status{Block: ui.NewBlock()}
func NewStatus() CompactCol {
s := &Status{
Block: ui.NewBlock(),
health: []ui.Cell{{Ch: ' '}},
}
s.Height = 1
s.Border = false
s.Set("")
s.setState("")
return s
}
func (s *Status) Buffer() ui.Buffer {
buf := s.Block.Buffer()
x := 0
for _, c := range s.status {
for _, c := range s.health {
buf.Set(s.InnerX()+x, s.InnerY(), c)
x += c.Width()
}
for _, c := range s.health {
x += 1
for _, c := range s.status {
buf.Set(s.InnerX()+x, s.InnerY(), c)
x += c.Width()
}
return buf
}
func (s *Status) Set(val string) {
func (s *Status) SetMeta(m models.Meta) {
s.setState(m.Get("state"))
s.setHealth(m.Get("health"))
}
// Status implements CompactCol
func (s *Status) Reset() {}
func (s *Status) SetMetrics(models.Metrics) {}
func (s *Status) Highlight() {}
func (s *Status) UnHighlight() {}
func (s *Status) Header() string { return "" }
func (s *Status) FixedWidth() int { return 3 }
func (s *Status) setState(val string) {
// defaults
text := mark
color := ui.ColorDefault
@@ -53,31 +72,25 @@ func (s *Status) Set(val string) {
text = vBar
}
var cells []ui.Cell
for _, ch := range text {
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
}
s.status = cells
s.status = ui.TextCells(text, color, ui.ColorDefault)
}
func (s *Status) SetHealth(val string) {
if val == "" {
return
}
func (s *Status) setHealth(val string) {
color := ui.ColorDefault
mark := healthMark
switch val {
case "":
return
case "healthy":
color = ui.ThemeAttr("status.ok")
case "unhealthy":
color = ui.ThemeAttr("status.danger")
case "starting":
color = ui.ThemeAttr("status.warn")
default:
log.Warningf("unknown health state string: \"%v\"", val)
}
var cells []ui.Cell
for _, ch := range healthMark {
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
}
s.health = cells
s.health = ui.TextCells(mark, color, ui.ColorDefault)
}

View File

@@ -1,35 +1,113 @@
package compact
import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
type TextCol struct {
*ui.Par
type NameCol struct {
*TextCol
}
func NewTextCol(s string) *TextCol {
p := ui.NewPar(s)
func NewNameCol() CompactCol {
return &NameCol{NewTextCol("NAME")}
}
func (w *NameCol) SetMeta(m models.Meta) {
w.Text = m.Get("name")
// truncate container id
if len(w.Text) > 12 {
w.Text = w.Text[:12]
}
}
type CIDCol struct {
*TextCol
}
func NewCIDCol() CompactCol {
return &CIDCol{NewTextCol("CID")}
}
func (w *CIDCol) SetMeta(m models.Meta) {
w.Text = m.Get("id")
if len(w.Text) > 12 {
w.Text = w.Text[:12]
}
}
type NetCol struct {
*TextCol
}
func NewNetCol() CompactCol {
return &NetCol{NewTextCol("NET RX/TX")}
}
func (w *NetCol) SetMetrics(m models.Metrics) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.NetRx), cwidgets.ByteFormat64Short(m.NetTx))
w.Text = label
}
type IOCol struct {
*TextCol
}
func NewIOCol() CompactCol {
return &IOCol{NewTextCol("IO R/W")}
}
func (w *IOCol) SetMetrics(m models.Metrics) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.IOBytesRead), cwidgets.ByteFormat64Short(m.IOBytesWrite))
w.Text = label
}
type PIDCol struct {
*TextCol
}
func NewPIDCol() CompactCol {
w := &PIDCol{NewTextCol("PIDS")}
w.fWidth = 4
return w
}
func (w *PIDCol) SetMetrics(m models.Metrics) {
w.Text = fmt.Sprintf("%d", m.Pids)
}
type TextCol struct {
*ui.Par
header string
fWidth int
}
func NewTextCol(header string) *TextCol {
p := ui.NewPar("-")
p.Border = false
p.Height = 1
p.Width = 20
return &TextCol{p}
return &TextCol{p, header, 0}
}
func (w *TextCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg")
w.TextFgColor = ui.ThemeAttr("par.text.hi")
w.TextBgColor = ui.ThemeAttr("par.text.fg")
}
func (w *TextCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg")
w.TextFgColor = ui.ThemeAttr("par.text.fg")
w.TextBgColor = ui.ThemeAttr("par.text.bg")
}
func (w *TextCol) Reset() {
w.Text = "-"
}
func (w *TextCol) Set(s string) {
w.Text = s
}
func (w *TextCol) Reset() { w.Text = "-" }
func (w *TextCol) SetMeta(models.Meta) {}
func (w *TextCol) SetMetrics(models.Metrics) {}
func (w *TextCol) Header() string { return w.header }
func (w *TextCol) FixedWidth() int { return w.fWidth }

View File

@@ -4,36 +4,12 @@ package compact
import (
"fmt"
ui "github.com/gizak/termui"
)
const colSpacing = 1
// per-column width. 0 == auto width
var colWidths = []int{
3, // status
0, // name
0, // cid
0, // cpu
0, // memory
0, // net
0, // io
4, // pids
}
// Calculate per-column width, given total width
func calcWidth(width int) int {
spacing := colSpacing * len(colWidths)
var staticCols int
for _, w := range colWidths {
width -= w
if w == 0 {
staticCols += 1
}
}
return (width - spacing) / staticCols
}
func centerParText(p *ui.Par) {
var text string
var padding string

View File

@@ -8,6 +8,14 @@ import (
var log = logging.Init()
type WidgetUpdater interface {
SetMeta(string, string)
SetMeta(models.Meta)
SetMetrics(models.Metrics)
}
type NullWidgetUpdater struct{}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMeta(models.Meta) {}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}

36
cwidgets/single/env.go Normal file
View File

@@ -0,0 +1,36 @@
package single
import (
ui "github.com/gizak/termui"
"regexp"
)
var envPattern = regexp.MustCompile(`(?P<KEY>[^=]+)=(?P<VALUJE>.*)`)
type Env struct {
*ui.Table
data map[string]string
}
func NewEnv() *Env {
p := ui.NewTable()
p.Height = 4
p.Width = colWidth[0]
p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false
i := &Env{p, make(map[string]string)}
i.BorderLabel = "Env"
return i
}
func (w *Env) Set(k, v string) {
match := envPattern.FindStringSubmatch(v)
key := match[1]
value := match[2]
w.data[key] = value
w.Rows = [][]string{}
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
w.Height = len(w.Rows) + 2
}

View File

@@ -6,7 +6,7 @@ import (
ui "github.com/gizak/termui"
)
var displayInfo = []string{"id", "name", "image", "ports", "state", "created", "health"}
var displayInfo = []string{"id", "name", "image", "ports", "IPs", "state", "created", "health"}
type Info struct {
*ui.Table
@@ -45,7 +45,7 @@ func mkInfoRows(k, v string) (rows [][]string) {
// initial row with field name
rows = append(rows, []string{k, lines[0]})
// append any additional lines in seperate row
// append any additional lines in separate row
if len(lines) > 1 {
for _, line := range lines[1:] {
if line != "" {

View File

@@ -42,10 +42,10 @@ func (w *IO) Update(read int64, write int64) {
var rate string
w.readHist.Append(int(read))
rate = strings.ToLower(cwidgets.ByteFormatInt(w.readHist.Val))
rate = strings.ToLower(cwidgets.ByteFormatShort(w.readHist.Val))
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
w.writeHist.Append(int(write))
rate = strings.ToLower(cwidgets.ByteFormatInt(w.writeHist.Val))
rate = strings.ToLower(cwidgets.ByteFormatShort(w.writeHist.Val))
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
}

View File

@@ -18,6 +18,7 @@ type Single struct {
Cpu *Cpu
Mem *Mem
IO *IO
Env *Env
X, Y int
Width int
}
@@ -32,6 +33,7 @@ func NewSingle(id string) *Single {
Cpu: NewCpu(),
Mem: NewMem(),
IO: NewIO(),
Env: NewEnv(),
Width: ui.TermWidth(),
}
}
@@ -52,8 +54,16 @@ func (e *Single) Down() {
}
}
func (e *Single) SetWidth(w int) { e.Width = w }
func (e *Single) SetMeta(k, v string) { e.Info.Set(k, v) }
func (e *Single) SetWidth(w int) { e.Width = w }
func (e *Single) SetMeta(m models.Meta) {
for k, v := range m {
if k == "[ENV-VAR]" {
e.Env.Set(k, v)
} else {
e.Info.Set(k, v)
}
}
}
func (e *Single) SetMetrics(m models.Metrics) {
e.Cpu.Update(m.CPUUtil)
@@ -62,13 +72,14 @@ func (e *Single) SetMetrics(m models.Metrics) {
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
}
// Return total column height
// GetHeight returns total column height
func (e *Single) GetHeight() (h int) {
h += e.Info.Height
h += e.Net.Height
h += e.Cpu.Height
h += e.Mem.Height
h += e.IO.Height
h += e.Env.Height
return h
}
@@ -103,6 +114,7 @@ func (e *Single) Buffer() ui.Buffer {
buf.Merge(e.Mem.Buffer())
buf.Merge(e.Net.Buffer())
buf.Merge(e.IO.Buffer())
buf.Merge(e.Env.Buffer())
return buf
}
@@ -113,6 +125,7 @@ func (e *Single) all() []ui.GridBufferer {
e.Mem,
e.Net,
e.IO,
e.Env,
}
}

View File

@@ -70,7 +70,7 @@ func newMemChart() *ui.MBarChart {
mbar.BarColor[1] = ui.ColorBlack
mbar.NumColor[1] = ui.ColorBlack
mbar.NumFmt = cwidgets.ByteFormatInt
mbar.NumFmt = cwidgets.ByteFormatShort
//mbar.ShowScale = true
return mbar
}
@@ -78,6 +78,6 @@ func newMemChart() *ui.MBarChart {
func (w *Mem) Update(val int, limit int) {
w.valHist.Append(val)
w.limitHist.Append(limit - val)
w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatInt(val), cwidgets.ByteFormatInt(limit))
w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatShort(val), cwidgets.ByteFormatShort(limit))
//w.Data[0] = w.hist.data
}

View File

@@ -42,10 +42,10 @@ func (w *Net) Update(rx int64, tx int64) {
var rate string
w.rxHist.Append(int(rx))
rate = strings.ToLower(cwidgets.ByteFormatInt(w.rxHist.Val))
rate = strings.ToLower(cwidgets.ByteFormat(w.rxHist.Val))
w.Lines[0].Title = fmt.Sprintf("RX [%s/s]", rate)
w.txHist.Append(int(tx))
rate = strings.ToLower(cwidgets.ByteFormatInt(w.txHist.Val))
rate = strings.ToLower(cwidgets.ByteFormat(w.txHist.Val))
w.Lines[1].Title = fmt.Sprintf("TX [%s/s]", rate)
}

View File

@@ -1,53 +1,74 @@
package cwidgets
import (
"fmt"
"strconv"
)
const (
kb = 1024
mb = kb * 1024
gb = mb * 1024
tb = gb * 1024
// byte ratio constants
_ = iota
kib float64 = 1 << (10 * iota)
mib
gib
tib
pib
)
// convenience method
func ByteFormatInt(n int) string {
return ByteFormat(int64(n))
var (
units = []float64{
1,
kib,
mib,
gib,
tib,
pib,
}
// short, full unit labels
labels = [][2]string{
[2]string{"B", "B"},
[2]string{"K", "KiB"},
[2]string{"M", "MiB"},
[2]string{"G", "GiB"},
[2]string{"T", "TiB"},
[2]string{"P", "PiB"},
}
)
// convenience methods
func ByteFormat(n int) string { return byteFormat(float64(n), false) }
func ByteFormatShort(n int) string { return byteFormat(float64(n), true) }
func ByteFormat64(n int64) string { return byteFormat(float64(n), false) }
func ByteFormat64Short(n int64) string { return byteFormat(float64(n), true) }
func byteFormat(n float64, short bool) string {
i := len(units) - 1
for i > 0 {
if n >= units[i] {
n /= units[i]
break
}
i--
}
if short {
return unpadFloat(n, 0) + labels[i][0]
}
return unpadFloat(n, 2) + labels[i][1]
}
func ByteFormat(n int64) string {
if n < kb {
return fmt.Sprintf("%sB", strconv.FormatInt(n, 10))
}
if n < mb {
n = n / kb
return fmt.Sprintf("%sK", strconv.FormatInt(n, 10))
}
if n < gb {
n = n / mb
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
}
if n < tb {
nf := float64(n) / gb
return fmt.Sprintf("%sG", unpadFloat(nf))
}
nf := float64(n) / tb
return fmt.Sprintf("%sT", unpadFloat(nf))
func unpadFloat(f float64, maxp int) string {
return strconv.FormatFloat(f, 'f', getPrecision(f, maxp), 64)
}
func unpadFloat(f float64) string {
return strconv.FormatFloat(f, 'f', getPrecision(f), 64)
}
func getPrecision(f float64) int {
func getPrecision(f float64, maxp int) int {
frac := int((f - float64(int(f))) * 100)
if frac == 0 {
if frac == 0 || maxp == 0 {
return 0
}
if frac%10 == 0 {
if frac%10 == 0 || maxp < 2 {
return 1
}
return 2 // default precision
return maxp
}

21
go.mod Normal file
View File

@@ -0,0 +1,21 @@
module github.com/bcicen/ctop
require (
github.com/BurntSushi/toml v0.3.1
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd
github.com/fsouza/go-dockerclient v1.6.6
github.com/gizak/termui v2.3.0+incompatible
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
github.com/opencontainers/runc v1.0.0-rc92
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.4.0
)
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5
go 1.15

280
go.sum Normal file
View File

@@ -0,0 +1,280 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873 h1:93nQ7k53GjoMQ07HVP8g6Zj1fQZDDj7Xy2VkNNtvX8o=
github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 h1:2pI3ZsoefWIi++8EqmANoC7Px/v2lRwnleVUcCuFgLg=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5/go.mod h1:yIA9ITWZD1p4/DvCQ44xvhyVb9XEUlVnY1rzGSHwbiM=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd h1:xqaBnULC8wEnQpRDXAsDgXkU/STqoluz1REOoegSfNU=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775 h1:cHzBGGVew0ezFsq2grfy2RsB8hO/eNyBgOLHBCqfR1U=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ=
github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb h1:nXPkFq8X1a9ycY3GYQpFNxHh3j2JgY7zDZfq2EXMIzk=
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.1.0 h1:kq/SbG2BCKLkDKkjQf5OWwKWUKj1lgs3lFI4PxnR5lg=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200505174321-1655290016ac+incompatible h1:ZxJX4ZSNg1LORBsStUojbrLfkrE3Ut122XhzyZnN110=
github.com/docker/docker v17.12.0-ce-rc1.0.20200505174321-1655290016ac+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/go-dockerclient v1.6.6 h1:9e3xkBrVkPb81gzYq23i7iDUEd6sx2ooeJA/gnYU6R4=
github.com/fsouza/go-dockerclient v1.6.6/go.mod h1:3/oRIWoe7uT6bwtAayj/EmJmepBjeL4pYvt7ZxC7Rnk=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c h1:/hc+TxW4Q1v6aqNPHE5jiaNF2xEK0CzWTgo25RQhQ+U=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.mod h1:FJRkXmPrkHw0WDjB/LXMUhjWJ112Y6JUYnIVBOy8oH8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/moby/sys/mount v0.1.0 h1:Ytx78EatgFKtrqZ0BvJ0UtJE472ZvawVmil6pIfuCCU=
github.com/moby/sys/mount v0.1.0/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74=
github.com/moby/sys/mountinfo v0.1.0/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
github.com/moby/sys/mountinfo v0.1.3 h1:KIrhRO14+AkwKvG/g2yIpNMOUVZ02xNhOw8KY1WsLOI=
github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
github.com/moby/term v0.0.0-20200429084858-129dac9f73f6 h1:3Y9aosU6S5Bo8GYH0s+t1ej4m30GuUKvQ3c9ZLqdL28=
github.com/moby/term v0.0.0-20200429084858-129dac9f73f6/go.mod h1:or9wGItza1sRcM4Wd3dIv8DsFHYQuFsMHEdxUIlUxms=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976 h1:aZQToFSLH8ejFeSkTc3r3L4dPImcj7Ib/KgmkQqbGGg=
github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884 h1:fcs71SMqqDhUD+PbpIv9xf3EH9F9s6HfiLwr6jKm1VA=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc92 h1:+IczUKCRzDzFDnw99O/PAqrcBBCoRp9xN3cB1SYSNS4=
github.com/opencontainers/runc v1.0.0-rc92/go.mod h1:X1zlU4p7wOlX4+WRCz+hvlRv8phdL7UqbYD+vQwNMmE=
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6 h1:NhsM2gc769rVWDqJvapK37r+7+CBXI8xHhnfnt8uQsg=
github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.6.0 h1:+bIAS/Za3q5FTwWym4fTB0vObnfCf3G/NC7K6Jx62mY=
github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243 h1:R43TdZy32XXSXjJn7M/HhALJ9imq6ztLnChfYJpVDnM=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

78
grid.go
View File

@@ -6,6 +6,44 @@ import (
ui "github.com/gizak/termui"
)
func ShowConnError(err error) (exit bool) {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
setErr := func(err error) {
errView.Append(err.Error())
errView.Append("attempting to reconnect...")
ui.Render(errView)
}
HandleKeys("exit", func() {
exit = true
ui.StopLoop()
})
ui.Handle("/timer/1s", func(ui.Event) {
_, err := cursor.RefreshContainers()
if err == nil {
ui.StopLoop()
return
}
setErr(err)
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
errView.Resize()
ui.Clear()
ui.Render(errView)
log.Infof("RESIZE")
})
errView.Resize()
setErr(err)
ui.Loop()
return exit
}
func RedrawRows(clr bool) {
// reinit body rows
cGrid.Clear()
@@ -33,7 +71,6 @@ func RedrawRows(clr bool) {
}
cGrid.Align()
ui.Render(cGrid)
}
func SingleView() MenuFn {
@@ -68,16 +105,21 @@ func SingleView() MenuFn {
return nil
}
func RefreshDisplay() {
func RefreshDisplay() error {
// skip display refresh during scroll
if !cursor.isScrolling {
needsClear := cursor.RefreshContainers()
needsClear, err := cursor.RefreshContainers()
if err != nil {
return err
}
RedrawRows(needsClear)
}
return nil
}
func Display() bool {
var menu MenuFn
var connErr error
cGrid.SetWidth(ui.TermWidth())
ui.DefaultEvtStream.Hook(logEvent)
@@ -104,17 +146,32 @@ func Display() bool {
menu = ContainerMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<left>", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/<right>", func(ui.Event) {
menu = SingleView
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
menu = LogMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/e", func(ui.Event) {
menu = ExecShell
ui.StopLoop()
})
ui.Handle("/sys/kbd/o", func(ui.Event) {
menu = SingleView
ui.StopLoop()
})
ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers")
RefreshDisplay()
connErr = RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
})
ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected())
@@ -134,6 +191,10 @@ func Display() bool {
menu = SortMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/c", func(ui.Event) {
menu = ColumnsMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/S", func(ui.Event) {
path, err := config.Write()
if err == nil {
@@ -148,7 +209,10 @@ func Display() bool {
if log.StatusQueued() {
ui.StopLoop()
}
RefreshDisplay()
connErr = RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
@@ -162,6 +226,10 @@ func Display() bool {
ui.Loop()
if connErr != nil {
return ShowConnError(connErr)
}
if log.StatusQueued() {
for sm := range log.FlushStatus() {
if sm.IsError {

View File

@@ -5,6 +5,10 @@ KERNEL=$(uname -s)
function output() { echo -e "\033[32mctop-install\033[0m $@"; }
function command_exists() {
command -v "$@" > /dev/null 2>&1
}
# extract github download url matching pattern
function extract_url() {
match=$1; shift
@@ -28,6 +32,26 @@ case $KERNEL in
;;
esac
for req in curl wget; do
command_exists $req || {
output "missing required $req binary"
req_failed=1
}
done
[ "$req_failed" == 1 ] && exit 1
sh_c='sh -c'
if [ "$CURRENT_USER" != 'root' ]; then
if command_exists sudo; then
sh_c='sudo -E sh -c'
elif command_exists su; then
sh_c='su -c'
else
output "Error: this installer needs the ability to run commands as root. We are unable to find either "sudo" or "su" available to make this happen."
exit 1
fi
fi
TMP=$(mktemp -d "${TMPDIR:-/tmp}/ctop.XXXXX")
cd ${TMP}
@@ -55,6 +79,6 @@ wget -q --show-progress $url
output "installing to /usr/local/bin"
chmod +x ctop-*
sudo mv ctop-* /usr/local/bin/ctop
$sh_c "mv ctop-* /usr/local/bin/ctop"
output "done!"

View File

@@ -2,6 +2,7 @@ package logging
import (
"fmt"
"io"
"net"
"sync"
)
@@ -56,13 +57,13 @@ func StopServer() {
}
}
func handler(conn net.Conn) {
func handler(wc io.WriteCloser) {
server.wg.Add(1)
defer server.wg.Done()
defer conn.Close()
defer wc.Close()
for msg := range Log.tail() {
msg = fmt.Sprintf("%s\n", msg)
conn.Write([]byte(msg))
wc.Write([]byte(msg))
}
conn.Write([]byte("bye\n"))
wc.Write([]byte("bye\n"))
}

32
main.go
View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"runtime"
"strings"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector"
@@ -21,11 +22,12 @@ var (
version = "dev-build"
goVersion = runtime.Version()
log *logging.CTopLogger
cursor *GridCursor
cGrid *compact.CompactGrid
header *widgets.CTopHeader
status *widgets.StatusLine
log *logging.CTopLogger
cursor *GridCursor
cGrid *compact.CompactGrid
header *widgets.CTopHeader
status *widgets.StatusLine
errView *widgets.ErrorView
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
)
@@ -44,6 +46,7 @@ func main() {
invertFlag = flag.Bool("i", false, "invert default colors")
scaleCpu = flag.Bool("scale-cpu", false, "show cpu as % of system total")
connectorFlag = flag.String("connector", "docker", "container connector to use")
defaultShell = flag.String("shell", "sh", "exec shell to use")
)
flag.Parse()
@@ -62,7 +65,9 @@ func main() {
// init global config and read config file if exists
config.Init()
config.Read()
if err := config.Read(); err != nil {
log.Warningf("reading config: %s", err)
}
// override default config values with command line flags
if *filterFlag != "" {
@@ -86,6 +91,10 @@ func main() {
config.Toggle("scaleCpu")
}
if *defaultShell != "" {
config.Update("shell", *defaultShell)
}
// init ui
if *invertFlag {
InvertColorMap()
@@ -94,17 +103,19 @@ func main() {
if err := ui.Init(); err != nil {
panic(err)
}
tm.SetInputMode(tm.InputAlt)
defer Shutdown()
// init grid, cursor, header
conn, err := connector.ByName(*connectorFlag)
cSuper, err := connector.ByName(*connectorFlag)
if err != nil {
panic(err)
}
cursor = &GridCursor{cSource: conn}
cursor = &GridCursor{cSuper: cSuper}
cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader()
status = widgets.NewStatusLine()
errView = widgets.NewErrorView()
for {
exit := Display()
@@ -133,12 +144,13 @@ func validSort(s string) {
func panicExit() {
if r := recover(); r != nil {
Shutdown()
panic(r)
fmt.Printf("error: %s\n", r)
os.Exit(1)
}
}
var helpMsg = `ctop - container metric viewer
var helpMsg = `ctop - interactive container viewer
usage: ctop [options]
@@ -148,4 +160,6 @@ options:
func printHelp() {
fmt.Println(helpMsg)
flag.PrintDefaults()
fmt.Printf("\navailable connectors: ")
fmt.Println(strings.Join(connector.Enabled(), ", "))
}

220
menus.go
View File

@@ -2,6 +2,7 @@ package main
import (
"fmt"
"strings"
"time"
"github.com/bcicen/ctop/config"
@@ -25,6 +26,8 @@ var helpDialog = []menu.Item{
{"[r] - reverse container sort order", ""},
{"[o] - open single view", ""},
{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
{"[e] - exec shell", ""},
{"[c] - configure columns", ""},
{"[S] - save current configuration to file", ""},
{"[q] - exit ctop", ""},
}
@@ -37,7 +40,10 @@ func HelpMenu() MenuFn {
m := menu.NewMenu()
m.BorderLabel = "Help"
m.AddItems(helpDialog...)
ui.Render(m)
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Clear()
ui.Render(m)
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
@@ -100,7 +106,90 @@ func SortMenu() MenuFn {
HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("sortField", m.SelectedItem().Val)
config.Update("sortField", m.SelectedValue())
ui.StopLoop()
})
ui.Render(m)
ui.Loop()
return nil
}
func ColumnsMenu() MenuFn {
const (
enabledStr = "[X]"
disabledStr = "[ ]"
padding = 2
)
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.SortItems = false
m.BorderLabel = "Columns"
//m.SubText = "Enabled Columns"
rebuild := func() {
// get padding for right alignment of enabled status
var maxLen int
for _, col := range config.GlobalColumns {
if len(col.Label) > maxLen {
maxLen = len(col.Label)
}
}
maxLen += padding
// rebuild menu items
m.ClearItems()
for _, col := range config.GlobalColumns {
txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label))
if col.Enabled {
txt += enabledStr
} else {
txt += disabledStr
}
m.AddItems(menu.Item{col.Name, txt})
}
}
upFn := func() {
config.ColumnLeft(m.SelectedValue())
m.Up()
rebuild()
}
downFn := func() {
config.ColumnRight(m.SelectedValue())
m.Down()
rebuild()
}
toggleFn := func() {
config.ColumnToggle(m.SelectedValue())
rebuild()
}
rebuild()
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("enter", toggleFn)
HandleKeys("pgup", upFn)
HandleKeys("pgdown", downFn)
ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { toggleFn() })
HandleKeys("exit", func() {
cSource, err := cursor.cSuper.Get()
if err == nil {
for _, c := range cSource.All() {
c.RecreateWidgets()
}
}
ui.StopLoop()
})
@@ -123,44 +212,111 @@ func ContainerMenu() MenuFn {
m.BorderLabel = "Menu"
items := []menu.Item{
menu.Item{Val: "single", Label: "single view"},
menu.Item{Val: "logs", Label: "log view"},
menu.Item{Val: "single", Label: "[o] single view"},
menu.Item{Val: "logs", Label: "[l] log view"},
}
if c.Meta["state"] == "running" {
items = append(items, menu.Item{Val: "stop", Label: "stop"})
items = append(items, menu.Item{Val: "stop", Label: "[s] stop"})
items = append(items, menu.Item{Val: "pause", Label: "[p] pause"})
items = append(items, menu.Item{Val: "restart", Label: "[r] restart"})
items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"})
}
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
items = append(items, menu.Item{Val: "start", Label: "start"})
items = append(items, menu.Item{Val: "remove", Label: "remove"})
items = append(items, menu.Item{Val: "start", Label: "[s] start"})
items = append(items, menu.Item{Val: "remove", Label: "[R] remove"})
}
items = append(items, menu.Item{Val: "cancel", Label: "cancel"})
if c.Meta["state"] == "paused" {
items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"})
}
items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"})
m.AddItems(items...)
ui.Render(m)
var nextMenu MenuFn
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
var selected string
// shortcuts
ui.Handle("/sys/kbd/o", func(ui.Event) {
selected = "single"
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
selected = "logs"
ui.StopLoop()
})
if c.Meta["state"] != "paused" {
ui.Handle("/sys/kbd/s", func(ui.Event) {
if c.Meta["state"] == "running" {
selected = "stop"
} else {
selected = "start"
}
ui.StopLoop()
})
}
if c.Meta["state"] != "exited" || c.Meta["state"] != "created" {
ui.Handle("/sys/kbd/p", func(ui.Event) {
if c.Meta["state"] == "paused" {
selected = "unpause"
} else {
selected = "pause"
}
ui.StopLoop()
})
}
if c.Meta["state"] == "running" {
ui.Handle("/sys/kbd/e", func(ui.Event) {
selected = "exec"
ui.StopLoop()
})
ui.Handle("/sys/kbd/r", func(ui.Event) {
selected = "restart"
ui.StopLoop()
})
}
ui.Handle("/sys/kbd/R", func(ui.Event) {
selected = "remove"
ui.StopLoop()
})
ui.Handle("/sys/kbd/c", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
switch m.SelectedItem().Val {
case "single":
nextMenu = SingleView
case "logs":
nextMenu = LogMenu
case "start":
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
case "stop":
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
case "remove":
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
}
selected = m.SelectedValue()
ui.StopLoop()
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
var nextMenu MenuFn
switch selected {
case "single":
nextMenu = SingleView
case "logs":
nextMenu = LogMenu
case "exec":
nextMenu = ExecShell
case "start":
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
case "stop":
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
case "remove":
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
case "pause":
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
case "unpause":
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
return nextMenu
}
@@ -176,7 +332,7 @@ func LogMenu() MenuFn {
logs, quit := logReader(c)
m := widgets.NewTextView(logs)
m.BorderLabel = "Logs"
m.BorderLabel = fmt.Sprintf("Logs [%s]", c.GetMeta("name"))
ui.Render(m)
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
@@ -193,6 +349,24 @@ func LogMenu() MenuFn {
return nil
}
func ExecShell() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
shell := config.Get("shell")
if err := c.Exec([]string{shell.Val, "-c", "printf '\\e[0m\\e[?25h' && clear && " + shell.Val}); err != nil {
log.Fatal(err)
}
return nil
}
// Create a confirmation dialog with a given description string and
// func to perform if confirmed
func Confirm(txt string, fn func()) MenuFn {
@@ -232,7 +406,7 @@ func Confirm(txt string, fn func()) MenuFn {
ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
switch m.SelectedItem().Val {
switch m.SelectedValue() {
case "cancel":
no()
case "yes":

View File

@@ -7,6 +7,29 @@ type Log struct {
Message string
}
type Meta map[string]string
// NewMeta returns an initialized Meta map.
// An optional series of key, values may be provided to populate the map prior to returning
func NewMeta(kvs ...string) Meta {
m := make(Meta)
var i int
for i < len(kvs)-1 {
m[kvs[i]] = kvs[i+1]
i += 2
}
return m
}
func (m Meta) Get(k string) string {
if s, ok := m[k]; ok {
return s
}
return ""
}
type Metrics struct {
CPUUtil int
NetTx int64

60
widgets/error.go Normal file
View File

@@ -0,0 +1,60 @@
package widgets
import (
"fmt"
"strings"
"time"
ui "github.com/gizak/termui"
)
type ErrorView struct {
*ui.Par
lines []string
}
func NewErrorView() *ErrorView {
const yPad = 1
const xPad = 2
p := ui.NewPar("")
p.X = xPad
p.Y = yPad
p.Border = true
p.Height = 10
p.Width = 20
p.PaddingTop = yPad
p.PaddingBottom = yPad
p.PaddingLeft = xPad
p.PaddingRight = xPad
p.BorderLabel = " ctop - error "
p.Bg = ui.ThemeAttr("bg")
p.TextFgColor = ui.ThemeAttr("status.warn")
p.TextBgColor = ui.ThemeAttr("menu.text.bg")
p.BorderFg = ui.ThemeAttr("status.warn")
p.BorderLabelFg = ui.ThemeAttr("status.warn")
return &ErrorView{p, make([]string, 0, 50)}
}
func (w *ErrorView) Append(s string) {
if len(w.lines)+2 >= cap(w.lines) {
w.lines = append(w.lines[:0], w.lines[2:]...)
}
ts := time.Now().Local().Format("15:04:05 MST")
w.lines = append(w.lines, fmt.Sprintf("[%s] %s", ts, s))
w.lines = append(w.lines, "")
}
func (w *ErrorView) Buffer() ui.Buffer {
offset := len(w.lines) - w.InnerHeight()
if offset < 0 {
offset = 0
}
w.Text = strings.Join(w.lines[offset:len(w.lines)], "\n")
return w.Par.Buffer()
}
func (w *ErrorView) Resize() {
w.Height = ui.TermHeight() - (w.PaddingTop + w.PaddingBottom)
w.SetWidth(ui.TermWidth() - (w.PaddingLeft + w.PaddingRight))
}

View File

@@ -16,7 +16,7 @@ type CTopHeader struct {
func NewCTopHeader() *CTopHeader {
return &CTopHeader{
Time: headerPar(2, timeStr()),
Time: headerPar(2, ""),
Count: headerPar(24, "-"),
Filter: headerPar(40, ""),
bg: headerBg(),

View File

@@ -77,7 +77,7 @@ func (i *Input) KeyPress(e ui.Event) {
if len(i.Data) >= i.MaxLen {
return
}
if strings.Index(input_chars, ch) > -1 {
if strings.Contains(input_chars, ch) {
i.Data += ch
i.stream <- i.Data
ui.Render(i)

View File

@@ -11,13 +11,14 @@ type Padding [2]int // x,y padding
type Menu struct {
ui.Block
SortItems bool // enable automatic sorting of menu items
Selectable bool // whether menu is navigable
SubText string // optional text to display before items
TextFgColor ui.Attribute
TextBgColor ui.Attribute
Selectable bool
cursorPos int
items Items
padding Padding
toolTip *ToolTip
}
func NewMenu() *Menu {
@@ -42,7 +43,7 @@ func (m *Menu) AddItems(items ...Item) {
m.refresh()
}
// Remove menu item by value or label
// DelItem removes menu item by value or label
func (m *Menu) DelItem(s string) (success bool) {
for n, i := range m.items {
if i.Val == s || i.Label == s {
@@ -55,6 +56,11 @@ func (m *Menu) DelItem(s string) (success bool) {
return success
}
// ClearItems removes all current menu items
func (m *Menu) ClearItems() {
m.items = m.items[:0]
}
// Move cursor to an position by Item value or label
func (m *Menu) SetCursor(s string) (success bool) {
for n, i := range m.items {
@@ -66,19 +72,19 @@ func (m *Menu) SetCursor(s string) (success bool) {
return false
}
// Sort menu items(if enabled) and re-calculate window size
func (m *Menu) refresh() {
if m.SortItems {
sort.Sort(m.items)
}
m.calcSize()
ui.Render(m)
// SetToolTip sets an optional tooltip string to show at bottom of screen
func (m *Menu) SetToolTip(lines ...string) {
m.toolTip = NewToolTip(lines...)
}
func (m *Menu) SelectedItem() Item {
return m.items[m.cursorPos]
}
func (m *Menu) SelectedValue() string {
return m.items[m.cursorPos].Val
}
func (m *Menu) Buffer() ui.Buffer {
var cell ui.Cell
buf := m.Block.Buffer()
@@ -108,6 +114,10 @@ func (m *Menu) Buffer() ui.Buffer {
}
}
if m.toolTip != nil {
buf.Merge(m.toolTip.Buffer())
}
return buf
}
@@ -125,6 +135,15 @@ func (m *Menu) Down() {
}
}
// Sort menu items(if enabled) and re-calculate window size
func (m *Menu) refresh() {
if m.SortItems {
sort.Sort(m.items)
}
m.calcSize()
ui.Render(m)
}
// Set width and height based on menu items
func (m *Menu) calcSize() {
m.Width = 7 // minimum width

55
widgets/menu/tooltip.go Normal file
View File

@@ -0,0 +1,55 @@
package menu
import (
ui "github.com/gizak/termui"
)
type ToolTip struct {
ui.Block
Lines []string
TextFgColor ui.Attribute
TextBgColor ui.Attribute
padding Padding
}
func NewToolTip(lines ...string) *ToolTip {
t := &ToolTip{
Block: *ui.NewBlock(),
Lines: lines,
TextFgColor: ui.ThemeAttr("menu.text.fg"),
TextBgColor: ui.ThemeAttr("menu.text.bg"),
padding: Padding{2, 1},
}
t.BorderFg = ui.ThemeAttr("menu.border.fg")
t.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
t.X = 1
t.Align()
return t
}
func (t *ToolTip) Buffer() ui.Buffer {
var cell ui.Cell
buf := t.Block.Buffer()
y := t.Y + t.padding[1]
for n, line := range t.Lines {
x := t.X + t.padding[0]
for _, ch := range line {
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
buf.Set(x, y+n, cell)
x++
}
}
return buf
}
// Set width and height based on screen size
func (t *ToolTip) Align() {
t.Width = ui.TermWidth() - (t.padding[0] * 2)
t.Height = len(t.Lines) + (t.padding[1] * 2)
t.Y = ui.TermHeight() - t.Height
t.Block.Align()
}